/** * Console Command Input with Autocomplete * Ball Studios */ import React, { useState, useRef, useCallback, useEffect } from 'react'; import { useParams } from 'react-router-dom'; import http from '@/api/http'; import AutocompleteDropdown from './AutocompleteDropdown'; import SyntaxHelper from './SyntaxHelper'; import DangerCommandWarning from './DangerCommandWarning'; interface Suggestion { command: string; description: string; syntax: string; dangerous: boolean; danger_reason?: string; plugin: string; examples: string[]; } interface Props { onSend: (command: string) => void; disabled?: boolean; } const DANGEROUS = ['stop','restart','ban','ban-ip','op','deop','whitelist off','save-off','kill @e','kill @a']; export default function CommandInput({ onSend, disabled = false }: Props) { const { id: serverUuid } = useParams<{ id: string }>(); const [value, setValue] = useState(''); const [suggestions, setSugs]= useState([]); const [selected, setSelected]= useState(-1); const [showDanger, setDanger]= useState(null); const [activeSug, setActiveSug] = useState(null); const debounceRef = useRef>(); const inputRef = useRef(null); const isDanger = (cmd: string) => DANGEROUS.some(d => cmd.trim().toLowerCase().startsWith(d)); const fetchSuggestions = useCallback(async (q: string) => { if (!q.trim() || !serverUuid) { setSugs([]); return; } try { const { data } = await http.get( `/api/client/servers/${serverUuid}/addons/console/autocomplete`, { params: { q } } ); setSugs(data.data ?? []); setSelected(-1); } catch { setSugs([]); } }, [serverUuid]); const handleChange = (e: React.ChangeEvent) => { const v = e.target.value; setValue(v); clearTimeout(debounceRef.current); debounceRef.current = setTimeout(() => fetchSuggestions(v), 150); // Update active suggestion for syntax helper const match = suggestions.find(s => s.command === v.split(' ')[0]); setActiveSug(match ?? null); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'ArrowDown') { e.preventDefault(); setSelected(s => Math.min(s + 1, suggestions.length - 1)); } if (e.key === 'ArrowUp') { e.preventDefault(); setSelected(s => Math.max(s - 1, -1)); } if (e.key === 'Tab' && suggestions.length > 0) { e.preventDefault(); const idx = selected >= 0 ? selected : 0; applySelection(suggestions[idx]); } if (e.key === 'Escape') { setSugs([]); setSelected(-1); } if (e.key === 'Enter') { e.preventDefault(); submit(); } }; const applySelection = (sug: Suggestion) => { setValue(sug.command + ' '); setSugs([]); setSelected(-1); setActiveSug(sug); inputRef.current?.focus(); }; const submit = () => { const cmd = value.trim(); if (!cmd) return; if (isDanger(cmd)) { const sug = suggestions.find(s => cmd.startsWith(s.command)) ?? { command: cmd, description: '', syntax: '', dangerous: true, danger_reason: 'Potentially destructive command.', plugin: '', examples: [], }; setDanger(sug); return; } send(cmd); }; const send = (cmd: string) => { onSend(cmd); setValue(''); setSugs([]); setActiveSug(null); setDanger(null); }; useEffect(() => () => clearTimeout(debounceRef.current), []); return (
{/* Syntax helper */} {activeSug && } {/* Dropdown */} {suggestions.length > 0 && ( )} {/* Danger modal */} {showDanger && ( send(value.trim())} onCancel={() => { setDanger(null); inputRef.current?.focus(); }} /> )}
); }