// @ts-nocheck import React, { useEffect, useState } from "react"; import { useKeyboard } from "@opentui/react"; import type { SmithersDb } from "../../../db/adapter.js"; export function FramesPane({ adapter, runId, focused, filterNodeId, nodeAttempt, }: { adapter: SmithersDb; runId: string; focused: boolean; filterNodeId?: string; nodeAttempt?: any; }) { const [frames, setFrames] = useState([]); const [selectedIndex, setSelectedIndex] = useState(0); useEffect(() => { let mounted = true; async function fetchFrames() { if (!mounted) return; try { // limit(500) to ensure we get a chunk safely, reverse to ASCENDING chronological order const data = await adapter.listFrames(runId, 500); if (mounted && data.length > 0) { const ascData = data.slice().reverse(); setFrames(ascData); if (frames.length === 0) { setSelectedIndex(ascData.length - 1); // default to newest frame (bottom) } } } catch (err) {} if (mounted) setTimeout(fetchFrames, 2000); // Polling for live updates } fetchFrames(); return () => { mounted = false; }; }, [adapter, runId]); let displayFrames = frames; if (filterNodeId && nodeAttempt && frames.length > 0) { const sMs = nodeAttempt.startedAtMs ?? 0; const fMs = nodeAttempt.finishedAtMs; const beforeF = frames.slice().reverse().find(f => f.createdAtMs <= sMs) || frames[0]; const afterF = fMs ? (frames.find(f => f.createdAtMs >= fMs) || frames[frames.length - 1]) : frames[frames.length - 1]; displayFrames = frames.map(f => { if (f.frameNo === beforeF.frameNo && f.frameNo === afterF.frameNo) { return { ...f, uiLabel: "Frame (Active)" }; } if (f.frameNo === beforeF.frameNo) return { ...f, uiLabel: "Frame (Before)" }; if (f.frameNo === afterF.frameNo) return { ...f, uiLabel: "Frame (After)" }; return f; }); } useKeyboard((key) => { if (!focused || displayFrames.length === 0) return; if (key.name === "up" || key.name === "k") { setSelectedIndex((s) => Math.max(0, s - 1)); } if (key.name === "down" || key.name === "j") { setSelectedIndex((s) => Math.min(s + 1, displayFrames.length - 1)); } }); if (displayFrames.length === 0) { return No frame history available...; } // Bound check in case selectedIndex drifted const validIndex = Math.max(0, Math.min(selectedIndex, displayFrames.length - 1)); const selectedFrame = displayFrames[validIndex]; // Recursive formatter to convert `xmlJson` into a JSX code block function formatJsxNode(node: any, indent: number = 0): string { if (!node || typeof node !== "object") return ""; const space = " ".repeat(indent); if (node.kind === "text") { const escaped = String(node.text || "").replace(/\n/g, `\n${space}`); return `${space}${escaped}`; } if (node.kind === "element" && typeof node.tag === "string") { let propsStr = ""; if (node.props && typeof node.props === "object") { for (const [k, v] of Object.entries(node.props)) { if (typeof v === "string") propsStr += ` ${k}="${v.replace(/"/g, '"')}"`; else propsStr += ` ${k}={${JSON.stringify(v)}}`; } } const tag = node.tag; const isTargetNode = filterNodeId && node.props?.id === filterNodeId; const colorPrefix = isTargetNode ? "👉 " : ""; if (!Array.isArray(node.children) || node.children.length === 0) { return `${space}${colorPrefix}<${tag}${propsStr} />`; } let res = `${space}${colorPrefix}<${tag}${propsStr}>\n`; for (const child of node.children) { res += formatJsxNode(child, indent + 1) + "\n"; } res += `${space}`; return res; } // Fallback if neither text nor element return `${space}${JSON.stringify(node)}`; } let xmlString = "Empty Frame"; try { if (selectedFrame?.xmlJson) { const parsed = typeof selectedFrame.xmlJson === "string" ? JSON.parse(selectedFrame.xmlJson) : selectedFrame.xmlJson; xmlString = formatJsxNode(parsed); } } catch (err: any) { xmlString = `[Format Error: ${err?.message}]\n\n${selectedFrame?.xmlJson ?? "Parse error"}`; } return ( {/* Left Sidebar: Frame List */} {displayFrames.map((frame, i) => { const isSelected = validIndex === i; const label = frame.uiLabel ? `${frame.uiLabel}` : `Frame ${frame.frameNo}`; return ( {isSelected ? "▶ " : " "}{label} ); })} {/* Right Content: XML/JSX Dump */} {xmlString} ); }