// Dipoli "console" — a stylized product screenshot that lives in the Variant C hero. // Single-pane view of a transaction being scored, with live-updating layer bars, // streaming activity on the right, and a verdict stamp that re-asserts on a loop. function ConsoleMockup({ scale = 1 }) { const [phase, setPhase] = React.useState(0); // 0 scanning → 1 settled const [tick, setTick] = React.useState(0); // sample rotation const samples = [ { id: 'pay_01K5Z0QXM3', amt: '4,820', ccy: 'USDT', net: 'TRON', src: 'TXo8yq…4f1m', dst: 'TGx9Bk…eKz', dec: 'BLOCK', tone: 'block', L1: 84, L2: 88, L3: 96, score: 92, latency: 42, rules: ['ip_blocklist', 'velocity_21', 'dest_new_14d', 'cluster_match'], narrative: 'Destination first seen 14 days ago on unrelated tenant. IP matches known laundering ring cluster. Escalation recommended.', features: [['baseline_dev', 0.38], ['dest_risk', 0.26], ['ip_rep', 0.22], ['time_anom', 0.14]] }, { id: 'pay_01K5Z0PN7C', amt: '12,500', ccy: 'USDT', net: 'TRON', src: 'TLv3n1…9q8a', dst: 'TMz4Xk…9Lw', dec: 'REVIEW', tone: 'review', L1: 44, L2: 62, L3: 68, score: 64, latency: 39, rules: ['amount_2x_baseline', 'new_destination'], narrative: 'Amount 2.1× client rolling baseline but source IP is whitelisted, client history is clean. Hold for human review.', features: [['amount_dev', 0.42], ['dest_new', 0.28], ['client_age', -0.12], ['ip_trust', -0.18]] }, { id: 'pay_01K5Z0NNF9', amt: '180', ccy: 'USDT', net: 'ETH', src: '0x3f…21a', dst: '0xe1…7c9', dec: 'APPROVE', tone: 'approve', L1: 4, L2: 10, L3: 8, score: 7, latency: 22, rules: [], narrative: 'Known client, known IP, in-hours, amount within baseline. All three layers agree — auto-approved.', features: [['client_age', -0.32], ['ip_trust', -0.28], ['amount_norm', -0.22], ['dest_known', -0.12]] }, ]; React.useEffect(() => { const loop = async () => { setPhase(0); setTimeout(() => setPhase(1), 900); setTimeout(() => setTick(t => t + 1), 5200); }; loop(); const iv = setInterval(loop, 5200); return () => clearInterval(iv); }, [tick]); const s = samples[tick % samples.length]; return (
{/* Window chrome */}
console.dipoli.io / tx / {s.id}
K kseniya@dipoli
{/* App topbar — mimics the real topnav */}
Overview Transactions Clients Blocklist Settings
prod · eu-west-1
{/* Body */}
{/* Left — transaction summary */}
Transactions / {s.id}
{s.amt}{s.ccy}
{s.net} · 11:42:08 UTC · inxy_main
{s.dec}
Source {s.src}
Destination {s.dst}
{/* Narrative from Model β */}
Model β · narrative LIVE
{s.narrative}
{/* Features chart */}
Model α · top contributions
{s.features.map(([name, weight]) => (
{name}
0 ? 'pos' : 'neg')} style={{ width: phase === 1 ? Math.abs(weight * 180) + 'px' : 0 }} />
0 ? 'var(--block)' : 'var(--approve)', minWidth: 42, textAlign: 'right' }}> {weight > 0 ? '+' : ''}{weight.toFixed(2)}
))}
{/* Right — score card + activity */}
Combined verdict {s.latency} ms
3-layer score
{phase === 1 ? s.score : '—'}
of 100 · {s.dec === 'BLOCK' ? 'blocked' : s.dec === 'REVIEW' ? 'held for review' : 'auto-approved'}
{[ { n: 'L1', label: 'Rules', val: s.L1, tone: '#63D4E6' }, { n: 'L2', label: 'Model α', val: s.L2, tone: '#9387FF' }, { n: 'L3', label: 'Model β', val: s.L3, tone: '#81A8FF' }, ].map((l, i) => (
{l.n} {l.label}
{phase === 1 ? l.val : 0}
))}
Live activity 28/s
); } // Small live-activity feed used inside the console mockup's right column. function MiniFeed() { const pool = [ { t: '11:42:08', id: 'pay_9KR7', amt: '4,820 USDT', dec: 'BLOCK', s: 92, tone: 'block' }, { t: '11:42:07', id: 'pay_9KR6', amt: '180 USDT', dec: 'APPROVE', s: 7, tone: 'approve' }, { t: '11:42:06', id: 'pay_9KR5', amt: '12,500 USDT', dec: 'REVIEW', s: 64, tone: 'review' }, { t: '11:42:05', id: 'pay_9KR4', amt: '340 USDT', dec: 'APPROVE', s: 11, tone: 'approve' }, { t: '11:42:04', id: 'pay_9KR3', amt: '940 USDT', dec: 'BLOCK', s: 88, tone: 'block' }, { t: '11:42:03', id: 'pay_9KR2', amt: '72 USDT', dec: 'APPROVE', s: 4, tone: 'approve' }, { t: '11:42:02', id: 'pay_9KR1', amt: '26,800 USDT', dec: 'REVIEW', s: 71, tone: 'review' }, ]; const [seed, setSeed] = React.useState(0); const [rows, setRows] = React.useState(() => pool.slice(0, 5).map((r, i) => ({ ...r, _k: i }))); React.useEffect(() => { const iv = setInterval(() => { setSeed(s => s + 1); const nxt = pool[(seed + 5) % pool.length]; setRows(prev => [{ ...nxt, _k: seed + 100 }, ...prev.slice(0, 4)]); }, 1900); return () => clearInterval(iv); }, [seed]); return (
{rows.map(r => (
{r.t}
{r.id}
{r.amt}
{r.dec}
))}
); } Object.assign(window, { ConsoleMockup, MiniFeed });