Skip to content

Commit c4ce55a

Browse files
committed
use signal-style implementation
1 parent 2eb91bf commit c4ce55a

File tree

3 files changed

+66
-33
lines changed

3 files changed

+66
-33
lines changed
114 Bytes
Binary file not shown.

examples/server/webui/src/components/ChatScreen.tsx

Lines changed: 61 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -99,16 +99,9 @@ export default function ChatScreen() {
9999
canvasData,
100100
replaceMessageAndGenerate,
101101
} = useAppContext();
102-
const inputRef = useRef<HTMLTextAreaElement>(null);
102+
const textarea = useOptimizedTextarea(prefilledMsg.content());
103103

104-
const { extraContext, clearExtraContext } = useVSCodeContext(
105-
inputRef,
106-
(msg) => {
107-
if (inputRef?.current) {
108-
inputRef.current.value = msg;
109-
}
110-
}
111-
);
104+
const { extraContext, clearExtraContext } = useVSCodeContext(textarea);
112105
// TODO: improve this when we have "upload file" feature
113106
const currExtra: Message['extra'] = extraContext ? [extraContext] : undefined;
114107

@@ -138,14 +131,10 @@ export default function ChatScreen() {
138131
};
139132

140133
const sendNewMessage = async () => {
141-
if (
142-
!inputRef.current ||
143-
inputRef.current.value.trim().length === 0 ||
144-
isGenerating(currConvId ?? '')
145-
)
134+
const lastInpMsg = textarea.value();
135+
if (lastInpMsg.trim().length === 0 || isGenerating(currConvId ?? ''))
146136
return;
147-
const inputValue = inputRef.current.value;
148-
inputRef.current.value = '';
137+
textarea.setValue('');
149138
scrollToBottom(false);
150139
setCurrNodeId(-1);
151140
// get the last message node
@@ -154,13 +143,13 @@ export default function ChatScreen() {
154143
!(await sendMessage(
155144
currConvId,
156145
lastMsgNodeId,
157-
inputValue,
146+
lastInpMsg,
158147
currExtra,
159148
onChunk
160149
))
161150
) {
162151
// restore the input message if failed
163-
inputRef.current.value = inputValue;
152+
textarea.setValue(lastInpMsg);
164153
}
165154
// OK
166155
clearExtraContext();
@@ -203,16 +192,13 @@ export default function ChatScreen() {
203192
// send the prefilled message if needed
204193
sendNewMessage();
205194
} else {
206-
// otherwise, focus on the input and move the cursor to the end
207-
if (inputRef.current) {
208-
inputRef.current.focus();
209-
inputRef.current.selectionStart = inputRef.current.value.length;
210-
}
195+
// otherwise, focus on the input
196+
textarea.focus();
211197
}
212198
prefilledMsg.clear();
213199
// no need to keep track of sendNewMessage
214200
// eslint-disable-next-line react-hooks/exhaustive-deps
215-
}, [inputRef]);
201+
}, [textarea.ref]);
216202

217203
// 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)
218204
const pendingMsgDisplay: MessageDisplay[] =
@@ -266,8 +252,7 @@ export default function ChatScreen() {
266252
<textarea
267253
className="textarea textarea-bordered w-full"
268254
placeholder="Type a message (Shift+Enter to add a new line)"
269-
ref={inputRef}
270-
defaultValue={prefilledMsg.content()}
255+
ref={textarea.ref}
271256
onKeyDown={(e) => {
272257
if (e.nativeEvent.isComposing || e.keyCode === 229) return;
273258
if (e.key === 'Enter' && e.shiftKey) return;
@@ -301,3 +286,53 @@ export default function ChatScreen() {
301286
</div>
302287
);
303288
}
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] = useState<string>(initValue);
301+
const textareaRef = useRef<HTMLTextAreaElement>(null);
302+
303+
useEffect(() => {
304+
if (textareaRef.current) {
305+
textareaRef.current.value = savedInitValue;
306+
console.log(
307+
'useOptimizedTextarea: set initValue',
308+
savedInitValue,
309+
textareaRef.current
310+
);
311+
}
312+
}, [textareaRef, savedInitValue]);
313+
314+
return {
315+
value: () => {
316+
return textareaRef.current?.value ?? savedInitValue;
317+
},
318+
setValue: (value: string) => {
319+
if (textareaRef.current) {
320+
textareaRef.current.value = value;
321+
console.log(
322+
'useOptimizedTextarea: set value',
323+
value,
324+
textareaRef.current
325+
);
326+
}
327+
},
328+
focus: () => {
329+
if (textareaRef.current) {
330+
// focus and move the cursor to the end
331+
textareaRef.current.focus();
332+
textareaRef.current.selectionStart = textareaRef.current.value.length;
333+
console.log('useOptimizedTextarea: focus', textareaRef.current);
334+
}
335+
},
336+
ref: textareaRef,
337+
};
338+
}

examples/server/webui/src/utils/llama-vscode.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useEffect, useState } from 'react';
22
import { MessageExtraContext } from './types';
3+
import { OptimizedTextareaValue } from '../components/ChatScreen';
34

45
// Extra context when using llama.cpp WebUI from llama-vscode, inside an iframe
56
// Ref: https://github.com/ggml-org/llama.cpp/pull/11940
@@ -14,10 +15,7 @@ interface SetTextEvData {
1415
* window.postMessage({ command: 'setText', text: 'Spot the syntax error', context: 'def test()\n return 123' }, '*');
1516
*/
1617

17-
export const useVSCodeContext = (
18-
inputRef: React.RefObject<HTMLTextAreaElement>,
19-
setInputMsg: (text: string) => void
20-
) => {
18+
export const useVSCodeContext = (textarea: OptimizedTextareaValue) => {
2119
const [extraContext, setExtraContext] = useState<MessageExtraContext | null>(
2220
null
2321
);
@@ -27,20 +25,20 @@ export const useVSCodeContext = (
2725
const handleMessage = (event: MessageEvent) => {
2826
if (event.data?.command === 'setText') {
2927
const data: SetTextEvData = event.data;
30-
setInputMsg(data?.text);
28+
textarea.setValue(data?.text);
3129
if (data?.context && data.context.length > 0) {
3230
setExtraContext({
3331
type: 'context',
3432
content: data.context,
3533
});
3634
}
37-
inputRef.current?.focus();
35+
textarea.focus();
3836
}
3937
};
4038

4139
window.addEventListener('message', handleMessage);
4240
return () => window.removeEventListener('message', handleMessage);
43-
}, [inputRef, setInputMsg]);
41+
}, [textarea]);
4442

4543
// Add a keydown listener that sends the "escapePressed" message to the parent window
4644
useEffect(() => {

0 commit comments

Comments
 (0)