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} - +