ui : pick backends from recommended provider cards

Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash
This commit is contained in:
Aleksander Grygier
2026-09-16 23:48:44 +02:00
parent ddf146248f
commit a0360ca737
5 changed files with 71 additions and 65 deletions
@@ -0,0 +1,41 @@
<script lang="ts">
import BackendPresetIcon from './BackendPresetIcon.svelte';
import { Check } from '@lucide/svelte';
import { Badge } from '$lib/components/ui/badge';
import * as Card from '$lib/components/ui/card';
import type { BackendPreset } from '$lib/types';
interface Props {
preset: BackendPreset;
onClick?: () => void;
selected?: boolean;
dimmed?: boolean;
/** A backend already points at this provider. */
added?: boolean;
}
let { added = false, dimmed = false, onClick, preset, selected = false }: Props = $props();
// an added provider is a label, not an action: no hover, no selection
let interactive = $derived(!added && Boolean(onClick));
</script>
<Card.Root
class={`relative gap-3! select-none bg-muted/30 p-4 transition-all ${interactive ? 'cursor-pointer hover:bg-muted/50 hover:opacity-100' : ''} ${selected && !added ? 'bg-muted/30 ring-1 ring-primary/40' : ''} ${dimmed || added ? 'opacity-50' : ''}`}
onclick={interactive ? onClick : undefined}
>
{#if added}
<Badge class="absolute top-2 right-2 gap-1 px-1.5 text-[10px]" variant="secondary">
<Check class="h-3 w-3" />
Added
</Badge>
{/if}
<div class="flex min-w-0 items-center gap-2">
<BackendPresetIcon class="h-5 w-5" {preset} />
<h4 class="min-w-0 flex-1 truncate font-medium">{preset.name}</h4>
</div>
<p class="text-xs text-muted-foreground">{preset.description}</p>
</Card.Root>
@@ -1,6 +1,6 @@
<script lang="ts">
import BackendForm from './BackendForm.svelte';
import BackendPresetIcon from './BackendPresetIcon.svelte';
import BackendPresetCard from './BackendPresetCard.svelte';
import { CheckCircle2, Loader2, XCircle } from '@lucide/svelte';
import { Button } from '$lib/components/ui/button';
import * as Dialog from '$lib/components/ui/dialog';
@@ -9,7 +9,7 @@
import type { BackendTestResult } from '$lib/services/backends.service';
import { backendsStore } from '$lib/stores';
import type { Backend, BackendPreset } from '$lib/types';
import { uuid } from '$lib/utils';
import { findBackendPreset, uuid } from '$lib/utils';
interface Props {
backend?: Backend | null;
@@ -21,10 +21,18 @@
let { backend = null, onOpenChange, onSaved, open = $bindable(false) }: Props = $props();
let draft = $state<Backend>(createBackend());
let selectedPresetId = $state<string | null>(null);
let testResult = $state<BackendTestResult | null>(null);
let testing = $state(false);
// the card follows the URL, so editing any field deselects it
let selectedPresetId = $derived(findBackendPreset(draft.baseUrl)?.id ?? null);
// presets a configured backend already points at
let addedPresetIds = $derived(
backendsStore.external
.map((backend) => findBackendPreset(backend.baseUrl)?.id)
.filter((id) => id !== undefined)
);
let isEdit = $derived(backend !== null);
let urlError = $derived.by(() => {
const url = draft.baseUrl.trim();
@@ -46,7 +54,6 @@
if (!open) return;
draft = backend ? { ...backend } : createBackend();
selectedPresetId = null;
testResult = null;
testing = false;
});
@@ -62,14 +69,13 @@
}
function applyPreset(preset: BackendPreset) {
selectedPresetId = preset.id;
draft = {
...draft,
baseUrl: preset.baseUrl,
chatPath: preset.chatPath,
compat: preset.compat,
modelsPath: preset.modelsPath,
name: preset.id === 'custom' ? '' : preset.name,
name: preset.name,
protocol: preset.protocol
};
testResult = null;
@@ -136,19 +142,20 @@
</Dialog.Header>
{#if !isEdit}
<div class="grid grid-cols-2 gap-2 pt-2 sm:grid-cols-3">
{#each BACKEND_PRESETS as preset (preset.id)}
<Button
class="justify-start"
onclick={() => applyPreset(preset)}
size="sm"
variant={selectedPresetId === preset.id ? 'secondary' : 'outline'}
>
<BackendPresetIcon class="h-4 w-4" {preset} />
<div class="space-y-3 pt-2">
<h3 class="text-sm font-medium">Recommended providers</h3>
{preset.name}
</Button>
{/each}
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
{#each BACKEND_PRESETS as preset (preset.id)}
<BackendPresetCard
added={addedPresetIds.includes(preset.id)}
dimmed={Boolean(selectedPresetId) && selectedPresetId !== preset.id}
onClick={() => applyPreset(preset)}
{preset}
selected={selectedPresetId === preset.id}
/>
{/each}
</div>
</div>
{/if}
@@ -1,5 +1,6 @@
export { default as BackendCard } from './BackendCard.svelte';
export { default as BackendForm } from './BackendForm.svelte';
export { default as BackendPresetCard } from './BackendPresetCard.svelte';
export { default as BackendPresetIcon } from './BackendPresetIcon.svelte';
export { default as DialogBackendForm } from './DialogBackendForm.svelte';
export { default as SettingsBackends } from './SettingsBackends.svelte';
@@ -63,6 +63,7 @@ export const BACKEND_COMPAT: Record<BackendProtocol, BackendCompat> = {
export const BACKEND_PRESETS: readonly BackendPreset[] = [
{
baseUrl: 'https://router.huggingface.co',
description: 'Open models from the Hub, served by inference providers.',
iconUrl: '/backend-presets/huggingface.svg',
id: 'huggingface',
name: 'Hugging Face',
@@ -70,58 +71,12 @@ export const BACKEND_PRESETS: readonly BackendPreset[] = [
},
{
baseUrl: 'https://openrouter.ai/api',
description: 'Models from many providers behind a single API.',
// v2 brand: purple on light, lime on dark
iconUrlDark: '/backend-presets/openrouter-dark.svg',
iconUrlLight: '/backend-presets/openrouter-light.svg',
id: 'openrouter',
name: 'OpenRouter',
protocol: 'openai'
},
{
baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
chatPath: '/chat/completions',
iconUrl: '/backend-presets/google.svg',
id: 'google',
modelsPath: '/models',
name: 'Google',
protocol: 'openai'
},
{
baseUrl: 'https://api.z.ai/api/paas/v4',
chatPath: '/chat/completions',
iconUrl: '/backend-presets/zai.png',
id: 'zai',
modelsPath: '/models',
name: 'Z.ai',
protocol: 'openai'
},
{
baseUrl: 'https://api.deepseek.com',
iconUrl: '/backend-presets/deepseek.svg',
id: 'deepseek',
name: 'DeepSeek',
protocol: 'openai'
},
{
baseUrl: 'https://api.moonshot.ai',
iconUrl: '/backend-presets/kimi.png',
id: 'kimi',
name: 'Kimi',
protocol: 'openai'
},
{
baseUrl: 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1',
chatPath: '/chat/completions',
iconUrl: '/backend-presets/qwen.png',
id: 'qwen',
modelsPath: '/models',
name: 'Qwen',
protocol: 'openai'
},
{
baseUrl: '',
id: 'custom',
name: 'Custom',
protocol: 'openai'
}
];
+2
View File
@@ -75,6 +75,8 @@ export interface BackendPreset {
/** Optional help text shown under the API key field. */
apiKeyHelp?: string;
baseUrl: string;
/** One line describing the endpoint, shown on the preset card. */
description?: string;
chatPath?: string;
/** Brand mark used in both themes, for logos that carry their own background. */
iconUrl?: string;