"use client" import type React from "react" import { useEffect, useRef, useState } from "react" import { API_PORT } from "../lib/api-config" import { fetchApi } from "@/lib/api-config" // Cambiando import para usar fetchApi directamente import { Activity, Trash2, X, Search, Send, Lightbulb, Terminal, Plus, AlignJustify, Grid2X2, GripHorizontal, } from "lucide-react" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs" import type { CheatSheetResult } from "@/lib/cheat-sheet-result" // Declare CheatSheetResult here type TerminalPanelProps = { websocketUrl?: string onClose?: () => void } interface TerminalInstance { id: string title: string term: any ws: WebSocket | null isConnected: boolean fitAddon: any // Added fitAddon to TerminalInstance } function getWebSocketUrl(): string { if (typeof window === "undefined") { return "ws://localhost:8008/ws/terminal" } 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/terminal` } else { return `${wsProtocol}//${hostname}:${API_PORT}/ws/terminal` } } function getApiUrl(endpoint?: string): string { if (typeof window === "undefined") { return "http://localhost:8008" } const { protocol, hostname } = window.location const apiProtocol = protocol === "https:" ? "https:" : "http:" return `${apiProtocol}//${hostname}:${API_PORT}${endpoint || ""}` } const proxmoxCommands = [ { cmd: "pvesh get /nodes", desc: "List all Proxmox nodes" }, { cmd: "pvesh get /nodes/{node}/qemu", desc: "List VMs on a node" }, { cmd: "pvesh get /nodes/{node}/lxc", desc: "List LXC containers on a node" }, { cmd: "pvesh get /nodes/{node}/storage", desc: "List storage on a node" }, { cmd: "pvesh get /nodes/{node}/network", desc: "List network interfaces" }, { cmd: "qm list", desc: "List all QEMU/KVM virtual machines" }, { cmd: "qm start ", desc: "Start a virtual machine" }, { cmd: "qm stop ", desc: "Stop a virtual machine" }, { cmd: "qm shutdown ", desc: "Shutdown a virtual machine gracefully" }, { cmd: "qm status ", desc: "Show VM status" }, { cmd: "qm config ", desc: "Show VM configuration" }, { cmd: "qm snapshot ", desc: "Create VM snapshot" }, { cmd: "pct list", desc: "List all LXC containers" }, { cmd: "pct start ", desc: "Start LXC container" }, { cmd: "pct stop ", desc: "Stop LXC container" }, { cmd: "pct enter ", desc: "Enter LXC container console" }, { cmd: "pct config ", desc: "Show container configuration" }, { cmd: "pvesm status", desc: "Show storage status" }, { cmd: "pvesm list ", desc: "List storage content" }, { cmd: "pveperf", desc: "Test Proxmox system performance" }, { cmd: "pveversion", desc: "Show Proxmox VE version" }, { cmd: "systemctl status pve-cluster", desc: "Check cluster status" }, { cmd: "pvecm status", desc: "Show cluster status" }, { cmd: "pvecm nodes", desc: "List cluster nodes" }, { cmd: "zpool status", desc: "Show ZFS pool status" }, { cmd: "zpool list", desc: "List all ZFS pools" }, { cmd: "zfs list", desc: "List all ZFS datasets" }, { cmd: "ls -la", desc: "List all files with details" }, { cmd: "cd /path/to/dir", desc: "Change directory" }, { cmd: "mkdir dirname", desc: "Create new directory" }, { cmd: "rm -rf dirname", desc: "Remove directory recursively" }, { cmd: "cp source dest", desc: "Copy files or directories" }, { cmd: "mv source dest", desc: "Move or rename files" }, { cmd: "cat filename", desc: "Display file contents" }, { cmd: "grep 'pattern' file", desc: "Search for pattern in file" }, { cmd: "find . -name 'file'", desc: "Find files by name" }, { cmd: "chmod 755 file", desc: "Change file permissions" }, { cmd: "chown user:group file", desc: "Change file owner" }, { cmd: "tar -xzf file.tar.gz", desc: "Extract tar.gz archive" }, { cmd: "tar -czf archive.tar.gz dir/", desc: "Create tar.gz archive" }, { cmd: "df -h", desc: "Show disk usage" }, { cmd: "du -sh *", desc: "Show directory sizes" }, { cmd: "free -h", desc: "Show memory usage" }, { cmd: "top", desc: "Show running processes" }, { cmd: "ps aux | grep process", desc: "Find running process" }, { cmd: "kill -9 PID", desc: "Force kill process" }, { cmd: "systemctl status service", desc: "Check service status" }, { cmd: "systemctl start service", desc: "Start a service" }, { cmd: "systemctl stop service", desc: "Stop a service" }, { cmd: "systemctl restart service", desc: "Restart a service" }, { cmd: "apt update && apt upgrade", desc: "Update Debian/Ubuntu packages" }, { cmd: "apt install package", desc: "Install package on Debian/Ubuntu" }, { cmd: "apt remove package", desc: "Remove package" }, { cmd: "docker ps", desc: "List running containers" }, { cmd: "docker images", desc: "List Docker images" }, { cmd: "docker exec -it container bash", desc: "Enter container shell" }, { cmd: "ip addr show", desc: "Show IP addresses" }, { cmd: "ping host", desc: "Test network connectivity" }, { cmd: "curl -I url", desc: "Get HTTP headers" }, { cmd: "wget url", desc: "Download file from URL" }, { cmd: "ssh user@host", desc: "Connect via SSH" }, { cmd: "scp file user@host:/path", desc: "Copy file via SSH" }, { cmd: "tail -f /var/log/syslog", desc: "Follow log file in real-time" }, { cmd: "history", desc: "Show command history" }, { cmd: "clear", desc: "Clear terminal screen" }, ] export const TerminalPanel: React.FC = ({ websocketUrl, onClose }) => { 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 }>({}) 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 setIsMobile(width < 768) setIsTablet(isTouchDevice && isTabletSize) } updateDeviceType() const handleResize = () => updateDeviceType() window.addEventListener("resize", handleResize) const savedHeight = localStorage.getItem("terminalHeight") if (savedHeight) { setTerminalHeight(Number.parseInt(savedHeight, 10)) } return () => { window.removeEventListener("resize", handleResize) } }, []) const handleResizeStart = (e: React.MouseEvent | React.TouchEvent) => { // Bloquear solo en pantallas muy pequeñas (móviles) if (window.innerWidth < 640 && !isTablet) { return } 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) return } try { setIsSearching(true) const searchEndpoint = `/api/terminal/search-command?q=${encodeURIComponent(query)}` const data = await fetchApi<{ success: boolean; examples: any[] }>(searchEndpoint, { method: "GET", signal: AbortSignal.timeout(10000), }) if (!data.success || !data.examples || data.examples.length === 0) { throw new Error("No examples found") } 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]) 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) } } ws.onopen = () => { setTerminals((prev) => prev.map((t) => (t.id === terminal.id ? { ...t, isConnected: true, term, ws, fitAddon } : t)), ) term.writeln("\x1b[32mConnected to ProxMenux terminal.\x1b[0m") syncSizeWithBackend() } ws.onmessage = (event) => { 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() } } const handleKeyButton = (key: string, e?: React.MouseEvent | React.TouchEvent) => { // Prevenir comportamientos por defecto del navegador if (e) { e.preventDefault() e.stopPropagation() } 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 case "ENTER": seq = "\r" 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" } 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 && ( <> )}
{ 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 && ( )}
(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 && (
)} {(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...

)}
{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" >
{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} ))}
{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}
) }