// 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 (
);
}
// ── 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 (
);
}
// ── 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,
});