import { useState, useEffect, useRef } from 'react'; import { LogService, LogEntry } from '@esengine/editor-core'; import { LogLevel } from '@esengine/ecs-framework'; import { Trash2, AlertCircle, Info, AlertTriangle, XCircle, Bug, Search, Maximize2, ChevronRight, ChevronDown, Wifi } from 'lucide-react'; import { JsonViewer } from './JsonViewer'; import '../styles/ConsolePanel.css'; interface ConsolePanelProps { logService: LogService; } export function ConsolePanel({ logService }: ConsolePanelProps) { const [logs, setLogs] = useState([]); const [filter, setFilter] = useState(''); const [levelFilter, setLevelFilter] = useState>(new Set([ LogLevel.Debug, LogLevel.Info, LogLevel.Warn, LogLevel.Error, LogLevel.Fatal ])); const [showRemoteOnly, setShowRemoteOnly] = useState(false); const [autoScroll, setAutoScroll] = useState(true); const [expandedLogs, setExpandedLogs] = useState>(new Set()); const [jsonViewerData, setJsonViewerData] = useState(null); const logContainerRef = useRef(null); useEffect(() => { setLogs(logService.getLogs()); const unsubscribe = logService.subscribe((entry) => { setLogs(prev => [...prev, entry]); }); return unsubscribe; }, [logService]); useEffect(() => { if (autoScroll && logContainerRef.current) { logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight; } }, [logs, autoScroll]); const handleClear = () => { logService.clear(); setLogs([]); }; const handleScroll = () => { if (logContainerRef.current) { const { scrollTop, scrollHeight, clientHeight } = logContainerRef.current; const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10; setAutoScroll(isAtBottom); } }; const toggleLevelFilter = (level: LogLevel) => { const newFilter = new Set(levelFilter); if (newFilter.has(level)) { newFilter.delete(level); } else { newFilter.add(level); } setLevelFilter(newFilter); }; const filteredLogs = logs.filter(log => { if (!levelFilter.has(log.level)) return false; if (showRemoteOnly && log.source !== 'remote') return false; if (filter && !log.message.toLowerCase().includes(filter.toLowerCase())) { return false; } return true; }); const getLevelIcon = (level: LogLevel) => { switch (level) { case LogLevel.Debug: return ; case LogLevel.Info: return ; case LogLevel.Warn: return ; case LogLevel.Error: case LogLevel.Fatal: return ; default: return ; } }; const getLevelClass = (level: LogLevel): string => { switch (level) { case LogLevel.Debug: return 'log-entry-debug'; case LogLevel.Info: return 'log-entry-info'; case LogLevel.Warn: return 'log-entry-warn'; case LogLevel.Error: case LogLevel.Fatal: return 'log-entry-error'; default: return ''; } }; const formatTime = (date: Date): string => { return date.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 3 }); }; const toggleLogExpand = (logId: number) => { const newExpanded = new Set(expandedLogs); if (newExpanded.has(logId)) { newExpanded.delete(logId); } else { newExpanded.add(logId); } setExpandedLogs(newExpanded); }; const extractJSON = (message: string): { prefix: string; json: string; suffix: string } | null => { const jsonStartChars = ['{', '[']; let startIndex = -1; for (const char of jsonStartChars) { const index = message.indexOf(char); if (index !== -1 && (startIndex === -1 || index < startIndex)) { startIndex = index; } } if (startIndex === -1) return null; for (let endIndex = message.length; endIndex > startIndex; endIndex--) { const possibleJson = message.substring(startIndex, endIndex); try { JSON.parse(possibleJson); return { prefix: message.substring(0, startIndex).trim(), json: possibleJson, suffix: message.substring(endIndex).trim() }; } catch { continue; } } return null; }; const tryParseJSON = (message: string): { isJSON: boolean; parsed?: any; jsonStr?: string } => { try { const parsed = JSON.parse(message); return { isJSON: true, parsed, jsonStr: message }; } catch { const extracted = extractJSON(message); if (extracted) { try { const parsed = JSON.parse(extracted.json); return { isJSON: true, parsed, jsonStr: extracted.json }; } catch { return { isJSON: false }; } } return { isJSON: false }; } }; const openJsonViewer = (jsonStr: string) => { try { const parsed = JSON.parse(jsonStr); setJsonViewerData(parsed); } catch { console.error('Failed to parse JSON:', jsonStr); } }; const formatMessage = (message: string, isExpanded: boolean): JSX.Element => { const MAX_PREVIEW_LENGTH = 200; const { isJSON, jsonStr } = tryParseJSON(message); const extracted = extractJSON(message); const shouldTruncate = message.length > MAX_PREVIEW_LENGTH && !isExpanded; return (
{shouldTruncate ? ( <> {extracted && extracted.prefix && {extracted.prefix} } {message.substring(0, MAX_PREVIEW_LENGTH)}... ) : ( {message} )}
{isJSON && jsonStr && ( )}
); }; const levelCounts = { [LogLevel.Debug]: logs.filter(l => l.level === LogLevel.Debug).length, [LogLevel.Info]: logs.filter(l => l.level === LogLevel.Info).length, [LogLevel.Warn]: logs.filter(l => l.level === LogLevel.Warn).length, [LogLevel.Error]: logs.filter(l => l.level === LogLevel.Error || l.level === LogLevel.Fatal).length }; const remoteLogCount = logs.filter(l => l.source === 'remote').length; return (
setFilter(e.target.value)} />
{filteredLogs.length === 0 ? (

No logs to display

) : ( filteredLogs.map(log => { const isExpanded = expandedLogs.has(log.id); const shouldShowExpander = log.message.length > 200; return (
{shouldShowExpander && (
toggleLogExpand(log.id)} > {isExpanded ? : }
)}
{getLevelIcon(log.level)}
{formatTime(log.timestamp)}
[{log.source === 'remote' ? '🌐 Remote' : log.source}]
{log.clientId && (
{log.clientId}
)}
{formatMessage(log.message, isExpanded)}
); }) )}
{jsonViewerData && ( setJsonViewerData(null)} /> )} {!autoScroll && ( )}
); }