import React, { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { motion, AnimatePresence } from 'framer-motion'; import { Terminal, X, Trash2, Search, ArrowDownToLine, Pause, Play, Bug, Info, AlertTriangle, AlertOctagon } from 'lucide-react'; import { useDebugConsole, LogEntry, LogLevel } from '../../stores/useDebugConsole'; import { cn } from '../../utils/cn'; const LEVEL_CONFIG: Record = { 'ERROR': { color: 'text-red-500', icon: , label: 'Error' }, 'WARN': { color: 'text-amber-500', icon: , label: 'Warn' }, 'INFO': { color: 'text-blue-500', icon: , label: 'Info' }, 'DEBUG': { color: 'text-zinc-400', icon: , label: 'Debug' }, 'TRACE': { color: 'text-zinc-600', icon: , label: 'Trace' }, }; const LogRow = React.memo(({ log }: { log: LogEntry }) => { const [expanded, setExpanded] = useState(false); const date = new Date(log.timestamp); const timeStr = date.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }) + '.' + date.getMilliseconds().toString().padStart(3, '0'); const hasFields = Object.keys(log.fields).length > 0; return (
hasFields && setExpanded(!expanded)} > {timeStr} {LEVEL_CONFIG[log.level as LogLevel].icon} {log.level} {log.target.split('::').slice(-2).join('::')} {log.message}
{expanded && hasFields && (
{Object.entries(log.fields).map(([key, value]) => ( {key}: {value} ))}
)}
); }); interface DebugConsoleProps { embedded?: boolean; } const DebugConsole: React.FC = ({ embedded = false }) => { const { t } = useTranslation(); const { isOpen, close, logs, clearLogs, filter, setFilter, searchTerm, setSearchTerm, autoScroll, setAutoScroll, checkEnabled } = useDebugConsole(); const scrollRef = useRef(null); const [height, setHeight] = useState(320); // Initial check useEffect(() => { checkEnabled(); }, []); // Auto scroll useEffect(() => { if (autoScroll && scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }, [logs, autoScroll, isOpen]); // Handle resize const startResizing = (e: React.MouseEvent) => { e.preventDefault(); document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', stopResizing); }; const handleMouseMove = (e: MouseEvent) => { const newHeight = window.innerHeight - e.clientY; if (newHeight > 100 && newHeight < window.innerHeight - 100) { setHeight(newHeight); } }; const stopResizing = () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', stopResizing); }; const toggleLevel = (level: LogLevel) => { if (filter.includes(level)) { setFilter(filter.filter(l => l !== level)); } else { setFilter([...filter, level]); } }; const scrollToBottom = () => { if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; setAutoScroll(true); } }; const handleScroll = (e: React.UIEvent) => { const element = e.currentTarget; const isAtBottom = Math.abs(element.scrollHeight - element.scrollTop - element.clientHeight) < 20; if (!isAtBottom && autoScroll) { setAutoScroll(false); } else if (isAtBottom && !autoScroll) { setAutoScroll(true); } }; const filteredLogs = logs.filter(log => { if (!filter.includes(log.level as LogLevel)) return false; if (searchTerm && !log.message.toLowerCase().includes(searchTerm.toLowerCase()) && !log.target.toLowerCase().includes(searchTerm.toLowerCase())) return false; return true; }); const content = (
{/* Resize Handle (only for non-embedded) */} {!embedded && (
)} {/* Toolbar */}
CONSOLE
{/* Filter Toggles */}
{(Object.keys(LEVEL_CONFIG) as LogLevel[]).map(level => ( ))}
{/* Search */}
setSearchTerm(e.target.value)} placeholder="Filter logs..." className={cn( "border border-transparent rounded-md pl-8 pr-3 py-1 text-xs w-40 focus:w-64 transition-all focus:outline-none placeholder:text-zinc-400", "bg-zinc-100 dark:bg-black/20", "text-zinc-800 dark:text-zinc-300", "focus:bg-white dark:focus:bg-black/40", "focus:border-zinc-200 dark:focus:border-white/10" )} />
{!embedded && ( )}
{/* Log content */}
{filteredLogs.length === 0 ? (

{t('debug_console.no_logs', { defaultValue: 'No logs to display' })}

{t('debug_console.no_logs_hint', { defaultValue: 'Logs will appear here in real-time' })}

) : (
{filteredLogs.map(log => )}
)}
{/* Footer */}
{/* Level stats */} {(Object.keys(LEVEL_CONFIG) as LogLevel[]).map(level => { const count = logs.filter(l => l.level === level).length; if (count === 0) return null; const icon = LEVEL_CONFIG[level].icon; // FIX: Don't clone, just render new one or use generic // Since I cannot easily clone with correct types without casting, and the icon is simple // I will just use a mapping or switch, OR just render the icon node as is if it fits, // BUT I want to force size and color. // Simplest fix: Just allow the icon to be rendered, and assume it inherits size? No, Lucide icons have explicit size. // Let's just redefine a small helper to get the icon component type if possible, or just ignore the size override since 12 is small enough. // Actually, the LEVEL_CONFIG defines size=12. So I can just render it. return ( {icon} {count} ); })}
{/* Auto-scroll indicator & Status */}
{!autoScroll && ( )}
Live
); if (embedded) { return content; } return ( {isOpen && ( <> {/* Backdrop */} {/* Animated Panel */} {content} )} ); }; export default DebugConsole;