gng / popup.tsx
plexdx's picture
Upload 21 files
f589dab verified
/**
* entrypoints/popup.tsx
*
* Extension popup UI β€” 380Γ—540px premium dark glass interface.
* Features:
* - Master on/off toggle with animated glow
* - Mode selector (Minimal / Normal / Advanced) with animated indicator
* - Live WebSocket connection badge with animated status dot
* - Real-time stats counter
* - Smooth framer-motion transitions throughout
*/
import React, { useEffect, useState, useCallback } from 'react';
import { createRoot } from 'react-dom/client';
import { motion, AnimatePresence } from 'framer-motion';
// ── Types ─────────────────────────────────────────────────────────────────────
type Mode = 'minimal' | 'normal' | 'advanced';
type WsStatus = 'connected' | 'reconnecting' | 'offline';
interface State {
enabled: boolean;
mode: Mode;
wsStatus: WsStatus;
totalAnalyzed: number;
totalFlagged: number;
}
// ── Storage helpers ───────────────────────────────────────────────────────────
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 {}
});
}
// ── Color palette ─────────────────────────────────────────────────────────────
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' },
};
// ── Main popup component ──────────────────────────────────────────────────────
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); });
// Poll WS status from background
const poll = setInterval(() => {
chrome.runtime?.sendMessage?.({ type: 'get_ws_status' }, (res) => {
if (res?.status) setState(prev => ({ ...prev, wsStatus: res.status }));
});
}, 2000);
// Listen for storage changes (live sync)
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>
);
}
// ── Sub-components ────────────────────────────────────────────────────────────
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>
);
}
// ── Styles ────────────────────────────────────────────────────────────────────
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,
},
};
// ── Mount ─────────────────────────────────────────────────────────────────────
const root = document.getElementById('root');
if (root) {
// Inject Google Fonts
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>
);
}