summaryrefslogtreecommitdiffhomepage
path: root/packages/frontend/src/lib/components/ModelStatus.svelte
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-04 21:21:20 +0900
committerAdam Malczewski <[email protected]>2026-06-04 21:21:20 +0900
commit394f1ed37ce860da6fdc385769bf29f9737105cd (patch)
tree4b825dc642cb6eb9a060e54bf8d69288fbee4904 /packages/frontend/src/lib/components/ModelStatus.svelte
parent81a9cdbadf8c9d940d4fe9a2a0de607dee1f5f1a (diff)
downloaddispatch-394f1ed37ce860da6fdc385769bf29f9737105cd.tar.gz
dispatch-394f1ed37ce860da6fdc385769bf29f9737105cd.zip
chore: genesis — remove all files to rebuild from scratch (arch rewrite)
Diffstat (limited to 'packages/frontend/src/lib/components/ModelStatus.svelte')
-rw-r--r--packages/frontend/src/lib/components/ModelStatus.svelte356
1 files changed, 0 insertions, 356 deletions
diff --git a/packages/frontend/src/lib/components/ModelStatus.svelte b/packages/frontend/src/lib/components/ModelStatus.svelte
deleted file mode 100644
index 57c5efd..0000000
--- a/packages/frontend/src/lib/components/ModelStatus.svelte
+++ /dev/null
@@ -1,356 +0,0 @@
-<script lang="ts">
-interface KeyInfo {
- id: string;
- provider: string;
- status: "active" | "exhausted";
- lastError: string | null;
- exhaustedAt: number | null;
-}
-
-interface CredentialStatus {
- keyId: string;
- provider: string;
- subscriptionType: string | null;
- importedAt: number;
- updatedAt: number;
- expired: boolean;
-}
-
-const {
- keys = [],
- currentModel,
- apiBase = "",
- onAddKey = () => {},
-}: {
- keys?: KeyInfo[];
- currentModel?: string;
- apiBase?: string;
- onAddKey?: () => void;
-} = $props();
-
-const activeKeys = $derived(keys.filter((k) => k.status === "active").length);
-const totalKeys = $derived(keys.length);
-const allActive = $derived(totalKeys > 0 && activeKeys === totalKeys);
-const allExhausted = $derived(totalKeys > 0 && activeKeys === 0);
-const someExhausted = $derived(totalKeys > 0 && activeKeys < totalKeys && activeKeys > 0);
-
-let credentialStatus = $state<Record<string, CredentialStatus>>({});
-let importingKey = $state<string | null>(null);
-let importError = $state<string | null>(null);
-let importSuccess = $state<string | null>(null);
-
-let apiKeyStatus = $state<
- Record<string, { keyId: string; provider: string; importedAt: number; updatedAt: number }>
->({});
-let showKeyModal = $state<string | null>(null); // keyId or null
-let keyModalValue = $state("");
-let keyModalError = $state<string | null>(null);
-let keyModalSaving = $state(false);
-let removingKey = $state<string | null>(null);
-
-async function loadCredentialStatus(): Promise<void> {
- try {
- const res = await fetch(`${apiBase}/models/credentials-status`);
- if (!res.ok) return;
- const data = (await res.json()) as { credentials: CredentialStatus[] };
- const map: Record<string, CredentialStatus> = {};
- for (const cred of data.credentials) {
- map[cred.keyId] = cred;
- }
- credentialStatus = map;
- } catch {
- // ignore
- }
-}
-
-async function importCredentials(keyId: string): Promise<void> {
- importingKey = keyId;
- importError = null;
- importSuccess = null;
- try {
- const res = await fetch(`${apiBase}/models/import-credentials`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ keyId }),
- });
- const data = (await res.json()) as { success?: boolean; error?: string };
- if (!res.ok || !data.success) {
- importError = data.error ?? "Import failed";
- } else {
- importSuccess = keyId;
- await loadCredentialStatus();
- setTimeout(() => {
- importSuccess = null;
- }, 3000);
- }
- } catch (e) {
- importError = e instanceof Error ? e.message : "Network error";
- } finally {
- importingKey = null;
- }
-}
-
-async function loadApiKeyStatus(): Promise<void> {
- try {
- const res = await fetch(`${apiBase}/models/api-keys-status`);
- if (!res.ok) return;
- const data = (await res.json()) as {
- keys: Array<{ keyId: string; provider: string; importedAt: number; updatedAt: number }>;
- };
- const map: Record<string, (typeof data.keys)[0]> = {};
- for (const k of data.keys) {
- map[k.keyId] = k;
- }
- apiKeyStatus = map;
- } catch {
- // ignore
- }
-}
-
-async function saveApiKey(): Promise<void> {
- if (!showKeyModal || !keyModalValue.trim()) return;
- keyModalSaving = true;
- keyModalError = null;
- try {
- const res = await fetch(`${apiBase}/models/set-api-key`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ keyId: showKeyModal, apiKey: keyModalValue.trim() }),
- });
- const data = (await res.json()) as { success?: boolean; error?: string };
- if (!res.ok || !data.success) {
- keyModalError = data.error ?? "Failed to save";
- } else {
- showKeyModal = null;
- keyModalValue = "";
- await loadApiKeyStatus();
- }
- } catch (e) {
- keyModalError = e instanceof Error ? e.message : "Network error";
- } finally {
- keyModalSaving = false;
- }
-}
-
-async function removeKey(keyId: string): Promise<void> {
- if (!confirm(`Remove key "${keyId}" from config?`)) return;
- removingKey = keyId;
- try {
- const res = await fetch(`${apiBase}/models/remove-key`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ id: keyId }),
- });
- const data = (await res.json()) as { success?: boolean; error?: string };
- if (res.ok && data.success) {
- await new Promise((r) => setTimeout(r, 500));
- window.location.reload();
- }
- } catch {
- // ignore
- } finally {
- removingKey = null;
- }
-}
-
-$effect(() => {
- void loadCredentialStatus();
- void loadApiKeyStatus();
-});
-
-function timeAgo(ts: number | null): string {
- if (ts === null) return "";
- const diffMs = Date.now() - ts;
- const diffSec = Math.floor(diffMs / 1000);
- if (diffSec < 60) return `${diffSec}s ago`;
- const diffMin = Math.floor(diffSec / 60);
- if (diffMin < 60) return `${diffMin}m ago`;
- const diffHr = Math.floor(diffMin / 60);
- return `${diffHr}h ago`;
-}
-
-function truncate(str: string | null, max: number): string {
- if (!str) return "";
- return str.length > max ? `${str.slice(0, max)}...` : str;
-}
-</script>
-
-<div class="flex flex-col gap-3">
- {#if keys.length === 0}
- <p class="text-xs text-base-content/50">
- No models configured. Using environment defaults.
- </p>
- {:else}
- <!-- Overall status -->
- {#if allActive}
- <div class="flex items-center gap-1.5">
- <span class="badge badge-success badge-xs">●</span>
- <span class="text-xs text-success">All keys available</span>
- </div>
- {:else if allExhausted}
- <div class="flex items-center gap-1.5">
- <span class="badge badge-error badge-xs">●</span>
- <span class="text-xs text-error">All keys exhausted — waiting for refresh</span>
- </div>
- {:else if someExhausted}
- <div class="flex items-center gap-1.5">
- <span class="badge badge-warning badge-xs">●</span>
- <span class="text-xs text-warning">
- Fallback active ({activeKeys}/{totalKeys} keys available)
- </span>
- </div>
- {/if}
-
- <!-- Current model -->
- {#if currentModel}
- <div class="flex flex-col gap-0.5">
- <p class="text-xs text-base-content/50 uppercase tracking-wide">Current Model</p>
- <p class="text-sm font-mono font-semibold text-primary">{currentModel}</p>
- </div>
- {/if}
-
- <!-- Import error/success banners -->
- {#if importError}
- <div role="alert" class="text-xs text-error/80">{importError}</div>
- {/if}
- {#if importSuccess}
- <div class="text-xs text-success/80">Imported credentials for {importSuccess}</div>
- {/if}
-
- <!-- Keys -->
- {#if keys.length > 0}
- <div class="flex flex-col gap-1">
- <p class="text-xs text-base-content/50 uppercase tracking-wide">API Keys</p>
- <ul class="flex flex-col gap-1">
- {#each keys as key (key.id)}
- {@const cred = credentialStatus[key.id]}
- <li class="flex flex-col gap-0.5 rounded p-1 hover:bg-base-200 transition-colors">
- <div class="flex items-center gap-1.5">
- <span
- class="badge badge-xs {key.status === 'active'
- ? 'badge-success'
- : 'badge-error'}"
- >
- {key.status}
- </span>
- <span class="text-xs font-mono flex-1">{key.id}</span>
- <span class="text-xs text-base-content/40">{key.provider}</span>
- <button
- type="button"
- class="btn btn-xs btn-ghost btn-square text-base-content/30 hover:text-error"
- disabled={removingKey === key.id}
- onclick={() => removeKey(key.id)}
- title="Remove key"
- >
- {#if removingKey === key.id}
- <span class="loading loading-spinner loading-xs"></span>
- {:else}
- ✕
- {/if}
- </button>
- </div>
- {#if key.status === "exhausted"}
- <div class="pl-2 flex flex-col gap-0.5">
- {#if key.lastError}
- <p class="text-xs text-error/70 line-clamp-1">
- {truncate(key.lastError, 80)}
- </p>
- {/if}
- {#if key.exhaustedAt !== null}
- <p class="text-xs text-base-content/40">{timeAgo(key.exhaustedAt)}</p>
- {/if}
- </div>
- {/if}
- <!-- Credential import for anthropic keys -->
- {#if key.provider === "anthropic"}
- <div class="pl-2 flex items-center gap-1.5 mt-0.5">
- {#if cred}
- <span class="badge badge-xs {cred.expired ? 'badge-warning' : 'badge-info'}">
- {cred.expired ? "expired" : "imported"}
- </span>
- {#if cred.subscriptionType}
- <span class="text-xs text-base-content/40">{cred.subscriptionType}</span>
- {/if}
- {/if}
- <button
- type="button"
- class="btn btn-xs btn-ghost btn-outline"
- disabled={importingKey === key.id}
- onclick={() => importCredentials(key.id)}
- >
- {#if importingKey === key.id}
- <span class="loading loading-spinner loading-xs"></span>
- {:else}
- {cred ? "Re-import" : "Import Credentials"}
- {/if}
- </button>
- </div>
- {/if}
- <!-- API key import for env-based keys (opencode, copilot, etc) -->
- {#if key.provider !== "anthropic"}
- <div class="pl-2 flex items-center gap-1.5 mt-0.5">
- {#if apiKeyStatus[key.id]}
- <span class="badge badge-xs badge-info">imported</span>
- {/if}
- <button
- type="button"
- class="btn btn-xs btn-ghost btn-outline"
- onclick={() => { showKeyModal = key.id; keyModalValue = ""; keyModalError = null; }}
- >
- {apiKeyStatus[key.id] ? "Update Key" : "Import Key"}
- </button>
- </div>
- {/if}
- </li>
- {/each}
- </ul>
- </div>
- {/if}
- {/if}
- <button
- type="button"
- class="btn btn-sm btn-primary btn-outline w-full mt-2"
- onclick={onAddKey}
- >
- + Add New Key
- </button>
-<!-- Import Key Modal -->
-{#if showKeyModal}
- <div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50" role="dialog">
- <div class="bg-base-100 rounded-lg p-4 w-80 flex flex-col gap-3 shadow-xl">
- <h3 class="text-sm font-semibold">Import Key: {showKeyModal}</h3>
- <input
- type="password"
- class="input input-bordered input-sm w-full"
- placeholder="Paste API key..."
- bind:value={keyModalValue}
- />
- {#if keyModalError}
- <p class="text-xs text-error">{keyModalError}</p>
- {/if}
- <div class="flex justify-end gap-2">
- <button
- type="button"
- class="btn btn-sm btn-ghost"
- onclick={() => { showKeyModal = null; keyModalValue = ""; keyModalError = null; }}
- >
- Cancel
- </button>
- <button
- type="button"
- class="btn btn-sm btn-primary"
- disabled={!keyModalValue.trim() || keyModalSaving}
- onclick={saveApiKey}
- >
- {#if keyModalSaving}
- <span class="loading loading-spinner loading-xs"></span>
- {:else}
- Save
- {/if}
- </button>
- </div>
- </div>
- </div>
-{/if}
-
-</div>