'use client'; import { useState, useEffect, useRef, useMemo } from 'react'; import { LogEntry, LogsApiResponse, ConfigApiResponse } from '../../types'; function parseLogLine(line: string): LogEntry | null { const match = line.match(/\[([^\]]+)\] \[([^\]]+)\] (.+)/); if (!match) return null; const [, timestamp, source, message] = match; const screenshot = message.match(/\[SCREENSHOT\] (.+)/)?.[1]; return { timestamp, source, message, screenshot, original: line }; } function LogEntryComponent({ entry }: { entry: LogEntry }) { return (
{new Date(entry.timestamp).toLocaleTimeString()} {entry.source}
{entry.message}
{entry.screenshot && (
Screenshot
)}
); } interface LogsClientProps { version: string; } export default function LogsClient({ version }: LogsClientProps) { const [logs, setLogs] = useState([]); const [mode, setMode] = useState<'head' | 'tail'>('tail'); const [isAtBottom, setIsAtBottom] = useState(true); const [isLoadingNew, setIsLoadingNew] = useState(false); const [lastLogCount, setLastLogCount] = useState(0); const [lastFetched, setLastFetched] = useState(null); const bottomRef = useRef(null); const containerRef = useRef(null); const pollIntervalRef = useRef(null); const pollForNewLogs = async () => { if (mode !== 'tail' || !isAtBottom) return; try { const response = await fetch('/api/logs/tail?lines=1000'); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data: LogsApiResponse = await response.json(); if (!data.logs) { console.warn('No logs data in response'); return; } const entries = data.logs .split('\n') .filter((line: string) => line.trim()) .map(parseLogLine) .filter((entry: LogEntry | null): entry is LogEntry => entry !== null); if (entries.length > lastLogCount) { setIsLoadingNew(true); setLastFetched(new Date()); setTimeout(() => { setLogs(entries); setLastLogCount(entries.length); setIsLoadingNew(false); // Auto-scroll to bottom for new content setTimeout(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); }, 50); }, 250); } } catch (error) { console.error('Error polling logs:', error); // Don't spam console on network errors during polling } }; // Start/stop polling based on mode and scroll position useEffect(() => { if (mode === 'tail' && isAtBottom) { pollIntervalRef.current = setInterval(pollForNewLogs, 2000); // Poll every 2 seconds return () => { if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); } }; } else { if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); } } }, [mode, isAtBottom, lastLogCount]); const loadInitialLogs = async () => { try { const response = await fetch(`/api/logs/${mode}?lines=1000`); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data: LogsApiResponse = await response.json(); if (!data.logs) { console.warn('No logs data in response'); setLogs([]); return; } const entries = data.logs .split('\n') .filter((line: string) => line.trim()) .map(parseLogLine) .filter((entry: LogEntry | null): entry is LogEntry => entry !== null); setLogs(entries); setLastLogCount(entries.length); setLastFetched(new Date()); // Auto-scroll to bottom for tail mode if (mode === 'tail') { setTimeout(() => { bottomRef.current?.scrollIntoView({ behavior: 'auto' }); setIsAtBottom(true); }, 100); } } catch (error) { console.error('Error loading logs:', error); setLogs([]); } }; useEffect(() => { loadInitialLogs(); }, [mode]); useEffect(() => { return () => { if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); } }; }, []); const handleScroll = () => { if (containerRef.current) { const { scrollTop, scrollHeight, clientHeight } = containerRef.current; const atBottom = scrollTop + clientHeight >= scrollHeight - 10; setIsAtBottom(atBottom); } }; const filteredLogs = useMemo(() => { return logs; }, [logs]); return (

🎭 dev-playwright

(v{version}) {logs.length} entries
{/* Mode Toggle */}
{/* Live indicator */}
Live
{filteredLogs.length === 0 ? (
📝 No logs yet
Logs will appear here as your development server runs
) : (
{filteredLogs.map((entry, index) => ( ))}
)}
{/* Footer with status and scroll indicator - full width like header */}
{isLoadingNew && (
Loading...
)} {!isLoadingNew && isAtBottom && lastFetched && ( Last updated {lastFetched.toLocaleTimeString()} )}
{/* Scroll to bottom button - positioned on the right */} {mode === 'tail' && !isAtBottom && !isLoadingNew && ( )}
); }