ui : mark external provider rows with the provider icon

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 a5ad735e93
commit 12f9bd83b8
5 changed files with 59 additions and 1 deletions
@@ -0,0 +1,32 @@
<script lang="ts">
import BackendPresetIcon from './BackendPresetIcon.svelte';
import type { Backend } from '$lib/types';
import { backendFaviconUrl, findBackendPreset } from '$lib/utils';
import { SvelteSet } from 'svelte/reactivity';
/** Favicons that failed to load, so a row remount does not ask again. */
const failedFavicons = new SvelteSet<string>();
interface Props {
backend?: Backend;
class?: string;
}
let { backend, class: className = 'h-4 w-4' }: Props = $props();
let preset = $derived(backend ? findBackendPreset(backend.baseUrl) : undefined);
let faviconUrl = $derived(preset || !backend ? null : backendFaviconUrl(backend.baseUrl));
</script>
{#if preset}
<BackendPresetIcon class={className} {preset} />
{:else if faviconUrl && !failedFavicons.has(faviconUrl)}
<img
alt=""
class={['shrink-0 rounded-sm object-contain', className]}
decoding="async"
loading="lazy"
onerror={() => failedFavicons.add(faviconUrl)}
src={faviconUrl}
/>
{/if}
@@ -1,5 +1,6 @@
export { default as BackendCard } from './BackendCard.svelte';
export { default as BackendForm } from './BackendForm.svelte';
export { default as BackendIcon } from './BackendIcon.svelte';
export { default as BackendPresetCard } from './BackendPresetCard.svelte';
export { default as BackendPresetIcon } from './BackendPresetIcon.svelte';
export { default as DialogBackendForm } from './DialogBackendForm.svelte';
@@ -12,6 +12,7 @@
RotateCw
} from '@lucide/svelte';
import { ActionIcon, ModelId } from '$lib/components/app';
import { BackendIcon } from '$lib/components/app/backends';
import { HF_BASE_MODEL_TAG_REGEX, ICON_CLASS_DEFAULT } from '$lib/constants';
import { ModelCapability, ServerModelStatus } from '$lib/enums';
import { HuggingFaceService, ModelsService } from '$lib/services';
@@ -222,7 +223,13 @@
</div>
{#if !canLoad}
<!-- remote rows have no load state, the column stays out of the way -->
<!-- external providers carry no load state: the row shows the provider
mark instead, which is what identifies it in a flat list -->
{#if rowBackend}
<div class="flex w-5 items-center justify-center">
<BackendIcon backend={rowBackend} class="h-3.5 w-3.5" />
</div>
{/if}
{:else if isLoading}
<div class="flex w-5 items-center justify-center">
<Loader2 class="{ICON_CLASS_DEFAULT} animate-spin text-muted-foreground" />
+17
View File
@@ -34,6 +34,23 @@ export function backendModelsUrl(backend: Backend): string {
return joinBackendUrl(backend.baseUrl, backend.modelsPath ?? DEFAULT_BACKEND_MODELS_PATH);
}
/**
* Favicon of a backend's root domain, for endpoints with no bundled mark. API
* hosts rarely serve one, so the subdomain is dropped: `api.z.ai` -> `z.ai`.
* Returns null when the URL carries no usable host.
*/
export function backendFaviconUrl(baseUrl: string): string | null {
try {
const { hostname, protocol } = new URL(baseUrl);
const labels = hostname.split('.');
const root = labels.length > 2 ? labels.slice(-2).join('.') : hostname;
return `${protocol}//${root}/favicon.ico`;
} catch {
return null;
}
}
/**
* Preset a backend was created from, matched on origin and path so a saved
* backend keeps its branding. Returns undefined for edited or custom URLs.
+1
View File
@@ -26,6 +26,7 @@ export { ApiError, apiDelete, apiFetch, apiFetchWithParams, apiPost } from './ap
export { validateApiKey } from './api-key-validation';
export {
backendChatUrl,
backendFaviconUrl,
backendModelsUrl,
createLocalBackend,
findBackendPreset,