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:
Aleksander Grygier
2026-05-08 11:26:04 +02:00
committed by GitHub
parent a8fd165fec
commit 9b2925e1e0
55 changed files with 5133 additions and 4615 deletions
@@ -7,11 +7,11 @@
import { onMount } from 'svelte';
import { page } from '$app/state';
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 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
let showModelNotAvailable = $state(false);
@@ -26,7 +26,7 @@
url.searchParams.delete('q');
url.searchParams.delete('model');
url.searchParams.delete('new_chat');
url.searchParams.delete(NEW_CHAT_PARAM);
replaceState(url.toString(), {});
}
@@ -3,13 +3,10 @@
import { page } from '$app/state';
import { afterNavigate } from '$app/navigation';
import { DialogModelNotAvailable } from '$lib/components/app';
import { ROUTES } from '$lib/constants/routes';
import { chatStore, isLoading } from '$lib/stores/chat.svelte';
import {
conversationsStore,
activeConversation,
activeMessages
} from '$lib/stores/conversations.svelte';
import { modelsStore, modelOptions, selectedModelId } from '$lib/stores/models.svelte';
import { conversationsStore, activeConversation } from '$lib/stores/conversations.svelte';
import { modelsStore, modelOptions } from '$lib/stores/models.svelte';
let chatId = $derived(page.params.id);
let currentChatId: string | undefined = undefined;
@@ -73,47 +70,9 @@
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(() => {
setTimeout(() => {
selectModelFromLastAssistantResponse();
void modelsStore.selectModelFromLastAssistantResponse();
}, 100);
});
@@ -141,7 +100,7 @@
await handleUrlParams();
}
} else {
await goto('#/');
await goto(ROUTES.START);
}
})();
}
+3 -2
View File
@@ -2,6 +2,7 @@
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { ServerErrorSplash } from '$lib/components/app';
import { ROUTES } from '$lib/constants/routes';
let error = $derived($page.error);
let status = $derived($page.status);
@@ -17,7 +18,7 @@
function handleRetry() {
// Navigate back to home page after successful API key validation
goto('#/');
goto(ROUTES.START);
}
</script>
@@ -60,7 +61,7 @@
</p>
</div>
<button
onclick={() => goto('#/')}
onclick={() => goto(ROUTES.START)}
class="rounded-md bg-primary px-4 py-2 text-primary-foreground hover:bg-primary/90"
>
Go Home
+5 -3
View File
@@ -18,6 +18,8 @@
import { isRouterMode, serverStore } from '$lib/stores/server.svelte';
import { config, settingsStore } from '$lib/stores/settings.svelte';
import { ModeWatcher } from 'mode-watcher';
import { ROUTES } from '$lib/constants/routes';
import { RouterService } from '$lib/services/router.service';
import { Toaster } from 'svelte-sonner';
import { modelsStore } from '$lib/stores/models.svelte';
import { mcpStore } from '$lib/stores/mcp.svelte';
@@ -53,7 +55,7 @@
const currentId = page.params.id;
if (!currentId) {
goto(`#/chat/${allConvs[direction === 1 ? 0 : allConvs.length - 1].id}`);
goto(RouterService.chat(allConvs[direction === 1 ? 0 : allConvs.length - 1].id));
return;
}
@@ -64,9 +66,9 @@
const targetIdx = idx + direction;
if (targetIdx >= 0 && targetIdx < allConvs.length) {
goto(`#/chat/${allConvs[targetIdx].id}`);
goto(RouterService.chat(allConvs[targetIdx].id));
} else {
goto('?new_chat=true#/');
goto(ROUTES.NEW_CHAT);
}
}
@@ -4,6 +4,7 @@
import { browser } from '$app/environment';
import { page } from '$app/state';
import { ActionIcon } from '$lib/components/app';
import { SETTINGS_FALLBACK_EXIT_ROUTE } from '$lib/constants';
let { children } = $props();
@@ -21,7 +22,7 @@
if (browser && window.history.length > 1 && !prevIsSettings) {
history.back();
} else {
goto('#/');
goto(SETTINGS_FALLBACK_EXIT_ROUTE);
}
}
</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} />