2025-11-30 22:50:40 +01:00
|
|
|
"use client"
|
|
|
|
|
|
|
|
|
|
import { useEffect, useState, useRef } from "react"
|
2025-11-30 23:47:33 +01:00
|
|
|
import { fetchApi, getApiUrl } from "@/lib/api-config"
|
2025-11-30 22:50:40 +01:00
|
|
|
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"
|
2025-12-01 00:31:23 +01:00
|
|
|
import { Loader2, CheckCircle2, XCircle, TerminalIcon } from "lucide-react"
|
|
|
|
|
|
2025-12-01 00:38:42 +01:00
|
|
|
import type { Terminal } from "xterm"
|
|
|
|
|
import type { FitAddon } from "xterm-addon-fit"
|
2025-11-30 22:50:40 +01:00
|
|
|
|
|
|
|
|
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<ScriptInteraction | null>(null)
|
|
|
|
|
const [status, setStatus] = useState<"running" | "completed" | "failed">("running")
|
|
|
|
|
const [inputValue, setInputValue] = useState("")
|
|
|
|
|
const [selectedMenuItem, setSelectedMenuItem] = useState<string>("")
|
|
|
|
|
const [isResponding, setIsResponding] = useState(false)
|
2025-12-01 00:31:23 +01:00
|
|
|
|
|
|
|
|
const terminalRef = useRef<HTMLDivElement>(null)
|
|
|
|
|
const xtermRef = useRef<Terminal | null>(null)
|
|
|
|
|
const fitAddonRef = useRef<FitAddon | null>(null)
|
2025-11-30 22:50:40 +01:00
|
|
|
const pollingIntervalRef = useRef<NodeJS.Timeout | null>(null)
|
2025-11-30 23:13:37 +01:00
|
|
|
|
|
|
|
|
const decodeBase64 = (str: string): string => {
|
|
|
|
|
try {
|
|
|
|
|
return atob(str)
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error("[v0] Failed to decode base64:", str, e)
|
|
|
|
|
return str
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-30 22:50:40 +01:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
2025-12-01 00:38:42 +01:00
|
|
|
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!)
|
2025-12-01 00:31:23 +01:00
|
|
|
fitAddon.fit()
|
2025-12-01 00:38:42 +01:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
2025-11-30 22:50:40 +01:00
|
|
|
}
|
2025-12-01 00:38:42 +01:00
|
|
|
|
|
|
|
|
loadTerminal()
|
2025-12-01 00:31:23 +01:00
|
|
|
}, [])
|
2025-11-30 22:50:40 +01:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
2025-12-01 00:31:23 +01:00
|
|
|
if (!sessionId || !xtermRef.current) return
|
2025-11-30 22:50:40 +01:00
|
|
|
|
2025-12-01 00:31:23 +01:00
|
|
|
const term = xtermRef.current
|
|
|
|
|
term.writeln("\x1b[32m[INFO] Conectando al stream de logs...\x1b[0m")
|
2025-11-30 23:40:13 +01:00
|
|
|
|
2025-12-01 00:31:23 +01:00
|
|
|
const eventSourceUrl = getApiUrl(`/api/scripts/logs/${sessionId}`)
|
2025-11-30 23:40:13 +01:00
|
|
|
const eventSource = new EventSource(eventSourceUrl)
|
|
|
|
|
|
|
|
|
|
eventSource.onopen = () => {
|
2025-12-01 00:31:23 +01:00
|
|
|
term.writeln("\x1b[32m[INFO] Conexión establecida con el servidor\x1b[0m")
|
2025-11-30 23:40:13 +01:00
|
|
|
}
|
2025-11-30 23:20:34 +01:00
|
|
|
|
|
|
|
|
eventSource.onmessage = (event) => {
|
2025-11-30 22:50:40 +01:00
|
|
|
try {
|
2025-11-30 23:20:34 +01:00
|
|
|
const data = JSON.parse(event.data)
|
|
|
|
|
|
2025-11-30 23:27:32 +01:00
|
|
|
if (data.type === "init") {
|
2025-12-01 00:31:23 +01:00
|
|
|
term.writeln(`\x1b[36m[INICIO] Ejecutando: ${data.script}\x1b[0m`)
|
|
|
|
|
term.writeln(`\x1b[36m[INICIO] Session ID: ${data.session_id}\x1b[0m`)
|
|
|
|
|
term.writeln("")
|
2025-11-30 23:27:32 +01:00
|
|
|
} else if (data.type === "raw") {
|
|
|
|
|
const message = data.message
|
|
|
|
|
|
2025-12-01 00:31:23 +01:00
|
|
|
// Detectar WEB_INTERACTION y mostrar modal, pero NO escribir en terminal
|
2025-11-30 23:27:32 +01:00
|
|
|
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,
|
|
|
|
|
})
|
|
|
|
|
}
|
2025-11-30 23:20:34 +01:00
|
|
|
}
|
2025-11-30 23:27:32 +01:00
|
|
|
} else {
|
2025-12-01 00:31:23 +01:00
|
|
|
term.writeln(message)
|
2025-11-30 23:20:34 +01:00
|
|
|
}
|
2025-11-30 23:27:32 +01:00
|
|
|
} else if (data.type === "error") {
|
2025-12-01 00:31:23 +01:00
|
|
|
term.writeln(`\x1b[31m[ERROR] ${data.message}\x1b[0m`)
|
2025-11-30 23:20:34 +01:00
|
|
|
}
|
|
|
|
|
} catch (e) {
|
2025-12-01 00:31:23 +01:00
|
|
|
term.writeln(`\x1b[31m[PARSE ERROR] ${event.data.substring(0, 100)}\x1b[0m`)
|
2025-11-30 23:20:34 +01:00
|
|
|
}
|
|
|
|
|
}
|
2025-11-30 22:50:40 +01:00
|
|
|
|
2025-12-01 00:31:23 +01:00
|
|
|
eventSource.onerror = () => {
|
|
|
|
|
term.writeln("\x1b[31m[ERROR] Conexión perdida, reintentando...\x1b[0m")
|
2025-11-30 23:20:34 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const pollStatus = async () => {
|
|
|
|
|
try {
|
2025-11-30 22:50:40 +01:00
|
|
|
const statusData = await fetchApi(`/api/scripts/status/${sessionId}`)
|
2025-11-30 23:40:13 +01:00
|
|
|
|
2025-11-30 23:13:37 +01:00
|
|
|
if (statusData.status === "completed" || statusData.exit_code === 0) {
|
2025-12-01 00:31:23 +01:00
|
|
|
term.writeln("")
|
|
|
|
|
term.writeln("\x1b[32m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m")
|
|
|
|
|
term.writeln("\x1b[32m✓ Script completado exitosamente\x1b[0m")
|
|
|
|
|
term.writeln("\x1b[32m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m")
|
2025-11-30 22:50:40 +01:00
|
|
|
setStatus("completed")
|
2025-11-30 23:20:34 +01:00
|
|
|
eventSource.close()
|
2025-11-30 22:50:40 +01:00
|
|
|
if (pollingIntervalRef.current) {
|
|
|
|
|
clearInterval(pollingIntervalRef.current)
|
|
|
|
|
}
|
|
|
|
|
onComplete?.(true)
|
2025-11-30 23:13:37 +01:00
|
|
|
} else if (statusData.status === "failed" || (statusData.exit_code !== null && statusData.exit_code !== 0)) {
|
2025-12-01 00:31:23 +01:00
|
|
|
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")
|
2025-11-30 22:50:40 +01:00
|
|
|
setStatus("failed")
|
2025-11-30 23:20:34 +01:00
|
|
|
eventSource.close()
|
2025-11-30 22:50:40 +01:00
|
|
|
if (pollingIntervalRef.current) {
|
|
|
|
|
clearInterval(pollingIntervalRef.current)
|
|
|
|
|
}
|
|
|
|
|
onComplete?.(false)
|
|
|
|
|
}
|
2025-11-30 23:00:02 +01:00
|
|
|
|
2025-12-01 00:31:23 +01:00
|
|
|
// Detectar interacciones pendientes desde el status
|
2025-11-30 23:13:37 +01:00
|
|
|
if (statusData.pending_interaction) {
|
|
|
|
|
const parts = statusData.pending_interaction.split(":")
|
2025-11-30 23:00:02 +01:00
|
|
|
if (parts.length >= 4) {
|
2025-11-30 23:13:37 +01:00
|
|
|
const [type, id, titleB64, textB64, ...dataParts] = parts
|
|
|
|
|
const dataB64 = dataParts.join(":")
|
2025-11-30 23:00:02 +01:00
|
|
|
|
|
|
|
|
setInteraction({
|
|
|
|
|
type: type as ScriptInteraction["type"],
|
|
|
|
|
id,
|
2025-11-30 23:13:37 +01:00
|
|
|
title: decodeBase64(titleB64),
|
|
|
|
|
text: decodeBase64(textB64),
|
|
|
|
|
data: dataB64 ? decodeBase64(dataB64) : undefined,
|
2025-11-30 23:00:02 +01:00
|
|
|
})
|
2025-11-30 23:13:37 +01:00
|
|
|
}
|
|
|
|
|
}
|
2025-11-30 22:50:40 +01:00
|
|
|
} catch (error) {
|
2025-11-30 23:20:34 +01:00
|
|
|
console.error("[v0] Error polling status:", error)
|
2025-11-30 22:50:40 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-30 23:20:34 +01:00
|
|
|
pollStatus()
|
|
|
|
|
pollingIntervalRef.current = setInterval(pollStatus, 2000)
|
2025-11-30 22:50:40 +01:00
|
|
|
|
|
|
|
|
return () => {
|
2025-11-30 23:20:34 +01:00
|
|
|
eventSource.close()
|
2025-11-30 22:50:40 +01:00
|
|
|
if (pollingIntervalRef.current) {
|
|
|
|
|
clearInterval(pollingIntervalRef.current)
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-01 00:31:23 +01:00
|
|
|
}, [sessionId, onComplete])
|
2025-11-30 22:50:40 +01:00
|
|
|
|
|
|
|
|
const handleInteractionResponse = async (response: string) => {
|
2025-12-01 00:31:23 +01:00
|
|
|
if (!interaction || !sessionId || !xtermRef.current) return
|
2025-11-30 22:50:40 +01:00
|
|
|
|
2025-12-01 00:31:23 +01:00
|
|
|
const term = xtermRef.current
|
2025-11-30 22:50:40 +01:00
|
|
|
setIsResponding(true)
|
|
|
|
|
|
|
|
|
|
try {
|
2025-12-01 00:31:23 +01:00
|
|
|
term.writeln(`\x1b[33m[USUARIO] Respuesta: ${response}\x1b[0m`)
|
2025-11-30 22:50:40 +01:00
|
|
|
|
|
|
|
|
await fetchApi("/api/scripts/respond", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
session_id: sessionId,
|
|
|
|
|
interaction_id: interaction.id,
|
2025-11-30 23:13:37 +01:00
|
|
|
value: response,
|
2025-11-30 22:50:40 +01:00
|
|
|
}),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
setInteraction(null)
|
|
|
|
|
setInputValue("")
|
|
|
|
|
setSelectedMenuItem("")
|
|
|
|
|
} catch (error) {
|
2025-12-01 00:31:23 +01:00
|
|
|
term.writeln(`\x1b[31m[ERROR] Error enviando respuesta: ${error}\x1b[0m`)
|
2025-11-30 22:50:40 +01:00
|
|
|
} finally {
|
|
|
|
|
setIsResponding(false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const renderInteractionModal = () => {
|
|
|
|
|
if (!interaction) return null
|
|
|
|
|
|
|
|
|
|
switch (interaction.type) {
|
|
|
|
|
case "msgbox":
|
|
|
|
|
return (
|
|
|
|
|
<Dialog open={true} onOpenChange={() => {}}>
|
2025-12-01 00:31:23 +01:00
|
|
|
<DialogContent className="sm:max-w-md">
|
2025-11-30 22:50:40 +01:00
|
|
|
<DialogHeader>
|
|
|
|
|
<DialogTitle>{interaction.title}</DialogTitle>
|
|
|
|
|
</DialogHeader>
|
2025-12-01 00:31:23 +01:00
|
|
|
<DialogDescription className="py-6 text-base whitespace-pre-wrap">{interaction.text}</DialogDescription>
|
2025-11-30 22:50:40 +01:00
|
|
|
<div className="flex justify-end">
|
|
|
|
|
<Button onClick={() => handleInteractionResponse("ok")} disabled={isResponding}>
|
|
|
|
|
{isResponding ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
|
|
|
|
|
OK
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
case "yesno":
|
|
|
|
|
return (
|
|
|
|
|
<Dialog open={true} onOpenChange={() => {}}>
|
2025-12-01 00:31:23 +01:00
|
|
|
<DialogContent className="sm:max-w-md">
|
2025-11-30 22:50:40 +01:00
|
|
|
<DialogHeader>
|
|
|
|
|
<DialogTitle>{interaction.title}</DialogTitle>
|
|
|
|
|
</DialogHeader>
|
2025-12-01 00:31:23 +01:00
|
|
|
<DialogDescription className="py-6 text-base whitespace-pre-wrap">{interaction.text}</DialogDescription>
|
|
|
|
|
<div className="flex justify-end gap-3">
|
2025-11-30 22:50:40 +01:00
|
|
|
<Button variant="outline" onClick={() => handleInteractionResponse("no")} disabled={isResponding}>
|
|
|
|
|
No
|
|
|
|
|
</Button>
|
|
|
|
|
<Button onClick={() => handleInteractionResponse("yes")} disabled={isResponding}>
|
|
|
|
|
{isResponding ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
|
|
|
|
|
Yes
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
case "inputbox":
|
|
|
|
|
return (
|
|
|
|
|
<Dialog open={true} onOpenChange={() => {}}>
|
2025-12-01 00:31:23 +01:00
|
|
|
<DialogContent className="sm:max-w-md">
|
2025-11-30 22:50:40 +01:00
|
|
|
<DialogHeader>
|
|
|
|
|
<DialogTitle>{interaction.title}</DialogTitle>
|
|
|
|
|
</DialogHeader>
|
2025-12-01 00:31:23 +01:00
|
|
|
<DialogDescription className="py-4 text-base whitespace-pre-wrap">{interaction.text}</DialogDescription>
|
2025-11-30 22:50:40 +01:00
|
|
|
<div className="space-y-4">
|
|
|
|
|
<div>
|
2025-12-01 00:31:23 +01:00
|
|
|
<Label htmlFor="input-value">Valor</Label>
|
2025-11-30 22:50:40 +01:00
|
|
|
<Input
|
|
|
|
|
id="input-value"
|
|
|
|
|
value={inputValue}
|
|
|
|
|
onChange={(e) => setInputValue(e.target.value)}
|
2025-12-01 00:31:23 +01:00
|
|
|
placeholder={interaction.data || "Introduce el valor..."}
|
|
|
|
|
autoFocus
|
2025-11-30 22:50:40 +01:00
|
|
|
/>
|
|
|
|
|
</div>
|
2025-12-01 00:31:23 +01:00
|
|
|
<div className="flex justify-end gap-3">
|
2025-11-30 22:50:40 +01:00
|
|
|
<Button variant="outline" onClick={() => handleInteractionResponse("")} disabled={isResponding}>
|
2025-12-01 00:31:23 +01:00
|
|
|
Cancelar
|
2025-11-30 22:50:40 +01:00
|
|
|
</Button>
|
|
|
|
|
<Button onClick={() => handleInteractionResponse(inputValue)} disabled={isResponding || !inputValue}>
|
|
|
|
|
{isResponding ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
|
|
|
|
|
OK
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
case "menu":
|
|
|
|
|
const menuItems = interaction.data?.split("|").filter(Boolean) || []
|
|
|
|
|
return (
|
|
|
|
|
<Dialog open={true} onOpenChange={() => {}}>
|
2025-12-01 00:31:23 +01:00
|
|
|
<DialogContent className="sm:max-w-2xl max-h-[80vh]">
|
2025-11-30 22:50:40 +01:00
|
|
|
<DialogHeader>
|
|
|
|
|
<DialogTitle>{interaction.title}</DialogTitle>
|
|
|
|
|
</DialogHeader>
|
2025-12-01 00:31:23 +01:00
|
|
|
<DialogDescription className="py-4 text-base whitespace-pre-wrap">{interaction.text}</DialogDescription>
|
|
|
|
|
<ScrollArea className="max-h-96 pr-4">
|
2025-11-30 22:50:40 +01:00
|
|
|
<div className="space-y-2">
|
2025-12-01 00:31:23 +01:00
|
|
|
{menuItems.map((item, index) => {
|
|
|
|
|
const [value, label] = item.includes(":") ? item.split(":") : [item, item]
|
|
|
|
|
return (
|
|
|
|
|
<Button
|
|
|
|
|
key={index}
|
|
|
|
|
variant={selectedMenuItem === value ? "default" : "outline"}
|
|
|
|
|
className="w-full justify-start text-left h-auto py-3 px-4"
|
|
|
|
|
onClick={() => setSelectedMenuItem(value)}
|
|
|
|
|
>
|
|
|
|
|
{label}
|
|
|
|
|
</Button>
|
|
|
|
|
)
|
|
|
|
|
})}
|
2025-11-30 22:50:40 +01:00
|
|
|
</div>
|
|
|
|
|
</ScrollArea>
|
2025-12-01 00:31:23 +01:00
|
|
|
<div className="flex justify-end gap-3 mt-4 pt-4 border-t">
|
2025-11-30 22:50:40 +01:00
|
|
|
<Button variant="outline" onClick={() => handleInteractionResponse("")} disabled={isResponding}>
|
2025-12-01 00:31:23 +01:00
|
|
|
Cancelar
|
2025-11-30 22:50:40 +01:00
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
onClick={() => handleInteractionResponse(selectedMenuItem)}
|
|
|
|
|
disabled={isResponding || !selectedMenuItem}
|
|
|
|
|
>
|
|
|
|
|
{isResponding ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
|
2025-12-01 00:31:23 +01:00
|
|
|
Seleccionar
|
2025-11-30 22:50:40 +01:00
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!sessionId) return null
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<Dialog open={true} onOpenChange={status !== "running" ? onClose : undefined}>
|
2025-12-01 00:31:23 +01:00
|
|
|
<DialogContent className="max-w-5xl max-h-[85vh]">
|
2025-11-30 22:50:40 +01:00
|
|
|
<DialogHeader>
|
|
|
|
|
<DialogTitle className="flex items-center gap-2">
|
|
|
|
|
{status === "running" && <Loader2 className="h-5 w-5 animate-spin" />}
|
|
|
|
|
{status === "completed" && <CheckCircle2 className="h-5 w-5 text-green-500" />}
|
|
|
|
|
{status === "failed" && <XCircle className="h-5 w-5 text-red-500" />}
|
2025-12-01 00:31:23 +01:00
|
|
|
<TerminalIcon className="h-5 w-5" />
|
2025-11-30 22:50:40 +01:00
|
|
|
{title}
|
|
|
|
|
</DialogTitle>
|
|
|
|
|
<DialogDescription>{description}</DialogDescription>
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-4">
|
2025-12-01 00:31:23 +01:00
|
|
|
<div className="border rounded-lg overflow-hidden bg-[#1e1e1e]">
|
|
|
|
|
<div ref={terminalRef} className="h-[500px] p-2" style={{ width: "100%", height: "500px" }} />
|
2025-11-30 22:50:40 +01:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center justify-between pt-4 border-t">
|
2025-12-01 00:31:23 +01:00
|
|
|
<div className="text-sm text-muted-foreground">
|
|
|
|
|
Session ID: <span className="font-mono">{sessionId}</span>
|
|
|
|
|
</div>
|
|
|
|
|
{status !== "running" && (
|
|
|
|
|
<Button onClick={onClose} size="lg">
|
|
|
|
|
Cerrar
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
2025-11-30 22:50:40 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
|
|
|
|
{renderInteractionModal()}
|
|
|
|
|
</>
|
|
|
|
|
)
|
|
|
|
|
}
|