ui : put manage, discover and providers in one dialog

Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash
This commit is contained in:
Aleksander Grygier
2026-09-28 12:10:13 +02:00
parent 3dea340073
commit 9d2ca5ac05
9 changed files with 108 additions and 93 deletions
@@ -4,7 +4,8 @@
import { CheckCircle2, Loader2, XCircle } from '@lucide/svelte';
import { Button } from '$lib/components/ui/button';
import * as Dialog from '$lib/components/ui/dialog';
import { BACKEND_ID_PREFIX, BACKEND_PRESETS } from '$lib/constants';
import * as ToggleGroup from '$lib/components/ui/toggle-group';
import { BACKEND_ID_PREFIX, BACKEND_PRESETS, BACKEND_PROTOCOLS } from '$lib/constants';
import { BackendsService } from '$lib/services';
import type { BackendTestResult } from '$lib/services/backends.service';
import { backendsStore } from '$lib/stores';
@@ -29,8 +30,15 @@
let draft = $state<Backend>(createBackend());
// presets offered for the protocol the caller is adding
let presets = $derived(BACKEND_PRESETS.filter((preset) => preset.protocol === defaultProtocol));
// the caller seeds the protocol, the picker overrides it while adding
let protocolPick = $state<string | undefined>(undefined);
let protocol = $derived(
BACKEND_PROTOCOLS.includes(protocolPick as BackendProtocol)
? (protocolPick as BackendProtocol)
: (backend?.protocol ?? defaultProtocol)
);
// presets offered for the protocol being added
let presets = $derived(BACKEND_PRESETS.filter((preset) => preset.protocol === protocol));
let testResult = $state<BackendTestResult | null>(null);
let testing = $state(false);
@@ -149,12 +157,26 @@
<Dialog.Title>{isEdit ? 'Edit backend' : 'Add backend'}</Dialog.Title>
<Dialog.Description>
{defaultProtocol === 'llama.cpp'
{protocol === 'llama.cpp'
? 'Point at another llama-server.'
: 'Connect an OpenAI-compatible endpoint.'}
</Dialog.Description>
</Dialog.Header>
{#if !isEdit}
<div class="space-y-3 pt-2">
<h3 class="text-sm font-medium">Protocol</h3>
<ToggleGroup.Root bind:value={protocolPick} type="single" variant="outline">
{#each BACKEND_PROTOCOLS as option (option)}
<ToggleGroup.Item value={option}>
{option === 'llama.cpp' ? 'llama.cpp' : 'OpenAI-compatible'}
</ToggleGroup.Item>
{/each}
</ToggleGroup.Root>
</div>
{/if}
{#if !isEdit && presets.length > 0}
<div class="space-y-3 pt-2">
<h3 class="text-sm font-medium">Recommended providers</h3>
@@ -9,14 +9,33 @@
interface Props {
class?: string;
protocol: BackendProtocol;
/** Narrows the list to one protocol; omit it to list every provider at once. */
protocol?: BackendProtocol;
}
let { class: className, protocol }: Props = $props();
// the switcher above the list narrows it to one protocol
let backends = $derived(backendsStore.external.filter((b) => b.protocol === protocol));
let isLlamaCpp = $derived(protocol === 'llama.cpp');
let backends = $derived(
protocol
? backendsStore.external.filter((b) => b.protocol === protocol)
: backendsStore.external
);
// the bundled server belongs to the list whenever llama.cpp providers are in it
let showsLocal = $derived(protocol === undefined || protocol === 'llama.cpp');
let addTitle = $derived(
protocol === 'openai'
? 'Add a backend'
: protocol === 'llama.cpp'
? 'Add a llama.cpp backend'
: 'Add a provider'
);
let addDescription = $derived(
protocol === 'openai'
? 'Connect an OpenAI-compatible endpoint.'
: protocol === 'llama.cpp'
? 'Point at another llama-server.'
: 'Point at a llama-server or connect an OpenAI-compatible endpoint.'
);
let isAdding = $state(false);
let editing = $state<Backend | null>(null);
@@ -48,12 +67,12 @@
<DialogBackendForm
bind:open={isAdding}
backend={editing}
defaultProtocol={protocol}
defaultProtocol={protocol ?? 'llama.cpp'}
onOpenChange={handleOpenChange}
onSaved={() => void backendsModelsStore.loadAll()}
/>
{#if isLlamaCpp && !serverStore.localServerMissing}
{#if showsLocal && !serverStore.localServerMissing}
<BackendCard
backend={backendsStore.local}
isLocal
@@ -82,11 +101,9 @@
<Plus />
</Empty.Media>
<Empty.Title>{isLlamaCpp ? 'Add a llama.cpp backend' : 'Add a backend'}</Empty.Title>
<Empty.Title>{addTitle}</Empty.Title>
<Empty.Description>
{isLlamaCpp ? 'Point at another llama-server.' : 'Connect an OpenAI-compatible endpoint.'}
</Empty.Description>
<Empty.Description>{addDescription}</Empty.Description>
</Empty.Header>
<Empty.Content>
@@ -1,8 +1,10 @@
<script lang="ts">
import { Box, PackageSearch } from '@lucide/svelte';
import { Box, Compass, Server } from '@lucide/svelte';
import SettingsBackends from '$lib/components/app/backends/SettingsBackends.svelte';
import ModelsDiscover from '$lib/components/app/models/discover/ModelsDiscover.svelte';
import ModelsManager from '$lib/components/app/models/ModelsManager/ModelsManager.svelte';
import { Button } from '$lib/components/ui/button';
import * as Dialog from '$lib/components/ui/dialog';
import * as Tabs from '$lib/components/ui/tabs';
import { uiStore } from '$lib/stores';
interface Props {
@@ -12,6 +14,17 @@
let { onOpenChange, open = $bindable(false) }: Props = $props();
let tab = $state('manage');
// the sidebar's Discover entry opens this dialog on its Discover tab
$effect(() => {
if (!uiStore.discoverModelsOpen) return;
uiStore.discoverModelsOpen = false;
tab = 'discover';
handleOpenChange(true);
});
function handleOpenChange(value: boolean) {
open = value;
onOpenChange?.(value);
@@ -20,7 +33,7 @@
<Dialog.Root onOpenChange={handleOpenChange} {open}>
<Dialog.Content
class="md:h-[calc(100vh-4rem)]! md:max-h-240! md:w-[calc(100vw-4rem)]! md:max-w-360! flex flex-col p-4"
class="md:h-[calc(100vh-4rem)]! md:max-h-240! md:w-[calc(100vw-4rem)]! md:max-w-380! flex flex-col p-4"
onCloseAutoFocus={(event) => event.preventDefault()}
onOpenAutoFocus={(event) => event.preventDefault()}
>
@@ -28,16 +41,46 @@
<Dialog.Title class="flex items-center gap-2">
<Box class="h-5 w-5" />
<span>Manage models</span>
<span>Models</span>
</Dialog.Title>
<Button onclick={() => uiStore.openDiscoverModels()} size="sm" variant="outline">
<PackageSearch class="h-3.5 w-3.5" />
Discover models
</Button>
</Dialog.Header>
<ModelsManager class="mt-4" onClose={() => handleOpenChange(false)} />
<Tabs.Root bind:value={tab} class="mt-2 min-h-0 flex-1 gap-0">
<div class="px-2">
<Tabs.List>
<Tabs.Trigger value="manage">
<Box class="h-3.5 w-3.5" />
Manage
</Tabs.Trigger>
<Tabs.Trigger value="discover">
<Compass class="h-3.5 w-3.5" />
Discover
</Tabs.Trigger>
<Tabs.Trigger value="providers">
<Server class="h-3.5 w-3.5" />
Providers
</Tabs.Trigger>
</Tabs.List>
</div>
<Tabs.Content class="flex min-h-0 flex-1 flex-col pt-4" value="manage">
<ModelsManager />
</Tabs.Content>
<Tabs.Content class="min-h-0 flex-1 overflow-hidden pt-4" value="discover">
<div class="grid h-full min-h-0 gap-0" style="grid-template-columns: auto 1fr;">
<ModelsDiscover />
</div>
</Tabs.Content>
<Tabs.Content class="min-h-0 flex-1 overflow-y-auto px-2 pt-4" value="providers">
<SettingsBackends />
</Tabs.Content>
</Tabs.Root>
</Dialog.Content>
</Dialog.Root>
@@ -1,25 +0,0 @@
<script lang="ts">
import { ModelsDiscover } from '$lib/components/app/models/discover';
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="grid gap-0 p-0 md:h-[calc(100vh-4rem)]! md:max-h-240! md:w-[calc(100vw-4rem)]! md:max-w-380!"
style="grid-template-columns: auto 1fr;"
>
<ModelsDiscover />
</Dialog.Content>
</Dialog.Root>
@@ -549,4 +549,3 @@ export { default as DialogMermaidPreview } from './DialogMermaidPreview.svelte';
*
* @see ModelsDiscover in $lib/components/app/models/discover
*/
export { default as DialogModelsDiscover } from './DialogModelsDiscover.svelte';
@@ -6,7 +6,6 @@
ActionIcon,
DialogConversationRename,
DialogManageModels,
DialogModelsDiscover,
DialogSettingsChat,
Logo,
SidebarNavigationActions,
@@ -457,8 +456,6 @@
<DialogManageModels bind:open={uiStore.manageModelsOpen} />
<DialogModelsDiscover bind:open={uiStore.discoverModelsOpen} />
<style>
aside {
@media (max-width: 768px) {
@@ -5,7 +5,6 @@
SettingsChatFields,
SettingsChatImportExportTab,
SettingsChatMobileHeader,
SettingsChatProvidersTab,
SettingsChatToolsTab,
SettingsFooter
} from '$lib/components/app/settings';
@@ -161,9 +160,7 @@
<div class="mx-auto max-w-2xl px-4 flex-1 md:mt-4">
<div class="space-y-6 pt-3">
<div class="grid">
{#if currentSection.slug === SETTINGS_SECTION_SLUGS.BACKENDS}
<SettingsChatProvidersTab />
{:else if currentSection.slug === SETTINGS_SECTION_SLUGS.TOOLS}
{#if currentSection.slug === SETTINGS_SECTION_SLUGS.TOOLS}
<SettingsChatToolsTab />
{:else if currentSection.slug === SETTINGS_SECTION_SLUGS.IMPORT_EXPORT}
<SettingsChatImportExportTab />
@@ -1,34 +0,0 @@
<script lang="ts">
import { SettingsBackends } from '$lib/components/app/backends';
import * as Tabs from '$lib/components/ui/tabs';
import type { BackendProtocol } from '$lib/types';
const PROTOCOL_TABS: Array<{ label: string; value: BackendProtocol }> = [
{ label: 'Llama-compat', value: 'llama.cpp' },
{ label: 'OpenAI-compat', value: 'openai' }
];
let protocol = $state<BackendProtocol>('llama.cpp');
</script>
<div class="space-y-3">
<p class="text-sm text-muted-foreground">Endpoints this UI can talk to.</p>
<Tabs.Root
class="gap-3"
onValueChange={(value) => (protocol = value as BackendProtocol)}
value={protocol}
>
<Tabs.List>
{#each PROTOCOL_TABS as protocolTab (protocolTab.value)}
<Tabs.Trigger value={protocolTab.value}>{protocolTab.label}</Tabs.Trigger>
{/each}
</Tabs.List>
{#each PROTOCOL_TABS as protocolTab (protocolTab.value)}
<Tabs.Content value={protocolTab.value}>
<SettingsBackends protocol={protocolTab.value} />
</Tabs.Content>
{/each}
</Tabs.Root>
</div>
@@ -79,4 +79,3 @@ export { default as SettingsChatToolsTab } from './SettingsChat/SettingsChatTool
* Providers panel for chat settings. Switches between the llama.cpp and
* OpenAI-compatible protocols and lists the endpoints configured for each.
*/
export { default as SettingsChatProvidersTab } from './SettingsChat/SettingsChatProvidersTab.svelte';