diff options
| author | Adam Malczewski <[email protected]> | 2026-05-20 20:40:35 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-05-20 20:40:35 +0900 |
| commit | 8151447758e6826a578363758a755c6cebd1c05f (patch) | |
| tree | 6afa780c28ca6e4622c1ab30238665caaad4371e /packages/frontend/src/lib/components | |
| parent | f05099d450748cc7508f8cbde4e6539db2105f6d (diff) | |
| download | dispatch-8151447758e6826a578363758a755c6cebd1c05f.tar.gz dispatch-8151447758e6826a578363758a755c6cebd1c05f.zip | |
feat: claude max oauth support with multi-account switching, reasoning effort, and dynamic model listing
Diffstat (limited to 'packages/frontend/src/lib/components')
6 files changed, 259 insertions, 5 deletions
diff --git a/packages/frontend/src/lib/components/ChatMessage.svelte b/packages/frontend/src/lib/components/ChatMessage.svelte index 387dbdc..c8b3f6e 100644 --- a/packages/frontend/src/lib/components/ChatMessage.svelte +++ b/packages/frontend/src/lib/components/ChatMessage.svelte @@ -6,8 +6,20 @@ import ToolCallDisplay from "./ToolCallDisplay.svelte"; const { message }: { message: ChatMessage } = $props(); const isUser = $derived(message.role === "user"); +const isSystem = $derived(message.role === "system"); </script> +{#if isSystem} + <div class="flex justify-center my-2"> + <div class="badge badge-ghost gap-1 text-xs opacity-60"> + {#each message.content as segment} + {#if segment.type === "text"} + {segment.text} + {/if} + {/each} + </div> + </div> +{:else} <div class="chat chat-start mb-2"> <div class="chat-bubble max-w-[80%] break-words {isUser ? 'chat-bubble-primary' : 'bg-transparent'}"> {#if message.thinking} @@ -31,3 +43,4 @@ const isUser = $derived(message.role === "user"); {/if} </div> </div> +{/if} diff --git a/packages/frontend/src/lib/components/Header.svelte b/packages/frontend/src/lib/components/Header.svelte index 5d14593..721a773 100644 --- a/packages/frontend/src/lib/components/Header.svelte +++ b/packages/frontend/src/lib/components/Header.svelte @@ -30,7 +30,7 @@ async function handleCopy() { </div> <div class="navbar-end flex items-center gap-3"> <span class="text-xs text-base-content/60 hidden sm:block"> - DeepSeek V4 Flash via OpenCode Go + {chatStore.activeModelId ?? "Default Model"} </span> <button type="button" diff --git a/packages/frontend/src/lib/components/MarkdownRenderer.svelte b/packages/frontend/src/lib/components/MarkdownRenderer.svelte index f9925c6..f73140f 100644 --- a/packages/frontend/src/lib/components/MarkdownRenderer.svelte +++ b/packages/frontend/src/lib/components/MarkdownRenderer.svelte @@ -92,9 +92,10 @@ const promise = (async () => { try { - // Vite resolves this template literal at build time into a glob - // over all matching language modules, so each is a separate chunk. - const mod = await import(`highlight.js/lib/languages/${name}`); + // Dynamic import for languages not in the hot set above. + // @vite-ignore: the variable `name` is intentionally dynamic; + // missing modules are caught by the try/catch below. + const mod = await import(/* @vite-ignore */ `highlight.js/lib/languages/${name}`); hljs.registerLanguage(name, mod.default); return true; } catch { diff --git a/packages/frontend/src/lib/components/ModelSelector.svelte b/packages/frontend/src/lib/components/ModelSelector.svelte new file mode 100644 index 0000000..b88b2e3 --- /dev/null +++ b/packages/frontend/src/lib/components/ModelSelector.svelte @@ -0,0 +1,187 @@ +<script module> + const modelCache = new Map<string, string[]>(); +</script> + +<script lang="ts"> + import type { KeyInfo } from "../types.js"; + import { config } from "../config.js"; + + // Moves an element to document.body so modals escape the sidebar's + // transform stacking context and cover the full viewport. + function portal(node: HTMLElement) { + document.body.appendChild(node); + return { + destroy() { + node.remove(); + }, + }; + } + + const { + keys = [], + activeKeyId = null, + activeModelId = null, + reasoningEffort = "max", + onKeyChange, + onModelChange, + onReasoningChange, + }: { + keys?: KeyInfo[]; + activeKeyId?: string | null; + activeModelId?: string | null; + reasoningEffort?: string; + onKeyChange: (keyId: string) => void; + onModelChange: (keyId: string, modelId: string) => void; + onReasoningChange: (effort: string) => void; + } = $props(); + + let showKeyModal = $state(false); + let showModelModal = $state(false); + let availableModels = $state<string[]>([]); + let loadingModels = $state(false); + let modelError = $state<string | null>(null); + + function selectKey(keyId: string) { + showKeyModal = false; + onKeyChange(keyId); + } + + async function openModelModal() { + if (!activeKeyId) return; + showModelModal = true; + modelError = null; + + // Check session cache + if (modelCache.has(activeKeyId)) { + availableModels = modelCache.get(activeKeyId)!; + loadingModels = false; + return; + } + + loadingModels = true; + availableModels = []; + + try { + const res = await fetch( + `${config.apiBase}/models/available?keyId=${encodeURIComponent(activeKeyId)}`, + ); + if (!res.ok) { + const data = await res.json().catch(() => ({})); + modelError = data.error ?? `Failed to fetch models (HTTP ${res.status})`; + return; + } + const data = await res.json(); + availableModels = data.models ?? []; + // Cache for session + modelCache.set(activeKeyId, availableModels); + } catch (err) { + modelError = err instanceof Error ? err.message : "Failed to fetch models"; + } finally { + loadingModels = false; + } + } + + function selectModel(model: string) { + showModelModal = false; + if (activeKeyId) { + onModelChange(activeKeyId, model); + } + } +</script> + +<div class="bg-base-200 rounded-lg p-3"> + <div class="flex items-center justify-between"> + <span class="text-sm font-medium">Key</span> + <button class="btn btn-sm btn-outline" onclick={() => (showKeyModal = true)}> + {activeKeyId ?? "Select Key"} + </button> + </div> + + <div class="flex items-center justify-between mt-2"> + <span class="text-sm font-medium">Model</span> + <button class="btn btn-sm btn-outline" onclick={openModelModal} disabled={!activeKeyId}> + {activeModelId ?? "Select Model"} + </button> + </div> + + {#if activeModelId} + <div class="flex items-center justify-between mt-2"> + <span class="text-sm font-medium">Thinking</span> + <select + class="select select-bordered select-sm" + value={reasoningEffort} + onchange={(e) => onReasoningChange(e.currentTarget.value)} + > + <option value="none">Off</option> + <option value="low">Low</option> + <option value="medium">Medium</option> + <option value="high">High</option> + <option value="max">Max</option> + </select> + </div> + {/if} +</div> + +{#if showKeyModal} + <div class="modal modal-open" use:portal> + <div class="modal-box"> + <h3 class="font-bold text-xl">Select Key</h3> + <div class="mt-4 flex flex-col gap-2"> + {#each keys as key} + <button + class="btn {key.id === activeKeyId + ? 'btn-primary' + : 'btn-ghost'} justify-start text-base" + onclick={() => selectKey(key.id)} + > + <span class="font-mono">{key.id}</span> + <span class="badge ml-auto">{key.provider}</span> + <span + class="badge {key.status === 'active' + ? 'badge-success' + : 'badge-error'}">{key.status}</span + > + </button> + {/each} + </div> + <div class="modal-action"> + <button class="btn" onclick={() => (showKeyModal = false)}>Cancel</button> + </div> + </div> + <button type="button" class="modal-backdrop" onclick={() => (showKeyModal = false)} aria-label="Close modal"></button> + </div> +{/if} + +{#if showModelModal} + <div class="modal modal-open" use:portal> + <div class="modal-box"> + <h3 class="font-bold text-xl">Select Model</h3> + {#if loadingModels} + <div class="flex justify-center py-8"> + <span class="loading loading-spinner loading-lg"></span> + </div> + {:else if modelError} + <div class="alert alert-error mt-4 text-base"> + <span>{modelError}</span> + </div> + {:else} + <div class="mt-4 flex flex-col gap-1 max-h-96 overflow-y-auto"> + {#each availableModels as model} + <button + class="btn {model === activeModelId + ? 'btn-primary' + : 'btn-ghost'} justify-start font-mono text-base" + onclick={() => selectModel(model)} + > + {model} + </button> + {/each} + </div> + {/if} + <div class="modal-action"> + <button class="btn" onclick={() => (showModelModal = false)}>Cancel</button> + </div> + </div> + <button type="button" class="modal-backdrop" onclick={() => (showModelModal = false)} aria-label="Close modal"></button> + </div> +{/if} diff --git a/packages/frontend/src/lib/components/SidebarPanel.svelte b/packages/frontend/src/lib/components/SidebarPanel.svelte new file mode 100644 index 0000000..fe92486 --- /dev/null +++ b/packages/frontend/src/lib/components/SidebarPanel.svelte @@ -0,0 +1,53 @@ +<script lang="ts"> + import ModelStatus from "./ModelStatus.svelte"; + import TaskListPanel from "./TaskListPanel.svelte"; + import ConfigPanel from "./ConfigPanel.svelte"; + import SkillsBrowser from "./SkillsBrowser.svelte"; + import PermissionLog from "./PermissionLog.svelte"; + import type { TaskItem, LogEntry, KeyInfo, ModelInfo } from "../types.js"; + + const { + models = [], + keys = [], + tags = [], + tasks = [], + permissionLog = [], + apiBase = "", + }: { + models?: ModelInfo[]; + keys?: KeyInfo[]; + tags?: string[]; + tasks?: TaskItem[]; + permissionLog?: LogEntry[]; + apiBase?: string; + } = $props(); + + let selected = $state("Tasks"); + + const options = ["Model Status", "Tasks", "Config", "Skills", "Permission Log"]; +</script> + +<div class="bg-base-200 rounded-lg p-3"> + <select + class="select select-bordered select-sm w-full" + bind:value={selected} + > + {#each options as option} + <option value={option}>{option}</option> + {/each} + </select> + + <div class="mt-2"> + {#if selected === "Model Status"} + <ModelStatus {models} {keys} {tags} /> + {:else if selected === "Tasks"} + <TaskListPanel {tasks} /> + {:else if selected === "Config"} + <ConfigPanel {apiBase} /> + {:else if selected === "Skills"} + <SkillsBrowser {apiBase} /> + {:else if selected === "Permission Log"} + <PermissionLog entries={permissionLog} /> + {/if} + </div> +</div> diff --git a/packages/frontend/src/lib/components/ToolCallDisplay.svelte b/packages/frontend/src/lib/components/ToolCallDisplay.svelte index d85372c..805de6d 100644 --- a/packages/frontend/src/lib/components/ToolCallDisplay.svelte +++ b/packages/frontend/src/lib/components/ToolCallDisplay.svelte @@ -3,7 +3,7 @@ import type { ToolCallDisplay } from "../types.js"; const { toolCall }: { toolCall: ToolCallDisplay } = $props(); -let isExpanded = $state(toolCall.isExpanded); +let isExpanded = $state(false); function toggle() { isExpanded = !isExpanded; |
