@@ -99,13 +99,9 @@ export default function ChatScreen() {
9999 canvasData,
100100 replaceMessageAndGenerate,
101101 } = useAppContext ( ) ;
102- const [ inputMsg , setInputMsg ] = useState ( prefilledMsg . content ( ) ) ;
103- const inputRef = useRef < HTMLTextAreaElement > ( null ) ;
102+ const textarea = useOptimizedTextarea ( prefilledMsg . content ( ) ) ;
104103
105- const { extraContext, clearExtraContext } = useVSCodeContext (
106- inputRef ,
107- setInputMsg
108- ) ;
104+ const { extraContext, clearExtraContext } = useVSCodeContext ( textarea ) ;
109105 // TODO: improve this when we have "upload file" feature
110106 const currExtra : Message [ 'extra' ] = extraContext ? [ extraContext ] : undefined ;
111107
@@ -135,9 +131,10 @@ export default function ChatScreen() {
135131 } ;
136132
137133 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 ( '' ) ;
141138 scrollToBottom ( false ) ;
142139 setCurrNodeId ( - 1 ) ;
143140 // get the last message node
@@ -146,13 +143,13 @@ export default function ChatScreen() {
146143 ! ( await sendMessage (
147144 currConvId ,
148145 lastMsgNodeId ,
149- inputMsg ,
146+ lastInpMsg ,
150147 currExtra ,
151148 onChunk
152149 ) )
153150 ) {
154151 // restore the input message if failed
155- setInputMsg ( lastInpMsg ) ;
152+ textarea . setValue ( lastInpMsg ) ;
156153 }
157154 // OK
158155 clearExtraContext ( ) ;
@@ -195,16 +192,13 @@ export default function ChatScreen() {
195192 // send the prefilled message if needed
196193 sendNewMessage ( ) ;
197194 } 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 ( ) ;
203197 }
204198 prefilledMsg . clear ( ) ;
205199 // no need to keep track of sendNewMessage
206200 // eslint-disable-next-line react-hooks/exhaustive-deps
207- } , [ inputRef ] ) ;
201+ } , [ textarea . ref ] ) ;
208202
209203 // 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)
210204 const pendingMsgDisplay : MessageDisplay [ ] =
@@ -258,9 +252,7 @@ export default function ChatScreen() {
258252 < textarea
259253 className = "textarea textarea-bordered w-full"
260254 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 }
264256 onKeyDown = { ( e ) => {
265257 if ( e . nativeEvent . isComposing || e . keyCode === 229 ) return ;
266258 if ( e . key === 'Enter' && e . shiftKey ) return ;
@@ -280,11 +272,7 @@ export default function ChatScreen() {
280272 Stop
281273 </ button >
282274 ) : (
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 } >
288276 Send
289277 </ button >
290278 ) }
@@ -298,3 +286,43 @@ export default function ChatScreen() {
298286 </ div >
299287 ) ;
300288}
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