Files
Atomizer/atomizer-dashboard/frontend/src/components/ConsoleOutput.tsx

189 lines
6.2 KiB
TypeScript
Raw Normal View History

/**
* 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>
);
}