// 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}
))}
);
}
// 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 (
);
}
Object.assign(window, { ConsoleMockup, MiniFeed });