import { INPUT_CLASSES } from '$lib/constants/input-classes';
import { SETTING_CONFIG_DEFAULT } from '$lib/constants/settings-config';
import { config } from '$lib/stores/settings.svelte';
- import { modelsStore, modelOptions, selectedModelId } from '$lib/stores/models.svelte';
+ import { modelOptions, selectedModelId } from '$lib/stores/models.svelte';
import { isRouterMode } from '$lib/stores/server.svelte';
import { chatStore } from '$lib/stores/chat.svelte';
import { activeMessages } from '$lib/stores/conversations.svelte';
- import {
- FileTypeCategory,
- MimeTypeApplication,
- FileExtensionAudio,
- FileExtensionImage,
- FileExtensionPdf,
- FileExtensionText,
- MimeTypeAudio,
- MimeTypeImage,
- MimeTypeText
- } from '$lib/enums';
+ import { MimeTypeText } from '$lib/enums';
import { isIMEComposing, parseClipboardContent } from '$lib/utils';
import {
AudioRecorder,
let audioRecorder: AudioRecorder | undefined;
let chatFormActionsRef: ChatFormActions | undefined = $state(undefined);
let currentConfig = $derived(config());
- let fileAcceptString = $state<string | undefined>(undefined);
let fileInputRef: ChatFormFileInputInvisible | undefined = $state(undefined);
let isRecording = $state(false);
let message = $state('');
return null;
});
- // State for model props reactivity
- let modelPropsVersion = $state(0);
-
- // Fetch model props when active model changes (works for both MODEL and ROUTER mode)
- $effect(() => {
- if (activeModelId) {
- const cached = modelsStore.getModelProps(activeModelId);
- if (!cached) {
- modelsStore.fetchModelProps(activeModelId).then(() => {
- modelPropsVersion++;
- });
- }
- }
- });
-
- // Derive modalities from active model (works for both MODEL and ROUTER mode)
- let hasAudioModality = $derived.by(() => {
- if (activeModelId) {
- void modelPropsVersion; // Trigger reactivity on props fetch
- return modelsStore.modelSupportsAudio(activeModelId);
- }
-
- return false;
- });
-
- let hasVisionModality = $derived.by(() => {
- if (activeModelId) {
- void modelPropsVersion; // Trigger reactivity on props fetch
- return modelsStore.modelSupportsVision(activeModelId);
- }
-
- return false;
- });
-
function checkModelSelected(): boolean {
if (!hasModelSelected) {
// Open the model selector
return true;
}
- function getAcceptStringForFileType(fileType: FileTypeCategory): string {
- switch (fileType) {
- case FileTypeCategory.IMAGE:
- return [...Object.values(FileExtensionImage), ...Object.values(MimeTypeImage)].join(',');
-
- case FileTypeCategory.AUDIO:
- return [...Object.values(FileExtensionAudio), ...Object.values(MimeTypeAudio)].join(',');
-
- case FileTypeCategory.PDF:
- return [...Object.values(FileExtensionPdf), ...Object.values(MimeTypeApplication)].join(
- ','
- );
-
- case FileTypeCategory.TEXT:
- return [...Object.values(FileExtensionText), MimeTypeText.PLAIN].join(',');
-
- default:
- return '';
- }
- }
-
function handleFileSelect(files: File[]) {
onFileUpload?.(files);
}
- function handleFileUpload(fileType?: FileTypeCategory) {
- if (fileType) {
- fileAcceptString = getAcceptStringForFileType(fileType);
- } else {
- fileAcceptString = undefined;
- }
-
- // Use setTimeout to ensure the accept attribute is applied before opening dialog
- setTimeout(() => {
- fileInputRef?.click();
- }, 10);
+ function handleFileUpload() {
+ fileInputRef?.click();
}
async function handleKeydown(event: KeyboardEvent) {
});
</script>
-<ChatFormFileInputInvisible
- bind:this={fileInputRef}
- bind:accept={fileAcceptString}
- {hasAudioModality}
- {hasVisionModality}
- onFileSelect={handleFileSelect}
-/>
+<ChatFormFileInputInvisible bind:this={fileInputRef} onFileSelect={handleFileSelect} />
<form
onsubmit={handleSubmit}
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import * as Tooltip from '$lib/components/ui/tooltip';
import { FILE_TYPE_ICONS } from '$lib/constants/icons';
- import { FileTypeCategory } from '$lib/enums';
interface Props {
class?: string;
disabled?: boolean;
hasAudioModality?: boolean;
hasVisionModality?: boolean;
- onFileUpload?: (fileType?: FileTypeCategory) => void;
+ onFileUpload?: () => void;
}
let {
? 'Text files and PDFs supported. Images, audio, and video require vision models.'
: 'Attach files';
});
-
- function handleFileUpload(fileType?: FileTypeCategory) {
- onFileUpload?.(fileType);
- }
</script>
<div class="flex items-center gap-1 {className}">
<DropdownMenu.Item
class="images-button flex cursor-pointer items-center gap-2"
disabled={!hasVisionModality}
- onclick={() => handleFileUpload(FileTypeCategory.IMAGE)}
+ onclick={() => onFileUpload?.()}
>
<FILE_TYPE_ICONS.image class="h-4 w-4" />
<DropdownMenu.Item
class="audio-button flex cursor-pointer items-center gap-2"
disabled={!hasAudioModality}
- onclick={() => handleFileUpload(FileTypeCategory.AUDIO)}
+ onclick={() => onFileUpload?.()}
>
<FILE_TYPE_ICONS.audio class="h-4 w-4" />
<DropdownMenu.Item
class="flex cursor-pointer items-center gap-2"
- onclick={() => handleFileUpload(FileTypeCategory.TEXT)}
+ onclick={() => onFileUpload?.()}
>
<FILE_TYPE_ICONS.text class="h-4 w-4" />
<Tooltip.Trigger class="w-full">
<DropdownMenu.Item
class="flex cursor-pointer items-center gap-2"
- onclick={() => handleFileUpload(FileTypeCategory.PDF)}
+ onclick={() => onFileUpload?.()}
>
<FILE_TYPE_ICONS.pdf class="h-4 w-4" />
isRecording?: boolean;
hasText?: boolean;
uploadedFiles?: ChatUploadedFile[];
- onFileUpload?: (fileType?: FileTypeCategory) => void;
+ onFileUpload?: () => void;
onMicClick?: () => void;
onStop?: () => void;
}
<script lang="ts">
- import { generateModalityAwareAcceptString } from '$lib/utils';
-
interface Props {
- accept?: string;
class?: string;
- hasAudioModality?: boolean;
- hasVisionModality?: boolean;
multiple?: boolean;
onFileSelect?: (files: File[]) => void;
}
- let {
- accept = $bindable(),
- class: className = '',
- hasAudioModality = false,
- hasVisionModality = false,
- multiple = true,
- onFileSelect
- }: Props = $props();
+ let { class: className = '', multiple = true, onFileSelect }: Props = $props();
let fileInputElement: HTMLInputElement | undefined;
- // Use modality-aware accept string by default, but allow override
- let finalAccept = $derived(
- accept ??
- generateModalityAwareAcceptString({
- hasVision: hasVisionModality,
- hasAudio: hasAudioModality
- })
- );
-
export function click() {
fileInputElement?.click();
}
bind:this={fileInputElement}
type="file"
{multiple}
- accept={finalAccept}
onchange={handleFileSelect}
class="hidden {className}"
/>
}
export function isFileTypeSupported(filename: string, mimeType?: string): boolean {
- if (mimeType && getFileTypeCategory(mimeType)) {
+ // Images are detected and handled separately for vision models
+ if (mimeType) {
+ const category = getFileTypeCategory(mimeType);
+ if (
+ category === FileTypeCategory.IMAGE ||
+ category === FileTypeCategory.AUDIO ||
+ category === FileTypeCategory.PDF
+ ) {
+ return true;
+ }
+ }
+
+ // Check extension for known types (especially images without MIME)
+ const extCategory = getFileTypeCategoryByExtension(filename);
+ if (
+ extCategory === FileTypeCategory.IMAGE ||
+ extCategory === FileTypeCategory.AUDIO ||
+ extCategory === FileTypeCategory.PDF
+ ) {
return true;
}
- return getFileTypeByExtension(filename) !== null;
+ // Fallback: treat everything else as text (inclusive by default)
+ return true;
}
isFileTypeSupportedByModel,
filterFilesByModalities,
generateModalityErrorMessage,
- generateModalityAwareAcceptString,
type ModalityCapabilities
} from './modality-file-validation';
*/
import { getFileTypeCategory } from '$lib/utils';
-import {
- FileExtensionAudio,
- FileExtensionImage,
- FileExtensionPdf,
- FileExtensionText,
- MimeTypeAudio,
- MimeTypeImage,
- MimeTypeApplication,
- MimeTypeText,
- FileTypeCategory
-} from '$lib/enums';
+import { FileTypeCategory } from '$lib/enums';
/** Modality capabilities for file validation */
export interface ModalityCapabilities {
* @param capabilities - The modality capabilities to check against
* @returns Accept string for HTML file input element
*/
-export function generateModalityAwareAcceptString(capabilities: ModalityCapabilities): string {
- const { hasVision, hasAudio } = capabilities;
-
- const acceptedExtensions: string[] = [];
- const acceptedMimeTypes: string[] = [];
-
- // Always include text files and PDFs
- acceptedExtensions.push(...Object.values(FileExtensionText));
- acceptedMimeTypes.push(...Object.values(MimeTypeText));
- acceptedExtensions.push(...Object.values(FileExtensionPdf));
- acceptedMimeTypes.push(...Object.values(MimeTypeApplication));
-
- // Include images only if vision is supported
- if (hasVision) {
- acceptedExtensions.push(...Object.values(FileExtensionImage));
- acceptedMimeTypes.push(...Object.values(MimeTypeImage));
- }
-
- // Include audio only if audio is supported
- if (hasAudio) {
- acceptedExtensions.push(...Object.values(FileExtensionAudio));
- acceptedMimeTypes.push(...Object.values(MimeTypeAudio));
- }
-
- return [...acceptedExtensions, ...acceptedMimeTypes].join(',');
-}
import { isSvgMimeType, svgBase64UrlToPngDataURL } from './svg-to-png';
-import { isTextFileByName } from './text-files';
import { isWebpMimeType, webpBase64UrlToPngDataURL } from './webp-to-png';
import { FileTypeCategory } from '$lib/enums';
import { modelsStore } from '$lib/stores/models.svelte';
}
results.push({ ...base, preview });
- } else if (
- getFileTypeCategory(file.type) === FileTypeCategory.TEXT ||
- isTextFileByName(file.name)
- ) {
- try {
- const textContent = await readFileAsUTF8(file);
- results.push({ ...base, textContent });
- } catch (err) {
- console.warn('Failed to read text file, adding without content:', err);
- results.push(base);
- }
} else if (getFileTypeCategory(file.type) === FileTypeCategory.PDF) {
// Extract text content from PDF for preview
try {
const preview = await readFileAsDataURL(file);
results.push({ ...base, preview });
} else {
- // Other files: add as-is
- results.push(base);
+ // Fallback: treat unknown files as text
+ try {
+ const textContent = await readFileAsUTF8(file);
+ results.push({ ...base, textContent });
+ } catch (err) {
+ console.warn('Failed to read file as text, adding without content:', err);
+ results.push(base);
+ }
}
} catch (error) {
console.error('Error processing file', file.name, error);
await expect(textarea).toHaveValue(text);
const fileInput = document.querySelector('input[type="file"]');
- const acceptAttr = fileInput?.getAttribute('accept');
- await expect(fileInput).toHaveAttribute('accept');
- await expect(acceptAttr).not.toContain('image/');
- await expect(acceptAttr).not.toContain('audio/');
+ await expect(fileInput).not.toHaveAttribute('accept');
// Open file attachments dropdown
const fileUploadButton = canvas.getByText('Attach files');