diff --git a/AppImage/components/script-terminal-modal.tsx b/AppImage/components/script-terminal-modal.tsx index 50b3683..1fc312e 100644 --- a/AppImage/components/script-terminal-modal.tsx +++ b/AppImage/components/script-terminal-modal.tsx @@ -6,7 +6,7 @@ import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { X, CheckCircle2, XCircle, Loader2 } from "lucide-react" -import { TerminalPanel } from "./terminal-panel" +import { TerminalPanel, type TerminalPanelHandle } from "./terminal-panel" import { API_PORT } from "@/lib/api-config" interface WebInteraction { @@ -42,7 +42,7 @@ export function ScriptTerminalModal({ const [exitCode, setExitCode] = useState(null) const [currentInteraction, setCurrentInteraction] = useState(null) const [interactionInput, setInteractionInput] = useState("") - const terminalRef = useRef(null) + const terminalRef = useRef(null) useEffect(() => { console.log("[v0] currentInteraction changed:", currentInteraction) @@ -106,14 +106,8 @@ export function ScriptTerminalModal({ console.log("[v0] Sending interaction response:", response) - // Access the terminal instance to send the response - const terminal = terminalRef.current - if (terminal?.terminals?.[0]?.ws) { - terminal.terminals[0].ws.send(response) - console.log("[v0] Response sent successfully") - } else { - console.log("[v0] Could not send response - no WebSocket available") - } + terminalRef.current.sendMessage(response) + console.log("[v0] Response sent successfully") console.log("[v0] Clearing currentInteraction after response") setCurrentInteraction(null) diff --git a/AppImage/components/terminal-panel.tsx b/AppImage/components/terminal-panel.tsx index a0da963..97f6d66 100644 --- a/AppImage/components/terminal-panel.tsx +++ b/AppImage/components/terminal-panel.tsx @@ -1,7 +1,7 @@ "use client" import type React from "react" -import { useEffect, useRef, useState } from "react" +import { useEffect, useRef, useState, forwardRef, useImperativeHandle } from "react" import { API_PORT } from "../lib/api-config" import { fetchApi } from "@/lib/api-config" // Cambiando import para usar fetchApi directamente import { @@ -30,6 +30,10 @@ type TerminalPanelProps = { onWebInteraction?: (interaction: any) => void } +export interface TerminalPanelHandle { + sendMessage: (message: string) => void +} + interface TerminalInstance { id: string title: string @@ -134,892 +138,911 @@ const proxmoxCommands = [ { cmd: "clear", desc: "Clear terminal screen" }, ] -export const TerminalPanel: React.FC = ({ - websocketUrl, - onClose, - initMessage, - onWebInteraction, -}) => { - const [terminals, setTerminals] = useState([]) - const [activeTerminalId, setActiveTerminalId] = useState("") - const [layout, setLayout] = useState<"single" | "grid">("grid") - const [isMobile, setIsMobile] = useState(false) - const [isTablet, setIsTablet] = useState(false) - const [terminalHeight, setTerminalHeight] = useState(500) // altura por defecto en px - const [searchModalOpen, setSearchModalOpen] = useState(false) - const [searchQuery, setSearchQuery] = useState("") - const [filteredCommands, setFilteredCommands] = useState>(proxmoxCommands) - const [isSearching, setIsSearching] = useState(false) - const [searchResults, setSearchResults] = useState([]) - const [useOnline, setUseOnline] = useState(true) +const TerminalPanel = forwardRef( + ({ websocketUrl, onClose, initMessage, onWebInteraction }, ref) => { + const [terminals, setTerminals] = useState([]) + const [activeTerminalId, setActiveTerminalId] = useState("") + const [layout, setLayout] = useState<"single" | "grid">("grid") + const [isMobile, setIsMobile] = useState(false) + const [isTablet, setIsTablet] = useState(false) + const [terminalHeight, setTerminalHeight] = useState(500) // altura por defecto en px + const [searchModalOpen, setSearchModalOpen] = useState(false) + const [searchQuery, setSearchQuery] = useState("") + const [filteredCommands, setFilteredCommands] = useState>(proxmoxCommands) + const [isSearching, setIsSearching] = useState(false) + const [searchResults, setSearchResults] = useState([]) + const [useOnline, setUseOnline] = useState(true) - const containerRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}) + const containerRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}) - useEffect(() => { - const updateDeviceType = () => { - const width = window.innerWidth - const isTouchDevice = "ontouchstart" in window || navigator.maxTouchPoints > 0 - const isTabletSize = width >= 768 && width <= 1366 // iPads Pro pueden llegar a 1366px + useImperativeHandle( + ref, + () => ({ + sendMessage: (message: string) => { + console.log("[v0] TerminalPanel.sendMessage called with:", message) + // Send to the first terminal (or active terminal if available) + const terminal = terminals.find((t) => t.id === activeTerminalId) || terminals[0] + if (terminal?.ws && terminal.ws.readyState === WebSocket.OPEN) { + console.log("[v0] Sending message via WebSocket") + terminal.ws.send(message) + } else { + console.log("[v0] Cannot send message - no active WebSocket") + } + }, + }), + [terminals, activeTerminalId], + ) - setIsMobile(width < 768) - setIsTablet(isTouchDevice && isTabletSize) - } + useEffect(() => { + const updateDeviceType = () => { + const width = window.innerWidth + const isTouchDevice = "ontouchstart" in window || navigator.maxTouchPoints > 0 + const isTabletSize = width >= 768 && width <= 1366 // iPads Pro pueden llegar a 1366px - updateDeviceType() - const handleResize = () => updateDeviceType() - window.addEventListener("resize", handleResize) + setIsMobile(width < 768) + setIsTablet(isTouchDevice && isTabletSize) + } - const savedHeight = localStorage.getItem("terminalHeight") - if (savedHeight) { - setTerminalHeight(Number.parseInt(savedHeight, 10)) - } + updateDeviceType() + const handleResize = () => updateDeviceType() + window.addEventListener("resize", handleResize) - return () => { - window.removeEventListener("resize", handleResize) - } - }, []) + const savedHeight = localStorage.getItem("terminalHeight") + if (savedHeight) { + setTerminalHeight(Number.parseInt(savedHeight, 10)) + } - const handleResizeStart = (e: React.MouseEvent | React.TouchEvent) => { - // Bloquear solo en pantallas muy pequeñas (móviles) - if (window.innerWidth < 640 && !isTablet) { - return - } + return () => { + window.removeEventListener("resize", handleResize) + } + }, []) - e.preventDefault() - e.stopPropagation() - - // Detectar si es touch o mouse - const clientY = "touches" in e ? e.touches[0].clientY : e.clientY - const startY = clientY - const startHeight = terminalHeight - - const handleMove = (moveEvent: MouseEvent | TouchEvent) => { - const currentY = "touches" in moveEvent ? moveEvent.touches[0].clientY : moveEvent.clientY - const deltaY = currentY - startY - const newHeight = Math.max(200, Math.min(2400, startHeight + deltaY)) - - setTerminalHeight(newHeight) - } - - const handleEnd = () => { - document.removeEventListener("mousemove", handleMove as any) - document.removeEventListener("mouseup", handleEnd) - document.removeEventListener("touchmove", handleMove as any) - document.removeEventListener("touchend", handleEnd) - - localStorage.setItem("terminalHeight", terminalHeight.toString()) - } - - document.addEventListener("mousemove", handleMove as any) - document.addEventListener("mouseup", handleEnd) - document.addEventListener("touchmove", handleMove as any, { passive: false }) - document.addEventListener("touchend", handleEnd) - } - - useEffect(() => { - if (terminals.length === 0) { - addNewTerminal() - } - }, []) - - useEffect(() => { - const searchCheatSh = async (query: string) => { - if (!query.trim()) { - setSearchResults([]) - setFilteredCommands(proxmoxCommands) + const handleResizeStart = (e: React.MouseEvent | React.TouchEvent) => { + // Bloquear solo en pantallas muy pequeñas (móviles) + if (window.innerWidth < 640 && !isTablet) { return } - try { - setIsSearching(true) + e.preventDefault() + e.stopPropagation() - const searchEndpoint = `/api/terminal/search-command?q=${encodeURIComponent(query)}` + // Detectar si es touch o mouse + const clientY = "touches" in e ? e.touches[0].clientY : e.clientY + const startY = clientY + const startHeight = terminalHeight - const data = await fetchApi<{ success: boolean; examples: any[] }>(searchEndpoint, { - method: "GET", - signal: AbortSignal.timeout(10000), - }) + const handleMove = (moveEvent: MouseEvent | TouchEvent) => { + const currentY = "touches" in moveEvent ? moveEvent.touches[0].clientY : moveEvent.clientY + const deltaY = currentY - startY + const newHeight = Math.max(200, Math.min(2400, startHeight + deltaY)) - if (!data.success || !data.examples || data.examples.length === 0) { - throw new Error("No examples found") + setTerminalHeight(newHeight) + } + + const handleEnd = () => { + document.removeEventListener("mousemove", handleMove as any) + document.removeEventListener("mouseup", handleEnd) + document.removeEventListener("touchmove", handleMove as any) + document.removeEventListener("touchend", handleEnd) + + localStorage.setItem("terminalHeight", terminalHeight.toString()) + } + + document.addEventListener("mousemove", handleMove as any) + document.addEventListener("mouseup", handleEnd) + document.addEventListener("touchmove", handleMove as any, { passive: false }) + document.addEventListener("touchend", handleEnd) + } + + useEffect(() => { + if (terminals.length === 0) { + addNewTerminal() + } + }, []) + + useEffect(() => { + const searchCheatSh = async (query: string) => { + if (!query.trim()) { + setSearchResults([]) + setFilteredCommands(proxmoxCommands) + return } - console.log("[v0] Received parsed examples from server:", data.examples.length) - - const formattedResults: CheatSheetResult[] = data.examples.map((example: any) => ({ - command: example.command, - description: example.description || "", - examples: [example.command], - })) - - setUseOnline(true) - setSearchResults(formattedResults) - } catch (error) { - console.log("[v0] Error fetching from cheat.sh proxy, using offline commands:", error) - const filtered = proxmoxCommands.filter( - (item) => - item.cmd.toLowerCase().includes(query.toLowerCase()) || - item.desc.toLowerCase().includes(query.toLowerCase()), - ) - setFilteredCommands(filtered) - setSearchResults([]) - setUseOnline(false) - } finally { - setIsSearching(false) - } - } - - const debounce = setTimeout(() => { - if (searchQuery && searchQuery.length >= 2) { - searchCheatSh(searchQuery) - } else { - setSearchResults([]) - setFilteredCommands(proxmoxCommands) - } - }, 800) - - return () => clearTimeout(debounce) - }, [searchQuery]) - - const addNewTerminal = () => { - if (terminals.length >= 4) return - - const newId = `terminal-${Date.now()}` - setTerminals((prev) => [ - ...prev, - { - id: newId, - title: `Terminal ${prev.length + 1}`, - term: null, - ws: null, - isConnected: false, - fitAddon: null, // Added fitAddon initialization - }, - ]) - setActiveTerminalId(newId) - } - - const closeTerminal = (id: string) => { - const terminal = terminals.find((t) => t.id === id) - if (terminal) { - if (terminal.ws) { - terminal.ws.close() - } - if (terminal.term) { - terminal.term.dispose() - } - } - - setTerminals((prev) => { - const filtered = prev.filter((t) => t.id !== id) - if (filtered.length > 0 && activeTerminalId === id) { - setActiveTerminalId(filtered[0].id) - } - return filtered - }) - - delete containerRefs.current[id] - } - - useEffect(() => { - terminals.forEach((terminal) => { - const container = containerRefs.current[terminal.id] - if (!terminal.term && container) { - initializeTerminal(terminal, container) - } - }) - }, [terminals, isMobile]) - - useEffect(() => { - if (window.innerWidth < 640) return - - terminals.forEach((terminal) => { - if (terminal.term && terminal.fitAddon && terminal.isConnected) { try { - setTimeout(() => { - terminal.fitAddon?.fit() - if (terminal.ws?.readyState === WebSocket.OPEN) { - const cols = terminal.term?.cols || 80 - const rows = terminal.term?.rows || 24 - terminal.ws.send( - JSON.stringify({ - type: "resize", - cols, - rows, - }), - ) - } - }, 100) - } catch (err) { - console.warn("[Terminal] resize on height change failed:", err) - } - } - }) - }, [terminalHeight, layout, terminals, isMobile]) + setIsSearching(true) - const initializeTerminal = async (terminal: TerminalInstance, container: HTMLDivElement) => { - 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"), - ]).then(([Terminal, FitAddon]) => [Terminal, FitAddon]) + const searchEndpoint = `/api/terminal/search-command?q=${encodeURIComponent(query)}` - const fontSize = window.innerWidth < 768 ? 12 : 16 + const data = await fetchApi<{ success: boolean; examples: any[] }>(searchEndpoint, { + method: "GET", + signal: AbortSignal.timeout(10000), + }) - 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", - }, - }) + if (!data.success || !data.examples || data.examples.length === 0) { + throw new Error("No examples found") + } - const fitAddon = new FitAddonClass() - term.loadAddon(fitAddon) + console.log("[v0] Received parsed examples from server:", data.examples.length) - term.open(container) + const formattedResults: CheatSheetResult[] = data.examples.map((example: any) => ({ + command: example.command, + description: example.description || "", + examples: [example.command], + })) - fitAddon.fit() - - const wsUrl = websocketUrl || getWebSocketUrl() - const ws = new WebSocket(wsUrl) - - const syncSizeWithBackend = () => { - try { - fitAddon.fit() - if (ws.readyState === WebSocket.OPEN) { - const cols = term.cols - const rows = term.rows - ws.send( - JSON.stringify({ - type: "resize", - cols, - rows, - }), + setUseOnline(true) + setSearchResults(formattedResults) + } catch (error) { + console.log("[v0] Error fetching from cheat.sh proxy, using offline commands:", error) + const filtered = proxmoxCommands.filter( + (item) => + item.cmd.toLowerCase().includes(query.toLowerCase()) || + item.desc.toLowerCase().includes(query.toLowerCase()), ) + setFilteredCommands(filtered) + setSearchResults([]) + setUseOnline(false) + } finally { + setIsSearching(false) } - } catch (err) { - console.warn("[Terminal] resize failed:", err) - } - } - - ws.onopen = () => { - setTerminals((prev) => - prev.map((t) => (t.id === terminal.id ? { ...t, isConnected: true, term, ws, fitAddon } : t)), - ) - - if (initMessage) { - console.log("[v0] TerminalPanel: Sending init message:", initMessage) - ws.send(JSON.stringify(initMessage)) - } else { - term.writeln("\x1b[32mConnected to ProxMenux terminal.\x1b[0m") } - syncSizeWithBackend() - } - - ws.onmessage = (event) => { - try { - const data = JSON.parse(event.data) - if (data.type === "web_interaction" && onWebInteraction) { - console.log("[v0] TerminalPanel: Intercepted web_interaction:", data.interaction) - onWebInteraction(data.interaction) - return // Don't write to terminal + const debounce = setTimeout(() => { + if (searchQuery && searchQuery.length >= 2) { + searchCheatSh(searchQuery) + } else { + setSearchResults([]) + setFilteredCommands(proxmoxCommands) + } + }, 800) + + return () => clearTimeout(debounce) + }, [searchQuery]) + + const addNewTerminal = () => { + if (terminals.length >= 4) return + + const newId = `terminal-${Date.now()}` + setTerminals((prev) => [ + ...prev, + { + id: newId, + title: `Terminal ${prev.length + 1}`, + term: null, + ws: null, + isConnected: false, + fitAddon: null, // Added fitAddon initialization + }, + ]) + setActiveTerminalId(newId) + } + + const closeTerminal = (id: string) => { + const terminal = terminals.find((t) => t.id === id) + if (terminal) { + if (terminal.ws) { + terminal.ws.close() + } + if (terminal.term) { + terminal.term.dispose() } - } catch (e) { - // Not JSON, it's regular terminal output } - term.write(event.data) + setTerminals((prev) => { + const filtered = prev.filter((t) => t.id !== id) + if (filtered.length > 0 && activeTerminalId === id) { + setActiveTerminalId(filtered[0].id) + } + return filtered + }) + + delete containerRefs.current[id] } - ws.onerror = (error) => { - console.error("[v0] TerminalPanel: WebSocket error:", error) - setTerminals((prev) => prev.map((t) => (t.id === terminal.id ? { ...t, isConnected: false } : t))) - term.writeln("\r\n\x1b[31m[ERROR] WebSocket connection error\x1b[0m") - } + useEffect(() => { + terminals.forEach((terminal) => { + const container = containerRefs.current[terminal.id] + if (!terminal.term && container) { + initializeTerminal(terminal, container) + } + }) + }, [terminals, isMobile]) - ws.onclose = () => { - setTerminals((prev) => prev.map((t) => (t.id === terminal.id ? { ...t, isConnected: false } : t))) - term.writeln("\r\n\x1b[33m[INFO] Connection closed\x1b[0m") - } + useEffect(() => { + if (window.innerWidth < 640) return - term.onData((data) => { - if (ws.readyState === WebSocket.OPEN) { - ws.send(data) + terminals.forEach((terminal) => { + if (terminal.term && terminal.fitAddon && terminal.isConnected) { + try { + setTimeout(() => { + terminal.fitAddon?.fit() + if (terminal.ws?.readyState === WebSocket.OPEN) { + const cols = terminal.term?.cols || 80 + const rows = terminal.term?.rows || 24 + terminal.ws.send( + JSON.stringify({ + type: "resize", + cols, + rows, + }), + ) + } + }, 100) + } catch (err) { + console.warn("[Terminal] resize on height change failed:", err) + } + } + }) + }, [terminalHeight, layout, terminals, isMobile]) + + const initializeTerminal = async (terminal: TerminalInstance, container: HTMLDivElement) => { + 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"), + ]).then(([Terminal, FitAddon]) => [Terminal, FitAddon]) + + 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) + + term.open(container) + + fitAddon.fit() + + const wsUrl = websocketUrl || getWebSocketUrl() + const ws = new WebSocket(wsUrl) + + const syncSizeWithBackend = () => { + try { + fitAddon.fit() + if (ws.readyState === WebSocket.OPEN) { + const cols = term.cols + const rows = term.rows + ws.send( + JSON.stringify({ + type: "resize", + cols, + rows, + }), + ) + } + } catch (err) { + console.warn("[Terminal] resize failed:", err) + } } - }) - let resizeTimeout: any = null + ws.onopen = () => { + setTerminals((prev) => + prev.map((t) => (t.id === terminal.id ? { ...t, isConnected: true, term, ws, fitAddon } : t)), + ) + + if (initMessage) { + console.log("[v0] TerminalPanel: Sending init message:", initMessage) + ws.send(JSON.stringify(initMessage)) + } else { + term.writeln("\x1b[32mConnected to ProxMenux terminal.\x1b[0m") + } - const handleResize = () => { - clearTimeout(resizeTimeout) - resizeTimeout = setTimeout(() => { syncSizeWithBackend() - }, 150) + } + + ws.onmessage = (event) => { + try { + const data = JSON.parse(event.data) + if (data.type === "web_interaction" && onWebInteraction) { + console.log("[v0] TerminalPanel: Intercepted web_interaction:", data.interaction) + onWebInteraction(data.interaction) + return // Don't write to terminal + } + } catch (e) { + // Not JSON, it's regular terminal output + } + + term.write(event.data) + } + + ws.onerror = (error) => { + console.error("[v0] TerminalPanel: WebSocket error:", error) + setTerminals((prev) => prev.map((t) => (t.id === terminal.id ? { ...t, isConnected: false } : t))) + term.writeln("\r\n\x1b[31m[ERROR] WebSocket connection error\x1b[0m") + } + + ws.onclose = () => { + setTerminals((prev) => prev.map((t) => (t.id === terminal.id ? { ...t, isConnected: false } : t))) + term.writeln("\r\n\x1b[33m[INFO] Connection closed\x1b[0m") + } + + term.onData((data) => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(data) + } + }) + + let resizeTimeout: any = null + + const handleResize = () => { + clearTimeout(resizeTimeout) + resizeTimeout = setTimeout(() => { + syncSizeWithBackend() + }, 150) + } + + window.addEventListener("resize", handleResize) + + return () => { + window.removeEventListener("resize", handleResize) + ws.close() + term.dispose() + } } - window.addEventListener("resize", handleResize) + const handleKeyButton = (key: string, e?: React.MouseEvent | React.TouchEvent) => { + // Prevenir comportamientos por defecto del navegador + if (e) { + e.preventDefault() + e.stopPropagation() + } - return () => { - window.removeEventListener("resize", handleResize) - ws.close() - term.dispose() - } - } + const activeTerminal = terminals.find((t) => t.id === activeTerminalId) + if (!activeTerminal || !activeTerminal.ws || activeTerminal.ws.readyState !== WebSocket.OPEN) return - const handleKeyButton = (key: string, e?: React.MouseEvent | React.TouchEvent) => { - // Prevenir comportamientos por defecto del navegador - if (e) { - e.preventDefault() - e.stopPropagation() - } + let seq = "" + switch (key) { + case "UP": + seq = "\x1bOA" + break + case "DOWN": + seq = "\x1bOB" + break + case "RIGHT": + seq = "\x1bOC" + break + case "LEFT": + seq = "\x1bOD" + break + case "ESC": + seq = "\x1b" + break + case "TAB": + seq = "\t" + break + case "CTRL_C": + seq = "\x03" + break + default: + break + } - const activeTerminal = terminals.find((t) => t.id === activeTerminalId) - if (!activeTerminal || !activeTerminal.ws || activeTerminal.ws.readyState !== WebSocket.OPEN) return - - let seq = "" - switch (key) { - case "UP": - seq = "\x1bOA" - break - case "DOWN": - seq = "\x1bOB" - break - case "RIGHT": - seq = "\x1bOC" - break - case "LEFT": - seq = "\x1bOD" - break - case "ESC": - seq = "\x1b" - break - case "TAB": - seq = "\t" - break - case "CTRL_C": - seq = "\x03" - break - default: - break - } - - activeTerminal.ws.send(seq) - } - - const handleClear = () => { - const activeTerminal = terminals.find((t) => t.id === activeTerminalId) - if (activeTerminal?.term) { - activeTerminal.term.clear() - } - } - - const handleClose = () => { - terminals.forEach((terminal) => { - if (terminal.ws) terminal.ws.close() - if (terminal.term) terminal.term.dispose() - }) - onClose?.() - } - - const sendToActiveTerminal = (command: string) => { - const activeTerminal = terminals.find((t) => t.id === activeTerminalId) - - if (activeTerminal?.ws && activeTerminal.ws.readyState === WebSocket.OPEN) { - activeTerminal.ws.send(command) - - setTimeout(() => { - setSearchModalOpen(false) - }, 100) - } - } - - const sendSequence = (seq: string, e?: React.MouseEvent | React.TouchEvent) => { - if (e) { - e.preventDefault() - e.stopPropagation() - } - - const activeTerminal = terminals.find((t) => t.id === activeTerminalId) - if (activeTerminal?.ws && activeTerminal.ws.readyState === WebSocket.OPEN) { activeTerminal.ws.send(seq) } - } - const getLayoutClass = () => { - const count = terminals.length - if (isMobile || count === 1) return "grid grid-cols-1" - - // Vista de cuadrícula 2x2 - if (layout === "grid") { - if (count === 2) return "grid grid-cols-2" - if (count === 3) return "grid grid-cols-2 grid-rows-2" - if (count === 4) return "grid grid-cols-2 grid-rows-2" + const handleClear = () => { + const activeTerminal = terminals.find((t) => t.id === activeTerminalId) + if (activeTerminal?.term) { + activeTerminal.term.clear() + } } - if (count === 2) return "grid grid-cols-1 grid-rows-2" - if (count === 3) return "grid grid-cols-1 grid-rows-3" - if (count === 4) return "grid grid-cols-1 grid-rows-4" + const handleClose = () => { + terminals.forEach((terminal) => { + if (terminal.ws) terminal.ws.close() + if (terminal.term) terminal.term.dispose() + }) + onClose?.() + } - // Vista de filas apiladas (single) - una terminal debajo de otra - return "grid grid-cols-1" - } + const sendToActiveTerminal = (command: string) => { + const activeTerminal = terminals.find((t) => t.id === activeTerminalId) - const activeTerminal = terminals.find((t) => t.id === activeTerminalId) + if (activeTerminal?.ws && activeTerminal.ws.readyState === WebSocket.OPEN) { + activeTerminal.ws.send(command) - return ( -
-
-
- -
- {terminals.length} / 4 terminals + setTimeout(() => { + setSearchModalOpen(false) + }, 100) + } + } + + const sendSequence = (seq: string, e?: React.MouseEvent | React.TouchEvent) => { + if (e) { + e.preventDefault() + e.stopPropagation() + } + + const activeTerminal = terminals.find((t) => t.id === activeTerminalId) + if (activeTerminal?.ws && activeTerminal.ws.readyState === WebSocket.OPEN) { + activeTerminal.ws.send(seq) + } + } + + const getLayoutClass = () => { + const count = terminals.length + if (isMobile || count === 1) return "grid grid-cols-1" + + // Vista de cuadrícula 2x2 + if (layout === "grid") { + if (count === 2) return "grid grid-cols-2" + if (count === 3) return "grid grid-cols-2 grid-rows-2" + if (count === 4) return "grid grid-cols-2 grid-rows-2" + } + + if (count === 2) return "grid grid-cols-1 grid-rows-2" + if (count === 3) return "grid grid-cols-1 grid-rows-3" + if (count === 4) return "grid grid-cols-1 grid-rows-4" + + // Vista de filas apiladas (single) - una terminal debajo de otra + return "grid grid-cols-1" + } + + const activeTerminal = terminals.find((t) => t.id === activeTerminalId) + + return ( +
+
+
+ +
+ {terminals.length} / 4 terminals +
+ +
+ {!isMobile && terminals.length > 1 && ( + <> + + + + )} + + + + +
-
- {!isMobile && terminals.length > 1 && ( - <> - - - - )} - - - - -
-
- -
{ - containerRefs.current["main"] = el - }} - className={`overflow-hidden flex flex-col ${isMobile ? "flex-1 h-[60vh]" : "overflow-hidden"} w-full max-w-full`} - style={!isMobile || isTablet ? { height: `${terminalHeight}px`, flexShrink: 0 } : undefined} - > - {isMobile ? ( - - - {terminals.map((terminal) => ( - - {terminal.title} - {terminals.length > 1 && ( - - )} - - ))} - - {terminals.map((terminal) => ( - -
(containerRefs.current[terminal.id] = el)} - className="w-full h-full flex-1 bg-black overflow-hidden" - /> - - ))} - - ) : ( -
- {terminals.map((terminal) => ( -
1 && activeTerminalId === terminal.id ? "ring-2 ring-blue-500" : "" - }`} - > -
- - {terminals.length > 1 && ( - - )} -
+ {terminals.length > 1 && ( + + )} + + ))} + + {terminals.map((terminal) => ( + +
(containerRefs.current[terminal.id] = el)} + className="w-full h-full flex-1 bg-black overflow-hidden" + /> + + ))} + + ) : ( +
+ {terminals.map((terminal) => (
(containerRefs.current[terminal.id] = el)} - onClick={() => setActiveTerminalId(terminal.id)} - className="flex-1 w-full max-w-full bg-black overflow-hidden cursor-pointer" - data-terminal-container - /> -
- ))} + key={terminal.id} + className={`relative bg-zinc-900 overflow-hidden flex flex-col min-h-0 w-full ${ + terminals.length > 1 && activeTerminalId === terminal.id ? "ring-2 ring-blue-500" : "" + }`} + > +
+ + {terminals.length > 1 && ( + + )} +
+
(containerRefs.current[terminal.id] = el)} + onClick={() => setActiveTerminalId(terminal.id)} + className="flex-1 w-full max-w-full bg-black overflow-hidden cursor-pointer" + data-terminal-container + /> +
+ ))} +
+ )} +
+ + {(isTablet || (!isMobile && !isTablet)) && terminals.length > 0 && ( +
+
)} -
- {(isTablet || (!isMobile && !isTablet)) && terminals.length > 0 && ( -
- -
- )} + {(isMobile || isTablet) && ( +
+ + + + + + + +
+ )} - {(isMobile || isTablet) && ( -
- - - - - - - -
- )} - - - - - Search Commands -
-
-
- - - Search for Linux and Proxmox commands - -
-
- - setSearchQuery(e.target.value)} - className="pl-10 bg-zinc-900 border-zinc-700 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 text-base" - autoCapitalize="none" - autoComplete="off" - autoCorrect="off" - spellCheck={false} - /> -
- - {isSearching && ( -
-
-

Searching cheat.sh...

+ + + + Search Commands +
+
- )} + -
- {searchResults.length > 0 ? ( - <> - {searchResults.map((result, index) => ( + Search for Linux and Proxmox commands + +
+
+ + setSearchQuery(e.target.value)} + className="pl-10 bg-zinc-900 border-zinc-700 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 text-base" + autoCapitalize="none" + autoComplete="off" + autoCorrect="off" + spellCheck={false} + /> +
+ + {isSearching && ( +
+
+

Searching cheat.sh...

+
+ )} + +
+ {searchResults.length > 0 ? ( + <> + {searchResults.map((result, index) => ( +
+ {result.description && ( +

# {result.description}

+ )} +
sendToActiveTerminal(result.command)} + className="flex items-start justify-between gap-2 cursor-pointer group hover:bg-zinc-800/50 rounded p-2 -m-2" + > + {result.command} + +
+
+ ))} + +
+

+ + Powered by cheat.sh +

+
+ + ) : filteredCommands.length > 0 && !useOnline ? ( + filteredCommands.map((item, index) => (
sendToActiveTerminal(item.cmd)} + className="p-3 rounded-lg border border-zinc-700 bg-zinc-800/50 hover:bg-zinc-800 hover:border-blue-500 cursor-pointer transition-colors" > - {result.description && ( -

# {result.description}

- )} -
sendToActiveTerminal(result.command)} - className="flex items-start justify-between gap-2 cursor-pointer group hover:bg-zinc-800/50 rounded p-2 -m-2" - > - {result.command} - +
+
+ {item.cmd} +

{item.desc}

+
+
- ))} - -
-

- - Powered by cheat.sh -

-
- - ) : filteredCommands.length > 0 && !useOnline ? ( - filteredCommands.map((item, index) => ( -
sendToActiveTerminal(item.cmd)} - className="p-3 rounded-lg border border-zinc-700 bg-zinc-800/50 hover:bg-zinc-800 hover:border-blue-500 cursor-pointer transition-colors" - > -
-
- {item.cmd} -

{item.desc}

+ )) + ) : !isSearching && !searchQuery && !useOnline ? ( + proxmoxCommands.map((item, index) => ( +
sendToActiveTerminal(item.cmd)} + className="p-3 rounded-lg border border-zinc-700 bg-zinc-800/50 hover:bg-zinc-800 hover:border-blue-500 cursor-pointer transition-colors" + > +
+
+ {item.cmd} +

{item.desc}

+
+
-
-
- )) - ) : !isSearching && !searchQuery && !useOnline ? ( - proxmoxCommands.map((item, index) => ( -
sendToActiveTerminal(item.cmd)} - className="p-3 rounded-lg border border-zinc-700 bg-zinc-800/50 hover:bg-zinc-800 hover:border-blue-500 cursor-pointer transition-colors" - > -
-
- {item.cmd} -

{item.desc}

-
- -
-
- )) - ) : !isSearching ? ( -
- {searchQuery ? ( - <> - -
-

No results found for "{searchQuery}"

-

Try a different command or check your spelling

-
- - ) : ( - <> - -
-

Search for any command

-
-

Try searching for:

-
- {["tar", "grep", "docker", "qm", "systemctl"].map((cmd) => ( - setSearchQuery(cmd)} - className="px-2 py-1 bg-zinc-800 rounded text-blue-400 cursor-pointer hover:bg-zinc-700" - > - {cmd} - - ))} + )) + ) : !isSearching ? ( +
+ {searchQuery ? ( + <> + +
+

No results found for "{searchQuery}"

+

Try a different command or check your spelling

+
+ + ) : ( + <> + +
+

Search for any command

+
+

Try searching for:

+
+ {["tar", "grep", "docker", "qm", "systemctl"].map((cmd) => ( + setSearchQuery(cmd)} + className="px-2 py-1 bg-zinc-800 rounded text-blue-400 cursor-pointer hover:bg-zinc-700" + > + {cmd} + + ))} +
-
- {useOnline && ( -
- - Powered by cheat.sh -
- )} - - )} -
- ) : null} -
- -
-
- - Tip: Search for any Linux command or Proxmox commands (qm, pct, zpool) + {useOnline && ( +
+ + Powered by cheat.sh +
+ )} + + )} +
+ ) : null} +
+ +
+
+ + Tip: Search for any Linux command or Proxmox commands (qm, pct, zpool) +
+ {useOnline && searchResults.length > 0 && Powered by cheat.sh}
- {useOnline && searchResults.length > 0 && Powered by cheat.sh}
-
- -
-
- ) -} + +
+
+ ) + }, +) + +TerminalPanel.displayName = "TerminalPanel" + +export default TerminalPanel