/** * stores/extensionStore.ts * * Zustand store with chrome.storage.sync persistence. * Cross-context state: popup ↔ background ↔ content scripts * all read from / write to chrome.storage.sync, so the store * stays in sync across the extension's execution contexts. */ import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; // ── Types ───────────────────────────────────────────────────────────────────── export type HighlightMode = 'minimal' | 'normal' | 'advanced'; export type WsStatus = 'connected' | 'reconnecting' | 'offline'; export interface AnalysisResult { claimHash: string; claimText: string; color: 'green' | 'yellow' | 'red' | 'purple'; confidence: number; // 0-100 verdict: string; explanation: string; sources: string[]; trustScore: number; cached: boolean; processingMs: number; } export interface ExtensionState { // User preferences enabled: boolean; mode: HighlightMode; // Connection wsStatus: WsStatus; backendUrl: string; clientId: string; // Results cache (in-memory, not persisted) results: Record; // keyed by claimHash // Stats totalAnalyzed: number; totalFlagged: number; // Actions setEnabled: (v: boolean) => void; setMode: (m: HighlightMode) => void; setWsStatus: (s: WsStatus) => void; setBackendUrl: (url: string) => void; addResults: (results: AnalysisResult[]) => void; clearResults: () => void; } // ── chrome.storage.sync adapter for zustand/persist ─────────────────────────── const chromeStorageAdapter = createJSONStorage(() => ({ getItem: (key: string): Promise => { return new Promise((resolve) => { if (typeof chrome === 'undefined' || !chrome.storage) { resolve(localStorage.getItem(key)); return; } chrome.storage.sync.get([key], (result) => { resolve(result[key] ?? null); }); }); }, setItem: (key: string, value: string): Promise => { return new Promise((resolve) => { if (typeof chrome === 'undefined' || !chrome.storage) { localStorage.setItem(key, value); resolve(); return; } chrome.storage.sync.set({ [key]: value }, resolve); }); }, removeItem: (key: string): Promise => { return new Promise((resolve) => { if (typeof chrome === 'undefined' || !chrome.storage) { localStorage.removeItem(key); resolve(); return; } chrome.storage.sync.remove([key], resolve); }); }, })); // ── Generate stable client ID ───────────────────────────────────────────────── function generateClientId(): string { const arr = new Uint8Array(8); crypto.getRandomValues(arr); return 'ext-' + Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join(''); } // ── Store ───────────────────────────────────────────────────────────────────── export const useExtensionStore = create()( persist( (set, get) => ({ enabled: true, mode: 'normal', wsStatus: 'offline', backendUrl: import.meta.env.VITE_WS_URL || 'ws://localhost:7860', clientId: generateClientId(), results: {}, totalAnalyzed: 0, totalFlagged: 0, setEnabled: (v) => set({ enabled: v }), setMode: (m) => set({ mode: m }), setWsStatus: (s) => set({ wsStatus: s }), setBackendUrl: (url) => set({ backendUrl: url }), addResults: (newResults) => { const { results, totalAnalyzed, totalFlagged } = get(); const updated = { ...results }; let flaggedDelta = 0; for (const r of newResults) { updated[r.claimHash] = r; if (r.color === 'red' || r.color === 'purple') flaggedDelta++; } set({ results: updated, totalAnalyzed: totalAnalyzed + newResults.length, totalFlagged: totalFlagged + flaggedDelta, }); }, clearResults: () => set({ results: {}, totalAnalyzed: 0, totalFlagged: 0 }), }), { name: 'fact-engine-store', storage: chromeStorageAdapter, // Only persist user preferences, not runtime state partialize: (state) => ({ enabled: state.enabled, mode: state.mode, backendUrl: state.backendUrl, clientId: state.clientId, totalAnalyzed: state.totalAnalyzed, totalFlagged: state.totalFlagged, }), } ) );