/** * 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([]); const [totalLines, setTotalLines] = useState(0); const [logFile, setLogFile] = useState(null); const [lastUpdate, setLastUpdate] = useState(''); const [isExpanded, setIsExpanded] = useState(false); const [autoScroll, setAutoScroll] = useState(true); const [error, setError] = useState(null); const consoleRef = useRef(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 (
{/* Header */}
Console Output {logFile && ( {logFile.split(/[/\\]/).pop()} )}
{/* Stats */} {lines.length} / {totalLines} lines {lastUpdate && ( Updated: {lastUpdate} )} {/* Controls */}
{/* Console content */}
{ 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 ? (
{error}
) : lines.length === 0 ? (
No console output available. Optimization may not have started yet or no log file found.
) : ( lines.map((line, idx) => { const { className } = parseLogLine(line); return (
{line || ' '}
); }) )}
); }