"use client" import type React from "react" import { useState, useEffect, useRef, useCallback } from "react" import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Loader2, Activity, ArrowUp, ArrowDown, ArrowLeft, ArrowRight, CornerDownLeft, GripHorizontal, } from "lucide-react" import "xterm/css/xterm.css" import { API_PORT } from "@/lib/api-config" interface WebInteraction { type: "yesno" | "menu" | "msgbox" | "input" | "inputbox" id: string title: string message: string options?: Array<{ label: string; value: string }> default?: string } interface ScriptTerminalModalProps { open: boolean onClose: () => void scriptPath: string scriptName: string scriptDescription?: string title: string description: string } export function ScriptTerminalModal({ open: isOpen, onClose, scriptPath, scriptName, scriptDescription, title, description, }: ScriptTerminalModalProps) { const termRef = useRef(null) const wsRef = useRef(null) const fitAddonRef = useRef(null) const sessionIdRef = useRef(Math.random().toString(36).substring(2, 8)) const [connectionStatus, setConnectionStatus] = useState<"connecting" | "online" | "offline">("connecting") const [isComplete, setIsComplete] = useState(false) const [exitCode, setExitCode] = useState(null) const [currentInteraction, setCurrentInteraction] = useState(null) const [interactionInput, setInteractionInput] = useState("") const checkConnectionInterval = useRef(null) const [isMobile, setIsMobile] = useState(false) const [isTablet, setIsTablet] = useState(false) const [isWaitingNextInteraction, setIsWaitingNextInteraction] = useState(false) const waitingTimeoutRef = useRef(null) const [modalHeight, setModalHeight] = useState(600) const [isResizing, setIsResizing] = useState(false) const resizeBarRef = useRef(null) const modalHeightRef = useRef(600) // Ref para mantener el valor actualizado // Debug visual para tablets const [debugInfo, setDebugInfo] = useState([]) const terminalContainerRef = useRef(null) const sendKey = useCallback((key: string) => { if (!termRef.current) return const keyMap: Record = { escape: "\x1b", tab: "\t", up: "\x1b[A", down: "\x1b[B", left: "\x1b[D", right: "\x1b[C", enter: "\r", ctrlc: "\x03", } const sequence = keyMap[key] if (sequence && wsRef.current?.readyState === WebSocket.OPEN) { wsRef.current.send(JSON.stringify({ type: "input", data: sequence })) } }, []) const initializeTerminal = async () => { const [TerminalClass, FitAddonClass] = await Promise.all([ import("xterm").then((mod) => mod.Terminal), import("xterm-addon-fit").then((mod) => mod.FitAddon), import("xterm/css/xterm.css"), ]) const fontSize = window.innerWidth < 768 ? 12 : 16 const term = new TerminalClass({ rendererType: "dom", fontFamily: '"Courier", "Courier New", "Liberation Mono", "DejaVu Sans Mono", monospace', fontSize: fontSize, lineHeight: 1, cursorBlink: true, scrollback: 2000, disableStdin: false, customGlyphs: true, fontWeight: "500", fontWeightBold: "700", theme: { background: "#000000", foreground: "#ffffff", cursor: "#ffffff", cursorAccent: "#000000", black: "#2e3436", red: "#cc0000", green: "#4e9a06", yellow: "#c4a000", blue: "#3465a4", magenta: "#75507b", cyan: "#06989a", white: "#d3d7cf", brightBlack: "#555753", brightRed: "#ef2929", brightGreen: "#8ae234", brightYellow: "#fce94f", brightBlue: "#729fcf", brightMagenta: "#ad7fa8", brightCyan: "#34e2e2", brightWhite: "#eeeeec", }, }) const fitAddon = new FitAddonClass() term.loadAddon(fitAddon) if (terminalContainerRef.current) { term.open(terminalContainerRef.current) } termRef.current = term fitAddonRef.current = fitAddon setTimeout(() => { if (fitAddonRef.current && termRef.current) { fitAddonRef.current.fit() } }, 100) const wsUrl = getScriptWebSocketUrl(sessionIdRef.current) const ws = new WebSocket(wsUrl) wsRef.current = ws ws.onopen = () => { setConnectionStatus("online") const initMessage = { script_path: scriptPath, params: { EXECUTION_MODE: "web", }, } ws.send(JSON.stringify(initMessage)) setTimeout(() => { if (fitAddonRef.current && termRef.current && ws.readyState === WebSocket.OPEN) { const cols = termRef.current.cols const rows = termRef.current.rows ws.send( JSON.stringify({ type: "resize", cols: cols, rows: rows, }), ) } }, 100) } ws.onmessage = (event) => { try { const msg = JSON.parse(event.data) if (msg.type === "web_interaction" && msg.interaction) { setIsWaitingNextInteraction(false) if (waitingTimeoutRef.current) { clearTimeout(waitingTimeoutRef.current) } setCurrentInteraction({ type: msg.interaction.type, id: msg.interaction.id, title: msg.interaction.title || "", message: msg.interaction.message || "", options: msg.interaction.options, default: msg.interaction.default, }) return } if (msg.type === "error") { term.writeln(`\x1b[31m${msg.message}\x1b[0m`) return } } catch { // Not JSON, es output normal de terminal } term.write(event.data) setIsWaitingNextInteraction(false) if (waitingTimeoutRef.current) { clearTimeout(waitingTimeoutRef.current) } } ws.onerror = (error) => { setConnectionStatus("offline") term.writeln("\x1b[31mWebSocket error occurred\x1b[0m") } ws.onclose = (event) => { setConnectionStatus("offline") term.writeln("\x1b[33mConnection closed\x1b[0m") if (!isComplete) { setIsComplete(true) setExitCode(event.code === 1000 ? 0 : 1) } } term.onData((data) => { if (ws.readyState === WebSocket.OPEN) { ws.send(data) } }) checkConnectionInterval.current = setInterval(() => { if (wsRef.current) { setConnectionStatus( wsRef.current.readyState === WebSocket.OPEN ? "online" : wsRef.current.readyState === WebSocket.CONNECTING ? "connecting" : "offline", ) } }, 500) let resizeTimeout: NodeJS.Timeout | null = null const resizeObserver = new ResizeObserver(() => { if (resizeTimeout) clearTimeout(resizeTimeout) resizeTimeout = setTimeout(() => { if (fitAddonRef.current && termRef.current && wsRef.current?.readyState === WebSocket.OPEN) { fitAddonRef.current.fit() wsRef.current.send( JSON.stringify({ type: "resize", cols: termRef.current.cols, rows: termRef.current.rows, }), ) } }, 100) }) if (terminalContainerRef.current) { resizeObserver.observe(terminalContainerRef.current) } } useEffect(() => { const savedHeight = localStorage.getItem("scriptModalHeight") if (savedHeight) { const height = Number.parseInt(savedHeight, 10) setModalHeight(height) modalHeightRef.current = height // Sincronizar el ref } if (isOpen) { initializeTerminal() } else { if (checkConnectionInterval.current) { clearInterval(checkConnectionInterval.current) } if (waitingTimeoutRef.current) { clearTimeout(waitingTimeoutRef.current) } if (wsRef.current) { wsRef.current.close() wsRef.current = null } if (termRef.current) { termRef.current.dispose() termRef.current = null } sessionIdRef.current = Math.random().toString(36).substring(2, 8) setIsComplete(false) setExitCode(null) setInteractionInput("") setCurrentInteraction(null) setIsWaitingNextInteraction(false) setConnectionStatus("connecting") } }, [isOpen]) useEffect(() => { const updateDeviceType = () => { const width = window.innerWidth const isTouchDevice = "ontouchstart" in window || navigator.maxTouchPoints > 0 const isTabletSize = width >= 768 && width <= 1366 setIsMobile(width < 768) setIsTablet(isTouchDevice && isTabletSize) } updateDeviceType() const handleResize = () => updateDeviceType() window.addEventListener("resize", handleResize) return () => { window.removeEventListener("resize", handleResize) } }, []) const getScriptWebSocketUrl = (sid: string): string => { if (typeof window === "undefined") { return `ws://localhost:${API_PORT}/ws/script/${sid}` } const { protocol, hostname, port } = window.location const isStandardPort = port === "" || port === "80" || port === "443" const wsProtocol = protocol === "https:" ? "wss:" : "ws:" if (isStandardPort) { return `${wsProtocol}//${hostname}/ws/script/${sid}` } else { return `${wsProtocol}//${hostname}:${API_PORT}/ws/script/${sid}` } } const handleInteractionResponse = (value: string) => { if (!wsRef.current || !currentInteraction) { return } if (value === "cancel" || value === "") { setCurrentInteraction(null) setInteractionInput("") handleCloseModal() return } const response = JSON.stringify({ type: "interaction_response", id: currentInteraction.id, value: value, }) if (wsRef.current.readyState === WebSocket.OPEN) { wsRef.current.send(response) } setCurrentInteraction(null) setInteractionInput("") waitingTimeoutRef.current = setTimeout(() => { setIsWaitingNextInteraction(true) }, 50) } const handleCloseModal = () => { if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { wsRef.current.close() } if (checkConnectionInterval.current) { clearInterval(checkConnectionInterval.current) } if (termRef.current) { termRef.current.dispose() } onClose() } const handleResizeStart = (e: React.MouseEvent | React.TouchEvent) => { e.preventDefault() e.stopPropagation() const debugMsg = `[${new Date().toLocaleTimeString()}] Resize start - Type: ${e.type}, isMobile: ${isMobile}, isTablet: ${isTablet}` console.log(debugMsg) setDebugInfo(prev => [...prev.slice(-4), debugMsg]) setIsResizing(true) // Detectar si es touch o mouse const clientY = "touches" in e ? e.touches[0].clientY : e.clientY const startY = clientY const startHeight = modalHeight const debugMsg2 = `Start Y: ${startY}, Start height: ${startHeight}` console.log(debugMsg2) setDebugInfo(prev => [...prev.slice(-4), debugMsg2]) let moveCount = 0 const handleMove = (moveEvent: MouseEvent | TouchEvent) => { moveEvent.preventDefault() moveEvent.stopPropagation() const currentY = "touches" in moveEvent ? moveEvent.touches[0].clientY : moveEvent.clientY const deltaY = currentY - startY const newHeight = Math.max(300, Math.min(window.innerHeight - 100, startHeight + deltaY)) moveCount++ if (moveCount % 5 === 0) { console.log(`Move #${moveCount} - currentY: ${currentY}, deltaY: ${deltaY}, newHeight: ${newHeight}`) } // Actualizar tanto el state como el ref modalHeightRef.current = newHeight setModalHeight(newHeight) if (fitAddonRef.current && termRef.current && wsRef.current?.readyState === WebSocket.OPEN) { setTimeout(() => { if (fitAddonRef.current && termRef.current) { fitAddonRef.current.fit() wsRef.current?.send( JSON.stringify({ type: "resize", cols: termRef.current.cols, rows: termRef.current.rows, }), ) } }, 10) } } const handleEnd = () => { // Usar el ref que tiene el valor actualizado const finalHeight = modalHeightRef.current const debugMsg3 = `Resize end - Final height: ${finalHeight}, Total moves: ${moveCount}` console.log(debugMsg3) setDebugInfo(prev => [...prev.slice(-4), debugMsg3]) setIsResizing(false) document.removeEventListener("mousemove", handleMove as any, false) document.removeEventListener("mouseup", handleEnd, false) document.removeEventListener("touchmove", handleMove as any, false) document.removeEventListener("touchend", handleEnd, false) document.removeEventListener("touchcancel", handleEnd, false) // Guardar usando el valor del ref localStorage.setItem("scriptModalHeight", finalHeight.toString()) } document.addEventListener("mousemove", handleMove as any, false) document.addEventListener("mouseup", handleEnd, false) document.addEventListener("touchmove", handleMove as any, { passive: false, capture: false }) document.addEventListener("touchend", handleEnd, false) document.addEventListener("touchcancel", handleEnd, false) } const sendCommand = (command: string) => { if (wsRef.current?.readyState === WebSocket.OPEN) { wsRef.current.send(JSON.stringify({ type: "input", data: command })) } } return ( <> e.preventDefault()} onEscapeKeyDown={(e) => e.preventDefault()} hideClose > {title}

{title}

{description &&

{description}

}
{/* Debug panel - solo visible en tablets */} {isTablet && debugInfo.length > 0 && (
{debugInfo.map((info, i) => (
{info}
))}
)}
{isWaitingNextInteraction && !currentInteraction && (

Processing...

)}
{/* Resize bar - visible en tablet y escritorio */} {!isMobile && (
)} {/* Mobile/Tablet button toolbar */} {(isMobile || isTablet) && (
)} {/* Footer with connection status and close button */}
{connectionStatus === "online" ? "Online" : connectionStatus === "connecting" ? "Connecting..." : "Offline"}
{currentInteraction && ( e.preventDefault()} onEscapeKeyDown={(e) => e.preventDefault()} hideClose > {currentInteraction.title}

").replace(/\n/g, "
"), }} /> {currentInteraction.type === "yesno" && (

)} {currentInteraction.type === "menu" && currentInteraction.options && (
{currentInteraction.options.map((option, index) => ( ))}
)} {(currentInteraction.type === "input" || currentInteraction.type === "inputbox") && (
setInteractionInput(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { handleInteractionResponse(interactionInput) } }} placeholder={currentInteraction.default || ""} className="transition-all duration-150" />
)} {currentInteraction.type === "msgbox" && (
)}
)} ) }