"use client"; import { Pause, Play } from "lucide-react"; import Link from "next/link"; import { useEffect, useMemo, useState } from "react"; import { EventRow } from "./event-row"; import { toneOf } from "@/components/ui/price"; import { Pill } from "@/components/ui/section"; import { cx, EVENT_TYPE_LABEL, formatClock, formatPercent, formatQuoteValue, instrumentHref } from "@/lib/format"; import { useLiveEvents, useMarketStream, useTape } from "@/lib/stream"; import type { MarketEvent, StreamQuote } from "@/lib/types"; const CLASSES = ["CRYPTO", "EQUITY", "ETF", "INDEX", "FOREX", "TREASURY", "COMMODITY"]; const TYPES = ["PRICE_CHANGE", "SESSION_HIGH", "SESSION_LOW", "VOLATILITY_SPIKE", "TRADING_HALT", "TRADING_RESUME", "FILING_PUBLISHED", "MARKET_OPEN", "MARKET_CLOSE", "SOURCE_DIVERGENCE", "SOURCE_FAILURE", "SCHEMA_DRIFT", "DOCUMENT_CHANGED"]; const SEVERITIES = ["INFO", "NOTICE", "WARNING", "CRITICAL"]; type Item = { kind: "event"; ts: number; e: MarketEvent } | { kind: "quote"; ts: number; q: StreamQuote }; /** Flagship /live view: merged stream of events and price changes with client-side filters and pause. */ export function LiveFeed({ initial }: { initial: MarketEvent[] }) { const [paused, setPaused] = useState(false); const [showQuotes, setShowQuotes] = useState(true); const [cls, setCls] = useState(null); const [country, setCountry] = useState(""); const [types, setTypes] = useState>(new Set()); const [sev, setSev] = useState(null); const [minConf, setMinConf] = useState(0); const state = useMarketStream(showQuotes ? ["events:*", "tape"] : ["events:*"]); const liveEvents = useLiveEvents(300); const tape = useTape(120); const [frozen, setFrozen] = useState(null); const items = useMemo(() => { const seen = new Set(); const out: Item[] = []; for (const e of [...liveEvents, ...initial]) { if (seen.has(e.id)) continue; seen.add(e.id); out.push({ kind: "event", ts: typeof e.timestamp === "number" ? e.timestamp : Date.parse(String(e.timestamp).replace(" ", "T").replace(/([+-]\d{2})$/, "$1:00")), e }); } if (showQuotes) for (const q of tape) out.push({ kind: "quote", ts: q.received, q }); return out .filter((it) => { if (it.kind === "event") { const e = it.e; if (types.size && !types.has(e.type)) return false; if (sev && SEVERITIES.indexOf(e.severity) < SEVERITIES.indexOf(sev)) return false; if (e.confidence < minConf) return false; if (cls && !e.instruments.some((i) => i.asset_class === cls) && e.instruments.length) return false; if (country && !e.instruments.some((i) => i.country === country.toUpperCase())) return false; return true; } if (types.size || sev) return false; if (it.q.confidence < minConf) return false; return true; }) .sort((a, b) => b.ts - a.ts) .slice(0, 300); }, [liveEvents, initial, tape, showQuotes, types, sev, minConf, cls, country]); useEffect(() => { if (paused && !frozen) setFrozen(items); if (!paused && frozen) setFrozen(null); // eslint-disable-next-line react-hooks/exhaustive-deps }, [paused]); const shown = paused && frozen ? frozen : items; const toggleType = (t: string) => setTypes((s) => { const n = new Set(s); if (n.has(t)) n.delete(t); else n.add(t); return n; }); return (
{shown.length} items{paused ? " · paused" : ""} Browse the event archive →
{shown.length === 0 ? (
Nothing matches these filters yet. The feed fills as sources emit changes.
) : (
    {shown.map((it) => it.kind === "event" ? ( ) : (
  • {formatClock(it.q.timestamp)} quote {it.q.symbol} {formatQuoteValue(it.q.price, undefined, it.q.currency)} {formatPercent(it.q.change_pct)} {it.q.sources} src · {Math.round(it.q.confidence * 100)}%
  • ), )}
)}
); } function Filter({ title, children }: { title: string; children: React.ReactNode }) { return (
{title}
{children}
); }