ui : manage backends in their own dialog

Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash
This commit is contained in:
Aleksander Grygier
2026-09-28 12:10:11 +02:00
parent 93391d0a34
commit 1f89bfe475
10 changed files with 87 additions and 61 deletions
@@ -0,0 +1,35 @@
<script lang="ts">
import { SettingsBackends } from '$lib/components/app/backends';
import { Logo } from '$lib/components/app/misc';
import * as Dialog from '$lib/components/ui/dialog';
interface Props {
open?: boolean;
onOpenChange?: (open: boolean) => void;
}
let { onOpenChange, open = $bindable(false) }: Props = $props();
function handleOpenChange(value: boolean) {
open = value;
onOpenChange?.(value);
}
</script>
<Dialog.Root onOpenChange={handleOpenChange} {open}>
<Dialog.Content class="md:max-h-[80vh]! md:w-[calc(100vw-4rem)]! md:max-w-164! flex flex-col">
<Dialog.Header>
<Dialog.Title class="flex items-center gap-2">
<Logo class="h-5 w-5" style="--size: 1.25rem" />
<span>Backends</span>
</Dialog.Title>
<Dialog.Description>
Endpoints this UI can talk to. The built-in backend is the llama.cpp server serving it.
</Dialog.Description>
</Dialog.Header>
<SettingsBackends class="mt-4 overflow-y-auto" />
</Dialog.Content>
</Dialog.Root>
@@ -106,6 +106,7 @@ export { default as DialogExportSettings } from './DialogExportSettings.svelte';
* />
* ```
*/
export { default as DialogBackends } from './DialogBackends.svelte';
export { default as DialogConfirmation } from './DialogConfirmation.svelte';
/**
@@ -244,7 +244,7 @@
>
<MODEL_SELECTOR_ICON class="h-3.5 w-3.5 shrink-0" />
<span class="flex min-w-0 items-center gap-1">
<span class="flex min-w-0 items-center gap-0.5">
{#if selectedOption}
<ModelId
class="min-w-0 overflow-hidden"
@@ -364,16 +364,17 @@
</div>
{#snippet footer()}
<!-- Sticky actions footer: reasoning effort panel.
Sticks to the bottom of the content scrollport. -->
<!-- Sticky actions footer: backends, then the reasoning effort
panel. Sticks to the bottom of the content scrollport. -->
<ModelsSelectorReasoningPanel inMenu />
<div
class="px-1.5"
class="flex flex-col px-1.5"
onfocusin={clearHighlight}
onmouseenter={clearHighlight}
role="none"
>
<ModelsSelectorReasoningPanel inMenu />
</div>
></div>
{/snippet}
</DropdownMenuSearchable>
</DropdownMenu.Content>
@@ -204,7 +204,7 @@
/>
</div>
<div class="px-2 pb-1">
<div class="flex flex-col px-2 pb-1">
<ModelsSelectorReasoningPanel />
</div>
</div>
@@ -4,6 +4,7 @@
import { page } from '$app/state';
import {
ActionIcon,
DialogBackends,
DialogConversationRename,
DialogModelsDiscover,
DialogSettingsChat,
@@ -94,6 +95,7 @@
let renameDialogOpen = $state(false);
let settingsDialogOpen = $state(false);
let backendsDialogOpen = $state(false);
let modelsDiscoverOpen = $state(false);
let renameTargetConversationId = $state<string | null>(null);
let renameDraft = $state('');
@@ -391,6 +393,7 @@
bind:searchQuery
class="px-2"
isExpandedMode={innerWidth > 768 ? uiStore.isSidebarExpanded : true}
onBackendsClick={() => (backendsDialogOpen = true)}
onDiscoverModelsClick={() => (modelsDiscoverOpen = true)}
onNewChat={() => {
if (deviceStore.isMobile) {
@@ -455,6 +458,8 @@
<DialogSettingsChat bind:open={settingsDialogOpen} />
<DialogBackends bind:open={backendsDialogOpen} />
<DialogModelsDiscover bind:open={modelsDiscoverOpen} />
<style>
@@ -9,6 +9,7 @@
ICON_STRIP_TRANSITION_DELAY_MULTIPLIER,
ICON_STRIP_TRANSITION_DURATION,
ROUTES,
SETTINGS_KEYS,
SIDEBAR_ACTIONS_ITEMS
} from '$lib/constants';
import { SidebarAction, TooltipSide } from '$lib/enums';
@@ -25,8 +26,9 @@
searchQuery: string;
onSearchDeactivated?: () => void;
onSearchClick?: () => void;
onNewChat?: () => void;
onBackendsClick?: () => void;
onDiscoverModelsClick?: () => void;
onNewChat?: () => void;
onSettingsClick?: () => void;
}
@@ -34,6 +36,7 @@
class: className,
isExpandedMode = false,
isSearchModeActive = $bindable(false),
onBackendsClick,
onDiscoverModelsClick,
onNewChat,
onSearchClick,
@@ -48,10 +51,9 @@
const isOnMobile = $derived(deviceStore.isMobile);
// Discover models is opt-in (General settings) and needs the router's
// download endpoints; hide it when either gate is closed
// discovery needs the local router's download endpoints
const actionsItems = $derived(
serverStore.localIsRouter && settingsStore.config.enableDiscoverModels
serverStore.localIsRouter && settingsStore.config[SETTINGS_KEYS.ENABLE_DISCOVER_MODELS] === true
? SIDEBAR_ACTIONS_ITEMS
: SIDEBAR_ACTIONS_ITEMS.filter((item) => item.action !== SidebarAction.DISCOVER_MODELS)
);
@@ -129,16 +131,18 @@
}
: item.action === SidebarAction.DISCOVER_MODELS
? () => onDiscoverModelsClick?.()
: item.action === SidebarAction.SETTINGS
? () => onSettingsClick?.()
: item.route
? () => {
onNewChat?.();
goto(item.route!);
}
: isSearchOnMobile
? undefined
: onSearchClick}
: item.action === SidebarAction.BACKENDS
? () => onBackendsClick?.()
: item.action === SidebarAction.SETTINGS
? () => onSettingsClick?.()
: item.route
? () => {
onNewChat?.();
goto(item.route!);
}
: isSearchOnMobile
? undefined
: onSearchClick}
{@const itemTransition = {
delay: !initialized ? i * ICON_STRIP_TRANSITION_DELAY_MULTIPLIER : 0,
duration: ICON_STRIP_TRANSITION_DURATION,
@@ -187,16 +191,18 @@
}
: item.action === SidebarAction.DISCOVER_MODELS
? () => onDiscoverModelsClick?.()
: item.action === SidebarAction.SETTINGS
? () => onSettingsClick?.()
: item.route
? () => {
onNewChat?.();
goto(item.route!);
}
: isSearchOnMobile
? undefined
: onSearchClick}
: item.action === SidebarAction.BACKENDS
? () => onBackendsClick?.()
: item.action === SidebarAction.SETTINGS
? () => onSettingsClick?.()
: item.route
? () => {
onNewChat?.();
goto(item.route!);
}
: isSearchOnMobile
? undefined
: onSearchClick}
{@const itemTransition = {
delay: !initialized ? i * ICON_STRIP_TRANSITION_DELAY_MULTIPLIER : 0,
duration: ICON_STRIP_TRANSITION_DURATION,
@@ -1,6 +1,5 @@
<script lang="ts">
import { RefreshCw } from '@lucide/svelte';
import { SettingsBackends } from '$lib/components/app/backends';
import {
SettingsChatDesktopSidebar,
SettingsChatFields,
@@ -165,8 +164,6 @@
<SettingsChatToolsTab />
{:else if currentSection.slug === SETTINGS_SECTION_SLUGS.IMPORT_EXPORT}
<SettingsChatImportExportTab />
{:else if currentSection.slug === SETTINGS_SECTION_SLUGS.BACKENDS}
<SettingsBackends />
{:else if currentSection.fields}
<div class="space-y-6">
<SettingsChatFields
@@ -11,7 +11,6 @@ import {
Monitor,
Moon,
PencilRuler,
Server,
SlidersVertical,
Sun
} from '@lucide/svelte';
@@ -197,30 +196,6 @@ export const SETTINGS_REGISTRY: SettingsSectionEntry[] = [
slug: SETTINGS_SECTION_SLUGS.GENERAL,
title: SETTINGS_SECTION_TITLES.GENERAL
},
// Backends (non-UI config object)
{
icon: Server,
settings: [
{
defaultValue: '[]',
help: 'Configure external API backends as a JSON list. The local backend is always available.',
key: SETTINGS_KEYS.BACKENDS,
label: 'Backends',
standaloneField: false,
type: SettingsFieldType.INPUT
},
{
defaultValue: true,
help: 'Query the llama-server that serves this UI. Disable it when this UI runs standalone and talks to external backends only.',
key: SETTINGS_KEYS.LOCAL_BACKEND_ENABLED,
label: 'Local backend',
standaloneField: false,
type: SettingsFieldType.CHECKBOX
}
],
slug: SETTINGS_SECTION_SLUGS.BACKENDS,
title: SETTINGS_SECTION_TITLES.BACKENDS
},
// Display
{
icon: Monitor,
+6 -1
View File
@@ -1,4 +1,4 @@
import { Package, PackageSearch, Search, Settings, SquarePen } from '@lucide/svelte';
import { Package, PackageSearch, Search, Server, Settings, SquarePen } from '@lucide/svelte';
import { SidebarAction, ToolSource } from '$lib/enums';
import type { DesktopIconStripItem } from '$lib/types';
@@ -74,6 +74,11 @@ export const SIDEBAR_ACTIONS_ITEMS: DesktopIconStripItem[] = [
icon: PackageSearch,
tooltip: 'Discover models'
},
{
action: SidebarAction.BACKENDS,
icon: Server,
tooltip: 'Backends'
},
{
action: SidebarAction.SETTINGS,
icon: Settings,
+1
View File
@@ -23,6 +23,7 @@ export enum ScrollCarouselVariant {
* Sidebar icon strip actions handled directly by the sidebar.
*/
export enum SidebarAction {
BACKENDS = 'backends',
DISCOVER_MODELS = 'discover-models',
NEW_CHAT = 'new-chat',
SETTINGS = 'settings'