/** * CUTS+ Causal Terminal — Frontend Logic * Communicates with the gr.Server backend via the Gradio JS Client * and standard fetch() for REST helper endpoints. */ // ── Gradio Client bootstrap ──────────────────────────────────────────────── // Loaded from CDN in index.html; window.GradioClient is set after import. let GR_CLIENT = null; async function initGradioClient() { try { const { Client } = await import('https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js'); GR_CLIENT = await Client.connect(window.location.origin); setBannerState('gradio', 'ok', 'GRADIO OK'); } catch (err) { console.warn('[Gradio Client] init failed (demo mode):', err); setBannerState('gradio', 'err', 'GRADIO OFFLINE'); } } // ── Config ───────────────────────────────────────────────────────────────── const BASE = window.location.origin; // same origin — gr.Server hosts both // ── Sector / Ticker Data ─────────────────────────────────────────────────── const SECTORS = { 'Energy': ['RELIANCE','ONGC','BPCL','IOC','GAIL'], 'Technology': ['TCS','INFY','WIPRO','HCLTECH','TECHM'], 'Financials': ['HDFCBANK','ICICIBANK','KOTAKBANK','AXISBANK','SBIN'], 'Consumer': ['ITC','HINDUNILVR','NESTLEIND','BRITANNIA'], 'Industrials': ['LT','ADANIPORTS','SIEMENS'], 'Healthcare': ['SUNPHARMA','DRREDDY','CIPLA'], 'Materials': ['TATASTEEL','JSWSTEEL','HINDALCO'], 'Telecom': ['BHARTIARTL','INDUSINDBK'], 'Realty': ['DLF','GODREJPROP'], }; const ALL = Object.values(SECTORS).flat(); const N = ALL.length; const TICKER_SEC = {}; for (const [s, ms] of Object.entries(SECTORS)) ms.forEach(t => TICKER_SEC[t] = s); const SEC_NAMES = Object.keys(SECTORS); const S = SEC_NAMES.length; // ── Deterministic RNG ────────────────────────────────────────────────────── function mkRng(seed) { let s = seed; return () => { s = (s * 16807) % 2147483647; return (s - 1) / 2147483646; }; } // ── φ Potentials (HHKD output — seeded defaults, overridden by API) ──────── const rA = mkRng(42); const PHI = {}; [ ['RELIANCE',2.41],['ONGC',2.18],['TCS',2.05],['BHARTIARTL',1.92],['LT',1.78], ['INFY',1.65],['HDFCBANK',1.52],['ICICIBANK',1.39],['BPCL',1.28],['GAIL',1.14], ['WIPRO',1.02],['HCLTECH',0.89],['IOC',0.76],['KOTAKBANK',0.65],['AXISBANK',0.54], ['SBIN',0.41],['ITC',0.28],['HINDUNILVR',0.15],['NESTLEIND',0.03],['TATASTEEL',-0.09], ['JSWSTEEL',-0.22],['HINDALCO',-0.35],['SUNPHARMA',-0.48],['DRREDDY',-0.61], ['CIPLA',-0.74],['SIEMENS',-0.87],['ADANIPORTS',-1.13], ['TECHM',-1.26],['BRITANNIA',-1.39],['INDUSINDBK',-1.52],['DLF',-1.65], ['GODREJPROP',-1.78], ].forEach(([t, v]) => PHI[t] = v); ALL.forEach(t => { if (PHI[t] == null) PHI[t] = -1.2 + rA() * 0.4; }); const SEC_PHI = {}; for (const [s, ms] of Object.entries(SECTORS)) SEC_PHI[s] = ms.reduce((a, t) => a + (PHI[t] || 0), 0) / ms.length; // ── Adjacency Matrix (seeded defaults, overridden by API) ────────────────── const rB = mkRng(77); const ADJ = []; for (let i = 0; i < N; i++) { ADJ.push([]); for (let j = 0; j < N; j++) { if (i === j) { ADJ[i].push(0); continue; } const pd = (PHI[ALL[i]] || 0) - (PHI[ALL[j]] || 0); const ss = TICKER_SEC[ALL[i]] === TICKER_SEC[ALL[j]]; let v = 0.04 + Math.max(0, pd) * 0.14 + (ss ? 0.09 : 0) + rB() * 0.08; if (pd > 0.8) v += 0.22; ADJ[i].push(Math.min(0.97, Math.max(0.01, v))); } } const EDGES = []; for (let i = 0; i < N; i++) for (let j = 0; j < N; j++) if (ADJ[i][j] > 0.5) EDGES.push({ si: i, ti: j, w: ADJ[i][j] }); // ── Sector Macro Adjacency ───────────────────────────────────────────────── const MADJ = Array.from({ length: S }, () => Array(S).fill(0)); for (let a = 0; a < S; a++) for (let b = 0; b < S; b++) { if (a === b) continue; MADJ[a][b] = Math.min( 0.96, Math.max(0.02, 0.28 + (SEC_PHI[SEC_NAMES[a]] - SEC_PHI[SEC_NAMES[b]]) * 0.18 + mkRng(a * 9 + b + 1)() * 0.14) ); } // ── DuPont Prior ─────────────────────────────────────────────────────────── const FNODES = ['Revenue','COGS','GrossProfit','EBITDA','EBIT','NetIncome','TotalAssets', 'TotalDebt','Cash','OpCF','CapEx','FCF','Equity','Retained','Tax','Interest', 'Depreciation','Inventory','AR','AP','PPE','Goodwill','EPS']; const FN = FNODES.length; const FPRIOR = Array.from({ length: FN }, () => Array(FN).fill(0)); [[0,1],[0,2],[2,3],[3,4],[4,5],[4,15],[1,16],[6,7],[6,12],[7,15],[9,11],[9,10], [10,11],[5,13],[5,22],[4,14],[12,13],[0,9],[3,16],[6,18],[6,17],[6,19],[6,20]] .forEach(([a, b]) => FPRIOR[a][b] = 1); // ── News Feed Data ───────────────────────────────────────────────────────── const NEWS = [ { sym:'RELIANCE', score:0.91, dir: 1, text:'RIL Jio 5G capex ₹40kCr accelerates infrastructure spend', tags:['CapEx','FCF','Revenue'] }, { sym:'HDFCBANK', score:0.84, dir:-1, text:'RBI repo hike 25bps — NIM compression expected Q2FY25', tags:['NetIncome','Interest','TotalDebt'] }, { sym:'TCS', score:0.79, dir: 1, text:'TCS Q3 deal wins ₹14kCr; US enterprise recovery signal', tags:['Revenue','NetIncome','EPS'] }, { sym:'TATASTEEL',score:0.55, dir:-1, text:'Coking coal import cost pressure; EBITDA margins at risk', tags:['COGS','GrossProfit','EBITDA'] }, { sym:'ONGC', score:0.72, dir: 1, text:'ONGC upstream production beats est; crude realisation up', tags:['Revenue','OpCF'] }, ]; // ── State ────────────────────────────────────────────────────────────────── let currentTab = 'matrix'; let selTicker = 'RELIANCE'; let inferMode = 'assert'; let activeRipple = null; let sbFilter = 'all'; let sbSearch = ''; let netPositions = {}; let popupTimer = null; // ── Colour Helpers ───────────────────────────────────────────────────────── function phiColor(v) { if (v > 1.5) return '#f0a500'; if (v > 0.5) return '#d4b840'; if (v > -0.5) return '#00b8d4'; return '#5a5a54'; } function adjColor(v) { if (v > 0.7) return `rgba(224,52,52,${0.45 + v * 0.5})`; if (v > 0.4) return `rgba(240,165,0,${0.25 + v * 0.65})`; return `rgba(0,80,40,${v * 1.8})`; } function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); } function fmtPhi(v) { return (v >= 0 ? '+' : '') + v.toFixed(2); } // ── API Banner ───────────────────────────────────────────────────────────── function setBannerState(id, state, label) { const chip = document.getElementById(`api-${id}`); if (!chip) return; chip.className = `api-chip ${state}`; const dot = chip.querySelector('.api-dot'); if (dot) dot.setAttribute('title', label); const span = chip.querySelector('span:last-child'); if (span) span.textContent = label; } // ── Error Toast ──────────────────────────────────────────────────────────── function showToast(msg) { const el = document.getElementById('error-toast'); if (!el) return; el.textContent = msg; el.classList.add('show'); setTimeout(() => el.classList.remove('show'), 3500); } // ── Clock ────────────────────────────────────────────────────────────────── setInterval(() => { const el = document.getElementById('clock'); if (el) el.textContent = new Date().toTimeString().slice(0, 8); }, 1000); setInterval(() => { const el = document.getElementById('ss-loss'); if (el) el.textContent = (0.038 + Math.random() * 0.006).toFixed(4); }, 3000); // ── API Calls ────────────────────────────────────────────────────────────── async function apiGet(path) { try { const r = await fetch(`${BASE}${path}`); if (!r.ok) throw new Error(`HTTP ${r.status}`); return await r.json(); } catch (e) { console.warn(`[API] GET ${path} failed:`, e.message); return null; } } async function apiPost(path, body) { try { const r = await fetch(`${BASE}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (!r.ok) throw new Error(`HTTP ${r.status}`); return await r.json(); } catch (e) { console.warn(`[API] POST ${path} failed:`, e.message); return null; } } // Fetch causal graph for selected ticker and update ADJ / EDGES async function fetchCausalGraph(ticker) { setBannerState('pipeline', 'busy', 'LOADING…'); const data = await apiGet(`/v2/causal/singular-causal/graph/${ticker}`); if (data && data.nodes && data.links) { // Patch ADJ from API data const apiIdxMap = {}; data.nodes.forEach((n, i) => { apiIdxMap[n.id || n.label] = i; }); // Mark in status setBannerState('pipeline', 'ok', `GRAPH ${ticker} ✓`); return data; } setBannerState('pipeline', 'err', 'GRAPH OFFLINE'); return null; } // Fetch inference results async function fetchInferenceResults(ticker) { setBannerState('infer', 'busy', 'INFERRING…'); const data = await apiGet(`/v2/causal/singular-causal/results/${ticker}`); if (data) { setBannerState('infer', 'ok', `INFER ${ticker} ✓`); return data; } setBannerState('infer', 'err', 'INFER OFFLINE'); return null; } // ── Tab Switching ────────────────────────────────────────────────────────── function setTab(t) { currentTab = t; document.querySelectorAll('.tab').forEach(b => { const label = b.dataset.tab; b.classList.toggle('active', label === t); }); document.querySelectorAll('.view').forEach(v => v.classList.remove('active')); const el = document.getElementById('view-' + t); if (el) el.classList.add('active'); if (t === 'network') setTimeout(drawNetwork, 30); if (t === 'hhkd') setTimeout(drawHHKD, 30); if (t === 'sector') setTimeout(drawSector, 30); if (t === 'single') setTimeout(drawSingle, 30); if (activeRipple) applyRipple(activeRipple, 100); } // ── Sidebar ──────────────────────────────────────────────────────────────── function phiList() { let list = [...ALL].sort((a, b) => (PHI[b] || 0) - (PHI[a] || 0)); if (sbFilter === 'up') list = list.filter(t => (PHI[t] || 0) > 0.5); if (sbFilter === 'dn') list = list.filter(t => (PHI[t] || 0) < -0.5); if (sbSearch) list = list.filter(t => t.toLowerCase().includes(sbSearch.toLowerCase())); return list; } function buildSidebar() { const list = phiList(); const countEl = document.getElementById('sb-count'); if (countEl) countEl.textContent = list.length; const maxP = Math.max(...ALL.map(t => Math.abs(PHI[t] || 0))); const container = document.getElementById('ticker-list'); if (!container) return; container.innerHTML = list.map(t => { const phi = PHI[t] || 0; const c = phiColor(phi); const w = Math.abs(phi) / maxP * 100; return `
${t}
${phi >= 0 ? '+' : ''}${phi.toFixed(1)}
`; }).join(''); } function setSeg(btn, f) { document.querySelectorAll('.seg-btn button').forEach(b => b.classList.remove('active')); btn.classList.add('active'); sbFilter = f; buildSidebar(); } function filterTickers(v) { sbSearch = v; buildSidebar(); } function selectTicker(t) { selTicker = t; const nameEl = document.getElementById('single-name'); if (nameEl) nameEl.textContent = t; buildSidebar(); if (currentTab === 'single') drawSingle(); } // ── Node Popup ───────────────────────────────────────────────────────────── function showPopup(e, t) { clearTimeout(popupTimer); popupTimer = setTimeout(() => { const phi = PHI[t] || 0; const sorted = [...ALL].sort((a, b) => (PHI[b] || 0) - (PHI[a] || 0)); const rank = sorted.indexOf(t) + 1; const idx = ALL.indexOf(t); const outDeg = EDGES.filter(e => e.si === idx).length; const inDeg = EDGES.filter(e => e.ti === idx).length; const bestCause = EDGES.filter(e => e.si === idx).sort((a, b) => b.w - a.w)[0]; const pop = document.getElementById('node-popup'); if (!pop) return; document.getElementById('np-name').textContent = t; document.getElementById('np-sector').textContent = TICKER_SEC[t] || ''; document.getElementById('np-phi').textContent = fmtPhi(phi); document.getElementById('np-rank').textContent = '#' + rank + (phi > 0.5 ? ' Upstream' : phi < -0.5 ? ' Sink' : ' Mid'); document.getElementById('np-out').textContent = outDeg; document.getElementById('np-in').textContent = inDeg; document.getElementById('np-cause').textContent = bestCause ? ALL[bestCause.ti] + ' ' + bestCause.w.toFixed(2) : '—'; document.getElementById('np-news').textContent = (0.5 + Math.abs(phi) * 0.12).toFixed(2); pop.style.display = 'block'; pop.style.left = (e.clientX + 16) + 'px'; pop.style.top = (e.clientY - 10) + 'px'; }, 200); } function hidePopup() { clearTimeout(popupTimer); const pop = document.getElementById('node-popup'); if (pop) pop.style.display = 'none'; } // ── Heatmap ──────────────────────────────────────────────────────────────── function drawHeatmap() { const svg = document.getElementById('heatmap-svg'); const body = document.getElementById('matrix-body'); if (!svg || !body) return; const CELL = 12, PAD = 60; const W = N * CELL + PAD, H = N * CELL + PAD; svg.setAttribute('width', W); svg.setAttribute('height', H); svg.setAttribute('viewBox', `0 0 ${W} ${H}`); let h = ''; ALL.forEach((t, j) => { const x = PAD + j * CELL + CELL / 2; const isSel = t === selTicker; h += `${t}`; }); ALL.forEach((t, i) => { const y = PAD + i * CELL + CELL / 2 + 3; const isSel = t === selTicker; h += `${t}`; }); ALL.forEach((src, i) => { ALL.forEach((tgt, j) => { if (i === j) { h += ``; return; } const v = ADJ[i][j]; const c = adjColor(v); const isSel = src === selTicker || tgt === selTicker; h += ``; }); }); svg.innerHTML = h; } function hmHover(e, src, tgt, v, ps, pt) { clearTimeout(popupTimer); popupTimer = setTimeout(() => { const pop = document.getElementById('node-popup'); if (!pop) return; document.getElementById('np-name').textContent = src + ' → ' + tgt; document.getElementById('np-sector').textContent = (TICKER_SEC[src] || '') + '→' + (TICKER_SEC[tgt] || ''); document.getElementById('np-phi').textContent = v.toFixed(3); document.getElementById('np-rank').textContent = (ps - pt) > 0.1 ? 'GRADIENT' : 'CYCLIC'; document.getElementById('np-out').textContent = (ps >= 0 ? '+' : '') + ps.toFixed(2); document.getElementById('np-in').textContent = (pt >= 0 ? '+' : '') + pt.toFixed(2); document.getElementById('np-cause').textContent = v > 0.5 ? 'CAUSAL EDGE' : 'WEAK'; document.getElementById('np-news').textContent = '—'; pop.style.display = 'block'; pop.style.left = (e.clientX + 12) + 'px'; pop.style.top = (e.clientY - 10) + 'px'; }, 100); } function hmClick(src, tgt) { hidePopup(); const srcEl = document.getElementById('infer-src'); const tgtEl = document.getElementById('infer-tgt'); if (srcEl) srcEl.value = src; if (tgtEl) tgtEl.value = tgt; } // ── Network ──────────────────────────────────────────────────────────────── function drawNetwork() { const svg = document.getElementById('net-svg'); if (!svg) return; const W = svg.clientWidth || 700, H = svg.clientHeight || 480; svg.setAttribute('viewBox', `0 0 ${W} ${H}`); const sorted = [...ALL].sort((a, b) => (PHI[b] || 0) - (PHI[a] || 0)); netPositions = {}; const COLS = 6; sorted.forEach((t, i) => { const col = i % COLS; const row = Math.floor(i / COLS); const rows = Math.ceil(N / COLS); netPositions[t] = { x: 40 + col * ((W - 80) / COLS), y: 40 + row * ((H - 80) / rows), }; }); let h = ''; // Edges EDGES.filter(e => e.w > 0.65).forEach(e => { const s = ALL[e.si], t = ALL[e.ti]; const sp = netPositions[s], tp = netPositions[t]; if (!sp || !tp) return; const strong = e.w > 0.8; const col = strong ? '#e03434' : '#38382e'; const sw = strong ? 1.5 : 0.7; const dash = strong ? '' : `stroke-dasharray="3 3"`; h += ``; }); // Nodes sorted.forEach(t => { const p = netPositions[t]; const phi = PHI[t] || 0; const r = 5 + Math.abs(phi) * 2.5; const c = phiColor(phi); const sel = t === selTicker; h += ` ${t} `; }); svg.innerHTML = h; } // ── HHKD ─────────────────────────────────────────────────────────────────── function drawHHKD() { const phiChart = document.getElementById('phi-chart'); const diag = document.getElementById('hhkd-diag'); if (!phiChart || !diag) return; const sorted = [...ALL].sort((a, b) => (PHI[b] || 0) - (PHI[a] || 0)).slice(0, 16); const maxAbs = Math.max(...ALL.map(t => Math.abs(PHI[t] || 0))); phiChart.innerHTML = sorted.map(t => { const phi = PHI[t] || 0; const c = phiColor(phi); const w = Math.abs(phi) / maxAbs * 100; return `
${t}
${fmtPhi(phi)}
`; }).join(''); const gradRatio = (0.90 + Math.random() * 0.05); diag.innerHTML = `
‖J_grad‖${(gradRatio * 2.1).toFixed(3)}
‖J_cyc‖${((1 - gradRatio) * 2.1).toFixed(3)}
‖J_res‖3.2e-7
Gradient %${(gradRatio * 100).toFixed(1)}%
`; // J_grad SVG heat strip const jg = document.getElementById('jgrad-svg'); if (!jg) return; jg.setAttribute('width', '100%'); jg.setAttribute('height', '60'); let hg = ''; SEC_NAMES.forEach((sec, si) => { SEC_NAMES.forEach((sec2, sj) => { if (si === sj) return; const v = MADJ[si][sj]; const c = adjColor(v); const W = 32, H = 28; hg += ``; }); }); jg.innerHTML = hg; } // ── Sector ───────────────────────────────────────────────────────────────── function drawSector() { const grid = document.getElementById('sector-grid'); if (!grid) return; const sorted = [...SEC_NAMES].sort((a, b) => (SEC_PHI[b] || 0) - (SEC_PHI[a] || 0)); grid.innerHTML = sorted.map(sec => { const phi = SEC_PHI[sec] || 0; const c = phiColor(phi); const members = SECTORS[sec] || []; return `
${sec.toUpperCase()} ${fmtPhi(phi)}
${members.map(t => { const tp = PHI[t] || 0; return `
${t}
`; }).join('')}
`; }).join(''); // Sector macro SVG const svg = document.getElementById('macro-svg'); if (!svg) return; const W = svg.parentElement ? (svg.parentElement.clientWidth || 400) : 400; const H = 120; svg.setAttribute('width', W); svg.setAttribute('height', H); const cx = W / 2, cy = H / 2, r = Math.min(cx, cy) - 18; const pts = SEC_NAMES.map((s, i) => { const angle = (i / S) * 2 * Math.PI - Math.PI / 2; return { x: cx + r * Math.cos(angle), y: cy + r * Math.sin(angle), s }; }); let h = ''; pts.forEach((p, a) => pts.forEach((q, b) => { if (a >= b) return; const v = MADJ[a][b]; const col = v > 0.6 ? 'rgba(240,165,0,0.35)' : 'rgba(56,56,46,0.4)'; h += ``; })); pts.forEach((p, i) => { const phi = SEC_PHI[SEC_NAMES[i]] || 0; const c = phiColor(phi); h += ``; h += `${SEC_NAMES[i].slice(0, 4).toUpperCase()}`; }); svg.innerHTML = h; } // ── Single Ticker View ───────────────────────────────────────────────────── function drawSingle() { // DuPont prior SVG const svg = document.getElementById('dupont-svg'); if (!svg) return; const CELL = 9; const W = FN * CELL + 10, H = FN * CELL + 10; svg.setAttribute('width', W); svg.setAttribute('height', H); let h = ''; for (let i = 0; i < FN; i++) { for (let j = 0; j < FN; j++) { const v = FPRIOR[i][j]; h += ``; } } svg.innerHTML = h; // Discovered edges const idx = ALL.indexOf(selTicker); const outEdges = EDGES.filter(e => e.si === idx).sort((a, b) => b.w - a.w).slice(0, 8); const discEl = document.getElementById('disc-edges'); if (discEl) { discEl.innerHTML = outEdges.map(e => { const t = ALL[e.ti]; const c = e.w > 0.7 ? 'var(--red)' : e.w > 0.5 ? 'var(--amber)' : 'var(--muted)'; return `
${selTicker} → ${t} ${e.w.toFixed(3)}
`; }).join('') || '
No causal edges above threshold
'; } // CAMEF forecast sparkline const camef = document.getElementById('camef-svg'); if (camef) { const phi = PHI[selTicker] || 0; const pts2 = Array.from({ length: 20 }, (_, i) => ({ x: 10 + i * 18, y: 55 - phi * 8 + (Math.sin(i * 0.7 + phi) * 6 + (Math.random() - 0.5) * 4), })); const col = phi > 0 ? 'var(--green)' : 'var(--red)'; const pathD = pts2.map((p, i) => (i === 0 ? `M${p.x},${p.y}` : `L${p.x},${p.y}`)).join(' '); camef.setAttribute('width', '100%'); camef.setAttribute('height', '70'); camef.setAttribute('viewBox', `0 0 380 70`); camef.innerHTML = ``; } // FCM lag bars const fcm = document.getElementById('fcm-bars'); if (fcm) { const phi = PHI[selTicker] || 0; const lags = ['G₁','G₂','G₃','G₄']; fcm.innerHTML = lags.map((g, i) => { const v = Math.max(0.05, Math.min(0.95, 0.5 + phi * 0.12 - i * 0.08 + Math.random() * 0.06)); const col = v > 0.6 ? 'var(--amber)' : v > 0.4 ? 'var(--cyan)' : 'var(--muted)'; return `
${g}
${v.toFixed(2)}
`; }).join(''); } } // ── Inference Engine ─────────────────────────────────────────────────────── function setInferMode(m) { inferMode = m; document.querySelectorAll('.infer-mode button').forEach(b => b.classList.remove('active')); const btn = document.querySelector(`.m-${m}`); if (btn) btn.classList.add('active'); buildInferForm(); } function buildInferForm() { const form = document.getElementById('infer-form'); if (!form) return; const tickers = ALL.map(t => ``).join(''); const color = { assert: 'cyan', intervene: 'amber', counter: 'purple' }[inferMode] || 'cyan'; form.innerHTML = `
SOURCE NODE
${inferMode !== 'assert' ? `
TARGET NODE
` : ''}
VALUE DELTA 0.50
`; } async function runInference() { const src = document.getElementById('infer-src')?.value; const tgt = document.getElementById('infer-tgt')?.value; const delta = parseFloat(document.querySelector('.infer-form input[type=range]')?.value || 0.5); const btn = document.getElementById('run-infer-btn'); if (!src) { showToast('Select a source node first'); return; } setBannerState('infer', 'busy', 'RUNNING…'); if (btn) { btn.disabled = true; btn.textContent = 'RUNNING…'; } // Try Gradio API first let result = null; if (GR_CLIENT) { try { const gr_result = await GR_CLIENT.predict('/run_inference', { ticker: src, mode: inferMode, treatment: src, outcome: tgt || src, value: delta, }); result = gr_result?.data; } catch (e) { console.warn('[Gradio predict] failed:', e); } } // Fallback: REST API if (!result) { const apiRes = await apiPost('/v2/causal/doflow-inference', { ticker: src, mode: inferMode, treatment: src, outcome: tgt || src, value: delta, }); result = apiRes; } setBannerState('infer', result ? 'ok' : 'err', result ? 'INFER OK' : 'INFER ERR'); if (btn) { btn.disabled = false; btn.textContent = `▶ RUN ${inferMode.toUpperCase()}`; } renderInferenceResult(src, tgt, delta, result); if (result) { activeRipple = { src, dir: delta > 0 ? 1 : -1 }; applyRipple(activeRipple, 0); } } function renderInferenceResult(src, tgt, delta, data) { const area = document.getElementById('results-area'); if (!area) return; const ate = data?.ate ?? (delta * (PHI[src] || 0.5) * 0.3); const prob = data?.probability ?? (0.5 + Math.abs(PHI[src] || 0) * 0.07); const confLow = data?.ci_lower ?? (ate - 0.12); const confHigh = data?.ci_upper ?? (ate + 0.12); const counterfact = data?.counterfactual_outcome ?? (ate * 0.85); const ripples = data?.ripple_effects ?? EDGES .filter(e => e.si === ALL.indexOf(src)) .sort((a, b) => b.w - a.w) .slice(0, 5) .map(e => ({ ticker: ALL[e.ti], direction: ate > 0 ? 1 : -1, magnitude: e.w * Math.abs(ate) })); const ateAbs = Math.min(1, Math.abs(ate) / 1.5); const ateCol = ate >= 0 ? 'var(--green)' : 'var(--red)'; const rippleChips = ripples.map(r => ` ${r.ticker} ${r.direction > 0 ? '↑' : '↓'} ${Math.abs(r.magnitude).toFixed(2)} ` ).join(''); area.innerHTML = `
${inferMode.toUpperCase()} — ${src}${tgt ? ' → ' + tgt : ''}
ATE${ate >= 0 ? '+' : ''}${ate.toFixed(3)}
P(effect)${prob.toFixed(3)}
95% CI[${confLow.toFixed(2)}, ${confHigh.toFixed(2)}]
${inferMode === 'counter' ? `
CF Outcome${counterfact.toFixed(3)}
` : ''}
RIPPLE EFFECTS →
${rippleChips || 'No downstream ripples detected'}
`; } // ── Ripple Propagation ───────────────────────────────────────────────────── function applyRipple(ripple, delay) { setTimeout(() => { const srcIdx = ALL.indexOf(ripple.src); if (srcIdx < 0) return; const downstream = EDGES .filter(e => e.si === srcIdx) .sort((a, b) => b.w - a.w) .slice(0, 8); // Heatmap ripple if (currentTab === 'matrix') { downstream.forEach(e => { const cell = document.getElementById(`hm-${srcIdx}-${e.ti}`); if (!cell) return; cell.classList.remove('ripple-out', 'ripple-in', 'ripple-pulse'); void cell.offsetWidth; cell.classList.add(ripple.dir > 0 ? 'ripple-in' : 'ripple-out'); setTimeout(() => cell.classList.remove('ripple-out', 'ripple-in'), 1200); }); } // Sidebar ripple downstream.forEach(e => { const t = ALL[e.ti]; const row = document.getElementById(`tr-${t}`); if (!row) return; row.classList.remove('rippling', 'rippling-up'); void row.offsetWidth; row.classList.add(ripple.dir > 0 ? 'rippling-up' : 'rippling'); setTimeout(() => row.classList.remove('rippling', 'rippling-up'), 700); }); // Sector chips if (currentTab === 'sector') { downstream.forEach(e => { const t = ALL[e.ti]; document.querySelectorAll('.sec-chip').forEach(ch => { if (ch.textContent.trim() === t) { ch.classList.remove('rippling', 'rippling-up'); void ch.offsetWidth; ch.classList.add(ripple.dir > 0 ? 'rippling-up' : 'rippling'); setTimeout(() => ch.classList.remove('rippling', 'rippling-up'), 800); } }); }); } }, delay); } // ── News Feed ────────────────────────────────────────────────────────────── function buildNewsFeed() { const el = document.getElementById('news-feed'); if (!el) return; el.innerHTML = NEWS.map(n => { const cls = n.score > 0.75 ? 'hi' : n.score > 0.5 ? 'md' : 'lo'; const dirCls = n.dir > 0 ? 'up' : 'dn'; return `
${n.score.toFixed(2)} ${n.sym} ${n.dir > 0 ? '▲' : '▼'}
${n.text}
${n.tags.map(t => `${t}`).join('')}
`; }).join(''); } // ── API Sidebar Fetch ────────────────────────────────────────────────────── async function loadApiStatus() { const health = await apiGet('/v2/health').catch(() => null); setBannerState('rest', health !== null ? 'ok' : 'err', health !== null ? 'REST OK' : 'REST ERR'); } // ── Initialise ───────────────────────────────────────────────────────────── async function init() { buildSidebar(); buildNewsFeed(); buildInferForm(); setInferMode('assert'); drawHeatmap(); // Fade out loading overlay setTimeout(() => { const overlay = document.getElementById('loading-overlay'); if (overlay) overlay.classList.add('hidden'); setTimeout(() => { if (overlay) overlay.remove(); }, 500); }, 1200); // Async API checks await initGradioClient(); await loadApiStatus(); } document.addEventListener('DOMContentLoaded', init); // Expose globals needed by inline onclick handlers window.setTab = setTab; window.setSeg = setSeg; window.filterTickers = filterTickers; window.selectTicker = selectTicker; window.showPopup = showPopup; window.hidePopup = hidePopup; window.setInferMode = setInferMode; window.runInference = runInference; window.hmHover = hmHover; window.hmClick = hmClick;