From: Aleksander Grygier Date: Sat, 14 Feb 2026 08:06:41 +0000 (+0100) Subject: webui: Architecture and UI improvements (#19596) X-Git-Tag: upstream/0.0.8067~17 X-Git-Url: https://git.djapps.eu/?a=commitdiff_plain;h=baa12f3831d088503717a8f85c70340936992343;p=pkg%2Fggml%2Fsources%2Fllama.cpp webui: Architecture and UI improvements (#19596) --- diff --git a/tools/server/public/index.html.gz b/tools/server/public/index.html.gz index f4ff57b4c..75fc856f5 100644 Binary files a/tools/server/public/index.html.gz and b/tools/server/public/index.html.gz differ diff --git a/tools/server/webui/.storybook/main.ts b/tools/server/webui/.storybook/main.ts index bfd16fa22..4f6945f21 100644 --- a/tools/server/webui/.storybook/main.ts +++ b/tools/server/webui/.storybook/main.ts @@ -1,17 +1,24 @@ import type { StorybookConfig } from '@storybook/sveltekit'; +import { dirname, resolve } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); const config: StorybookConfig = { stories: ['../tests/stories/**/*.mdx', '../tests/stories/**/*.stories.@(js|ts|svelte)'], addons: [ '@storybook/addon-svelte-csf', '@chromatic-com/storybook', - '@storybook/addon-docs', + '@storybook/addon-vitest', '@storybook/addon-a11y', - '@storybook/addon-vitest' + '@storybook/addon-docs' ], - framework: { - name: '@storybook/sveltekit', - options: {} + framework: '@storybook/sveltekit', + viteFinal: async (config) => { + config.server = config.server || {}; + config.server.fs = config.server.fs || {}; + config.server.fs.allow = [...(config.server.fs.allow || []), resolve(__dirname, '../tests')]; + return config; } }; export default config; diff --git a/tools/server/webui/.storybook/preview.ts b/tools/server/webui/.storybook/preview.ts index 8d530e43e..566dbfd28 100644 --- a/tools/server/webui/.storybook/preview.ts +++ b/tools/server/webui/.storybook/preview.ts @@ -13,7 +13,7 @@ const preview: Preview = { }, backgrounds: { - disable: true + disabled: true }, a11y: { diff --git a/tools/server/webui/docs/flows/settings-flow.md b/tools/server/webui/docs/flows/settings-flow.md index 474aef01b..40ad3bd94 100644 --- a/tools/server/webui/docs/flows/settings-flow.md +++ b/tools/server/webui/docs/flows/settings-flow.md @@ -49,14 +49,20 @@ sequenceDiagram settingsStore->>serverStore: defaultParams serverStore-->>settingsStore: {temperature, top_p, top_k, ...} - settingsStore->>ParamSvc: extractServerDefaults(defaultParams) - ParamSvc-->>settingsStore: Record + loop each SYNCABLE_PARAMETER + alt key NOT in userOverrides + settingsStore->>settingsStore: config[key] = serverDefault[key] + Note right of settingsStore: Non-overridden params adopt server default + else key in userOverrides + Note right of settingsStore: Keep user value, skip server default + end + end - settingsStore->>ParamSvc: mergeWithServerDefaults(config, serverDefaults) - Note right of ParamSvc: For each syncable parameter:
- If NOT in userOverrides → use server default
- If in userOverrides → keep user value - ParamSvc-->>settingsStore: mergedConfig + alt serverStore.props has webuiSettings + settingsStore->>settingsStore: Apply webuiSettings from server + Note right of settingsStore: Server-provided UI settings
(e.g. showRawOutputSwitch) + end - settingsStore->>settingsStore: config = mergedConfig settingsStore->>settingsStore: saveConfig() deactivate settingsStore @@ -67,11 +73,18 @@ sequenceDiagram UI->>settingsStore: updateConfig(key, value) activate settingsStore settingsStore->>settingsStore: config[key] = value - settingsStore->>settingsStore: userOverrides.add(key) - Note right of settingsStore: Mark as user-modified (won't be overwritten by server) + + alt value matches server default for key + settingsStore->>settingsStore: userOverrides.delete(key) + Note right of settingsStore: Matches server default, remove override + else value differs from server default + settingsStore->>settingsStore: userOverrides.add(key) + Note right of settingsStore: Mark as user-modified (won't be overwritten) + end + settingsStore->>settingsStore: saveConfig() - settingsStore->>LS: set("llama-config", config) - settingsStore->>LS: set("llama-userOverrides", [...userOverrides]) + settingsStore->>LS: set(CONFIG_LOCALSTORAGE_KEY, config) + settingsStore->>LS: set(USER_OVERRIDES_LOCALSTORAGE_KEY, [...userOverrides]) deactivate settingsStore UI->>settingsStore: updateMultipleConfig({key1: val1, key2: val2}) @@ -88,10 +101,9 @@ sequenceDiagram UI->>settingsStore: resetConfig() activate settingsStore - settingsStore->>settingsStore: config = SETTING_CONFIG_DEFAULT + settingsStore->>settingsStore: config = {...SETTING_CONFIG_DEFAULT} settingsStore->>settingsStore: userOverrides.clear() - settingsStore->>settingsStore: syncWithServerDefaults() - Note right of settingsStore: Apply server defaults for syncable params + Note right of settingsStore: All params reset to defaults
Next syncWithServerDefaults will adopt server values settingsStore->>settingsStore: saveConfig() deactivate settingsStore diff --git a/tools/server/webui/src/lib/components/app/actions/ActionIconsCodeBlock.svelte b/tools/server/webui/src/lib/components/app/actions/ActionIconsCodeBlock.svelte index 54ff0af1a..b20e79b5e 100644 --- a/tools/server/webui/src/lib/components/app/actions/ActionIconsCodeBlock.svelte +++ b/tools/server/webui/src/lib/components/app/actions/ActionIconsCodeBlock.svelte @@ -1,6 +1,6 @@ + +
+ + + + + + + + +

{triggerTooltipText}

+
+
+
+ + + {#each actions as item (item.id)} + {@const hasDisabledTooltip = !!item.disabled && !!item.disabledReason} + {@const hasEnabledTooltip = !item.disabled && !!item.tooltip} + + {#if hasDisabledTooltip} + + + + {#if item.id === 'images'} + + {:else if item.id === 'audio'} + + {:else if item.id === 'text'} + + {:else if item.id === 'pdf'} + + {:else} + + {/if} + + {item.label} + + + + +

{item.disabledReason}

+
+
+ {:else if hasEnabledTooltip} + + + handleActionClick(item.id)}> + {#if item.id === 'images'} + + {:else if item.id === 'audio'} + + {:else if item.id === 'text'} + + {:else if item.id === 'pdf'} + + {:else} + + {/if} + + {item.label} + + + + +

{item.tooltip}

+
+
+ {:else} + handleActionClick(item.id)}> + {#if item.id === 'images'} + + {:else if item.id === 'audio'} + + {:else if item.id === 'text'} + + {:else if item.id === 'pdf'} + + {:else} + + {/if} + + {item.label} + + {/if} + {/each} +
+
+
diff --git a/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActions.svelte b/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActions.svelte index c621a69e0..cf5aca42a 100644 --- a/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActions.svelte +++ b/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActions.svelte @@ -2,7 +2,7 @@ import { Square } from '@lucide/svelte'; import { Button } from '$lib/components/ui/button'; import { - ChatFormActionFileAttachments, + ChatFormActionAttachmentsDropdown, ChatFormActionRecord, ChatFormActionSubmit, ModelsSelector @@ -157,7 +157,7 @@ const { handleModelChange } = useModelChangeValidation({ getRequiredModalities: () => usedModalities(), - onValidationFailure: async (previousModelId) => { + onValidationFailure: async (previousModelId: string | null) => { if (previousModelId) { await modelsStore.selectModelById(previousModelId); } @@ -166,32 +166,39 @@
- - - +
+ +
+ +
+ +
{#if isLoading} {:else if shouldShowRecordButton} diff --git a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessage.svelte b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessage.svelte index 3470e2f71..25895c83b 100644 --- a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessage.svelte +++ b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessage.svelte @@ -62,8 +62,8 @@ assistantMessages: number; messageTypes: string[]; } | null>(null); - let editedContent = $state(message.content); - let editedExtras = $state(message.extra ? [...message.extra] : []); + let editedContent = $derived(message.content); + let editedExtras = $derived(message.extra ? [...message.extra] : []); let editedUploadedFiles = $state([]); let isEditing = $state(false); let showDeleteDialog = $state(false); diff --git a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAssistant.svelte b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAssistant.svelte index 1cb6b274b..867def5fc 100644 --- a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAssistant.svelte +++ b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAssistant.svelte @@ -105,7 +105,7 @@ const { handleModelChange } = useModelChangeValidation({ getRequiredModalities: () => conversationsStore.getModalitiesUpToMessage(message.id), - onSuccess: (modelName) => onRegenerate(modelName) + onSuccess: (modelName: string) => onRegenerate(modelName) }); function handleCopyModel() { diff --git a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageEditForm.svelte b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageEditForm.svelte index f812ea2fd..c216ea690 100644 --- a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageEditForm.svelte +++ b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageEditForm.svelte @@ -133,7 +133,7 @@ const { handleModelChange } = useModelChangeValidation({ getRequiredModalities, - onValidationFailure: async (previousModelId) => { + onValidationFailure: async (previousModelId: string | null) => { if (previousModelId) { await modelsStore.selectModelById(previousModelId); } diff --git a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageStatistics.svelte b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageStatistics.svelte index d457e042f..b53e82aaf 100644 --- a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageStatistics.svelte +++ b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageStatistics.svelte @@ -28,7 +28,7 @@ initialView = ChatMessageStatsView.GENERATION }: Props = $props(); - let activeView: ChatMessageStatsView = $state(initialView); + let activeView: ChatMessageStatsView = $derived(initialView); let hasAutoSwitchedToGeneration = $state(false); // In live mode: auto-switch to GENERATION tab when prompt processing completes diff --git a/tools/server/webui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte b/tools/server/webui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte index a5450e6af..3d432e26b 100644 --- a/tools/server/webui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte +++ b/tools/server/webui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte @@ -35,6 +35,7 @@ import { modelsStore, modelOptions, selectedModelId } from '$lib/stores/models.svelte'; import { isFileTypeSupported, filterFilesByModalities } from '$lib/utils'; import { parseFilesToMessageExtras, processFilesToChatUploaded } from '$lib/utils/browser-only'; + import { ErrorDialogType } from '$lib/enums'; import { onMount } from 'svelte'; import { fade, fly, slide } from 'svelte/transition'; import { Trash2, AlertTriangle, RefreshCw } from '@lucide/svelte'; @@ -616,7 +617,7 @@ contextInfo={activeErrorDialog?.contextInfo} onOpenChange={handleErrorDialogOpenChange} open={Boolean(activeErrorDialog)} - type={activeErrorDialog?.type ?? 'server'} + type={(activeErrorDialog?.type as ErrorDialogType) ?? ErrorDialogType.SERVER} /> diff --git a/tools/server/webui/src/lib/components/app/dialogs/DialogConfirmation.svelte b/tools/server/webui/src/lib/components/app/dialogs/DialogConfirmation.svelte index b5175a992..d8aa66f3e 100644 --- a/tools/server/webui/src/lib/components/app/dialogs/DialogConfirmation.svelte +++ b/tools/server/webui/src/lib/components/app/dialogs/DialogConfirmation.svelte @@ -1,6 +1,7 @@ + +
+
+ {#if sectionLabel} + + {sectionLabel} + {#if sectionLabelOptional} + (optional) + {/if} + + {/if} + + +
+ {#if pairs.length > 0} +
+ {#each pairs as pair, index (index)} +
+ updatePairKey(index, e.currentTarget.value)} + class="flex-1" + /> + + + + +
+ {/each} +
+ {:else} +

{emptyMessage}

+ {/if} +
diff --git a/tools/server/webui/src/lib/components/app/forms/SearchInput.svelte b/tools/server/webui/src/lib/components/app/forms/SearchInput.svelte new file mode 100644 index 000000000..9a8088d9d --- /dev/null +++ b/tools/server/webui/src/lib/components/app/forms/SearchInput.svelte @@ -0,0 +1,73 @@ + + +
+ + + + + {#if showClearButton} + + {/if} +
diff --git a/tools/server/webui/src/lib/components/app/forms/index.ts b/tools/server/webui/src/lib/components/app/forms/index.ts new file mode 100644 index 000000000..b0280a20a --- /dev/null +++ b/tools/server/webui/src/lib/components/app/forms/index.ts @@ -0,0 +1,30 @@ +/** + * + * FORMS & INPUTS + * + * Form-related utility components. + * + */ + +/** + * **SearchInput** - Search field with clear button + * + * Input field optimized for search with clear button and keyboard handling. + * Supports placeholder, autofocus, and change callbacks. + */ +export { default as SearchInput } from './SearchInput.svelte'; + +/** + * **KeyValuePairs** - Editable key-value list + * + * Dynamic list of key-value pairs with add/remove functionality. + * Used for HTTP headers, metadata, and configuration. + * + * **Features:** + * - Add new pairs with button + * - Remove individual pairs + * - Customizable placeholders and labels + * - Empty state message + * - Auto-resize value textarea + */ +export { default as KeyValuePairs } from './KeyValuePairs.svelte'; diff --git a/tools/server/webui/src/lib/components/app/index.ts b/tools/server/webui/src/lib/components/app/index.ts index 8631d4fb3..142622ef0 100644 --- a/tools/server/webui/src/lib/components/app/index.ts +++ b/tools/server/webui/src/lib/components/app/index.ts @@ -1,12 +1,20 @@ -// Chat +export * from './actions'; +export * from './badges'; +export * from './content'; +export * from './forms'; +export * from './misc'; +export * from './models'; +export * from './navigation'; +export * from './server'; +// Chat export { default as ChatAttachmentPreview } from './chat/ChatAttachments/ChatAttachmentPreview.svelte'; export { default as ChatAttachmentThumbnailFile } from './chat/ChatAttachments/ChatAttachmentThumbnailFile.svelte'; export { default as ChatAttachmentThumbnailImage } from './chat/ChatAttachments/ChatAttachmentThumbnailImage.svelte'; export { default as ChatAttachmentsList } from './chat/ChatAttachments/ChatAttachmentsList.svelte'; export { default as ChatAttachmentsViewAll } from './chat/ChatAttachments/ChatAttachmentsViewAll.svelte'; - export { default as ChatForm } from './chat/ChatForm/ChatForm.svelte'; +export { default as ChatFormActionAttachmentsDropdown } from './chat/ChatForm/ChatFormActions/ChatFormActionAttachmentsDropdown.svelte'; export { default as ChatFormActionFileAttachments } from './chat/ChatForm/ChatFormActions/ChatFormActionFileAttachments.svelte'; export { default as ChatFormActionRecord } from './chat/ChatForm/ChatFormActions/ChatFormActionRecord.svelte'; export { default as ChatFormActions } from './chat/ChatForm/ChatFormActions/ChatFormActions.svelte'; @@ -14,36 +22,38 @@ export { default as ChatFormActionSubmit } from './chat/ChatForm/ChatFormActions export { default as ChatFormFileInputInvisible } from './chat/ChatForm/ChatFormFileInputInvisible.svelte'; export { default as ChatFormHelperText } from './chat/ChatForm/ChatFormHelperText.svelte'; export { default as ChatFormTextarea } from './chat/ChatForm/ChatFormTextarea.svelte'; - export { default as ChatMessage } from './chat/ChatMessages/ChatMessage.svelte'; export { default as ChatMessageActions } from './chat/ChatMessages/ChatMessageActions.svelte'; +export { default as ChatMessageAssistant } from './chat/ChatMessages/ChatMessageAssistant.svelte'; export { default as ChatMessageBranchingControls } from './chat/ChatMessages/ChatMessageBranchingControls.svelte'; +export { default as ChatMessageEditForm } from './chat/ChatMessages/ChatMessageEditForm.svelte'; export { default as ChatMessageStatistics } from './chat/ChatMessages/ChatMessageStatistics.svelte'; export { default as ChatMessageSystem } from './chat/ChatMessages/ChatMessageSystem.svelte'; export { default as ChatMessageThinkingBlock } from './chat/ChatMessages/ChatMessageThinkingBlock.svelte'; +export { default as ChatMessageUser } from './chat/ChatMessages/ChatMessageUser.svelte'; export { default as ChatMessages } from './chat/ChatMessages/ChatMessages.svelte'; export { default as MessageBranchingControls } from './chat/ChatMessages/ChatMessageBranchingControls.svelte'; - export { default as ChatScreen } from './chat/ChatScreen/ChatScreen.svelte'; +export { default as ChatScreenDragOverlay } from './chat/ChatScreen/ChatScreenDragOverlay.svelte'; +export { default as ChatScreenForm } from './chat/ChatScreen/ChatScreenForm.svelte'; export { default as ChatScreenHeader } from './chat/ChatScreen/ChatScreenHeader.svelte'; export { default as ChatScreenProcessingInfo } from './chat/ChatScreen/ChatScreenProcessingInfo.svelte'; - export { default as ChatSettings } from './chat/ChatSettings/ChatSettings.svelte'; export { default as ChatSettingsFooter } from './chat/ChatSettings/ChatSettingsFooter.svelte'; export { default as ChatSettingsFields } from './chat/ChatSettings/ChatSettingsFields.svelte'; export { default as ChatSettingsImportExportTab } from './chat/ChatSettings/ChatSettingsImportExportTab.svelte'; export { default as ChatSettingsParameterSourceIndicator } from './chat/ChatSettings/ChatSettingsParameterSourceIndicator.svelte'; - export { default as ChatSidebar } from './chat/ChatSidebar/ChatSidebar.svelte'; +export { default as ChatSidebarActions } from './chat/ChatSidebar/ChatSidebarActions.svelte'; export { default as ChatSidebarConversationItem } from './chat/ChatSidebar/ChatSidebarConversationItem.svelte'; export { default as ChatSidebarSearch } from './chat/ChatSidebar/ChatSidebarSearch.svelte'; // Dialogs - export { default as DialogChatAttachmentPreview } from './dialogs/DialogChatAttachmentPreview.svelte'; export { default as DialogChatAttachmentsViewAll } from './dialogs/DialogChatAttachmentsViewAll.svelte'; export { default as DialogChatError } from './dialogs/DialogChatError.svelte'; export { default as DialogChatSettings } from './dialogs/DialogChatSettings.svelte'; +export { default as DialogCodePreview } from './dialogs/DialogCodePreview.svelte'; export { default as DialogConfirmation } from './dialogs/DialogConfirmation.svelte'; export { default as DialogConversationSelection } from './dialogs/DialogConversationSelection.svelte'; export { default as DialogConversationTitleUpdate } from './dialogs/DialogConversationTitleUpdate.svelte'; @@ -51,25 +61,8 @@ export { default as DialogEmptyFileAlert } from './dialogs/DialogEmptyFileAlert. export { default as DialogModelInformation } from './dialogs/DialogModelInformation.svelte'; export { default as DialogModelNotAvailable } from './dialogs/DialogModelNotAvailable.svelte'; -// Miscellanous - -export { default as ActionButton } from './misc/ActionButton.svelte'; -export { default as ActionDropdown } from './misc/ActionDropdown.svelte'; -export { default as BadgeChatStatistic } from './misc/BadgeChatStatistic.svelte'; -export { default as BadgeInfo } from './misc/BadgeInfo.svelte'; -export { default as ModelBadge } from './models/ModelBadge.svelte'; -export { default as BadgeModality } from './misc/BadgeModality.svelte'; -export { default as ConversationSelection } from './misc/ConversationSelection.svelte'; -export { default as CopyToClipboardIcon } from './misc/CopyToClipboardIcon.svelte'; -export { default as KeyboardShortcutInfo } from './misc/KeyboardShortcutInfo.svelte'; -export { default as MarkdownContent } from './misc/MarkdownContent.svelte'; -export { default as RemoveButton } from './misc/RemoveButton.svelte'; -export { default as SearchInput } from './misc/SearchInput.svelte'; -export { default as SyntaxHighlightedCode } from './misc/SyntaxHighlightedCode.svelte'; -export { default as ModelsSelector } from './models/ModelsSelector.svelte'; - -// Server - -export { default as ServerStatus } from './server/ServerStatus.svelte'; -export { default as ServerErrorSplash } from './server/ServerErrorSplash.svelte'; -export { default as ServerLoadingSplash } from './server/ServerLoadingSplash.svelte'; +// Compatibility aliases +export { default as ActionButton } from './actions/ActionIcon.svelte'; +export { default as ActionDropdown } from './navigation/DropdownMenuActions.svelte'; +export { default as CopyToClipboardIcon } from './actions/ActionIconCopyToClipboard.svelte'; +export { default as RemoveButton } from './actions/ActionIconRemove.svelte'; diff --git a/tools/server/webui/src/lib/components/app/misc/ActionButton.svelte b/tools/server/webui/src/lib/components/app/misc/ActionButton.svelte deleted file mode 100644 index 411a8b609..000000000 --- a/tools/server/webui/src/lib/components/app/misc/ActionButton.svelte +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - -

{tooltip}

-
-
diff --git a/tools/server/webui/src/lib/components/app/misc/ActionDropdown.svelte b/tools/server/webui/src/lib/components/app/misc/ActionDropdown.svelte deleted file mode 100644 index 83d856d10..000000000 --- a/tools/server/webui/src/lib/components/app/misc/ActionDropdown.svelte +++ /dev/null @@ -1,86 +0,0 @@ - - - - e.stopPropagation()} - > - {#if triggerTooltip} - - - {@render iconComponent(triggerIcon, 'h-3 w-3')} - {triggerTooltip} - - -

{triggerTooltip}

-
-
- {:else} - {@render iconComponent(triggerIcon, 'h-3 w-3')} - {/if} -
- - - {#each actions as action, index (action.label)} - {#if action.separator && index > 0} - - {/if} - - -
- {@render iconComponent( - action.icon, - `h-4 w-4 ${action.variant === 'destructive' ? 'text-destructive' : ''}` - )} - {action.label} -
- - {#if action.shortcut} - - {/if} -
- {/each} -
-
- -{#snippet iconComponent(IconComponent: Component, className: string)} - -{/snippet} diff --git a/tools/server/webui/src/lib/components/app/misc/BadgeChatStatistic.svelte b/tools/server/webui/src/lib/components/app/misc/BadgeChatStatistic.svelte deleted file mode 100644 index a2b28d205..000000000 --- a/tools/server/webui/src/lib/components/app/misc/BadgeChatStatistic.svelte +++ /dev/null @@ -1,44 +0,0 @@ - - -{#if tooltipLabel} - - - - {#snippet icon()} - - {/snippet} - - {value} - - - -

{tooltipLabel}

-
-
-{:else} - - {#snippet icon()} - - {/snippet} - - {value} - -{/if} diff --git a/tools/server/webui/src/lib/components/app/misc/BadgeInfo.svelte b/tools/server/webui/src/lib/components/app/misc/BadgeInfo.svelte deleted file mode 100644 index c70af6f42..000000000 --- a/tools/server/webui/src/lib/components/app/misc/BadgeInfo.svelte +++ /dev/null @@ -1,27 +0,0 @@ - - - diff --git a/tools/server/webui/src/lib/components/app/misc/BadgeModality.svelte b/tools/server/webui/src/lib/components/app/misc/BadgeModality.svelte deleted file mode 100644 index a0d5e863c..000000000 --- a/tools/server/webui/src/lib/components/app/misc/BadgeModality.svelte +++ /dev/null @@ -1,39 +0,0 @@ - - -{#each displayableModalities as modality, index (index)} - {@const IconComponent = MODALITY_ICONS[modality]} - {@const label = MODALITY_LABELS[modality]} - - - {#if IconComponent} - - {/if} - - {label} - -{/each} diff --git a/tools/server/webui/src/lib/components/app/misc/CodePreviewDialog.svelte b/tools/server/webui/src/lib/components/app/misc/CodePreviewDialog.svelte deleted file mode 100644 index 702519f9f..000000000 --- a/tools/server/webui/src/lib/components/app/misc/CodePreviewDialog.svelte +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - - - - - - - Close preview - - - - - - diff --git a/tools/server/webui/src/lib/components/app/misc/CopyToClipboardIcon.svelte b/tools/server/webui/src/lib/components/app/misc/CopyToClipboardIcon.svelte deleted file mode 100644 index bf6cd4fb2..000000000 --- a/tools/server/webui/src/lib/components/app/misc/CopyToClipboardIcon.svelte +++ /dev/null @@ -1,18 +0,0 @@ - - - canCopy && copyToClipboard(text)} -/> diff --git a/tools/server/webui/src/lib/components/app/misc/DropdownMenuSearchable.svelte b/tools/server/webui/src/lib/components/app/misc/DropdownMenuSearchable.svelte deleted file mode 100644 index 21ba04cf6..000000000 --- a/tools/server/webui/src/lib/components/app/misc/DropdownMenuSearchable.svelte +++ /dev/null @@ -1,88 +0,0 @@ - - - - { - e.preventDefault(); - e.stopPropagation(); - }} - > - {@render trigger()} - - - -
- -
- -
- {@render children()} - - {#if isEmpty} -
{emptyMessage}
- {/if} -
- - {#if footer} - - - {@render footer()} - {/if} -
-
diff --git a/tools/server/webui/src/lib/components/app/misc/MarkdownContent.svelte b/tools/server/webui/src/lib/components/app/misc/MarkdownContent.svelte deleted file mode 100644 index 0084499f8..000000000 --- a/tools/server/webui/src/lib/components/app/misc/MarkdownContent.svelte +++ /dev/null @@ -1,872 +0,0 @@ - - -
- {#each renderedBlocks as block (block.id)} -
- - {@html block.html} -
- {/each} - - {#if unstableBlockHtml} -
- - {@html unstableBlockHtml} -
- {/if} -
- - - - diff --git a/tools/server/webui/src/lib/components/app/misc/RemoveButton.svelte b/tools/server/webui/src/lib/components/app/misc/RemoveButton.svelte deleted file mode 100644 index 173685510..000000000 --- a/tools/server/webui/src/lib/components/app/misc/RemoveButton.svelte +++ /dev/null @@ -1,26 +0,0 @@ - - - diff --git a/tools/server/webui/src/lib/components/app/misc/SearchInput.svelte b/tools/server/webui/src/lib/components/app/misc/SearchInput.svelte deleted file mode 100644 index 15cd6abaa..000000000 --- a/tools/server/webui/src/lib/components/app/misc/SearchInput.svelte +++ /dev/null @@ -1,73 +0,0 @@ - - -
- - - - - {#if showClearButton} - - {/if} -
diff --git a/tools/server/webui/src/lib/components/app/misc/SyntaxHighlightedCode.svelte b/tools/server/webui/src/lib/components/app/misc/SyntaxHighlightedCode.svelte deleted file mode 100644 index bc42f9dd1..000000000 --- a/tools/server/webui/src/lib/components/app/misc/SyntaxHighlightedCode.svelte +++ /dev/null @@ -1,97 +0,0 @@ - - -
- -
{@html highlightedHtml}
-
- - diff --git a/tools/server/webui/src/lib/components/app/models/ModelBadge.svelte b/tools/server/webui/src/lib/components/app/models/ModelBadge.svelte index bea1bf6e3..f98ba7d78 100644 --- a/tools/server/webui/src/lib/components/app/models/ModelBadge.svelte +++ b/tools/server/webui/src/lib/components/app/models/ModelBadge.svelte @@ -1,6 +1,6 @@ - - { - const textarea = await canvas.findByRole('textbox'); - const submitButton = await canvas.findByRole('button', { name: 'Send' }); - - // Expect the input to be focused after the component is mounted - await expect(textarea).toHaveFocus(); - - // Expect the submit button to be disabled - await expect(submitButton).toBeDisabled(); - - const text = 'What is the meaning of life?'; - - await userEvent.clear(textarea); - await userEvent.type(textarea, text); - - await expect(textarea).toHaveValue(text); - - const fileInput = document.querySelector('input[type="file"]'); - await expect(fileInput).not.toHaveAttribute('accept'); - }} -/> - - - - { - const jpgAttachment = canvas.getByAltText('1.jpg'); - const svgAttachment = canvas.getByAltText('hf-logo.svg'); - const pdfFileExtension = canvas.getByText('PDF'); - const pdfAttachment = canvas.getByText('example.pdf'); - const pdfSize = canvas.getByText('342.82 KB'); - - await expect(jpgAttachment).toBeInTheDocument(); - await expect(jpgAttachment).toHaveAttribute('src', jpgAsset); - - await expect(svgAttachment).toBeInTheDocument(); - await expect(svgAttachment).toHaveAttribute('src', svgAsset); - - await expect(pdfFileExtension).toBeInTheDocument(); - await expect(pdfAttachment).toBeInTheDocument(); - await expect(pdfSize).toBeInTheDocument(); - }} -/> diff --git a/tools/server/webui/tests/stories/ChatScreenForm.stories.svelte b/tools/server/webui/tests/stories/ChatScreenForm.stories.svelte new file mode 100644 index 000000000..f3cbde217 --- /dev/null +++ b/tools/server/webui/tests/stories/ChatScreenForm.stories.svelte @@ -0,0 +1,96 @@ + + + { + const { canvas, userEvent } = context; + const textarea = await canvas.findByRole('textbox'); + const submitButton = await canvas.findByRole('button', { name: 'Send' }); + + // Expect the input to be focused after the component is mounted + await expect(textarea).toHaveFocus(); + + // Expect the submit button to be disabled + await expect(submitButton).toBeDisabled(); + + const text = 'What is the meaning of life?'; + + await userEvent.clear(textarea); + await userEvent.type(textarea, text); + + await expect(textarea).toHaveValue(text); + + const fileInput = document.querySelector('input[type="file"]'); + await expect(fileInput).not.toHaveAttribute('accept'); + }} +/> + + + + { + const { canvas } = context; + const jpgAttachment = canvas.getByAltText('1.jpg'); + const svgAttachment = canvas.getByAltText('hf-logo.svg'); + const pdfFileExtension = canvas.getByText('PDF'); + const pdfAttachment = canvas.getByText('example.pdf'); + const pdfSize = canvas.getByText('342.82 KB'); + + await expect(jpgAttachment).toBeInTheDocument(); + await expect(jpgAttachment).toHaveAttribute('src', jpgAsset); + + await expect(svgAttachment).toBeInTheDocument(); + await expect(svgAttachment).toHaveAttribute('src', svgAsset); + + await expect(pdfFileExtension).toBeInTheDocument(); + await expect(pdfAttachment).toBeInTheDocument(); + await expect(pdfSize).toBeInTheDocument(); + }} +/> diff --git a/tools/server/webui/tests/stories/MarkdownContent.stories.svelte b/tools/server/webui/tests/stories/MarkdownContent.stories.svelte index 90aa90bb0..04f270a43 100644 --- a/tools/server/webui/tests/stories/MarkdownContent.stories.svelte +++ b/tools/server/webui/tests/stories/MarkdownContent.stories.svelte @@ -68,18 +68,22 @@ You can also test inline links like https://example.com or https://docs.python.o All links should have \`target="_blank"\` and \`rel="noopener noreferrer"\` attributes for security.`, class: 'max-w-[56rem] w-[calc(100vw-2rem)]' }} - play={async ({ canvasElement }) => { + play={async (context) => { + const { canvasElement } = context; // Wait for component to render await new Promise((resolve) => setTimeout(resolve, 100)); // Find all links in the rendered content - const links = canvasElement.querySelectorAll('a[href]'); + const links = (canvasElement as HTMLElement).querySelectorAll( + 'a[href]' + ) as NodeListOf; + const linkList = Array.from(links) as HTMLAnchorElement[]; // Test that we have the expected number of links expect(links.length).toBeGreaterThan(0); // Test each link for proper attributes - links.forEach((link) => { + links.forEach((link: HTMLAnchorElement) => { const href = link.getAttribute('href'); // Test that external links have proper security attributes @@ -90,37 +94,35 @@ All links should have \`target="_blank"\` and \`rel="noopener noreferrer"\` attr }); // Test specific links exist - const hugginFaceLink = Array.from(links).find( + const hugginFaceLink = linkList.find( (link) => link.getAttribute('href') === 'https://huggingface.co' ); expect(hugginFaceLink).toBeTruthy(); expect(hugginFaceLink?.textContent).toBe('Hugging Face Homepage'); - const githubLink = Array.from(links).find( + const githubLink = linkList.find( (link) => link.getAttribute('href') === 'https://github.com/ggml-org/llama.cpp' ); expect(githubLink).toBeTruthy(); expect(githubLink?.textContent).toBe('GitHub Repository'); - const openaiLink = Array.from(links).find( - (link) => link.getAttribute('href') === 'https://openai.com' - ); + const openaiLink = linkList.find((link) => link.getAttribute('href') === 'https://openai.com'); expect(openaiLink).toBeTruthy(); expect(openaiLink?.textContent).toBe('OpenAI Website'); - const googleLink = Array.from(links).find( + const googleLink = linkList.find( (link) => link.getAttribute('href') === 'https://www.google.com' ); expect(googleLink).toBeTruthy(); expect(googleLink?.textContent).toBe('Google Search'); // Test inline links (auto-linked URLs) - const exampleLink = Array.from(links).find( + const exampleLink = linkList.find( (link) => link.getAttribute('href') === 'https://example.com' ); expect(exampleLink).toBeTruthy(); - const pythonDocsLink = Array.from(links).find( + const pythonDocsLink = linkList.find( (link) => link.getAttribute('href') === 'https://docs.python.org' ); expect(pythonDocsLink).toBeTruthy(); diff --git a/tools/server/webui/vite.config.ts b/tools/server/webui/vite.config.ts index 5183c09fc..e4408f09e 100644 --- a/tools/server/webui/vite.config.ts +++ b/tools/server/webui/vite.config.ts @@ -2,11 +2,15 @@ import tailwindcss from '@tailwindcss/vite'; import { sveltekit } from '@sveltejs/kit/vite'; import * as fflate from 'fflate'; import { readFileSync, writeFileSync, existsSync } from 'fs'; -import { resolve } from 'path'; -import { defineConfig } from 'vite'; +import { dirname, resolve } from 'path'; +import { fileURLToPath } from 'url'; + +import { defineConfig, searchForWorkspaceRoot } from 'vite'; import devtoolsJson from 'vite-plugin-devtools-json'; import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; +const __dirname = dirname(fileURLToPath(import.meta.url)); + const GUIDE_FOR_FRONTEND = `