Merge branch 'worktree-agent-af5a53986b9441608' into feature_chore_bug_005

# Conflicts:
#	src-tauri/src/llm/mod.rs
#	src/lib/views/Settings.svelte
#	src/lib/views/SummaryPanel.svelte
This commit is contained in:
iamdoubz committed 2026-07-07 08:15:04 -05:00
commit e966181ab8
9 files changed
+1207 -72

No files matched your search

+130 -13
View File
@@ -6,6 +6,7 @@
import { settings } from "../stores/settings.svelte";
import { calendar } from "../stores/calendar.svelte";
import ConsentNotice from "../components/ConsentNotice.svelte";
import HostedAiBanner from "../components/HostedAiBanner.svelte";
import { open } from "@tauri-apps/plugin-dialog";
import { api, errorMessage, events } from "../api";
import type { BackendId, SyncKind, SyncTargetConfig, SyncTargetInfo } from "../api";
@@ -28,6 +29,9 @@
Bot,
Copy,
KeyRound,
Eye,
EyeOff,
Globe,
} from "@lucide/svelte";
import {
OLLAMA_OPTIONS,
@@ -47,22 +51,57 @@
const SOURCE_URL = "https://git.dou.bet/iamdoubz/WhispAssist";
let appInfo = $state<{ version: string; commit: string } | null>(null);
// ---- AI summary provider (T5.2, FR-LLM-1) ----
// ---- AI summary provider (T5.2/T10.1/T10.2, FR-LLM-1, ADR-0011) ----
let llmProvider = $state(settings.settings.llm_provider);
let llmEndpoint = $state(settings.settings.llm_endpoint);
let llmModel = $state(settings.settings.llm_model);
// Hosted (Anthropic) API key — never read back from the backend (it's
// never returned by llm_status/get_settings, FR-SEC-1); this is a
// write-only field that's blank unless the user is actively (re)typing
// one, and is dropped from memory the moment saveLlm() sends it.
let llmApiKey = $state("");
let showApiKey = $state(false);
$effect(() => {
// Re-sync the form when settings (re)load or are saved elsewhere.
llmProvider = settings.settings.llm_provider;
llmEndpoint = settings.settings.llm_endpoint;
llmModel = settings.settings.llm_model;
});
const HOSTED_PROVIDER_LABELS: Record<string, string> = { anthropic: "Anthropic (Claude)" };
/** True when the *currently selected* provider is hosted (leaves the
* device): Anthropic always is; "custom" is hosted only once its endpoint
* resolves off-network — mirrors the live warning already shown below. */
function isHostedProviderSelection(): boolean {
if (llmProvider === "anthropic") return true;
if (llmProvider === "custom" && llmEndpoint) return !endpointIsLocalOrLan(llmEndpoint);
return false;
}
// ---- Hosted-AI "leaves your device" one-time banner (T10.3/M3.3) ----
let showHostedBanner = $state(false);
function onSaveLlmClick() {
if (isHostedProviderSelection() && !settings.settings.hosted_ai_acknowledged) {
showHostedBanner = true;
return;
}
saveLlm();
}
async function acceptHostedBannerAndSave() {
await settings.acknowledgeHostedAi();
showHostedBanner = false;
await saveLlm();
}
async function saveLlm() {
await settings.setLlmProvider({
provider: llmProvider,
endpoint: llmEndpoint || undefined,
endpoint: llmProvider === "anthropic" ? undefined : llmEndpoint || undefined,
model: llmModel || undefined,
apiKey: llmApiKey.trim() || undefined,
});
llmApiKey = ""; // never linger in memory once sent
}
/** Client-side mirror of the backend's loopback+private-LAN check, for a live
* "leaves your network" hint while typing. */
@@ -1035,8 +1074,10 @@
<section>
<h3>AI summary provider</h3>
<p class="muted">
Summaries run on a local LLM. Point this at Ollama on this PC or another machine on your
LAN (e.g. <code>192.168.0.x</code>) — both count as local, so nothing leaves your network.
Summaries run on a local LLM by default. Point this at Ollama on this PC or another
machine on your LAN (e.g. <code>192.168.0.x</code>) — both count as local, so nothing
leaves your network. Hosted providers (Anthropic) are optional, off by default, and send
the transcript to a third party once you turn one on.
</p>
<label
>Provider
@@ -1044,32 +1085,76 @@
<option value="off">Off</option>
<option value="ollama">Ollama (local / LAN)</option>
<option value="custom">Custom (OpenAI-compatible)</option>
<option value="anthropic">Anthropic (Claude) — hosted, leaves this device</option>
</select>
</label>
{#if llmProvider !== "off"}
<div class="grid">
<label class="wide"
>Endpoint<input
bind:value={llmEndpoint}
placeholder="http://192.168.0.42:11434"
/></label
>
<label>Model<input bind:value={llmModel} placeholder="llama3.1" /></label>
{#if llmProvider !== "anthropic"}
<label class="wide"
>Endpoint<input
bind:value={llmEndpoint}
placeholder="http://192.168.0.42:11434"
/></label
>
<label>Model<input bind:value={llmModel} placeholder="llama3.1" /></label>
{:else}
<label class="wide"
>Model<input bind:value={llmModel} placeholder="claude-3-5-sonnet-latest" /></label
>
<label class="wide"
>API key
<div class="key-input">
<input
type={showApiKey ? "text" : "password"}
autocomplete="off"
bind:value={llmApiKey}
placeholder={settings.llmStatus?.provider === "anthropic"
? "•••••••••••••••• (already set — leave blank to keep it)"
: "sk-ant-…"}
/>
<button
type="button"
class="icon-toggle"
onclick={() => (showApiKey = !showApiKey)}
aria-label={showApiKey ? "Hide API key" : "Show API key"}
>
{#if showApiKey}<EyeOff size={14} aria-hidden="true" />{:else}<Eye
size={14}
aria-hidden="true"
/>{/if}
</button>
</div>
</label>
{/if}
</div>
{#if llmEndpoint && !endpointIsLocalOrLan(llmEndpoint)}
{#if llmProvider === "anthropic"}
<div class="banner hosted">
<Globe size={14} aria-hidden="true" />
Anthropic is a hosted, third-party service — this meeting's transcript leaves your device
when you generate a summary.
</div>
{:else if llmEndpoint && !endpointIsLocalOrLan(llmEndpoint)}
<div class="banner">
<AlertTriangle size={14} aria-hidden="true" />
This endpoint isn't on your machine or LAN — your transcript would leave your network.
</div>
{/if}
<div class="actions">
<button class="primary" onclick={saveLlm} disabled={settings.llmSaving}>
<button class="primary" onclick={onSaveLlmClick} disabled={settings.llmSaving}>
{settings.llmSaving ? "Saving…" : "Save"}
</button>
<button class="ghost" onclick={() => settings.loadLlmStatus()}>
<RefreshCw size={14} aria-hidden="true" /> Test connection
</button>
</div>
{#if showHostedBanner}
<HostedAiBanner
providerLabel={HOSTED_PROVIDER_LABELS[llmProvider] ?? "this hosted provider"}
onAccept={acceptHostedBannerAndSave}
onCancel={() => (showHostedBanner = false)}
/>
{/if}
{#if settings.llmStatus}
{@const s = settings.llmStatus}
<div class="status-card">
@@ -1719,6 +1804,38 @@
margin-top: 0.15rem;
color: var(--warning);
}
/* Informational (not a warning-severity) variant for "this is hosted, by
your own choice" notices — Anthropic's fixed endpoint (ADR-0011). */
.banner.hosted {
background: color-mix(in srgb, var(--accent) 12%, var(--bg));
}
.banner.hosted :global(svg) {
color: var(--accent);
}
.key-input {
display: flex;
align-items: center;
gap: 0.3rem;
}
.key-input input {
flex: 1;
min-width: 0;
}
.icon-toggle {
display: flex;
align-items: center;
justify-content: center;
background: none;
border: 1px solid var(--border);
border-radius: 5px;
padding: 0.35rem;
color: var(--muted);
cursor: pointer;
}
.icon-toggle:hover {
background: var(--bg-hover);
color: var(--fg);
}
ul.targets,
ul.models,
ul.events {