2026-06-26 10:50:24 +02:00
|
|
|
|
import React, { useState, useRef, useMemo, useEffect } from 'react';
|
|
|
|
|
|
import type { CommandLineProps } from '../types/ui.types';
|
|
|
|
|
|
import { getCommandRegistry, type CommandDefinition } from '../services/commandRegistry';
|
2026-08-29 02:55:53 +02:00
|
|
|
|
import { useUIMode } from '../services/uiModeService';
|
2026-06-26 10:50:24 +02:00
|
|
|
|
|
|
|
|
|
|
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 }) => {
|
2026-08-29 02:55:53 +02:00
|
|
|
|
const uiMode = useUIMode();
|
|
|
|
|
|
if (uiMode === 'simple') return null;
|
|
|
|
|
|
|
2026-06-26 10:50:24 +02:00
|
|
|
|
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;
|