2025-12-01 01:04:31 +01:00
|
|
|
"use client"
|
|
|
|
|
|
2025-12-06 18:36:34 +01:00
|
|
|
import type React from "react"
|
2025-12-06 23:06:18 +01:00
|
|
|
import { useState, useEffect, useRef, useCallback } from "react"
|
2025-12-10 17:08:41 +01:00
|
|
|
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"
|
2025-12-01 01:04:31 +01:00
|
|
|
import { Button } from "@/components/ui/button"
|
2025-12-06 11:25:27 +01:00
|
|
|
import { Input } from "@/components/ui/input"
|
|
|
|
|
import { Label } from "@/components/ui/label"
|
2025-12-10 17:08:41 +01:00
|
|
|
import { Loader2, Activity, GripHorizontal } from "lucide-react"
|
2025-12-10 21:47:52 +01:00
|
|
|
import "xterm/css/xterm.css"
|
|
|
|
|
import { API_PORT } from "@/lib/api-config"
|
2025-12-01 01:04:31 +01:00
|
|
|
|
2025-12-06 11:25:27 +01:00
|
|
|
interface WebInteraction {
|
2025-12-06 12:25:57 +01:00
|
|
|
type: "yesno" | "menu" | "msgbox" | "input" | "inputbox"
|
2025-12-06 11:25:27 +01:00
|
|
|
id: string
|
|
|
|
|
title: string
|
|
|
|
|
message: string
|
|
|
|
|
options?: Array<{ label: string; value: string }>
|
2025-12-06 12:25:57 +01:00
|
|
|
default?: string
|
2025-12-06 11:25:27 +01:00
|
|
|
}
|
|
|
|
|
|
2025-12-01 01:04:31 +01:00
|
|
|
interface ScriptTerminalModalProps {
|
|
|
|
|
open: boolean
|
|
|
|
|
onClose: () => void
|
|
|
|
|
scriptPath: string
|
2025-12-10 20:17:13 +01:00
|
|
|
scriptName: string
|
2025-12-10 21:47:52 +01:00
|
|
|
scriptDescription?: string
|
2025-12-10 20:17:13 +01:00
|
|
|
title: string
|
|
|
|
|
description: string
|
2025-12-01 01:04:31 +01:00
|
|
|
}
|
|
|
|
|
|
2025-12-10 17:08:41 +01:00
|
|
|
export function ScriptTerminalModal({
|
|
|
|
|
open: isOpen,
|
2025-12-01 01:04:31 +01:00
|
|
|
onClose,
|
|
|
|
|
scriptPath,
|
2025-12-10 20:17:13 +01:00
|
|
|
scriptName,
|
2025-12-10 21:47:52 +01:00
|
|
|
scriptDescription,
|
2025-12-10 20:17:13 +01:00
|
|
|
title,
|
|
|
|
|
description,
|
2025-12-01 01:04:31 +01:00
|
|
|
}: ScriptTerminalModalProps) {
|
2025-12-10 17:08:41 +01:00
|
|
|
const termRef = useRef<any>(null)
|
2025-12-06 23:06:18 +01:00
|
|
|
const wsRef = useRef<WebSocket | null>(null)
|
2025-12-10 17:08:41 +01:00
|
|
|
const fitAddonRef = useRef<any>(null)
|
2025-12-06 23:06:18 +01:00
|
|
|
const sessionIdRef = useRef<string>(Math.random().toString(36).substring(2, 8))
|
|
|
|
|
|
2025-12-10 21:47:52 +01:00
|
|
|
const [connectionStatus, setConnectionStatus] = useState<"connecting" | "online" | "offline">("connecting")
|
2025-12-10 17:08:41 +01:00
|
|
|
const [isComplete, setIsComplete] = useState(false)
|
|
|
|
|
const [exitCode, setExitCode] = useState<number | null>(null)
|
2025-12-06 11:25:27 +01:00
|
|
|
const [currentInteraction, setCurrentInteraction] = useState<WebInteraction | null>(null)
|
2025-12-10 17:08:41 +01:00
|
|
|
const [interactionInput, setInteractionInput] = useState("")
|
|
|
|
|
const checkConnectionInterval = useRef<NodeJS.Timeout | null>(null)
|
2025-12-10 18:54:35 +01:00
|
|
|
const [isMobile, setIsMobile] = useState(false)
|
|
|
|
|
const [isTablet, setIsTablet] = useState(false)
|
2025-12-10 17:08:41 +01:00
|
|
|
|
2025-12-06 23:06:18 +01:00
|
|
|
const [isWaitingNextInteraction, setIsWaitingNextInteraction] = useState(false)
|
2025-12-10 17:08:41 +01:00
|
|
|
const waitingTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
2025-12-06 23:06:18 +01:00
|
|
|
|
2025-12-10 17:08:41 +01:00
|
|
|
const [modalHeight, setModalHeight] = useState(600)
|
2025-12-06 18:36:34 +01:00
|
|
|
const [isResizing, setIsResizing] = useState(false)
|
2025-12-06 23:25:35 +01:00
|
|
|
const resizeHandlersRef = useRef<{
|
2025-12-10 18:36:31 +01:00
|
|
|
handleMouseMove: ((e: MouseEvent) => void) | null
|
|
|
|
|
handleMouseUp: (() => void) | null
|
|
|
|
|
handleTouchMove: ((e: TouchEvent) => void) | null
|
|
|
|
|
handleTouchEnd: (() => void) | null
|
|
|
|
|
}>({
|
|
|
|
|
handleMouseMove: null,
|
|
|
|
|
handleMouseUp: null,
|
|
|
|
|
handleTouchMove: null,
|
|
|
|
|
handleTouchEnd: null,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const terminalContainerRef = useRef<HTMLDivElement>(null)
|
|
|
|
|
|
|
|
|
|
const sendKey = useCallback((key: string) => {
|
|
|
|
|
if (!termRef.current) return
|
|
|
|
|
|
|
|
|
|
const keyMap: Record<string, string> = {
|
|
|
|
|
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 () => {
|
|
|
|
|
console.log("[v0] Loading xterm modules...")
|
|
|
|
|
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"),
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
console.log("[v0] Creating terminal instance...")
|
|
|
|
|
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)
|
|
|
|
|
console.log("[v0] Opening terminal in container...")
|
|
|
|
|
if (terminalContainerRef.current) {
|
|
|
|
|
term.open(terminalContainerRef.current)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
termRef.current = term
|
|
|
|
|
fitAddonRef.current = fitAddon
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
try {
|
|
|
|
|
fitAddon.fit()
|
|
|
|
|
console.log("[v0] Terminal fitted, cols:", term.cols, "rows:", term.rows)
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.log("[v0] Fit error:", err)
|
2025-12-06 23:06:18 +01:00
|
|
|
}
|
2025-12-10 18:36:31 +01:00
|
|
|
}, 50)
|
2025-12-06 23:06:18 +01:00
|
|
|
|
2025-12-10 18:36:31 +01:00
|
|
|
const wsUrl = getScriptWebSocketUrl(sessionIdRef.current)
|
|
|
|
|
console.log("[v0] Connecting to WebSocket:", wsUrl)
|
|
|
|
|
const ws = new WebSocket(wsUrl)
|
|
|
|
|
wsRef.current = ws
|
|
|
|
|
|
|
|
|
|
ws.onopen = () => {
|
2025-12-10 21:47:52 +01:00
|
|
|
setConnectionStatus("online")
|
2025-12-10 18:36:31 +01:00
|
|
|
|
|
|
|
|
const initMessage = {
|
|
|
|
|
script_path: scriptPath,
|
|
|
|
|
params: {
|
|
|
|
|
EXECUTION_MODE: "web",
|
|
|
|
|
},
|
|
|
|
|
}
|
2025-12-06 23:06:18 +01:00
|
|
|
|
2025-12-10 18:36:31 +01:00
|
|
|
ws.send(JSON.stringify(initMessage))
|
2025-12-06 23:06:18 +01:00
|
|
|
|
2025-12-10 18:36:31 +01:00
|
|
|
setTimeout(() => {
|
|
|
|
|
try {
|
|
|
|
|
fitAddon.fit()
|
|
|
|
|
const cols = term.cols
|
|
|
|
|
const rows = term.rows
|
|
|
|
|
console.log("[v0] Sending resize:", { cols, rows })
|
|
|
|
|
ws.send(
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
type: "resize",
|
|
|
|
|
cols: cols,
|
|
|
|
|
rows: rows,
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.log("[v0] Resize error:", err)
|
2025-12-06 23:06:18 +01:00
|
|
|
}
|
2025-12-10 18:36:31 +01:00
|
|
|
}, 100)
|
|
|
|
|
}
|
2025-12-06 23:06:18 +01:00
|
|
|
|
2025-12-10 18:36:31 +01:00
|
|
|
ws.onmessage = (event) => {
|
|
|
|
|
console.log("[v0] WebSocket message received:", event.data.substring(0, 100))
|
|
|
|
|
try {
|
|
|
|
|
const msg = JSON.parse(event.data)
|
2025-12-06 23:06:18 +01:00
|
|
|
|
2025-12-10 18:36:31 +01:00
|
|
|
if (msg.type === "web_interaction" && msg.interaction) {
|
|
|
|
|
console.log("[v0] Web interaction detected:", msg.interaction.type)
|
2025-12-06 23:06:18 +01:00
|
|
|
setIsWaitingNextInteraction(false)
|
2025-12-10 17:08:41 +01:00
|
|
|
if (waitingTimeoutRef.current) {
|
|
|
|
|
clearTimeout(waitingTimeoutRef.current)
|
2025-12-06 23:06:18 +01:00
|
|
|
}
|
2025-12-10 18:36:31 +01:00
|
|
|
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
|
2025-12-06 23:06:18 +01:00
|
|
|
}
|
|
|
|
|
|
2025-12-10 18:36:31 +01:00
|
|
|
if (msg.type === "error") {
|
|
|
|
|
console.log("[v0] Error message:", msg.message)
|
|
|
|
|
term.writeln(`\x1b[31m${msg.message}\x1b[0m`)
|
|
|
|
|
return
|
2025-12-06 23:06:18 +01:00
|
|
|
}
|
2025-12-10 18:36:31 +01:00
|
|
|
} catch {
|
|
|
|
|
// Not JSON, es output normal de terminal
|
|
|
|
|
}
|
2025-12-06 23:06:18 +01:00
|
|
|
|
2025-12-10 18:36:31 +01:00
|
|
|
term.write(event.data)
|
2025-12-06 23:06:18 +01:00
|
|
|
|
2025-12-10 18:36:31 +01:00
|
|
|
setIsWaitingNextInteraction(false)
|
|
|
|
|
if (waitingTimeoutRef.current) {
|
|
|
|
|
clearTimeout(waitingTimeoutRef.current)
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-06 23:06:18 +01:00
|
|
|
|
2025-12-10 18:36:31 +01:00
|
|
|
ws.onerror = (error) => {
|
2025-12-10 21:47:52 +01:00
|
|
|
setConnectionStatus("offline")
|
2025-12-10 18:36:31 +01:00
|
|
|
term.writeln("\x1b[31mWebSocket error occurred\x1b[0m")
|
|
|
|
|
}
|
2025-12-06 23:06:18 +01:00
|
|
|
|
2025-12-10 18:36:31 +01:00
|
|
|
ws.onclose = (event) => {
|
2025-12-10 21:47:52 +01:00
|
|
|
setConnectionStatus("offline")
|
2025-12-10 18:36:31 +01:00
|
|
|
term.writeln("\x1b[33mConnection closed\x1b[0m")
|
|
|
|
|
|
|
|
|
|
if (!isComplete) {
|
|
|
|
|
setIsComplete(true)
|
|
|
|
|
setExitCode(event.code === 1000 ? 0 : 1)
|
2025-12-06 23:06:18 +01:00
|
|
|
}
|
2025-12-10 18:36:31 +01:00
|
|
|
}
|
2025-12-06 23:06:18 +01:00
|
|
|
|
2025-12-10 18:36:31 +01:00
|
|
|
term.onData((data) => {
|
|
|
|
|
if (ws.readyState === WebSocket.OPEN) {
|
|
|
|
|
ws.send(data)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
checkConnectionInterval.current = setInterval(() => {
|
|
|
|
|
if (ws) {
|
2025-12-10 21:47:52 +01:00
|
|
|
setConnectionStatus(
|
|
|
|
|
ws.readyState === WebSocket.OPEN
|
|
|
|
|
? "online"
|
|
|
|
|
: ws.readyState === WebSocket.CONNECTING
|
|
|
|
|
? "connecting"
|
|
|
|
|
: "offline",
|
|
|
|
|
)
|
2025-12-10 18:36:31 +01:00
|
|
|
}
|
|
|
|
|
}, 500)
|
|
|
|
|
|
|
|
|
|
let resizeTimeout: NodeJS.Timeout | null = null
|
|
|
|
|
|
|
|
|
|
const resizeObserver = new ResizeObserver(() => {
|
|
|
|
|
if (resizeTimeout) clearTimeout(resizeTimeout)
|
|
|
|
|
resizeTimeout = setTimeout(() => {
|
|
|
|
|
if (fitAddon && term && ws?.readyState === WebSocket.OPEN) {
|
|
|
|
|
try {
|
|
|
|
|
fitAddon.fit()
|
|
|
|
|
ws.send(
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
type: "resize",
|
|
|
|
|
cols: term.cols,
|
|
|
|
|
rows: term.rows,
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
} catch (err) {
|
|
|
|
|
// Ignore
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, 100)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (terminalContainerRef.current) {
|
|
|
|
|
resizeObserver.observe(terminalContainerRef.current)
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-06 23:06:18 +01:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
2025-12-10 17:08:41 +01:00
|
|
|
const savedHeight = localStorage.getItem("scriptModalHeight")
|
|
|
|
|
if (savedHeight) {
|
|
|
|
|
setModalHeight(Number.parseInt(savedHeight, 10))
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-10 18:36:31 +01:00
|
|
|
if (isOpen) {
|
|
|
|
|
initializeTerminal()
|
|
|
|
|
} else {
|
2025-12-10 17:08:41 +01:00
|
|
|
if (checkConnectionInterval.current) {
|
|
|
|
|
clearInterval(checkConnectionInterval.current)
|
|
|
|
|
}
|
|
|
|
|
if (waitingTimeoutRef.current) {
|
|
|
|
|
clearTimeout(waitingTimeoutRef.current)
|
|
|
|
|
}
|
2025-12-06 23:06:18 +01:00
|
|
|
if (wsRef.current) {
|
|
|
|
|
wsRef.current.close()
|
|
|
|
|
wsRef.current = null
|
|
|
|
|
}
|
|
|
|
|
if (termRef.current) {
|
|
|
|
|
termRef.current.dispose()
|
|
|
|
|
termRef.current = null
|
|
|
|
|
}
|
2025-12-10 18:36:31 +01:00
|
|
|
if (resizeHandlersRef.current.handleMouseMove) {
|
|
|
|
|
document.removeEventListener("mousemove", resizeHandlersRef.current.handleMouseMove)
|
|
|
|
|
}
|
|
|
|
|
if (resizeHandlersRef.current.handleMouseUp) {
|
|
|
|
|
document.removeEventListener("mouseup", resizeHandlersRef.current.handleMouseUp)
|
|
|
|
|
}
|
|
|
|
|
if (resizeHandlersRef.current.handleTouchMove) {
|
|
|
|
|
document.removeEventListener("touchmove", resizeHandlersRef.current.handleTouchMove)
|
2025-12-06 23:25:35 +01:00
|
|
|
}
|
2025-12-10 18:36:31 +01:00
|
|
|
if (resizeHandlersRef.current.handleTouchEnd) {
|
|
|
|
|
document.removeEventListener("touchend", resizeHandlersRef.current.handleTouchEnd)
|
|
|
|
|
}
|
|
|
|
|
resizeHandlersRef.current = {
|
|
|
|
|
handleMouseMove: null,
|
|
|
|
|
handleMouseUp: null,
|
|
|
|
|
handleTouchMove: null,
|
|
|
|
|
handleTouchEnd: null,
|
2025-12-06 23:25:35 +01:00
|
|
|
}
|
|
|
|
|
|
2025-12-10 17:08:41 +01:00
|
|
|
sessionIdRef.current = Math.random().toString(36).substring(2, 8)
|
2025-12-06 23:06:18 +01:00
|
|
|
setIsComplete(false)
|
|
|
|
|
setExitCode(null)
|
2025-12-10 17:08:41 +01:00
|
|
|
setInteractionInput("")
|
2025-12-06 23:06:18 +01:00
|
|
|
setCurrentInteraction(null)
|
|
|
|
|
setIsWaitingNextInteraction(false)
|
2025-12-10 21:47:52 +01:00
|
|
|
setConnectionStatus("connecting")
|
2025-12-06 23:06:18 +01:00
|
|
|
}
|
2025-12-10 17:08:41 +01:00
|
|
|
}, [isOpen])
|
2025-12-06 23:06:18 +01:00
|
|
|
|
2025-12-10 18:54:35 +01:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}, [])
|
|
|
|
|
|
2025-12-06 23:06:18 +01:00
|
|
|
const getScriptWebSocketUrl = (sid: string): string => {
|
|
|
|
|
if (typeof window === "undefined") {
|
|
|
|
|
return `ws://localhost:${API_PORT}/ws/script/${sid}`
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-10 18:36:31 +01:00
|
|
|
const { protocol, hostname, port } = window.location
|
|
|
|
|
const isStandardPort = port === "" || port === "80" || port === "443"
|
2025-12-06 23:06:18 +01:00
|
|
|
const wsProtocol = protocol === "https:" ? "wss:" : "ws:"
|
2025-12-10 18:36:31 +01:00
|
|
|
|
|
|
|
|
if (isStandardPort) {
|
|
|
|
|
// When using standard port (proxy scenario), don't add port number
|
|
|
|
|
return `${wsProtocol}//${hostname}/ws/script/${sid}`
|
|
|
|
|
} else {
|
|
|
|
|
// Development or custom port, use API_PORT
|
|
|
|
|
return `${wsProtocol}//${hostname}:${API_PORT}/ws/script/${sid}`
|
|
|
|
|
}
|
2025-12-01 01:15:19 +01:00
|
|
|
}
|
|
|
|
|
|
2025-12-06 11:25:27 +01:00
|
|
|
const handleInteractionResponse = (value: string) => {
|
2025-12-06 23:06:18 +01:00
|
|
|
if (!wsRef.current || !currentInteraction) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 19:03:19 +01:00
|
|
|
if (value === "cancel" || value === "") {
|
|
|
|
|
setCurrentInteraction(null)
|
2025-12-10 17:08:41 +01:00
|
|
|
setInteractionInput("")
|
2025-12-06 23:06:18 +01:00
|
|
|
handleCloseModal()
|
2025-12-06 19:03:19 +01:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 23:06:18 +01:00
|
|
|
const response = JSON.stringify({
|
|
|
|
|
type: "interaction_response",
|
|
|
|
|
id: currentInteraction.id,
|
|
|
|
|
value: value,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (wsRef.current.readyState === WebSocket.OPEN) {
|
|
|
|
|
wsRef.current.send(response)
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 11:25:27 +01:00
|
|
|
setCurrentInteraction(null)
|
2025-12-10 17:08:41 +01:00
|
|
|
setInteractionInput("")
|
2025-12-06 23:06:18 +01:00
|
|
|
|
2025-12-10 17:08:41 +01:00
|
|
|
waitingTimeoutRef.current = setTimeout(() => {
|
|
|
|
|
setIsWaitingNextInteraction(true)
|
|
|
|
|
}, 50)
|
2025-12-06 23:06:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleCloseModal = () => {
|
|
|
|
|
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
|
|
|
|
wsRef.current.close()
|
|
|
|
|
}
|
2025-12-10 17:08:41 +01:00
|
|
|
if (checkConnectionInterval.current) {
|
|
|
|
|
clearInterval(checkConnectionInterval.current)
|
|
|
|
|
}
|
2025-12-06 23:06:18 +01:00
|
|
|
if (termRef.current) {
|
|
|
|
|
termRef.current.dispose()
|
|
|
|
|
}
|
|
|
|
|
onClose()
|
2025-12-06 18:36:34 +01:00
|
|
|
}
|
|
|
|
|
|
2025-12-06 22:20:34 +01:00
|
|
|
const handleResizeStart = (e: React.MouseEvent | React.TouchEvent) => {
|
2025-12-06 23:25:35 +01:00
|
|
|
e.preventDefault()
|
|
|
|
|
e.stopPropagation()
|
|
|
|
|
|
2025-12-06 22:20:34 +01:00
|
|
|
setIsResizing(true)
|
2025-12-06 23:25:35 +01:00
|
|
|
const startY = "clientY" in e ? e.clientY : e.touches[0].clientY
|
|
|
|
|
const startHeight = modalHeight
|
2025-12-06 22:20:34 +01:00
|
|
|
|
2025-12-06 23:25:35 +01:00
|
|
|
const handleMove = (moveEvent: MouseEvent | TouchEvent) => {
|
|
|
|
|
const currentY = moveEvent instanceof MouseEvent ? moveEvent.clientY : moveEvent.touches[0].clientY
|
|
|
|
|
const deltaY = currentY - startY
|
2025-12-10 16:50:52 +01:00
|
|
|
const newHeight = Math.max(300, Math.min(2400, startHeight + deltaY))
|
2025-12-06 23:25:35 +01:00
|
|
|
|
|
|
|
|
setModalHeight(newHeight)
|
|
|
|
|
|
|
|
|
|
if (fitAddonRef.current && termRef.current && wsRef.current?.readyState === WebSocket.OPEN) {
|
|
|
|
|
try {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
fitAddonRef.current.fit()
|
|
|
|
|
wsRef.current?.send(
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
type: "resize",
|
|
|
|
|
cols: termRef.current.cols,
|
|
|
|
|
rows: termRef.current.rows,
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
}, 10)
|
|
|
|
|
} catch (err) {
|
|
|
|
|
// Ignore
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleEnd = () => {
|
|
|
|
|
setIsResizing(false)
|
|
|
|
|
|
2025-12-10 17:08:41 +01:00
|
|
|
localStorage.setItem("scriptModalHeight", modalHeight.toString())
|
|
|
|
|
|
2025-12-06 23:25:35 +01:00
|
|
|
if (fitAddonRef.current && termRef.current && wsRef.current?.readyState === WebSocket.OPEN) {
|
|
|
|
|
try {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
fitAddonRef.current.fit()
|
|
|
|
|
wsRef.current?.send(
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
type: "resize",
|
|
|
|
|
cols: termRef.current.cols,
|
|
|
|
|
rows: termRef.current.rows,
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
}, 50)
|
|
|
|
|
} catch (err) {
|
|
|
|
|
// Ignore
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-10 18:36:31 +01:00
|
|
|
document.removeEventListener("mousemove", handleMove)
|
|
|
|
|
document.removeEventListener("touchmove", handleMove)
|
2025-12-06 23:25:35 +01:00
|
|
|
document.removeEventListener("mouseup", handleEnd)
|
|
|
|
|
document.removeEventListener("touchend", handleEnd)
|
|
|
|
|
|
2025-12-10 18:36:31 +01:00
|
|
|
resizeHandlersRef.current = {
|
|
|
|
|
handleMouseMove: null,
|
|
|
|
|
handleMouseUp: null,
|
|
|
|
|
handleTouchMove: null,
|
|
|
|
|
handleTouchEnd: null,
|
|
|
|
|
}
|
2025-12-06 23:25:35 +01:00
|
|
|
}
|
|
|
|
|
|
2025-12-10 18:36:31 +01:00
|
|
|
resizeHandlersRef.current = {
|
|
|
|
|
handleMouseMove: handleMove as any,
|
|
|
|
|
handleMouseUp: handleEnd,
|
|
|
|
|
handleTouchMove: handleMove as any,
|
|
|
|
|
handleTouchEnd: handleEnd,
|
|
|
|
|
}
|
2025-12-06 22:20:34 +01:00
|
|
|
|
2025-12-06 23:25:35 +01:00
|
|
|
document.addEventListener("mousemove", handleMove as any)
|
|
|
|
|
document.addEventListener("touchmove", handleMove as any, { passive: false })
|
|
|
|
|
document.addEventListener("mouseup", handleEnd)
|
|
|
|
|
document.addEventListener("touchend", handleEnd)
|
2025-12-06 22:20:34 +01:00
|
|
|
}
|
|
|
|
|
|
2025-12-01 01:04:31 +01:00
|
|
|
return (
|
|
|
|
|
<>
|
2025-12-10 17:08:41 +01:00
|
|
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
2025-12-06 18:36:34 +01:00
|
|
|
<DialogContent
|
2025-12-10 17:08:41 +01:00
|
|
|
className="max-w-7xl p-0 flex flex-col gap-0 overflow-hidden"
|
2025-12-10 18:54:35 +01:00
|
|
|
style={{ height: isMobile || isTablet ? "80vh" : `${modalHeight}px`, maxHeight: "none" }}
|
2025-12-06 18:36:34 +01:00
|
|
|
onInteractOutside={(e) => e.preventDefault()}
|
|
|
|
|
onEscapeKeyDown={(e) => e.preventDefault()}
|
|
|
|
|
>
|
2025-12-10 20:17:13 +01:00
|
|
|
<DialogTitle className="sr-only">{title}</DialogTitle>
|
2025-12-10 17:08:41 +01:00
|
|
|
|
|
|
|
|
<div className="flex items-center gap-2 p-4 border-b">
|
|
|
|
|
<div>
|
2025-12-10 20:17:13 +01:00
|
|
|
<h2 className="text-lg font-semibold">{title}</h2>
|
|
|
|
|
{description && <p className="text-sm text-muted-foreground">{description}</p>}
|
2025-12-10 17:08:41 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-12-01 01:04:31 +01:00
|
|
|
|
2025-12-06 23:06:18 +01:00
|
|
|
<div className="overflow-hidden relative flex-1">
|
|
|
|
|
<div ref={terminalContainerRef} className="w-full h-full" />
|
|
|
|
|
|
|
|
|
|
{isWaitingNextInteraction && !currentInteraction && (
|
|
|
|
|
<div className="absolute inset-0 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
|
|
|
|
<div className="flex flex-col items-center gap-3">
|
|
|
|
|
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
|
|
|
|
|
<p className="text-sm text-muted-foreground">Processing...</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-12-01 01:15:19 +01:00
|
|
|
</div>
|
2025-12-01 01:04:31 +01:00
|
|
|
|
2025-12-10 18:54:35 +01:00
|
|
|
{(isMobile || isTablet) && (
|
|
|
|
|
<div className="flex flex-wrap gap-1.5 justify-center items-center px-1 bg-zinc-900 text-sm rounded-b-md border-t border-zinc-700 py-1.5">
|
|
|
|
|
<Button
|
|
|
|
|
onClick={() => sendKey("escape")}
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="px-2.5 py-2 text-xs h-9 bg-zinc-800 hover:bg-zinc-700 border-zinc-700"
|
|
|
|
|
>
|
|
|
|
|
ESC
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
onClick={() => sendKey("tab")}
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="px-2.5 py-2 text-xs h-9 bg-zinc-800 hover:bg-zinc-700 border-zinc-700"
|
|
|
|
|
>
|
|
|
|
|
TAB
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
onClick={() => sendKey("up")}
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="px-3 py-2 text-base h-9 bg-zinc-800 hover:bg-zinc-700 border-zinc-700"
|
|
|
|
|
>
|
|
|
|
|
↑
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
onClick={() => sendKey("down")}
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="px-3 py-2 text-base h-9 bg-zinc-800 hover:bg-zinc-700 border-zinc-700"
|
|
|
|
|
>
|
|
|
|
|
↓
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
onClick={() => sendKey("left")}
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="px-3 py-2 text-base h-9 bg-zinc-800 hover:bg-zinc-700 border-zinc-700"
|
|
|
|
|
>
|
|
|
|
|
←
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
onClick={() => sendKey("right")}
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="px-3 py-2 text-base h-9 bg-zinc-800 hover:bg-zinc-700 border-zinc-700"
|
|
|
|
|
>
|
|
|
|
|
→
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
onClick={() => sendKey("enter")}
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="px-3 py-2 text-base h-9 bg-zinc-800 hover:bg-zinc-700 border-zinc-700"
|
|
|
|
|
>
|
|
|
|
|
↵
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
onClick={() => sendKey("ctrlc")}
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="px-2 py-2 text-xs h-9 bg-zinc-800 hover:bg-zinc-700 border-zinc-700"
|
|
|
|
|
>
|
|
|
|
|
CTRL+C
|
|
|
|
|
</Button>
|
2025-12-10 18:36:31 +01:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2025-12-10 20:12:03 +01:00
|
|
|
{(isTablet || (!isMobile && !isTablet)) && (
|
|
|
|
|
<div
|
|
|
|
|
className={`h-2 cursor-ns-resize flex items-center justify-center transition-all duration-150 ${
|
|
|
|
|
isResizing ? "bg-blue-500 h-3" : "bg-zinc-800 hover:bg-blue-500/50"
|
|
|
|
|
}`}
|
|
|
|
|
onMouseDown={handleResizeStart}
|
|
|
|
|
onTouchStart={handleResizeStart}
|
|
|
|
|
style={{ touchAction: "none" }}
|
|
|
|
|
>
|
|
|
|
|
<GripHorizontal
|
|
|
|
|
className={`h-4 w-4 transition-all duration-150 ${isResizing ? "text-white scale-110" : "text-zinc-500"}`}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center justify-between p-4 border-t">
|
2025-12-06 23:06:18 +01:00
|
|
|
<div className="flex items-center gap-3">
|
2025-12-10 17:08:41 +01:00
|
|
|
<Activity className="h-5 w-5 text-blue-500" />
|
|
|
|
|
<div
|
2025-12-10 21:47:52 +01:00
|
|
|
className={`w-2 h-2 rounded-full ${
|
|
|
|
|
connectionStatus === "online"
|
|
|
|
|
? "bg-green-500"
|
|
|
|
|
: connectionStatus === "connecting"
|
|
|
|
|
? "bg-blue-500"
|
|
|
|
|
: "bg-red-500"
|
|
|
|
|
}`}
|
|
|
|
|
title={
|
|
|
|
|
connectionStatus === "online"
|
|
|
|
|
? "Connected"
|
|
|
|
|
: connectionStatus === "connecting"
|
|
|
|
|
? "Connecting"
|
|
|
|
|
: "Disconnected"
|
|
|
|
|
}
|
2025-12-10 17:08:41 +01:00
|
|
|
></div>
|
2025-12-10 21:47:52 +01:00
|
|
|
<span className="text-xs text-muted-foreground">
|
|
|
|
|
{connectionStatus === "online"
|
|
|
|
|
? "Online"
|
|
|
|
|
: connectionStatus === "connecting"
|
|
|
|
|
? "Connecting..."
|
|
|
|
|
: "Offline"}
|
|
|
|
|
</span>
|
2025-12-06 23:06:18 +01:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Button
|
|
|
|
|
onClick={handleCloseModal}
|
|
|
|
|
variant="outline"
|
|
|
|
|
className="bg-red-600 hover:bg-red-700 border-red-500 text-white"
|
|
|
|
|
>
|
|
|
|
|
Close
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
2025-12-01 01:04:31 +01:00
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
2025-12-06 11:25:27 +01:00
|
|
|
|
2025-12-10 17:08:41 +01:00
|
|
|
{currentInteraction && (
|
2025-12-06 13:54:37 +01:00
|
|
|
<Dialog open={true}>
|
2025-12-06 13:11:04 +01:00
|
|
|
<DialogContent
|
2025-12-06 23:25:35 +01:00
|
|
|
className="max-w-4xl max-h-[80vh] overflow-y-auto animate-in fade-in-0 zoom-in-95 duration-100"
|
2025-12-06 13:54:37 +01:00
|
|
|
onInteractOutside={(e) => e.preventDefault()}
|
|
|
|
|
onEscapeKeyDown={(e) => e.preventDefault()}
|
2025-12-06 19:03:19 +01:00
|
|
|
hideClose
|
2025-12-06 13:11:04 +01:00
|
|
|
>
|
2025-12-06 11:25:27 +01:00
|
|
|
<DialogTitle>{currentInteraction.title}</DialogTitle>
|
|
|
|
|
<div className="space-y-4">
|
2025-12-10 17:35:43 +01:00
|
|
|
<p
|
|
|
|
|
className="whitespace-pre-wrap"
|
|
|
|
|
dangerouslySetInnerHTML={{
|
|
|
|
|
__html: currentInteraction.message.replace(/\\n/g, "<br/>").replace(/\n/g, "<br/>"),
|
|
|
|
|
}}
|
|
|
|
|
/>
|
2025-12-06 11:25:27 +01:00
|
|
|
|
|
|
|
|
{currentInteraction.type === "yesno" && (
|
|
|
|
|
<div className="flex gap-2">
|
2025-12-06 13:00:29 +01:00
|
|
|
<Button
|
|
|
|
|
onClick={() => handleInteractionResponse("yes")}
|
2025-12-06 23:25:35 +01:00
|
|
|
className="flex-1 bg-blue-600 hover:bg-blue-700 text-white transition-all duration-150"
|
2025-12-06 13:00:29 +01:00
|
|
|
>
|
2025-12-06 11:25:27 +01:00
|
|
|
Yes
|
|
|
|
|
</Button>
|
2025-12-06 19:03:19 +01:00
|
|
|
<Button
|
|
|
|
|
onClick={() => handleInteractionResponse("cancel")}
|
|
|
|
|
variant="outline"
|
2025-12-06 23:25:35 +01:00
|
|
|
className="flex-1 hover:bg-red-600 hover:text-white hover:border-red-600 transition-all duration-150"
|
2025-12-06 19:03:19 +01:00
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
2025-12-06 11:25:27 +01:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{currentInteraction.type === "menu" && currentInteraction.options && (
|
|
|
|
|
<div className="space-y-2">
|
2025-12-06 23:25:35 +01:00
|
|
|
{currentInteraction.options.map((option, index) => (
|
2025-12-06 11:25:27 +01:00
|
|
|
<Button
|
|
|
|
|
key={option.value}
|
|
|
|
|
onClick={() => handleInteractionResponse(option.value)}
|
|
|
|
|
variant="outline"
|
2025-12-06 23:25:35 +01:00
|
|
|
className="w-full justify-start hover:bg-blue-600 hover:text-white transition-all duration-100 animate-in fade-in-0 slide-in-from-left-2"
|
|
|
|
|
style={{ animationDelay: `${index * 30}ms` }}
|
2025-12-06 11:25:27 +01:00
|
|
|
>
|
|
|
|
|
{option.label}
|
|
|
|
|
</Button>
|
|
|
|
|
))}
|
2025-12-06 18:36:34 +01:00
|
|
|
<Button
|
|
|
|
|
onClick={() => handleInteractionResponse("cancel")}
|
|
|
|
|
variant="outline"
|
2025-12-06 23:25:35 +01:00
|
|
|
className="w-full hover:bg-red-600 hover:text-white hover:border-red-600 transition-all duration-150"
|
2025-12-06 18:36:34 +01:00
|
|
|
>
|
2025-12-06 19:03:19 +01:00
|
|
|
Cancel
|
2025-12-06 18:36:34 +01:00
|
|
|
</Button>
|
2025-12-06 11:25:27 +01:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2025-12-06 13:54:37 +01:00
|
|
|
{(currentInteraction.type === "input" || currentInteraction.type === "inputbox") && (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label>Your input:</Label>
|
|
|
|
|
<Input
|
2025-12-10 17:08:41 +01:00
|
|
|
value={interactionInput}
|
|
|
|
|
onChange={(e) => setInteractionInput(e.target.value)}
|
2025-12-06 13:54:37 +01:00
|
|
|
onKeyDown={(e) => {
|
|
|
|
|
if (e.key === "Enter") {
|
2025-12-10 17:08:41 +01:00
|
|
|
handleInteractionResponse(interactionInput)
|
2025-12-06 13:54:37 +01:00
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
placeholder={currentInteraction.default || ""}
|
2025-12-06 23:25:35 +01:00
|
|
|
className="transition-all duration-150"
|
2025-12-06 13:54:37 +01:00
|
|
|
/>
|
2025-12-10 17:08:41 +01:00
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<Button
|
|
|
|
|
onClick={() => handleInteractionResponse(interactionInput)}
|
|
|
|
|
className="flex-1 bg-blue-600 hover:bg-blue-700 transition-all duration-150"
|
|
|
|
|
>
|
|
|
|
|
Submit
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
onClick={() => handleInteractionResponse("cancel")}
|
|
|
|
|
variant="outline"
|
|
|
|
|
className="flex-1 hover:bg-red-600 hover:text-white hover:border-red-600 transition-all duration-150"
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
2025-12-06 13:54:37 +01:00
|
|
|
</div>
|
|
|
|
|
)}
|
2025-12-06 11:25:27 +01:00
|
|
|
|
|
|
|
|
{currentInteraction.type === "msgbox" && (
|
2025-12-06 19:03:19 +01:00
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<Button
|
|
|
|
|
onClick={() => handleInteractionResponse("ok")}
|
2025-12-06 23:25:35 +01:00
|
|
|
className="flex-1 bg-blue-600 hover:bg-blue-700 transition-all duration-150"
|
2025-12-06 19:03:19 +01:00
|
|
|
>
|
|
|
|
|
OK
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
onClick={() => handleInteractionResponse("cancel")}
|
|
|
|
|
variant="outline"
|
2025-12-06 23:25:35 +01:00
|
|
|
className="flex-1 hover:bg-red-600 hover:text-white hover:border-red-600 transition-all duration-150"
|
2025-12-06 19:03:19 +01:00
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
2025-12-06 11:25:27 +01:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
)}
|
2025-12-01 01:04:31 +01:00
|
|
|
</>
|
|
|
|
|
)
|
|
|
|
|
}
|