// LiveFeed — the information-bias trader's primary surface. // // Aggregates EVERY autonomous signal the system produced into one // chronological feed: persona memos (cron + event + alert + twitter + // manual + team), raw Twitter signals (matched but maybe not yet fired), // event-trigger fires, recent order fills. Auto-refresh 30s. Bounded // to last 24h by default; expandable to 7d. // // Design decisions tuned for "what changed since I last looked": // - Time-stamped left rail with "5m ago / 2h ago / yesterday" deltas // - Source pill color (memo type / twitter / fire / fill) // - Ticker chips inline + clickable filter // - "since-you-last-looked" badge on the topbar trigger // - Auto-scroll to top on new items so the most recent always anchored // - Compact rendering — info-density over whitespace // // Not duplicating the Memo Timeline (which is the audit/forensic surface, // drill-down focused). LiveFeed is the "I'm at my desk, what's happening // NOW" surface. const { useState: lfUseState, useEffect: lfUseEffect, useCallback: lfUseCallback, useMemo: lfUseMemo, useRef: lfUseRef } = React; const LF_ROLE_META = { 'phd-researcher': { label: 'PhD', color: '#8db4d8' }, 'hedging-specialist': { label: 'Hedging', color: '#d96a6a' }, 'framework-portfolio-modeling': { label: 'Sizing', color: '#5fb37c' }, 'framework-fundamentals': { label: 'Fundamentals', color: '#d9a85f' }, 'framework-value-investing': { label: 'Value', color: '#b48dd8' }, 'framework-growth-investing': { label: 'Growth', color: '#7fc7d9' }, 'framework-mental-models': { label: 'Mental', color: '#d8a07f' }, 'managing-director': { label: 'MD', color: '#ffffff' }, 'jarvis': { label: 'Tibeb', color: 'var(--accent)' }, 'editor-jarvis': { label: 'Editor', color: 'var(--accent)' }, 'regime-watcher': { label: 'Regime', color: '#ff9c5e' }, 'promotion-proposer': { label: 'Promote', color: '#5fd9b3' }, }; const LF_SOURCE_META = { cron: { label: 'cron', color: 'var(--text-dim)', bg: 'rgba(255,255,255,0.04)' }, event: { label: 'EVENT', color: '#d96a6a', bg: 'rgba(217,106,106,0.14)' }, alert: { label: 'ALERT', color: '#d9a85f', bg: 'rgba(217,168,95,0.14)' }, twitter: { label: 'TWITTER', color: '#5fb3d9', bg: 'rgba(95,179,217,0.14)' }, team: { label: 'team', color: '#b48dd8', bg: 'rgba(180,141,216,0.10)' }, manual: { label: 'manual', color: 'var(--text-2)', bg: 'transparent' }, solo: { label: 'solo', color: 'var(--text-2)', bg: 'transparent' }, fill: { label: 'FILL', color: '#5fb37c', bg: 'rgba(95,179,124,0.14)' }, signal: { label: 'signal', color: '#5fb3d9', bg: 'rgba(95,179,217,0.08)' }, }; // Relative time — "5m ago", "2h ago", "yesterday at 3:42pm" function relativeTime(ts) { const t = new Date(ts).getTime(); const now = Date.now(); const delta = now - t; if (delta < 60_000) return 'just now'; if (delta < 3600_000) return `${Math.floor(delta / 60_000)}m ago`; if (delta < 86400_000) return `${Math.floor(delta / 3600_000)}h ago`; if (delta < 172800_000) return `yesterday`; const days = Math.floor(delta / 86400_000); if (days < 7) return `${days}d ago`; return new Date(ts).toLocaleDateString(); } function LiveFeed({ open, onClose }) { if (!open) return null; const [items, setItems] = lfUseState([]); const [loading, setLoading] = lfUseState(true); const [err, setErr] = lfUseState(''); const [filter, setFilter] = lfUseState({ ticker: '', source: '', role: '' }); const [windowH, setWindowH] = lfUseState(24); // hours to look back const lastSeenRef = lfUseRef( Number(localStorage.getItem('jarvis.livefeed.lastSeen') || 0) ); const load = lfUseCallback(async () => { setLoading(true); setErr(''); try { const cutoff = Date.now() - windowH * 3600_000; const cutoffISO = new Date(cutoff).toISOString(); const [memosR, signalsR, ordersR] = await Promise.all([ fetch(`/api/personas/memos?limit=50&since=${encodeURIComponent(cutoffISO)}`).catch(() => null), fetch(`/api/twitter/signals?limit=50`).catch(() => null), fetch(`/api/orders?limit=20`).catch(() => null), ]); const all = []; if (memosR && memosR.ok) { const j = await memosR.json(); for (const m of (j.memos || [])) { all.push({ kind: 'memo', id: m.path, ts: m.ts, role: m.role, source: m.source || 'cron', topic: m.topic, excerpt: m.excerpt, ticker_hits: m.ticker_hits || [], tickers: m.tickers || [], verdict: m.verdict, path: m.path, }); } } if (signalsR && signalsR.ok) { const j = await signalsR.json(); for (const s of (j.signals || [])) { // Drop signals whose memo we already loaded — avoid double-count if (s.memo_path && all.some(a => a.kind === 'memo' && a.path === s.memo_path)) { continue; } // Drop signals older than the window const sigTs = new Date(s.created || s.fired_at || s.posted_at || 0).getTime(); if (sigTs < cutoff) continue; all.push({ kind: 'signal', id: `sig:${s.tweet_id}`, ts: s.created || s.fired_at || s.posted_at, handle: s.handle, sector: s.sector, text: s.text, engagement: s.engagement, matched_keywords: s.matched_keywords || [], routed_to: s.routed_to, memo_path: s.memo_path, source: 'signal', }); } } if (ordersR && ordersR.ok) { const j = await ordersR.json(); for (const o of (j.orders || [])) { if (o.status !== 'filled' && o.status !== 'partially_filled') continue; const ts = o.filled_at || o.created; if (!ts) continue; if (new Date(ts).getTime() < cutoff) continue; all.push({ kind: 'fill', id: `ord:${o.id}`, ts: ts, ticker: o.ticker, side: o.side, qty: o.filled_qty || o.qty, price: o.fill_price, source: 'fill', }); } } // Newest first all.sort((a, b) => new Date(b.ts) - new Date(a.ts)); setItems(all); } catch (e) { setErr(String(e.message || e)); } finally { setLoading(false); } }, [windowH]); // Auto-poll every 30s while open lfUseEffect(() => { if (!open) return; load(); const id = setInterval(load, 30_000); return () => clearInterval(id); }, [open, load]); // Mark as seen when panel opens lfUseEffect(() => { if (open) { lastSeenRef.current = Date.now(); try { localStorage.setItem('jarvis.livefeed.lastSeen', String(Date.now())); } catch {} } }, [open]); // Apply filters const filtered = lfUseMemo(() => { return items.filter(it => { if (filter.source && it.source !== filter.source) return false; if (filter.role && it.kind === 'memo' && it.role !== filter.role) return false; if (filter.ticker) { const tk = filter.ticker.replace(/^\$/, '').toUpperCase(); const inMemo = it.kind === 'memo' && ((it.tickers || []).some(t => t.toUpperCase() === tk) || (it.ticker_hits || []).some(t => t.toUpperCase() === tk) || (it.topic || '').toUpperCase().includes(tk)); const inSignal = it.kind === 'signal' && (it.text || '').toUpperCase().includes(tk); const inFill = it.kind === 'fill' && (it.ticker || '').toUpperCase() === tk; if (!(inMemo || inSignal || inFill)) return false; } return true; }); }, [items, filter]); // Distinct tickers seen across the feed — clickable chips const tickersInFeed = lfUseMemo(() => { const counts = {}; for (const it of items) { const ts = (it.tickers || []).concat(it.ticker_hits || []); if (it.ticker) ts.push(it.ticker); for (const t of ts) { if (t) counts[t.toUpperCase()] = (counts[t.toUpperCase()] || 0) + 1; } } return Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, 12); }, [items]); return (