feat: initial commit web-cad-neu with docker-compose, frontend and backend
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
import React, { useState, useRef, useMemo, useEffect } from 'react';
|
||||
import type { CommandLineProps } from '../types/ui.types';
|
||||
import { getCommandRegistry, type CommandDefinition } from '../services/commandRegistry';
|
||||
|
||||
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 [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;
|
||||
Reference in New Issue
Block a user