-
- ←
- →
- 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,
+ };
+}
+