feat(ai): real local Ollama chat + BYOK public provider
Decisions 2026-08-05 evening (reprioritizing docs/AI-ASSISTANT-CONCEPT.md's original P1/P2 server-first sequencing to local-first, since a real Ollama instance already runs on this Mac with a full model set): - `local` ships free, no entitlement check — always available wherever supportsLocalLlm() is true. - `public` (BYOK) is available too, explicitly unmonitored for now — no seats/metering/consent backend. This reverses the concept doc's decision #1 (server-side-only key custody): the client holds its own key, matching vncmail-native's existing pattern. - `server` (VNC-hosted) stays unwired client-side; that infra is "this MacBook tonight, the dev k8s cluster tomorrow." New: - lib/ai/local-client.ts: listLocalModels/testLocalConnection/chatLocal/ chatPublic, ported near-verbatim from vncmail-native's proven src/api/ai.ts. Direct browser-side fetch, not proxied through this app's own server — a server-side proxy would reach the *server's* loopback, not the user's own laptop, which defeats the point of "local" once this app is hosted remotely. - lib/ai/key-store.ts: client-held BYOK storage (localStorage — this repo's existing convention for client state, no OS keychain reachable from a browser tab). - lib/ai/local-settings.ts: isolated persistence for provider/model/base-URL choices. Deliberately NOT folded into stores/settings-store.ts, which has a hand-maintained export/import enumeration this prototype-scope state doesn't belong in yet. - Retrieval reuses this app's own already-built app/api/offline/search (encrypted SQLite/FTS5 mail index) as context when available, and degrades to unaugmented chat — not an error — when it 404s/503s (no index in this session, e.g. plain browser rather than Electron). Rewrote components/settings/ai-assistant-settings.tsx: provider picker, local runtime config (base URL, model list/refresh, test connection with a CORS-aware diagnostic per the concept doc's own note on the browser row), public BYOK config (base URL, model, key, client-side consent toggle), and a working Ask box. Verified: typecheck clean, lint clean, translations pass, production build succeeds. Live-tested against the real Ollama on this machine (confirmed running: qwen2.5:32b, gemma4, deepseek-r1, llama3.2, hermes3, qwen3) via a local server + demo-mode session — admin flag round-trips correctly, the pane renders both provider options, and the CORS-diagnostic path fires correctly on a real (if here environment-sandboxed, not Ollama-side) connection failure. Full success end-to-end still wants a real, unsandboxed browser tab against this Mac's loopback to close out.
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
// Client-held storage for the user's own public-provider API key (BYOK).
|
||||
//
|
||||
// Decision 2026-08-05 (reverses docs/AI-ASSISTANT-CONCEPT.md decision #1's
|
||||
// server-side-custody design): the user brings and holds their own key,
|
||||
// client-side, not VNC. This is the same custody model as
|
||||
// vncmail-native's lib/ai-key-store.ts (expo-secure-store there; this repo
|
||||
// has no OS keychain access from a browser tab, so localStorage is the
|
||||
// honest equivalent here — plain, not hidden behind a false sense of
|
||||
// "secure storage"). A fuller Paperclip-style key-management UI (multiple
|
||||
// providers, masking, rotation) is good follow-up work, not built tonight.
|
||||
const KEY_PREFIX = 'vncmail:ai:key:';
|
||||
|
||||
export function getAiApiKey(provider: 'public'): string | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
return window.localStorage.getItem(KEY_PREFIX + provider);
|
||||
}
|
||||
|
||||
export function setAiApiKey(provider: 'public', key: string): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
window.localStorage.setItem(KEY_PREFIX + provider, key);
|
||||
}
|
||||
|
||||
export function clearAiApiKey(provider: 'public'): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
window.localStorage.removeItem(KEY_PREFIX + provider);
|
||||
}
|
||||
Reference in New Issue
Block a user