// Shared primitive components. const { useState, useEffect, useRef, useMemo, useCallback } = React; // ── helpers ─────────────────────────────────────────────────────────────────── function signalColor(prob){ if (prob >= 0.6) return 'red'; if (prob <= 0.4) return 'green'; return 'amber'; } function signalLabel(prob){ if (prob >= 0.6) return 'AI'; if (prob <= 0.4) return 'HUMAN'; return 'MIXED'; } function fmt3(n){ return Number(n).toFixed(3); } function pct0(n){ return Math.round(n*100) + '%'; } function fmtBytes(n){ if (n < 1024) return n + ' B'; if (n < 1024*1024) return (n/1024).toFixed(1) + ' KB'; return (n/1024/1024).toFixed(2) + ' MB'; } function commas(n){ return Number(n).toLocaleString('en-US'); } function layerMeta(key){ return window.LAYER_META.find(l=>l.key===key); } function verdictTone(score){ if (score >= 0.7) return { tone:'red', bg:'rgba(239,68,68,.08)', border:'rgba(239,68,68,.35)' }; if (score <= 0.3) return { tone:'green', bg:'rgba(16,185,129,.08)', border:'rgba(16,185,129,.35)' }; return { tone:'amber', bg:'rgba(245,158,11,.08)', border:'rgba(245,158,11,.35)' }; } // ── 9-cell AI probability bar ───────────────────────────────────────────────── function ProbBar({ value, cells = 9, animate = false, delayMs = 0 }){ const [filled, setFilled] = useState(animate ? 0 : value); useEffect(() => { if (!animate) { setFilled(value); return; } let raf, t0; const t = setTimeout(() => { const dur = 400; const tick = (now) => { if (!t0) t0 = now; const k = Math.min(1, (now - t0)/dur); const eased = 1 - Math.pow(1-k, 2.4); setFilled(value * eased); if (k < 1) raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); }, delayMs); return () => { clearTimeout(t); cancelAnimationFrame(raf); }; }, [value, animate, delayMs]); const color = signalColor(value); const filledCells = Math.round(filled * cells); return ( {Array.from({length: cells}).map((_,i) => ( ))} ); } // ── ASCII spinner ───────────────────────────────────────────────────────────── const SPINNER_FRAMES = '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'; function AsciiSpinner({ color = 'var(--blue)' }){ const [i, setI] = useState(0); useEffect(() => { const id = setInterval(() => setI(x => (x+1) % SPINNER_FRAMES.length), 80); return () => clearInterval(id); }, []); return {SPINNER_FRAMES[i]}; } // ── Signal pill ─────────────────────────────────────────────────────────────── function SignalPill({ signal }){ const map = { AI:'red', HUMAN:'green', MIXED:'amber' }; const cls = map[signal] || 'muted'; const dot = signal === 'AI' ? '🔴' : signal === 'HUMAN' ? '🟢' : '🟡'; return {dot}{signal}; } // ── Tiny inline line chart ──────────────────────────────────────────────────── function MiniLine({ values, width=440, height=120, color='var(--blue)', threshold }){ if (!values || values.length === 0) return null; const min = Math.min(...values), max = Math.max(...values); const pad = (max-min) * 0.15 + 1; const lo = min - pad, hi = max + pad; const w = width, h = height, mx = 28, my = 14; const stepX = (w - mx*2) / Math.max(1, values.length - 1); const y = v => h - my - ((v - lo)/(hi - lo)) * (h - my*2); const path = values.map((v,i) => `${i===0?'M':'L'} ${mx + i*stepX} ${y(v)}`).join(' '); const ticks = [lo, (lo+hi)/2, hi].map(t => +t.toFixed(0)); return ( {/* grid */} {ticks.map((t,i) => ( {t} ))} {threshold != null && ( )} {values.map((v,i) => ( ))} ); } // ── Tiny histogram ──────────────────────────────────────────────────────────── function MiniHistogram({ buckets, width=420, height=60, color='var(--blue)' }){ const max = Math.max(...buckets, 1); const w = width, h = height; const bw = (w - 4) / buckets.length; return ( {buckets.map((v,i) => { const bh = (v/max) * (h - 14); const t = i / (buckets.length-1); const c = t < 0.4 ? 'var(--green)' : t > 0.6 ? 'var(--red)' : 'var(--amber)'; return ; })} ); } // ── Heatmap of sentences ────────────────────────────────────────────────────── function HeatColor(p){ const paper = document.body.dataset.theme === 'paper'; // green → amber → red gradient (theme-aware) const palette = paper ? { g:[79,107,58], a:[176,124,29], r:[176,58,46] } // muted ink tones : { g:[16,185,129], a:[245,158,11], r:[239,68,68] }; if (p < 0.4){ const t = p / 0.4; const r = Math.round(palette.g[0] + (palette.a[0]-palette.g[0])*t); const g = Math.round(palette.g[1] + (palette.a[1]-palette.g[1])*t); const b = Math.round(palette.g[2] + (palette.a[2]-palette.g[2])*t); return `rgb(${r}, ${g}, ${b})`; } else { const t = (p - 0.4) / 0.6; const r = Math.round(palette.a[0] + (palette.r[0]-palette.a[0])*t); const g = Math.round(palette.a[1] + (palette.r[1]-palette.a[1])*t); const b = Math.round(palette.a[2] + (palette.r[2]-palette.a[2])*t); return `rgb(${r}, ${g}, ${b})`; } } Object.assign(window, { signalColor, signalLabel, fmt3, pct0, fmtBytes, commas, layerMeta, verdictTone, ProbBar, AsciiSpinner, SignalPill, MiniLine, MiniHistogram, HeatColor, });