import { useEffect, useRef, useState, useCallback } from 'react'; import { Loader2, CheckCircle2, AlertCircle, X, Minimize2, Terminal as TerminalIcon, } from 'lucide-react'; import { Modal } from '@/components/ui/modal'; import { DialogTitle } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { StructuredLogRow } from '@/components/log-rendering/StructuredLogRow'; import TerminalComponent from '@/components/Terminal'; import { useDeployFeedback, VERB_LABELS } from '@/context/DeployFeedbackContext'; const AUTO_CLOSE_SECONDS = 4; interface DeployFeedbackModalProps { isMinimized: boolean; onMinimize: () => void; } function formatElapsed(seconds: number): string { if (seconds >= 60) { const m = Math.floor(seconds / 60); const s = seconds % 60; return `${m}m ${s}s`; } return `${seconds}s`; } export function DeployFeedbackModal({ isMinimized, onMinimize }: DeployFeedbackModalProps) { const { panelState, logRows, onTerminalReady, onMessage, onPanelClose } = useDeployFeedback(); const [showRaw, setShowRaw] = useState(false); const [elapsedSeconds, setElapsedSeconds] = useState(0); const [countdown, setCountdown] = useState(AUTO_CLOSE_SECONDS); const [userScrolledUp, setUserScrolledUp] = useState(false); const startTimeRef = useRef(0); const elapsedIntervalRef = useRef | null>(null); const countdownIntervalRef = useRef | null>(null); const autoCloseHoveredRef = useRef(false); const scrollRef = useRef(null); const { isOpen, stackName, action, status, errorMessage } = panelState; useEffect(() => { if (isOpen) { // eslint-disable-next-line react-hooks/set-state-in-effect setShowRaw(false); // eslint-disable-next-line react-hooks/set-state-in-effect setElapsedSeconds(0); // eslint-disable-next-line react-hooks/set-state-in-effect setCountdown(AUTO_CLOSE_SECONDS); // eslint-disable-next-line react-hooks/set-state-in-effect setUserScrolledUp(false); startTimeRef.current = 0; } }, [isOpen]); useEffect(() => { if (isOpen && status !== 'preparing') { if (startTimeRef.current === 0) startTimeRef.current = Date.now(); if (elapsedIntervalRef.current !== null) return; elapsedIntervalRef.current = setInterval(() => { setElapsedSeconds(Math.floor((Date.now() - startTimeRef.current) / 1000)); }, 1000); } else { if (elapsedIntervalRef.current !== null) { clearInterval(elapsedIntervalRef.current); elapsedIntervalRef.current = null; } } return () => { if (elapsedIntervalRef.current !== null) { clearInterval(elapsedIntervalRef.current); elapsedIntervalRef.current = null; } }; }, [isOpen, status]); const clearCountdownInterval = useCallback(() => { if (countdownIntervalRef.current !== null) { clearInterval(countdownIntervalRef.current); countdownIntervalRef.current = null; } }, []); const startCountdown = useCallback(() => { clearCountdownInterval(); setCountdown(AUTO_CLOSE_SECONDS); let remaining = AUTO_CLOSE_SECONDS; countdownIntervalRef.current = setInterval(() => { remaining -= 1; setCountdown(remaining); if (remaining <= 0) { clearCountdownInterval(); if (!autoCloseHoveredRef.current) { onPanelClose(); } } }, 1000); }, [clearCountdownInterval, onPanelClose]); useEffect(() => { if (status === 'succeeded' && isOpen) { // eslint-disable-next-line react-hooks/set-state-in-effect startCountdown(); } else { clearCountdownInterval(); } return () => { clearCountdownInterval(); }; }, [status, isOpen, startCountdown, clearCountdownInterval]); useEffect(() => { if (!userScrolledUp && scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }, [logRows.length, userScrolledUp]); const handleScroll = useCallback(() => { const el = scrollRef.current; if (!el) return; const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight; setUserScrolledUp(distanceFromBottom > 32); }, []); const handleMouseEnter = useCallback(() => { autoCloseHoveredRef.current = true; clearCountdownInterval(); }, [clearCountdownInterval]); const handleMouseLeave = useCallback(() => { autoCloseHoveredRef.current = false; if (status === 'succeeded' && isOpen) { startCountdown(); } }, [status, isOpen, startCountdown]); const handleOpenChange = useCallback( (open: boolean) => { if (!open) onPanelClose(); }, [onPanelClose] ); const isDialogOpen = isOpen && !isMinimized; const verbLabel = VERB_LABELS[action]; return (
{verbLabel.present} {stackName} {/* Header */}
{verbLabel.present} {stackName} {status !== 'preparing' && elapsedSeconds > 0 && ( {formatElapsed(elapsedSeconds)} )}
{/* Body */}
{logRows.length === 0 ? ( ) : (
{logRows.map((row) => ( ))}
)}
{/* always mounted so onMessage feeds structured rows even before user toggles raw view */}
{/* Footer */}
); } interface StatusIndicatorProps { status: 'preparing' | 'streaming' | 'succeeded' | 'failed'; rowCount: number; errorMessage?: string; countdown: number; } function StatusIndicator({ status, rowCount, errorMessage, countdown }: StatusIndicatorProps) { if (status === 'preparing') { return (
Connecting...
); } if (status === 'streaming') { return (
{rowCount} {rowCount === 1 ? 'line' : 'lines'}
); } if (status === 'succeeded') { return (
Succeeded closes in {countdown}s
); } // failed return (
{errorMessage ?? 'Failed'}
); } interface EmptyBodyProps { status: 'preparing' | 'streaming' | 'succeeded' | 'failed'; } function EmptyBody({ status }: EmptyBodyProps) { if (status === 'preparing') { return (
Connecting to log stream...
); } return (
Waiting for output...
); }