import { Trash2 } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { Button } from "@/features/ui/primitives/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/features/ui/primitives/select"; import { cn } from "@/features/ui/primitives/styles"; import { Switch } from "@/features/ui/primitives/switch"; import type { LogEntry } from "../types"; interface ExecutionLogsProps { /** Log entries to display (from SSE stream or historical data) */ logs: LogEntry[]; /** Whether logs are from live SSE stream (shows "Live" indicator) */ isLive?: boolean; /** Callback to clear logs (optional, defaults to no-op) */ onClearLogs?: () => void; } /** * Get color class for log level badge - STATIC lookup */ const logLevelColors: Record = { info: "bg-blue-500/20 text-blue-600 dark:text-blue-400 border-blue-400/30", warning: "bg-yellow-500/20 text-yellow-600 dark:text-yellow-400 border-yellow-400/30", error: "bg-red-500/20 text-red-600 dark:text-red-400 border-red-400/30", debug: "bg-gray-500/20 text-gray-600 dark:text-gray-400 border-gray-400/30", }; /** * Format timestamp to relative time */ function formatRelativeTime(timestamp: string): string { const now = Date.now(); const logTime = new Date(timestamp).getTime(); const diffSeconds = Math.floor((now - logTime) / 1000); if (diffSeconds < 0) return "just now"; if (diffSeconds < 60) return `${diffSeconds}s ago`; if (diffSeconds < 3600) return `${Math.floor(diffSeconds / 60)}m ago`; return `${Math.floor(diffSeconds / 3600)}h ago`; } /** * Individual log entry component */ function LogEntryRow({ log }: { log: LogEntry }) { const colorClass = logLevelColors[log.level] || logLevelColors.debug; return (
{formatRelativeTime(log.timestamp)} {log.level} {log.step && [{log.step}]} {log.event} {log.progress && ( {log.progress} )}
); } export function ExecutionLogs({ logs, isLive = false, onClearLogs = () => {} }: ExecutionLogsProps) { const [autoScroll, setAutoScroll] = useState(true); const [levelFilter, setLevelFilter] = useState("all"); const [localLogs, setLocalLogs] = useState(logs); const [isCleared, setIsCleared] = useState(false); const previousLogsLengthRef = useRef(logs.length); const scrollContainerRef = useRef(null); // Update local logs when props change useEffect(() => { const currentLogsLength = logs.length; const previousLogsLength = previousLogsLengthRef.current; // If we cleared logs, only update if new logs arrive (length increases) if (isCleared) { if (currentLogsLength > previousLogsLength) { // New logs arrived after clear - reset cleared state and show new logs setLocalLogs(logs); setIsCleared(false); } // Otherwise, keep local logs empty (user's cleared view) } else { // Normal case: update local logs with prop changes setLocalLogs(logs); } previousLogsLengthRef.current = currentLogsLength; // eslint-disable-next-line react-hooks/exhaustive-deps }, [logs]); // Filter logs by level const filteredLogs = levelFilter === "all" ? localLogs : localLogs.filter((log) => log.level === levelFilter); /** * Handle clear logs button click */ const handleClearLogs = () => { setLocalLogs([]); setIsCleared(true); onClearLogs(); }; /** * Auto-scroll to bottom when new logs arrive (if enabled) */ useEffect(() => { if (autoScroll && scrollContainerRef.current) { scrollContainerRef.current.scrollTop = scrollContainerRef.current.scrollHeight; } }, [localLogs.length, autoScroll]); // Trigger on new logs, not filtered logs return (
{/* Header with controls */}
Execution Logs {/* Live/Historical indicator */} {isLive ? (
Live
) : (
Historical
)} ({filteredLogs.length} entries)
{/* Controls */}
{/* Level filter using proper Select primitive */} {/* Auto-scroll toggle using Switch primitive */}
{autoScroll ? "ON" : "OFF"}
{/* Clear logs button */}
{/* Log content - scrollable area */}
{filteredLogs.length === 0 ? (

No logs match the current filter

) : (
{filteredLogs.map((log, index) => ( ))}
)}
); }