diff --git a/visualizer/src/components/CodeBlock.tsx b/visualizer/src/components/CodeBlock.tsx index 376a82fd..0d433419 100644 --- a/visualizer/src/components/CodeBlock.tsx +++ b/visualizer/src/components/CodeBlock.tsx @@ -44,7 +44,7 @@ export function CodeBlock({ block, index }: CodeBlockProps) {
{executionTime && ( - {executionTime}s + {executionTime} )} {hasError && ( @@ -66,7 +66,8 @@ export function CodeBlock({ block, index }: CodeBlockProps) { - +
+ {/* Code */}
@@ -74,7 +75,7 @@ export function CodeBlock({ block, index }: CodeBlockProps) { Python
-
+
@@ -97,7 +98,7 @@ export function CodeBlock({ block, index }: CodeBlockProps) { {/* Errors */} {hasError && ( -
+
stderr @@ -178,8 +179,11 @@ export function CodeBlock({ block, index }: CodeBlockProps) {
)} +
+ + ); } diff --git a/visualizer/src/components/CodeWithLineNumbers.tsx b/visualizer/src/components/CodeWithLineNumbers.tsx index 6133475b..8af98f04 100644 --- a/visualizer/src/components/CodeWithLineNumbers.tsx +++ b/visualizer/src/components/CodeWithLineNumbers.tsx @@ -19,7 +19,7 @@ export function CodeWithLineNumbers({ return (
{/* Line numbers */} -
+
{lines.map((_, idx) => (
{/* Code */} -
+
diff --git a/visualizer/src/components/Dashboard.tsx b/visualizer/src/components/Dashboard.tsx index 79a9e00e..193b5be5 100644 --- a/visualizer/src/components/Dashboard.tsx +++ b/visualizer/src/components/Dashboard.tsx @@ -4,308 +4,332 @@ import { useState, useCallback, useEffect } from 'react'; import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { ScrollArea } from '@/components/ui/scroll-area'; +import { useDemoLogs, DemoLogInfo } from '@/hooks/use-demo-logs'; import { FileUploader } from './FileUploader'; import { LogViewer } from './LogViewer'; import { AsciiRLM } from './AsciiGlobe'; import { ThemeToggle } from './ThemeToggle'; -import { parseLogFile, extractContextVariable } from '@/lib/parse-logs'; +import { parseLogFile } from '@/lib/parse-logs'; import { RLMLogFile } from '@/lib/types'; import { cn } from '@/lib/utils'; -interface DemoLogInfo { - fileName: string; - contextPreview: string | null; - hasFinalAnswer: boolean; - iterations: number; -} - export function Dashboard() { const [logFiles, setLogFiles] = useState([]); const [selectedLog, setSelectedLog] = useState(null); - const [demoLogs, setDemoLogs] = useState([]); - const [loadingDemos, setLoadingDemos] = useState(true); - // Load demo log previews on mount - fetches latest 10 from API - useEffect(() => { - async function loadDemoPreviews() { - try { - // Fetch list of log files from API - const listResponse = await fetch('/api/logs'); - if (!listResponse.ok) { - throw new Error('Failed to fetch log list'); - } - const { files } = await listResponse.json(); - - const previews: DemoLogInfo[] = []; - - for (const fileName of files) { - try { - const response = await fetch(`/logs/${fileName}`); - if (!response.ok) continue; - const content = await response.text(); - const parsed = parseLogFile(fileName, content); - const contextVar = extractContextVariable(parsed.iterations); - - previews.push({ - fileName, - contextPreview: contextVar, - hasFinalAnswer: !!parsed.metadata.finalAnswer, - iterations: parsed.metadata.totalIterations, - }); - } catch (e) { - console.error('Failed to load demo preview:', fileName, e); - } - } - - setDemoLogs(previews); - } catch (e) { - console.error('Failed to load demo logs:', e); - } finally { - setLoadingDemos(false); - } - } - - loadDemoPreviews(); - }, []); + // Log files from server that users can load ("Recent Traces" section in UI) + const { demoLogs, loadingDemos } = useDemoLogs(); + // When a full log file is loaded (upload or demo), parse and register it. const handleFileLoaded = useCallback((fileName: string, content: string) => { const parsed = parseLogFile(fileName, content); setLogFiles(prev => { if (prev.some(f => f.fileName === fileName)) { - return prev.map(f => f.fileName === fileName ? parsed : f); + return prev.map(f => (f.fileName === fileName ? parsed : f)); } return [...prev, parsed]; }); setSelectedLog(parsed); }, []); - const loadDemoLog = useCallback(async (fileName: string) => { - try { - const response = await fetch(`/logs/${fileName}`); - if (!response.ok) throw new Error('Failed to load demo log'); - const content = await response.text(); - handleFileLoaded(fileName, content); - } catch (error) { - console.error('Error loading demo log:', error); - alert('Failed to load demo log. Make sure the log files are in the public/logs folder.'); - } - }, [handleFileLoaded]); + const loadDemoLog = useCallback( + async (fileName: string) => { + try { + const response = await fetch(`/logs/${fileName}`); + if (!response.ok) throw new Error('Failed to load demo log'); + const content = await response.text(); + handleFileLoaded(fileName, content); + } catch (error) { + console.error('Error loading demo log:', error); + alert('Failed to load demo log. Make sure the log files are in the public/logs folder.'); + } + }, + [handleFileLoaded], + ); if (selectedLog) { return ( - setSelectedLog(null)} + setSelectedLog(null)} /> ); } return (
- {/* Background effects */} -
-
-
- + +
- {/* Header */} -
-
-
-
-

- RLM - Visualizer -

-

- Debug recursive language model execution traces -

-
-
- -
- - - READY - -
-
-
-
-
+ - {/* Main Content */}
- {/* Left Column - Upload & ASCII Art */} +
- {/* Upload Section */} -
-

- 01 - Upload Log File -

- -
- - {/* ASCII Architecture Diagram */} -
-

- - RLM Architecture -

-
- -
-
+ +
- {/* Right Column - Demo Logs & Loaded Files */}
- {/* Demo Logs Section */} -
-

- 02 - Recent Traces - (latest 10) -

- - {loadingDemos ? ( - - -
- Loading traces... -
-
-
- ) : demoLogs.length === 0 ? ( - - - No log files found in /public/logs/ - - - ) : ( - -
- {demoLogs.map((demo) => ( - loadDemoLog(demo.fileName)} - className={cn( - 'cursor-pointer transition-all hover:scale-[1.01]', - 'hover:border-primary/50 hover:bg-primary/5' - )} - > - -
- {/* Status indicator */} -
-
- {demo.hasFinalAnswer && ( -
- )} -
- - {/* Content */} -
-
- - {demo.fileName} - - - {demo.iterations} iter - -
- {demo.contextPreview && ( -

- {demo.contextPreview.length > 80 - ? demo.contextPreview.slice(0, 80) + '...' - : demo.contextPreview} -

- )} -
-
- - - ))} -
- - )} -
- - {/* Loaded Files Section */} - {logFiles.length > 0 && ( -
-

- 03 - Loaded Files -

- -
- {logFiles.map((log) => ( - setSelectedLog(log)} - > - -
-
-
- {log.metadata.finalAnswer && ( -
- )} -
-
-
- - {log.fileName} - - - {log.metadata.totalIterations} iter - -
-

- {log.metadata.contextQuestion} -

-
-
- - - ))} -
- -
- )} + +
+
- {/* Footer */} -
-
-

- RLM Visualizer • Recursive Language Models -

-

- Prompt → [LM ↔ REPL] → Answer + +

+
+ ); +} + +function BackgroundEffects() { + return ( + <> +
+
+
+ + ); +} + +function DashboardHeader() { + return ( +
+
+
+
+

+ RLM + Visualizer +

+

+ Debug recursive language model execution traces

- +
+ +
+ + + READY + +
+
+
+
+ ); +} + +interface UploadSectionProps { + onFileLoaded: (fileName: string, content: string) => void; +} + +function UploadSection({ onFileLoaded }: UploadSectionProps) { + return ( +
+

+ 01 + Upload Log File +

+ +
+ ); +} + +function AsciiSection() { + return ( +
+

+ + RLM Architecture +

+
+ +
+
+ ); +} + +interface RecentTracesSectionProps { + demoLogs: DemoLogInfo[]; + loading: boolean; + onSelectDemo: (fileName: string) => void; +} + +function RecentTracesSection({ + demoLogs, + loading, + onSelectDemo, +}: RecentTracesSectionProps) { + return ( +
+

+ 02 + Recent Traces + (latest 10) +

+ + {loading ? ( + + +
+ Loading traces... +
+
+
+ ) : demoLogs.length === 0 ? ( + + + No log files found in /public/logs/ + + + ) : ( + +
ssss + {demoLogs.map((demo) => ( + onSelectDemo(demo.fileName)} + className={cn( + 'cursor-pointer transition-all hover:scale-[1.01]', + 'hover:border-primary/50 hover:bg-primary/5', + )} + > + +
+
+
+ {demo.hasFinalAnswer && ( +
+ )} +
+ +
+
+ + {demo.fileName} + + + {demo.iterations} iter + +
+ {demo.contextPreview && ( +

+ {demo.contextPreview.length > 80 + ? `${demo.contextPreview.slice(0, 80)}...` + : demo.contextPreview} +

+ )} +
+
+ + + ))} +
+ + )} +
+ ); +} + +interface LoadedFilesSectionProps { + logFiles: RLMLogFile[]; + onSelectLog: (log: RLMLogFile) => void; +} + +function LoadedFilesSection({ logFiles, onSelectLog }: LoadedFilesSectionProps) { + if (logFiles.length === 0) { + return null; + } + + return ( +
+

+ 03 + Loaded Files +

+ +
+ {logFiles.map((log) => ( + onSelectLog(log)} + > + +
+
+
+ {log.metadata.finalAnswer && ( +
+ )} +
+
+
+ + {log.fileName} + + + {log.metadata.totalIterations} iter + +
+

+ {log.metadata.contextQuestion} +

+
+
+ + + ))} +
+
); } + +function DashboardFooter() { + return ( +
+
+

+ RLM Visualizer • Recursive Language Models +

+

+ Prompt → [LM ↔ REPL] → Answer +

+
+
+ ); +} diff --git a/visualizer/src/components/ExecutionPanel.tsx b/visualizer/src/components/ExecutionPanel.tsx index e7457dfd..5ed16fb8 100644 --- a/visualizer/src/components/ExecutionPanel.tsx +++ b/visualizer/src/components/ExecutionPanel.tsx @@ -33,47 +33,13 @@ export function ExecutionPanel({ iteration }: ExecutionPanelProps) { ); return ( -
- {/* Header */} -
-
-
-
- ⟨⟩ -
-
-

Code & Sub-LM Calls

-

- Iteration {iteration.iteration} • {new Date(iteration.timestamp).toLocaleString()} -

-
-
-
- - {/* Quick stats */} -
- - {iteration.code_blocks.length} code block{iteration.code_blocks.length !== 1 ? 's' : ''} - - {totalSubCalls > 0 && ( - - {totalSubCalls} sub-LM call{totalSubCalls !== 1 ? 's' : ''} - - )} - {iteration.final_answer && ( - - Has Final Answer - - )} -
-
- +
{/* Tabs - Code Execution and Sub-LM Calls only */} -
+
- Code Execution + Code Blocks ({iteration.code_blocks.length}) Sub-LM Calls ({totalSubCalls}) @@ -84,7 +50,7 @@ export function ExecutionPanel({ iteration }: ExecutionPanelProps) {
-
+
{iteration.code_blocks.length > 0 ? ( iteration.code_blocks.map((block, idx) => ( diff --git a/visualizer/src/components/IterationTimeline.tsx b/visualizer/src/components/IterationTimeline.tsx index b03f5ed4..0c524653 100644 --- a/visualizer/src/components/IterationTimeline.tsx +++ b/visualizer/src/components/IterationTimeline.tsx @@ -68,19 +68,22 @@ export function IterationTimeline({
{/* Section header */}
-
- - - -
- Recursive Language Model Trajectory - ({iterations.length} total) + All Iterations ({iterations.length} total)
- - ← scroll → - +
+ + + + prev / next iteration + + + + Esc + back to files + +
@@ -147,8 +150,14 @@ export function IterationTimeline({ {stats.subCalls} sub )} - + {stats.execTime.toFixed(2)}s + + + {(stats.inputTokens / 1000).toFixed(1)}k + + {(stats.outputTokens / 1000).toFixed(1)}k +
@@ -157,22 +166,14 @@ export function IterationTimeline({ {responseSnippet}{iteration.response.length > 60 ? '...' : ''}

- {/* Bottom row: tokens */} -
- - {(stats.inputTokens / 1000).toFixed(1)}k - - {(stats.outputTokens / 1000).toFixed(1)}k - - {stats.hasFinal && finalAnswer && ( - <> - - - = {finalAnswer} - - - )} -
+ {/* Bottom row: final answer */} + {stats.hasFinal && finalAnswer && ( +
+ + = {finalAnswer} + +
+ )}
diff --git a/visualizer/src/components/LogViewer.tsx b/visualizer/src/components/LogViewer.tsx index cdfd751a..fe5d2a53 100644 --- a/visualizer/src/components/LogViewer.tsx +++ b/visualizer/src/components/LogViewer.tsx @@ -5,12 +5,12 @@ import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable'; -import { StatsCard } from './StatsCard'; +import { StatsRow } from './StatsRow'; import { TrajectoryPanel } from './TrajectoryPanel'; import { ExecutionPanel } from './ExecutionPanel'; import { IterationTimeline } from './IterationTimeline'; import { ThemeToggle } from './ThemeToggle'; -import { RLMLogFile } from '@/lib/types'; +import { RLMLogFile, RLMIteration } from '@/lib/types'; interface LogViewerProps { logFile: RLMLogFile; @@ -47,150 +47,262 @@ export function LogViewer({ logFile, onBack }: LogViewerProps) { return (
- {/* Top Bar - Compact header */} -
-
-
-
- -
-
-

- - {logFile.fileName} -

-

- {config.root_model ?? 'Unknown model'} • {config.backend ?? 'Unknown backend'} • {config.environment_type ?? 'Unknown env'} -

-
-
-
- {metadata.hasErrors && ( - Has Errors - )} - {metadata.finalAnswer && ( - - Completed - - )} - + + +
+ +
+ +
+ +
+ + + + + +
+ +
+ + {/* */} +
+ ); +} + +interface LogViewerHeaderProps { + fileName: string; + config: RLMLogFile['config']; + metadata: RLMLogFile['metadata']; + onBack: () => void; +} + +function LogViewerHeader({ + fileName, + config, + metadata, + onBack, +}: LogViewerHeaderProps) { + return ( +
+
+
+
+ +
+
+

+ + {fileName} +

+

+ {config.root_model ?? 'Unknown model'} •{' '} + {config.backend ?? 'Unknown backend'} •{' '} + {config.environment_type ?? 'Unknown env'} +

-
-
- - {/* Question & Answer + Stats Row */} -
-
- {/* Question & Answer Summary */} - - -
-
-

- Context / Question -

-

- {metadata.contextQuestion} -

-
-
-

- Final Answer -

-

- {metadata.finalAnswer || 'Not yet completed'} -

-
-
-
-
- - {/* Quick Stats */} -
- - - - +
+ {metadata.hasErrors && ( + + Has Errors + + )} + {metadata.finalAnswer && ( + + Completed + + )} +
+
+ ); +} - {/* Iteration Timeline - Full width scrollable row */} - +interface LogViewerSummaryProps { + metadata: RLMLogFile['metadata']; +} - {/* Main Content - Resizable Split View */} -
- - {/* Left Panel - Prompt & Response */} - -
- -
-
+function LogViewerSummary({ metadata }: LogViewerSummaryProps) { + return ( +
- +
+

+ Overview +

+ +
+ + + + +
+ +
+ +

{metadata.contextQuestion}

+
+ +
+ +

{metadata.finalAnswer || 'Not yet completed'}

+
- {/* Right Panel - Code Execution & Sub-LM Calls */} - -
- -
-
-
+
+ ); +} + +interface LogViewerMainContentProps { + iterations: RLMLogFile['iterations']; + selectedIteration: number; + onSelectIteration: (index: number) => void; +} + +function LogViewerMainContent({ + iterations, + selectedIteration, + onSelectIteration, +}: LogViewerMainContentProps) { + return ( +
+ + + +
+ +
+
+ + + + + + + +
+
+ ); +} - {/* Keyboard hint footer */} -
-
- - - - Navigate - - - Esc - Back - +interface SelectedIterationInfoProps { + iteration: RLMIteration | undefined; + iterationNumber: number; +} + +function SelectedIterationInfo({ iteration, iterationNumber }: SelectedIterationInfoProps) { + if (!iteration) return null; + + // Calculate stats similar to IterationTimeline + let totalSubCalls = 0; + let codeExecTime = 0; + + for (const block of iteration.code_blocks) { + if (block.result) { + codeExecTime += block.result.execution_time || 0; + if (block.result.rlm_calls) { + totalSubCalls += block.result.rlm_calls.length; + } + } + } + + // Use iteration_time if available, otherwise fall back to code execution time + const execTime = iteration.iteration_time ?? codeExecTime; + + // Estimate token counts from prompt (rough estimation) + const promptText = iteration.prompt.map(m => m.content).join(''); + const estimatedInputTokens = Math.round(promptText.length / 4); + const estimatedOutputTokens = Math.round(iteration.response.length / 4); + + const codeBlocks = iteration.code_blocks.length; + const inputTokensK = (estimatedInputTokens / 1000).toFixed(1); + const outputTokensK = (estimatedOutputTokens / 1000).toFixed(1); + + return ( +
+ {/* Section header and stats */} +
+
+ + + +
+ Iteration {iterationNumber} +
+
+
+ {execTime.toFixed(2)}s +
+ +
+ Est. Tokens + + {inputTokensK}k + + {outputTokensK}k + +
+ +
+ + {new Date(iteration.timestamp).toLocaleString()} + +
); } + +function LogViewerFooter() { + return ( +
+
+ + + + Navigate + + + Esc + Back + +
+
+ ); +} diff --git a/visualizer/src/components/StatsRow.tsx b/visualizer/src/components/StatsRow.tsx new file mode 100644 index 00000000..ee71b6bc --- /dev/null +++ b/visualizer/src/components/StatsRow.tsx @@ -0,0 +1,22 @@ +'use client'; + +import * as React from 'react'; + +interface StatsRowProps { + label: string; + value: string | number; + valueClassName?: string; +} + +export function StatsRow({ label, value, valueClassName }: StatsRowProps) { + return ( +
+

+ {label} +

+

+ {value} +

+
+ ); +} diff --git a/visualizer/src/components/TrajectoryPanel.tsx b/visualizer/src/components/TrajectoryPanel.tsx index 86a061f1..2d08b8d1 100644 --- a/visualizer/src/components/TrajectoryPanel.tsx +++ b/visualizer/src/components/TrajectoryPanel.tsx @@ -77,22 +77,18 @@ export function TrajectoryPanel({ {/* Header */}
-
- -
-

Conversation

-

- Iteration {selectedIteration + 1} of {iterations.length} +

+ Conversation

- {currentIteration?.code_blocks.length > 0 && ( + {/* {currentIteration?.code_blocks.length > 0 && ( {currentIteration.code_blocks.length} code - )} + )} */} {currentIteration?.final_answer && ( ✓ Answer diff --git a/visualizer/src/components/ui/collapsible.tsx b/visualizer/src/components/ui/collapsible.tsx index ae9fad04..6f6ed3ad 100644 --- a/visualizer/src/components/ui/collapsible.tsx +++ b/visualizer/src/components/ui/collapsible.tsx @@ -5,7 +5,7 @@ import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" function Collapsible({ ...props }: React.ComponentProps) { - return + return } function CollapsibleTrigger({ diff --git a/visualizer/src/hooks/use-demo-logs.ts b/visualizer/src/hooks/use-demo-logs.ts new file mode 100644 index 00000000..d4408eff --- /dev/null +++ b/visualizer/src/hooks/use-demo-logs.ts @@ -0,0 +1,81 @@ +import { useCallback, useEffect, useState } from 'react'; + +import { extractContextVariable, parseLogFile } from '@/lib/parse-logs'; + +// Lightweight summary of a log file for the "Recent Traces" list +export interface DemoLogInfo { + fileName: string; + contextPreview: string | null; + hasFinalAnswer: boolean; + iterations: number; +} + +interface UseDemoLogsResult { + demoLogs: DemoLogInfo[]; + loadingDemos: boolean; + refetch: () => Promise; +} + +// Encapsulates fetching + parsing of demo log previews from the backend +export function useDemoLogs(): UseDemoLogsResult { + const [demoLogs, setDemoLogs] = useState([]); + const [loadingDemos, setLoadingDemos] = useState(true); + + const loadDemoPreviews = useCallback(async () => { + setLoadingDemos(true); + + try { + // 1) Ask API for the list of available log files + const listResponse = await fetch('/api/logs'); + if (!listResponse.ok) { + throw new Error('Failed to fetch log list'); + } + + const { files } = await listResponse.json() as { files: string[] }; + + const previews: DemoLogInfo[] = []; + + // 2) For each file, fetch the JSONL content and build a small preview object + for (const fileName of files) { + try { + const response = await fetch(`/logs/${fileName}`); + if (!response.ok) continue; + + const content = await response.text(); + const parsed = parseLogFile(fileName, content); + const contextVar = extractContextVariable(parsed.iterations); + + previews.push({ + fileName, + contextPreview: contextVar, + hasFinalAnswer: !!parsed.metadata.finalAnswer, + iterations: parsed.metadata.totalIterations, + }); + } catch (error) { + // If one file fails, log it and keep going with the rest + // eslint-disable-next-line no-console + console.error('Failed to load demo preview:', fileName, error); + } + } + + setDemoLogs(previews); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Failed to load demo logs:', error); + } finally { + setLoadingDemos(false); + } + }, []); + + // Load once on mount so Dashboard just consumes the data. + useEffect(() => { + void loadDemoPreviews(); + }, [loadDemoPreviews]); + + return { + demoLogs, + loadingDemos, + refetch: loadDemoPreviews, + }; +} +