Files
Atomizer/atomizer-dashboard/frontend/src/components/ConsoleOutput.tsx
Antoine 8cbdbcad78 feat: Add Protocol 13 adaptive optimization, Plotly charts, and dashboard improvements
## Protocol 13: Adaptive Multi-Objective Optimization
- Iterative FEA + Neural Network surrogate workflow
- Initial FEA sampling, NN training, NN-accelerated search
- FEA validation of top NN predictions, retraining loop
- adaptive_state.json tracks iteration history and best values
- M1 mirror study (V11) with 103 FEA, 3000 NN trials

## Dashboard Visualization Enhancements
- Added Plotly.js interactive charts (parallel coords, Pareto, convergence)
- Lazy loading with React.lazy() for performance
- Code splitting: plotly.js-basic-dist (~1MB vs 3.5MB)
- Chart library toggle (Recharts default, Plotly on-demand)
- ExpandableChart component for full-screen modal views
- ConsoleOutput component for real-time log viewing

## Documentation
- Protocol 13 detailed documentation
- Dashboard visualization guide
- Plotly components README
- Updated run-optimization skill with Mode 5 (adaptive)

## Bug Fixes
- Fixed TypeScript errors in dashboard components
- Fixed Card component to accept ReactNode title
- Removed unused imports across components

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 07:41:54 -05:00

189 lines
6.2 KiB
TypeScript

/**
* ConsoleOutput - Real-time console/log viewer for optimization runs
* Displays the last N lines from the optimization log file
* Auto-refreshes to show live progress
*/
import { useState, useEffect, useRef } from 'react';
import { apiClient } from '../api/client';
interface ConsoleOutputProps {
studyId: string | null;
refreshInterval?: number; // ms between refreshes (default: 2000)
maxLines?: number; // max lines to display (default: 200)
}
export function ConsoleOutput({
studyId,
refreshInterval = 2000,
maxLines = 200
}: ConsoleOutputProps) {
const [lines, setLines] = useState<string[]>([]);
const [totalLines, setTotalLines] = useState(0);
const [logFile, setLogFile] = useState<string | null>(null);
const [lastUpdate, setLastUpdate] = useState<string>('');
const [isExpanded, setIsExpanded] = useState(false);
const [autoScroll, setAutoScroll] = useState(true);
const [error, setError] = useState<string | null>(null);
const consoleRef = useRef<HTMLDivElement>(null);
// Fetch console output
useEffect(() => {
if (!studyId) {
setLines([]);
setLogFile(null);
return;
}
const fetchLogs = async () => {
try {
const data = await apiClient.getConsoleOutput(studyId, maxLines);
setLines(data.lines);
setTotalLines(data.total_lines);
setLogFile(data.log_file);
setLastUpdate(new Date().toLocaleTimeString());
setError(null);
} catch (err) {
setError('Failed to fetch console output');
}
};
// Initial fetch
fetchLogs();
// Set up interval for auto-refresh
const interval = setInterval(fetchLogs, refreshInterval);
return () => clearInterval(interval);
}, [studyId, refreshInterval, maxLines]);
// Auto-scroll to bottom when new lines arrive
useEffect(() => {
if (autoScroll && consoleRef.current) {
consoleRef.current.scrollTop = consoleRef.current.scrollHeight;
}
}, [lines, autoScroll]);
// Parse log line to extract level and colorize
const parseLogLine = (line: string) => {
// Match common log patterns: timestamp | LEVEL | message
const levelMatch = line.match(/\|\s*(INFO|WARNING|ERROR|DEBUG|CRITICAL)\s*\|/);
const level = levelMatch ? levelMatch[1] : null;
// Also detect trial completion patterns
const isTrialComplete = line.includes('Trial') && (line.includes('finished') || line.includes('Best'));
const isBestTrial = line.includes('Best is trial');
const isIterationHeader = line.includes('ITERATION') || line.includes('======');
let className = 'text-gray-300';
if (level === 'ERROR' || level === 'CRITICAL') {
className = 'text-red-400';
} else if (level === 'WARNING') {
className = 'text-yellow-400';
} else if (level === 'DEBUG') {
className = 'text-gray-500';
} else if (isBestTrial) {
className = 'text-green-400 font-semibold';
} else if (isTrialComplete) {
className = 'text-blue-300';
} else if (isIterationHeader) {
className = 'text-cyan-400 font-bold';
}
return { line, className };
};
if (!studyId) {
return null;
}
const displayHeight = isExpanded ? 'h-96' : 'h-48';
return (
<div className="bg-gray-900 rounded-lg border border-gray-700 shadow-lg overflow-hidden">
{/* Header */}
<div className="bg-gray-800 px-4 py-2 flex items-center justify-between border-b border-gray-700">
<div className="flex items-center gap-3">
<div className="flex gap-1.5">
<div className="w-3 h-3 rounded-full bg-red-500" />
<div className="w-3 h-3 rounded-full bg-yellow-500" />
<div className="w-3 h-3 rounded-full bg-green-500" />
</div>
<span className="text-gray-300 font-mono text-sm">Console Output</span>
{logFile && (
<span className="text-gray-500 text-xs truncate max-w-md" title={logFile}>
{logFile.split(/[/\\]/).pop()}
</span>
)}
</div>
<div className="flex items-center gap-4">
{/* Stats */}
<span className="text-gray-500 text-xs">
{lines.length} / {totalLines} lines
</span>
{lastUpdate && (
<span className="text-gray-500 text-xs">
Updated: {lastUpdate}
</span>
)}
{/* Controls */}
<button
onClick={() => setAutoScroll(!autoScroll)}
className={`text-xs px-2 py-1 rounded ${
autoScroll
? 'bg-green-600 text-white'
: 'bg-gray-700 text-gray-400'
}`}
title={autoScroll ? 'Auto-scroll ON' : 'Auto-scroll OFF'}
>
{autoScroll ? '↓ Auto' : '↓ Manual'}
</button>
<button
onClick={() => setIsExpanded(!isExpanded)}
className="text-gray-400 hover:text-white text-sm px-2"
title={isExpanded ? 'Collapse' : 'Expand'}
>
{isExpanded ? '▼ Collapse' : '▲ Expand'}
</button>
</div>
</div>
{/* Console content */}
<div
ref={consoleRef}
className={`${displayHeight} overflow-y-auto p-4 font-mono text-xs leading-relaxed transition-all duration-300`}
onScroll={() => {
if (consoleRef.current) {
const { scrollTop, scrollHeight, clientHeight } = consoleRef.current;
// Disable auto-scroll if user scrolls up
const isAtBottom = scrollHeight - scrollTop - clientHeight < 50;
if (!isAtBottom && autoScroll) {
setAutoScroll(false);
}
}
}}
>
{error ? (
<div className="text-red-400">{error}</div>
) : lines.length === 0 ? (
<div className="text-gray-500 italic">
No console output available. Optimization may not have started yet or no log file found.
</div>
) : (
lines.map((line, idx) => {
const { className } = parseLogLine(line);
return (
<div key={idx} className={`${className} whitespace-pre-wrap break-all`}>
{line || ' '}
</div>
);
})
)}
</div>
</div>
);
}