diff --git a/AppImage/components/hybrid-script-monitor.tsx b/AppImage/components/hybrid-script-monitor.tsx index b1c59f2..8e87b66 100644 --- a/AppImage/components/hybrid-script-monitor.tsx +++ b/AppImage/components/hybrid-script-monitor.tsx @@ -7,7 +7,11 @@ 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" +import { Loader2, CheckCircle2, XCircle, TerminalIcon } from "lucide-react" + +import { Terminal } from "xterm" +import { FitAddon } from "xterm-addon-fit" +import "xterm/css/xterm.css" interface HybridScriptMonitorProps { sessionId: string | null @@ -25,12 +29,6 @@ interface ScriptInteraction { data?: string } -interface LogEntry { - timestamp: string - message: string - type: "info" | "error" | "warning" | "success" -} - export function HybridScriptMonitor({ sessionId, title = "Script Execution", @@ -38,17 +36,16 @@ export function HybridScriptMonitor({ 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 terminalRef = useRef(null) + const xtermRef = useRef(null) + const fitAddonRef = useRef(null) const pollingIntervalRef = useRef(null) - const lastLogPositionRef = useRef(0) const decodeBase64 = (str: string): string => { try { @@ -60,61 +57,84 @@ export function HybridScriptMonitor({ } useEffect(() => { - if (scrollRef.current) { - scrollRef.current.scrollTop = scrollRef.current.scrollHeight + if (!terminalRef.current || xtermRef.current) return + + 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, // Terminal es solo lectura + }) + + const fitAddon = new FitAddon() + term.loadAddon(fitAddon) + term.open(terminalRef.current) + fitAddon.fit() + + xtermRef.current = term + fitAddonRef.current = fitAddon + + // Ajustar terminal cuando cambia el tamaño + const resizeObserver = new ResizeObserver(() => { + fitAddon.fit() + }) + resizeObserver.observe(terminalRef.current) + + return () => { + resizeObserver.disconnect() + term.dispose() + xtermRef.current = null + fitAddonRef.current = null } - }, [logs]) + }, []) useEffect(() => { - if (!sessionId) return + if (!sessionId || !xtermRef.current) return + + const term = xtermRef.current + term.writeln("\x1b[32m[INFO] Conectando al stream de logs...\x1b[0m") - 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", - }, - ]) + term.writeln("\x1b[32m[INFO] Conexión establecida con el servidor\x1b[0m") } eventSource.onmessage = (event) => { - setLastEventTime(new Date()) - - console.log("[v0] RAW SSE event.data:", event.data) - console.log("[v0] RAW SSE event.data type:", typeof event.data) - console.log("[v0] RAW SSE event.data length:", event.data.length) - try { const data = JSON.parse(event.data) - console.log("[v0] Parsed SSE event:", data) - console.log("[v0] Event type:", data.type) - console.log("[v0] Event keys:", Object.keys(data)) if (data.type === "init") { - console.log("[v0] INIT event - script:", data.script, "session_id:", data.session_id) - setLogs((prev) => [ - ...prev, - { - timestamp: new Date().toLocaleTimeString(), - message: `Starting script: ${data.script}`, - type: "info", - }, - ]) + 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 - console.log("[v0] RAW event - message:", message) - console.log("[v0] RAW event - message length:", message?.length) + // Detectar WEB_INTERACTION y mostrar modal, pero NO escribir en terminal if (message.includes("WEB_INTERACTION:")) { const interactionPart = message.split("WEB_INTERACTION:")[1] @@ -125,8 +145,6 @@ export function HybridScriptMonitor({ 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, @@ -137,115 +155,49 @@ export function HybridScriptMonitor({ } } } 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", - }, - ]) + term.writeln(message) } } else if (data.type === "error") { - console.log("[v0] ERROR event - message:", data.message) - setLogs((prev) => [ - ...prev, - { - timestamp: new Date().toLocaleTimeString(), - message: `Error: ${data.message}`, - type: "error", - }, - ]) - } else { - console.warn("[v0] UNKNOWN EVENT TYPE:", data.type) - console.warn("[v0] Full event data:", JSON.stringify(data, null, 2)) - setLogs((prev) => [ - ...prev, - { - timestamp: new Date().toLocaleTimeString(), - message: `[Unknown event type: ${data.type}] ${JSON.stringify(data)}`, - type: "warning", - }, - ]) + term.writeln(`\x1b[31m[ERROR] ${data.message}\x1b[0m`) } } catch (e) { - console.error("[v0] ERROR parsing SSE event:", e) - console.error("[v0] Stack:", (e as Error).stack) - console.error("[v0] Raw event.data that failed to parse:", event.data) - console.error("[v0] First 200 chars:", event.data.substring(0, 200)) - setLogs((prev) => [ - ...prev, - { - timestamp: new Date().toLocaleTimeString(), - message: `Parse error: ${event.data.substring(0, 100)}`, - type: "error", - }, - ]) + term.writeln(`\x1b[31m[PARSE ERROR] ${event.data.substring(0, 100)}\x1b[0m`) } } - 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", - }, - ]) + 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}`) - console.log("[v0] ==> Status poll result:", JSON.stringify(statusData, null, 2)) - console.log("[v0] ==> Status:", statusData.status) - console.log("[v0] ==> Exit code:", statusData.exit_code) - console.log("[v0] ==> Pending interaction:", statusData.pending_interaction) - - 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") + 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() - 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) + 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() - setEventSourceState("closed") 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) { @@ -270,25 +222,21 @@ export function HybridScriptMonitor({ 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]) + }, [sessionId, onComplete]) const handleInteractionResponse = async (response: string) => { - if (!interaction || !sessionId) return + if (!interaction || !sessionId || !xtermRef.current) return + const term = xtermRef.current setIsResponding(true) try { - console.log("[v0] Sending interaction response:", { - session_id: sessionId, - interaction_id: interaction.id, - value: response, - }) + term.writeln(`\x1b[33m[USUARIO] Respuesta: ${response}\x1b[0m`) await fetchApi("/api/scripts/respond", { method: "POST", @@ -299,20 +247,11 @@ export function HybridScriptMonitor({ }), }) - 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", - }, - ]) + term.writeln(`\x1b[31m[ERROR] Error enviando respuesta: ${error}\x1b[0m`) } finally { setIsResponding(false) } @@ -325,11 +264,11 @@ export function HybridScriptMonitor({ case "msgbox": return ( {}}> - + {interaction.title} - {interaction.text} + {interaction.text}
@@ -364,24 +303,25 @@ export function HybridScriptMonitor({ case "inputbox": return ( {}}> - + {interaction.title} - {interaction.text} + {interaction.text}
- + setInputValue(e.target.value)} - placeholder={interaction.data || "Enter value..."} + placeholder={interaction.data || "Introduce el valor..."} + autoFocus />
-
+
- ))} + {menuItems.map((item, index) => { + const [value, label] = item.includes(":") ? item.split(":") : [item, item] + return ( + + ) + })}
-
+
@@ -442,58 +385,32 @@ export function HybridScriptMonitor({ 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" && } +
+ Session ID: {sessionId} +
+ {status !== "running" && ( + + )}