Skip to content

Commit f1c4f5b

Browse files
committed
nicer control input
1 parent db2ddc4 commit f1c4f5b

File tree

1 file changed

+80
-24
lines changed

1 file changed

+80
-24
lines changed

chat/src/components/MessageInput.tsx

Lines changed: 80 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,18 @@ interface MessageInputProps {
77
disabled?: boolean;
88
}
99

10+
interface SentChar {
11+
char: string;
12+
id: number;
13+
timestamp: number;
14+
}
15+
1016
export default function MessageInput({ onSendMessage, disabled = false }: MessageInputProps) {
1117
const [message, setMessage] = useState('');
1218
const [inputMode, setInputMode] = useState<'text' | 'control'>('text');
19+
const [sentChars, setSentChars] = useState<SentChar[]>([]);
1320
const textareaRef = useRef<HTMLTextAreaElement>(null);
21+
const nextCharId = useRef(0);
1422

1523
const handleSubmit = (e: FormEvent) => {
1624
e.preventDefault();
@@ -20,6 +28,27 @@ export default function MessageInput({ onSendMessage, disabled = false }: Messag
2028
}
2129
};
2230

31+
// Remove sent characters after they expire (2 seconds)
32+
useEffect(() => {
33+
if (sentChars.length === 0) return;
34+
35+
const interval = setInterval(() => {
36+
const now = Date.now();
37+
setSentChars(chars => chars.filter(char => now - char.timestamp < 2000));
38+
}, 100);
39+
40+
return () => clearInterval(interval);
41+
}, [sentChars]);
42+
43+
const addSentChar = (char: string) => {
44+
const newChar: SentChar = {
45+
char,
46+
id: nextCharId.current++,
47+
timestamp: Date.now()
48+
};
49+
setSentChars(prev => [...prev, newChar]);
50+
};
51+
2352
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
2453
// In control mode, send special keys as raw messages
2554
if (inputMode === 'control' && !disabled) {
@@ -42,13 +71,15 @@ export default function MessageInput({ onSendMessage, disabled = false }: Messag
4271
// Check if the pressed key is in our special keys map
4372
if (specialKeys[e.key]) {
4473
e.preventDefault();
74+
addSentChar(e.key);
4575
onSendMessage(specialKeys[e.key], 'raw');
4676
return;
4777
}
4878

4979
// Handle Enter as raw newline when in control mode
5080
if (e.key === 'Enter' && !e.shiftKey) {
5181
e.preventDefault();
82+
addSentChar('⏎');
5283
onSendMessage('\r', 'raw');
5384
return;
5485
}
@@ -68,6 +99,7 @@ export default function MessageInput({ onSendMessage, disabled = false }: Messag
6899

69100
if (ctrlMappings[e.key.toLowerCase()]) {
70101
e.preventDefault();
102+
addSentChar(`Ctrl+${e.key.toUpperCase()}`);
71103
onSendMessage(ctrlMappings[e.key.toLowerCase()], 'raw');
72104
return;
73105
}
@@ -76,6 +108,7 @@ export default function MessageInput({ onSendMessage, disabled = false }: Messag
76108
// If it's a printable character (length 1), send it as raw input
77109
if (e.key.length === 1) {
78110
e.preventDefault();
111+
addSentChar(e.key);
79112
onSendMessage(e.key, 'raw');
80113
return;
81114
}
@@ -93,7 +126,6 @@ export default function MessageInput({ onSendMessage, disabled = false }: Messag
93126
}
94127
}, [inputMode]);
95128

96-
97129
return (
98130
<form onSubmit={handleSubmit} className="border-t border-gray-300 p-4 bg-white">
99131
<div className="flex flex-col">
@@ -127,33 +159,57 @@ export default function MessageInput({ onSendMessage, disabled = false }: Messag
127159
{inputMode === 'control' && !disabled && (
128160
<div className="mb-1 text-xs text-blue-600 font-mono flex justify-between">
129161
<span>Control mode - keystrokes sent directly to terminal</span>
162+
{sentChars.length > 0 && (
163+
<div className="flex space-x-1">
164+
{sentChars.map(char => (
165+
<span
166+
key={char.id}
167+
className="font-mono px-1 bg-blue-100 rounded text-blue-800 transition-opacity"
168+
style={{
169+
opacity: Math.max(0, 1 - (Date.now() - char.timestamp) / 2000),
170+
}}
171+
>
172+
{char.char}
173+
</span>
174+
))}
175+
</div>
176+
)}
130177
</div>
131178
)}
132179

133180
<div className="flex">
134-
<textarea
135-
ref={textareaRef}
136-
value={inputMode === 'text' ? message : ''}
137-
onChange={(e) => inputMode === 'text' && setMessage(e.target.value)}
138-
onKeyDown={handleKeyDown}
139-
placeholder={
140-
disabled ? 'Server offline...' :
141-
inputMode === 'control' ? 'Control mode - keystrokes sent directly...' :
142-
'Type a message...'
143-
}
144-
className={`flex-1 resize-none border rounded-l-lg p-2 focus:outline-none focus:ring-2 focus:ring-blue-500 text-gray-900 ${
145-
inputMode === 'control' && !disabled ? 'bg-gray-50 border-blue-200' : 'bg-white'
146-
}`}
147-
rows={2}
148-
disabled={disabled}
149-
/>
150-
<button
151-
type="submit"
152-
disabled={disabled || inputMode === 'control' || !message.trim()}
153-
className="bg-blue-500 text-white px-4 rounded-r-lg font-medium disabled:opacity-50 disabled:cursor-not-allowed"
154-
>
155-
Send
156-
</button>
181+
{inputMode === 'control' && !disabled ? (
182+
<div
183+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
184+
ref={textareaRef as any}
185+
tabIndex={0}
186+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
187+
onKeyDown={handleKeyDown as any}
188+
className="flex-1 cursor-text border rounded-lg p-2 focus:outline-none focus:ring-2 focus:ring-blue-500 text-gray-500 bg-gray-50 border-blue-200 min-h-[3.5rem] flex items-center justify-center"
189+
>
190+
Press any key to send to terminal
191+
</div>
192+
) : (
193+
<>
194+
<textarea
195+
ref={textareaRef}
196+
value={message}
197+
onChange={(e) => setMessage(e.target.value)}
198+
onKeyDown={handleKeyDown}
199+
placeholder={disabled ? 'Server offline...' : 'Type a message...'}
200+
className="flex-1 resize-none border rounded-l-lg p-2 focus:outline-none focus:ring-2 focus:ring-blue-500 text-gray-900 bg-white"
201+
rows={2}
202+
disabled={disabled}
203+
/>
204+
<button
205+
type="submit"
206+
disabled={disabled || !message.trim()}
207+
className="bg-blue-500 text-white px-4 rounded-r-lg font-medium disabled:opacity-50 disabled:cursor-not-allowed"
208+
>
209+
Send
210+
</button>
211+
</>
212+
)}
157213
</div>
158214
</div>
159215
</form>

0 commit comments

Comments
 (0)