"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, TerminalIcon } from "lucide-react" import type { Terminal } from "xterm" import type { FitAddon } from "xterm-addon-fit" 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 } export function HybridScriptMonitor({ sessionId, title = "Script Execution", description = "Monitoring script execution...", onClose, onComplete, }: HybridScriptMonitorProps) { 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 terminalRef = useRef(null) const xtermRef = useRef(null) const fitAddonRef = useRef(null) const pollingIntervalRef = useRef(null) const decodeBase64 = (str: string): string => { try { return atob(str) } catch (e) { console.error("[v0] Failed to decode base64:", str, e) return str } } useEffect(() => { if (!terminalRef.current || xtermRef.current || typeof window === "undefined") return const loadTerminal = async () => { const { Terminal } = await import("xterm") const { FitAddon } = await import("xterm-addon-fit") await import("xterm/css/xterm.css") const term = new Terminal({ cursorBlink: false, fontSize: 13, fontFamily: 'Menlo, Monaco, "Courier New", monospace', theme: { background: "#1e1e1e", foreground: "#d4d4d4", cursor: "#d4d4d4", black: "#000000", red: "#cd3131", green: "#0dbc79", yellow: "#e5e510", blue: "#2472c8", magenta: "#bc3fbc", cyan: "#11a8cd", white: "#e5e5e5", brightBlack: "#666666", brightRed: "#f14c4c", brightGreen: "#23d18b", brightYellow: "#f5f543", brightBlue: "#3b8eea", brightMagenta: "#d670d6", brightCyan: "#29b8db", brightWhite: "#ffffff", }, convertEol: true, disableStdin: true, }) const fitAddon = new FitAddon() term.loadAddon(fitAddon) term.open(terminalRef.current!) fitAddon.fit() xtermRef.current = term fitAddonRef.current = fitAddon const resizeObserver = new ResizeObserver(() => { fitAddon.fit() }) resizeObserver.observe(terminalRef.current!) return () => { resizeObserver.disconnect() term.dispose() xtermRef.current = null fitAddonRef.current = null } } loadTerminal() }, []) useEffect(() => { if (!sessionId || !xtermRef.current) return const term = xtermRef.current term.writeln("\x1b[32m[INFO] Conectando al stream de logs...\x1b[0m") const eventSourceUrl = getApiUrl(`/api/scripts/logs/${sessionId}`) const eventSource = new EventSource(eventSourceUrl) eventSource.onopen = () => { term.writeln("\x1b[32m[INFO] Conexión establecida con el servidor\x1b[0m") } eventSource.onmessage = (event) => { try { const data = JSON.parse(event.data) if (data.type === "init") { term.writeln(`\x1b[36m[INICIO] Ejecutando: ${data.script}\x1b[0m`) term.writeln(`\x1b[36m[INICIO] Session ID: ${data.session_id}\x1b[0m`) term.writeln("") } else if (data.type === "raw") { const message = data.message // Detectar WEB_INTERACTION y mostrar modal, pero NO escribir en terminal 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(":") setInteraction({ type: type as ScriptInteraction["type"], id, title: decodeBase64(titleB64), text: decodeBase64(textB64), data: dataB64 ? decodeBase64(dataB64) : undefined, }) } } } else { term.writeln(message) } } else if (data.type === "error") { term.writeln(`\x1b[31m[ERROR] ${data.message}\x1b[0m`) } } catch (e) { term.writeln(`\x1b[31m[PARSE ERROR] ${event.data.substring(0, 100)}\x1b[0m`) } } eventSource.onerror = () => { term.writeln("\x1b[31m[ERROR] Conexión perdida, reintentando...\x1b[0m") } const pollStatus = async () => { try { const statusData = await fetchApi(`/api/scripts/status/${sessionId}`) if (statusData.status === "completed" || statusData.exit_code === 0) { term.writeln("") term.writeln("\x1b[32m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m") term.writeln("\x1b[32m✓ Script completado exitosamente\x1b[0m") term.writeln("\x1b[32m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m") setStatus("completed") eventSource.close() if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current) } onComplete?.(true) } else if (statusData.status === "failed" || (statusData.exit_code !== null && statusData.exit_code !== 0)) { term.writeln("") term.writeln("\x1b[31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m") term.writeln(`\x1b[31m✗ Script falló con código de salida: ${statusData.exit_code}\x1b[0m`) term.writeln("\x1b[31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m") setStatus("failed") eventSource.close() if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current) } onComplete?.(false) } // Detectar interacciones pendientes desde el status 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 () => { eventSource.close() if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current) } } }, [sessionId, onComplete]) const handleInteractionResponse = async (response: string) => { if (!interaction || !sessionId || !xtermRef.current) return const term = xtermRef.current setIsResponding(true) try { term.writeln(`\x1b[33m[USUARIO] Respuesta: ${response}\x1b[0m`) await fetchApi("/api/scripts/respond", { method: "POST", body: JSON.stringify({ session_id: sessionId, interaction_id: interaction.id, value: response, }), }) setInteraction(null) setInputValue("") setSelectedMenuItem("") } catch (error) { term.writeln(`\x1b[31m[ERROR] Error enviando respuesta: ${error}\x1b[0m`) } 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 || "Introduce el valor..."} autoFocus />
) case "menu": const menuItems = interaction.data?.split("|").filter(Boolean) || [] return ( {}}> {interaction.title} {interaction.text}
{menuItems.map((item, index) => { const [value, label] = item.includes(":") ? item.split(":") : [item, item] return ( ) })}
) default: return null } } if (!sessionId) return null return ( <> {status === "running" && } {status === "completed" && } {status === "failed" && } {title} {description}
Session ID: {sessionId}
{status !== "running" && ( )}
{renderInteractionModal()} ) }