/** * Pipeline admin dashboard — video list with status, retrigger/revoke, * expandable event log with token usage and collapsible JSON viewer. */ import { useCallback, useEffect, useRef, useState } from "react"; import { useSearchParams } from "react-router-dom"; import { fetchPipelineVideos, fetchPipelineEvents, fetchWorkerStatus, fetchDebugMode, setDebugMode, triggerPipeline, revokePipeline, type PipelineVideoItem, type PipelineEvent, type WorkerStatusResponse, } from "../api/public-client"; // ── Helpers ────────────────────────────────────────────────────────────────── function formatDate(iso: string | null): string { if (!iso) return "—"; return new Date(iso).toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit", }); } function formatTokens(n: number): string { if (n === 0) return "0"; if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`; if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`; return String(n); } function statusBadgeClass(status: string): string { switch (status) { case "completed": case "indexed": return "pipeline-badge--success"; case "processing": case "extracted": case "classified": case "synthesized": return "pipeline-badge--active"; case "failed": case "error": return "pipeline-badge--error"; case "pending": case "queued": return "pipeline-badge--pending"; default: return ""; } } function eventTypeIcon(eventType: string): string { switch (eventType) { case "start": return "▶"; case "complete": return "✓"; case "error": return "✗"; case "llm_call": return "🤖"; default: return "·"; } } // ── Collapsible JSON ───────────────────────────────────────────────────────── function JsonViewer({ data }: { data: Record | null }) { const [open, setOpen] = useState(false); if (!data || Object.keys(data).length === 0) return null; return (
{open && (
          {JSON.stringify(data, null, 2)}
        
)}
); } // ── Debug Payload Viewer ───────────────────────────────────────────────────── interface DebugSection { label: string; content: string; } function DebugPayloadViewer({ event }: { event: PipelineEvent }) { const sections: DebugSection[] = []; if (event.system_prompt_text) sections.push({ label: "System Prompt", content: event.system_prompt_text }); if (event.user_prompt_text) sections.push({ label: "User Prompt", content: event.user_prompt_text }); if (event.response_text) sections.push({ label: "Response", content: event.response_text }); const [openSections, setOpenSections] = useState>({}); const [copiedKey, setCopiedKey] = useState(null); if (sections.length === 0) return null; const toggleSection = (label: string) => { setOpenSections((prev) => ({ ...prev, [label]: !prev[label] })); }; const copyToClipboard = async (label: string, text: string) => { try { await navigator.clipboard.writeText(text); setCopiedKey(label); setTimeout(() => setCopiedKey(null), 1500); } catch { // Fallback for non-HTTPS contexts const ta = document.createElement("textarea"); ta.value = text; ta.style.position = "fixed"; ta.style.opacity = "0"; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); document.body.removeChild(ta); setCopiedKey(label); setTimeout(() => setCopiedKey(null), 1500); } }; const exportAsJson = () => { const data: Record = { event_id: event.id, stage: event.stage, event_type: event.event_type, model: event.model, system_prompt_text: event.system_prompt_text, user_prompt_text: event.user_prompt_text, response_text: event.response_text, }; const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `debug-${event.stage}-${event.id.slice(0, 8)}.json`; a.click(); URL.revokeObjectURL(url); }; return (
LLM Debug
{sections.map((sec) => { const isOpen = !!openSections[sec.label]; return (
{isOpen && (
{sec.content}
)}
); })}
); } // ── Event Log ──────────────────────────────────────────────────────────────── function EventLog({ videoId }: { videoId: string }) { const [events, setEvents] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [offset, setOffset] = useState(0); const [viewMode, setViewMode] = useState<"head" | "tail">("tail"); const limit = 50; const load = useCallback(async () => { setLoading(true); setError(null); try { const res = await fetchPipelineEvents(videoId, { offset, limit, order: viewMode === "head" ? "asc" : "desc", }); setEvents(res.items); setTotal(res.total); } catch (err) { setError(err instanceof Error ? err.message : "Failed to load events"); } finally { setLoading(false); } }, [videoId, offset, viewMode]); useEffect(() => { void load(); }, [load]); if (loading) return
Loading events…
; if (error) return
Error: {error}
; if (events.length === 0) return
No events recorded.
; const hasNext = offset + limit < total; const hasPrev = offset > 0; return (
{total} event{total !== 1 ? "s" : ""}
{events.map((evt) => (
{eventTypeIcon(evt.event_type)} {evt.stage} {evt.event_type} {evt.model && {evt.model}} {evt.total_tokens != null && evt.total_tokens > 0 && ( {formatTokens(evt.total_tokens)} tok )} {evt.duration_ms != null && ( {evt.duration_ms}ms )} {formatDate(evt.created_at)}
))}
{(hasPrev || hasNext) && (
{offset + 1}–{Math.min(offset + limit, total)} of {total}
)}
); } // ── Worker Status ──────────────────────────────────────────────────────────── function WorkerStatus() { const [status, setStatus] = useState(null); const [error, setError] = useState(null); const load = useCallback(async () => { try { setError(null); const res = await fetchWorkerStatus(); setStatus(res); } catch (err) { setError(err instanceof Error ? err.message : "Failed"); } }, []); useEffect(() => { void load(); const id = setInterval(() => void load(), 15_000); return () => clearInterval(id); }, [load]); if (error) { return (
Worker: error ({error})
); } if (!status) { return (
Worker: checking…
); } return (
{status.online ? `${status.workers.length} worker${status.workers.length !== 1 ? "s" : ""} online` : "Workers offline"} {status.workers.map((w) => ( {w.active_tasks.length > 0 ? `${w.active_tasks.length} active` : "idle"} {w.pool_size != null && ` · pool ${w.pool_size}`} ))}
); } // ── Debug Mode Toggle ──────────────────────────────────────────────────────── function DebugModeToggle({ debugMode, onDebugModeChange, }: { debugMode: boolean | null; onDebugModeChange: (mode: boolean) => void; }) { const [debugLoading, setDebugLoading] = useState(false); async function handleToggle() { if (debugMode === null || debugLoading) return; setDebugLoading(true); try { const res = await setDebugMode(!debugMode); onDebugModeChange(res.debug_mode); } catch { // swallow — leave previous state } finally { setDebugLoading(false); } } if (debugMode === null) return null; return (
Debug {debugMode ? "On" : "Off"}
); } // ── Status Filter ──────────────────────────────────────────────────────────── function StatusFilter({ videos, activeFilter, onFilterChange, }: { videos: PipelineVideoItem[]; activeFilter: string | null; onFilterChange: (filter: string | null) => void; }) { const statuses = Array.from(new Set(videos.map((v) => v.processing_status))).sort(); if (statuses.length <= 1) return null; return (
{statuses.map((status) => ( ))}
); } // ── Main Page ──────────────────────────────────────────────────────────────── export default function AdminPipeline() { const [searchParams] = useSearchParams(); const [videos, setVideos] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [expandedId, setExpandedId] = useState(null); const [actionLoading, setActionLoading] = useState(null); const [actionMessage, setActionMessage] = useState<{ id: string; text: string; ok: boolean } | null>(null); const [activeFilter, setActiveFilter] = useState(null); const [debugMode, setDebugModeState] = useState(null); const videoRefs = useRef>(new Map()); const deepLinked = useRef(false); const load = useCallback(async () => { setLoading(true); setError(null); try { const res = await fetchPipelineVideos(); setVideos(res.items); } catch (err) { setError(err instanceof Error ? err.message : "Failed to load videos"); } finally { setLoading(false); } }, []); useEffect(() => { void load(); }, [load]); // Deep-link: auto-expand and scroll to ?video= on first load useEffect(() => { if (deepLinked.current || loading || videos.length === 0) return; const targetVideoId = searchParams.get("video"); if (!targetVideoId) return; const match = videos.find((v) => v.id === targetVideoId); if (!match) return; deepLinked.current = true; setExpandedId(targetVideoId); // Scroll after the expanded detail renders requestAnimationFrame(() => { const el = videoRefs.current.get(targetVideoId); if (el) el.scrollIntoView({ behavior: "smooth", block: "start" }); }); }, [loading, videos, searchParams]); useEffect(() => { let cancelled = false; fetchDebugMode() .then((res) => { if (!cancelled) setDebugModeState(res.debug_mode); }) .catch(() => { // silently fail — toggle stays hidden }); return () => { cancelled = true; }; }, []); const handleTrigger = async (videoId: string) => { setActionLoading(videoId); setActionMessage(null); try { const res = await triggerPipeline(videoId); setActionMessage({ id: videoId, text: `Triggered (${res.status})`, ok: true }); // Refresh after short delay to let status update setTimeout(() => void load(), 2000); } catch (err) { setActionMessage({ id: videoId, text: err instanceof Error ? err.message : "Trigger failed", ok: false, }); } finally { setActionLoading(null); } }; const handleRevoke = async (videoId: string) => { setActionLoading(videoId); setActionMessage(null); try { const res = await revokePipeline(videoId); setActionMessage({ id: videoId, text: res.tasks_revoked > 0 ? `Revoked ${res.tasks_revoked} task${res.tasks_revoked !== 1 ? "s" : ""}` : "No active tasks", ok: true, }); setTimeout(() => void load(), 2000); } catch (err) { setActionMessage({ id: videoId, text: err instanceof Error ? err.message : "Revoke failed", ok: false, }); } finally { setActionLoading(null); } }; const toggleExpand = (id: string) => { setExpandedId((prev) => (prev === id ? null : id)); }; return (

Pipeline Management

{videos.length} video{videos.length !== 1 ? "s" : ""}

{loading ? (
Loading videos…
) : error ? (
Error: {error}
) : videos.length === 0 ? (
No videos in pipeline.
) : ( <>
{videos .filter((v) => activeFilter === null || v.processing_status === activeFilter) .map((video) => (
{ if (el) videoRefs.current.set(video.id, el); }}>
toggleExpand(video.id)} >
{video.filename} {video.creator_name}
{video.processing_status} {video.event_count} events {formatTokens(video.total_tokens_used)} tokens {formatDate(video.last_event_at)} e.stopPropagation()} > → Moments
e.stopPropagation()}>
{actionMessage?.id === video.id && (
{actionMessage.text}
)} {expandedId === video.id && (
Created: {formatDate(video.created_at)} Updated: {formatDate(video.updated_at)}
)}
))}
)}
); }