"use client"; import Link from "next/link"; import { AnimatePresence, motion } from "framer-motion"; import { useLive, type LiveEvent } from "@/lib/api"; import { eventColor, fmtTime, truncate } from "@/lib/format"; import { Tag } from "./ui"; export function describeEvent(e: LiveEvent): string { const p = e.payload ?? {}; switch (e.event_type) { case "ACTION_PLANNED": { const a = p.action as { type?: string; label?: string } | undefined; return `${a?.type ?? ""} — ${truncate(a?.label, 80)} · gain ${Number(p.expected_information_gain ?? 0).toFixed(3)} · ${truncate(String(p.reason ?? ""), 90)}`; } case "ACTION_EXECUTED": case "ACTION_FAILED": { const a = p.action as { type?: string } | undefined; const after = p.after as { new_entities?: number; network_responses?: number } | undefined; return `${a?.type ?? ""} ${p.ok ? "ok" : "failed"}${p.error ? ` (${truncate(String(p.error), 60)})` : ""} · +${after?.new_entities ?? 0} entities · ${after?.network_responses ?? 0} responses · ${p.duration_ms} ms`; } case "PAGE_OPENED": return p.page_type ? `${p.page_type} (${Number(p.confidence ?? 0).toFixed(2)}) ${truncate(String(p.url ?? ""), 70)} · dom ${p.dom_entities} · net ${p.network_entities} · both ${p.both_surfaces}` : `${truncate(String(p.url ?? ""), 80)}`; case "NETWORK_RESPONSE_OBSERVED": return `${p.kind} ${p.method} ${p.hostname}${p.path_pattern}${p.graphql_operation ? ` [${p.graphql_operation}]` : ""} · ${p.entity_count} entities · ${p.body_size} B`; case "NETWORK_SCHEMA_DISCOVERED": { const fp = p.fingerprint as { hostname?: string; path_pattern?: string } | undefined; const sc = p.schema as { candidate_entity_types?: { type: string; confidence: number }[] } | undefined; return `new shape ${fp?.hostname}${fp?.path_pattern} → ${(sc?.candidate_entity_types ?? []).map((c) => `${c.type} ${c.confidence.toFixed(2)}`).join(", ") || "?"}`; } case "CONNECTOR_PATTERN_LEARNED": return `learned ${p.hostname}${p.path_pattern} → ${Object.keys((p.likely_entity_types as object) ?? {}).join(",")} · conf ${Number(p.confidence ?? 0).toFixed(2)} · seen ${p.observed_count}×`; case "DOM_CHANGED": return `+${p.added} −${p.removed} nodes · ${Object.entries((p.regions as Record) ?? {}).map(([k, v]) => `${k} ${v}`).join(" ")}${p.modal_opened ? " · modal" : ""}${Number(p.new_videos) ? ` · ${p.new_videos} video` : ""}`; case "MEDIA_DISCOVERED": return `${p.media_type} ${truncate(String(p.title ?? p.platform_media_id ?? ""), 60)} · ${p.duration_s ? Math.round(Number(p.duration_s)) + "s" : ""} ${p.width ? `${p.width}×${p.height}` : ""} ${(p.delivery as { kind?: string } | undefined)?.kind ?? ""}`; case "LOOP_DETECTED": return `loop ${p.pattern} at ${truncate(String(p.url ?? ""), 60)} — breaking out`; case "CONNECTOR_DEGRADED": return `expected ${p.expected} entities on ${p.page_type}, observed ${p.observed} — ${p.action}`; case "BUDGET_EXHAUSTED": return `session ended: ${p.reason}`; default: { const ent = p.entity as { type?: string; name?: string; platform_id?: string; author?: string } | undefined; if (ent) return `${ent.type}: ${truncate(ent.name ?? ent.platform_id ?? "", 80)}${ent.author ? ` — ${truncate(ent.author, 30)}` : ""}`; return truncate(JSON.stringify(p), 120); } } } export function LiveFeed({ session, types, max = 60, height = 420 }: { session?: string; types?: string[]; max?: number; height?: number }) { const { events, connected } = useLive({ session, types, max }); return (
{!connected && events.length === 0 &&
connecting to the observation stream…
} {connected && events.length === 0 &&
stream connected — waiting for observations (start a mission)
}
); }