]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/commitdiff
Webui/file upload (#18694)
authorPascal <redacted>
Fri, 9 Jan 2026 15:45:32 +0000 (16:45 +0100)
committerGitHub <redacted>
Fri, 9 Jan 2026 15:45:32 +0000 (16:45 +0100)
* webui: fix restrictive file type validation

* webui: simplify file processing logic

* chore: update webui build output

* webui: remove file picker extension whitelist (1/2)

* webui: remove file picker extension whitelist (2/2)

* chore: update webui build output

* refactor: Cleanup

* chore: update webui build output

* fix: update ChatForm storybook test after removing accept attribute

* chore: update webui build output

* refactor: more cleanup

* chore: update webui build output

tools/server/public/index.html.gz
tools/server/webui/src/lib/components/app/chat/ChatForm/ChatForm.svelte
tools/server/webui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionFileAttachments.svelte
tools/server/webui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActions.svelte
tools/server/webui/src/lib/components/app/chat/ChatForm/ChatFormFileInputInvisible.svelte
tools/server/webui/src/lib/utils/file-type.ts
tools/server/webui/src/lib/utils/index.ts
tools/server/webui/src/lib/utils/modality-file-validation.ts
tools/server/webui/src/lib/utils/process-uploaded-files.ts
tools/server/webui/tests/stories/ChatForm.stories.svelte

index e572817dca0c5ea1f264f7ac9ff8b200a0db80f7..a3fcf8dcdbecf36207a3baa28b8efbc20b92a3ac 100644 (file)
Binary files a/tools/server/public/index.html.gz and b/tools/server/public/index.html.gz differ
index fd2f7f60e579f2e2011a46e98434618ff208b4c4..27ab975cbd0b406cc55ddb1a2d81b459f3c6424f 100644 (file)
        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,
@@ -61,7 +51,6 @@
        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}
index 127130fb8470e324371978c4d8226a94b3a6aadd..dd372680964781cccc0e8b8fc0ac92ae9f079416 100644 (file)
@@ -4,14 +4,13 @@
        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}">
@@ -61,7 +56,7 @@
                                        <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" />
 
@@ -81,7 +76,7 @@
                                        <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" />
 
@@ -98,7 +93,7 @@
 
                        <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" />
 
index 730c348b3025372f24db0073140a68bcb8785a47..dde9bda2d8f2ba0040a0fc4f996f6278140ac63a 100644 (file)
@@ -24,7 +24,7 @@
                isRecording?: boolean;
                hasText?: boolean;
                uploadedFiles?: ChatUploadedFile[];
-               onFileUpload?: (fileType?: FileTypeCategory) => void;
+               onFileUpload?: () => void;
                onMicClick?: () => void;
                onStop?: () => void;
        }
index 52f3913b93bda3fd1d5daacdc35caa5713e7e23f..d758822f3cdf64bd66122d917c5af332f69829e4 100644 (file)
@@ -1,35 +1,14 @@
 <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();
        }
@@ -46,7 +25,6 @@
        bind:this={fileInputElement}
        type="file"
        {multiple}
-       accept={finalAccept}
        onchange={handleFileSelect}
        class="hidden {className}"
 />
index ff7ed6b0c9864fd4915abce92c40e29d33b3c2dd..9a9996d171c6ae20807d2c1826aeb90cf9f7507d 100644 (file)
@@ -195,9 +195,28 @@ export function getFileTypeByExtension(filename: string): string | null {
 }
 
 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;
 }
index ab600619912d659c2296f32ec715260bb21ed7c9..588167b8ca4fb7ae4c9408885c4e3dcc1a4dffc4 100644 (file)
@@ -76,7 +76,6 @@ export {
        isFileTypeSupportedByModel,
        filterFilesByModalities,
        generateModalityErrorMessage,
-       generateModalityAwareAcceptString,
        type ModalityCapabilities
 } from './modality-file-validation';
 
index e3c00f9e97dd6130efcfc24825760081c2baa607..136c084146773661da197a18cd75922b9a8a7b2e 100644 (file)
@@ -4,17 +4,7 @@
  */
 
 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 {
@@ -170,29 +160,3 @@ export function generateModalityErrorMessage(
  * @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(',');
-}
index f00116ccc1d2b70795e6890d4ef8432bc2131927..0342dce1f7ab011c817fb8b4fc34787544e817fc 100644 (file)
@@ -1,5 +1,4 @@
 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';
@@ -84,17 +83,6 @@ export async function processFilesToChatUploaded(
                                }
 
                                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 {
@@ -129,8 +117,14 @@ export async function processFilesToChatUploaded(
                                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);
index fe6f14bd8e085b0b28ba6e4dce1a6d6417f6ea10..18319e8e611817e981238669b705a8eeb75d8768 100644 (file)
                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');