## 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>
189 lines
6.2 KiB
TypeScript
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>
|
|
);
|
|
}
|