gng / extensionStore.ts
plexdx's picture
Upload 21 files
f589dab verified
/**
* 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<string, AnalysisResult>; // 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<string | null> => {
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<void> => {
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<void> => {
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<ExtensionState>()(
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,
}),
}
)
);