@@ -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+
1016export 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