spb/social-runtime-crawler
Public
TypeScript 91.8%
HTML 3.2%
JavaScript 3%
SQL 1.4%
CSS 0.7%
1"use client";23import Link from "next/link";4import { AnimatePresence, motion } from "framer-motion";5import { useLive, type LiveEvent } from "@/lib/api";6import { eventColor, fmtTime, truncate } from "@/lib/format";7import { Tag } from "./ui";89export function describeEvent(e: LiveEvent): string {10 const p = e.payload ?? {};11 switch (e.event_type) {12 case "ACTION_PLANNED": {13 const a = p.action as { type?: string; label?: string } | undefined;14 return `${a?.type ?? ""} — ${truncate(a?.label, 80)} · gain ${Number(p.expected_information_gain ?? 0).toFixed(3)} · ${truncate(String(p.reason ?? ""), 90)}`;15 }16 case "ACTION_EXECUTED":17 case "ACTION_FAILED": {18 const a = p.action as { type?: string } | undefined;19 const after = p.after as { new_entities?: number; network_responses?: number } | undefined;20 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`;21 }22 case "PAGE_OPENED":23 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)}`;24 case "NETWORK_RESPONSE_OBSERVED":25 return `${p.kind} ${p.method} ${p.hostname}${p.path_pattern}${p.graphql_operation ? ` [${p.graphql_operation}]` : ""} · ${p.entity_count} entities · ${p.body_size} B`;26 case "NETWORK_SCHEMA_DISCOVERED": {27 const fp = p.fingerprint as { hostname?: string; path_pattern?: string } | undefined;28 const sc = p.schema as { candidate_entity_types?: { type: string; confidence: number }[] } | undefined;29 return `new shape ${fp?.hostname}${fp?.path_pattern} → ${(sc?.candidate_entity_types ?? []).map((c) => `${c.type} ${c.confidence.toFixed(2)}`).join(", ") || "?"}`;30 }31 case "CONNECTOR_PATTERN_LEARNED":32 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}×`;33 case "DOM_CHANGED":34 return `+${p.added} −${p.removed} nodes · ${Object.entries((p.regions as Record<string, number>) ?? {}).map(([k, v]) => `${k} ${v}`).join(" ")}${p.modal_opened ? " · modal" : ""}${Number(p.new_videos) ? ` · ${p.new_videos} video` : ""}`;35 case "MEDIA_DISCOVERED":36 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 ?? ""}`;37 case "LOOP_DETECTED":38 return `loop ${p.pattern} at ${truncate(String(p.url ?? ""), 60)} — breaking out`;39 case "CONNECTOR_DEGRADED":40 return `expected ${p.expected} entities on ${p.page_type}, observed ${p.observed} — ${p.action}`;41 case "BUDGET_EXHAUSTED":42 return `session ended: ${p.reason}`;43 default: {44 const ent = p.entity as { type?: string; name?: string; platform_id?: string; author?: string } | undefined;45 if (ent) return `${ent.type}: ${truncate(ent.name ?? ent.platform_id ?? "", 80)}${ent.author ? ` — ${truncate(ent.author, 30)}` : ""}`;46 return truncate(JSON.stringify(p), 120);47 }48 }49}5051export function LiveFeed({ session, types, max = 60, height = 420 }: { session?: string; types?: string[]; max?: number; height?: number }) {52 const { events, connected } = useLive({ session, types, max });53 return (54 <div className="overflow-hidden" style={{ height }}>55 {!connected && events.length === 0 && <div className="text-xs text-dim p-2">connecting to the observation stream…</div>}56 {connected && events.length === 0 && <div className="text-xs text-dim p-2">stream connected — waiting for observations (start a mission)</div>}57 <ul className="space-y-0.5 mono text-[11.5px]">58 <AnimatePresence initial={false}>59 {events.map((e) => (60 <motion.li key={e.event_id} initial={{ opacity: 0, y: -6 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0 }} transition={{ duration: 0.18 }} className="flex gap-2 items-start leading-5 rounded px-1 hover:bg-white/[0.03]">61 <span className="text-dim shrink-0">{fmtTime(e.ts)}</span>62 <span className="text-dim shrink-0 w-7 text-right">#{e.step ?? 0}</span>63 <Tag color={eventColor(e.event_type)} className="shrink-0">64 {e.event_type.replace(/_/g, " ").toLowerCase()}65 </Tag>66 <span className="text-fg-2 truncate">{describeEvent(e)}</span>67 {!session && (68 <Link href={`/sessions/${e.session_id}`} className="ml-auto shrink-0 text-dim hover:text-cyan">69 {e.platform}70 </Link>71 )}72 </motion.li>73 ))}74 </AnimatePresence>75 </ul>76 </div>77 );78}79