From a211f88ad4ab7ec0f175ef2fa5055d4365022dfb Mon Sep 17 00:00:00 2001 From: iamdoubz <> Date: Tue, 7 Jul 2026 07:59:57 -0500 Subject: [PATCH] feat(ui): active-provider indicator + per-use quick switch in SummaryPanel (T10.3/M3.3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a provider row above Summary (dropdown mirroring Settings' AI options + a local/hosted badge sourced from llm_status) so it's always visible which provider a summary/tag generation will actually use. Generate/Regenerate and the quick switch itself route hosted selections (Anthropic, or "custom" once its endpoint resolves off-network) through the same HostedAiBanner one-time acknowledgment gate as Settings — the generate-click gate is the one that actually matters, since selecting a provider alone sends nothing; it's kept even though the quick-switch already pre-empts Anthropic specifically. Also drops a stale svelte-ignore comment (pre-existing, unrelated to this change — eslint-plugin-svelte no longer flags that element) that was failing `eslint .` for this file. --- src/lib/views/SummaryPanel.svelte | 156 ++++++++++++++++++++++++++++-- 1 file changed, 150 insertions(+), 6 deletions(-) diff --git a/src/lib/views/SummaryPanel.svelte b/src/lib/views/SummaryPanel.svelte index 9d71653..daeed28 100644 --- a/src/lib/views/SummaryPanel.svelte +++ b/src/lib/views/SummaryPanel.svelte @@ -13,6 +13,7 @@ } from "../api"; import { renderMarkdown } from "../markdown"; import TagChip from "../components/TagChip.svelte"; + import HostedAiBanner from "../components/HostedAiBanner.svelte"; import { Tags, Sparkles, @@ -22,6 +23,8 @@ Mic2, Bell, UploadCloud, + Cpu, + Globe, } from "@lucide/svelte"; onMount(() => calendar.load()); @@ -48,19 +51,93 @@ // ---- Summary + action items (T5.4/T5.5/T5.6, FR-LLM-2/3/4) ---- let llmStatus = $state(null); - onMount(async () => { + async function refreshLlmStatus() { try { llmStatus = await api.llmStatus(); } catch { llmStatus = null; } - }); + } + onMount(refreshLlmStatus); function generateSummary() { const m = meetings.selected; if (m) meetings.generateSummary(m.id); } + // ---- Active provider indicator + per-use quick switch (T10.3/M3.3) ---- + // Same four providers Settings' AI section offers; switching here reuses + // the exact same set_llm_provider command (and hence the same "anthropic's + // endpoint is fixed" / "switching away resets a stale hosted endpoint" + // guarantees — see apply_llm_provider_args in commands.rs). + const PROVIDER_LABELS: Record = { + off: "Off", + ollama: "Ollama", + custom: "Custom", + anthropic: "Anthropic (Claude)", + }; + function isHostedStatus(status: LlmStatus | null): boolean { + return !!status && status.provider !== "off" && !status.isLocal; + } + let switchingProvider = $state(false); + + // ---- Hosted-AI "leaves your device" one-time gate (T10.3/M3.3) — shared + // by the quick switch (switching TO Anthropic) and Generate/Regenerate + // (the actual point a transcript would leave the device, so this is the + // gate that matters even if the quick switch's own check is skipped, e.g. + // "custom" resolving to a hosted endpoint that was configured elsewhere). + let showHostedBanner = $state(false); + let hostedBannerLabel = $state("this hosted provider"); + let pendingHostedAction = $state<(() => void) | null>(null); + function requireHostedAck(providerLabel: string, action: () => void) { + if (!settings.settings.hosted_ai_acknowledged) { + hostedBannerLabel = providerLabel; + pendingHostedAction = action; + showHostedBanner = true; + return; + } + action(); + } + async function acceptHostedBanner() { + await settings.acknowledgeHostedAi(); + showHostedBanner = false; + const action = pendingHostedAction; + pendingHostedAction = null; + action?.(); + } + function cancelHostedBanner() { + showHostedBanner = false; + pendingHostedAction = null; + } + + async function switchProvider(provider: string) { + switchingProvider = true; + try { + await settings.setLlmProvider({ provider }); + await refreshLlmStatus(); + } finally { + switchingProvider = false; + } + } + function onProviderChange(e: Event) { + const next = (e.target as HTMLSelectElement).value; + if (next === "anthropic") { + // Deterministically hosted — worth confirming before even switching to + // it, not just before the next generate. + requireHostedAck(PROVIDER_LABELS[next], () => void switchProvider(next)); + } else { + void switchProvider(next); + } + } + function onGenerateClick() { + const label = llmStatus ? (PROVIDER_LABELS[llmStatus.provider] ?? llmStatus.provider) : ""; + if (isHostedStatus(llmStatus)) { + requireHostedAck(label, generateSummary); + } else { + generateSummary(); + } + } + // Editable copy so toggling a checkbox doesn't persist until "Save" is // pressed; recomputes whenever a different meeting (or a freshly generated // summary) comes in. bind:checked mutates each item in place, which a plain @@ -256,7 +333,8 @@ {#if meetings.selected?.recorded}

{#if audioSrc} - +