feat: Switching models logic for ChatForm or when regenerating messges + modality detection logic
This commit is contained in:
parent
4c24ead8e0
commit
b9a3129d42
|
|
@ -9,8 +9,14 @@
|
||||||
} from '$lib/components/app';
|
} from '$lib/components/app';
|
||||||
import { INPUT_CLASSES } from '$lib/constants/input-classes';
|
import { INPUT_CLASSES } from '$lib/constants/input-classes';
|
||||||
import { config } from '$lib/stores/settings.svelte';
|
import { config } from '$lib/stores/settings.svelte';
|
||||||
import { selectedModelId } from '$lib/stores/models.svelte';
|
import { modelOptions, selectedModelId } from '$lib/stores/models.svelte';
|
||||||
import { isRouterMode } from '$lib/stores/props.svelte';
|
import {
|
||||||
|
isRouterMode,
|
||||||
|
supportsAudio,
|
||||||
|
supportsVision,
|
||||||
|
fetchModelProps,
|
||||||
|
getModelProps
|
||||||
|
} from '$lib/stores/props.svelte';
|
||||||
import { getConversationModel } from '$lib/stores/chat.svelte';
|
import { getConversationModel } from '$lib/stores/chat.svelte';
|
||||||
import { activeMessages } from '$lib/stores/conversations.svelte';
|
import { activeMessages } from '$lib/stores/conversations.svelte';
|
||||||
import {
|
import {
|
||||||
|
|
@ -74,6 +80,68 @@
|
||||||
let isRouter = $derived(isRouterMode());
|
let isRouter = $derived(isRouterMode());
|
||||||
let hasModelSelected = $derived(!isRouter || !!conversationModel || !!selectedModelId());
|
let hasModelSelected = $derived(!isRouter || !!conversationModel || !!selectedModelId());
|
||||||
|
|
||||||
|
// Get active model ID for capability detection
|
||||||
|
let activeModelId = $derived.by(() => {
|
||||||
|
if (!isRouter) return null;
|
||||||
|
|
||||||
|
const options = modelOptions();
|
||||||
|
|
||||||
|
// First try user-selected model
|
||||||
|
const selectedId = selectedModelId();
|
||||||
|
if (selectedId) {
|
||||||
|
const model = options.find((m) => m.id === selectedId);
|
||||||
|
if (model) return model.model;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to conversation model
|
||||||
|
if (conversationModel) {
|
||||||
|
const model = options.find((m) => m.model === conversationModel);
|
||||||
|
if (model) return model.model;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// State for model props reactivity
|
||||||
|
let modelPropsVersion = $state(0);
|
||||||
|
|
||||||
|
// Fetch model props when active model changes
|
||||||
|
$effect(() => {
|
||||||
|
if (isRouter && activeModelId) {
|
||||||
|
const cached = getModelProps(activeModelId);
|
||||||
|
if (!cached) {
|
||||||
|
fetchModelProps(activeModelId).then(() => {
|
||||||
|
modelPropsVersion++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Derive modalities from model props (ROUTER) or server props (MODEL)
|
||||||
|
let hasAudioModality = $derived.by(() => {
|
||||||
|
if (!isRouter) return supportsAudio();
|
||||||
|
|
||||||
|
if (activeModelId) {
|
||||||
|
void modelPropsVersion;
|
||||||
|
const props = getModelProps(activeModelId);
|
||||||
|
if (props) return props.modalities?.audio ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
let hasVisionModality = $derived.by(() => {
|
||||||
|
if (!isRouter) return supportsVision();
|
||||||
|
|
||||||
|
if (activeModelId) {
|
||||||
|
void modelPropsVersion;
|
||||||
|
const props = getModelProps(activeModelId);
|
||||||
|
if (props) return props.modalities?.vision ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
function checkModelSelected(): boolean {
|
function checkModelSelected(): boolean {
|
||||||
if (!hasModelSelected) {
|
if (!hasModelSelected) {
|
||||||
// Open the model selector
|
// Open the model selector
|
||||||
|
|
@ -251,6 +319,8 @@
|
||||||
<ChatFormFileInputInvisible
|
<ChatFormFileInputInvisible
|
||||||
bind:this={fileInputRef}
|
bind:this={fileInputRef}
|
||||||
bind:accept={fileAcceptString}
|
bind:accept={fileAcceptString}
|
||||||
|
{hasAudioModality}
|
||||||
|
{hasVisionModality}
|
||||||
onFileSelect={handleFileSelect}
|
onFileSelect={handleFileSelect}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,18 +5,25 @@
|
||||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||||
import { TOOLTIP_DELAY_DURATION } from '$lib/constants/tooltip-config';
|
import { TOOLTIP_DELAY_DURATION } from '$lib/constants/tooltip-config';
|
||||||
import { FileTypeCategory } from '$lib/enums';
|
import { FileTypeCategory } from '$lib/enums';
|
||||||
import { supportsAudio, supportsVision } from '$lib/stores/props.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
class?: string;
|
class?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
hasAudioModality?: boolean;
|
||||||
|
hasVisionModality?: boolean;
|
||||||
onFileUpload?: (fileType?: FileTypeCategory) => void;
|
onFileUpload?: (fileType?: FileTypeCategory) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { class: className = '', disabled = false, onFileUpload }: Props = $props();
|
let {
|
||||||
|
class: className = '',
|
||||||
|
disabled = false,
|
||||||
|
hasAudioModality = false,
|
||||||
|
hasVisionModality = false,
|
||||||
|
onFileUpload
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
const fileUploadTooltipText = $derived.by(() => {
|
const fileUploadTooltipText = $derived.by(() => {
|
||||||
return !supportsVision()
|
return !hasVisionModality
|
||||||
? 'Text files and PDFs supported. Images, audio, and video require vision models.'
|
? 'Text files and PDFs supported. Images, audio, and video require vision models.'
|
||||||
: 'Attach files';
|
: 'Attach files';
|
||||||
});
|
});
|
||||||
|
|
@ -53,7 +60,7 @@
|
||||||
<Tooltip.Trigger class="w-full">
|
<Tooltip.Trigger class="w-full">
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
class="images-button flex cursor-pointer items-center gap-2"
|
class="images-button flex cursor-pointer items-center gap-2"
|
||||||
disabled={!supportsVision()}
|
disabled={!hasVisionModality}
|
||||||
onclick={() => handleFileUpload(FileTypeCategory.IMAGE)}
|
onclick={() => handleFileUpload(FileTypeCategory.IMAGE)}
|
||||||
>
|
>
|
||||||
<Image class="h-4 w-4" />
|
<Image class="h-4 w-4" />
|
||||||
|
|
@ -62,7 +69,7 @@
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
|
|
||||||
{#if !supportsVision()}
|
{#if !hasVisionModality}
|
||||||
<Tooltip.Content>
|
<Tooltip.Content>
|
||||||
<p>Images require vision models to be processed</p>
|
<p>Images require vision models to be processed</p>
|
||||||
</Tooltip.Content>
|
</Tooltip.Content>
|
||||||
|
|
@ -73,7 +80,7 @@
|
||||||
<Tooltip.Trigger class="w-full">
|
<Tooltip.Trigger class="w-full">
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
class="audio-button flex cursor-pointer items-center gap-2"
|
class="audio-button flex cursor-pointer items-center gap-2"
|
||||||
disabled={!supportsAudio()}
|
disabled={!hasAudioModality}
|
||||||
onclick={() => handleFileUpload(FileTypeCategory.AUDIO)}
|
onclick={() => handleFileUpload(FileTypeCategory.AUDIO)}
|
||||||
>
|
>
|
||||||
<Volume2 class="h-4 w-4" />
|
<Volume2 class="h-4 w-4" />
|
||||||
|
|
@ -82,7 +89,7 @@
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
|
|
||||||
{#if !supportsAudio()}
|
{#if !hasAudioModality}
|
||||||
<Tooltip.Content>
|
<Tooltip.Content>
|
||||||
<p>Audio files require audio models to be processed</p>
|
<p>Audio files require audio models to be processed</p>
|
||||||
</Tooltip.Content>
|
</Tooltip.Content>
|
||||||
|
|
@ -110,7 +117,7 @@
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
|
|
||||||
{#if !supportsVision()}
|
{#if !hasVisionModality}
|
||||||
<Tooltip.Content>
|
<Tooltip.Content>
|
||||||
<p>PDFs will be converted to text. Image-based PDFs may not work properly.</p>
|
<p>PDFs will be converted to text. Image-based PDFs may not work properly.</p>
|
||||||
</Tooltip.Content>
|
</Tooltip.Content>
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@
|
||||||
import { Mic, Square } from '@lucide/svelte';
|
import { Mic, Square } from '@lucide/svelte';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||||
import { supportsAudio } from '$lib/stores/props.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
class?: string;
|
class?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
hasAudioModality?: boolean;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
isRecording?: boolean;
|
isRecording?: boolean;
|
||||||
onMicClick?: () => void;
|
onMicClick?: () => void;
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
let {
|
let {
|
||||||
class: className = '',
|
class: className = '',
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
hasAudioModality = false,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
isRecording = false,
|
isRecording = false,
|
||||||
onMicClick
|
onMicClick
|
||||||
|
|
@ -28,7 +29,7 @@
|
||||||
class="h-8 w-8 rounded-full p-0 {isRecording
|
class="h-8 w-8 rounded-full p-0 {isRecording
|
||||||
? 'animate-pulse bg-red-500 text-white hover:bg-red-600'
|
? 'animate-pulse bg-red-500 text-white hover:bg-red-600'
|
||||||
: ''}"
|
: ''}"
|
||||||
disabled={disabled || isLoading || !supportsAudio()}
|
disabled={disabled || isLoading || !hasAudioModality}
|
||||||
onclick={onMicClick}
|
onclick={onMicClick}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
|
@ -42,7 +43,7 @@
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
|
|
||||||
{#if !supportsAudio()}
|
{#if !hasAudioModality}
|
||||||
<Tooltip.Content>
|
<Tooltip.Content>
|
||||||
<p>Current model does not support audio</p>
|
<p>Current model does not support audio</p>
|
||||||
</Tooltip.Content>
|
</Tooltip.Content>
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,17 @@
|
||||||
} from '$lib/components/app';
|
} from '$lib/components/app';
|
||||||
import { FileTypeCategory } from '$lib/enums';
|
import { FileTypeCategory } from '$lib/enums';
|
||||||
import { getFileTypeCategory } from '$lib/utils/file-type';
|
import { getFileTypeCategory } from '$lib/utils/file-type';
|
||||||
import { supportsAudio } from '$lib/stores/props.svelte';
|
import {
|
||||||
|
supportsAudio,
|
||||||
|
supportsVision,
|
||||||
|
isRouterMode,
|
||||||
|
fetchModelProps,
|
||||||
|
getModelProps
|
||||||
|
} from '$lib/stores/props.svelte';
|
||||||
import { config } from '$lib/stores/settings.svelte';
|
import { config } from '$lib/stores/settings.svelte';
|
||||||
import { modelOptions, selectedModelId, selectModelByName } from '$lib/stores/models.svelte';
|
import { modelOptions, selectedModelId, selectModelByName } from '$lib/stores/models.svelte';
|
||||||
import { getConversationModel } from '$lib/stores/chat.svelte';
|
import { getConversationModel } from '$lib/stores/chat.svelte';
|
||||||
import { activeMessages } from '$lib/stores/conversations.svelte';
|
import { activeMessages } from '$lib/stores/conversations.svelte';
|
||||||
import { isRouterMode } from '$lib/stores/props.svelte';
|
|
||||||
import type { ChatUploadedFile } from '$lib/types/chat';
|
import type { ChatUploadedFile } from '$lib/types/chat';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -44,7 +49,84 @@
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let currentConfig = $derived(config());
|
let currentConfig = $derived(config());
|
||||||
let hasAudioModality = $derived(supportsAudio());
|
let isRouter = $derived(isRouterMode());
|
||||||
|
|
||||||
|
let conversationModel = $derived(getConversationModel(activeMessages() as DatabaseMessage[]));
|
||||||
|
|
||||||
|
let previousConversationModel: string | null = null;
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (conversationModel && conversationModel !== previousConversationModel) {
|
||||||
|
previousConversationModel = conversationModel;
|
||||||
|
selectModelByName(conversationModel);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get active model ID for fetching props
|
||||||
|
// Priority: user-selected model > conversation model (allows changing model mid-chat)
|
||||||
|
let activeModelId = $derived.by(() => {
|
||||||
|
if (!isRouter) return null;
|
||||||
|
|
||||||
|
const options = modelOptions();
|
||||||
|
|
||||||
|
const selectedId = selectedModelId();
|
||||||
|
if (selectedId) {
|
||||||
|
const model = options.find((m) => m.id === selectedId);
|
||||||
|
if (model) return model.model;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conversationModel) {
|
||||||
|
const model = options.find((m) => m.model === conversationModel);
|
||||||
|
if (model) return model.model;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// State for model props (fetched from /props?model=<id>)
|
||||||
|
let modelPropsVersion = $state(0); // Used to trigger reactivity after fetch
|
||||||
|
|
||||||
|
// Fetch model props when active model changes
|
||||||
|
$effect(() => {
|
||||||
|
if (isRouter && activeModelId) {
|
||||||
|
// Check if we already have cached props
|
||||||
|
const cached = getModelProps(activeModelId);
|
||||||
|
if (!cached) {
|
||||||
|
// Fetch props for this model
|
||||||
|
fetchModelProps(activeModelId).then(() => {
|
||||||
|
// Trigger reactivity update
|
||||||
|
modelPropsVersion++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let hasAudioModality = $derived.by(() => {
|
||||||
|
if (!isRouter) return supportsAudio();
|
||||||
|
|
||||||
|
if (activeModelId) {
|
||||||
|
void modelPropsVersion;
|
||||||
|
|
||||||
|
const props = getModelProps(activeModelId);
|
||||||
|
if (props) return props.modalities?.audio ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
let hasVisionModality = $derived.by(() => {
|
||||||
|
if (!isRouter) return supportsVision();
|
||||||
|
|
||||||
|
if (activeModelId) {
|
||||||
|
void modelPropsVersion;
|
||||||
|
|
||||||
|
const props = getModelProps(activeModelId);
|
||||||
|
if (props) return props.modalities?.vision ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
let hasAudioAttachments = $derived(
|
let hasAudioAttachments = $derived(
|
||||||
uploadedFiles.some((file) => getFileTypeCategory(file.type) === FileTypeCategory.AUDIO)
|
uploadedFiles.some((file) => getFileTypeCategory(file.type) === FileTypeCategory.AUDIO)
|
||||||
);
|
);
|
||||||
|
|
@ -52,22 +134,6 @@
|
||||||
hasAudioModality && !hasText && !hasAudioAttachments && currentConfig.autoMicOnEmpty
|
hasAudioModality && !hasText && !hasAudioAttachments && currentConfig.autoMicOnEmpty
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get model from conversation messages (last assistant message with model)
|
|
||||||
let conversationModel = $derived(getConversationModel(activeMessages() as DatabaseMessage[]));
|
|
||||||
|
|
||||||
// Sync selected model with conversation model when it changes
|
|
||||||
// Only sync when conversation HAS a model - don't clear selection for new chats
|
|
||||||
// to allow user to select a model before first message
|
|
||||||
$effect(() => {
|
|
||||||
if (conversationModel) {
|
|
||||||
selectModelByName(conversationModel);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let isRouter = $derived(isRouterMode());
|
|
||||||
|
|
||||||
// Check if any model is selected (either from conversation or user selection)
|
|
||||||
// In single MODEL mode, there's always a model available
|
|
||||||
let hasModelSelected = $derived(!isRouter || !!conversationModel || !!selectedModelId());
|
let hasModelSelected = $derived(!isRouter || !!conversationModel || !!selectedModelId());
|
||||||
|
|
||||||
let isSelectedModelInCache = $derived.by(() => {
|
let isSelectedModelInCache = $derived.by(() => {
|
||||||
|
|
@ -91,28 +157,35 @@
|
||||||
if (!hasModelSelected) {
|
if (!hasModelSelected) {
|
||||||
return 'Please select a model first';
|
return 'Please select a model first';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isSelectedModelInCache) {
|
if (!isSelectedModelInCache) {
|
||||||
return 'Selected model is not available, please select another';
|
return 'Selected model is not available, please select another';
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ref to SelectorModel for programmatic opening
|
|
||||||
let selectorModelRef: SelectorModel | undefined = $state(undefined);
|
let selectorModelRef: SelectorModel | undefined = $state(undefined);
|
||||||
|
|
||||||
// Export function to open the model selector
|
|
||||||
export function openModelSelector() {
|
export function openModelSelector() {
|
||||||
selectorModelRef?.open();
|
selectorModelRef?.open();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex w-full items-center gap-3 {className}" style="container-type: inline-size">
|
<div class="flex w-full items-center gap-3 {className}" style="container-type: inline-size">
|
||||||
<ChatFormActionFileAttachments class="mr-auto" {disabled} {onFileUpload} />
|
<ChatFormActionFileAttachments
|
||||||
|
class="mr-auto"
|
||||||
|
{disabled}
|
||||||
|
{hasAudioModality}
|
||||||
|
{hasVisionModality}
|
||||||
|
{onFileUpload}
|
||||||
|
/>
|
||||||
|
|
||||||
<SelectorModel
|
<SelectorModel
|
||||||
bind:this={selectorModelRef}
|
bind:this={selectorModelRef}
|
||||||
currentModel={conversationModel}
|
currentModel={conversationModel}
|
||||||
forceForegroundText={true}
|
forceForegroundText={true}
|
||||||
|
useGlobalSelection={true}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if isLoading}
|
{#if isLoading}
|
||||||
|
|
@ -125,7 +198,7 @@
|
||||||
<Square class="h-8 w-8 fill-destructive stroke-destructive" />
|
<Square class="h-8 w-8 fill-destructive stroke-destructive" />
|
||||||
</Button>
|
</Button>
|
||||||
{:else if shouldShowRecordButton}
|
{:else if shouldShowRecordButton}
|
||||||
<ChatFormActionRecord {disabled} {isLoading} {isRecording} {onMicClick} />
|
<ChatFormActionRecord {disabled} {hasAudioModality} {isLoading} {isRecording} {onMicClick} />
|
||||||
{:else}
|
{:else}
|
||||||
<ChatFormActionSubmit
|
<ChatFormActionSubmit
|
||||||
canSend={canSend && hasModelSelected && isSelectedModelInCache}
|
canSend={canSend && hasModelSelected && isSelectedModelInCache}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
interface Props {
|
interface Props {
|
||||||
accept?: string;
|
accept?: string;
|
||||||
class?: string;
|
class?: string;
|
||||||
|
hasAudioModality?: boolean;
|
||||||
|
hasVisionModality?: boolean;
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
onFileSelect?: (files: File[]) => void;
|
onFileSelect?: (files: File[]) => void;
|
||||||
}
|
}
|
||||||
|
|
@ -11,6 +13,8 @@
|
||||||
let {
|
let {
|
||||||
accept = $bindable(),
|
accept = $bindable(),
|
||||||
class: className = '',
|
class: className = '',
|
||||||
|
hasAudioModality = false,
|
||||||
|
hasVisionModality = false,
|
||||||
multiple = true,
|
multiple = true,
|
||||||
onFileSelect
|
onFileSelect
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
@ -18,7 +22,13 @@
|
||||||
let fileInputElement: HTMLInputElement | undefined;
|
let fileInputElement: HTMLInputElement | undefined;
|
||||||
|
|
||||||
// Use modality-aware accept string by default, but allow override
|
// Use modality-aware accept string by default, but allow override
|
||||||
let finalAccept = $derived(accept ?? generateModalityAwareAcceptString());
|
let finalAccept = $derived(
|
||||||
|
accept ??
|
||||||
|
generateModalityAwareAcceptString({
|
||||||
|
hasVision: hasVisionModality,
|
||||||
|
hasAudio: hasAudioModality
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
export function click() {
|
export function click() {
|
||||||
fileInputElement?.click();
|
fileInputElement?.click();
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
) => void;
|
) => void;
|
||||||
onEditUserMessagePreserveResponses?: (message: DatabaseMessage, newContent: string) => void;
|
onEditUserMessagePreserveResponses?: (message: DatabaseMessage, newContent: string) => void;
|
||||||
onNavigateToSibling?: (siblingId: string) => void;
|
onNavigateToSibling?: (siblingId: string) => void;
|
||||||
onRegenerateWithBranching?: (message: DatabaseMessage) => void;
|
onRegenerateWithBranching?: (message: DatabaseMessage, modelOverride?: string) => void;
|
||||||
siblingInfo?: ChatMessageSiblingInfo | null;
|
siblingInfo?: ChatMessageSiblingInfo | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -133,8 +133,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRegenerate() {
|
function handleRegenerate(modelOverride?: string) {
|
||||||
onRegenerateWithBranching?.(message);
|
onRegenerateWithBranching?.(message, modelOverride);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleContinue() {
|
function handleContinue() {
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@
|
||||||
onEditKeydown?: (event: KeyboardEvent) => void;
|
onEditKeydown?: (event: KeyboardEvent) => void;
|
||||||
onEditedContentChange?: (content: string) => void;
|
onEditedContentChange?: (content: string) => void;
|
||||||
onNavigateToSibling?: (siblingId: string) => void;
|
onNavigateToSibling?: (siblingId: string) => void;
|
||||||
onRegenerate: () => void;
|
onRegenerate: (modelOverride?: string) => void;
|
||||||
onSaveEdit?: () => void;
|
onSaveEdit?: () => void;
|
||||||
onShowDeleteDialogChange: (show: boolean) => void;
|
onShowDeleteDialogChange: (show: boolean) => void;
|
||||||
onShouldBranchAfterEditChange?: (value: boolean) => void;
|
onShouldBranchAfterEditChange?: (value: boolean) => void;
|
||||||
|
|
@ -101,11 +101,12 @@
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
async function handleModelChange(modelId: string) {
|
async function handleModelChange(modelId: string, modelName: string) {
|
||||||
try {
|
try {
|
||||||
await selectModel(modelId);
|
await selectModel(modelId);
|
||||||
|
|
||||||
onRegenerate();
|
// Pass the selected model name for regeneration
|
||||||
|
onRegenerate(modelName);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to change model:', error);
|
console.error('Failed to change model:', error);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,10 +87,10 @@
|
||||||
refreshAllMessages();
|
refreshAllMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRegenerateWithBranching(message: DatabaseMessage) {
|
async function handleRegenerateWithBranching(message: DatabaseMessage, modelOverride?: string) {
|
||||||
onUserAction?.();
|
onUserAction?.();
|
||||||
|
|
||||||
await regenerateMessageWithBranching(message.id);
|
await regenerateMessageWithBranching(message.id, modelOverride);
|
||||||
|
|
||||||
refreshAllMessages();
|
refreshAllMessages();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,13 @@
|
||||||
supportsAudio,
|
supportsAudio,
|
||||||
propsLoading,
|
propsLoading,
|
||||||
serverWarning,
|
serverWarning,
|
||||||
propsStore
|
propsStore,
|
||||||
|
isRouterMode,
|
||||||
|
fetchModelProps,
|
||||||
|
getModelProps
|
||||||
} from '$lib/stores/props.svelte';
|
} from '$lib/stores/props.svelte';
|
||||||
|
import { modelOptions, selectedModelId } from '$lib/stores/models.svelte';
|
||||||
|
import { getConversationModel } from '$lib/stores/chat.svelte';
|
||||||
import { parseFilesToMessageExtras } from '$lib/utils/convert-files-to-extra';
|
import { parseFilesToMessageExtras } from '$lib/utils/convert-files-to-extra';
|
||||||
import { isFileTypeSupported } from '$lib/utils/file-type';
|
import { isFileTypeSupported } from '$lib/utils/file-type';
|
||||||
import { filterFilesByModalities } from '$lib/utils/modality-file-validation';
|
import { filterFilesByModalities } from '$lib/utils/modality-file-validation';
|
||||||
|
|
@ -89,6 +94,72 @@
|
||||||
|
|
||||||
let isCurrentConversationLoading = $derived(isLoading());
|
let isCurrentConversationLoading = $derived(isLoading());
|
||||||
|
|
||||||
|
// Model-specific capability detection (same logic as ChatFormActions)
|
||||||
|
let isRouter = $derived(isRouterMode());
|
||||||
|
let conversationModel = $derived(getConversationModel(activeMessages() as DatabaseMessage[]));
|
||||||
|
|
||||||
|
// Get active model ID for fetching props
|
||||||
|
let activeModelId = $derived.by(() => {
|
||||||
|
if (!isRouter) return null;
|
||||||
|
|
||||||
|
const options = modelOptions();
|
||||||
|
|
||||||
|
// First try user-selected model
|
||||||
|
const selectedId = selectedModelId();
|
||||||
|
if (selectedId) {
|
||||||
|
const model = options.find((m) => m.id === selectedId);
|
||||||
|
if (model) return model.model;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to conversation model
|
||||||
|
if (conversationModel) {
|
||||||
|
const model = options.find((m) => m.model === conversationModel);
|
||||||
|
if (model) return model.model;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// State for model props reactivity
|
||||||
|
let modelPropsVersion = $state(0);
|
||||||
|
|
||||||
|
// Fetch model props when active model changes
|
||||||
|
$effect(() => {
|
||||||
|
if (isRouter && activeModelId) {
|
||||||
|
const cached = getModelProps(activeModelId);
|
||||||
|
if (!cached) {
|
||||||
|
fetchModelProps(activeModelId).then(() => {
|
||||||
|
modelPropsVersion++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Derive modalities from model props (ROUTER) or server props (MODEL)
|
||||||
|
let hasAudioModality = $derived.by(() => {
|
||||||
|
if (!isRouter) return supportsAudio();
|
||||||
|
|
||||||
|
if (activeModelId) {
|
||||||
|
void modelPropsVersion;
|
||||||
|
const props = getModelProps(activeModelId);
|
||||||
|
if (props) return props.modalities?.audio ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
let hasVisionModality = $derived.by(() => {
|
||||||
|
if (!isRouter) return supportsVision();
|
||||||
|
|
||||||
|
if (activeModelId) {
|
||||||
|
void modelPropsVersion;
|
||||||
|
const props = getModelProps(activeModelId);
|
||||||
|
if (props) return props.modalities?.vision ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
async function handleDeleteConfirm() {
|
async function handleDeleteConfirm() {
|
||||||
const conversation = activeConversation();
|
const conversation = activeConversation();
|
||||||
if (conversation) {
|
if (conversation) {
|
||||||
|
|
@ -220,16 +291,20 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { supportedFiles, unsupportedFiles, modalityReasons } =
|
// Use model-specific capabilities for file validation
|
||||||
filterFilesByModalities(generallySupported);
|
const capabilities = { hasVision: hasVisionModality, hasAudio: hasAudioModality };
|
||||||
|
const { supportedFiles, unsupportedFiles, modalityReasons } = filterFilesByModalities(
|
||||||
|
generallySupported,
|
||||||
|
capabilities
|
||||||
|
);
|
||||||
|
|
||||||
const allUnsupportedFiles = [...generallyUnsupported, ...unsupportedFiles];
|
const allUnsupportedFiles = [...generallyUnsupported, ...unsupportedFiles];
|
||||||
|
|
||||||
if (allUnsupportedFiles.length > 0) {
|
if (allUnsupportedFiles.length > 0) {
|
||||||
const supportedTypes: string[] = ['text files', 'PDFs'];
|
const supportedTypes: string[] = ['text files', 'PDFs'];
|
||||||
|
|
||||||
if (supportsVision()) supportedTypes.push('images');
|
if (hasVisionModality) supportedTypes.push('images');
|
||||||
if (supportsAudio()) supportedTypes.push('audio files');
|
if (hasAudioModality) supportedTypes.push('audio files');
|
||||||
|
|
||||||
fileErrorData = {
|
fileErrorData = {
|
||||||
generallyUnsupported,
|
generallyUnsupported,
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@
|
||||||
onModelChange?: (modelId: string, modelName: string) => void;
|
onModelChange?: (modelId: string, modelName: string) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
forceForegroundText?: boolean;
|
forceForegroundText?: boolean;
|
||||||
|
/** When true, user's global selection takes priority over currentModel (for form selector) */
|
||||||
|
useGlobalSelection?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
|
|
@ -28,7 +30,8 @@
|
||||||
currentModel = null,
|
currentModel = null,
|
||||||
onModelChange,
|
onModelChange,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
forceForegroundText = false
|
forceForegroundText = false,
|
||||||
|
useGlobalSelection = false
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let options = $derived(modelOptions());
|
let options = $derived(modelOptions());
|
||||||
|
|
@ -260,6 +263,14 @@
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When useGlobalSelection is true (form selector), prioritize user selection
|
||||||
|
// Otherwise (message display), prioritize currentModel
|
||||||
|
if (useGlobalSelection && activeId) {
|
||||||
|
const selected = options.find((option) => option.id === activeId);
|
||||||
|
if (selected) return selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show currentModel (from message payload or conversation)
|
||||||
if (currentModel) {
|
if (currentModel) {
|
||||||
if (!isCurrentModelInCache()) {
|
if (!isCurrentModelInCache()) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -273,7 +284,7 @@
|
||||||
return options.find((option) => option.model === currentModel);
|
return options.find((option) => option.model === currentModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user has selected a model (for new chats before first message)
|
// Fallback to user selection (for new chats before first message)
|
||||||
if (activeId) {
|
if (activeId) {
|
||||||
return options.find((option) => option.id === activeId);
|
return options.find((option) => option.id === activeId);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,7 @@ export class ChatService {
|
||||||
};
|
};
|
||||||
|
|
||||||
const isRouter = isRouterMode();
|
const isRouter = isRouterMode();
|
||||||
const activeModel = isRouter ? selectedModelName() : null;
|
const activeModel = isRouter ? options.model || selectedModelName() : null;
|
||||||
|
|
||||||
if (isRouter && activeModel) {
|
if (isRouter && activeModel) {
|
||||||
requestBody.model = activeModel;
|
requestBody.model = activeModel;
|
||||||
|
|
|
||||||
|
|
@ -40,4 +40,34 @@ export class PropsService {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data as ApiLlamaCppServerProps;
|
return data as ApiLlamaCppServerProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches server properties for a specific model (ROUTER mode)
|
||||||
|
*
|
||||||
|
* @param modelId - The model ID to fetch properties for
|
||||||
|
* @returns {Promise<ApiLlamaCppServerProps>} Server properties for the model
|
||||||
|
* @throws {Error} If the request fails or returns invalid data
|
||||||
|
*/
|
||||||
|
static async fetchForModel(modelId: string): Promise<ApiLlamaCppServerProps> {
|
||||||
|
const currentConfig = config();
|
||||||
|
const apiKey = currentConfig.apiKey?.toString().trim();
|
||||||
|
|
||||||
|
const url = new URL('./props', window.location.href);
|
||||||
|
url.searchParams.set('model', modelId);
|
||||||
|
|
||||||
|
const response = await fetch(url.toString(), {
|
||||||
|
headers: {
|
||||||
|
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to fetch model properties: ${response.status} ${response.statusText}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data as ApiLlamaCppServerProps;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -489,7 +489,8 @@ class ChatStore {
|
||||||
allMessages: DatabaseMessage[],
|
allMessages: DatabaseMessage[],
|
||||||
assistantMessage: DatabaseMessage,
|
assistantMessage: DatabaseMessage,
|
||||||
onComplete?: (content: string) => Promise<void>,
|
onComplete?: (content: string) => Promise<void>,
|
||||||
onError?: (error: Error) => void
|
onError?: (error: Error) => void,
|
||||||
|
modelOverride?: string | null
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
let streamedContent = '';
|
let streamedContent = '';
|
||||||
let streamedReasoningContent = '';
|
let streamedReasoningContent = '';
|
||||||
|
|
@ -520,6 +521,7 @@ class ChatStore {
|
||||||
allMessages,
|
allMessages,
|
||||||
{
|
{
|
||||||
...this.getApiOptions(),
|
...this.getApiOptions(),
|
||||||
|
...(modelOverride ? { model: modelOverride } : {}),
|
||||||
onChunk: (chunk: string) => {
|
onChunk: (chunk: string) => {
|
||||||
streamedContent += chunk;
|
streamedContent += chunk;
|
||||||
this.setChatStreaming(assistantMessage.convId, streamedContent, assistantMessage.id);
|
this.setChatStreaming(assistantMessage.convId, streamedContent, assistantMessage.id);
|
||||||
|
|
@ -996,7 +998,7 @@ class ChatStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async regenerateMessageWithBranching(messageId: string): Promise<void> {
|
async regenerateMessageWithBranching(messageId: string, modelOverride?: string): Promise<void> {
|
||||||
const activeConv = conversationsStore.activeConversation;
|
const activeConv = conversationsStore.activeConversation;
|
||||||
if (!activeConv || this.isLoading) return;
|
if (!activeConv || this.isLoading) return;
|
||||||
try {
|
try {
|
||||||
|
|
@ -1035,7 +1037,16 @@ class ChatStore {
|
||||||
parentMessage.id,
|
parentMessage.id,
|
||||||
false
|
false
|
||||||
) as DatabaseMessage[];
|
) as DatabaseMessage[];
|
||||||
await this.streamChatCompletion(conversationPath, newAssistantMessage);
|
// Use modelOverride if provided, otherwise use the original message's model
|
||||||
|
// If neither is available, don't pass model (will use global selection)
|
||||||
|
const modelToUse = modelOverride || msg.model || undefined;
|
||||||
|
await this.streamChatCompletion(
|
||||||
|
conversationPath,
|
||||||
|
newAssistantMessage,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
modelToUse
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!this.isAbortError(error))
|
if (!this.isAbortError(error))
|
||||||
console.error('Failed to regenerate message with branching:', error);
|
console.error('Failed to regenerate message with branching:', error);
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,10 @@ class PropsStore {
|
||||||
private _serverMode = $state<ServerMode | null>(null);
|
private _serverMode = $state<ServerMode | null>(null);
|
||||||
private fetchPromise: Promise<void> | null = null;
|
private fetchPromise: Promise<void> | null = null;
|
||||||
|
|
||||||
|
// Model-specific props cache (ROUTER mode)
|
||||||
|
private _modelPropsCache = $state<Map<string, ApiLlamaCppServerProps>>(new Map());
|
||||||
|
private _modelPropsFetching = $state<Set<string>>(new Set());
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// LocalStorage persistence
|
// LocalStorage persistence
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -238,6 +242,52 @@ class PropsStore {
|
||||||
await fetchPromise;
|
await fetchPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Fetch Model-Specific Properties (ROUTER mode)
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cached props for a specific model
|
||||||
|
*/
|
||||||
|
getModelProps(modelId: string): ApiLlamaCppServerProps | null {
|
||||||
|
return this._modelPropsCache.get(modelId) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if model props are being fetched
|
||||||
|
*/
|
||||||
|
isModelPropsFetching(modelId: string): boolean {
|
||||||
|
return this._modelPropsFetching.has(modelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches properties for a specific model (ROUTER mode)
|
||||||
|
* Results are cached for subsequent calls
|
||||||
|
*/
|
||||||
|
async fetchModelProps(modelId: string): Promise<ApiLlamaCppServerProps | null> {
|
||||||
|
// Return cached if available
|
||||||
|
const cached = this._modelPropsCache.get(modelId);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
// Don't fetch if already fetching
|
||||||
|
if (this._modelPropsFetching.has(modelId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._modelPropsFetching.add(modelId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const props = await PropsService.fetchForModel(modelId);
|
||||||
|
this._modelPropsCache.set(modelId, props);
|
||||||
|
return props;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to fetch props for model ${modelId}:`, error);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
this._modelPropsFetching.delete(modelId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// Error Handling
|
// Error Handling
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -365,3 +415,5 @@ export const isModelMode = () => propsStore.isModelMode;
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
export const fetchProps = propsStore.fetch.bind(propsStore);
|
export const fetchProps = propsStore.fetch.bind(propsStore);
|
||||||
|
export const fetchModelProps = propsStore.fetchModelProps.bind(propsStore);
|
||||||
|
export const getModelProps = propsStore.getModelProps.bind(propsStore);
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ export interface SettingsFieldConfig {
|
||||||
|
|
||||||
export interface SettingsChatServiceOptions {
|
export interface SettingsChatServiceOptions {
|
||||||
stream?: boolean;
|
stream?: boolean;
|
||||||
|
// Model override (for regenerate with specific model)
|
||||||
|
model?: string;
|
||||||
// Generation parameters
|
// Generation parameters
|
||||||
temperature?: number;
|
temperature?: number;
|
||||||
max_tokens?: number;
|
max_tokens?: number;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getFileTypeCategory } from '$lib/utils/file-type';
|
import { getFileTypeCategory } from '$lib/utils/file-type';
|
||||||
import { supportsVision, supportsAudio } from '$lib/stores/props.svelte';
|
|
||||||
import {
|
import {
|
||||||
FileExtensionAudio,
|
FileExtensionAudio,
|
||||||
FileExtensionImage,
|
FileExtensionImage,
|
||||||
|
|
@ -17,13 +16,24 @@ import {
|
||||||
FileTypeCategory
|
FileTypeCategory
|
||||||
} from '$lib/enums';
|
} from '$lib/enums';
|
||||||
|
|
||||||
|
/** Modality capabilities for file validation */
|
||||||
|
export interface ModalityCapabilities {
|
||||||
|
hasVision: boolean;
|
||||||
|
hasAudio: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a file type is supported by the current model's modalities
|
* Check if a file type is supported by the given modalities
|
||||||
* @param filename - The filename to check
|
* @param filename - The filename to check
|
||||||
* @param mimeType - The MIME type of the file
|
* @param mimeType - The MIME type of the file
|
||||||
* @returns true if the file type is supported by the current model
|
* @param capabilities - The modality capabilities to check against
|
||||||
|
* @returns true if the file type is supported
|
||||||
*/
|
*/
|
||||||
export function isFileTypeSupportedByModel(filename: string, mimeType?: string): boolean {
|
export function isFileTypeSupportedByModel(
|
||||||
|
filename: string,
|
||||||
|
mimeType: string | undefined,
|
||||||
|
capabilities: ModalityCapabilities
|
||||||
|
): boolean {
|
||||||
const category = mimeType ? getFileTypeCategory(mimeType) : null;
|
const category = mimeType ? getFileTypeCategory(mimeType) : null;
|
||||||
|
|
||||||
// If we can't determine the category from MIME type, fall back to general support check
|
// If we can't determine the category from MIME type, fall back to general support check
|
||||||
|
|
@ -44,11 +54,11 @@ export function isFileTypeSupportedByModel(filename: string, mimeType?: string):
|
||||||
|
|
||||||
case FileTypeCategory.IMAGE:
|
case FileTypeCategory.IMAGE:
|
||||||
// Images require vision support
|
// Images require vision support
|
||||||
return supportsVision();
|
return capabilities.hasVision;
|
||||||
|
|
||||||
case FileTypeCategory.AUDIO:
|
case FileTypeCategory.AUDIO:
|
||||||
// Audio files require audio support
|
// Audio files require audio support
|
||||||
return supportsAudio();
|
return capabilities.hasAudio;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Unknown categories - be conservative and allow
|
// Unknown categories - be conservative and allow
|
||||||
|
|
@ -59,9 +69,13 @@ export function isFileTypeSupportedByModel(filename: string, mimeType?: string):
|
||||||
/**
|
/**
|
||||||
* Filter files based on model modalities and return supported/unsupported lists
|
* Filter files based on model modalities and return supported/unsupported lists
|
||||||
* @param files - Array of files to filter
|
* @param files - Array of files to filter
|
||||||
|
* @param capabilities - The modality capabilities to check against
|
||||||
* @returns Object with supportedFiles and unsupportedFiles arrays
|
* @returns Object with supportedFiles and unsupportedFiles arrays
|
||||||
*/
|
*/
|
||||||
export function filterFilesByModalities(files: File[]): {
|
export function filterFilesByModalities(
|
||||||
|
files: File[],
|
||||||
|
capabilities: ModalityCapabilities
|
||||||
|
): {
|
||||||
supportedFiles: File[];
|
supportedFiles: File[];
|
||||||
unsupportedFiles: File[];
|
unsupportedFiles: File[];
|
||||||
modalityReasons: Record<string, string>;
|
modalityReasons: Record<string, string>;
|
||||||
|
|
@ -70,8 +84,7 @@ export function filterFilesByModalities(files: File[]): {
|
||||||
const unsupportedFiles: File[] = [];
|
const unsupportedFiles: File[] = [];
|
||||||
const modalityReasons: Record<string, string> = {};
|
const modalityReasons: Record<string, string> = {};
|
||||||
|
|
||||||
const hasVision = supportsVision();
|
const { hasVision, hasAudio } = capabilities;
|
||||||
const hasAudio = supportsAudio();
|
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const category = getFileTypeCategory(file.type);
|
const category = getFileTypeCategory(file.type);
|
||||||
|
|
@ -119,16 +132,17 @@ export function filterFilesByModalities(files: File[]): {
|
||||||
* Generate a user-friendly error message for unsupported files
|
* Generate a user-friendly error message for unsupported files
|
||||||
* @param unsupportedFiles - Array of unsupported files
|
* @param unsupportedFiles - Array of unsupported files
|
||||||
* @param modalityReasons - Reasons why files are unsupported
|
* @param modalityReasons - Reasons why files are unsupported
|
||||||
|
* @param capabilities - The modality capabilities to check against
|
||||||
* @returns Formatted error message
|
* @returns Formatted error message
|
||||||
*/
|
*/
|
||||||
export function generateModalityErrorMessage(
|
export function generateModalityErrorMessage(
|
||||||
unsupportedFiles: File[],
|
unsupportedFiles: File[],
|
||||||
modalityReasons: Record<string, string>
|
modalityReasons: Record<string, string>,
|
||||||
|
capabilities: ModalityCapabilities
|
||||||
): string {
|
): string {
|
||||||
if (unsupportedFiles.length === 0) return '';
|
if (unsupportedFiles.length === 0) return '';
|
||||||
|
|
||||||
const hasVision = supportsVision();
|
const { hasVision, hasAudio } = capabilities;
|
||||||
const hasAudio = supportsAudio();
|
|
||||||
|
|
||||||
let message = '';
|
let message = '';
|
||||||
|
|
||||||
|
|
@ -152,12 +166,12 @@ export function generateModalityErrorMessage(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate file input accept string based on current model modalities
|
* Generate file input accept string based on model modalities
|
||||||
|
* @param capabilities - The modality capabilities to check against
|
||||||
* @returns Accept string for HTML file input element
|
* @returns Accept string for HTML file input element
|
||||||
*/
|
*/
|
||||||
export function generateModalityAwareAcceptString(): string {
|
export function generateModalityAwareAcceptString(capabilities: ModalityCapabilities): string {
|
||||||
const hasVision = supportsVision();
|
const { hasVision, hasAudio } = capabilities;
|
||||||
const hasAudio = supportsAudio();
|
|
||||||
|
|
||||||
const acceptedExtensions: string[] = [];
|
const acceptedExtensions: string[] = [];
|
||||||
const acceptedMimeTypes: string[] = [];
|
const acceptedMimeTypes: string[] = [];
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue