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,43 @@
|
||||
// Small, isolated persistence for AI Assistant settings — deliberately NOT
|
||||
// folded into stores/settings-store.ts tonight. That store's export/import
|
||||
// feature enumerates every field by hand; this is prototype-scope UI state
|
||||
// (docs/AI-ASSISTANT-CONCEPT.md §12's P0/P5), and migrating it into the
|
||||
// shared store belongs with whichever phase makes these settings real
|
||||
// product config rather than a local-AI test harness.
|
||||
export type AiProvider = 'local' | 'public';
|
||||
|
||||
export interface AiLocalSettings {
|
||||
provider: AiProvider | null;
|
||||
localBaseUrl: string;
|
||||
localModel: string | null;
|
||||
publicBaseUrl: string;
|
||||
publicModel: string;
|
||||
publicConsentAccepted: boolean;
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'vncmail:ai:settings';
|
||||
|
||||
export const DEFAULT_AI_SETTINGS: AiLocalSettings = {
|
||||
provider: null,
|
||||
localBaseUrl: 'http://127.0.0.1:11434',
|
||||
localModel: null,
|
||||
publicBaseUrl: 'https://openrouter.ai/api/v1',
|
||||
publicModel: '',
|
||||
publicConsentAccepted: false,
|
||||
};
|
||||
|
||||
export function loadAiSettings(): AiLocalSettings {
|
||||
if (typeof window === 'undefined') return { ...DEFAULT_AI_SETTINGS };
|
||||
try {
|
||||
const raw = window.localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return { ...DEFAULT_AI_SETTINGS };
|
||||
return { ...DEFAULT_AI_SETTINGS, ...JSON.parse(raw) };
|
||||
} catch {
|
||||
return { ...DEFAULT_AI_SETTINGS };
|
||||
}
|
||||
}
|
||||
|
||||
export function saveAiSettings(settings: AiLocalSettings): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
|
||||
}
|
||||
Reference in New Issue
Block a user