webui: Add Import/Export of Settings configuration + improve architecture (#22803)
* refactor: Settings keys as constant object keys * chore: Run `npm audit fix` * refactor: Settings Sections UI * feat: Refactor Settings structure and implement import/export logic * feat: Introduce ROUTES constant and RouterService * refactor: Consolidate settings definitions into registry * refactor: Update settings page routing structure * chore: Migrate hardcoded URLs to use ROUTES and RouterService * feat: Enhance model selection logic for settings and chat * chore: Update webui static build * refactor: Address PR review comments * fix: Remove unneeded setting * fix: Re-add missing settings * fix: Add missing `/slots` proxy for webui dev mode * chore: Dev-mode logs * fix: Data binding * fix: Steering for non-agentic flow
This commit is contained in:
committed by
GitHub
parent
a8fd165fec
commit
9b2925e1e0
File diff suppressed because one or more lines are too long
+3538
-3507
File diff suppressed because it is too large
Load Diff
Generated
+18
-18
@@ -2307,9 +2307,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sveltejs/kit": {
|
"node_modules/@sveltejs/kit": {
|
||||||
"version": "2.57.1",
|
"version": "2.59.1",
|
||||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.59.1.tgz",
|
||||||
"integrity": "sha512-VRdSbB96cI1EnRh09CqmnQqP/YJvET5buj8S6k7CxaJqBJD4bw4fRKDjcarAj/eX9k2eHifQfDH8NtOh+ZxxPw==",
|
"integrity": "sha512-d8OON70AphLdDesuTIl//M2O6fRTIicX8aYv8vhCiYEhTTI2OboKqey0Hu1A4VFhqwgqtq0vKDmPFGkw8kKmgw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -3640,9 +3640,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bits-ui": {
|
"node_modules/bits-ui": {
|
||||||
"version": "2.18.0",
|
"version": "2.18.1",
|
||||||
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.18.1.tgz",
|
||||||
"integrity": "sha512-GLOBZRVy3hxNHIQ2MpD/+5aK9KcBFZRhUJtZ1UDABXdlVR4K6zFpgt4T+Rwuhf2sQzlc6yK1q/DprHPjwT4Pjw==",
|
"integrity": "sha512-KkemzKFH4T3gt3H+P86JcnAWExjByv/6vlwjm/BoCwTPHu03yiCdxbghdJLvFReQTe0acCAiRcKfmixxD6XvlA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -4856,9 +4856,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/express-rate-limit": {
|
"node_modules/express-rate-limit": {
|
||||||
"version": "8.3.2",
|
"version": "8.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.0.tgz",
|
||||||
"integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==",
|
"integrity": "sha512-XKhFohWaSBdVJNTi5TaHziqnPkv04I9UQV6q1Wy7Ui6GGQZVW12ojDFwqer14EvCXxjvPG0CyWXx7cAXpALB4Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ip-address": "10.1.0"
|
"ip-address": "10.1.0"
|
||||||
@@ -7943,9 +7943,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.6",
|
"version": "8.5.14",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
|
||||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -10084,9 +10084,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/uuid": {
|
"node_modules/uuid": {
|
||||||
"version": "13.0.0",
|
"version": "13.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.2.tgz",
|
||||||
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
|
"integrity": "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
"https://github.com/sponsors/broofa",
|
"https://github.com/sponsors/broofa",
|
||||||
@@ -10302,9 +10302,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite-plugin-devtools-json/node_modules/uuid": {
|
"node_modules/vite-plugin-devtools-json/node_modules/uuid": {
|
||||||
"version": "11.1.0",
|
"version": "11.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz",
|
||||||
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
|
"integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
"https://github.com/sponsors/broofa",
|
"https://github.com/sponsors/broofa",
|
||||||
|
|||||||
+2
-1
@@ -8,6 +8,7 @@
|
|||||||
import { HealthCheckStatus } from '$lib/enums';
|
import { HealthCheckStatus } from '$lib/enums';
|
||||||
import type { MCPServerSettingsEntry } from '$lib/types';
|
import type { MCPServerSettingsEntry } from '$lib/types';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
import { ROUTES } from '$lib/constants/routes';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onMcpSettingsClick?: () => void;
|
onMcpSettingsClick?: () => void;
|
||||||
@@ -52,7 +53,7 @@
|
|||||||
function handleMcpSettingsClick() {
|
function handleMcpSettingsClick() {
|
||||||
onMcpSettingsClick?.();
|
onMcpSettingsClick?.();
|
||||||
|
|
||||||
goto(`${hasMcpServers ? '' : '?add'}#/settings/mcp`);
|
goto(`${hasMcpServers ? '' : '?add'}${ROUTES.MCP_SERVERS}`);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
+7
-2
@@ -12,6 +12,8 @@
|
|||||||
import { useAttachmentMenu } from '$lib/hooks/use-attachment-menu.svelte';
|
import { useAttachmentMenu } from '$lib/hooks/use-attachment-menu.svelte';
|
||||||
import { AttachmentMenuItemId } from '$lib/enums';
|
import { AttachmentMenuItemId } from '$lib/enums';
|
||||||
import { PencilRuler } from '@lucide/svelte';
|
import { PencilRuler } from '@lucide/svelte';
|
||||||
|
import { ROUTES, SETTINGS_SECTION_SLUGS } from '$lib/constants/routes';
|
||||||
|
import { RouterService } from '$lib/services/router.service';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
class?: string;
|
class?: string;
|
||||||
@@ -146,13 +148,16 @@
|
|||||||
|
|
||||||
<div class="my-2 border-t"></div>
|
<div class="my-2 border-t"></div>
|
||||||
|
|
||||||
<a href="#/settings/mcp" class="flex items-center gap-3 px-3 py-2">
|
<a href={ROUTES.MCP_SERVERS} class="flex items-center gap-3 px-3 py-2">
|
||||||
<McpLogo class="inline h-4 w-4" />
|
<McpLogo class="inline h-4 w-4" />
|
||||||
|
|
||||||
<span class="text-sm">MCP Servers</span>
|
<span class="text-sm">MCP Servers</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="#/settings/chat/tools" class="flex items-center gap-3 px-3 py-2">
|
<a
|
||||||
|
href={RouterService.settings(SETTINGS_SECTION_SLUGS.TOOLS)}
|
||||||
|
class="flex items-center gap-3 px-3 py-2"
|
||||||
|
>
|
||||||
<PencilRuler class="inline h-4 w-4" />
|
<PencilRuler class="inline h-4 w-4" />
|
||||||
|
|
||||||
<span class="text-sm">Tools</span>
|
<span class="text-sm">Tools</span>
|
||||||
|
|||||||
+2
-1
@@ -13,6 +13,7 @@
|
|||||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||||
import { getFileTypeCategory } from '$lib/utils';
|
import { getFileTypeCategory } from '$lib/utils';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
import { ROUTES } from '$lib/constants/routes';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
canSend?: boolean;
|
canSend?: boolean;
|
||||||
@@ -100,7 +101,7 @@
|
|||||||
{onSystemPromptClick}
|
{onSystemPromptClick}
|
||||||
{onMcpPromptClick}
|
{onMcpPromptClick}
|
||||||
{onMcpResourcesClick}
|
{onMcpResourcesClick}
|
||||||
onMcpSettingsClick={() => goto('#/settings/mcp')}
|
onMcpSettingsClick={() => goto(ROUTES.MCP_SERVERS)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
+4
-3
@@ -17,6 +17,7 @@
|
|||||||
import { parseFilesToMessageExtras } from '$lib/utils/browser-only';
|
import { parseFilesToMessageExtras } from '$lib/utils/browser-only';
|
||||||
import { deriveAgenticSections } from '$lib/utils';
|
import { deriveAgenticSections } from '$lib/utils';
|
||||||
import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
|
import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
|
||||||
|
import { ROUTES } from '$lib/constants/routes';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
class?: string;
|
class?: string;
|
||||||
@@ -182,7 +183,7 @@
|
|||||||
const conversationDeleted = await chatStore.removeSystemPromptPlaceholder(message.id);
|
const conversationDeleted = await chatStore.removeSystemPromptPlaceholder(message.id);
|
||||||
|
|
||||||
if (conversationDeleted) {
|
if (conversationDeleted) {
|
||||||
goto(`#/`);
|
goto(ROUTES.START);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -205,7 +206,7 @@
|
|||||||
const conversationDeleted = await chatStore.removeSystemPromptPlaceholder(message.id);
|
const conversationDeleted = await chatStore.removeSystemPromptPlaceholder(message.id);
|
||||||
|
|
||||||
if (conversationDeleted) {
|
if (conversationDeleted) {
|
||||||
goto(`#/`);
|
goto(ROUTES.START);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
chatActions.delete(message);
|
chatActions.delete(message);
|
||||||
@@ -271,7 +272,7 @@
|
|||||||
const conversationDeleted = await chatStore.removeSystemPromptPlaceholder(message.id);
|
const conversationDeleted = await chatStore.removeSystemPromptPlaceholder(message.id);
|
||||||
isEditing = false;
|
isEditing = false;
|
||||||
if (conversationDeleted) {
|
if (conversationDeleted) {
|
||||||
goto(`#/`);
|
goto(ROUTES.START);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-6
@@ -85,10 +85,6 @@
|
|||||||
editCtx.setUploadedFiles([...editCtx.editedUploadedFiles, ...processed]);
|
editCtx.setUploadedFiles([...editCtx.editedUploadedFiles, ...processed]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleUploadedFilesChange(files: ChatUploadedFile[]) {
|
|
||||||
editCtx.setUploadedFiles(files);
|
|
||||||
}
|
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
chatStore.setEditModeActive(handleFilesAdd);
|
chatStore.setEditModeActive(handleFilesAdd);
|
||||||
|
|
||||||
@@ -104,7 +100,7 @@
|
|||||||
<ChatForm
|
<ChatForm
|
||||||
value={editCtx.editedContent}
|
value={editCtx.editedContent}
|
||||||
attachments={editCtx.editedExtras}
|
attachments={editCtx.editedExtras}
|
||||||
uploadedFiles={editCtx.editedUploadedFiles}
|
bind:uploadedFiles={editCtx.editedUploadedFiles}
|
||||||
placeholder="Edit your message..."
|
placeholder="Edit your message..."
|
||||||
showMcpPromptButton
|
showMcpPromptButton
|
||||||
showAddButton={editCtx.messageRole === MessageRole.USER}
|
showAddButton={editCtx.messageRole === MessageRole.USER}
|
||||||
@@ -112,7 +108,6 @@
|
|||||||
onValueChange={editCtx.setContent}
|
onValueChange={editCtx.setContent}
|
||||||
onAttachmentRemove={handleAttachmentRemove}
|
onAttachmentRemove={handleAttachmentRemove}
|
||||||
onUploadedFileRemove={handleUploadedFileRemove}
|
onUploadedFileRemove={handleUploadedFileRemove}
|
||||||
onUploadedFilesChange={handleUploadedFilesChange}
|
|
||||||
onFilesAdd={handleFilesAdd}
|
onFilesAdd={handleFilesAdd}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as AlertDialog from '$lib/components/ui/alert-dialog';
|
||||||
|
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||||
|
import Label from '$lib/components/ui/label/label.svelte';
|
||||||
|
import { Shield, ShieldOff } from '@lucide/svelte';
|
||||||
|
|
||||||
|
let {
|
||||||
|
open = $bindable(),
|
||||||
|
includeSensitiveData = $bindable(false),
|
||||||
|
onCancel,
|
||||||
|
onConfirm
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
includeSensitiveData: boolean;
|
||||||
|
onCancel: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
function handleOpenChange(newOpen: boolean) {
|
||||||
|
if (!newOpen) {
|
||||||
|
onCancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AlertDialog.Root {open} onOpenChange={handleOpenChange}>
|
||||||
|
<AlertDialog.Content>
|
||||||
|
<AlertDialog.Header>
|
||||||
|
<AlertDialog.Title class="flex items-center gap-2">
|
||||||
|
{#if includeSensitiveData}
|
||||||
|
<ShieldOff class="h-5 w-5 text-destructive" />
|
||||||
|
{:else}
|
||||||
|
<Shield class="h-5 w-5 text-destructive" />
|
||||||
|
{/if}
|
||||||
|
Export Settings
|
||||||
|
</AlertDialog.Title>
|
||||||
|
|
||||||
|
<AlertDialog.Description>
|
||||||
|
{#if includeSensitiveData}
|
||||||
|
<p class="text-amber-500">
|
||||||
|
Warning: This export will include sensitive data such as API keys and MCP server custom
|
||||||
|
headers (e.g., authorization tokens). Do not share this file with anyone you don't
|
||||||
|
trust.
|
||||||
|
</p>
|
||||||
|
{:else}
|
||||||
|
<p>
|
||||||
|
Sensitive data (API keys, MCP server custom headers) will not be included in the export
|
||||||
|
to protect your credentials.
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</AlertDialog.Description>
|
||||||
|
</AlertDialog.Header>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 py-2">
|
||||||
|
<Checkbox id="include-sensitive" bind:checked={includeSensitiveData} />
|
||||||
|
|
||||||
|
<Label
|
||||||
|
for="include-sensitive"
|
||||||
|
class="text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
>
|
||||||
|
{#if includeSensitiveData}
|
||||||
|
<span class="text-destructive">Include sensitive data (not recommended)</span>
|
||||||
|
{:else}
|
||||||
|
<span>Include sensitive data</span>
|
||||||
|
{/if}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AlertDialog.Footer>
|
||||||
|
<AlertDialog.Cancel onclick={onCancel}>Cancel</AlertDialog.Cancel>
|
||||||
|
<AlertDialog.Action
|
||||||
|
onclick={onConfirm}
|
||||||
|
class="bg-destructive text-white hover:bg-destructive/80"
|
||||||
|
>
|
||||||
|
{#if includeSensitiveData}
|
||||||
|
Export Anyway
|
||||||
|
{:else}
|
||||||
|
Export Without Sensitive Data
|
||||||
|
{/if}
|
||||||
|
</AlertDialog.Action>
|
||||||
|
</AlertDialog.Footer>
|
||||||
|
</AlertDialog.Content>
|
||||||
|
</AlertDialog.Root>
|
||||||
@@ -18,6 +18,37 @@
|
|||||||
*/
|
*/
|
||||||
export { default as DialogMcpServerAddNew } from './DialogMcpServerAddNew.svelte';
|
export { default as DialogMcpServerAddNew } from './DialogMcpServerAddNew.svelte';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* **DialogExportSettings** - Settings export dialog with sensitive data warning
|
||||||
|
*
|
||||||
|
* Dialog for exporting settings with an option to include or exclude
|
||||||
|
* sensitive data (API keys, MCP server custom headers). Defaults to excluding
|
||||||
|
* sensitive data for security. User must explicitly opt-in to include them.
|
||||||
|
*
|
||||||
|
* **Architecture:**
|
||||||
|
* - Uses ShadCN AlertDialog
|
||||||
|
* - Checkbox to toggle sensitive data inclusion (defaults to false)
|
||||||
|
* - Warning icon and message when sensitive data is included
|
||||||
|
* - Destructive variant for the action button when exporting with sensitive data
|
||||||
|
*
|
||||||
|
* **Features:**
|
||||||
|
* - Secure default: sensitive data excluded by default
|
||||||
|
* - User must explicitly opt-in to include sensitive data
|
||||||
|
* - Visual warning (ShieldOff icon) when sensitive data is included
|
||||||
|
* - Different action text based on sensitive data state
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```svelte
|
||||||
|
* <DialogExportSettings
|
||||||
|
* bind:open={showExportSettings}
|
||||||
|
* bind:includeSensitiveData
|
||||||
|
* onConfirm={handleSettingsExport}
|
||||||
|
* onCancel={() => showExportSettings = false}
|
||||||
|
* />
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export { default as DialogExportSettings } from './DialogExportSettings.svelte';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* CONFIRMATION DIALOGS
|
* CONFIRMATION DIALOGS
|
||||||
|
|||||||
+4
-2
@@ -11,6 +11,8 @@
|
|||||||
import ScrollArea from '$lib/components/ui/scroll-area/scroll-area.svelte';
|
import ScrollArea from '$lib/components/ui/scroll-area/scroll-area.svelte';
|
||||||
import * as Sidebar from '$lib/components/ui/sidebar';
|
import * as Sidebar from '$lib/components/ui/sidebar';
|
||||||
import Input from '$lib/components/ui/input/input.svelte';
|
import Input from '$lib/components/ui/input/input.svelte';
|
||||||
|
import { ROUTES } from '$lib/constants/routes';
|
||||||
|
import { RouterService } from '$lib/services/router.service';
|
||||||
import {
|
import {
|
||||||
conversationsStore,
|
conversationsStore,
|
||||||
conversations,
|
conversations,
|
||||||
@@ -159,7 +161,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleMobileSidebarItemClick();
|
handleMobileSidebarItemClick();
|
||||||
await goto(`#/chat/${id}`);
|
await goto(RouterService.chat(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleStopGeneration(id: string) {
|
function handleStopGeneration(id: string) {
|
||||||
@@ -171,7 +173,7 @@
|
|||||||
<ScrollArea class="h-full flex-1">
|
<ScrollArea class="h-full flex-1">
|
||||||
<Sidebar.Header class="gap-4 bg-sidebar/50 p-3 backdrop-blur-lg md:pt-4 md:pb-2">
|
<Sidebar.Header class="gap-4 bg-sidebar/50 p-3 backdrop-blur-lg md:pt-4 md:pb-2">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<a href="#/" onclick={handleMobileSidebarItemClick}>
|
<a href={ROUTES.START} onclick={handleMobileSidebarItemClick}>
|
||||||
<h1 class="inline-flex items-center gap-1 px-2 text-xl font-semibold">{APP_NAME}</h1>
|
<h1 class="inline-flex items-center gap-1 px-2 text-xl font-semibold">{APP_NAME}</h1>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -11,6 +11,7 @@
|
|||||||
import { DropdownMenuActions } from '$lib/components/app';
|
import { DropdownMenuActions } from '$lib/components/app';
|
||||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||||
import { FORK_TREE_DEPTH_PADDING } from '$lib/constants';
|
import { FORK_TREE_DEPTH_PADDING } from '$lib/constants';
|
||||||
|
import { RouterService } from '$lib/services/router.service';
|
||||||
import { getAllLoadingChats } from '$lib/stores/chat.svelte';
|
import { getAllLoadingChats } from '$lib/stores/chat.svelte';
|
||||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||||
import { TruncatedText } from '$lib/components/app';
|
import { TruncatedText } from '$lib/components/app';
|
||||||
@@ -113,7 +114,7 @@
|
|||||||
<Tooltip.Root>
|
<Tooltip.Root>
|
||||||
<Tooltip.Trigger>
|
<Tooltip.Trigger>
|
||||||
<a
|
<a
|
||||||
href="#/chat/{conversation.forkedFromConversationId}"
|
href={RouterService.chat(conversation.forkedFromConversationId)}
|
||||||
class="flex shrink-0 items-center text-muted-foreground transition-colors hover:text-foreground"
|
class="flex shrink-0 items-center text-muted-foreground transition-colors hover:text-foreground"
|
||||||
>
|
>
|
||||||
<GitBranch class="h-3.5 w-3.5" />
|
<GitBranch class="h-3.5 w-3.5" />
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
import Label from '$lib/components/ui/label/label.svelte';
|
import Label from '$lib/components/ui/label/label.svelte';
|
||||||
import { serverStore, serverLoading } from '$lib/stores/server.svelte';
|
import { serverStore, serverLoading } from '$lib/stores/server.svelte';
|
||||||
import { config, settingsStore } from '$lib/stores/settings.svelte';
|
import { config, settingsStore } from '$lib/stores/settings.svelte';
|
||||||
|
import { SETTINGS_KEYS } from '$lib/constants';
|
||||||
|
import { ROUTES } from '$lib/constants/routes';
|
||||||
import { fade, fly, scale } from 'svelte/transition';
|
import { fade, fly, scale } from 'svelte/transition';
|
||||||
import { KeyboardKey } from '$lib/enums';
|
import { KeyboardKey } from '$lib/enums';
|
||||||
|
|
||||||
@@ -63,7 +65,7 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Update the API key in settings first
|
// Update the API key in settings first
|
||||||
settingsStore.updateConfig('apiKey', apiKeyInput.trim());
|
settingsStore.updateConfig(SETTINGS_KEYS.API_KEY, apiKeyInput.trim());
|
||||||
|
|
||||||
// Test the API key by making a real request to the server
|
// Test the API key by making a real request to the server
|
||||||
const response = await fetch(`${base}/props`, {
|
const response = await fetch(`${base}/props`, {
|
||||||
@@ -79,7 +81,7 @@
|
|||||||
|
|
||||||
// Show success state briefly, then navigate to home
|
// Show success state briefly, then navigate to home
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
goto(`#/`);
|
goto(ROUTES.START);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} else {
|
} else {
|
||||||
// API key is invalid - User Story A
|
// API key is invalid - User Story A
|
||||||
|
|||||||
+29
-9
@@ -1,11 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import SettingsChatFooter from './SettingsChatFooter.svelte';
|
|
||||||
import SettingsChatFields from './SettingsChatFields.svelte';
|
|
||||||
import SettingsChatToolsTab from './SettingsChatToolsTab.svelte';
|
|
||||||
import SettingsChatImportExportTab from './SettingsChatImportExportTab.svelte';
|
|
||||||
import {
|
import {
|
||||||
SettingsChatDesktopSidebar,
|
SettingsChatDesktopSidebar,
|
||||||
SettingsChatMobileHeader
|
SettingsChatFields,
|
||||||
|
SettingsChatImportExportTab,
|
||||||
|
SettingsChatMobileHeader,
|
||||||
|
SettingsChatToolsTab,
|
||||||
|
SettingsFooter
|
||||||
} from '$lib/components/app/settings';
|
} from '$lib/components/app/settings';
|
||||||
import { config, settingsStore } from '$lib/stores/settings.svelte';
|
import { config, settingsStore } from '$lib/stores/settings.svelte';
|
||||||
import {
|
import {
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
SETTINGS_SECTION_TITLES,
|
SETTINGS_SECTION_TITLES,
|
||||||
type SettingsSection
|
type SettingsSection
|
||||||
} from '$lib/constants';
|
} from '$lib/constants';
|
||||||
|
import { RouterService } from '$lib/services/router.service';
|
||||||
import { setMode } from 'mode-watcher';
|
import { setMode } from 'mode-watcher';
|
||||||
import { ColorMode } from '$lib/enums/ui';
|
import { ColorMode } from '$lib/enums/ui';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
@@ -22,7 +23,8 @@
|
|||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { setChatSettingsConfigContext } from '$lib/contexts';
|
import { setChatSettingsConfigContext } from '$lib/contexts';
|
||||||
import { settingsReferrer } from '$lib/stores/settings-referrer.svelte';
|
import { settingsReferrer } from '$lib/stores/settings-referrer.svelte';
|
||||||
|
import { modelsStore } from '$lib/stores/models.svelte';
|
||||||
|
import { isRouterMode } from '$lib/stores/server.svelte';
|
||||||
interface Props {
|
interface Props {
|
||||||
initialSection?: string;
|
initialSection?: string;
|
||||||
getSectionHref?: (section: SettingsSection) => string;
|
getSectionHref?: (section: SettingsSection) => string;
|
||||||
@@ -33,14 +35,30 @@
|
|||||||
let activeSlug = $derived(
|
let activeSlug = $derived(
|
||||||
initialSection ?? (page.params as Record<string, string | undefined>).section ?? 'general'
|
initialSection ?? (page.params as Record<string, string | undefined>).section ?? 'general'
|
||||||
);
|
);
|
||||||
|
|
||||||
let currentSection = $derived(
|
let currentSection = $derived(
|
||||||
SETTINGS_CHAT_SECTIONS.find((section) => section.slug === activeSlug) ||
|
SETTINGS_CHAT_SECTIONS.find((section) => section.slug === activeSlug) ||
|
||||||
SETTINGS_CHAT_SECTIONS[0]
|
SETTINGS_CHAT_SECTIONS[0]
|
||||||
);
|
);
|
||||||
|
|
||||||
let localConfig: SettingsConfigType = $state({ ...config() });
|
let localConfig: SettingsConfigType = $state({ ...config() });
|
||||||
|
|
||||||
let mobileHeader: { updateCarousel: () => void } | undefined;
|
let mobileHeader: { updateCarousel: () => void } | undefined;
|
||||||
|
|
||||||
|
let fetchInitiated = false;
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (isRouterMode() && currentSection.fields && !fetchInitiated) {
|
||||||
|
fetchInitiated = true;
|
||||||
|
|
||||||
|
void modelsStore
|
||||||
|
.fetch()
|
||||||
|
.then(() => modelsStore.fetchRouterModels())
|
||||||
|
.then(() => modelsStore.fetchModalitiesForLoadedModels())
|
||||||
|
.then(() => modelsStore.ensureFirstModelSelected());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function handleThemeChange(newTheme: string) {
|
function handleThemeChange(newTheme: string) {
|
||||||
localConfig.theme = newTheme;
|
localConfig.theme = newTheme;
|
||||||
setMode(newTheme as ColorMode);
|
setMode(newTheme as ColorMode);
|
||||||
@@ -110,13 +128,15 @@
|
|||||||
<SettingsChatDesktopSidebar
|
<SettingsChatDesktopSidebar
|
||||||
sections={SETTINGS_CHAT_SECTIONS}
|
sections={SETTINGS_CHAT_SECTIONS}
|
||||||
isActive={(section: SettingsSection) => section.slug === activeSlug}
|
isActive={(section: SettingsSection) => section.slug === activeSlug}
|
||||||
getHref={getSectionHref ?? ((section: SettingsSection) => `#/settings/chat/${section.slug}`)}
|
getHref={getSectionHref ??
|
||||||
|
((section: SettingsSection) => RouterService.settings(section.slug))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingsChatMobileHeader
|
<SettingsChatMobileHeader
|
||||||
sections={SETTINGS_CHAT_SECTIONS}
|
sections={SETTINGS_CHAT_SECTIONS}
|
||||||
isActive={(section: SettingsSection) => section.slug === activeSlug}
|
isActive={(section: SettingsSection) => section.slug === activeSlug}
|
||||||
getHref={getSectionHref ?? ((section: SettingsSection) => `#/settings/chat/${section.slug}`)}
|
getHref={getSectionHref ??
|
||||||
|
((section: SettingsSection) => RouterService.settings(section.slug))}
|
||||||
bind:this={mobileHeader}
|
bind:this={mobileHeader}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -149,7 +169,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SettingsChatFooter onReset={handleReset} onSave={handleSave} />
|
<SettingsFooter onReset={handleReset} onSave={handleSave} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+20
-13
@@ -9,9 +9,9 @@
|
|||||||
import { SettingsFieldType } from '$lib/enums/settings';
|
import { SettingsFieldType } from '$lib/enums/settings';
|
||||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||||
import { serverStore } from '$lib/stores/server.svelte';
|
import { serverStore } from '$lib/stores/server.svelte';
|
||||||
import { modelsStore, selectedModelName } from '$lib/stores/models.svelte';
|
import { modelsStore, selectedModelName, propsCacheVersion } from '$lib/stores/models.svelte';
|
||||||
import { normalizeFloatingPoint } from '$lib/utils/precision';
|
import { normalizeFloatingPoint } from '$lib/utils/precision';
|
||||||
import SettingsChatParameterSourceIndicator from './SettingsChatParameterSourceIndicator.svelte';
|
import { SettingsChatParameterSourceIndicator } from '$lib/components/app/settings';
|
||||||
import type { Component } from 'svelte';
|
import type { Component } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -23,13 +23,19 @@
|
|||||||
|
|
||||||
let { fields, localConfig, onConfigChange, onThemeChange }: Props = $props();
|
let { fields, localConfig, onConfigChange, onThemeChange }: Props = $props();
|
||||||
|
|
||||||
// server sampling defaults for placeholders
|
let currentModelParams = $derived.by(() => {
|
||||||
let sp = $derived.by(() => {
|
propsCacheVersion();
|
||||||
|
|
||||||
if (serverStore.isRouterMode) {
|
if (serverStore.isRouterMode) {
|
||||||
const m = selectedModelName();
|
const currentModelName = selectedModelName();
|
||||||
if (m) {
|
|
||||||
const p = modelsStore.getModelProps(m);
|
if (currentModelName) {
|
||||||
return (p?.default_generation_settings?.params ?? {}) as Record<string, unknown>;
|
const currentModelProps = modelsStore.getModelProps(currentModelName);
|
||||||
|
|
||||||
|
return (currentModelProps?.default_generation_settings?.params ?? {}) as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (serverStore.defaultParams ?? {}) as Record<string, unknown>;
|
return (serverStore.defaultParams ?? {}) as Record<string, unknown>;
|
||||||
@@ -40,7 +46,7 @@
|
|||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
{#if field.type === SettingsFieldType.INPUT}
|
{#if field.type === SettingsFieldType.INPUT}
|
||||||
{@const currentValue = String(localConfig[field.key] ?? '')}
|
{@const currentValue = String(localConfig[field.key] ?? '')}
|
||||||
{@const serverDefault = sp[field.key]}
|
{@const serverDefault = currentModelParams[field.key]}
|
||||||
{@const isCustomRealTime = (() => {
|
{@const isCustomRealTime = (() => {
|
||||||
if (serverDefault == null) return false;
|
if (serverDefault == null) return false;
|
||||||
if (currentValue === '') return false;
|
if (currentValue === '') return false;
|
||||||
@@ -78,8 +84,8 @@
|
|||||||
// Update local config immediately for real-time badge feedback
|
// Update local config immediately for real-time badge feedback
|
||||||
onConfigChange(field.key, e.currentTarget.value);
|
onConfigChange(field.key, e.currentTarget.value);
|
||||||
}}
|
}}
|
||||||
placeholder={sp[field.key] != null
|
placeholder={currentModelParams[field.key] != null
|
||||||
? `Default: ${normalizeFloatingPoint(sp[field.key])}`
|
? `Default: ${normalizeFloatingPoint(currentModelParams[field.key])}`
|
||||||
: ''}
|
: ''}
|
||||||
class="w-full {isCustomRealTime ? 'pr-8' : ''}"
|
class="w-full {isCustomRealTime ? 'pr-8' : ''}"
|
||||||
/>
|
/>
|
||||||
@@ -133,7 +139,8 @@
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
id="showSystemMessage"
|
id="showSystemMessage"
|
||||||
checked={Boolean(localConfig.showSystemMessage ?? true)}
|
checked={Boolean(localConfig.showSystemMessage ?? true)}
|
||||||
onCheckedChange={(checked) => onConfigChange('showSystemMessage', Boolean(checked))}
|
onCheckedChange={(checked) =>
|
||||||
|
onConfigChange(SETTINGS_KEYS.SHOW_SYSTEM_MESSAGE, Boolean(checked))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Label for="showSystemMessage" class="cursor-pointer text-sm font-normal">
|
<Label for="showSystemMessage" class="cursor-pointer text-sm font-normal">
|
||||||
@@ -147,7 +154,7 @@
|
|||||||
opt.value === localConfig[field.key]
|
opt.value === localConfig[field.key]
|
||||||
)}
|
)}
|
||||||
{@const currentValue = localConfig[field.key]}
|
{@const currentValue = localConfig[field.key]}
|
||||||
{@const serverDefault = sp[field.key]}
|
{@const serverDefault = currentModelParams[field.key]}
|
||||||
{@const isCustomRealTime = (() => {
|
{@const isCustomRealTime = (() => {
|
||||||
if (serverDefault == null) return false;
|
if (serverDefault == null) return false;
|
||||||
if (currentValue === '' || currentValue === undefined) return false;
|
if (currentValue === '' || currentValue === undefined) return false;
|
||||||
|
|||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Component } from 'svelte';
|
||||||
|
import { Button, type ButtonVariant } from '$lib/components/ui/button';
|
||||||
|
|
||||||
|
let {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
IconComponent,
|
||||||
|
buttonText,
|
||||||
|
onclick,
|
||||||
|
titleClass,
|
||||||
|
buttonVariant,
|
||||||
|
buttonClass,
|
||||||
|
wrapperClass,
|
||||||
|
summary
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
IconComponent: Component;
|
||||||
|
buttonText: string;
|
||||||
|
onclick: () => void;
|
||||||
|
titleClass?: string;
|
||||||
|
buttonVariant?: ButtonVariant;
|
||||||
|
buttonClass?: string;
|
||||||
|
wrapperClass?: string;
|
||||||
|
summary?: { show: boolean; verb: string; items: DatabaseConversation[] };
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let sectionButtonClass = $derived(buttonClass ?? 'justify-start justify-self-start md:w-auto');
|
||||||
|
let sectionButtonVariant = $derived(buttonVariant ?? 'outline');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="grid gap-1 {wrapperClass ?? ''}">
|
||||||
|
<h4 class="mt-0 mb-2 text-sm font-medium {titleClass ?? ''}">{title}</h4>
|
||||||
|
|
||||||
|
<p class="mb-4 text-sm text-muted-foreground">{description}</p>
|
||||||
|
|
||||||
|
<Button class={sectionButtonClass} {onclick} variant={sectionButtonVariant}>
|
||||||
|
<IconComponent class="mr-2 h-4 w-4" />
|
||||||
|
|
||||||
|
{buttonText}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{#if summary && summary.show && summary.items.length > 0}
|
||||||
|
<div class="mt-4 grid overflow-x-auto rounded-lg border border-border/50 bg-muted/30 p-4">
|
||||||
|
<h5 class="mb-2 text-sm font-medium">
|
||||||
|
{summary.verb}
|
||||||
|
{summary.items.length} conversation{summary.items.length === 1 ? '' : 's'}
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<ul class="space-y-1 text-sm text-muted-foreground">
|
||||||
|
{#each summary.items.slice(0, 10) as conv (conv.id)}
|
||||||
|
<li class="truncate">• {conv.name || 'Untitled conversation'}</li>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if summary.items.length > 10}
|
||||||
|
<li class="italic">... and {summary.items.length - 10} more</li>
|
||||||
|
{/if}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
+138
-93
@@ -1,21 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Component } from 'svelte';
|
|
||||||
import { Download, Upload, Trash2 } from '@lucide/svelte';
|
import { Download, Upload, Trash2 } from '@lucide/svelte';
|
||||||
import { Button, type ButtonVariant } from '$lib/components/ui/button';
|
import {
|
||||||
import { DialogConversationSelection, DialogConfirmation } from '$lib/components/app';
|
DialogConversationSelection,
|
||||||
|
DialogConfirmation,
|
||||||
|
DialogExportSettings
|
||||||
|
} from '$lib/components/app';
|
||||||
import { createMessageCountMap } from '$lib/utils';
|
import { createMessageCountMap } from '$lib/utils';
|
||||||
|
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||||
import { conversationsStore, conversations } from '$lib/stores/conversations.svelte';
|
import { conversationsStore, conversations } from '$lib/stores/conversations.svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import { ConversationSelectionMode, HtmlInputType, FileExtensionText } from '$lib/enums';
|
import { ConversationSelectionMode, HtmlInputType, FileExtensionText } from '$lib/enums';
|
||||||
|
import SettingsChatImportExportSection from './SettingsChatImportExportSection.svelte';
|
||||||
interface SectionOpts {
|
import SettingsGroup from '$lib/components/app/settings/SettingsGroup.svelte';
|
||||||
wrapperClass?: string;
|
|
||||||
titleClass?: string;
|
|
||||||
buttonVariant?: ButtonVariant;
|
|
||||||
buttonClass?: string;
|
|
||||||
summary?: { show: boolean; verb: string; items: DatabaseConversation[] };
|
|
||||||
}
|
|
||||||
|
|
||||||
let exportedConversations = $state<DatabaseConversation[]>([]);
|
let exportedConversations = $state<DatabaseConversation[]>([]);
|
||||||
let importedConversations = $state<DatabaseConversation[]>([]);
|
let importedConversations = $state<DatabaseConversation[]>([]);
|
||||||
@@ -33,6 +30,82 @@
|
|||||||
// Delete functionality state
|
// Delete functionality state
|
||||||
let showDeleteDialog = $state(false);
|
let showDeleteDialog = $state(false);
|
||||||
|
|
||||||
|
// Settings import/export state
|
||||||
|
let showSettingsExportSummary = $state(false);
|
||||||
|
let showSettingsImportSummary = $state(false);
|
||||||
|
let showSettingsExportDialog = $state(false);
|
||||||
|
let includeSensitiveData = $state(false);
|
||||||
|
|
||||||
|
function handleSettingsExport() {
|
||||||
|
showSettingsExportDialog = true;
|
||||||
|
includeSensitiveData = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSettingsExportConfirm() {
|
||||||
|
showSettingsExportDialog = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = settingsStore.exportSettings(includeSensitiveData);
|
||||||
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `llama_settings_${new Date().toISOString().split('T')[0]}.json`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
showSettingsExportSummary = true;
|
||||||
|
showSettingsImportSummary = false;
|
||||||
|
toast.success('Settings exported');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to export settings:', err);
|
||||||
|
toast.error('Failed to export settings');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSettingsExportCancel() {
|
||||||
|
showSettingsExportDialog = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSettingsImport() {
|
||||||
|
try {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = HtmlInputType.FILE;
|
||||||
|
input.accept = FileExtensionText.JSON;
|
||||||
|
|
||||||
|
input.onchange = async (e) => {
|
||||||
|
const file = (e.target as HTMLInputElement)?.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const text = await file.text();
|
||||||
|
const data = JSON.parse(text);
|
||||||
|
|
||||||
|
if (!data || typeof data !== 'object' || !data.config) {
|
||||||
|
toast.error('Invalid settings file: missing config');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
settingsStore.importSettings(data);
|
||||||
|
|
||||||
|
showSettingsImportSummary = true;
|
||||||
|
showSettingsExportSummary = false;
|
||||||
|
toast.success('Settings imported successfully');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to import settings:', err);
|
||||||
|
toast.error('Failed to import settings');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
input.click();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to open file picker:', err);
|
||||||
|
toast.error('Failed to open file picker');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleExportClick() {
|
async function handleExportClick() {
|
||||||
try {
|
try {
|
||||||
const allConversations = conversations();
|
const allConversations = conversations();
|
||||||
@@ -181,94 +254,66 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet summaryList(show: boolean, verb: string, items: DatabaseConversation[])}
|
<div class="space-y-12" in:fade={{ duration: 150 }}>
|
||||||
{#if show && items.length > 0}
|
<SettingsGroup title="Conversations">
|
||||||
<div class="mt-4 grid overflow-x-auto rounded-lg border border-border/50 bg-muted/30 p-4">
|
<SettingsChatImportExportSection
|
||||||
<h5 class="mb-2 text-sm font-medium">
|
title="Export"
|
||||||
{verb}
|
description="Download your conversations as a JSON file. This includes all messages, attachments, and conversation history."
|
||||||
{items.length} conversation{items.length === 1 ? '' : 's'}
|
IconComponent={Download}
|
||||||
</h5>
|
buttonText="Export conversations"
|
||||||
|
onclick={handleExportClick}
|
||||||
|
summary={{ show: showExportSummary, verb: 'Exported', items: exportedConversations }}
|
||||||
|
/>
|
||||||
|
|
||||||
<ul class="space-y-1 text-sm text-muted-foreground">
|
<SettingsChatImportExportSection
|
||||||
{#each items.slice(0, 10) as conv (conv.id)}
|
title="Import"
|
||||||
<li class="truncate">• {conv.name || 'Untitled conversation'}</li>
|
description="Import one or more conversations from a previously exported JSON file. This will merge with your existing conversations."
|
||||||
{/each}
|
IconComponent={Upload}
|
||||||
|
buttonText="Import conversations"
|
||||||
|
onclick={handleImportClick}
|
||||||
|
summary={{ show: showImportSummary, verb: 'Imported', items: importedConversations }}
|
||||||
|
/>
|
||||||
|
|
||||||
{#if items.length > 10}
|
<SettingsChatImportExportSection
|
||||||
<li class="italic">... and {items.length - 10} more</li>
|
title="Delete All"
|
||||||
{/if}
|
description="Permanently delete all conversations and their messages. This action cannot be undone. Consider exporting your conversations first if you want to keep a backup."
|
||||||
</ul>
|
IconComponent={Trash2}
|
||||||
</div>
|
buttonText="Delete all conversations"
|
||||||
{/if}
|
onclick={handleDeleteAllClick}
|
||||||
{/snippet}
|
titleClass="text-destructive"
|
||||||
|
buttonVariant="destructive"
|
||||||
|
buttonClass="text-destructive-foreground justify-start justify-self-start bg-destructive hover:bg-destructive/80 md:w-auto"
|
||||||
|
/>
|
||||||
|
</SettingsGroup>
|
||||||
|
|
||||||
{#snippet section(
|
<SettingsGroup title="Settings">
|
||||||
title: string,
|
<SettingsChatImportExportSection
|
||||||
description: string,
|
title="Export"
|
||||||
IconComponent: Component,
|
description="Export your chat settings and preferences as a JSON file."
|
||||||
buttonText: string,
|
IconComponent={Download}
|
||||||
onclick: () => void,
|
buttonText="Export settings"
|
||||||
opts: SectionOpts
|
onclick={handleSettingsExport}
|
||||||
)}
|
summary={{ show: showSettingsExportSummary, verb: 'Exported', items: [] }}
|
||||||
{@const buttonClass = opts?.buttonClass ?? 'justify-start justify-self-start md:w-auto'}
|
/>
|
||||||
{@const buttonVariant = opts?.buttonVariant ?? 'outline'}
|
|
||||||
<div class="grid gap-1 {opts?.wrapperClass ?? ''}">
|
|
||||||
<h4 class="mt-0 mb-2 text-sm font-medium {opts?.titleClass ?? ''}">{title}</h4>
|
|
||||||
|
|
||||||
<p class="mb-4 text-sm text-muted-foreground">{description}</p>
|
<SettingsChatImportExportSection
|
||||||
|
title="Import"
|
||||||
<Button class={buttonClass} {onclick} variant={buttonVariant}>
|
description="Import chat settings from a previously exported JSON file. This will merge with your existing settings."
|
||||||
<IconComponent class="mr-2 h-4 w-4" />
|
IconComponent={Upload}
|
||||||
|
buttonText="Import settings"
|
||||||
{buttonText}
|
onclick={handleSettingsImport}
|
||||||
</Button>
|
summary={{ show: showSettingsImportSummary, verb: 'Imported', items: [] }}
|
||||||
|
/>
|
||||||
{#if opts?.summary}
|
</SettingsGroup>
|
||||||
{@render summaryList(opts.summary.show, opts.summary.verb, opts.summary.items)}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
|
|
||||||
<div class="space-y-6" in:fade={{ duration: 150 }}>
|
|
||||||
<div class="space-y-6">
|
|
||||||
{@render section(
|
|
||||||
'Export Conversations',
|
|
||||||
'Download all your conversations as a JSON file. This includes all messages, attachments, and conversation history.',
|
|
||||||
Download,
|
|
||||||
'Export conversations',
|
|
||||||
handleExportClick,
|
|
||||||
{ summary: { show: showExportSummary, verb: 'Exported', items: exportedConversations } }
|
|
||||||
)}
|
|
||||||
|
|
||||||
{@render section(
|
|
||||||
'Import Conversations',
|
|
||||||
'Import one or more conversations from a previously exported JSON file. This will merge with your existing conversations.',
|
|
||||||
Upload,
|
|
||||||
'Import conversations',
|
|
||||||
handleImportClick,
|
|
||||||
{
|
|
||||||
wrapperClass: 'border-t border-border/30 pt-6',
|
|
||||||
summary: { show: showImportSummary, verb: 'Imported', items: importedConversations }
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
|
|
||||||
{@render section(
|
|
||||||
'Delete All Conversations',
|
|
||||||
'Permanently delete all conversations and their messages. This action cannot be undone. Consider exporting your conversations first if you want to keep a backup.',
|
|
||||||
Trash2,
|
|
||||||
'Delete all conversations',
|
|
||||||
handleDeleteAllClick,
|
|
||||||
{
|
|
||||||
wrapperClass: 'border-t border-border/30 pt-4',
|
|
||||||
titleClass: 'text-destructive',
|
|
||||||
buttonVariant: 'destructive',
|
|
||||||
buttonClass:
|
|
||||||
'text-destructive-foreground justify-start justify-self-start bg-destructive hover:bg-destructive/80 md:w-auto'
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<DialogExportSettings
|
||||||
|
bind:open={showSettingsExportDialog}
|
||||||
|
bind:includeSensitiveData
|
||||||
|
onConfirm={handleSettingsExportConfirm}
|
||||||
|
onCancel={handleSettingsExportCancel}
|
||||||
|
/>
|
||||||
|
|
||||||
<DialogConversationSelection
|
<DialogConversationSelection
|
||||||
conversations={availableConversations}
|
conversations={availableConversations}
|
||||||
{messageCountMap}
|
{messageCountMap}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
children: Snippet;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { title, children }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="mb-6 text-base font-semibold">{title}</h3>
|
||||||
|
|
||||||
|
<div class="space-y-8">
|
||||||
|
{@render children()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -19,32 +19,6 @@ export { default as SettingsChatDesktopSidebar } from './SettingsChatDesktopSide
|
|||||||
*/
|
*/
|
||||||
export { default as SettingsChatMobileHeader } from './SettingsChatMobileHeader.svelte';
|
export { default as SettingsChatMobileHeader } from './SettingsChatMobileHeader.svelte';
|
||||||
|
|
||||||
/**
|
|
||||||
* Settings Import/Export panel.
|
|
||||||
* Provides UI for importing and exporting chat conversations.
|
|
||||||
*/
|
|
||||||
export { default as SettingsChatImportExportTab } from './SettingsChat/SettingsChatImportExportTab.svelte';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MCP Servers configuration panel.
|
|
||||||
* Provides UI for managing Model Context Protocol (MCP) server connections.
|
|
||||||
*/
|
|
||||||
export { default as SettingsMcpServers } from './SettingsMcpServers.svelte';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Footer with save/cancel buttons for settings panel. Positioned at bottom
|
|
||||||
* of settings dialog. Save button commits form state to config store,
|
|
||||||
* cancel button triggers reset and close.
|
|
||||||
*/
|
|
||||||
export { default as SettingsChatFooter } from './SettingsChat/SettingsChatFooter.svelte';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Form fields renderer for individual settings. Generates appropriate input
|
|
||||||
* components based on field type (text, number, select, checkbox, textarea).
|
|
||||||
* Handles validation, help text display, and parameter source indicators.
|
|
||||||
*/
|
|
||||||
export { default as SettingsChatFields } from './SettingsChat/SettingsChatFields.svelte';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Badge indicating parameter source for sampling settings. Shows one of:
|
* Badge indicating parameter source for sampling settings. Shows one of:
|
||||||
* - **Custom**: User has explicitly set this value (orange badge)
|
* - **Custom**: User has explicitly set this value (orange badge)
|
||||||
@@ -54,6 +28,44 @@ export { default as SettingsChatFields } from './SettingsChat/SettingsChatFields
|
|||||||
*/
|
*/
|
||||||
export { default as SettingsChatParameterSourceIndicator } from './SettingsChat/SettingsChatParameterSourceIndicator.svelte';
|
export { default as SettingsChatParameterSourceIndicator } from './SettingsChat/SettingsChatParameterSourceIndicator.svelte';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Section wrapper for settings panels. Displays a title heading with
|
||||||
|
* child content in a structured layout.
|
||||||
|
*/
|
||||||
|
export { default as SettingsGroup } from './SettingsGroup.svelte';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Footer with save/cancel buttons for settings panel. Positioned at bottom
|
||||||
|
* of settings dialog. Save button commits form state to config store,
|
||||||
|
* cancel button triggers reset and close.
|
||||||
|
*/
|
||||||
|
export { default as SettingsFooter } from './SettingsFooter.svelte';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings Import/Export panel.
|
||||||
|
* Provides UI for importing and exporting chat conversations.
|
||||||
|
*/
|
||||||
|
export { default as SettingsChatImportExportTab } from './SettingsChat/SettingsChatImportExportTab.svelte';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Section wrapper for import/export sections. Displays a title, description,
|
||||||
|
* icon button, and optional summary of recent actions.
|
||||||
|
*/
|
||||||
|
export { default as SettingsChatImportExportSection } from './SettingsChat/SettingsChatImportExportSection.svelte';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP Servers configuration panel.
|
||||||
|
* Provides UI for managing Model Context Protocol (MCP) server connections.
|
||||||
|
*/
|
||||||
|
export { default as SettingsMcpServers } from './SettingsMcpServers.svelte';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Form fields renderer for individual settings. Generates appropriate input
|
||||||
|
* components based on field type (text, number, select, checkbox, textarea).
|
||||||
|
* Handles validation, help text display, and parameter source indicators.
|
||||||
|
*/
|
||||||
|
export { default as SettingsChatFields } from './SettingsChat/SettingsChatFields.svelte';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* **SettingsChatToolsTab** - Tools configuration tab for chat settings
|
* **SettingsChatToolsTab** - Tools configuration tab for chat settings
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -29,10 +29,9 @@ export * from './message-export';
|
|||||||
export * from './model-id';
|
export * from './model-id';
|
||||||
export * from './precision';
|
export * from './precision';
|
||||||
export * from './processing-info';
|
export * from './processing-info';
|
||||||
export * from './settings-config';
|
export * from './routes';
|
||||||
export * from './settings-fields';
|
|
||||||
export * from './settings-keys';
|
export * from './settings-keys';
|
||||||
export * from './settings-sections';
|
export * from './settings-registry';
|
||||||
export * from './supported-file-types';
|
export * from './supported-file-types';
|
||||||
export * from './table-html-restorer';
|
export * from './table-html-restorer';
|
||||||
export * from './title-generation';
|
export * from './title-generation';
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
export const NEW_CHAT_PARAM = 'new_chat';
|
||||||
|
|
||||||
|
/** Settings section slugs — used for routes and navigation. */
|
||||||
|
export const SETTINGS_SECTION_SLUGS = {
|
||||||
|
GENERAL: 'general',
|
||||||
|
DISPLAY: 'display',
|
||||||
|
SAMPLING: 'sampling',
|
||||||
|
PENALTIES: 'penalties',
|
||||||
|
AGENTIC: 'agentic',
|
||||||
|
DEVELOPER: 'developer',
|
||||||
|
TOOLS: 'tools',
|
||||||
|
IMPORT_EXPORT: 'import-export'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const ROUTES = {
|
||||||
|
/** Root — start of the app. */
|
||||||
|
START: '#/',
|
||||||
|
/** New chat — root with new chat query param. */
|
||||||
|
NEW_CHAT: `?${NEW_CHAT_PARAM}=true#/`,
|
||||||
|
/** Chat base — for dynamic chat URLs use RouterService. */
|
||||||
|
CHAT: '#/chat',
|
||||||
|
/** MCP servers. */
|
||||||
|
MCP_SERVERS: '#/mcp-servers',
|
||||||
|
/** Settings base — for dynamic settings URLs use RouterService. */
|
||||||
|
SETTINGS: '#/settings'
|
||||||
|
} as const;
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
import { ColorMode } from '$lib/enums/ui';
|
|
||||||
import { Monitor, Moon, Sun } from '@lucide/svelte';
|
|
||||||
import { TITLE } from './title-generation';
|
|
||||||
|
|
||||||
export const SETTING_CONFIG_DEFAULT: Record<string, string | number | boolean | undefined> = {
|
|
||||||
// Note: in order not to introduce breaking changes, please keep the same data type (number, string, etc) if you want to change the default value.
|
|
||||||
// Do not use nested objects, keep it single level. Prefix the key if you need to group them.
|
|
||||||
apiKey: '',
|
|
||||||
systemMessage: '',
|
|
||||||
showSystemMessage: true,
|
|
||||||
theme: ColorMode.SYSTEM,
|
|
||||||
showThoughtInProgress: true,
|
|
||||||
disableReasoningParsing: false,
|
|
||||||
excludeReasoningFromContext: false,
|
|
||||||
showRawOutputSwitch: false,
|
|
||||||
keepStatsVisible: false,
|
|
||||||
showMessageStats: true,
|
|
||||||
askForTitleConfirmation: false,
|
|
||||||
titleGenerationUseFirstLine: false,
|
|
||||||
titleGenerationUseLLM: false,
|
|
||||||
titleGenerationPrompt: TITLE.DEFAULT_PROMPT,
|
|
||||||
pasteLongTextToFileLen: 2500,
|
|
||||||
copyTextAttachmentsAsPlainText: false,
|
|
||||||
pdfAsImage: false,
|
|
||||||
disableAutoScroll: false,
|
|
||||||
renderUserContentAsMarkdown: false,
|
|
||||||
alwaysShowSidebarOnDesktop: false,
|
|
||||||
autoShowSidebarOnNewChat: true,
|
|
||||||
sendOnEnter: true,
|
|
||||||
autoMicOnEmpty: false,
|
|
||||||
fullHeightCodeBlocks: false,
|
|
||||||
showRawModelNames: false,
|
|
||||||
mcpServers: '[]',
|
|
||||||
mcpServerUsageStats: '{}', // JSON object: { [serverId]: usageCount }
|
|
||||||
agenticMaxTurns: 10,
|
|
||||||
agenticMaxToolPreviewLines: 25,
|
|
||||||
showToolCallInProgress: false,
|
|
||||||
alwaysShowAgenticTurns: false,
|
|
||||||
// sampling params: empty means "use server default"
|
|
||||||
// the server / preset is the source of truth
|
|
||||||
// empty values are shown as placeholders from /props in the UI
|
|
||||||
// and are NOT sent in API requests, letting the server decide
|
|
||||||
samplers: '',
|
|
||||||
backend_sampling: false,
|
|
||||||
temperature: undefined,
|
|
||||||
dynatemp_range: undefined,
|
|
||||||
dynatemp_exponent: undefined,
|
|
||||||
top_k: undefined,
|
|
||||||
top_p: undefined,
|
|
||||||
min_p: undefined,
|
|
||||||
xtc_probability: undefined,
|
|
||||||
xtc_threshold: undefined,
|
|
||||||
typ_p: undefined,
|
|
||||||
repeat_last_n: undefined,
|
|
||||||
repeat_penalty: undefined,
|
|
||||||
presence_penalty: undefined,
|
|
||||||
frequency_penalty: undefined,
|
|
||||||
dry_multiplier: undefined,
|
|
||||||
dry_base: undefined,
|
|
||||||
dry_allowed_length: undefined,
|
|
||||||
dry_penalty_last_n: undefined,
|
|
||||||
max_tokens: undefined,
|
|
||||||
custom: '', // custom json-stringified object
|
|
||||||
preEncodeConversation: false,
|
|
||||||
// experimental features
|
|
||||||
pyInterpreterEnabled: false,
|
|
||||||
enableContinueGeneration: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SETTING_CONFIG_INFO: Record<string, string> = {
|
|
||||||
apiKey: 'Set the API Key if you are using <code>--api-key</code> option for the server.',
|
|
||||||
systemMessage: 'The starting message that defines how model should behave.',
|
|
||||||
showSystemMessage: 'Display the system message at the top of each conversation.',
|
|
||||||
theme:
|
|
||||||
'Choose the color theme for the interface. You can choose between System (follows your device settings), Light, or Dark.',
|
|
||||||
pasteLongTextToFileLen:
|
|
||||||
'On pasting long text, it will be converted to a file. You can control the file length by setting the value of this parameter. Value 0 means disable.',
|
|
||||||
copyTextAttachmentsAsPlainText:
|
|
||||||
'When copying a message with text attachments, combine them into a single plain text string instead of a special format that can be pasted back as attachments.',
|
|
||||||
samplers:
|
|
||||||
'The order at which samplers are applied, in simplified way. Default is "top_k;typ_p;top_p;min_p;temperature": top_k->typ_p->top_p->min_p->temperature',
|
|
||||||
backend_sampling:
|
|
||||||
'Enable backend-based samplers. When enabled, supported samplers run on the accelerator backend for faster sampling.',
|
|
||||||
temperature:
|
|
||||||
'Controls the randomness of the generated text by affecting the probability distribution of the output tokens. Higher = more random, lower = more focused.',
|
|
||||||
dynatemp_range:
|
|
||||||
'Addon for the temperature sampler. The added value to the range of dynamic temperature, which adjusts probabilities by entropy of tokens.',
|
|
||||||
dynatemp_exponent:
|
|
||||||
'Addon for the temperature sampler. Smoothes out the probability redistribution based on the most probable token.',
|
|
||||||
top_k: 'Keeps only k top tokens.',
|
|
||||||
top_p: 'Limits tokens to those that together have a cumulative probability of at least p',
|
|
||||||
min_p:
|
|
||||||
'Limits tokens based on the minimum probability for a token to be considered, relative to the probability of the most likely token.',
|
|
||||||
xtc_probability:
|
|
||||||
'XTC sampler cuts out top tokens; this parameter controls the chance of cutting tokens at all. 0 disables XTC.',
|
|
||||||
xtc_threshold:
|
|
||||||
'XTC sampler cuts out top tokens; this parameter controls the token probability that is required to cut that token.',
|
|
||||||
typ_p: 'Sorts and limits tokens based on the difference between log-probability and entropy.',
|
|
||||||
repeat_last_n: 'Last n tokens to consider for penalizing repetition',
|
|
||||||
repeat_penalty: 'Controls the repetition of token sequences in the generated text',
|
|
||||||
presence_penalty: 'Limits tokens based on whether they appear in the output or not.',
|
|
||||||
frequency_penalty: 'Limits tokens based on how often they appear in the output.',
|
|
||||||
dry_multiplier:
|
|
||||||
'DRY sampling reduces repetition in generated text even across long contexts. This parameter sets the DRY sampling multiplier.',
|
|
||||||
dry_base:
|
|
||||||
'DRY sampling reduces repetition in generated text even across long contexts. This parameter sets the DRY sampling base value.',
|
|
||||||
dry_allowed_length:
|
|
||||||
'DRY sampling reduces repetition in generated text even across long contexts. This parameter sets the allowed length for DRY sampling.',
|
|
||||||
dry_penalty_last_n:
|
|
||||||
'DRY sampling reduces repetition in generated text even across long contexts. This parameter sets DRY penalty for the last n tokens.',
|
|
||||||
max_tokens: 'The maximum number of token per output. Use -1 for infinite (no limit).',
|
|
||||||
custom: 'Custom JSON parameters to send to the API. Must be valid JSON format.',
|
|
||||||
showThoughtInProgress: 'Expand thought process by default when generating messages.',
|
|
||||||
disableReasoningParsing:
|
|
||||||
'Send reasoning_format=none so the server returns thinking tokens inline instead of extracting them into a separate field.',
|
|
||||||
excludeReasoningFromContext:
|
|
||||||
'Strip thinking from previous messages before sending. When off, thinking is sent back via the reasoning_content field so the model sees its own chain-of-thought across turns.',
|
|
||||||
showRawOutputSwitch:
|
|
||||||
'Show toggle button to display messages as plain text instead of Markdown-formatted content',
|
|
||||||
keepStatsVisible: 'Keep processing statistics visible after generation finishes.',
|
|
||||||
showMessageStats:
|
|
||||||
'Display generation statistics (tokens/second, token count, duration) below each assistant message.',
|
|
||||||
askForTitleConfirmation:
|
|
||||||
'Ask for confirmation before automatically changing conversation title when editing the first message.',
|
|
||||||
titleGenerationUseFirstLine:
|
|
||||||
'Use only the first non-empty line of the prompt to generate the conversation title.',
|
|
||||||
titleGenerationUseLLM:
|
|
||||||
'Use the LLM to automatically generate conversation titles based on the first message exchange.',
|
|
||||||
titleGenerationPrompt:
|
|
||||||
'Optional template for the title generation prompt. Use {{USER}} for the user message and {{ASSISTANT}} for the assistant message.',
|
|
||||||
pdfAsImage:
|
|
||||||
'Parse PDF as image instead of text. Automatically falls back to text processing for non-vision models.',
|
|
||||||
disableAutoScroll:
|
|
||||||
'Disable automatic scrolling while messages stream so you can control the viewport position manually.',
|
|
||||||
renderUserContentAsMarkdown: 'Render user messages using markdown formatting in the chat.',
|
|
||||||
alwaysShowSidebarOnDesktop:
|
|
||||||
'Always keep the sidebar visible on desktop instead of auto-hiding it.',
|
|
||||||
autoShowSidebarOnNewChat:
|
|
||||||
'Automatically show sidebar when starting a new chat. Disable to keep the sidebar hidden until you click on it.',
|
|
||||||
sendOnEnter:
|
|
||||||
'Use Enter to send messages and Shift + Enter for new lines. When disabled, use Ctrl/Cmd + Enter.',
|
|
||||||
autoMicOnEmpty:
|
|
||||||
'Automatically show microphone button instead of send button when textarea is empty for models with audio modality support.',
|
|
||||||
fullHeightCodeBlocks:
|
|
||||||
'Always display code blocks at their full natural height, overriding any height limits.',
|
|
||||||
showRawModelNames:
|
|
||||||
'Display full raw model identifiers (e.g. "ggml-org/GLM-4.7-Flash-GGUF:Q8_0") instead of parsed names with badges.',
|
|
||||||
mcpServers:
|
|
||||||
'Configure MCP servers as a JSON list. Use the form in the MCP Client settings section to edit.',
|
|
||||||
mcpServerUsageStats:
|
|
||||||
'Usage statistics for MCP servers. Tracks how many times tools from each server have been used.',
|
|
||||||
agenticMaxTurns:
|
|
||||||
'Maximum number of tool execution cycles before stopping (prevents infinite loops).',
|
|
||||||
agenticMaxToolPreviewLines:
|
|
||||||
'Number of lines shown in tool output previews (last N lines). Only these previews and the final LLM response persist after the agentic loop completes.',
|
|
||||||
showToolCallInProgress:
|
|
||||||
'Automatically expand tool call details while executing and keep them expanded after completion.',
|
|
||||||
pyInterpreterEnabled:
|
|
||||||
'Enable Python interpreter using Pyodide. Allows running Python code in markdown code blocks.',
|
|
||||||
preEncodeConversation:
|
|
||||||
'After each response, re-submit the conversation to pre-fill the server KV cache. Makes the next turn faster since the prompt is already encoded while you read the response.',
|
|
||||||
enableContinueGeneration:
|
|
||||||
'Enable "Continue" button for assistant messages. Currently works only with non-reasoning models.'
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SETTINGS_COLOR_MODES_CONFIG = [
|
|
||||||
{ value: ColorMode.SYSTEM, label: 'System', icon: Monitor },
|
|
||||||
{ value: ColorMode.LIGHT, label: 'Light', icon: Sun },
|
|
||||||
{ value: ColorMode.DARK, label: 'Dark', icon: Moon }
|
|
||||||
];
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
/**
|
|
||||||
* List of all numeric fields in settings configuration.
|
|
||||||
* These fields will be converted from strings to numbers during save.
|
|
||||||
*/
|
|
||||||
export const NUMERIC_FIELDS = [
|
|
||||||
'temperature',
|
|
||||||
'top_k',
|
|
||||||
'top_p',
|
|
||||||
'min_p',
|
|
||||||
'max_tokens',
|
|
||||||
'pasteLongTextToFileLen',
|
|
||||||
'dynatemp_range',
|
|
||||||
'dynatemp_exponent',
|
|
||||||
'typ_p',
|
|
||||||
'xtc_probability',
|
|
||||||
'xtc_threshold',
|
|
||||||
'repeat_last_n',
|
|
||||||
'repeat_penalty',
|
|
||||||
'presence_penalty',
|
|
||||||
'frequency_penalty',
|
|
||||||
'dry_multiplier',
|
|
||||||
'dry_base',
|
|
||||||
'dry_allowed_length',
|
|
||||||
'dry_penalty_last_n',
|
|
||||||
'agenticMaxTurns',
|
|
||||||
'agenticMaxToolPreviewLines'
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fields that must be positive integers (>= 1).
|
|
||||||
* These will be clamped to minimum 1 and rounded during save.
|
|
||||||
*/
|
|
||||||
export const POSITIVE_INTEGER_FIELDS = ['agenticMaxTurns', 'agenticMaxToolPreviewLines'] as const;
|
|
||||||
@@ -28,6 +28,7 @@ export const SETTINGS_KEYS = {
|
|||||||
ALWAYS_SHOW_SIDEBAR_ON_DESKTOP: 'alwaysShowSidebarOnDesktop',
|
ALWAYS_SHOW_SIDEBAR_ON_DESKTOP: 'alwaysShowSidebarOnDesktop',
|
||||||
FULL_HEIGHT_CODE_BLOCKS: 'fullHeightCodeBlocks',
|
FULL_HEIGHT_CODE_BLOCKS: 'fullHeightCodeBlocks',
|
||||||
SHOW_RAW_MODEL_NAMES: 'showRawModelNames',
|
SHOW_RAW_MODEL_NAMES: 'showRawModelNames',
|
||||||
|
SHOW_SYSTEM_MESSAGE: 'showSystemMessage',
|
||||||
// Sampling
|
// Sampling
|
||||||
TEMPERATURE: 'temperature',
|
TEMPERATURE: 'temperature',
|
||||||
DYNATEMP_RANGE: 'dynatemp_range',
|
DYNATEMP_RANGE: 'dynatemp_range',
|
||||||
@@ -51,6 +52,7 @@ export const SETTINGS_KEYS = {
|
|||||||
DRY_ALLOWED_LENGTH: 'dry_allowed_length',
|
DRY_ALLOWED_LENGTH: 'dry_allowed_length',
|
||||||
DRY_PENALTY_LAST_N: 'dry_penalty_last_n',
|
DRY_PENALTY_LAST_N: 'dry_penalty_last_n',
|
||||||
// MCP
|
// MCP
|
||||||
|
MCP_SERVERS: 'mcpServers',
|
||||||
AGENTIC_MAX_TURNS: 'agenticMaxTurns',
|
AGENTIC_MAX_TURNS: 'agenticMaxTurns',
|
||||||
ALWAYS_SHOW_AGENTIC_TURNS: 'alwaysShowAgenticTurns',
|
ALWAYS_SHOW_AGENTIC_TURNS: 'alwaysShowAgenticTurns',
|
||||||
AGENTIC_MAX_TOOL_PREVIEW_LINES: 'agenticMaxToolPreviewLines',
|
AGENTIC_MAX_TOOL_PREVIEW_LINES: 'agenticMaxToolPreviewLines',
|
||||||
@@ -61,5 +63,6 @@ export const SETTINGS_KEYS = {
|
|||||||
DISABLE_REASONING_PARSING: 'disableReasoningParsing',
|
DISABLE_REASONING_PARSING: 'disableReasoningParsing',
|
||||||
EXCLUDE_REASONING_FROM_CONTEXT: 'excludeReasoningFromContext',
|
EXCLUDE_REASONING_FROM_CONTEXT: 'excludeReasoningFromContext',
|
||||||
SHOW_RAW_OUTPUT_SWITCH: 'showRawOutputSwitch',
|
SHOW_RAW_OUTPUT_SWITCH: 'showRawOutputSwitch',
|
||||||
|
// PY_INTERPRETER_ENABLED: 'pyInterpreterEnabled',
|
||||||
CUSTOM: 'custom'
|
CUSTOM: 'custom'
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -0,0 +1,719 @@
|
|||||||
|
import { ColorMode } from '$lib/enums/ui';
|
||||||
|
import { SettingsFieldType } from '$lib/enums/settings';
|
||||||
|
import { SyncableParameterType } from '$lib/enums';
|
||||||
|
import {
|
||||||
|
Funnel,
|
||||||
|
AlertTriangle,
|
||||||
|
Code,
|
||||||
|
Monitor,
|
||||||
|
ListRestart,
|
||||||
|
Sliders,
|
||||||
|
PencilRuler,
|
||||||
|
Database,
|
||||||
|
Monitor as MonitorIcon,
|
||||||
|
Sun,
|
||||||
|
Moon
|
||||||
|
} from '@lucide/svelte';
|
||||||
|
import type { Component } from 'svelte';
|
||||||
|
import type {
|
||||||
|
SettingsConfigValue,
|
||||||
|
SyncableParameter,
|
||||||
|
SettingsEntry,
|
||||||
|
SettingsSectionTitle,
|
||||||
|
SettingsSectionEntry,
|
||||||
|
SettingsSection
|
||||||
|
} from '$lib/types';
|
||||||
|
import { SETTINGS_KEYS } from './settings-keys';
|
||||||
|
import { ROUTES, SETTINGS_SECTION_SLUGS } from './routes';
|
||||||
|
import { TITLE_GENERATION } from './title-generation';
|
||||||
|
|
||||||
|
export const SETTINGS_SECTION_TITLES = {
|
||||||
|
GENERAL: 'General',
|
||||||
|
DISPLAY: 'Display',
|
||||||
|
SAMPLING: 'Sampling',
|
||||||
|
PENALTIES: 'Penalties',
|
||||||
|
AGENTIC: 'Agentic',
|
||||||
|
TOOLS: 'Tools',
|
||||||
|
IMPORT_EXPORT: 'Import/Export',
|
||||||
|
DEVELOPER: 'Developer'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const STANDALONE_SECTIONS: { title: SettingsSectionTitle; slug: string; icon: Component }[] = [
|
||||||
|
{ title: SETTINGS_SECTION_TITLES.TOOLS, slug: SETTINGS_SECTION_SLUGS.TOOLS, icon: PencilRuler },
|
||||||
|
{
|
||||||
|
title: SETTINGS_SECTION_TITLES.IMPORT_EXPORT,
|
||||||
|
slug: SETTINGS_SECTION_SLUGS.IMPORT_EXPORT,
|
||||||
|
icon: Database
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const COLOR_MODE_OPTIONS: Array<{ value: string; label: string; icon: Component }> = [
|
||||||
|
{ value: ColorMode.SYSTEM, label: 'System', icon: MonitorIcon },
|
||||||
|
{ value: ColorMode.LIGHT, label: 'Light', icon: Sun },
|
||||||
|
{ value: ColorMode.DARK, label: 'Dark', icon: Moon }
|
||||||
|
];
|
||||||
|
|
||||||
|
const SETTINGS_REGISTRY: Record<string, SettingsSectionEntry> = {
|
||||||
|
[SETTINGS_SECTION_SLUGS.GENERAL]: {
|
||||||
|
title: SETTINGS_SECTION_TITLES.GENERAL,
|
||||||
|
slug: SETTINGS_SECTION_SLUGS.GENERAL,
|
||||||
|
icon: Sliders,
|
||||||
|
settings: [
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.THEME,
|
||||||
|
label: 'Theme',
|
||||||
|
help: 'Choose the color theme for the interface. You can choose between System (follows your device settings), Light, or Dark.',
|
||||||
|
defaultValue: ColorMode.SYSTEM,
|
||||||
|
type: SettingsFieldType.SELECT,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.GENERAL,
|
||||||
|
options: COLOR_MODE_OPTIONS,
|
||||||
|
sync: { serverKey: SETTINGS_KEYS.THEME, paramType: SyncableParameterType.STRING }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.API_KEY,
|
||||||
|
label: 'API Key',
|
||||||
|
help: 'Set the API Key if you are using <code>--api-key</code> option for the server.',
|
||||||
|
defaultValue: '',
|
||||||
|
type: SettingsFieldType.INPUT,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.GENERAL
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.SYSTEM_MESSAGE,
|
||||||
|
label: 'System Message',
|
||||||
|
help: 'The starting message that defines how model should behave.',
|
||||||
|
defaultValue: '',
|
||||||
|
type: SettingsFieldType.TEXTAREA,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.GENERAL,
|
||||||
|
sync: { serverKey: SETTINGS_KEYS.SYSTEM_MESSAGE, paramType: SyncableParameterType.STRING }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.PASTE_LONG_TEXT_TO_FILE_LEN,
|
||||||
|
label: 'Paste long text to file length',
|
||||||
|
help: 'On pasting long text, it will be converted to a file. You can control the file length by setting the value of this parameter. Value 0 means disable.',
|
||||||
|
defaultValue: 2500,
|
||||||
|
type: SettingsFieldType.INPUT,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.GENERAL,
|
||||||
|
sync: {
|
||||||
|
serverKey: SETTINGS_KEYS.PASTE_LONG_TEXT_TO_FILE_LEN,
|
||||||
|
paramType: SyncableParameterType.NUMBER
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.SEND_ON_ENTER,
|
||||||
|
label: 'Send message on Enter',
|
||||||
|
help: 'Use Enter to send messages and Shift + Enter for new lines. When disabled, use Ctrl/Cmd + Enter.',
|
||||||
|
defaultValue: true,
|
||||||
|
type: SettingsFieldType.CHECKBOX,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.GENERAL,
|
||||||
|
sync: { serverKey: SETTINGS_KEYS.SEND_ON_ENTER, paramType: SyncableParameterType.BOOLEAN }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.COPY_TEXT_ATTACHMENTS_AS_PLAIN_TEXT,
|
||||||
|
label: 'Copy text attachments as plain text',
|
||||||
|
help: 'When copying a message with text attachments, combine them into a single plain text string instead of a special format that can be pasted back as attachments.',
|
||||||
|
defaultValue: false,
|
||||||
|
type: SettingsFieldType.CHECKBOX,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.GENERAL,
|
||||||
|
sync: {
|
||||||
|
serverKey: SETTINGS_KEYS.COPY_TEXT_ATTACHMENTS_AS_PLAIN_TEXT,
|
||||||
|
paramType: SyncableParameterType.BOOLEAN
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.ENABLE_CONTINUE_GENERATION,
|
||||||
|
label: 'Enable "Continue" button',
|
||||||
|
help: 'Enable "Continue" button for assistant messages. Currently works only with non-reasoning models.',
|
||||||
|
defaultValue: false,
|
||||||
|
type: SettingsFieldType.CHECKBOX,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.GENERAL,
|
||||||
|
isExperimental: true,
|
||||||
|
sync: {
|
||||||
|
serverKey: SETTINGS_KEYS.ENABLE_CONTINUE_GENERATION,
|
||||||
|
paramType: SyncableParameterType.BOOLEAN
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.PDF_AS_IMAGE,
|
||||||
|
label: 'Parse PDF as image',
|
||||||
|
help: 'Parse PDF as image instead of text. Automatically falls back to text processing for non-vision models.',
|
||||||
|
defaultValue: false,
|
||||||
|
type: SettingsFieldType.CHECKBOX,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.GENERAL,
|
||||||
|
sync: { serverKey: SETTINGS_KEYS.PDF_AS_IMAGE, paramType: SyncableParameterType.BOOLEAN }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.ASK_FOR_TITLE_CONFIRMATION,
|
||||||
|
label: 'Ask for confirmation before changing conversation title',
|
||||||
|
help: 'Ask for confirmation before automatically changing conversation title when editing the first message.',
|
||||||
|
defaultValue: false,
|
||||||
|
type: SettingsFieldType.CHECKBOX,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.GENERAL,
|
||||||
|
sync: {
|
||||||
|
serverKey: SETTINGS_KEYS.ASK_FOR_TITLE_CONFIRMATION,
|
||||||
|
paramType: SyncableParameterType.BOOLEAN
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.TITLE_GENERATION_USE_FIRST_LINE,
|
||||||
|
label: 'Use first non-empty line for conversation title',
|
||||||
|
help: 'Use only the first non-empty line of the prompt to generate the conversation title.',
|
||||||
|
defaultValue: false,
|
||||||
|
type: SettingsFieldType.CHECKBOX,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.GENERAL,
|
||||||
|
sync: {
|
||||||
|
serverKey: SETTINGS_KEYS.TITLE_GENERATION_USE_FIRST_LINE,
|
||||||
|
paramType: SyncableParameterType.BOOLEAN
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.TITLE_GENERATION_USE_LLM,
|
||||||
|
label: 'Use LLM to generate conversation title',
|
||||||
|
help: 'Use the LLM to automatically generate conversation titles based on the first message exchange.',
|
||||||
|
defaultValue: false,
|
||||||
|
type: SettingsFieldType.CHECKBOX,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.GENERAL,
|
||||||
|
isExperimental: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.TITLE_GENERATION_PROMPT,
|
||||||
|
label: 'LLM title generation prompt',
|
||||||
|
help: 'Optional template for the title generation prompt. Use {{USER}} for the user message and {{ASSISTANT}} for the assistant message.',
|
||||||
|
defaultValue: TITLE_GENERATION.DEFAULT_PROMPT,
|
||||||
|
type: SettingsFieldType.TEXTAREA,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.GENERAL
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[SETTINGS_SECTION_SLUGS.DISPLAY]: {
|
||||||
|
title: SETTINGS_SECTION_TITLES.DISPLAY,
|
||||||
|
slug: SETTINGS_SECTION_SLUGS.DISPLAY,
|
||||||
|
icon: Monitor,
|
||||||
|
settings: [
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.SHOW_MESSAGE_STATS,
|
||||||
|
label: 'Show message generation statistics',
|
||||||
|
help: 'Display generation statistics (tokens/second, token count, duration) below each assistant message.',
|
||||||
|
defaultValue: true,
|
||||||
|
type: SettingsFieldType.CHECKBOX,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.DISPLAY,
|
||||||
|
sync: {
|
||||||
|
serverKey: SETTINGS_KEYS.SHOW_MESSAGE_STATS,
|
||||||
|
paramType: SyncableParameterType.BOOLEAN
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.SHOW_THOUGHT_IN_PROGRESS,
|
||||||
|
label: 'Show thought in progress',
|
||||||
|
help: 'Expand thought process by default when generating messages.',
|
||||||
|
defaultValue: true,
|
||||||
|
type: SettingsFieldType.CHECKBOX,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.DISPLAY,
|
||||||
|
sync: {
|
||||||
|
serverKey: SETTINGS_KEYS.SHOW_THOUGHT_IN_PROGRESS,
|
||||||
|
paramType: SyncableParameterType.BOOLEAN
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.SHOW_TOOL_CALL_IN_PROGRESS,
|
||||||
|
label: 'Show tool call in progress',
|
||||||
|
help: 'Automatically expand tool call details while executing and keep them expanded after completion.',
|
||||||
|
defaultValue: false,
|
||||||
|
type: SettingsFieldType.CHECKBOX,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.DISPLAY,
|
||||||
|
sync: {
|
||||||
|
serverKey: SETTINGS_KEYS.SHOW_TOOL_CALL_IN_PROGRESS,
|
||||||
|
paramType: SyncableParameterType.BOOLEAN
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.KEEP_STATS_VISIBLE,
|
||||||
|
label: 'Keep stats visible after generation',
|
||||||
|
help: 'Keep processing statistics visible after generation finishes.',
|
||||||
|
defaultValue: false,
|
||||||
|
type: SettingsFieldType.CHECKBOX,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.DISPLAY,
|
||||||
|
sync: {
|
||||||
|
serverKey: SETTINGS_KEYS.KEEP_STATS_VISIBLE,
|
||||||
|
paramType: SyncableParameterType.BOOLEAN
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.AUTO_MIC_ON_EMPTY,
|
||||||
|
label: 'Show microphone on empty input',
|
||||||
|
help: 'Automatically show microphone button instead of send button when textarea is empty for models with audio modality support.',
|
||||||
|
defaultValue: false,
|
||||||
|
type: SettingsFieldType.CHECKBOX,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.DISPLAY,
|
||||||
|
isExperimental: true,
|
||||||
|
sync: {
|
||||||
|
serverKey: SETTINGS_KEYS.AUTO_MIC_ON_EMPTY,
|
||||||
|
paramType: SyncableParameterType.BOOLEAN
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.RENDER_USER_CONTENT_AS_MARKDOWN,
|
||||||
|
label: 'Render user content as Markdown',
|
||||||
|
help: 'Render user messages using markdown formatting in the chat.',
|
||||||
|
defaultValue: false,
|
||||||
|
type: SettingsFieldType.CHECKBOX,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.DISPLAY,
|
||||||
|
sync: {
|
||||||
|
serverKey: SETTINGS_KEYS.RENDER_USER_CONTENT_AS_MARKDOWN,
|
||||||
|
paramType: SyncableParameterType.BOOLEAN
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.FULL_HEIGHT_CODE_BLOCKS,
|
||||||
|
label: 'Use full height code blocks',
|
||||||
|
help: 'Always display code blocks at their full natural height, overriding any height limits.',
|
||||||
|
defaultValue: false,
|
||||||
|
type: SettingsFieldType.CHECKBOX,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.DISPLAY,
|
||||||
|
sync: {
|
||||||
|
serverKey: SETTINGS_KEYS.FULL_HEIGHT_CODE_BLOCKS,
|
||||||
|
paramType: SyncableParameterType.BOOLEAN
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.DISABLE_AUTO_SCROLL,
|
||||||
|
label: 'Disable automatic scroll',
|
||||||
|
help: 'Disable automatic scrolling while messages stream so you can control the viewport position manually.',
|
||||||
|
defaultValue: false,
|
||||||
|
type: SettingsFieldType.CHECKBOX,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.DISPLAY,
|
||||||
|
sync: {
|
||||||
|
serverKey: SETTINGS_KEYS.DISABLE_AUTO_SCROLL,
|
||||||
|
paramType: SyncableParameterType.BOOLEAN
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.ALWAYS_SHOW_SIDEBAR_ON_DESKTOP,
|
||||||
|
label: 'Always show sidebar on desktop',
|
||||||
|
help: 'Always keep the sidebar visible on desktop instead of auto-hiding it.',
|
||||||
|
defaultValue: false,
|
||||||
|
type: SettingsFieldType.CHECKBOX,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.DISPLAY,
|
||||||
|
sync: {
|
||||||
|
serverKey: SETTINGS_KEYS.ALWAYS_SHOW_SIDEBAR_ON_DESKTOP,
|
||||||
|
paramType: SyncableParameterType.BOOLEAN
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.SHOW_RAW_MODEL_NAMES,
|
||||||
|
label: 'Show raw model names',
|
||||||
|
help: 'Display full raw model identifiers (e.g. "ggml-org/GLM-4.7-Flash-GGUF:Q8_0") instead of parsed names with badges.',
|
||||||
|
defaultValue: false,
|
||||||
|
type: SettingsFieldType.CHECKBOX,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.DISPLAY,
|
||||||
|
sync: {
|
||||||
|
serverKey: SETTINGS_KEYS.SHOW_RAW_MODEL_NAMES,
|
||||||
|
paramType: SyncableParameterType.BOOLEAN
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.ALWAYS_SHOW_AGENTIC_TURNS,
|
||||||
|
label: 'Always show agentic turns in conversation',
|
||||||
|
help: 'Always expand and display agentic loop turns in conversation messages.',
|
||||||
|
defaultValue: false,
|
||||||
|
type: SettingsFieldType.CHECKBOX,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.DISPLAY,
|
||||||
|
sync: {
|
||||||
|
serverKey: SETTINGS_KEYS.ALWAYS_SHOW_AGENTIC_TURNS,
|
||||||
|
paramType: SyncableParameterType.BOOLEAN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[SETTINGS_SECTION_SLUGS.SAMPLING]: {
|
||||||
|
title: SETTINGS_SECTION_TITLES.SAMPLING,
|
||||||
|
slug: SETTINGS_SECTION_SLUGS.SAMPLING,
|
||||||
|
icon: Funnel,
|
||||||
|
settings: [
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.TEMPERATURE,
|
||||||
|
label: 'Temperature',
|
||||||
|
help: 'Controls the randomness of the generated text by affecting the probability distribution of the output tokens. Higher = more random, lower = more focused.',
|
||||||
|
defaultValue: undefined,
|
||||||
|
type: SettingsFieldType.INPUT,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.SAMPLING,
|
||||||
|
sync: { serverKey: SETTINGS_KEYS.TEMPERATURE, paramType: SyncableParameterType.NUMBER }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.DYNATEMP_RANGE,
|
||||||
|
label: 'Dynamic temperature range',
|
||||||
|
help: 'Addon for the temperature sampler. The added value to the range of dynamic temperature, which adjusts probabilities by entropy of tokens.',
|
||||||
|
defaultValue: undefined,
|
||||||
|
type: SettingsFieldType.INPUT,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.SAMPLING,
|
||||||
|
sync: { serverKey: SETTINGS_KEYS.DYNATEMP_RANGE, paramType: SyncableParameterType.NUMBER }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.DYNATEMP_EXPONENT,
|
||||||
|
label: 'Dynamic temperature exponent',
|
||||||
|
help: 'Addon for the temperature sampler. Smoothes out the probability redistribution based on the most probable token.',
|
||||||
|
defaultValue: undefined,
|
||||||
|
type: SettingsFieldType.INPUT,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.SAMPLING,
|
||||||
|
sync: {
|
||||||
|
serverKey: SETTINGS_KEYS.DYNATEMP_EXPONENT,
|
||||||
|
paramType: SyncableParameterType.NUMBER
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.TOP_K,
|
||||||
|
label: 'Top K',
|
||||||
|
help: 'Keeps only k top tokens.',
|
||||||
|
defaultValue: undefined,
|
||||||
|
type: SettingsFieldType.INPUT,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.SAMPLING,
|
||||||
|
sync: { serverKey: SETTINGS_KEYS.TOP_K, paramType: SyncableParameterType.NUMBER }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.TOP_P,
|
||||||
|
label: 'Top P',
|
||||||
|
help: 'Limits tokens to those that together have a cumulative probability of at least p',
|
||||||
|
defaultValue: undefined,
|
||||||
|
type: SettingsFieldType.INPUT,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.SAMPLING,
|
||||||
|
sync: { serverKey: SETTINGS_KEYS.TOP_P, paramType: SyncableParameterType.NUMBER }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.MIN_P,
|
||||||
|
label: 'Min P',
|
||||||
|
help: 'Limits tokens based on the minimum probability for a token to be considered, relative to the probability of the most likely token.',
|
||||||
|
defaultValue: undefined,
|
||||||
|
type: SettingsFieldType.INPUT,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.SAMPLING,
|
||||||
|
sync: { serverKey: SETTINGS_KEYS.MIN_P, paramType: SyncableParameterType.NUMBER }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.XTC_PROBABILITY,
|
||||||
|
label: 'XTC probability',
|
||||||
|
help: 'XTC sampler cuts out top tokens; this parameter controls the chance of cutting tokens at all. 0 disables XTC.',
|
||||||
|
defaultValue: undefined,
|
||||||
|
type: SettingsFieldType.INPUT,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.SAMPLING,
|
||||||
|
sync: { serverKey: SETTINGS_KEYS.XTC_PROBABILITY, paramType: SyncableParameterType.NUMBER }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.XTC_THRESHOLD,
|
||||||
|
label: 'XTC threshold',
|
||||||
|
help: 'XTC sampler cuts out top tokens; this parameter controls the token probability that is required to cut that token.',
|
||||||
|
defaultValue: undefined,
|
||||||
|
type: SettingsFieldType.INPUT,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.SAMPLING,
|
||||||
|
sync: { serverKey: SETTINGS_KEYS.XTC_THRESHOLD, paramType: SyncableParameterType.NUMBER }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.TYP_P,
|
||||||
|
label: 'Typical P',
|
||||||
|
help: 'Sorts and limits tokens based on the difference between log-probability and entropy.',
|
||||||
|
defaultValue: undefined,
|
||||||
|
type: SettingsFieldType.INPUT,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.SAMPLING,
|
||||||
|
sync: { serverKey: SETTINGS_KEYS.TYP_P, paramType: SyncableParameterType.NUMBER }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.MAX_TOKENS,
|
||||||
|
label: 'Max tokens',
|
||||||
|
help: 'The maximum number of token per output. Use -1 for infinite (no limit).',
|
||||||
|
defaultValue: undefined,
|
||||||
|
type: SettingsFieldType.INPUT,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.SAMPLING,
|
||||||
|
sync: { serverKey: SETTINGS_KEYS.MAX_TOKENS, paramType: SyncableParameterType.NUMBER }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.SAMPLERS,
|
||||||
|
label: 'Samplers',
|
||||||
|
help: 'The order at which samplers are applied, in simplified way. Default is "top_k;typ_p;top_p;min_p;temperature": top_k->typ_p->top_p->min_p->temperature',
|
||||||
|
defaultValue: '',
|
||||||
|
type: SettingsFieldType.INPUT,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.SAMPLING,
|
||||||
|
sync: { serverKey: SETTINGS_KEYS.SAMPLERS, paramType: SyncableParameterType.STRING }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.BACKEND_SAMPLING,
|
||||||
|
label: 'Backend sampling',
|
||||||
|
help: 'Enable backend-based samplers. When enabled, supported samplers run on the accelerator backend for faster sampling.',
|
||||||
|
defaultValue: false,
|
||||||
|
type: SettingsFieldType.CHECKBOX,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.SAMPLING,
|
||||||
|
sync: {
|
||||||
|
serverKey: SETTINGS_KEYS.BACKEND_SAMPLING,
|
||||||
|
paramType: SyncableParameterType.BOOLEAN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[SETTINGS_SECTION_SLUGS.PENALTIES]: {
|
||||||
|
title: SETTINGS_SECTION_TITLES.PENALTIES,
|
||||||
|
slug: SETTINGS_SECTION_SLUGS.PENALTIES,
|
||||||
|
icon: AlertTriangle,
|
||||||
|
settings: [
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.REPEAT_LAST_N,
|
||||||
|
label: 'Repeat last N',
|
||||||
|
help: 'Last n tokens to consider for penalizing repetition',
|
||||||
|
defaultValue: undefined,
|
||||||
|
type: SettingsFieldType.INPUT,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.PENALTIES,
|
||||||
|
sync: { serverKey: SETTINGS_KEYS.REPEAT_LAST_N, paramType: SyncableParameterType.NUMBER }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.REPEAT_PENALTY,
|
||||||
|
label: 'Repeat penalty',
|
||||||
|
help: 'Controls the repetition of token sequences in the generated text',
|
||||||
|
defaultValue: undefined,
|
||||||
|
type: SettingsFieldType.INPUT,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.PENALTIES,
|
||||||
|
sync: { serverKey: SETTINGS_KEYS.REPEAT_PENALTY, paramType: SyncableParameterType.NUMBER }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.PRESENCE_PENALTY,
|
||||||
|
label: 'Presence penalty',
|
||||||
|
help: 'Limits tokens based on whether they appear in the output or not.',
|
||||||
|
defaultValue: undefined,
|
||||||
|
type: SettingsFieldType.INPUT,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.PENALTIES,
|
||||||
|
sync: { serverKey: SETTINGS_KEYS.PRESENCE_PENALTY, paramType: SyncableParameterType.NUMBER }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.FREQUENCY_PENALTY,
|
||||||
|
label: 'Frequency penalty',
|
||||||
|
help: 'Limits tokens based on how often they appear in the output.',
|
||||||
|
defaultValue: undefined,
|
||||||
|
type: SettingsFieldType.INPUT,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.PENALTIES,
|
||||||
|
sync: {
|
||||||
|
serverKey: SETTINGS_KEYS.FREQUENCY_PENALTY,
|
||||||
|
paramType: SyncableParameterType.NUMBER
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.DRY_MULTIPLIER,
|
||||||
|
label: 'DRY multiplier',
|
||||||
|
help: 'DRY sampling reduces repetition in generated text even across long contexts. This parameter sets the DRY sampling multiplier.',
|
||||||
|
defaultValue: undefined,
|
||||||
|
type: SettingsFieldType.INPUT,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.PENALTIES,
|
||||||
|
sync: { serverKey: SETTINGS_KEYS.DRY_MULTIPLIER, paramType: SyncableParameterType.NUMBER }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.DRY_BASE,
|
||||||
|
label: 'DRY base',
|
||||||
|
help: 'DRY sampling reduces repetition in generated text even across long contexts. This parameter sets the DRY sampling base value.',
|
||||||
|
defaultValue: undefined,
|
||||||
|
type: SettingsFieldType.INPUT,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.PENALTIES,
|
||||||
|
sync: { serverKey: SETTINGS_KEYS.DRY_BASE, paramType: SyncableParameterType.NUMBER }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.DRY_ALLOWED_LENGTH,
|
||||||
|
label: 'DRY allowed length',
|
||||||
|
help: 'DRY sampling reduces repetition in generated text even across long contexts. This parameter sets the allowed length for DRY sampling.',
|
||||||
|
defaultValue: undefined,
|
||||||
|
type: SettingsFieldType.INPUT,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.PENALTIES,
|
||||||
|
sync: {
|
||||||
|
serverKey: SETTINGS_KEYS.DRY_ALLOWED_LENGTH,
|
||||||
|
paramType: SyncableParameterType.NUMBER
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.DRY_PENALTY_LAST_N,
|
||||||
|
label: 'DRY penalty last N',
|
||||||
|
help: 'DRY sampling reduces repetition in generated text even across long contexts. This parameter sets DRY penalty for the last n tokens.',
|
||||||
|
defaultValue: undefined,
|
||||||
|
type: SettingsFieldType.INPUT,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.PENALTIES,
|
||||||
|
sync: {
|
||||||
|
serverKey: SETTINGS_KEYS.DRY_PENALTY_LAST_N,
|
||||||
|
paramType: SyncableParameterType.NUMBER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[SETTINGS_SECTION_SLUGS.AGENTIC]: {
|
||||||
|
title: SETTINGS_SECTION_TITLES.AGENTIC,
|
||||||
|
slug: SETTINGS_SECTION_SLUGS.AGENTIC,
|
||||||
|
icon: ListRestart,
|
||||||
|
settings: [
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.AGENTIC_MAX_TURNS,
|
||||||
|
label: 'Agentic turns',
|
||||||
|
help: 'Maximum number of tool execution cycles before stopping (prevents infinite loops).',
|
||||||
|
defaultValue: 10,
|
||||||
|
type: SettingsFieldType.INPUT,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.AGENTIC,
|
||||||
|
isPositiveInteger: true,
|
||||||
|
sync: {
|
||||||
|
serverKey: SETTINGS_KEYS.AGENTIC_MAX_TURNS,
|
||||||
|
paramType: SyncableParameterType.NUMBER
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.AGENTIC_MAX_TOOL_PREVIEW_LINES,
|
||||||
|
label: 'Max lines per tool preview',
|
||||||
|
help: 'Number of lines shown in tool output previews (last N lines). Only these previews and the final LLM response persist after the agentic loop completes.',
|
||||||
|
defaultValue: 25,
|
||||||
|
type: SettingsFieldType.INPUT,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.AGENTIC,
|
||||||
|
isPositiveInteger: true,
|
||||||
|
sync: {
|
||||||
|
serverKey: SETTINGS_KEYS.AGENTIC_MAX_TOOL_PREVIEW_LINES,
|
||||||
|
paramType: SyncableParameterType.NUMBER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[SETTINGS_SECTION_SLUGS.DEVELOPER]: {
|
||||||
|
title: SETTINGS_SECTION_TITLES.DEVELOPER,
|
||||||
|
slug: SETTINGS_SECTION_SLUGS.DEVELOPER,
|
||||||
|
icon: Code,
|
||||||
|
settings: [
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.PRE_ENCODE_CONVERSATION,
|
||||||
|
label: 'Pre-fill KV cache after response',
|
||||||
|
help: 'After each response, re-submit the conversation to pre-fill the server KV cache. Makes the next turn faster since the prompt is already encoded while you read the response.',
|
||||||
|
defaultValue: false,
|
||||||
|
type: SettingsFieldType.CHECKBOX,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.DEVELOPER
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.DISABLE_REASONING_PARSING,
|
||||||
|
label: 'Disable reasoning content parsing',
|
||||||
|
help: 'Send reasoning_format=none so the server returns thinking tokens inline instead of extracting them into a separate field.',
|
||||||
|
defaultValue: false,
|
||||||
|
type: SettingsFieldType.CHECKBOX,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.DEVELOPER
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.EXCLUDE_REASONING_FROM_CONTEXT,
|
||||||
|
label: 'Exclude reasoning from context',
|
||||||
|
help: 'Strip thinking from previous messages before sending. When off, thinking is sent back via the reasoning_content field so the model sees its own chain-of-thought across turns.',
|
||||||
|
defaultValue: false,
|
||||||
|
type: SettingsFieldType.CHECKBOX,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.DEVELOPER,
|
||||||
|
sync: {
|
||||||
|
serverKey: SETTINGS_KEYS.EXCLUDE_REASONING_FROM_CONTEXT,
|
||||||
|
paramType: SyncableParameterType.BOOLEAN
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.SHOW_RAW_OUTPUT_SWITCH,
|
||||||
|
label: 'Enable raw output toggle',
|
||||||
|
help: 'Show toggle button to display messages as plain text instead of Markdown-formatted content',
|
||||||
|
defaultValue: false,
|
||||||
|
type: SettingsFieldType.CHECKBOX,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.DEVELOPER,
|
||||||
|
sync: {
|
||||||
|
serverKey: SETTINGS_KEYS.SHOW_RAW_OUTPUT_SWITCH,
|
||||||
|
paramType: SyncableParameterType.BOOLEAN
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.CUSTOM,
|
||||||
|
label: 'Custom JSON',
|
||||||
|
help: 'Custom JSON parameters to send to the API. Must be valid JSON format.',
|
||||||
|
defaultValue: '',
|
||||||
|
type: SettingsFieldType.TEXTAREA,
|
||||||
|
section: SETTINGS_SECTION_SLUGS.DEVELOPER
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const NON_UI_SETTINGS: SettingsEntry[] = [
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.SHOW_SYSTEM_MESSAGE,
|
||||||
|
label: 'Show system message',
|
||||||
|
help: 'Display the system message at the top of each conversation.',
|
||||||
|
defaultValue: true,
|
||||||
|
type: SettingsFieldType.CHECKBOX,
|
||||||
|
sync: { serverKey: SETTINGS_KEYS.SHOW_SYSTEM_MESSAGE, paramType: SyncableParameterType.BOOLEAN }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.MCP_SERVERS,
|
||||||
|
label: 'MCP servers',
|
||||||
|
help: 'Configure MCP servers as a JSON list. Use the form in the MCP Client settings section to edit.',
|
||||||
|
defaultValue: '[]',
|
||||||
|
type: SettingsFieldType.INPUT,
|
||||||
|
sync: { serverKey: SETTINGS_KEYS.MCP_SERVERS, paramType: SyncableParameterType.STRING }
|
||||||
|
}
|
||||||
|
// {
|
||||||
|
// key: SETTINGS_KEYS.PY_INTERPRETER_ENABLED,
|
||||||
|
// label: 'Python interpreter enabled',
|
||||||
|
// help: 'Enable Python interpreter using Pyodide. Allows running Python code in markdown code blocks.',
|
||||||
|
// defaultValue: false,
|
||||||
|
// type: SettingsFieldType.CHECKBOX,
|
||||||
|
// isExperimental: true,
|
||||||
|
// sync: { serverKey: SETTINGS_KEYS.PY_INTERPRETER_ENABLED, paramType: SyncableParameterType.BOOLEAN }
|
||||||
|
// }
|
||||||
|
];
|
||||||
|
|
||||||
|
function getAllSettings(): SettingsEntry[] {
|
||||||
|
const result: SettingsEntry[] = [];
|
||||||
|
for (const section of Object.values(SETTINGS_REGISTRY)) {
|
||||||
|
result.push(...section.settings);
|
||||||
|
}
|
||||||
|
result.push(...NON_UI_SETTINGS);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Flat config object stored in localStorage. */
|
||||||
|
export const SETTING_CONFIG_DEFAULT: Record<string, SettingsConfigValue> = Object.fromEntries(
|
||||||
|
getAllSettings().map((s) => [s.key, s.defaultValue])
|
||||||
|
) as Record<string, SettingsConfigValue>;
|
||||||
|
|
||||||
|
/** Help text for every setting (including non-UI). */
|
||||||
|
export const SETTING_CONFIG_INFO: Record<string, string> = Object.fromEntries(
|
||||||
|
getAllSettings().map((s) => [s.key, s.help])
|
||||||
|
) as Record<string, string>;
|
||||||
|
|
||||||
|
/** Theme select options. */
|
||||||
|
export const SETTINGS_COLOR_MODES_CONFIG = COLOR_MODE_OPTIONS;
|
||||||
|
|
||||||
|
export type { SettingsSectionTitle } from '$lib/types';
|
||||||
|
export type { SettingsSection } from '$lib/types';
|
||||||
|
|
||||||
|
/** Sidebar sections + field configs (as consumed by UI). */
|
||||||
|
export const SETTINGS_CHAT_SECTIONS: SettingsSection[] = [
|
||||||
|
...Object.values(SETTINGS_REGISTRY).map((section) => ({
|
||||||
|
title: section.title,
|
||||||
|
slug: section.slug,
|
||||||
|
icon: section.icon,
|
||||||
|
fields: section.settings.map((s) => ({
|
||||||
|
key: s.key,
|
||||||
|
label: s.label,
|
||||||
|
type: s.type,
|
||||||
|
isExperimental: s.isExperimental,
|
||||||
|
help: s.help,
|
||||||
|
options: s.options
|
||||||
|
}))
|
||||||
|
})),
|
||||||
|
...STANDALONE_SECTIONS
|
||||||
|
];
|
||||||
|
|
||||||
|
/** INPUT-type settings whose value is a number. */
|
||||||
|
export const NUMERIC_FIELDS = getAllSettings()
|
||||||
|
.filter((s) => s.type === SettingsFieldType.INPUT && typeof s.defaultValue !== 'string')
|
||||||
|
.map((s) => s.key) as readonly string[];
|
||||||
|
|
||||||
|
/** Numeric fields clamped to ≥ 1 and rounded. */
|
||||||
|
export const POSITIVE_INTEGER_FIELDS = getAllSettings()
|
||||||
|
.filter((s) => s.isPositiveInteger)
|
||||||
|
.map((s) => s.key) as readonly string[];
|
||||||
|
|
||||||
|
/** Derived for the parameter sync service. */
|
||||||
|
export const SYNCABLE_PARAMETERS: SyncableParameter[] = getAllSettings()
|
||||||
|
.filter((s) => s.sync !== undefined)
|
||||||
|
.map((s) => ({
|
||||||
|
key: s.key,
|
||||||
|
serverKey: s.sync!.serverKey,
|
||||||
|
type: s.sync!.paramType,
|
||||||
|
canSync: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const SETTINGS_FALLBACK_EXIT_ROUTE = ROUTES.START;
|
||||||
|
|
||||||
|
export { SETTINGS_KEYS } from './settings-keys';
|
||||||
@@ -1,348 +0,0 @@
|
|||||||
/**
|
|
||||||
* Settings section titles constants for ChatSettings component.
|
|
||||||
*
|
|
||||||
* These titles define the navigation sections in the settings dialog.
|
|
||||||
* Used for both sidebar navigation and mobile horizontal scroll menu.
|
|
||||||
*/
|
|
||||||
export const SETTINGS_SECTION_TITLES = {
|
|
||||||
GENERAL: 'General',
|
|
||||||
DISPLAY: 'Display',
|
|
||||||
SAMPLING: 'Sampling',
|
|
||||||
PENALTIES: 'Penalties',
|
|
||||||
AGENTIC: 'Agentic',
|
|
||||||
TOOLS: 'Tools',
|
|
||||||
MCP: 'MCP',
|
|
||||||
IMPORT_EXPORT: 'Import/Export',
|
|
||||||
DEVELOPER: 'Developer'
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
/** Type for settings section titles */
|
|
||||||
export type SettingsSectionTitle =
|
|
||||||
(typeof SETTINGS_SECTION_TITLES)[keyof typeof SETTINGS_SECTION_TITLES];
|
|
||||||
|
|
||||||
import {
|
|
||||||
Funnel,
|
|
||||||
AlertTriangle,
|
|
||||||
Code,
|
|
||||||
Monitor,
|
|
||||||
ListRestart,
|
|
||||||
Sliders,
|
|
||||||
PencilRuler,
|
|
||||||
Database
|
|
||||||
} from '@lucide/svelte';
|
|
||||||
import { SettingsFieldType } from '$lib/enums/settings';
|
|
||||||
import { SETTINGS_COLOR_MODES_CONFIG } from '$lib/constants/settings-config';
|
|
||||||
import { SETTINGS_KEYS } from '$lib/constants/settings-keys';
|
|
||||||
import type { Component } from 'svelte';
|
|
||||||
|
|
||||||
export interface SettingsSection {
|
|
||||||
fields?: SettingsFieldConfig[];
|
|
||||||
icon: Component;
|
|
||||||
slug: string;
|
|
||||||
title: SettingsSectionTitle;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SETTINGS_CHAT_SECTIONS: SettingsSection[] = [
|
|
||||||
{
|
|
||||||
title: SETTINGS_SECTION_TITLES.GENERAL,
|
|
||||||
slug: 'general',
|
|
||||||
icon: Sliders,
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.THEME,
|
|
||||||
label: 'Theme',
|
|
||||||
type: SettingsFieldType.SELECT,
|
|
||||||
options: SETTINGS_COLOR_MODES_CONFIG
|
|
||||||
},
|
|
||||||
{ key: SETTINGS_KEYS.API_KEY, label: 'API Key', type: SettingsFieldType.INPUT },
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.SYSTEM_MESSAGE,
|
|
||||||
label: 'System Message',
|
|
||||||
type: SettingsFieldType.TEXTAREA
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.PASTE_LONG_TEXT_TO_FILE_LEN,
|
|
||||||
label: 'Paste long text to file length',
|
|
||||||
type: SettingsFieldType.INPUT
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.SEND_ON_ENTER,
|
|
||||||
label: 'Send message on Enter',
|
|
||||||
type: SettingsFieldType.CHECKBOX
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.COPY_TEXT_ATTACHMENTS_AS_PLAIN_TEXT,
|
|
||||||
label: 'Copy text attachments as plain text',
|
|
||||||
type: SettingsFieldType.CHECKBOX
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.ENABLE_CONTINUE_GENERATION,
|
|
||||||
label: 'Enable "Continue" button',
|
|
||||||
type: SettingsFieldType.CHECKBOX,
|
|
||||||
isExperimental: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.PDF_AS_IMAGE,
|
|
||||||
label: 'Parse PDF as image',
|
|
||||||
type: SettingsFieldType.CHECKBOX
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.ASK_FOR_TITLE_CONFIRMATION,
|
|
||||||
label: 'Ask for confirmation before changing conversation title',
|
|
||||||
type: SettingsFieldType.CHECKBOX
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.TITLE_GENERATION_USE_FIRST_LINE,
|
|
||||||
label: 'Use first non-empty line for conversation title',
|
|
||||||
type: SettingsFieldType.CHECKBOX
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.TITLE_GENERATION_USE_LLM,
|
|
||||||
label: 'Use LLM to generate conversation title',
|
|
||||||
type: SettingsFieldType.CHECKBOX,
|
|
||||||
isExperimental: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.TITLE_GENERATION_PROMPT,
|
|
||||||
type: SettingsFieldType.TEXTAREA,
|
|
||||||
isExperimental: true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: SETTINGS_SECTION_TITLES.DISPLAY,
|
|
||||||
slug: 'display',
|
|
||||||
icon: Monitor,
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.SHOW_MESSAGE_STATS,
|
|
||||||
label: 'Show message generation statistics',
|
|
||||||
type: SettingsFieldType.CHECKBOX
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.SHOW_THOUGHT_IN_PROGRESS,
|
|
||||||
label: 'Show thought in progress',
|
|
||||||
type: SettingsFieldType.CHECKBOX
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.SHOW_TOOL_CALL_IN_PROGRESS,
|
|
||||||
label: 'Show tool call in progress',
|
|
||||||
type: SettingsFieldType.CHECKBOX
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.KEEP_STATS_VISIBLE,
|
|
||||||
label: 'Keep stats visible after generation',
|
|
||||||
type: SettingsFieldType.CHECKBOX
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.AUTO_MIC_ON_EMPTY,
|
|
||||||
label: 'Show microphone on empty input',
|
|
||||||
type: SettingsFieldType.CHECKBOX,
|
|
||||||
isExperimental: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.RENDER_USER_CONTENT_AS_MARKDOWN,
|
|
||||||
label: 'Render user content as Markdown',
|
|
||||||
type: SettingsFieldType.CHECKBOX
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.FULL_HEIGHT_CODE_BLOCKS,
|
|
||||||
label: 'Use full height code blocks',
|
|
||||||
type: SettingsFieldType.CHECKBOX
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.DISABLE_AUTO_SCROLL,
|
|
||||||
label: 'Disable automatic scroll',
|
|
||||||
type: SettingsFieldType.CHECKBOX
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.ALWAYS_SHOW_SIDEBAR_ON_DESKTOP,
|
|
||||||
label: 'Always show sidebar on desktop',
|
|
||||||
type: SettingsFieldType.CHECKBOX
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.SHOW_RAW_MODEL_NAMES,
|
|
||||||
label: 'Show raw model names',
|
|
||||||
type: SettingsFieldType.CHECKBOX
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.ALWAYS_SHOW_AGENTIC_TURNS,
|
|
||||||
label: 'Always show agentic turns in conversation',
|
|
||||||
type: SettingsFieldType.CHECKBOX
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: SETTINGS_SECTION_TITLES.SAMPLING,
|
|
||||||
slug: 'sampling',
|
|
||||||
icon: Funnel,
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.TEMPERATURE,
|
|
||||||
label: 'Temperature',
|
|
||||||
type: SettingsFieldType.INPUT
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.DYNATEMP_RANGE,
|
|
||||||
label: 'Dynamic temperature range',
|
|
||||||
type: SettingsFieldType.INPUT
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.DYNATEMP_EXPONENT,
|
|
||||||
label: 'Dynamic temperature exponent',
|
|
||||||
type: SettingsFieldType.INPUT
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.TOP_K,
|
|
||||||
label: 'Top K',
|
|
||||||
type: SettingsFieldType.INPUT
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.TOP_P,
|
|
||||||
label: 'Top P',
|
|
||||||
type: SettingsFieldType.INPUT
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.MIN_P,
|
|
||||||
label: 'Min P',
|
|
||||||
type: SettingsFieldType.INPUT
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.XTC_PROBABILITY,
|
|
||||||
label: 'XTC probability',
|
|
||||||
type: SettingsFieldType.INPUT
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.XTC_THRESHOLD,
|
|
||||||
label: 'XTC threshold',
|
|
||||||
type: SettingsFieldType.INPUT
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.TYP_P,
|
|
||||||
label: 'Typical P',
|
|
||||||
type: SettingsFieldType.INPUT
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.MAX_TOKENS,
|
|
||||||
label: 'Max tokens',
|
|
||||||
type: SettingsFieldType.INPUT
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.SAMPLERS,
|
|
||||||
label: 'Samplers',
|
|
||||||
type: SettingsFieldType.INPUT
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.BACKEND_SAMPLING,
|
|
||||||
label: 'Backend sampling',
|
|
||||||
type: SettingsFieldType.CHECKBOX
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: SETTINGS_SECTION_TITLES.PENALTIES,
|
|
||||||
slug: 'penalties',
|
|
||||||
icon: AlertTriangle,
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.REPEAT_LAST_N,
|
|
||||||
label: 'Repeat last N',
|
|
||||||
type: SettingsFieldType.INPUT
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.REPEAT_PENALTY,
|
|
||||||
label: 'Repeat penalty',
|
|
||||||
type: SettingsFieldType.INPUT
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.PRESENCE_PENALTY,
|
|
||||||
label: 'Presence penalty',
|
|
||||||
type: SettingsFieldType.INPUT
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.FREQUENCY_PENALTY,
|
|
||||||
label: 'Frequency penalty',
|
|
||||||
type: SettingsFieldType.INPUT
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.DRY_MULTIPLIER,
|
|
||||||
label: 'DRY multiplier',
|
|
||||||
type: SettingsFieldType.INPUT
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.DRY_BASE,
|
|
||||||
label: 'DRY base',
|
|
||||||
type: SettingsFieldType.INPUT
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.DRY_ALLOWED_LENGTH,
|
|
||||||
label: 'DRY allowed length',
|
|
||||||
type: SettingsFieldType.INPUT
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.DRY_PENALTY_LAST_N,
|
|
||||||
label: 'DRY penalty last N',
|
|
||||||
type: SettingsFieldType.INPUT
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: SETTINGS_SECTION_TITLES.AGENTIC,
|
|
||||||
slug: 'agentic',
|
|
||||||
icon: ListRestart,
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.AGENTIC_MAX_TURNS,
|
|
||||||
label: 'Agentic turns',
|
|
||||||
type: SettingsFieldType.INPUT
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.AGENTIC_MAX_TOOL_PREVIEW_LINES,
|
|
||||||
label: 'Max lines per tool preview',
|
|
||||||
type: SettingsFieldType.INPUT
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: SETTINGS_SECTION_TITLES.TOOLS,
|
|
||||||
slug: 'tools',
|
|
||||||
icon: PencilRuler
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: SETTINGS_SECTION_TITLES.IMPORT_EXPORT,
|
|
||||||
slug: 'import-export',
|
|
||||||
icon: Database
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: SETTINGS_SECTION_TITLES.DEVELOPER,
|
|
||||||
slug: 'developer',
|
|
||||||
icon: Code,
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.PRE_ENCODE_CONVERSATION,
|
|
||||||
label: 'Pre-fill KV cache after response',
|
|
||||||
type: SettingsFieldType.CHECKBOX
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.DISABLE_REASONING_PARSING,
|
|
||||||
label: 'Disable reasoning content parsing',
|
|
||||||
type: SettingsFieldType.CHECKBOX
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.EXCLUDE_REASONING_FROM_CONTEXT,
|
|
||||||
label: 'Exclude reasoning from context',
|
|
||||||
type: SettingsFieldType.CHECKBOX
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.SHOW_RAW_OUTPUT_SWITCH,
|
|
||||||
label: 'Enable raw output toggle',
|
|
||||||
type: SettingsFieldType.CHECKBOX
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: SETTINGS_KEYS.CUSTOM,
|
|
||||||
label: 'Custom JSON',
|
|
||||||
type: SettingsFieldType.TEXTAREA
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/* Title generation constants */
|
/* Title generation constants */
|
||||||
export const TITLE = {
|
export const TITLE_GENERATION = {
|
||||||
MIN_LENGTH: 3,
|
MIN_LENGTH: 3,
|
||||||
FALLBACK: 'New Chat',
|
FALLBACK: 'New Chat',
|
||||||
DEFAULT_PROMPT:
|
DEFAULT_PROMPT:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Settings, Search, SquarePen } from '@lucide/svelte';
|
import { Settings, Search, SquarePen } from '@lucide/svelte';
|
||||||
import McpLogo from '$lib/components/app/mcp/McpLogo.svelte';
|
import McpLogo from '$lib/components/app/mcp/McpLogo.svelte';
|
||||||
import type { Component } from 'svelte';
|
import type { Component } from 'svelte';
|
||||||
|
import { ROUTES } from './routes';
|
||||||
|
|
||||||
export const FORK_TREE_DEPTH_PADDING = 8;
|
export const FORK_TREE_DEPTH_PADDING = 8;
|
||||||
export const SYSTEM_MESSAGE_PLACEHOLDER = 'System message';
|
export const SYSTEM_MESSAGE_PLACEHOLDER = 'System message';
|
||||||
@@ -19,18 +20,18 @@ export interface DesktopIconStripItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const SIDEBAR_ACTIONS_ITEMS: DesktopIconStripItem[] = [
|
export const SIDEBAR_ACTIONS_ITEMS: DesktopIconStripItem[] = [
|
||||||
{ icon: SquarePen, tooltip: 'New chat', route: '?new_chat=true#/', keys: ['shift', 'cmd', 'o'] },
|
{ icon: SquarePen, tooltip: 'New chat', route: ROUTES.NEW_CHAT, keys: ['shift', 'cmd', 'o'] },
|
||||||
{ icon: Search, tooltip: 'Search', keys: ['cmd', 'k'] },
|
{ icon: Search, tooltip: 'Search', keys: ['cmd', 'k'] },
|
||||||
{
|
{
|
||||||
icon: McpLogo,
|
icon: McpLogo,
|
||||||
tooltip: 'MCP Servers',
|
tooltip: 'MCP Servers',
|
||||||
route: '#/settings/mcp',
|
route: ROUTES.MCP_SERVERS,
|
||||||
activeRouteId: '/settings/mcp'
|
activeRouteId: '/mcp-servers'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: Settings,
|
icon: Settings,
|
||||||
tooltip: 'Settings',
|
tooltip: 'Settings',
|
||||||
route: '#/settings/chat/general',
|
route: ROUTES.SETTINGS,
|
||||||
activeRoutePrefix: '/settings/chat'
|
activeRoutePrefix: '/settings'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { KeyboardKey } from '$lib/enums';
|
import { KeyboardKey } from '$lib/enums';
|
||||||
|
import { ROUTES } from '$lib/constants/routes';
|
||||||
|
|
||||||
interface KeyboardShortcutsCallbacks {
|
interface KeyboardShortcutsCallbacks {
|
||||||
activateSearchMode?: () => void;
|
activateSearchMode?: () => void;
|
||||||
@@ -27,7 +28,7 @@ export function useKeyboardShortcuts(callbacks: KeyboardShortcutsCallbacks) {
|
|||||||
) {
|
) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
goto('?new_chat=true#/');
|
goto(ROUTES.NEW_CHAT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.shiftKey && isCmdOrCtrl && event.key === KeyboardKey.E_UPPER) {
|
if (event.shiftKey && isCmdOrCtrl && event.key === KeyboardKey.E_UPPER) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { beforeNavigate } from '$app/navigation';
|
import { beforeNavigate } from '$app/navigation';
|
||||||
import { settingsReferrer } from '$lib/stores/settings-referrer.svelte';
|
import { settingsReferrer } from '$lib/stores/settings-referrer.svelte';
|
||||||
|
import { ROUTES } from '$lib/constants/routes';
|
||||||
|
|
||||||
export interface ChatSettings {
|
export interface ChatSettings {
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
@@ -15,11 +16,8 @@ export function useSettingsNavigation() {
|
|||||||
const isSettingsRoute = $derived(!!page.route.id?.startsWith('/settings'));
|
const isSettingsRoute = $derived(!!page.route.id?.startsWith('/settings'));
|
||||||
|
|
||||||
beforeNavigate(({ to, from }) => {
|
beforeNavigate(({ to, from }) => {
|
||||||
if (
|
if (to?.route?.id?.startsWith('/settings') && !from?.route?.id?.startsWith('/settings')) {
|
||||||
to?.route?.id?.startsWith('/settings/chat') &&
|
settingsReferrer.url = window.location.hash || ROUTES.START;
|
||||||
!from?.route?.id?.startsWith('/settings/chat')
|
|
||||||
) {
|
|
||||||
settingsReferrer.url = window.location.hash || '#/';
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -513,7 +513,7 @@ export class ChatService {
|
|||||||
|
|
||||||
const serializedToolCalls = JSON.stringify(aggregatedToolCalls);
|
const serializedToolCalls = JSON.stringify(aggregatedToolCalls);
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV && import.meta.env.VITE_DEBUG) {
|
||||||
console.log('[ChatService] Aggregated tool calls:', serializedToolCalls);
|
console.log('[ChatService] Aggregated tool calls:', serializedToolCalls);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -260,3 +260,26 @@ export { ParameterSyncService } from './parameter-sync.service';
|
|||||||
* @see MCP Protocol Specification: https://modelcontextprotocol.io/specification/2025-06-18
|
* @see MCP Protocol Specification: https://modelcontextprotocol.io/specification/2025-06-18
|
||||||
*/
|
*/
|
||||||
export { MCPService } from './mcp.service';
|
export { MCPService } from './mcp.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* **RouterService** — Dynamic route URL construction utility
|
||||||
|
*
|
||||||
|
* Stateless utility for building dynamic route URLs from ROUTES base paths.
|
||||||
|
* Static routes (START, NEW_CHAT, MCP_SERVERS) live in ROUTES constants;
|
||||||
|
* dynamic routes (CHAT, SETTINGS) are constructed here by appending parameters.
|
||||||
|
*
|
||||||
|
* **Architecture & Relationships:**
|
||||||
|
* - **RouterService** (this class): Stateless URL construction
|
||||||
|
* - Builds dynamic route URLs from ROUTES base paths
|
||||||
|
* - No side effects — receives route parameters, returns route strings
|
||||||
|
*
|
||||||
|
* - **ROUTES constant** (constants/routes.ts): Static route base paths
|
||||||
|
* - **All components/stores**: Call RouterService for dynamic route URLs
|
||||||
|
*
|
||||||
|
* **Key Responsibilities:**
|
||||||
|
* - Build chat URLs for specific conversations: `RouterService.chat(id)` → `#/chat/:id`
|
||||||
|
* - Build settings URLs for sections: `RouterService.settings(section)` → `#/settings/:section`
|
||||||
|
*
|
||||||
|
* @see ROUTES in constants/routes.ts — static route base paths
|
||||||
|
*/
|
||||||
|
export { RouterService } from './router.service';
|
||||||
|
|||||||
@@ -1,253 +1,8 @@
|
|||||||
import { normalizeFloatingPoint } from '$lib/utils';
|
import { normalizeFloatingPoint } from '$lib/utils';
|
||||||
import type { SyncableParameter, ParameterRecord, ParameterInfo, ParameterValue } from '$lib/types';
|
import { SETTINGS_KEYS, SYNCABLE_PARAMETERS } from '$lib/constants';
|
||||||
|
import type { ParameterRecord, ParameterInfo, ParameterValue } from '$lib/types';
|
||||||
import { SyncableParameterType, ParameterSource } from '$lib/enums';
|
import { SyncableParameterType, ParameterSource } from '$lib/enums';
|
||||||
|
|
||||||
/**
|
|
||||||
* Mapping of webui setting keys to server parameter keys.
|
|
||||||
* Only parameters listed here can be synced from the server `/props` endpoint.
|
|
||||||
* Each entry defines the webui key, corresponding server key, value type,
|
|
||||||
* and whether sync is enabled.
|
|
||||||
*/
|
|
||||||
export const SYNCABLE_PARAMETERS: SyncableParameter[] = [
|
|
||||||
{
|
|
||||||
key: 'temperature',
|
|
||||||
serverKey: 'temperature',
|
|
||||||
type: SyncableParameterType.NUMBER,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{ key: 'top_k', serverKey: 'top_k', type: SyncableParameterType.NUMBER, canSync: true },
|
|
||||||
{ key: 'top_p', serverKey: 'top_p', type: SyncableParameterType.NUMBER, canSync: true },
|
|
||||||
{ key: 'min_p', serverKey: 'min_p', type: SyncableParameterType.NUMBER, canSync: true },
|
|
||||||
{
|
|
||||||
key: 'dynatemp_range',
|
|
||||||
serverKey: 'dynatemp_range',
|
|
||||||
type: SyncableParameterType.NUMBER,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'dynatemp_exponent',
|
|
||||||
serverKey: 'dynatemp_exponent',
|
|
||||||
type: SyncableParameterType.NUMBER,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'xtc_probability',
|
|
||||||
serverKey: 'xtc_probability',
|
|
||||||
type: SyncableParameterType.NUMBER,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'xtc_threshold',
|
|
||||||
serverKey: 'xtc_threshold',
|
|
||||||
type: SyncableParameterType.NUMBER,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{ key: 'typ_p', serverKey: 'typ_p', type: SyncableParameterType.NUMBER, canSync: true },
|
|
||||||
{
|
|
||||||
key: 'repeat_last_n',
|
|
||||||
serverKey: 'repeat_last_n',
|
|
||||||
type: SyncableParameterType.NUMBER,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'repeat_penalty',
|
|
||||||
serverKey: 'repeat_penalty',
|
|
||||||
type: SyncableParameterType.NUMBER,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'presence_penalty',
|
|
||||||
serverKey: 'presence_penalty',
|
|
||||||
type: SyncableParameterType.NUMBER,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'frequency_penalty',
|
|
||||||
serverKey: 'frequency_penalty',
|
|
||||||
type: SyncableParameterType.NUMBER,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'dry_multiplier',
|
|
||||||
serverKey: 'dry_multiplier',
|
|
||||||
type: SyncableParameterType.NUMBER,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{ key: 'dry_base', serverKey: 'dry_base', type: SyncableParameterType.NUMBER, canSync: true },
|
|
||||||
{
|
|
||||||
key: 'dry_allowed_length',
|
|
||||||
serverKey: 'dry_allowed_length',
|
|
||||||
type: SyncableParameterType.NUMBER,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'dry_penalty_last_n',
|
|
||||||
serverKey: 'dry_penalty_last_n',
|
|
||||||
type: SyncableParameterType.NUMBER,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{ key: 'max_tokens', serverKey: 'max_tokens', type: SyncableParameterType.NUMBER, canSync: true },
|
|
||||||
{ key: 'samplers', serverKey: 'samplers', type: SyncableParameterType.STRING, canSync: true },
|
|
||||||
{
|
|
||||||
key: 'backend_sampling',
|
|
||||||
serverKey: 'backend_sampling',
|
|
||||||
type: SyncableParameterType.BOOLEAN,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'pasteLongTextToFileLen',
|
|
||||||
serverKey: 'pasteLongTextToFileLen',
|
|
||||||
type: SyncableParameterType.NUMBER,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'pdfAsImage',
|
|
||||||
serverKey: 'pdfAsImage',
|
|
||||||
type: SyncableParameterType.BOOLEAN,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'showThoughtInProgress',
|
|
||||||
serverKey: 'showThoughtInProgress',
|
|
||||||
type: SyncableParameterType.BOOLEAN,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'keepStatsVisible',
|
|
||||||
serverKey: 'keepStatsVisible',
|
|
||||||
type: SyncableParameterType.BOOLEAN,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'showMessageStats',
|
|
||||||
serverKey: 'showMessageStats',
|
|
||||||
type: SyncableParameterType.BOOLEAN,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'askForTitleConfirmation',
|
|
||||||
serverKey: 'askForTitleConfirmation',
|
|
||||||
type: SyncableParameterType.BOOLEAN,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'titleGenerationUseFirstLine',
|
|
||||||
serverKey: 'titleGenerationUseFirstLine',
|
|
||||||
type: SyncableParameterType.BOOLEAN,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'disableAutoScroll',
|
|
||||||
serverKey: 'disableAutoScroll',
|
|
||||||
type: SyncableParameterType.BOOLEAN,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'renderUserContentAsMarkdown',
|
|
||||||
serverKey: 'renderUserContentAsMarkdown',
|
|
||||||
type: SyncableParameterType.BOOLEAN,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'autoMicOnEmpty',
|
|
||||||
serverKey: 'autoMicOnEmpty',
|
|
||||||
type: SyncableParameterType.BOOLEAN,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'pyInterpreterEnabled',
|
|
||||||
serverKey: 'pyInterpreterEnabled',
|
|
||||||
type: SyncableParameterType.BOOLEAN,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'enableContinueGeneration',
|
|
||||||
serverKey: 'enableContinueGeneration',
|
|
||||||
type: SyncableParameterType.BOOLEAN,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'fullHeightCodeBlocks',
|
|
||||||
serverKey: 'fullHeightCodeBlocks',
|
|
||||||
type: SyncableParameterType.BOOLEAN,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'systemMessage',
|
|
||||||
serverKey: 'systemMessage',
|
|
||||||
type: SyncableParameterType.STRING,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'showSystemMessage',
|
|
||||||
serverKey: 'showSystemMessage',
|
|
||||||
type: SyncableParameterType.BOOLEAN,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{ key: 'theme', serverKey: 'theme', type: SyncableParameterType.STRING, canSync: true },
|
|
||||||
{
|
|
||||||
key: 'copyTextAttachmentsAsPlainText',
|
|
||||||
serverKey: 'copyTextAttachmentsAsPlainText',
|
|
||||||
type: SyncableParameterType.BOOLEAN,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'showRawOutputSwitch',
|
|
||||||
serverKey: 'showRawOutputSwitch',
|
|
||||||
type: SyncableParameterType.BOOLEAN,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'alwaysShowSidebarOnDesktop',
|
|
||||||
serverKey: 'alwaysShowSidebarOnDesktop',
|
|
||||||
type: SyncableParameterType.BOOLEAN,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'showRawModelNames',
|
|
||||||
serverKey: 'showRawModelNames',
|
|
||||||
type: SyncableParameterType.BOOLEAN,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{ key: 'mcpServers', serverKey: 'mcpServers', type: SyncableParameterType.STRING, canSync: true },
|
|
||||||
{
|
|
||||||
key: 'agenticMaxTurns',
|
|
||||||
serverKey: 'agenticMaxTurns',
|
|
||||||
type: SyncableParameterType.NUMBER,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'agenticMaxToolPreviewLines',
|
|
||||||
serverKey: 'agenticMaxToolPreviewLines',
|
|
||||||
type: SyncableParameterType.NUMBER,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'showToolCallInProgress',
|
|
||||||
serverKey: 'showToolCallInProgress',
|
|
||||||
type: SyncableParameterType.BOOLEAN,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'alwaysShowAgenticTurns',
|
|
||||||
serverKey: 'alwaysShowAgenticTurns',
|
|
||||||
type: SyncableParameterType.BOOLEAN,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'excludeReasoningFromContext',
|
|
||||||
serverKey: 'excludeReasoningFromContext',
|
|
||||||
type: SyncableParameterType.BOOLEAN,
|
|
||||||
canSync: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'sendOnEnter',
|
|
||||||
serverKey: 'sendOnEnter',
|
|
||||||
type: SyncableParameterType.BOOLEAN,
|
|
||||||
canSync: true
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
export class ParameterSyncService {
|
export class ParameterSyncService {
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -298,7 +53,7 @@ export class ParameterSyncService {
|
|||||||
|
|
||||||
// Handle samplers array conversion to string
|
// Handle samplers array conversion to string
|
||||||
if (serverParams.samplers && Array.isArray(serverParams.samplers)) {
|
if (serverParams.samplers && Array.isArray(serverParams.samplers)) {
|
||||||
extracted.samplers = serverParams.samplers.join(';');
|
extracted[SETTINGS_KEYS.SAMPLERS] = serverParams.samplers.join(';');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { ROUTES } from '$lib/constants/routes';
|
||||||
|
|
||||||
|
export class RouterService {
|
||||||
|
static chat(id: string): string {
|
||||||
|
return `${ROUTES.CHAT}/${id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static settings(section: string): string {
|
||||||
|
return `${ROUTES.SETTINGS}/${section}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -413,8 +413,6 @@ class AgenticStore {
|
|||||||
|
|
||||||
const tools = toolsStore.getEnabledToolsForLLM();
|
const tools = toolsStore.getEnabledToolsForLLM();
|
||||||
if (tools.length === 0) {
|
if (tools.length === 0) {
|
||||||
console.log('[AgenticStore] No tools available, falling back to standard chat');
|
|
||||||
|
|
||||||
return { handled: false };
|
return { handled: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import {
|
|||||||
MAX_INACTIVE_CONVERSATION_STATES,
|
MAX_INACTIVE_CONVERSATION_STATES,
|
||||||
INACTIVE_CONVERSATION_STATE_MAX_AGE_MS,
|
INACTIVE_CONVERSATION_STATE_MAX_AGE_MS,
|
||||||
SYSTEM_MESSAGE_PLACEHOLDER,
|
SYSTEM_MESSAGE_PLACEHOLDER,
|
||||||
TITLE
|
TITLE_GENERATION
|
||||||
} from '$lib/constants';
|
} from '$lib/constants';
|
||||||
import type {
|
import type {
|
||||||
ChatMessageTimings,
|
ChatMessageTimings,
|
||||||
@@ -265,7 +265,7 @@ class ChatStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private isChatLoadingInternal(convId: string): boolean {
|
private isChatLoadingInternal(convId: string): boolean {
|
||||||
return this.chatStreamingStates.has(convId);
|
return this.chatLoadingStates.has(convId) || this.chatStreamingStates.has(convId);
|
||||||
}
|
}
|
||||||
|
|
||||||
hasPendingMessage(convId: string): boolean {
|
hasPendingMessage(convId: string): boolean {
|
||||||
@@ -950,7 +950,7 @@ class ChatStore {
|
|||||||
typeof configValue.titleGenerationPrompt === 'string' &&
|
typeof configValue.titleGenerationPrompt === 'string' &&
|
||||||
configValue.titleGenerationPrompt.trim()
|
configValue.titleGenerationPrompt.trim()
|
||||||
? configValue.titleGenerationPrompt
|
? configValue.titleGenerationPrompt
|
||||||
: TITLE.DEFAULT_PROMPT;
|
: TITLE_GENERATION.DEFAULT_PROMPT;
|
||||||
|
|
||||||
const titlePrompt = titlePromptTemplate
|
const titlePrompt = titlePromptTemplate
|
||||||
.replace('{{USER}}', String(userContent || ''))
|
.replace('{{USER}}', String(userContent || ''))
|
||||||
@@ -969,14 +969,14 @@ class ChatStore {
|
|||||||
|
|
||||||
let cleanTitle = titleResponse.trim();
|
let cleanTitle = titleResponse.trim();
|
||||||
cleanTitle = cleanTitle
|
cleanTitle = cleanTitle
|
||||||
.replace(TITLE.PREFIX_PATTERN, '')
|
.replace(TITLE_GENERATION.PREFIX_PATTERN, '')
|
||||||
.replace(TITLE.QUOTE_PATTERN, '')
|
.replace(TITLE_GENERATION.QUOTE_PATTERN, '')
|
||||||
.trim();
|
.trim();
|
||||||
if (!cleanTitle || cleanTitle.length < TITLE.MIN_LENGTH) {
|
if (!cleanTitle || cleanTitle.length < TITLE_GENERATION.MIN_LENGTH) {
|
||||||
const firstLine = userContent.split('\n').find((l) => l.trim().length > 0);
|
const firstLine = userContent.split('\n').find((l) => l.trim().length > 0);
|
||||||
cleanTitle = firstLine ? firstLine.trim() : TITLE.FALLBACK;
|
cleanTitle = firstLine ? firstLine.trim() : TITLE_GENERATION.FALLBACK;
|
||||||
}
|
}
|
||||||
if (cleanTitle && cleanTitle.length >= TITLE.MIN_LENGTH) {
|
if (cleanTitle && cleanTitle.length >= TITLE_GENERATION.MIN_LENGTH) {
|
||||||
await conversationsStore.updateConversationName(convId, cleanTitle);
|
await conversationsStore.updateConversationName(convId, cleanTitle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ import {
|
|||||||
MULTIPLE_UNDERSCORE_REGEX,
|
MULTIPLE_UNDERSCORE_REGEX,
|
||||||
MCP_DEFAULT_ENABLED_LOCALSTORAGE_KEY
|
MCP_DEFAULT_ENABLED_LOCALSTORAGE_KEY
|
||||||
} from '$lib/constants';
|
} from '$lib/constants';
|
||||||
|
import { ROUTES } from '$lib/constants/routes';
|
||||||
|
import { RouterService } from '$lib/services/router.service';
|
||||||
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
||||||
|
|
||||||
export interface ConversationTreeItem {
|
export interface ConversationTreeItem {
|
||||||
@@ -260,7 +262,7 @@ class ConversationsStore {
|
|||||||
this.activeConversation = conversation;
|
this.activeConversation = conversation;
|
||||||
this.activeMessages = [];
|
this.activeMessages = [];
|
||||||
|
|
||||||
await goto(`#/chat/${conversation.id}`);
|
await goto(RouterService.chat(conversation.id));
|
||||||
|
|
||||||
return conversation.id;
|
return conversation.id;
|
||||||
}
|
}
|
||||||
@@ -336,7 +338,7 @@ class ConversationsStore {
|
|||||||
|
|
||||||
if (this.activeConversation && idsToRemove.has(this.activeConversation.id)) {
|
if (this.activeConversation && idsToRemove.has(this.activeConversation.id)) {
|
||||||
this.clearActiveConversation();
|
this.clearActiveConversation();
|
||||||
await goto(`?new_chat=true#/`);
|
await goto(ROUTES.NEW_CHAT);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Reparent direct children to deleted conv's parent (or promote to top-level)
|
// Reparent direct children to deleted conv's parent (or promote to top-level)
|
||||||
@@ -352,7 +354,7 @@ class ConversationsStore {
|
|||||||
|
|
||||||
if (this.activeConversation?.id === convId) {
|
if (this.activeConversation?.id === convId) {
|
||||||
this.clearActiveConversation();
|
this.clearActiveConversation();
|
||||||
await goto(`?new_chat=true#/`);
|
await goto(ROUTES.NEW_CHAT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -376,7 +378,7 @@ class ConversationsStore {
|
|||||||
|
|
||||||
toast.success('All conversations deleted');
|
toast.success('All conversations deleted');
|
||||||
|
|
||||||
await goto(`?new_chat=true#/`);
|
await goto(ROUTES.NEW_CHAT);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete all conversations:', error);
|
console.error('Failed to delete all conversations:', error);
|
||||||
toast.error('Failed to delete conversations');
|
toast.error('Failed to delete conversations');
|
||||||
@@ -729,7 +731,7 @@ class ConversationsStore {
|
|||||||
|
|
||||||
this.conversations = [newConv, ...this.conversations];
|
this.conversations = [newConv, ...this.conversations];
|
||||||
|
|
||||||
await goto(`#/chat/${newConv.id}`);
|
await goto(RouterService.chat(newConv.id));
|
||||||
|
|
||||||
toast.success('Conversation forked');
|
toast.success('Conversation forked');
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { base } from '$app/paths';
|
import { base } from '$app/paths';
|
||||||
|
import { SETTINGS_KEYS } from '$lib/constants';
|
||||||
import { MCPService } from '$lib/services/mcp.service';
|
import { MCPService } from '$lib/services/mcp.service';
|
||||||
import { config, settingsStore } from '$lib/stores/settings.svelte';
|
import { config, settingsStore } from '$lib/stores/settings.svelte';
|
||||||
import { mcpResourceStore } from '$lib/stores/mcp-resources.svelte';
|
import { mcpResourceStore } from '$lib/stores/mcp-resources.svelte';
|
||||||
@@ -556,13 +557,13 @@ class MCPStore {
|
|||||||
requestTimeoutSeconds: DEFAULT_MCP_CONFIG.requestTimeoutSeconds,
|
requestTimeoutSeconds: DEFAULT_MCP_CONFIG.requestTimeoutSeconds,
|
||||||
useProxy: serverData.useProxy
|
useProxy: serverData.useProxy
|
||||||
};
|
};
|
||||||
settingsStore.updateConfig('mcpServers', JSON.stringify([...servers, newServer]));
|
settingsStore.updateConfig(SETTINGS_KEYS.MCP_SERVERS, JSON.stringify([...servers, newServer]));
|
||||||
}
|
}
|
||||||
|
|
||||||
updateServer(id: string, updates: Partial<MCPServerSettingsEntry>): void {
|
updateServer(id: string, updates: Partial<MCPServerSettingsEntry>): void {
|
||||||
const servers = this.getServers();
|
const servers = this.getServers();
|
||||||
settingsStore.updateConfig(
|
settingsStore.updateConfig(
|
||||||
'mcpServers',
|
SETTINGS_KEYS.MCP_SERVERS,
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
servers.map((server) => (server.id === id ? { ...server, ...updates } : server))
|
servers.map((server) => (server.id === id ? { ...server, ...updates } : server))
|
||||||
)
|
)
|
||||||
@@ -571,7 +572,10 @@ class MCPStore {
|
|||||||
|
|
||||||
removeServer(id: string): void {
|
removeServer(id: string): void {
|
||||||
const servers = this.getServers();
|
const servers = this.getServers();
|
||||||
settingsStore.updateConfig('mcpServers', JSON.stringify(servers.filter((s) => s.id !== id)));
|
settingsStore.updateConfig(
|
||||||
|
SETTINGS_KEYS.MCP_SERVERS,
|
||||||
|
JSON.stringify(servers.filter((s) => s.id !== id))
|
||||||
|
);
|
||||||
this.clearHealthCheck(id);
|
this.clearHealthCheck(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
MODEL_PROPS_CACHE_MAX_ENTRIES,
|
MODEL_PROPS_CACHE_MAX_ENTRIES,
|
||||||
FAVORITE_MODELS_LOCALSTORAGE_KEY
|
FAVORITE_MODELS_LOCALSTORAGE_KEY
|
||||||
} from '$lib/constants';
|
} from '$lib/constants';
|
||||||
|
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* modelsStore - Reactive store for model management in both MODEL and ROUTER modes
|
* modelsStore - Reactive store for model management in both MODEL and ROUTER modes
|
||||||
@@ -424,6 +425,103 @@ class ModelsStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the model name from the last assistant message in the active conversation.
|
||||||
|
* Iterates backward through messages to find the most recent message with a model.
|
||||||
|
* Used by both the chat page and settings page to maintain model consistency.
|
||||||
|
* @returns The model name or null if not found
|
||||||
|
*/
|
||||||
|
getModelFromLastAssistantResponse(): string | null {
|
||||||
|
const messages = conversationsStore.activeMessages;
|
||||||
|
if (!messages || messages.length === 0) return null;
|
||||||
|
|
||||||
|
// Iterate backward to find the last message with a model
|
||||||
|
for (let i = messages.length - 1; i >= 0; i--) {
|
||||||
|
if (messages[i].model) {
|
||||||
|
return messages[i].model;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-selects the model from the last assistant response if available and loaded.
|
||||||
|
* Returns true if a model was selected, false otherwise.
|
||||||
|
* This is used by the chat page to maintain model consistency across page navigation.
|
||||||
|
*/
|
||||||
|
async selectModelFromLastAssistantResponse(): Promise<boolean> {
|
||||||
|
const lastModel = this.getModelFromLastAssistantResponse();
|
||||||
|
if (!lastModel) return false;
|
||||||
|
|
||||||
|
// Skip if already selected
|
||||||
|
if (this.selectedModelName === lastModel) return false;
|
||||||
|
|
||||||
|
const matchingModel = this.models.find((option) => option.model === lastModel);
|
||||||
|
if (!matchingModel) return false;
|
||||||
|
|
||||||
|
if (!this.isModelLoaded(lastModel)) {
|
||||||
|
console.log('[modelsStore] last assistant model not loaded:', lastModel);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.selectModelById(matchingModel.id);
|
||||||
|
console.log(`[modelsStore] Automatically selected model: ${lastModel} from last message`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[modelsStore] Failed to automatically select model from last message:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-selects the first available model if none is selected, and fetches its props.
|
||||||
|
* Prioritizes:
|
||||||
|
* 1. Model from active conversation's last assistant response (if loaded)
|
||||||
|
* 2. Model from active conversation's last assistant response (if not loaded)
|
||||||
|
* 3. First loaded model (not from active conversation)
|
||||||
|
* 4. First available model
|
||||||
|
* This is used to ensure default values are populated in settings pages.
|
||||||
|
*/
|
||||||
|
async ensureFirstModelSelected(): Promise<void> {
|
||||||
|
if (this.selectedModelName) return;
|
||||||
|
|
||||||
|
// Filter models that are visible in webui
|
||||||
|
const availableModels = this.models.filter((option) => {
|
||||||
|
const modelProps = this.getModelProps(option.model);
|
||||||
|
return modelProps?.webui !== false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (availableModels.length === 0) return;
|
||||||
|
|
||||||
|
// Try to select model from last assistant response first
|
||||||
|
const lastModel = this.getModelFromLastAssistantResponse();
|
||||||
|
if (lastModel) {
|
||||||
|
const lastModelOption = availableModels.find((m) => m.model === lastModel);
|
||||||
|
if (lastModelOption) {
|
||||||
|
await this.selectModelById(lastModelOption.id);
|
||||||
|
if (this.isModelLoaded(lastModel)) {
|
||||||
|
await this.fetchModelProps(lastModel);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find a loaded model first
|
||||||
|
const loadedModel = availableModels.find((m) => this.isModelLoaded(m.model));
|
||||||
|
if (loadedModel) {
|
||||||
|
await this.selectModelById(loadedModel.id);
|
||||||
|
await this.fetchModelProps(loadedModel.model);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to the first available model
|
||||||
|
const firstModel = availableModels[0];
|
||||||
|
await this.selectModelById(firstModel.id);
|
||||||
|
// Don't fetch props for unloaded models (will fail in ROUTER mode)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update modalities for a specific model
|
* Update modalities for a specific model
|
||||||
* Called when a model is loaded or when we need fresh modality data
|
* Called when a model is loaded or when we need fresh modality data
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
let _url = $state('#/');
|
import { SETTINGS_FALLBACK_EXIT_ROUTE } from '$lib/constants';
|
||||||
|
|
||||||
|
let _url = $state<string>(SETTINGS_FALLBACK_EXIT_ROUTE);
|
||||||
|
|
||||||
export const settingsReferrer = {
|
export const settingsReferrer = {
|
||||||
get url() {
|
get url() {
|
||||||
|
|||||||
@@ -32,9 +32,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
|
import { ColorMode } from '$lib/enums';
|
||||||
|
import type { SettingsExportType } from '$lib/types';
|
||||||
|
import { setMode } from 'mode-watcher';
|
||||||
import {
|
import {
|
||||||
CONFIG_LOCALSTORAGE_KEY,
|
CONFIG_LOCALSTORAGE_KEY,
|
||||||
SETTING_CONFIG_DEFAULT,
|
SETTING_CONFIG_DEFAULT,
|
||||||
|
SETTINGS_KEYS,
|
||||||
USER_OVERRIDES_LOCALSTORAGE_KEY
|
USER_OVERRIDES_LOCALSTORAGE_KEY
|
||||||
} from '$lib/constants';
|
} from '$lib/constants';
|
||||||
import { IsMobile } from '$lib/hooks/is-mobile.svelte';
|
import { IsMobile } from '$lib/hooks/is-mobile.svelte';
|
||||||
@@ -57,7 +61,6 @@ class SettingsStore {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
config = $state<SettingsConfigType>({ ...SETTING_CONFIG_DEFAULT });
|
config = $state<SettingsConfigType>({ ...SETTING_CONFIG_DEFAULT });
|
||||||
theme = $state<string>('auto');
|
|
||||||
isInitialized = $state(false);
|
isInitialized = $state(false);
|
||||||
userOverrides = $state<Set<string>>(new Set());
|
userOverrides = $state<Set<string>>(new Set());
|
||||||
|
|
||||||
@@ -99,7 +102,9 @@ class SettingsStore {
|
|||||||
initialize() {
|
initialize() {
|
||||||
try {
|
try {
|
||||||
this.loadConfig();
|
this.loadConfig();
|
||||||
this.loadTheme();
|
this.migrateLegacyTheme();
|
||||||
|
// Apply the persisted theme from config on initial load
|
||||||
|
setMode(this.config[SETTINGS_KEYS.THEME] as ColorMode);
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize settings store:', error);
|
console.error('Failed to initialize settings store:', error);
|
||||||
@@ -124,9 +129,9 @@ class SettingsStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Default sendOnEnter to false on mobile when the user has no saved preference
|
// Default sendOnEnter to false on mobile when the user has no saved preference
|
||||||
if (!('sendOnEnter' in savedVal)) {
|
if (!(SETTINGS_KEYS.SEND_ON_ENTER in savedVal)) {
|
||||||
if (new IsMobile().current) {
|
if (new IsMobile().current) {
|
||||||
this.config.sendOnEnter = false;
|
this.config[SETTINGS_KEYS.SEND_ON_ENTER] = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,12 +148,21 @@ class SettingsStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load theme from localStorage
|
* Migrate the legacy un-namespaced "theme" localStorage key into config.
|
||||||
|
* Previously theme was stored separately in localStorage("theme") — now it lives
|
||||||
|
* inside the config object alongside all other settings.
|
||||||
|
* After migration the legacy key is removed.
|
||||||
*/
|
*/
|
||||||
private loadTheme() {
|
private migrateLegacyTheme() {
|
||||||
if (!browser) return;
|
if (!browser) return;
|
||||||
|
|
||||||
this.theme = localStorage.getItem('theme') || 'auto';
|
const legacyTheme = localStorage.getItem('theme');
|
||||||
|
if (legacyTheme) {
|
||||||
|
this.config[SETTINGS_KEYS.THEME] = legacyTheme;
|
||||||
|
localStorage.removeItem('theme');
|
||||||
|
this.saveConfig();
|
||||||
|
setMode(legacyTheme as ColorMode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -233,29 +247,13 @@ class SettingsStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the theme setting
|
* Update the theme setting.
|
||||||
* @param newTheme - The new theme value
|
* @param newTheme - The new theme value
|
||||||
*/
|
*/
|
||||||
updateTheme(newTheme: string) {
|
updateTheme(newTheme: string) {
|
||||||
this.theme = newTheme;
|
this.updateConfig(SETTINGS_KEYS.THEME, newTheme);
|
||||||
this.saveTheme();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
setMode(newTheme as ColorMode);
|
||||||
* Save the current theme to localStorage
|
|
||||||
*/
|
|
||||||
private saveTheme() {
|
|
||||||
if (!browser) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (this.theme === 'auto') {
|
|
||||||
localStorage.removeItem('theme');
|
|
||||||
} else {
|
|
||||||
localStorage.setItem('theme', this.theme);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to save theme to localStorage:', error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -271,22 +269,26 @@ class SettingsStore {
|
|||||||
*/
|
*/
|
||||||
resetConfig() {
|
resetConfig() {
|
||||||
this.config = { ...SETTING_CONFIG_DEFAULT };
|
this.config = { ...SETTING_CONFIG_DEFAULT };
|
||||||
|
|
||||||
this.saveConfig();
|
this.saveConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset theme to auto
|
* Reset theme to default value.
|
||||||
|
* Theme is now stored inside the config object.
|
||||||
*/
|
*/
|
||||||
resetTheme() {
|
resetTheme() {
|
||||||
this.theme = 'auto';
|
this.updateConfig(SETTINGS_KEYS.THEME, SETTING_CONFIG_DEFAULT[SETTINGS_KEYS.THEME]);
|
||||||
this.saveTheme();
|
|
||||||
|
setMode(SETTING_CONFIG_DEFAULT[SETTINGS_KEYS.THEME] as ColorMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset all settings to defaults
|
* Reset all settings to defaults.
|
||||||
*/
|
*/
|
||||||
resetAll() {
|
resetAll() {
|
||||||
this.resetConfig();
|
this.resetConfig();
|
||||||
|
|
||||||
this.resetTheme();
|
this.resetTheme();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -456,10 +458,86 @@ class SettingsStore {
|
|||||||
this.saveConfig();
|
this.saveConfig();
|
||||||
console.log('Cleared all user overrides');
|
console.log('Cleared all user overrides');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Import / Export
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export all settings as a versioned JSON-compatible object.
|
||||||
|
* The export captures the full config (excluding sensitive values like API key)
|
||||||
|
* and user overrides. Sensitive fields are filtered out for security by default.
|
||||||
|
* @param includeSensitiveData - If true, include sensitive fields (apiKey, MCP server headers) in export
|
||||||
|
*/
|
||||||
|
exportSettings(includeSensitiveData: boolean = false): SettingsExportType {
|
||||||
|
// Build config excluding sensitive data unless user opts in
|
||||||
|
const configToExport: Record<string, string | number | boolean | undefined> =
|
||||||
|
includeSensitiveData
|
||||||
|
? { ...this.config }
|
||||||
|
: Object.fromEntries(Object.entries(this.config).filter(([key]) => key !== 'apiKey'));
|
||||||
|
|
||||||
|
// Handle MCP servers: exclude custom headers unless user opts in
|
||||||
|
if ('mcpServers' in configToExport && !includeSensitiveData) {
|
||||||
|
try {
|
||||||
|
const mcpServers = JSON.parse(configToExport.mcpServers as string) as Array<
|
||||||
|
Record<string, unknown>
|
||||||
|
>;
|
||||||
|
const safeServers = mcpServers.map((server) => {
|
||||||
|
delete server.headers;
|
||||||
|
return server;
|
||||||
|
});
|
||||||
|
configToExport.mcpServers = JSON.stringify(safeServers);
|
||||||
|
} catch {
|
||||||
|
// If parsing fails, just exclude the entire mcpServers field
|
||||||
|
delete (configToExport as Record<string, unknown>).mcpServers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: 1,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
config: configToExport,
|
||||||
|
userOverrides: Array.from(this.userOverrides)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import settings from a previously exported object.
|
||||||
|
* Restores config (including theme) and user overrides.
|
||||||
|
* @param data - The exported settings object
|
||||||
|
*/
|
||||||
|
importSettings(data: SettingsExportType): void {
|
||||||
|
if (!browser) return;
|
||||||
|
|
||||||
|
if (!data || !data.config) {
|
||||||
|
throw new Error('Invalid settings data: missing config');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore config (theme is included in config)
|
||||||
|
this.config = {
|
||||||
|
...SETTING_CONFIG_DEFAULT,
|
||||||
|
...data.config
|
||||||
|
};
|
||||||
|
|
||||||
|
// Restore user overrides (derived state — may be stale if server defaults differ)
|
||||||
|
this.userOverrides = new Set(data.userOverrides ?? []);
|
||||||
|
|
||||||
|
// Persist to localStorage
|
||||||
|
this.saveConfig();
|
||||||
|
|
||||||
|
// Apply theme for immediate visual feedback
|
||||||
|
setMode(this.config[SETTINGS_KEYS.THEME] as ColorMode);
|
||||||
|
|
||||||
|
console.log('Settings imported successfully');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const settingsStore = new SettingsStore();
|
export const settingsStore = new SettingsStore();
|
||||||
|
|
||||||
export const config = () => settingsStore.config;
|
export const config = () => settingsStore.config;
|
||||||
export const theme = () => settingsStore.theme;
|
export const theme = () => settingsStore.config[SETTINGS_KEYS.THEME];
|
||||||
export const isInitialized = () => settingsStore.isInitialized;
|
export const isInitialized = () => settingsStore.isInitialized;
|
||||||
|
|||||||
@@ -76,10 +76,15 @@ export type {
|
|||||||
SettingsFieldConfig,
|
SettingsFieldConfig,
|
||||||
SettingsChatServiceOptions,
|
SettingsChatServiceOptions,
|
||||||
SettingsConfigType,
|
SettingsConfigType,
|
||||||
|
SettingsExportType,
|
||||||
ParameterValue,
|
ParameterValue,
|
||||||
ParameterRecord,
|
ParameterRecord,
|
||||||
ParameterInfo,
|
ParameterInfo,
|
||||||
SyncableParameter
|
SyncableParameter,
|
||||||
|
SettingsEntry,
|
||||||
|
SettingsSectionTitle,
|
||||||
|
SettingsSectionEntry,
|
||||||
|
SettingsSection
|
||||||
} from './settings';
|
} from './settings';
|
||||||
|
|
||||||
// Common types
|
// Common types
|
||||||
|
|||||||
+54
-1
@@ -1,12 +1,42 @@
|
|||||||
import type { SETTING_CONFIG_DEFAULT } from '$lib/constants';
|
import type { SETTING_CONFIG_DEFAULT, SETTINGS_SECTION_TITLES } from '$lib/constants';
|
||||||
import type { ChatMessagePromptProgress, ChatMessageTimings } from './chat';
|
import type { ChatMessagePromptProgress, ChatMessageTimings } from './chat';
|
||||||
import type { OpenAIToolDefinition } from './mcp';
|
import type { OpenAIToolDefinition } from './mcp';
|
||||||
import type { DatabaseMessageExtra } from './database';
|
import type { DatabaseMessageExtra } from './database';
|
||||||
import type { ParameterSource, SyncableParameterType, SettingsFieldType } from '$lib/enums';
|
import type { ParameterSource, SyncableParameterType, SettingsFieldType } from '$lib/enums';
|
||||||
import type { Icon } from '@lucide/svelte';
|
import type { Icon } from '@lucide/svelte';
|
||||||
|
import type { Component } from 'svelte';
|
||||||
|
|
||||||
export type SettingsConfigValue = string | number | boolean | undefined;
|
export type SettingsConfigValue = string | number | boolean | undefined;
|
||||||
|
|
||||||
|
/** Section title type derived from registry section titles. */
|
||||||
|
export type SettingsSectionTitle =
|
||||||
|
(typeof SETTINGS_SECTION_TITLES)[keyof typeof SETTINGS_SECTION_TITLES];
|
||||||
|
|
||||||
|
/** Per-setting metadata — one entry per setting. */
|
||||||
|
export interface SettingsEntry {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
help: string;
|
||||||
|
defaultValue: SettingsConfigValue;
|
||||||
|
type: SettingsFieldType;
|
||||||
|
section?: string;
|
||||||
|
options?: Array<{ value: string; label: string; icon: Component }>;
|
||||||
|
isExperimental?: boolean;
|
||||||
|
isPositiveInteger?: boolean;
|
||||||
|
sync?: {
|
||||||
|
serverKey: string;
|
||||||
|
paramType: SyncableParameterType;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A settings section with its icon, slug, title, and ordered settings. */
|
||||||
|
export interface SettingsSectionEntry {
|
||||||
|
title: SettingsSectionTitle;
|
||||||
|
slug: string;
|
||||||
|
icon: Component;
|
||||||
|
settings: SettingsEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface SettingsFieldConfig {
|
export interface SettingsFieldConfig {
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -16,6 +46,14 @@ export interface SettingsFieldConfig {
|
|||||||
options?: Array<{ value: string; label: string; icon?: typeof Icon }>;
|
options?: Array<{ value: string; label: string; icon?: typeof Icon }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Re-exported for backward compatibility. */
|
||||||
|
export interface SettingsSection {
|
||||||
|
fields?: SettingsFieldConfig[];
|
||||||
|
icon: Component;
|
||||||
|
slug: string;
|
||||||
|
title: SettingsSectionTitle;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SettingsChatServiceOptions {
|
export interface SettingsChatServiceOptions {
|
||||||
stream?: boolean;
|
stream?: boolean;
|
||||||
// Model (required in ROUTER mode, optional in MODEL mode)
|
// Model (required in ROUTER mode, optional in MODEL mode)
|
||||||
@@ -94,3 +132,18 @@ export interface SyncableParameter {
|
|||||||
type: SyncableParameterType;
|
type: SyncableParameterType;
|
||||||
canSync: boolean;
|
canSync: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shape of the settings JSON export file.
|
||||||
|
* Versioned to allow future schema evolution.
|
||||||
|
*/
|
||||||
|
export interface SettingsExportType {
|
||||||
|
/** Export format version — bumped on breaking changes */
|
||||||
|
version: number;
|
||||||
|
/** Unix timestamp of export */
|
||||||
|
timestamp: number;
|
||||||
|
/** Full settings config (includes theme as a config key) */
|
||||||
|
config: SettingsConfigType;
|
||||||
|
/** Keys that differ from server defaults (derived, but persisted for fidelity) */
|
||||||
|
userOverrides: string[];
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { convertPDFToImage, convertPDFToText } from './pdf-processing';
|
|||||||
import { isSvgMimeType, svgBase64UrlToPngDataURL } from './svg-to-png';
|
import { isSvgMimeType, svgBase64UrlToPngDataURL } from './svg-to-png';
|
||||||
import { isWebpMimeType, webpBase64UrlToPngDataURL } from './webp-to-png';
|
import { isWebpMimeType, webpBase64UrlToPngDataURL } from './webp-to-png';
|
||||||
import { FileTypeCategory, AttachmentType, SpecialFileType } from '$lib/enums';
|
import { FileTypeCategory, AttachmentType, SpecialFileType } from '$lib/enums';
|
||||||
|
import { SETTINGS_KEYS } from '$lib/constants';
|
||||||
import { config, settingsStore } from '$lib/stores/settings.svelte';
|
import { config, settingsStore } from '$lib/stores/settings.svelte';
|
||||||
import { modelsStore } from '$lib/stores/models.svelte';
|
import { modelsStore } from '$lib/stores/models.svelte';
|
||||||
import { getFileTypeCategory } from '$lib/utils';
|
import { getFileTypeCategory } from '$lib/utils';
|
||||||
@@ -106,7 +107,7 @@ export async function parseFilesToMessageExtras(
|
|||||||
console.log('Non-vision model detected: forcing PDF-to-text mode and updating settings');
|
console.log('Non-vision model detected: forcing PDF-to-text mode and updating settings');
|
||||||
|
|
||||||
// Update the setting in localStorage
|
// Update the setting in localStorage
|
||||||
settingsStore.updateConfig('pdfAsImage', false);
|
settingsStore.updateConfig(SETTINGS_KEYS.PDF_AS_IMAGE, false);
|
||||||
|
|
||||||
// Show toast notification to user
|
// Show toast notification to user
|
||||||
toast.warning(
|
toast.warning(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { isSvgMimeType, svgBase64UrlToPngDataURL } from './svg-to-png';
|
import { isSvgMimeType, svgBase64UrlToPngDataURL } from './svg-to-png';
|
||||||
import { isWebpMimeType, webpBase64UrlToPngDataURL } from './webp-to-png';
|
import { isWebpMimeType, webpBase64UrlToPngDataURL } from './webp-to-png';
|
||||||
import { FileTypeCategory } from '$lib/enums';
|
import { FileTypeCategory } from '$lib/enums';
|
||||||
|
import { SETTINGS_KEYS } from '$lib/constants';
|
||||||
import { modelsStore } from '$lib/stores/models.svelte';
|
import { modelsStore } from '$lib/stores/models.svelte';
|
||||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
@@ -104,7 +105,7 @@ export async function processFilesToChatUploaded(
|
|||||||
action: {
|
action: {
|
||||||
label: 'Enable PDF as Images',
|
label: 'Enable PDF as Images',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
settingsStore.updateConfig('pdfAsImage', true);
|
settingsStore.updateConfig(SETTINGS_KEYS.PDF_AS_IMAGE, true);
|
||||||
toast.success('PDF parsing as images enabled!', {
|
toast.success('PDF parsing as images enabled!', {
|
||||||
duration: 3000
|
duration: 3000
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,11 +7,11 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { replaceState } from '$app/navigation';
|
import { replaceState } from '$app/navigation';
|
||||||
import { APP_NAME } from '$lib/constants';
|
import { APP_NAME, NEW_CHAT_PARAM } from '$lib/constants';
|
||||||
|
|
||||||
let qParam = $derived(page.url.searchParams.get('q'));
|
let qParam = $derived(page.url.searchParams.get('q'));
|
||||||
let modelParam = $derived(page.url.searchParams.get('model'));
|
let modelParam = $derived(page.url.searchParams.get('model'));
|
||||||
let newChatParam = $derived(page.url.searchParams.get('new_chat'));
|
let newChatParam = $derived(page.url.searchParams.get(NEW_CHAT_PARAM));
|
||||||
|
|
||||||
// Dialog state for model not available error
|
// Dialog state for model not available error
|
||||||
let showModelNotAvailable = $state(false);
|
let showModelNotAvailable = $state(false);
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
url.searchParams.delete('q');
|
url.searchParams.delete('q');
|
||||||
url.searchParams.delete('model');
|
url.searchParams.delete('model');
|
||||||
url.searchParams.delete('new_chat');
|
url.searchParams.delete(NEW_CHAT_PARAM);
|
||||||
|
|
||||||
replaceState(url.toString(), {});
|
replaceState(url.toString(), {});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,10 @@
|
|||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { afterNavigate } from '$app/navigation';
|
import { afterNavigate } from '$app/navigation';
|
||||||
import { DialogModelNotAvailable } from '$lib/components/app';
|
import { DialogModelNotAvailable } from '$lib/components/app';
|
||||||
|
import { ROUTES } from '$lib/constants/routes';
|
||||||
import { chatStore, isLoading } from '$lib/stores/chat.svelte';
|
import { chatStore, isLoading } from '$lib/stores/chat.svelte';
|
||||||
import {
|
import { conversationsStore, activeConversation } from '$lib/stores/conversations.svelte';
|
||||||
conversationsStore,
|
import { modelsStore, modelOptions } from '$lib/stores/models.svelte';
|
||||||
activeConversation,
|
|
||||||
activeMessages
|
|
||||||
} from '$lib/stores/conversations.svelte';
|
|
||||||
import { modelsStore, modelOptions, selectedModelId } from '$lib/stores/models.svelte';
|
|
||||||
|
|
||||||
let chatId = $derived(page.params.id);
|
let chatId = $derived(page.params.id);
|
||||||
let currentChatId: string | undefined = undefined;
|
let currentChatId: string | undefined = undefined;
|
||||||
@@ -73,47 +70,9 @@
|
|||||||
urlParamsProcessed = true;
|
urlParamsProcessed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function selectModelFromLastAssistantResponse() {
|
|
||||||
const messages = activeMessages();
|
|
||||||
if (messages.length === 0) return;
|
|
||||||
|
|
||||||
let lastMessageWithModel: DatabaseMessage | undefined;
|
|
||||||
|
|
||||||
for (let i = messages.length - 1; i >= 0; i--) {
|
|
||||||
if (messages[i].model) {
|
|
||||||
lastMessageWithModel = messages[i];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!lastMessageWithModel) return;
|
|
||||||
|
|
||||||
const currentModelId = selectedModelId();
|
|
||||||
const currentModelName = modelOptions().find((m) => m.id === currentModelId)?.model;
|
|
||||||
|
|
||||||
if (currentModelName === lastMessageWithModel.model) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const matchingModel = modelOptions().find(
|
|
||||||
(option) => option.model === lastMessageWithModel.model
|
|
||||||
);
|
|
||||||
|
|
||||||
if (matchingModel && modelsStore.isModelLoaded(matchingModel.model)) {
|
|
||||||
try {
|
|
||||||
await modelsStore.selectModelById(matchingModel.id);
|
|
||||||
console.log(
|
|
||||||
`Automatically selected model: ${lastMessageWithModel.model} from last message`
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to automatically select model from last message:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
afterNavigate(() => {
|
afterNavigate(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
selectModelFromLastAssistantResponse();
|
void modelsStore.selectModelFromLastAssistantResponse();
|
||||||
}, 100);
|
}, 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -141,7 +100,7 @@
|
|||||||
await handleUrlParams();
|
await handleUrlParams();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await goto('#/');
|
await goto(ROUTES.START);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { ServerErrorSplash } from '$lib/components/app';
|
import { ServerErrorSplash } from '$lib/components/app';
|
||||||
|
import { ROUTES } from '$lib/constants/routes';
|
||||||
|
|
||||||
let error = $derived($page.error);
|
let error = $derived($page.error);
|
||||||
let status = $derived($page.status);
|
let status = $derived($page.status);
|
||||||
@@ -17,7 +18,7 @@
|
|||||||
|
|
||||||
function handleRetry() {
|
function handleRetry() {
|
||||||
// Navigate back to home page after successful API key validation
|
// Navigate back to home page after successful API key validation
|
||||||
goto('#/');
|
goto(ROUTES.START);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -60,7 +61,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onclick={() => goto('#/')}
|
onclick={() => goto(ROUTES.START)}
|
||||||
class="rounded-md bg-primary px-4 py-2 text-primary-foreground hover:bg-primary/90"
|
class="rounded-md bg-primary px-4 py-2 text-primary-foreground hover:bg-primary/90"
|
||||||
>
|
>
|
||||||
Go Home
|
Go Home
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
import { isRouterMode, serverStore } from '$lib/stores/server.svelte';
|
import { isRouterMode, serverStore } from '$lib/stores/server.svelte';
|
||||||
import { config, settingsStore } from '$lib/stores/settings.svelte';
|
import { config, settingsStore } from '$lib/stores/settings.svelte';
|
||||||
import { ModeWatcher } from 'mode-watcher';
|
import { ModeWatcher } from 'mode-watcher';
|
||||||
|
import { ROUTES } from '$lib/constants/routes';
|
||||||
|
import { RouterService } from '$lib/services/router.service';
|
||||||
import { Toaster } from 'svelte-sonner';
|
import { Toaster } from 'svelte-sonner';
|
||||||
import { modelsStore } from '$lib/stores/models.svelte';
|
import { modelsStore } from '$lib/stores/models.svelte';
|
||||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||||
@@ -53,7 +55,7 @@
|
|||||||
const currentId = page.params.id;
|
const currentId = page.params.id;
|
||||||
|
|
||||||
if (!currentId) {
|
if (!currentId) {
|
||||||
goto(`#/chat/${allConvs[direction === 1 ? 0 : allConvs.length - 1].id}`);
|
goto(RouterService.chat(allConvs[direction === 1 ? 0 : allConvs.length - 1].id));
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -64,9 +66,9 @@
|
|||||||
const targetIdx = idx + direction;
|
const targetIdx = idx + direction;
|
||||||
|
|
||||||
if (targetIdx >= 0 && targetIdx < allConvs.length) {
|
if (targetIdx >= 0 && targetIdx < allConvs.length) {
|
||||||
goto(`#/chat/${allConvs[targetIdx].id}`);
|
goto(RouterService.chat(allConvs[targetIdx].id));
|
||||||
} else {
|
} else {
|
||||||
goto('?new_chat=true#/');
|
goto(ROUTES.NEW_CHAT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { ActionIcon } from '$lib/components/app';
|
import { ActionIcon } from '$lib/components/app';
|
||||||
|
import { SETTINGS_FALLBACK_EXIT_ROUTE } from '$lib/constants';
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
|
|
||||||
@@ -21,7 +22,7 @@
|
|||||||
if (browser && window.history.length > 1 && !prevIsSettings) {
|
if (browser && window.history.length > 1 && !prevIsSettings) {
|
||||||
history.back();
|
history.back();
|
||||||
} else {
|
} else {
|
||||||
goto('#/');
|
goto(SETTINGS_FALLBACK_EXIT_ROUTE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { SettingsChat } from '$lib/components/app/settings';
|
||||||
|
import { page } from '$app/state';
|
||||||
|
import { replaceState } from '$app/navigation';
|
||||||
|
import { RouterService } from '$lib/services';
|
||||||
|
import { SETTINGS_SECTION_SLUGS } from '$lib/constants';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!page.params.section) {
|
||||||
|
replaceState(RouterService.settings(SETTINGS_SECTION_SLUGS.GENERAL), {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SettingsChat initialSection={(page.params as Record<string, string | undefined>).section} />
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { SettingsChat } from '$lib/components/app/settings';
|
|
||||||
import { page } from '$app/state';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<SettingsChat initialSection={(page.params as Record<string, string | undefined>).section} />
|
|
||||||
@@ -96,6 +96,7 @@ export default defineConfig({
|
|||||||
'/props': 'http://localhost:8080',
|
'/props': 'http://localhost:8080',
|
||||||
'/models': 'http://localhost:8080',
|
'/models': 'http://localhost:8080',
|
||||||
'/tools': 'http://localhost:8080',
|
'/tools': 'http://localhost:8080',
|
||||||
|
'/slots': 'http://localhost:8080',
|
||||||
'/cors-proxy': 'http://localhost:8080'
|
'/cors-proxy': 'http://localhost:8080'
|
||||||
},
|
},
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
Reference in New Issue
Block a user