"use client"; import { Check, Circle, ImageIcon, Loader2, ScrollText, Terminal, X, } from "lucide-react"; import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState, } from "react"; import Image from "next/image"; import { sanitizeLogText } from "@/lib/terminal-output"; export type ProcessLogStepStatus = | "pending" | "running" | "success" | "error" | "skipped"; export type ProcessLogStep = { id: string; label: string; description?: string; progress?: number | string; status?: ProcessLogStepStatus; }; export type ProcessLogsModalState = { title: string; description?: string; timelineLogs?: ProcessLogStep[]; terminalLogs?: string | string[]; imageAlt?: string; imageUrl?: string; imageSlot?: ReactNode; initialTab?: ProcessLogsModalTab; statusLabel?: string; }; export type ProcessLogsModalTab = "timeline" | "terminal"; type ProcessLogsModalProps = ProcessLogsModalState & { open: boolean; onClose: () => void; closeOnOverlayClick?: boolean; }; const defaultTimelineLogs: ProcessLogStep[] = [ { id: "queued", label: "Waiting for process data", progress: 0, status: "running", }, ]; const statusTone: Record< ProcessLogStepStatus, { color: string; border: string; background: string; icon: ReactNode; } > = { success: { color: "#16a34a", border: "rgba(22, 163, 74, 0.55)", background: "rgba(34, 197, 94, 0.2)", icon: , }, running: { color: "#16a34a", border: "rgba(22, 163, 74, 0.55)", background: "rgba(34, 197, 94, 0.18)", icon: , }, pending: { color: "#8b949e", border: "rgba(148, 163, 184, 0.48)", background: "rgba(148, 163, 184, 0.12)", icon: , }, skipped: { color: "#8b949e", border: "rgba(148, 163, 184, 0.42)", background: "rgba(148, 163, 184, 0.1)", icon: , }, error: { color: "#ef4444", border: "rgba(239, 68, 68, 0.58)", background: "rgba(239, 68, 68, 0.16)", icon: , }, }; function formatProgress(progress: ProcessLogStep["progress"]) { if (progress === undefined || progress === null || progress === "") { return ""; } return typeof progress === "number" ? `${progress}%` : progress; } export function useProcessLogsModal() { const [modalState, setModalState] = useState( null, ); const openProcessLogs = useCallback((state: ProcessLogsModalState) => { setModalState(state); }, []); const updateProcessLogs = useCallback( (nextState: Partial) => { setModalState((current) => current ? { ...current, ...nextState } : (nextState as ProcessLogsModalState), ); }, [], ); const closeProcessLogs = useCallback(() => { setModalState(null); }, []); return { modalState, isProcessLogsOpen: modalState !== null, openProcessLogs, updateProcessLogs, closeProcessLogs, }; } export default function ProcessLogsModal({ open, title, description = "Detail of the request log entry", timelineLogs = defaultTimelineLogs, terminalLogs = "", imageAlt = "Process preview", imageUrl, imageSlot, initialTab = "timeline", statusLabel, closeOnOverlayClick = true, onClose, }: ProcessLogsModalProps) { const [selectedTab, setSelectedTab] = useState( null, ); const terminalViewportRef = useRef(null); const activeTab = selectedTab ?? initialTab; const sanitizedTerminalLogs = useMemo(() => { const rawLogs = Array.isArray(terminalLogs) ? terminalLogs.join("\n") : terminalLogs; return sanitizeLogText(rawLogs || "No terminal logs available yet."); }, [terminalLogs]); useEffect(() => { if (!open || activeTab !== "terminal") return; const viewport = terminalViewportRef.current; if (!viewport) return; viewport.scrollTop = viewport.scrollHeight; }, [activeTab, open, sanitizedTerminalLogs]); const handleClose = useCallback(() => { setSelectedTab(null); onClose(); }, [onClose]); if (!open) { return null; } const latestActiveStep = [...timelineLogs] .reverse() .find((step) => step.status === "running" || step.status === "success") ?? timelineLogs[0]; const progressLabel = statusLabel ?? formatProgress(latestActiveStep?.progress); const progressBarWidth = typeof latestActiveStep?.progress === "number" ? `${Math.min(Math.max(latestActiveStep.progress, 0), 100)}%` : latestActiveStep?.status === "running" ? "50%" : "100%"; return (
event.stopPropagation()} >
{activeTab === "timeline" ? (
{imageSlot ?? (imageUrl ? ( {imageAlt} ) : ( ))}
{timelineLogs.map((step) => { const status = step.status ?? "pending"; const tone = statusTone[status]; const stepProgress = formatProgress(step.progress); return (
{step.label} {step.description ? ( {step.description} ) : null}
{stepProgress ? ( {stepProgress} ) : null}
); })}
{progressLabel ? (
) : null}
) : (
              {sanitizedTerminalLogs}
            
)}
); }