diff --git a/common/arg.cpp b/common/arg.cpp index 2df446d9d6c6..feffb4c12ef8 100644 --- a/common/arg.cpp +++ b/common/arg.cpp @@ -4077,7 +4077,6 @@ common_params_context common_params_parser_init(common_params & params, llama_ex params.sampling.top_k = 0; params.sampling.min_p = 0.01f; params.use_jinja = true; - //params.default_template_kwargs["reasoning_effort"] = "\"high\""; } ).set_examples({LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI})); @@ -4096,7 +4095,6 @@ common_params_context common_params_parser_init(common_params & params, llama_ex params.sampling.top_k = 0; params.sampling.min_p = 0.01f; params.use_jinja = true; - //params.default_template_kwargs["reasoning_effort"] = "\"high\""; } ).set_examples({LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI})); diff --git a/tools/ui/.env.example b/tools/ui/.env.example index 9a995b746f65..a1e6c54915ba 100644 --- a/tools/ui/.env.example +++ b/tools/ui/.env.example @@ -1,2 +1,3 @@ VITE_PUBLIC_APP_NAME='llama-ui' -# VITE_DEBUG='true' +VITE_DEBUG='true' +VITE_PUBLIC_SERVER_ORIGIN='http://localhost:8033' diff --git a/tools/ui/.prettierignore b/tools/ui/.prettierignore index 14b01e961bd2..7bbdcf6a0963 100644 --- a/tools/ui/.prettierignore +++ b/tools/ui/.prettierignore @@ -7,6 +7,9 @@ bun.lockb # Miscellaneous /static/ +dist/ +.svelte-kit/ +build/ # Build output /dist/ diff --git a/tools/ui/README.md b/tools/ui/README.md index 4334f968162d..1b99ebbfe806 100644 --- a/tools/ui/README.md +++ b/tools/ui/README.md @@ -115,16 +115,18 @@ This starts: - **Vite dev server** at `http://localhost:5173` - The main UI frontend app - **Storybook** at `http://localhost:6006` - Component documentation -The Vite dev server proxies API requests to `http://localhost:8080` (default llama-server port): +The Vite dev server proxies API requests to `SERVER_ORIGIN` (with fallback to default llama-server `8080` port): ```typescript // vite.config.ts proxy configuration proxy: { - '/v1': 'http://localhost:8080', - '/props': 'http://localhost:8080', - '/slots': 'http://localhost:8080', - '/models': 'http://localhost:8080' -} + '/v1': SERVER_ORIGIN, + '/props': SERVER_ORIGIN, + '/models': SERVER_ORIGIN, + '/tools': SERVER_ORIGIN, + '/slots': SERVER_ORIGIN, + '/cors-proxy': SERVER_ORIGIN +}, ``` ### Development Workflow diff --git a/tools/ui/playwright.config.ts b/tools/ui/playwright.config.ts index 178fd7ba856b..460379238591 100644 --- a/tools/ui/playwright.config.ts +++ b/tools/ui/playwright.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from '@playwright/test'; export default defineConfig({ webServer: { - command: 'npm run build && http-server ../../build/tools/ui/dist -p 8181', + command: 'npm run build && npx http-server ./dist -p 8181', port: 8181, timeout: 120000, reuseExistingServer: false diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatForm.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatForm.svelte index 99faecb6b412..ed26f9ea58e4 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatForm.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatForm.svelte @@ -111,6 +111,7 @@ let preSelectedResourceUri = $state(undefined); let currentConfig = $derived(config()); + let pasteLongTextToFileLength = $derived.by(() => { const n = Number(currentConfig.pasteLongTextToFileLen); return Number.isNaN(n) ? Number(SETTING_CONFIG_DEFAULT.pasteLongTextToFileLen) : n; diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddDropdown.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddDropdown.svelte index 0a8260a2952b..47954032117a 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddDropdown.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddDropdown.svelte @@ -1,22 +1,18 @@
{@render trigger({ disabled, onclick: () => (sheetOpen = true) })} - @@ -80,110 +92,206 @@
- {#each ATTACHMENT_FILE_ITEMS as item (item.id)} - {@const enabled = attachmentMenu.isItemEnabled(item.enabledWhen)} - {#if enabled} - - {:else if item.disabledTooltip} - - - - - - -

{item.disabledTooltip}

-
-
- {/if} - {/each} - - {#if !attachmentMenu.isItemEnabled('hasVisionModality')} - {@const pdfItem = ATTACHMENT_FILE_ITEMS.find((i) => i.id === AttachmentMenuItemId.PDF)} - {#if pdfItem} - - + (filesExpanded = open)}> + + {#if filesExpanded} + + {:else} + + {/if} + + + + Add files + + + +
+ {#each ATTACHMENT_FILE_ITEMS as item (item.id)} + {@const enabled = attachmentMenu.isItemEnabled(item.enabledWhen)} + {#if enabled} + + {:else if item.disabledTooltip} + + + + + + +

{item.disabledTooltip}

+
+
+ {/if} + {/each} +
+
+
+ + (mcpExpanded = open)}> + + {#if mcpExpanded} + + {:else} + + {/if} + + + + MCP Servers + + + {getEnabledMcpServers().length} server{getEnabledMcpServers().length !== 1 ? 's' : ''} + + + + +
+ {#each getEnabledMcpServers() as server (server.id)} + {@const healthState = mcpStore.getHealthCheckState(server.id)} + {@const hasError = healthState.status === HealthCheckStatus.ERROR} + {@const displayName = mcpStore.getServerLabel(server)} + {@const faviconUrl = mcpStore.getServerFavicon(server.id)} + {@const isEnabled = conversationsStore.isMcpServerEnabledForChat(server.id)} + - + {/each} - -

PDFs will be converted to text. Image-based PDFs may not work properly.

-
- - {/if} + {#if getEnabledMcpServers().length === 0} +
+ No MCP servers configured +
+ {/if} +
+
+
+ + {#if toolsPanel.totalToolCount > 0} + (toolsExpanded = open)}> + + {#if toolsExpanded} + + {:else} + + {/if} + + + + Tools + + + {toolsPanel.totalToolCount} tool{toolsPanel.totalToolCount !== 1 ? 's' : ''} + + + + +
+ {#each toolsPanel.activeGroups as group (group.label)} + {@const { checked, indeterminate } = toolsPanel.getGroupCheckedState(group)} + {@const enabledCount = toolsPanel.getEnabledToolCount(group)} + {@const favicon = toolsPanel.getFavicon(group)} + + + {/each} +
+
+
{/if} - {#each ATTACHMENT_EXTRA_ITEMS as item (item.id)} - {#if item.id === AttachmentMenuItemId.SYSTEM_MESSAGE} - - - - - - -

{attachmentMenu.getSystemMessageTooltip()}

-
-
- {/if} - {/each} - -
- - - - - MCP Servers - - - - - - Tools - - - {#each ATTACHMENT_MCP_ITEMS as item (item.id)} - {#if attachmentMenu.isItemVisible(item.visibleWhen)} - - {/if} - {/each} + System Message + + + {#if hasMcpPromptsSupport} + + {/if} + + {#if hasMcpResourcesSupport} + + {/if}
diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte index b11467da8236..813227fbce08 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte @@ -107,7 +107,7 @@ toolsStore.toggleGroup(group)} + onCheckedChange={() => toolsPanel.toggleGroupByLabel(group.label)} class="mr-2 h-4 w-4 shrink-0" /> diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionModels.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionModels.svelte index fd866b243eac..712326cba608 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionModels.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionModels.svelte @@ -1,6 +1,11 @@ + +{#if modelSupportsThinking} + + + {#if thinkingEnabled} + + {:else} + + {/if} + + Thinking + + {#if thinkingEnabled} + {currentEffort} + {:else} + off + {/if} + + + + {#each REASONING_EFFORT_LEVELS as level (level.value)} + + {/each} + + +{/if} diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormReasoningToggle.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormReasoningToggle.svelte new file mode 100644 index 000000000000..f6bcbcb09e35 --- /dev/null +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormReasoningToggle.svelte @@ -0,0 +1,145 @@ + + +{#if modelSupportsThinking} + + + + + {#if thinkingEnabled} + + {:else} + + {/if} + + + + +

{tooltipText}

+
+
+ + +
Reasoning effort
+ + {#each REASONING_EFFORT_LEVELS as level (level.value)} + + {/each} +
+
+{/if} diff --git a/tools/ui/src/lib/components/app/chat/index.ts b/tools/ui/src/lib/components/app/chat/index.ts index be5535960247..8ed3cc65ec80 100644 --- a/tools/ui/src/lib/components/app/chat/index.ts +++ b/tools/ui/src/lib/components/app/chat/index.ts @@ -240,6 +240,15 @@ export { default as ChatFormActionAddToolsSubmenu } from './ChatForm/ChatFormAct */ export { default as ChatFormActionAddMcpServersSubmenu } from './ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddMcpServersSubmenu.svelte'; +/** + * **ChatFormReasoningToggle** - Thinking toggle button with effort dropdown + * + * A toggle button with lightbulb icon that indicates thinking status. + * Shows the reasoning effort dropdown when clicked. + * Only visible when the current model supports thinking. + */ +export { default as ChatFormReasoningToggle } from './ChatForm/ChatFormActions/ChatFormReasoningToggle.svelte'; + /** * Hidden file input element for programmatic file selection. */ diff --git a/tools/ui/src/lib/components/app/models/ModelId.svelte b/tools/ui/src/lib/components/app/models/ModelId.svelte index 2fe952feea06..f566b55ee88b 100644 --- a/tools/ui/src/lib/components/app/models/ModelId.svelte +++ b/tools/ui/src/lib/components/app/models/ModelId.svelte @@ -8,6 +8,7 @@ hideOrgName?: boolean; showRaw?: boolean; hideQuantization?: boolean; + hideTags?: boolean; aliases?: string[]; tags?: string[]; class?: string; @@ -17,7 +18,8 @@ modelId, hideOrgName = false, showRaw = undefined, - hideQuantization = false, + hideQuantization, + hideTags, aliases, tags, class: className = '', @@ -31,6 +33,8 @@ let parsed = $derived(ModelsService.parseModelId(modelId)); let resolvedShowRaw = $derived(showRaw ?? (config().showRawModelNames as boolean) ?? false); + let resolvedHideQuantization = $derived(hideQuantization ?? !config().showModelQuantization); + let resolvedHideTags = $derived(hideTags ?? !config().showModelTags); let uniqueAliases = $derived([...new Set(aliases ?? [])]); let uniqueTags = $derived([...new Set([...(parsed.tags ?? []), ...(tags ?? [])])]); @@ -53,7 +57,7 @@ {/if} - {#if parsed.quantization && !hideQuantization} + {#if parsed.quantization && !resolvedHideQuantization} {parsed.quantization} @@ -69,7 +73,7 @@ {/each} {/if} - {#if uniqueTags.length > 0} + {#if uniqueTags.length > 0 && !resolvedHideTags} {#each uniqueTags as tag (tag)} {tag} {/each} diff --git a/tools/ui/src/lib/components/app/models/ModelsSelectorDropdown.svelte b/tools/ui/src/lib/components/app/models/ModelsSelectorDropdown.svelte index 998a6a0faaad..0f1fba880979 100644 --- a/tools/ui/src/lib/components/app/models/ModelsSelectorDropdown.svelte +++ b/tools/ui/src/lib/components/app/models/ModelsSelectorDropdown.svelte @@ -106,9 +106,7 @@ ]} style="max-width: min(calc(100cqw - 10rem), 20rem)" > - - - + {:else}

No models available.

@@ -120,7 +118,7 @@ - + {#if selectedOption} @@ -144,6 +142,7 @@ modelId={selectedOption.model} class="min-w-0 overflow-hidden" hideOrgName={false} + hideQuantization {...props} /> {/snippet} @@ -158,9 +157,9 @@ {/if} {#if ms.updating || ms.isLoadingModel} - + {:else} - + {/if} @@ -251,7 +250,7 @@ onclick={() => ms.handleOpenChange(true)} disabled={disabled || ms.updating} > - + {#if selectedOption} @@ -262,6 +261,7 @@ modelId={selectedOption.model} class="min-w-0 overflow-hidden" hideOrgName={false} + hideQuantization {...props} /> {/snippet} @@ -274,7 +274,7 @@ {/if} {#if ms.updating} - + {/if} {/if} diff --git a/tools/ui/src/lib/components/app/models/ModelsSelectorSheet.svelte b/tools/ui/src/lib/components/app/models/ModelsSelectorSheet.svelte index d38ed8c0752f..2ddbf24055b8 100644 --- a/tools/ui/src/lib/components/app/models/ModelsSelectorSheet.svelte +++ b/tools/ui/src/lib/components/app/models/ModelsSelectorSheet.svelte @@ -6,8 +6,7 @@ DialogModelInformation, ModelId, ModelsSelectorList, - SearchInput, - TruncatedText + SearchInput } from '$lib/components/app'; interface Props { @@ -67,7 +66,7 @@ @@ -168,12 +168,12 @@ onclick={() => ms.handleOpenChange(true)} disabled={disabled || ms.updating} > - + - + {#if ms.updating} - + {/if} {/if} diff --git a/tools/ui/src/lib/components/app/models/index.ts b/tools/ui/src/lib/components/app/models/index.ts index a6ba6817fc08..3ac6ecb678b2 100644 --- a/tools/ui/src/lib/components/app/models/index.ts +++ b/tools/ui/src/lib/components/app/models/index.ts @@ -74,8 +74,7 @@ export { default as ModelsSelectorOption } from './ModelsSelectorOption.svelte'; */ export { default as ModelsSelectorSheet } from './ModelsSelectorSheet.svelte'; -/** - * **ModelBadge** - Model name display badge +/** * **ModelBadge** - Model name display badge * * Compact badge showing current model name with package icon. * Only visible in single model mode. Supports tooltip and copy functionality. diff --git a/tools/ui/src/lib/constants/attachment-menu.ts b/tools/ui/src/lib/constants/attachment-menu.ts index 966f46f41d08..3d7381812e5d 100644 --- a/tools/ui/src/lib/constants/attachment-menu.ts +++ b/tools/ui/src/lib/constants/attachment-menu.ts @@ -79,7 +79,9 @@ export const ATTACHMENT_FILE_ITEMS: AttachmentMenuItem[] = [ } ]; -export const ATTACHMENT_EXTRA_ITEMS: AttachmentMenuItem[] = [ +export const ATTACHMENT_EXTRA_ITEMS: AttachmentMenuItem[] = []; + +export const ATTACHMENT_PROMPT_ITEMS: AttachmentMenuItem[] = [ { id: AttachmentMenuItemId.SYSTEM_MESSAGE, label: 'System Message', @@ -87,10 +89,7 @@ export const ATTACHMENT_EXTRA_ITEMS: AttachmentMenuItem[] = [ enabledWhen: AttachmentItemEnabledWhen.ALWAYS, hasEnabledTooltip: true, action: AttachmentAction.SYSTEM_PROMPT_CLICK - } -]; - -export const ATTACHMENT_MCP_ITEMS: AttachmentMenuItem[] = [ + }, { id: AttachmentMenuItemId.MCP_PROMPT, label: 'MCP Prompt', @@ -98,7 +97,10 @@ export const ATTACHMENT_MCP_ITEMS: AttachmentMenuItem[] = [ enabledWhen: AttachmentItemEnabledWhen.ALWAYS, action: AttachmentAction.MCP_PROMPT_CLICK, visibleWhen: AttachmentItemVisibleWhen.HAS_MCP_PROMPTS_SUPPORT - }, + } +]; + +export const ATTACHMENT_MCP_ITEMS: AttachmentMenuItem[] = [ { id: AttachmentMenuItemId.MCP_RESOURCES, label: 'MCP Resources', diff --git a/tools/ui/src/lib/constants/index.ts b/tools/ui/src/lib/constants/index.ts index eb85910370fb..54f39feee7b4 100644 --- a/tools/ui/src/lib/constants/index.ts +++ b/tools/ui/src/lib/constants/index.ts @@ -5,6 +5,8 @@ export * from './agentic'; export * from './api-endpoints'; export * from './attachment-labels'; export * from './database'; +export * from './reasoning-effort'; +export * from './reasoning-effort-tokens'; export * from './storage'; export * from './attachment-menu'; export * from './auto-scroll'; diff --git a/tools/ui/src/lib/constants/reasoning-effort-tokens.ts b/tools/ui/src/lib/constants/reasoning-effort-tokens.ts new file mode 100644 index 000000000000..059af71dea00 --- /dev/null +++ b/tools/ui/src/lib/constants/reasoning-effort-tokens.ts @@ -0,0 +1,12 @@ +import { ReasoningEffort } from '$lib/enums'; + +/** + * Reasoning effort to token budget mapping. + * Maps the ReasoningEffort enum values to concrete token counts for the server. + */ +export const REASONING_EFFORT_TOKENS: Record = { + [ReasoningEffort.LOW]: 512, + [ReasoningEffort.MEDIUM]: 2048, + [ReasoningEffort.HIGH]: 8192, + [ReasoningEffort.MAX]: -1 // unlimited +}; diff --git a/tools/ui/src/lib/constants/reasoning-effort.ts b/tools/ui/src/lib/constants/reasoning-effort.ts new file mode 100644 index 000000000000..d854e912a851 --- /dev/null +++ b/tools/ui/src/lib/constants/reasoning-effort.ts @@ -0,0 +1,21 @@ +import { ReasoningEffort } from '$lib/enums'; +import type { ReasoningEffortLevel } from '$lib/types'; + +/** + * Reasoning effort UI labels. + * Keys match the ReasoningEffort enum values for type-safe lookups. + */ +export const REASONING_EFFORT_LABELS: Record = { + [ReasoningEffort.LOW]: 'Low', + [ReasoningEffort.MEDIUM]: 'Medium', + [ReasoningEffort.HIGH]: 'High', + [ReasoningEffort.MAX]: 'Max' +}; + +export const REASONING_EFFORT_LEVELS: ReasoningEffortLevel[] = [ + { value: 'off', label: 'Off', isOff: true }, + { value: ReasoningEffort.LOW, label: 'Low' }, + { value: ReasoningEffort.MEDIUM, label: 'Medium' }, + { value: ReasoningEffort.HIGH, label: 'High' }, + { value: ReasoningEffort.MAX, label: 'Max', hasInfo: true } +]; diff --git a/tools/ui/src/lib/constants/settings-keys.ts b/tools/ui/src/lib/constants/settings-keys.ts index 7cdd2db7c4d7..5fff9f94c2ca 100644 --- a/tools/ui/src/lib/constants/settings-keys.ts +++ b/tools/ui/src/lib/constants/settings-keys.ts @@ -29,6 +29,8 @@ export const SETTINGS_KEYS = { ALWAYS_SHOW_SIDEBAR_ON_DESKTOP: 'alwaysShowSidebarOnDesktop', FULL_HEIGHT_CODE_BLOCKS: 'fullHeightCodeBlocks', SHOW_RAW_MODEL_NAMES: 'showRawModelNames', + SHOW_MODEL_QUANTIZATION: 'showModelQuantization', + SHOW_MODEL_TAGS: 'showModelTags', SHOW_SYSTEM_MESSAGE: 'showSystemMessage', // Sampling TEMPERATURE: 'temperature', @@ -64,6 +66,7 @@ export const SETTINGS_KEYS = { // Developer DISABLE_REASONING_PARSING: 'disableReasoningParsing', EXCLUDE_REASONING_FROM_CONTEXT: 'excludeReasoningFromContext', + ENABLE_THINKING: 'enableThinking', SHOW_RAW_OUTPUT_SWITCH: 'showRawOutputSwitch', // PY_INTERPRETER_ENABLED: 'pyInterpreterEnabled', CUSTOM_JSON: 'customJson', diff --git a/tools/ui/src/lib/constants/settings-registry.ts b/tools/ui/src/lib/constants/settings-registry.ts index 20ac33c85f80..9246b970335a 100644 --- a/tools/ui/src/lib/constants/settings-registry.ts +++ b/tools/ui/src/lib/constants/settings-registry.ts @@ -330,6 +330,30 @@ const SETTINGS_REGISTRY: Record = { paramType: SyncableParameterType.BOOLEAN } }, + { + key: SETTINGS_KEYS.SHOW_MODEL_QUANTIZATION, + label: 'Show model quantization information', + help: 'Display quantization badges (e.g. Q8_0, Q4_K_M) next to model names throughout the interface.', + defaultValue: true, + type: SettingsFieldType.CHECKBOX, + section: SETTINGS_SECTION_SLUGS.DISPLAY, + sync: { + serverKey: SETTINGS_KEYS.SHOW_MODEL_QUANTIZATION, + paramType: SyncableParameterType.BOOLEAN + } + }, + { + key: SETTINGS_KEYS.SHOW_MODEL_TAGS, + label: 'Show model tags', + help: 'Display model tags (e.g. "vision", "reasoning") next to model names throughout the interface.', + defaultValue: true, + type: SettingsFieldType.CHECKBOX, + section: SETTINGS_SECTION_SLUGS.DISPLAY, + sync: { + serverKey: SETTINGS_KEYS.SHOW_MODEL_TAGS, + paramType: SyncableParameterType.BOOLEAN + } + }, { key: SETTINGS_KEYS.ALWAYS_SHOW_AGENTIC_TURNS, label: 'Always show agentic turns in conversation', @@ -646,6 +670,14 @@ const SETTINGS_REGISTRY: Record = { paramType: SyncableParameterType.BOOLEAN } }, + { + key: SETTINGS_KEYS.ENABLE_THINKING, + label: 'Enable thinking', + help: 'Enable model thinking/reasoning for each request. When off, the model will skip the thinking phase and go straight to the response.', + defaultValue: false, + type: SettingsFieldType.CHECKBOX, + section: SETTINGS_SECTION_SLUGS.DEVELOPER + }, { key: SETTINGS_KEYS.SHOW_RAW_OUTPUT_SWITCH, label: 'Enable raw output toggle', diff --git a/tools/ui/src/lib/constants/storage.ts b/tools/ui/src/lib/constants/storage.ts index d03254b9ccb1..5d33e82f30d0 100644 --- a/tools/ui/src/lib/constants/storage.ts +++ b/tools/ui/src/lib/constants/storage.ts @@ -19,6 +19,8 @@ export const CONFIG_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.config`; export const DISABLED_TOOLS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.disabledTools`; export const FAVORITE_MODELS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.favoriteModels`; export const MCP_DEFAULT_ENABLED_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.mcpDefaultEnabled`; +export const THINKING_ENABLED_DEFAULT_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.thinkingEnabledDefault`; +export const REASONING_EFFORT_DEFAULT_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.reasoningEffortDefault`; export const USER_OVERRIDES_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.userOverrides`; // Deprecated old key names (kept for backward compat while users migrate) diff --git a/tools/ui/src/lib/enums/index.ts b/tools/ui/src/lib/enums/index.ts index b80b5b61ed85..7438a76429f2 100644 --- a/tools/ui/src/lib/enums/index.ts +++ b/tools/ui/src/lib/enums/index.ts @@ -19,6 +19,8 @@ export { ReasoningFormat } from './chat.enums'; +export { ReasoningEffort } from './reasoning-effort.enums'; + export { FileTypeCategory, FileTypeImage, diff --git a/tools/ui/src/lib/enums/reasoning-effort.enums.ts b/tools/ui/src/lib/enums/reasoning-effort.enums.ts new file mode 100644 index 000000000000..dadb0c7264e8 --- /dev/null +++ b/tools/ui/src/lib/enums/reasoning-effort.enums.ts @@ -0,0 +1,10 @@ +/** + * Reasoning effort levels for thinking models. + * These values are sent to the server and mapped to token budgets. + */ +export enum ReasoningEffort { + LOW = 'low', + MEDIUM = 'medium', + HIGH = 'high', + MAX = 'max' +} diff --git a/tools/ui/src/lib/hooks/use-tools-panel.svelte.ts b/tools/ui/src/lib/hooks/use-tools-panel.svelte.ts index 911af322a7f6..9a8acec0fa74 100644 --- a/tools/ui/src/lib/hooks/use-tools-panel.svelte.ts +++ b/tools/ui/src/lib/hooks/use-tools-panel.svelte.ts @@ -17,6 +17,8 @@ export interface UseToolsPanelReturn { getFavicon(group: { source: ToolSource; label: string }): string | null; isGroupDisabled(group: ToolGroup): boolean; toggleGroupExpanded(label: string): void; + /** Toggle all tools in a group by label (avoids stale group object references). */ + toggleGroupByLabel(label: string): void; handleOpen(): void; } @@ -91,6 +93,13 @@ export function useToolsPanel(): UseToolsPanelReturn { } } + function toggleGroupByLabel(label: string): void { + // Find current group by label to get up-to-date tool references + const group = activeGroups.find((g) => g.label === label); + if (!group) return; + toolsStore.toggleGroup(group); + } + function handleOpen(): void { if (toolsStore.builtinTools.length === 0 && !toolsStore.loading) { toolsStore.fetchBuiltinTools(); @@ -117,6 +126,7 @@ export function useToolsPanel(): UseToolsPanelReturn { getFavicon, isGroupDisabled, toggleGroupExpanded, + toggleGroupByLabel, handleOpen }; } diff --git a/tools/ui/src/lib/services/chat.service.ts b/tools/ui/src/lib/services/chat.service.ts index 09d616bd76aa..468009a54c86 100644 --- a/tools/ui/src/lib/services/chat.service.ts +++ b/tools/ui/src/lib/services/chat.service.ts @@ -6,6 +6,7 @@ import { ATTACHMENT_LABEL_MCP_PROMPT, ATTACHMENT_LABEL_MCP_RESOURCE, LEGACY_AGENTIC_REGEX, + REASONING_EFFORT_TOKENS, SETTINGS_KEYS, API_CHAT, API_SLOTS, @@ -162,6 +163,8 @@ export class ChatService { // Config options disableReasoningParsing, excludeReasoningFromContext, + enableThinking, + reasoningEffort, continueFinalMessage } = options; @@ -243,6 +246,18 @@ export class ChatService { ? ReasoningFormat.NONE : ReasoningFormat.AUTO; + const reasoningBudgetTokens = + enableThinking && reasoningEffort ? (REASONING_EFFORT_TOKENS[reasoningEffort] ?? -1) : -1; + + requestBody.chat_template_kwargs = { + ...(requestBody.chat_template_kwargs ?? {}), + enable_thinking: enableThinking + }; + + if (reasoningBudgetTokens >= 0) { + requestBody.thinking_budget_tokens = reasoningBudgetTokens; + } + // arms the budget sampler so reasoning can be ended at runtime via the control endpoint requestBody.reasoning_control = true; diff --git a/tools/ui/src/lib/stores/chat.svelte.ts b/tools/ui/src/lib/stores/chat.svelte.ts index 667fb417c31e..b899130e50a2 100644 --- a/tools/ui/src/lib/stores/chat.svelte.ts +++ b/tools/ui/src/lib/stores/chat.svelte.ts @@ -1852,6 +1852,9 @@ class ChatStore { if (currentConfig.excludeReasoningFromContext) apiOptions.excludeReasoningFromContext = true; + apiOptions.enableThinking = conversationsStore.getThinkingEnabled(); + apiOptions.reasoningEffort = conversationsStore.getReasoningEffort(); + if (hasValue(currentConfig.temperature)) apiOptions.temperature = Number(currentConfig.temperature); diff --git a/tools/ui/src/lib/stores/conversations.svelte.ts b/tools/ui/src/lib/stores/conversations.svelte.ts index d6589232f0f4..cab6f59faebd 100644 --- a/tools/ui/src/lib/stores/conversations.svelte.ts +++ b/tools/ui/src/lib/stores/conversations.svelte.ts @@ -26,7 +26,7 @@ import { MigrationService } from '$lib/services/migration.service'; import { config } from '$lib/stores/settings.svelte'; import { filterByLeafNodeId, findLeafNode, generateConversationTitle } from '$lib/utils'; import type { McpServerOverride } from '$lib/types/database'; -import { MessageRole, HtmlInputType, FileExtensionText } from '$lib/enums'; +import { MessageRole, HtmlInputType, FileExtensionText, ReasoningEffort } from '$lib/enums'; import { ISO_DATE_TIME_SEPARATOR, ISO_DATE_TIME_SEPARATOR_REPLACEMENT, @@ -38,7 +38,9 @@ import { ISO_TIME_SEPARATOR_REPLACEMENT, NON_ALPHANUMERIC_REGEX, MULTIPLE_UNDERSCORE_REGEX, - MCP_DEFAULT_ENABLED_LOCALSTORAGE_KEY + MCP_DEFAULT_ENABLED_LOCALSTORAGE_KEY, + THINKING_ENABLED_DEFAULT_LOCALSTORAGE_KEY, + REASONING_EFFORT_DEFAULT_LOCALSTORAGE_KEY } from '$lib/constants'; import { ROUTES } from '$lib/constants/routes'; @@ -74,6 +76,12 @@ class ConversationsStore { /** Pending MCP server overrides for new conversations (before first message) */ pendingMcpServerOverrides = $state(ConversationsStore.loadMcpDefaults()); + /** Global (non-conversation-specific) thinking toggle default */ + pendingThinkingEnabled = $state(ConversationsStore.loadThinkingDefaults()); + + /** Global (non-conversation-specific) reasoning effort default */ + pendingReasoningEffort = $state(ConversationsStore.loadReasoningEffortDefault()); + /** Load MCP default overrides from localStorage */ private static loadMcpDefaults(): McpServerOverride[] { if (typeof globalThis.localStorage === 'undefined') return []; @@ -104,6 +112,45 @@ class ConversationsStore { } } + /** Load thinking-enabled default from localStorage */ + private static loadThinkingDefaults(): boolean { + if (typeof globalThis.localStorage === 'undefined') return false; + try { + const raw = localStorage.getItem(THINKING_ENABLED_DEFAULT_LOCALSTORAGE_KEY); + if (!raw) return false; + const parsed = raw === 'true'; + return typeof parsed === 'boolean' ? parsed : false; + } catch { + return false; + } + } + + /** Persist thinking-enabled default to localStorage */ + private saveThinkingDefaults(): void { + if (typeof globalThis.localStorage === 'undefined') return; + localStorage.setItem( + THINKING_ENABLED_DEFAULT_LOCALSTORAGE_KEY, + this.pendingThinkingEnabled ? 'true' : 'false' + ); + } + + /** Load reasoning effort default from localStorage */ + private static loadReasoningEffortDefault(): ReasoningEffort { + if (typeof globalThis.localStorage === 'undefined') return ReasoningEffort.MEDIUM; + try { + const raw = localStorage.getItem(REASONING_EFFORT_DEFAULT_LOCALSTORAGE_KEY); + return (raw as ReasoningEffort) || ReasoningEffort.MEDIUM; + } catch { + return ReasoningEffort.MEDIUM; + } + } + + /** Persist reasoning effort default to localStorage */ + private saveReasoningEffortDefaults(): void { + if (typeof globalThis.localStorage === 'undefined') return; + localStorage.setItem(REASONING_EFFORT_DEFAULT_LOCALSTORAGE_KEY, this.pendingReasoningEffort); + } + /** Callback for title update confirmation dialog */ titleUpdateConfirmationCallback?: (currentTitle: string, newTitle: string) => Promise; @@ -253,6 +300,12 @@ class ConversationsStore { this.pendingMcpServerOverrides = []; } + // Inherit global thinking default into the new conversation + conversation.thinkingEnabled = this.pendingThinkingEnabled; + await DatabaseService.updateConversation(conversation.id, { + thinkingEnabled: this.pendingThinkingEnabled + }); + this.conversations = [conversation, ...this.conversations]; this.activeConversation = conversation; this.activeMessages = []; @@ -276,6 +329,7 @@ class ConversationsStore { } this.pendingMcpServerOverrides = []; + this.pendingThinkingEnabled = false; this.activeConversation = conversation; if (conversation.currNode) { @@ -304,8 +358,9 @@ class ConversationsStore { clearActiveConversation(): void { this.activeConversation = null; this.activeMessages = []; - // reload MCP defaults so new chats inherit persisted state + // reload defaults so new chats inherit persisted state this.pendingMcpServerOverrides = ConversationsStore.loadMcpDefaults(); + this.pendingThinkingEnabled = ConversationsStore.loadThinkingDefaults(); } /** @@ -703,6 +758,84 @@ class ConversationsStore { this.saveMcpDefaults(); } + /** + * Gets the effective thinking-enabled state for the active conversation. + * Returns the conversation override if set, otherwise the global default. + */ + getThinkingEnabled(): boolean { + if (this.activeConversation) { + return this.activeConversation.thinkingEnabled ?? this.pendingThinkingEnabled; + } + return this.pendingThinkingEnabled; + } + + /** + * Sets the thinking-enabled state for the active conversation. + * If no conversation exists, stores the global default. + * @param enabled - The enabled state + */ + async setThinkingEnabled(enabled: boolean): Promise { + if (!this.activeConversation) { + this.pendingThinkingEnabled = enabled; + this.saveThinkingDefaults(); + return; + } + + this.activeConversation = { + ...this.activeConversation, + thinkingEnabled: enabled + }; + + await DatabaseService.updateConversation(this.activeConversation.id, { + thinkingEnabled: enabled + }); + + const convIndex = this.conversations.findIndex((c) => c.id === this.activeConversation!.id); + if (convIndex !== -1) { + this.conversations[convIndex].thinkingEnabled = enabled; + this.conversations = [...this.conversations]; + } + } + + /** + * Gets the effective reasoning effort for the active conversation. + * Returns the conversation override if set, otherwise the global default. + */ + getReasoningEffort(): ReasoningEffort { + if (this.activeConversation) { + return this.activeConversation.reasoningEffort ?? this.pendingReasoningEffort; + } + return this.pendingReasoningEffort; + } + + /** + * Sets the reasoning effort for the active conversation. + * If no conversation exists, stores the global default. + * @param effort - The effort level ('low' | 'medium' | 'high' | 'max') + */ + async setReasoningEffort(effort: ReasoningEffort): Promise { + if (!this.activeConversation) { + this.pendingReasoningEffort = effort; + this.saveReasoningEffortDefaults(); + return; + } + + this.activeConversation = { + ...this.activeConversation, + reasoningEffort: effort + }; + + await DatabaseService.updateConversation(this.activeConversation.id, { + reasoningEffort: effort + }); + + const convIndex = this.conversations.findIndex((c) => c.id === this.activeConversation!.id); + if (convIndex !== -1) { + this.conversations[convIndex].reasoningEffort = effort; + this.conversations = [...this.conversations]; + } + } + /** * Forks a conversation at a specific message, creating a new conversation * containing messages from root up to the target message, then navigates to it. diff --git a/tools/ui/src/lib/stores/models.svelte.ts b/tools/ui/src/lib/stores/models.svelte.ts index bc99d7412cd3..1990ba6049de 100644 --- a/tools/ui/src/lib/stores/models.svelte.ts +++ b/tools/ui/src/lib/stores/models.svelte.ts @@ -4,6 +4,10 @@ import { ServerModelStatus, ModelModality } from '$lib/enums'; import { ModelsService } from '$lib/services/models.service'; import { PropsService } from '$lib/services/props.service'; import { serverStore, isRouterMode } from '$lib/stores/server.svelte'; +import { + detectThinkingSupport, + detectThinkingSupportWithReason +} from '$lib/utils/chat-template-thinking-detector'; import { TTLCache } from '$lib/utils'; import { MODEL_PROPS_CACHE_TTL_MS, @@ -215,6 +219,67 @@ class ModelsStore { return usage !== undefined && usage.size > 0; } + // + // Thinking Support Detection + // + + /** + * Whether the selected model's chat template supports thinking/reasoning. + * Uses heuristic detection on the model's chat_template from /props. + * + * - MODEL mode: uses serverStore.props.chat_template (single loaded model) + * - ROUTER mode: fetches /props?model= for the selected model (cached) + * + * Triggers an async fetch of model props if not yet cached in ROUTER mode. + */ + get supportsThinking(): boolean { + const modelId = this.selectedModelName; + if (!modelId) { + if (!isRouterMode()) { + return detectThinkingSupport(serverStore.props?.chat_template ?? ''); + } + return false; + } + + if (isRouterMode() && !this.modelPropsCache.get(modelId)) { + this.fetchModelProps(modelId); + } + const props = this.getModelProps(modelId); + return detectThinkingSupport(props?.chat_template ?? ''); + } + + /** + * Check if a specific model supports thinking. + * Fetches model props if not cached (in router mode). + */ + checkModelSupportsThinking(modelId: string): boolean { + if (!modelId) return false; + + if (isRouterMode() && !this.modelPropsCache.get(modelId)) { + this.fetchModelProps(modelId); + } + + const props = this.getModelProps(modelId); + return detectThinkingSupport(props?.chat_template ?? ''); + } + + /** + * Detailed thinking support detection result with reason for debugging/UI. + */ + get thinkingSupportDetails(): { supported: boolean; reason: string } { + const modelId = this.selectedModelName; + if (!modelId) { + if (!isRouterMode()) { + return detectThinkingSupportWithReason(serverStore.props?.chat_template ?? ''); + } + return { supported: false, reason: 'No model selected' }; + } + if (isRouterMode() && !this.modelPropsCache.get(modelId)) { + this.fetchModelProps(modelId); + } + const props = this.getModelProps(modelId); + return detectThinkingSupportWithReason(props?.chat_template ?? ''); + } /** * @@ -362,6 +427,7 @@ class ModelsStore { try { const props = await PropsService.fetchForModel(modelId); this.modelPropsCache.set(modelId, props); + this.propsCacheVersion++; return props; } catch (error) { console.warn(`Failed to fetch props for model ${modelId}:`, error); @@ -755,3 +821,7 @@ export const propsCacheVersion = () => modelsStore.propsCacheVersion; export const singleModelName = () => modelsStore.singleModelName; export const selectedModelContextSize = () => modelsStore.selectedModelContextSize; export const favoriteModelIds = () => modelsStore.favoriteModelIds; +export const supportsThinking = () => modelsStore.supportsThinking; +export const checkModelSupportsThinking = (modelId: string) => + modelsStore.checkModelSupportsThinking(modelId); +export const thinkingSupportDetails = () => modelsStore.thinkingSupportDetails; diff --git a/tools/ui/src/lib/types/database.d.ts b/tools/ui/src/lib/types/database.d.ts index aecc250e811a..add628476b74 100644 --- a/tools/ui/src/lib/types/database.d.ts +++ b/tools/ui/src/lib/types/database.d.ts @@ -1,5 +1,5 @@ import type { ChatMessageTimings, ChatRole, ChatMessageType } from '$lib/types/chat'; -import { AttachmentType } from '$lib/enums'; +import { AttachmentType, ReasoningEffort } from '$lib/enums'; export interface McpServerOverride { serverId: string; @@ -12,6 +12,8 @@ export interface DatabaseConversation { lastModified: number; name: string; mcpServerOverrides?: McpServerOverride[]; + thinkingEnabled?: boolean; + reasoningEffort?: ReasoningEffort; forkedFromConversationId?: string; } diff --git a/tools/ui/src/lib/types/index.ts b/tools/ui/src/lib/types/index.ts index 0eb1e670133c..b88de5d66d96 100644 --- a/tools/ui/src/lib/types/index.ts +++ b/tools/ui/src/lib/types/index.ts @@ -162,3 +162,6 @@ export type { // Tools types export type { ToolEntry, ToolGroup } from './tools'; + +// Reasoning +export type { ReasoningEffortLevel } from './reasoning'; diff --git a/tools/ui/src/lib/types/reasoning.ts b/tools/ui/src/lib/types/reasoning.ts new file mode 100644 index 000000000000..b72a3b77f1fe --- /dev/null +++ b/tools/ui/src/lib/types/reasoning.ts @@ -0,0 +1,6 @@ +export interface ReasoningEffortLevel { + value: string; + label: string; + isOff?: boolean; + hasInfo?: boolean; +} diff --git a/tools/ui/src/lib/types/settings.d.ts b/tools/ui/src/lib/types/settings.d.ts index 44e83f8f33c9..d1cdca957cff 100644 --- a/tools/ui/src/lib/types/settings.d.ts +++ b/tools/ui/src/lib/types/settings.d.ts @@ -2,7 +2,12 @@ import type { SETTING_CONFIG_DEFAULT, SETTINGS_SECTION_TITLES } from '$lib/const import type { ChatMessagePromptProgress, ChatMessageTimings } from './chat'; import type { OpenAIToolDefinition } from './mcp'; import type { DatabaseMessageExtra } from './database'; -import type { ParameterSource, SyncableParameterType, SettingsFieldType } from '$lib/enums'; +import type { + ParameterSource, + ReasoningEffort, + SyncableParameterType, + SettingsFieldType +} from '$lib/enums'; import type { Icon } from '@lucide/svelte'; import type { Component } from 'svelte'; @@ -65,6 +70,10 @@ export interface SettingsChatServiceOptions { disableReasoningParsing?: boolean; // Strip reasoning content from context before sending excludeReasoningFromContext?: boolean; + // Enable model thinking/reasoning via chat_template_kwargs + enableThinking?: boolean; + // Reasoning effort level (low/medium/high/max) for thinking models + reasoningEffort?: ReasoningEffort; tools?: OpenAIToolDefinition[]; // Generation parameters temperature?: number; diff --git a/tools/ui/src/lib/utils/chat-template-thinking-detector.ts b/tools/ui/src/lib/utils/chat-template-thinking-detector.ts new file mode 100644 index 000000000000..da6382f5cf94 --- /dev/null +++ b/tools/ui/src/lib/utils/chat-template-thinking-detector.ts @@ -0,0 +1,86 @@ +/** + * Detects whether a model's chat template supports thinking/reasoning control. + * + * The server "/props" endpoint does NOT expose a supports_thinking flag. + * It is computed internally by common_chat_templates_support_enable_thinking + * in common/chat.cpp. A proper server flag would make this unnecessary. + * + * Detection order (most reliable first): + * 1. Thinking-control Jinja2 variables === pass-through via chat_template_kwargs + * 2. Thinking-control Jinja2 conditionals === template-native on/off logic + * 3. Paired thinking-content tag pairs === models that output special tags + */ + +const THINKING_KWARG_VARS = ['enable_thinking', 'reasoning_effort', 'thinking_budget']; + +/** + * Paired thinking-content tag patterns. + * + * Inspected: llama-cpp-deepseek-r1/v3, nim-nemotron-{3,4}-nano, qwen-qwq-32b, + * qwen-3-32b, google-gemma-4-31b-it, kimikimi-k2-thinking, apertus-8b-instruct, + * mistralai-Mistral-Small-3.2-24B, ByteDance-Seed-OSS. + * + * The self-closing entry is Kimi-K2, Gemma4 fixed-length pair, + * where both tags always appear adjacent with no content between. + */ +const THINKING_TAG_PATTERNS: Array<[string, string | null]> = [ + ['', ''], + ['<|channel>thought', '<|channel|>'], + ['<|think|>', ''], + ['', ''], + ['', null] +]; + +const JINJA_THINKING_CONDITIONALS: RegExp[] = [ + // Matches: {% if enable thinking %}, {% if enable_thinking %}, {% if (enable_thinking is defined) %} + // Handles: underscore-separated (enable_thinking), space-separated (enable thinking), + // and optional parens/brackets before enable (if (enable_thinking ) + /\{%-?\s*if\s+\(?\s*\w*enable[\s_]+\w*(thinking|think|reasoning)/i, + /\{%-?\s*if\s+\w*(thinking|reasoning)\s*(is not|==|!=)/i, + /\{%-?\s*if\s+not\s+\w*enable/i, + /\{%-?\s*if\s+ns\.enable_thinking/i +]; + +/** Guards against false positives: + * - Generic thought keyword (tool descriptions say "chain of thought") + * - Qwen vertical-bar token (used for ALL tool calls, not thinking) + */ +export function detectThinkingSupport(t: string): boolean { + if (!t) return false; + for (const kwarg of THINKING_KWARG_VARS) { + const regex = new RegExp( + `(\\{\\{[^{}]*\\b${kwarg}\\b[^{}]*\\}\\}|\\{%[^{}]*\\b${kwarg}\\b[^{}]*%\\})`, + 'i' + ); + if (regex.test(t)) return true; + } + for (const p of JINJA_THINKING_CONDITIONALS) { + if (p.test(t)) return true; + } + for (const [s, e] of THINKING_TAG_PATTERNS) { + if (t.includes(s) && (!e || t.includes(e))) return true; + } + return false; +} + +export function detectThinkingSupportWithReason(t: string): { supported: boolean; reason: string } { + if (!t) return { supported: false, reason: 'No chat template available' }; + for (const kwarg of THINKING_KWARG_VARS) { + const regex = new RegExp( + `(\\{\\{[^{}]*\\b${kwarg}\\b[^{}]*\\}\\}|\\{%[^{}]*\\b${kwarg}\\b[^{}]*%\\})`, + 'i' + ); + if (regex.test(t)) { + return { supported: true, reason: 'Found: ' + kwarg }; + } + } + for (const p of JINJA_THINKING_CONDITIONALS) { + if (p.test(t)) return { supported: true, reason: 'Found: thinking conditional' }; + } + for (const [s, e] of THINKING_TAG_PATTERNS) { + if (t.includes(s) && (!e || t.includes(e))) { + return { supported: true, reason: 'Found: ' + s + (e ? ' .. ' + e : ' (self)') }; + } + } + return { supported: false, reason: 'No thinking patterns found' }; +} diff --git a/tools/ui/tests/e2e/demo.test.ts b/tools/ui/tests/e2e/demo.test.ts index b7b4bac33517..99a3e86da3d4 100644 --- a/tools/ui/tests/e2e/demo.test.ts +++ b/tools/ui/tests/e2e/demo.test.ts @@ -1,6 +1,7 @@ import { expect, test } from '@playwright/test'; -test('home page has expected h1', async ({ page }) => { +test('home page loads correctly', async ({ page }) => { await page.goto('/'); - await expect(page.locator('h1').first()).toBeVisible(); + // Wait for the greeting to become visible (stores need time to initialize) + await expect(page.locator('h1', { hasText: /Hello there/ })).toBeVisible(); }); diff --git a/tools/ui/tests/stories/ChatScreenForm.a11y.stories.svelte b/tools/ui/tests/stories/ChatScreenForm.a11y.stories.svelte index 5d28f420e573..46f852c06641 100644 --- a/tools/ui/tests/stories/ChatScreenForm.a11y.stories.svelte +++ b/tools/ui/tests/stories/ChatScreenForm.a11y.stories.svelte @@ -44,7 +44,7 @@ await screen.findByRole('menu'); await waitFor(() => { - expect(document.activeElement).toHaveTextContent('Text Files'); + expect(document.activeElement).toHaveTextContent('Add files'); }); }} /> diff --git a/tools/ui/vite.config.ts b/tools/ui/vite.config.ts index f89a689d5e6d..5b57eae3ad54 100644 --- a/tools/ui/vite.config.ts +++ b/tools/ui/vite.config.ts @@ -10,6 +10,8 @@ import { llamaCppBuildPlugin } from './scripts/vite-plugin-llama-cpp-build'; const __dirname = dirname(fileURLToPath(import.meta.url)); +const SERVER_ORIGIN = import.meta.env?.VITE_PUBLIC_SERVER_ORIGIN || 'http://localhost:8080'; + export default defineConfig({ resolve: { alias: { @@ -75,12 +77,12 @@ export default defineConfig({ server: { proxy: { - '/v1': 'http://localhost:8080', - '/props': 'http://localhost:8080', - '/models': 'http://localhost:8080', - '/tools': 'http://localhost:8080', - '/slots': 'http://localhost:8080', - '/cors-proxy': 'http://localhost:8080' + '/v1': SERVER_ORIGIN, + '/props': SERVER_ORIGIN, + '/models': SERVER_ORIGIN, + '/tools': SERVER_ORIGIN, + '/slots': SERVER_ORIGIN, + '/cors-proxy': SERVER_ORIGIN }, headers: { 'Cross-Origin-Embedder-Policy': 'require-corp',