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:
commit
e966181ab8
9 files changed
+1207
-72
No files matched your search
+130
-13
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user