"use client" import { useEffect, useState, useRef } from "react" import { fetchApi, getApiUrl } from "@/lib/api-config" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { ScrollArea } from "@/components/ui/scroll-area" import { Loader2, CheckCircle2, XCircle, AlertCircle } from "lucide-react" interface HybridScriptMonitorProps { sessionId: string | null title?: string description?: string onClose: () => void onComplete?: (success: boolean) => void } interface ScriptInteraction { type: "msgbox" | "yesno" | "inputbox" | "menu" id: string title: string text: string data?: string } interface LogEntry { timestamp: string message: string type: "info" | "error" | "warning" | "success" } export function HybridScriptMonitor({ sessionId, title = "Script Execution", description = "Monitoring script execution...", onClose, onComplete, }: HybridScriptMonitorProps) { const [logs, setLogs] = useState([]) const [interaction, setInteraction] = useState(null) const [status, setStatus] = useState<"running" | "completed" | "failed">("running") const [inputValue, setInputValue] = useState("") const [selectedMenuItem, setSelectedMenuItem] = useState("") const [isResponding, setIsResponding] = useState(false) const [eventSourceState, setEventSourceState] = useState<"connecting" | "open" | "closed" | "error">("connecting") const [lastEventTime, setLastEventTime] = useState(null) const scrollRef = useRef(null) const pollingIntervalRef = useRef(null) const lastLogPositionRef = useRef(0) const decodeBase64 = (str: string): string => { try { return atob(str) } catch (e) { console.error("[v0] Failed to decode base64:", str, e) return str } } useEffect(() => { if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight } }, [logs]) useEffect(() => { if (!sessionId) return console.log("[v0] Setting up EventSource for session:", sessionId) const eventSourceUrl = getApiUrl(`/api/scripts/logs/${sessionId}`) console.log("[v0] EventSource URL:", eventSourceUrl) const eventSource = new EventSource(eventSourceUrl) eventSource.onopen = () => { console.log("[v0] EventSource connection opened") setEventSourceState("open") setLogs((prev) => [ ...prev, { timestamp: new Date().toLocaleTimeString(), message: "Connected to log stream", type: "success", }, ]) } eventSource.onmessage = (event) => { setLastEventTime(new Date()) try { const data = JSON.parse(event.data) console.log("[v0] Received SSE event:", data) if (data.type === "init") { setLogs((prev) => [ ...prev, { timestamp: new Date().toLocaleTimeString(), message: `Starting script: ${data.script}`, type: "info", }, ]) } else if (data.type === "raw") { const message = data.message if (message.includes("WEB_INTERACTION:")) { const interactionPart = message.split("WEB_INTERACTION:")[1] if (interactionPart) { const parts = interactionPart.split(":") if (parts.length >= 4) { const [type, id, titleB64, textB64, ...dataParts] = parts const dataB64 = dataParts.join(":") console.log("[v0] Detected interaction:", { type, id, titleB64, textB64, dataB64 }) setInteraction({ type: type as ScriptInteraction["type"], id, title: decodeBase64(titleB64), text: decodeBase64(textB64), data: dataB64 ? decodeBase64(dataB64) : undefined, }) } } } else { setLogs((prev) => [ ...prev, { timestamp: new Date().toLocaleTimeString(), message, type: message.toLowerCase().includes("error") ? "error" : message.toLowerCase().includes("warning") ? "warning" : message.toLowerCase().includes("success") || message.toLowerCase().includes("complete") ? "success" : "info", }, ]) } } else if (data.type === "error") { setLogs((prev) => [ ...prev, { timestamp: new Date().toLocaleTimeString(), message: `Error: ${data.message}`, type: "error", }, ]) } else { console.warn("[v0] Unknown SSE event type:", data.type, "Full data:", data) setLogs((prev) => [ ...prev, { timestamp: new Date().toLocaleTimeString(), message: `[Unknown event type: ${data.type}] ${JSON.stringify(data)}`, type: "warning", }, ]) } } catch (e) { console.error("[v0] Error parsing SSE event:", e, "Raw data:", event.data) setLogs((prev) => [ ...prev, { timestamp: new Date().toLocaleTimeString(), message: event.data, type: "info", }, ]) } } eventSource.onerror = (error) => { console.error("[v0] EventSource error:", error) setEventSourceState("error") setLogs((prev) => [ ...prev, { timestamp: new Date().toLocaleTimeString(), message: "Connection to log stream lost. Retrying...", type: "error", }, ]) } const pollStatus = async () => { try { const statusData = await fetchApi(`/api/scripts/status/${sessionId}`) console.log("[v0] Status data:", statusData) if (eventSourceState === "open" && lastEventTime) { const timeSinceLastEvent = Date.now() - lastEventTime.getTime() if (timeSinceLastEvent > 10000) { console.warn("[v0] No logs received for 10 seconds. Flask may not be streaming logs.") setLogs((prev) => [ ...prev, { timestamp: new Date().toLocaleTimeString(), message: "Warning: No new logs received. Check Flask script_runner streaming.", type: "warning", }, ]) } } if (statusData.status === "completed" || statusData.exit_code === 0) { console.log("[v0] Script execution completed") setStatus("completed") eventSource.close() setEventSourceState("closed") if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current) } onComplete?.(true) } else if (statusData.status === "failed" || (statusData.exit_code !== null && statusData.exit_code !== 0)) { console.log("[v0] Script execution failed with exit code:", statusData.exit_code) setStatus("failed") eventSource.close() setEventSourceState("closed") if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current) } onComplete?.(false) } if (statusData.pending_interaction) { const parts = statusData.pending_interaction.split(":") if (parts.length >= 4) { const [type, id, titleB64, textB64, ...dataParts] = parts const dataB64 = dataParts.join(":") setInteraction({ type: type as ScriptInteraction["type"], id, title: decodeBase64(titleB64), text: decodeBase64(textB64), data: dataB64 ? decodeBase64(dataB64) : undefined, }) } } } catch (error) { console.error("[v0] Error polling status:", error) } } pollStatus() pollingIntervalRef.current = setInterval(pollStatus, 2000) return () => { console.log("[v0] Cleaning up EventSource and polling") eventSource.close() if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current) } } }, [sessionId, onComplete, eventSourceState, lastEventTime]) const handleInteractionResponse = async (response: string) => { if (!interaction || !sessionId) return setIsResponding(true) try { console.log("[v0] Sending interaction response:", { session_id: sessionId, interaction_id: interaction.id, value: response, }) await fetchApi("/api/scripts/respond", { method: "POST", body: JSON.stringify({ session_id: sessionId, interaction_id: interaction.id, value: response, }), }) console.log("[v0] Response sent successfully") setInteraction(null) setInputValue("") setSelectedMenuItem("") } catch (error) { console.error("[v0] Error responding to interaction:", error) setLogs((prev) => [ ...prev, { timestamp: new Date().toLocaleTimeString(), message: `Error responding: ${error}`, type: "error", }, ]) } finally { setIsResponding(false) } } const renderInteractionModal = () => { if (!interaction) return null switch (interaction.type) { case "msgbox": return ( {}}> {interaction.title} {interaction.text}
) case "yesno": return ( {}}> {interaction.title} {interaction.text}
) case "inputbox": return ( {}}> {interaction.title} {interaction.text}
setInputValue(e.target.value)} placeholder={interaction.data || "Enter value..."} />
) case "menu": const menuItems = interaction.data?.split("|").filter(Boolean) || [] return ( {}}> {interaction.title} {interaction.text}
{menuItems.map((item, index) => ( ))}
) default: return null } } if (!sessionId) return null return ( <> {status === "running" && } {status === "completed" && } {status === "failed" && } {title} {description}
Execution Logs Stream: {eventSourceState === "open" && "🟢 Connected"} {eventSourceState === "connecting" && "🟡 Connecting..."} {eventSourceState === "closed" && "⚫ Closed"} {eventSourceState === "error" && "🔴 Error"}
{logs.length === 0 ? (
Waiting for logs...
) : ( logs.map((log, index) => (
[{log.timestamp}] {log.message}
)) )}
Session ID: {sessionId}
{status !== "running" && }
{renderInteractionModal()} ) }