Files
web-cad/frontend/src/components/CommandLine.tsx
T

258 lines
8.6 KiB
TypeScript
Raw Normal View History

import React, { useState, useRef, useMemo, useEffect } from 'react';
import type { CommandLineProps } from '../types/ui.types';
import { getCommandRegistry, type CommandDefinition } from '../services/commandRegistry';
import { useUIMode } from '../services/uiModeService';
const defaultHistory = [
{ prefix: '·' as const, text: 'Bereit · Werkzeug: Auswahl · 110 Objekte · 5 Ebenen', type: 'info' as const },
{ prefix: '·' as const, text: 'Auto-Save aktiv · letzte Speicherung vor 3 Sekunden', type: 'info' as const },
{ prefix: '' as const, text: 'hallo', type: 'command' as const },
{ prefix: '·' as const, text: 'Hallo! Ich bin der KI Copilot. Tippe KI oder drücke Strg+K für Hilfe.', type: 'info' as const },
{ prefix: '' as const, text: 'BESTUHLUNG 5,22', type: 'command' as const },
{ prefix: '·' as const, text: '110 Stühle angelegt auf Ebene "Bestuhlung" ✓', type: 'info' as const },
];
const categoryColors: Record<string, string> = {
draw: '#22c55e',
modify: '#f97316',
view: '#06b6d4',
meta: '#a855f7',
special: '#ec4899',
};
const CommandLine: React.FC<CommandLineProps> = ({ history, onCommand }) => {
const uiMode = useUIMode();
if (uiMode === 'simple') return null;
const [input, setInput] = useState('');
const [selectedSuggestion, setSelectedSuggestion] = useState(0);
const [commandHistory, setCommandHistory] = useState<string[]>([]);
const [historyIndex, setHistoryIndex] = useState(-1);
const [showSuggestions, setShowSuggestions] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const historyRef = useRef<HTMLDivElement>(null);
const entries = history.length > 0 ? history : defaultHistory;
const suggestions = useMemo(() => {
if (!input.trim()) return [];
const registry = getCommandRegistry();
return registry.autocomplete(input.trim());
}, [input]);
useEffect(() => {
setSelectedSuggestion(0);
}, [suggestions]);
useEffect(() => {
if (historyRef.current) {
historyRef.current.scrollTop = historyRef.current.scrollHeight;
}
}, [entries]);
const executeCommand = (cmd: string) => {
const trimmed = cmd.trim();
if (!trimmed) return;
onCommand(trimmed);
setCommandHistory((prev) => [...prev, trimmed]);
setInput('');
setShowSuggestions(false);
setHistoryIndex(-1);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
const navKeys = ['ArrowUp', 'ArrowDown', 'Tab', 'Enter', 'Escape'];
if (showSuggestions && suggestions.length > 0 && navKeys.includes(e.key)) {
if (e.key === 'ArrowDown') {
e.preventDefault();
setSelectedSuggestion((prev) => Math.min(prev + 1, suggestions.length - 1));
return;
}
if (e.key === 'ArrowUp') {
e.preventDefault();
setSelectedSuggestion((prev) => Math.max(prev - 1, 0));
return;
}
if (e.key === 'Tab') {
e.preventDefault();
const suggestion = suggestions[selectedSuggestion] || suggestions[0];
if (suggestion) {
setInput(suggestion.name);
setShowSuggestions(false);
}
return;
}
if (e.key === 'Enter') {
e.preventDefault();
const suggestion = suggestions[selectedSuggestion];
if (suggestion) {
executeCommand(suggestion.name);
} else {
executeCommand(input);
}
return;
}
if (e.key === 'Escape') {
e.preventDefault();
setShowSuggestions(false);
return;
}
}
if (!showSuggestions || suggestions.length === 0) {
if (e.key === 'Enter' && input.trim()) {
executeCommand(input);
return;
}
if (e.key === 'ArrowUp') {
e.preventDefault();
if (commandHistory.length === 0) return;
const newIdx = historyIndex === -1 ? commandHistory.length - 1 : Math.max(historyIndex - 1, 0);
setHistoryIndex(newIdx);
setInput(commandHistory[newIdx]);
return;
}
if (e.key === 'ArrowDown') {
e.preventDefault();
if (historyIndex === -1) return;
const newIdx = historyIndex + 1;
if (newIdx >= commandHistory.length) {
setHistoryIndex(-1);
setInput('');
} else {
setHistoryIndex(newIdx);
setInput(commandHistory[newIdx]);
}
return;
}
if (e.key === 'Escape') {
e.preventDefault();
setInput('');
setShowSuggestions(false);
setHistoryIndex(-1);
return;
}
}
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInput(e.target.value);
setShowSuggestions(true);
setHistoryIndex(-1);
};
const handleBlur = () => {
setTimeout(() => setShowSuggestions(false), 150);
};
const handleFocus = () => {
if (input.trim()) setShowSuggestions(true);
};
const handleSuggestionClick = (cmd: CommandDefinition) => {
executeCommand(cmd.name);
};
return (
<section className="cmdline" aria-label="Befehlszeile">
<div className="cmdline-history" id="cmdline-history" aria-live="polite" ref={historyRef}>
{entries.map((entry, i) => (
<div key={i} className={`cmdline-history-entry${entry.type === 'info' ? ' info' : ''}`}>
<span className="prefix">{entry.prefix}</span>
<span className="text">{entry.text}</span>
</div>
))}
</div>
<div className="cmdline-input-wrap" style={{ position: 'relative' }}>
{showSuggestions && suggestions.length > 0 && (
<div
className="cmdline-suggestions"
style={{
position: 'absolute',
bottom: '100%',
left: 0,
right: 0,
background: '#1e293b',
border: '1px solid #334155',
borderRadius: '6px 6px 0 0',
maxHeight: '240px',
overflowY: 'auto',
zIndex: 1000,
boxShadow: '0 -4px 12px rgba(0,0,0,0.3)',
}}
>
{suggestions.map((cmd, i) => (
<div
key={cmd.name}
className={`cmdline-suggestion${i === selectedSuggestion ? ' selected' : ''}`}
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '6px 12px',
cursor: 'pointer',
background: i === selectedSuggestion ? '#334155' : 'transparent',
color: '#e2e8f0',
fontSize: '13px',
borderBottom: i < suggestions.length - 1 ? '1px solid #334155' : 'none',
}}
onMouseDown={(e) => {
e.preventDefault();
handleSuggestionClick(cmd);
}}
onMouseEnter={() => setSelectedSuggestion(i)}
>
<span
className="cmdline-suggestion-badge"
style={{
display: 'inline-block',
padding: '1px 6px',
borderRadius: '4px',
fontSize: '10px',
fontWeight: 600,
textTransform: 'uppercase',
background: categoryColors[cmd.category] || '#64748b',
color: '#fff',
minWidth: '44px',
textAlign: 'center',
}}
>
{cmd.category}
</span>
<span className="cmdline-suggestion-name" style={{ fontWeight: 600 }}>
{cmd.name}
</span>
{cmd.aliases.length > 0 && (
<span className="cmdline-suggestion-aliases" style={{ color: '#64748b', fontSize: '11px' }}>
({cmd.aliases.join(', ')})
</span>
)}
<span className="cmdline-suggestion-desc" style={{ color: '#94a3b8', fontSize: '12px', marginLeft: 'auto' }}>
{cmd.description}
</span>
</div>
))}
</div>
)}
<span className="cmdline-prompt"></span>
<input
className="cmdline-input"
type="text"
id="cmdline-input"
placeholder='Befehl eingeben (z. B. LINIE, KREIS, BESTUHLUNG)...'
aria-label="CAD Befehlseingabe"
autoComplete="off"
value={input}
onChange={handleChange}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
onFocus={handleFocus}
ref={inputRef}
/>
</div>
</section>
);
};
export default CommandLine;