| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import React, { useEffect, useState, useCallback } from 'react'; |
| import { createRoot } from 'react-dom/client'; |
| import { motion, AnimatePresence } from 'framer-motion'; |
|
|
| |
| type Mode = 'minimal' | 'normal' | 'advanced'; |
| type WsStatus = 'connected' | 'reconnecting' | 'offline'; |
|
|
| interface State { |
| enabled: boolean; |
| mode: Mode; |
| wsStatus: WsStatus; |
| totalAnalyzed: number; |
| totalFlagged: number; |
| } |
|
|
| |
| function loadState(): Promise<State> { |
| return new Promise((resolve) => { |
| const defaults: State = { |
| enabled: true, mode: 'normal', wsStatus: 'offline', |
| totalAnalyzed: 0, totalFlagged: 0, |
| }; |
| if (typeof chrome === 'undefined' || !chrome.storage) { |
| resolve(defaults); |
| return; |
| } |
| chrome.storage.sync.get(['fact-engine-store'], (data) => { |
| try { |
| const stored = JSON.parse(data['fact-engine-store'] || '{}').state || {}; |
| resolve({ ...defaults, ...stored }); |
| } catch { |
| resolve(defaults); |
| } |
| }); |
| }); |
| } |
|
|
| function savePartial(patch: Partial<State>) { |
| if (typeof chrome === 'undefined' || !chrome.storage) return; |
| chrome.storage.sync.get(['fact-engine-store'], (data) => { |
| try { |
| const existing = JSON.parse(data['fact-engine-store'] || '{"state":{}}'); |
| existing.state = { ...existing.state, ...patch }; |
| chrome.storage.sync.set({ 'fact-engine-store': JSON.stringify(existing) }); |
| } catch {} |
| }); |
| } |
|
|
| |
| const MODE_META: Record<Mode, { label: string; desc: string; color: string }> = { |
| minimal: { label: 'Minimal', desc: 'Red & purple only', color: '#f87171' }, |
| normal: { label: 'Normal', desc: 'Recommended', color: '#60a5fa' }, |
| advanced: { label: 'Advanced', desc: 'Full factual landscape', color: '#a78bfa' }, |
| }; |
|
|
| const STATUS_META: Record<WsStatus, { label: string; color: string }> = { |
| connected: { label: 'Live', color: '#22c55e' }, |
| reconnecting: { label: 'Reconnecting', color: '#eab308' }, |
| offline: { label: 'Offline', color: '#6b7280' }, |
| }; |
|
|
| |
| function Popup() { |
| const [state, setState] = useState<State>({ |
| enabled: true, mode: 'normal', wsStatus: 'offline', |
| totalAnalyzed: 0, totalFlagged: 0, |
| }); |
| const [loading, setLoading] = useState(true); |
| const [justToggled, setJustToggled] = useState(false); |
|
|
| useEffect(() => { |
| loadState().then((s) => { setState(s); setLoading(false); }); |
| |
| const poll = setInterval(() => { |
| chrome.runtime?.sendMessage?.({ type: 'get_ws_status' }, (res) => { |
| if (res?.status) setState(prev => ({ ...prev, wsStatus: res.status })); |
| }); |
| }, 2000); |
| |
| chrome.storage?.onChanged?.addListener((changes) => { |
| if (changes['fact-engine-store']) { |
| try { |
| const s = JSON.parse(changes['fact-engine-store'].newValue || '{}').state || {}; |
| setState(prev => ({ ...prev, ...s })); |
| } catch {} |
| } |
| }); |
| return () => clearInterval(poll); |
| }, []); |
|
|
| const toggle = useCallback(() => { |
| setJustToggled(true); |
| setTimeout(() => setJustToggled(false), 600); |
| const next = !state.enabled; |
| setState(prev => ({ ...prev, enabled: next })); |
| savePartial({ enabled: next }); |
| }, [state.enabled]); |
|
|
| const setMode = useCallback((m: Mode) => { |
| setState(prev => ({ ...prev, mode: m })); |
| savePartial({ mode: m }); |
| }, []); |
|
|
| const reconnect = useCallback(() => { |
| chrome.runtime?.sendMessage?.({ type: 'reconnect' }); |
| }, []); |
|
|
| const statusMeta = STATUS_META[state.wsStatus]; |
| const modeModes: Mode[] = ['minimal', 'normal', 'advanced']; |
|
|
| if (loading) { |
| return ( |
| <div style={styles.root}> |
| <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}> |
| <motion.div |
| animate={{ rotate: 360 }} |
| transition={{ duration: 1, repeat: Infinity, ease: 'linear' }} |
| style={{ width: 24, height: 24, border: '2px solid #334155', borderTopColor: '#60a5fa', borderRadius: '50%' }} |
| /> |
| </div> |
| </div> |
| ); |
| } |
|
|
| return ( |
| <div style={styles.root}> |
| {/* Animated background glow */} |
| <div style={{ |
| position: 'absolute', inset: 0, overflow: 'hidden', pointerEvents: 'none', |
| borderRadius: 12, |
| }}> |
| <motion.div |
| animate={{ |
| x: [0, 30, -20, 0], |
| y: [0, -20, 30, 0], |
| }} |
| transition={{ duration: 8, repeat: Infinity, ease: 'easeInOut' }} |
| style={{ |
| position: 'absolute', top: -80, left: -60, width: 220, height: 220, |
| background: state.enabled |
| ? 'radial-gradient(circle, rgba(96,165,250,0.08) 0%, transparent 70%)' |
| : 'radial-gradient(circle, rgba(75,85,99,0.06) 0%, transparent 70%)', |
| borderRadius: '50%', |
| transition: 'background 600ms', |
| }} |
| /> |
| <motion.div |
| animate={{ x: [0, -40, 20, 0], y: [0, 20, -30, 0] }} |
| transition={{ duration: 10, repeat: Infinity, ease: 'easeInOut', delay: 2 }} |
| style={{ |
| position: 'absolute', bottom: -60, right: -40, width: 180, height: 180, |
| background: state.enabled |
| ? 'radial-gradient(circle, rgba(168,85,247,0.07) 0%, transparent 70%)' |
| : 'transparent', |
| borderRadius: '50%', |
| transition: 'background 600ms', |
| }} |
| /> |
| </div> |
| |
| <div style={{ position: 'relative', zIndex: 1, padding: '20px 20px 16px' }}> |
| {/* Header */} |
| <div style={styles.header}> |
| <div> |
| <div style={styles.logo}>β‘ FACT ENGINE</div> |
| <div style={styles.tagline}>Omnichannel Intelligence</div> |
| </div> |
| {/* Connection badge */} |
| <motion.button |
| onClick={reconnect} |
| whileHover={{ scale: 1.05 }} |
| whileTap={{ scale: 0.95 }} |
| style={{ |
| display: 'flex', alignItems: 'center', gap: 6, |
| background: `${statusMeta.color}15`, |
| border: `1px solid ${statusMeta.color}40`, |
| borderRadius: 20, padding: '4px 10px', |
| cursor: 'pointer', color: statusMeta.color, |
| fontSize: 11, fontWeight: 600, letterSpacing: '0.05em', |
| }} |
| > |
| <motion.div |
| animate={state.wsStatus === 'connected' |
| ? { scale: [1, 1.3, 1], opacity: [1, 0.6, 1] } |
| : state.wsStatus === 'reconnecting' |
| ? { rotate: 360 } |
| : {}} |
| transition={state.wsStatus === 'reconnecting' |
| ? { duration: 1.2, repeat: Infinity, ease: 'linear' } |
| : { duration: 2, repeat: Infinity }} |
| style={{ |
| width: 6, height: 6, borderRadius: '50%', |
| background: statusMeta.color, |
| }} |
| /> |
| {statusMeta.label} |
| </motion.button> |
| </div> |
| |
| {/* Divider */} |
| <div style={{ height: 1, background: 'linear-gradient(90deg, transparent, #1e293b, transparent)', margin: '14px 0' }} /> |
| |
| {/* Master toggle */} |
| <div style={styles.toggleRow}> |
| <div> |
| <div style={{ color: '#e2e8f0', fontWeight: 600, fontSize: 14 }}> |
| Analysis {state.enabled ? 'Active' : 'Paused'} |
| </div> |
| <div style={{ color: '#475569', fontSize: 11, marginTop: 2 }}> |
| {state.enabled ? 'Analyzing text in real-time' : 'Click to resume analysis'} |
| </div> |
| </div> |
| <ToggleSwitch on={state.enabled} onToggle={toggle} pulsing={justToggled} /> |
| </div> |
| |
| {/* Mode selector */} |
| <AnimatePresence> |
| {state.enabled && ( |
| <motion.div |
| initial={{ opacity: 0, height: 0, marginTop: 0 }} |
| animate={{ opacity: 1, height: 'auto', marginTop: 16 }} |
| exit={{ opacity: 0, height: 0, marginTop: 0 }} |
| transition={{ duration: 0.25, ease: 'easeInOut' }} |
| style={{ overflow: 'hidden' }} |
| > |
| <div style={{ color: '#64748b', fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 8 }}> |
| Highlight Mode |
| </div> |
| <div style={styles.modeGrid}> |
| {modeModes.map((m) => { |
| const meta = MODE_META[m]; |
| const active = state.mode === m; |
| return ( |
| <motion.button |
| key={m} |
| onClick={() => setMode(m)} |
| whileHover={{ y: -1 }} |
| whileTap={{ scale: 0.97 }} |
| style={{ |
| position: 'relative', overflow: 'hidden', |
| background: active ? `${meta.color}12` : '#0f172a', |
| border: `1px solid ${active ? meta.color : '#1e293b'}`, |
| borderRadius: 8, padding: '8px 6px 7px', |
| cursor: 'pointer', textAlign: 'center', |
| transition: 'border-color 200ms, background 200ms', |
| }} |
| > |
| {active && ( |
| <motion.div |
| layoutId="mode-indicator" |
| style={{ |
| position: 'absolute', inset: 0, borderRadius: 7, |
| background: `${meta.color}08`, |
| }} |
| transition={{ duration: 0.2, ease: 'easeInOut' }} |
| /> |
| )} |
| <div style={{ |
| color: active ? meta.color : '#475569', |
| fontWeight: 700, fontSize: 11, |
| transition: 'color 200ms', |
| }}> |
| {meta.label} |
| </div> |
| <div style={{ |
| color: active ? `${meta.color}99` : '#334155', |
| fontSize: 9.5, marginTop: 3, lineHeight: 1.3, |
| transition: 'color 200ms', |
| }}> |
| {meta.desc} |
| </div> |
| </motion.button> |
| ); |
| })} |
| </div> |
| </motion.div> |
| )} |
| </AnimatePresence> |
| |
| {/* Stats */} |
| <motion.div |
| style={styles.statsRow} |
| initial={false} |
| animate={{ marginTop: state.enabled ? 16 : 20 }} |
| transition={{ duration: 0.25 }} |
| > |
| <StatCard |
| value={state.totalAnalyzed} |
| label="Analyzed" |
| color="#60a5fa" |
| /> |
| <div style={{ width: 1, background: '#1e293b', alignSelf: 'stretch' }} /> |
| <StatCard |
| value={state.totalFlagged} |
| label="Flagged" |
| color="#f87171" |
| highlight={state.totalFlagged > 0} |
| /> |
| <div style={{ width: 1, background: '#1e293b', alignSelf: 'stretch' }} /> |
| <StatCard |
| value={state.totalAnalyzed > 0 ? Math.round((state.totalFlagged / state.totalAnalyzed) * 100) : 0} |
| label="Flag rate" |
| color="#a78bfa" |
| suffix="%" |
| /> |
| </motion.div> |
| |
| {/* Color legend */} |
| <AnimatePresence> |
| {state.enabled && ( |
| <motion.div |
| initial={{ opacity: 0, y: 6 }} |
| animate={{ opacity: 1, y: 0 }} |
| exit={{ opacity: 0, y: 4 }} |
| transition={{ duration: 0.3, delay: 0.1 }} |
| style={{ marginTop: 16 }} |
| > |
| <div style={{ color: '#334155', fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 8 }}> |
| Color Key |
| </div> |
| <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '5px 8px' }}> |
| {[ |
| { color: '#22c55e', label: 'Verified fact' }, |
| { color: '#eab308', label: 'Unverified' }, |
| { color: '#ef4444', label: 'Misleading' }, |
| { color: '#a855f7', label: 'AI hallucination' }, |
| ].map(({ color, label }) => ( |
| <div key={label} style={{ display: 'flex', alignItems: 'center', gap: 6 }}> |
| <div style={{ width: 8, height: 8, borderRadius: 2, background: color, flexShrink: 0 }} /> |
| <span style={{ color: '#475569', fontSize: 11 }}>{label}</span> |
| </div> |
| ))} |
| </div> |
| </motion.div> |
| )} |
| </AnimatePresence> |
| |
| {/* Footer */} |
| <div style={styles.footer}> |
| <span>v1.0.0</span> |
| <span>β’</span> |
| <span>Powered by Groq + BGE-M3</span> |
| </div> |
| </div> |
| </div> |
| ); |
| } |
|
|
| |
| function ToggleSwitch({ on, onToggle, pulsing }: { on: boolean; onToggle: () => void; pulsing: boolean }) { |
| return ( |
| <motion.button |
| onClick={onToggle} |
| style={{ |
| position: 'relative', width: 48, height: 26, |
| borderRadius: 13, border: 'none', cursor: 'pointer', flexShrink: 0, |
| background: on ? 'linear-gradient(135deg, #22c55e, #16a34a)' : '#1e293b', |
| boxShadow: on ? '0 0 12px rgba(34,197,94,0.3)' : 'none', |
| transition: 'background 300ms, box-shadow 300ms', |
| outline: 'none', |
| }} |
| animate={pulsing && on ? { |
| boxShadow: ['0 0 12px rgba(34,197,94,0.3)', '0 0 24px rgba(34,197,94,0.6)', '0 0 12px rgba(34,197,94,0.3)'], |
| } : {}} |
| transition={{ duration: 0.5 }} |
| > |
| <motion.div |
| layout |
| animate={{ x: on ? 24 : 2 }} |
| transition={{ type: 'spring', stiffness: 600, damping: 35 }} |
| style={{ |
| position: 'absolute', top: 3, width: 20, height: 20, |
| borderRadius: '50%', background: 'white', |
| boxShadow: '0 1px 4px rgba(0,0,0,0.3)', |
| }} |
| /> |
| </motion.button> |
| ); |
| } |
|
|
| function StatCard({ value, label, color, highlight = false, suffix = '' }: { |
| value: number; label: string; color: string; |
| highlight?: boolean; suffix?: string; |
| }) { |
| return ( |
| <div style={{ flex: 1, textAlign: 'center', padding: '8px 4px' }}> |
| <motion.div |
| key={value} |
| initial={{ scale: 1.15, opacity: 0.7 }} |
| animate={{ scale: 1, opacity: 1 }} |
| transition={{ duration: 0.3, type: 'spring', stiffness: 400 }} |
| style={{ |
| color: highlight && value > 0 ? color : '#e2e8f0', |
| fontSize: 20, fontWeight: 700, |
| fontVariantNumeric: 'tabular-nums', |
| fontFamily: "'DM Mono', monospace", |
| }} |
| > |
| {value.toLocaleString()}{suffix} |
| </motion.div> |
| <div style={{ color: '#475569', fontSize: 10, marginTop: 2 }}>{label}</div> |
| </div> |
| ); |
| } |
|
|
| |
| const styles = { |
| root: { |
| width: 340, |
| minHeight: 420, |
| background: 'linear-gradient(160deg, #0a0a14 0%, #0d0d1a 50%, #0a0c14 100%)', |
| borderRadius: 12, |
| overflow: 'hidden', |
| position: 'relative' as const, |
| fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, sans-serif", |
| WebkitFontSmoothing: 'antialiased', |
| }, |
| header: { |
| display: 'flex', |
| alignItems: 'center', |
| justifyContent: 'space-between', |
| }, |
| logo: { |
| fontSize: 14, fontWeight: 800, letterSpacing: '0.15em', color: '#f1f5f9', |
| textTransform: 'uppercase' as const, |
| }, |
| tagline: { |
| fontSize: 10, color: '#475569', letterSpacing: '0.08em', |
| textTransform: 'uppercase' as const, marginTop: 1, |
| }, |
| toggleRow: { |
| display: 'flex', alignItems: 'center', justifyContent: 'space-between', |
| gap: 12, |
| }, |
| modeGrid: { |
| display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 6, |
| }, |
| statsRow: { |
| display: 'flex', alignItems: 'stretch', |
| background: '#0f172a', border: '1px solid #1e293b', |
| borderRadius: 10, overflow: 'hidden', |
| }, |
| footer: { |
| display: 'flex', gap: 6, marginTop: 16, |
| justifyContent: 'center', |
| color: '#1e293b', fontSize: 10, |
| }, |
| }; |
|
|
| |
| const root = document.getElementById('root'); |
| if (root) { |
| |
| const link = document.createElement('link'); |
| link.rel = 'stylesheet'; |
| link.href = 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=DM+Mono:wght@400;500&display=swap'; |
| document.head.appendChild(link); |
|
|
| createRoot(root).render( |
| <React.StrictMode> |
| <Popup /> |
| </React.StrictMode> |
| ); |
| } |
|
|