SPB Git forge
7commits 1branches 0releases
229.0 KBsize
maindefault branch
12 days agolast push
TypeScript 91.8% HTML 3.2% JavaScript 3% SQL 1.4% CSS 0.7%
5.0 KB · 79 lines tsx
Raw Blame History
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