@@ -99,13 +99,9 @@ export default function ChatScreen() {
99
99
canvasData,
100
100
replaceMessageAndGenerate,
101
101
} = useAppContext ( ) ;
102
- const [ inputMsg , setInputMsg ] = useState ( prefilledMsg . content ( ) ) ;
103
- const inputRef = useRef < HTMLTextAreaElement > ( null ) ;
102
+ const textarea = useOptimizedTextarea ( prefilledMsg . content ( ) ) ;
104
103
105
- const { extraContext, clearExtraContext } = useVSCodeContext (
106
- inputRef ,
107
- setInputMsg
108
- ) ;
104
+ const { extraContext, clearExtraContext } = useVSCodeContext ( textarea ) ;
109
105
// TODO: improve this when we have "upload file" feature
110
106
const currExtra : Message [ 'extra' ] = extraContext ? [ extraContext ] : undefined ;
111
107
@@ -135,9 +131,10 @@ export default function ChatScreen() {
135
131
} ;
136
132
137
133
const sendNewMessage = async ( ) => {
138
- if ( inputMsg . trim ( ) . length === 0 || isGenerating ( currConvId ?? '' ) ) return ;
139
- const lastInpMsg = inputMsg ;
140
- setInputMsg ( '' ) ;
134
+ const lastInpMsg = textarea . value ( ) ;
135
+ if ( lastInpMsg . trim ( ) . length === 0 || isGenerating ( currConvId ?? '' ) )
136
+ return ;
137
+ textarea . setValue ( '' ) ;
141
138
scrollToBottom ( false ) ;
142
139
setCurrNodeId ( - 1 ) ;
143
140
// get the last message node
@@ -146,13 +143,13 @@ export default function ChatScreen() {
146
143
! ( await sendMessage (
147
144
currConvId ,
148
145
lastMsgNodeId ,
149
- inputMsg ,
146
+ lastInpMsg ,
150
147
currExtra ,
151
148
onChunk
152
149
) )
153
150
) {
154
151
// restore the input message if failed
155
- setInputMsg ( lastInpMsg ) ;
152
+ textarea . setValue ( lastInpMsg ) ;
156
153
}
157
154
// OK
158
155
clearExtraContext ( ) ;
@@ -195,16 +192,13 @@ export default function ChatScreen() {
195
192
// send the prefilled message if needed
196
193
sendNewMessage ( ) ;
197
194
} else {
198
- // otherwise, focus on the input and move the cursor to the end
199
- if ( inputRef . current ) {
200
- inputRef . current . focus ( ) ;
201
- inputRef . current . selectionStart = inputRef . current . value . length ;
202
- }
195
+ // otherwise, focus on the input
196
+ textarea . focus ( ) ;
203
197
}
204
198
prefilledMsg . clear ( ) ;
205
199
// no need to keep track of sendNewMessage
206
200
// eslint-disable-next-line react-hooks/exhaustive-deps
207
- } , [ inputRef ] ) ;
201
+ } , [ textarea . ref ] ) ;
208
202
209
203
// due to some timing issues of StorageUtils.appendMsg(), we need to make sure the pendingMsg is not duplicated upon rendering (i.e. appears once in the saved conversation and once in the pendingMsg)
210
204
const pendingMsgDisplay : MessageDisplay [ ] =
@@ -258,9 +252,7 @@ export default function ChatScreen() {
258
252
< textarea
259
253
className = "textarea textarea-bordered w-full"
260
254
placeholder = "Type a message (Shift+Enter to add a new line)"
261
- ref = { inputRef }
262
- value = { inputMsg }
263
- onChange = { ( e ) => setInputMsg ( e . target . value ) }
255
+ ref = { textarea . ref }
264
256
onKeyDown = { ( e ) => {
265
257
if ( e . nativeEvent . isComposing || e . keyCode === 229 ) return ;
266
258
if ( e . key === 'Enter' && e . shiftKey ) return ;
@@ -280,11 +272,7 @@ export default function ChatScreen() {
280
272
Stop
281
273
</ button >
282
274
) : (
283
- < button
284
- className = "btn btn-primary ml-2"
285
- onClick = { sendNewMessage }
286
- disabled = { inputMsg . trim ( ) . length === 0 }
287
- >
275
+ < button className = "btn btn-primary ml-2" onClick = { sendNewMessage } >
288
276
Send
289
277
</ button >
290
278
) }
@@ -298,3 +286,43 @@ export default function ChatScreen() {
298
286
</ div >
299
287
) ;
300
288
}
289
+
290
+ export interface OptimizedTextareaValue {
291
+ value : ( ) => string ;
292
+ setValue : ( value : string ) => void ;
293
+ focus : ( ) => void ;
294
+ ref : React . RefObject < HTMLTextAreaElement > ;
295
+ }
296
+
297
+ // This is a workaround to prevent the textarea from re-rendering when the inner content changes
298
+ // See https://github.com/ggml-org/llama.cpp/pull/12299
299
+ function useOptimizedTextarea ( initValue : string ) : OptimizedTextareaValue {
300
+ const [ savedInitValue , setSavedInitValue ] = useState < string > ( initValue ) ;
301
+ const textareaRef = useRef < HTMLTextAreaElement > ( null ) ;
302
+
303
+ useEffect ( ( ) => {
304
+ if ( textareaRef . current && savedInitValue ) {
305
+ textareaRef . current . value = savedInitValue ;
306
+ setSavedInitValue ( '' ) ;
307
+ }
308
+ } , [ textareaRef , savedInitValue , setSavedInitValue ] ) ;
309
+
310
+ return {
311
+ value : ( ) => {
312
+ return textareaRef . current ?. value ?? savedInitValue ;
313
+ } ,
314
+ setValue : ( value : string ) => {
315
+ if ( textareaRef . current ) {
316
+ textareaRef . current . value = value ;
317
+ }
318
+ } ,
319
+ focus : ( ) => {
320
+ if ( textareaRef . current ) {
321
+ // focus and move the cursor to the end
322
+ textareaRef . current . focus ( ) ;
323
+ textareaRef . current . selectionStart = textareaRef . current . value . length ;
324
+ }
325
+ } ,
326
+ ref : textareaRef ,
327
+ } ;
328
+ }
0 commit comments