diff options
| author | Adam Malczewski <[email protected]> | 2026-05-20 15:04:26 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-05-20 15:04:26 +0900 |
| commit | adc8bd185b54935e7a31aae04da3175b7989927a (patch) | |
| tree | 031fa53009a4708ce0cc15c0e3dcb2f2a73cf2d9 /packages/frontend/src/lib/components/ModelStatus.svelte | |
| parent | 52af4ca33b1f83b8f863a6267d447944f55e729d (diff) | |
| download | dispatch-adc8bd185b54935e7a31aae04da3175b7989927a.tar.gz dispatch-adc8bd185b54935e7a31aae04da3175b7989927a.zip | |
feat: phase 3 — config, skills, model groups, task list, and sidebar UI
- Config system: TOML-based dispatch.toml with hot-reload via chokidar
- Model/key resolution: tag-based model selection, key fallback chains
- Skills system: directory loader with TOML frontmatter, agent mappings
- Task list tool: add/update/list/get operations with WebSocket events
- API routes: GET /config, /skills, /skills/:name, /models, /models/resolve
- Frontend: sidebar with model status, task list, config viewer, skills browser, permission log
- Sliding sidebar animation using CSS transitions (not Svelte transitions)
Diffstat (limited to 'packages/frontend/src/lib/components/ModelStatus.svelte')
| -rw-r--r-- | packages/frontend/src/lib/components/ModelStatus.svelte | 135 |
1 files changed, 135 insertions, 0 deletions
diff --git a/packages/frontend/src/lib/components/ModelStatus.svelte b/packages/frontend/src/lib/components/ModelStatus.svelte new file mode 100644 index 0000000..34a0563 --- /dev/null +++ b/packages/frontend/src/lib/components/ModelStatus.svelte @@ -0,0 +1,135 @@ +<script lang="ts"> + interface KeyInfo { + id: string; + provider: string; + status: "active" | "exhausted"; + lastError: string | null; + exhaustedAt: number | null; + } + + interface ModelInfo { + id: string; + provider: string; + tags: string[]; + } + + const { + models = [], + keys = [], + tags = [], + currentModel, + }: { + models?: ModelInfo[]; + keys?: KeyInfo[]; + tags?: string[]; + currentModel?: string; + } = $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); + + const uniqueTags = $derived([...new Set(tags)]); + + 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 models.length === 0 && 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} + + <!-- Tags --> + {#if uniqueTags.length > 0} + <div class="flex flex-col gap-1"> + <p class="text-xs text-base-content/50 uppercase tracking-wide">Tags</p> + <div class="flex flex-wrap gap-1"> + {#each uniqueTags as tag (tag)} + <span class="badge badge-outline badge-xs">{tag}</span> + {/each} + </div> + </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)} + <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">{key.id}</span> + <span class="text-xs text-base-content/40">{key.provider}</span> + </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} + </li> + {/each} + </ul> + </div> + {/if} + {/if} +</div> |
