mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2025-06-28 20:25:20 +00:00
webui : Prevent rerendering on textarea input (#12299)
* webui: Make textarea uncontrolled to eliminate devastating lag * Update index.html.gz * use signal-style implementation * rm console log * no duplicated savedInitValue set --------- Co-authored-by: Xuan Son Nguyen <son@huggingface.co>
This commit is contained in:
Binary file not shown.
@ -99,13 +99,9 @@ export default function ChatScreen() {
|
|||||||
canvasData,
|
canvasData,
|
||||||
replaceMessageAndGenerate,
|
replaceMessageAndGenerate,
|
||||||
} = useAppContext();
|
} = useAppContext();
|
||||||
const [inputMsg, setInputMsg] = useState(prefilledMsg.content());
|
const textarea = useOptimizedTextarea(prefilledMsg.content());
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
||||||
|
|
||||||
const { extraContext, clearExtraContext } = useVSCodeContext(
|
const { extraContext, clearExtraContext } = useVSCodeContext(textarea);
|
||||||
inputRef,
|
|
||||||
setInputMsg
|
|
||||||
);
|
|
||||||
// TODO: improve this when we have "upload file" feature
|
// TODO: improve this when we have "upload file" feature
|
||||||
const currExtra: Message['extra'] = extraContext ? [extraContext] : undefined;
|
const currExtra: Message['extra'] = extraContext ? [extraContext] : undefined;
|
||||||
|
|
||||||
@ -135,9 +131,10 @@ export default function ChatScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const sendNewMessage = async () => {
|
const sendNewMessage = async () => {
|
||||||
if (inputMsg.trim().length === 0 || isGenerating(currConvId ?? '')) return;
|
const lastInpMsg = textarea.value();
|
||||||
const lastInpMsg = inputMsg;
|
if (lastInpMsg.trim().length === 0 || isGenerating(currConvId ?? ''))
|
||||||
setInputMsg('');
|
return;
|
||||||
|
textarea.setValue('');
|
||||||
scrollToBottom(false);
|
scrollToBottom(false);
|
||||||
setCurrNodeId(-1);
|
setCurrNodeId(-1);
|
||||||
// get the last message node
|
// get the last message node
|
||||||
@ -146,13 +143,13 @@ export default function ChatScreen() {
|
|||||||
!(await sendMessage(
|
!(await sendMessage(
|
||||||
currConvId,
|
currConvId,
|
||||||
lastMsgNodeId,
|
lastMsgNodeId,
|
||||||
inputMsg,
|
lastInpMsg,
|
||||||
currExtra,
|
currExtra,
|
||||||
onChunk
|
onChunk
|
||||||
))
|
))
|
||||||
) {
|
) {
|
||||||
// restore the input message if failed
|
// restore the input message if failed
|
||||||
setInputMsg(lastInpMsg);
|
textarea.setValue(lastInpMsg);
|
||||||
}
|
}
|
||||||
// OK
|
// OK
|
||||||
clearExtraContext();
|
clearExtraContext();
|
||||||
@ -195,16 +192,13 @@ export default function ChatScreen() {
|
|||||||
// send the prefilled message if needed
|
// send the prefilled message if needed
|
||||||
sendNewMessage();
|
sendNewMessage();
|
||||||
} else {
|
} else {
|
||||||
// otherwise, focus on the input and move the cursor to the end
|
// otherwise, focus on the input
|
||||||
if (inputRef.current) {
|
textarea.focus();
|
||||||
inputRef.current.focus();
|
|
||||||
inputRef.current.selectionStart = inputRef.current.value.length;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
prefilledMsg.clear();
|
prefilledMsg.clear();
|
||||||
// no need to keep track of sendNewMessage
|
// no need to keep track of sendNewMessage
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [inputRef]);
|
}, [textarea.ref]);
|
||||||
|
|
||||||
// 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)
|
// 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)
|
||||||
const pendingMsgDisplay: MessageDisplay[] =
|
const pendingMsgDisplay: MessageDisplay[] =
|
||||||
@ -258,9 +252,7 @@ export default function ChatScreen() {
|
|||||||
<textarea
|
<textarea
|
||||||
className="textarea textarea-bordered w-full"
|
className="textarea textarea-bordered w-full"
|
||||||
placeholder="Type a message (Shift+Enter to add a new line)"
|
placeholder="Type a message (Shift+Enter to add a new line)"
|
||||||
ref={inputRef}
|
ref={textarea.ref}
|
||||||
value={inputMsg}
|
|
||||||
onChange={(e) => setInputMsg(e.target.value)}
|
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.nativeEvent.isComposing || e.keyCode === 229) return;
|
if (e.nativeEvent.isComposing || e.keyCode === 229) return;
|
||||||
if (e.key === 'Enter' && e.shiftKey) return;
|
if (e.key === 'Enter' && e.shiftKey) return;
|
||||||
@ -280,11 +272,7 @@ export default function ChatScreen() {
|
|||||||
Stop
|
Stop
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button className="btn btn-primary ml-2" onClick={sendNewMessage}>
|
||||||
className="btn btn-primary ml-2"
|
|
||||||
onClick={sendNewMessage}
|
|
||||||
disabled={inputMsg.trim().length === 0}
|
|
||||||
>
|
|
||||||
Send
|
Send
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@ -298,3 +286,43 @@ export default function ChatScreen() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OptimizedTextareaValue {
|
||||||
|
value: () => string;
|
||||||
|
setValue: (value: string) => void;
|
||||||
|
focus: () => void;
|
||||||
|
ref: React.RefObject<HTMLTextAreaElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a workaround to prevent the textarea from re-rendering when the inner content changes
|
||||||
|
// See https://github.com/ggml-org/llama.cpp/pull/12299
|
||||||
|
function useOptimizedTextarea(initValue: string): OptimizedTextareaValue {
|
||||||
|
const [savedInitValue, setSavedInitValue] = useState<string>(initValue);
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (textareaRef.current && savedInitValue) {
|
||||||
|
textareaRef.current.value = savedInitValue;
|
||||||
|
setSavedInitValue('');
|
||||||
|
}
|
||||||
|
}, [textareaRef, savedInitValue, setSavedInitValue]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: () => {
|
||||||
|
return textareaRef.current?.value ?? savedInitValue;
|
||||||
|
},
|
||||||
|
setValue: (value: string) => {
|
||||||
|
if (textareaRef.current) {
|
||||||
|
textareaRef.current.value = value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
focus: () => {
|
||||||
|
if (textareaRef.current) {
|
||||||
|
// focus and move the cursor to the end
|
||||||
|
textareaRef.current.focus();
|
||||||
|
textareaRef.current.selectionStart = textareaRef.current.value.length;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ref: textareaRef,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { MessageExtraContext } from './types';
|
import { MessageExtraContext } from './types';
|
||||||
|
import { OptimizedTextareaValue } from '../components/ChatScreen';
|
||||||
|
|
||||||
// Extra context when using llama.cpp WebUI from llama-vscode, inside an iframe
|
// Extra context when using llama.cpp WebUI from llama-vscode, inside an iframe
|
||||||
// Ref: https://github.com/ggml-org/llama.cpp/pull/11940
|
// Ref: https://github.com/ggml-org/llama.cpp/pull/11940
|
||||||
@ -14,10 +15,7 @@ interface SetTextEvData {
|
|||||||
* window.postMessage({ command: 'setText', text: 'Spot the syntax error', context: 'def test()\n return 123' }, '*');
|
* window.postMessage({ command: 'setText', text: 'Spot the syntax error', context: 'def test()\n return 123' }, '*');
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const useVSCodeContext = (
|
export const useVSCodeContext = (textarea: OptimizedTextareaValue) => {
|
||||||
inputRef: React.RefObject<HTMLTextAreaElement>,
|
|
||||||
setInputMsg: (text: string) => void
|
|
||||||
) => {
|
|
||||||
const [extraContext, setExtraContext] = useState<MessageExtraContext | null>(
|
const [extraContext, setExtraContext] = useState<MessageExtraContext | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
@ -27,20 +25,20 @@ export const useVSCodeContext = (
|
|||||||
const handleMessage = (event: MessageEvent) => {
|
const handleMessage = (event: MessageEvent) => {
|
||||||
if (event.data?.command === 'setText') {
|
if (event.data?.command === 'setText') {
|
||||||
const data: SetTextEvData = event.data;
|
const data: SetTextEvData = event.data;
|
||||||
setInputMsg(data?.text);
|
textarea.setValue(data?.text);
|
||||||
if (data?.context && data.context.length > 0) {
|
if (data?.context && data.context.length > 0) {
|
||||||
setExtraContext({
|
setExtraContext({
|
||||||
type: 'context',
|
type: 'context',
|
||||||
content: data.context,
|
content: data.context,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
inputRef.current?.focus();
|
textarea.focus();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('message', handleMessage);
|
window.addEventListener('message', handleMessage);
|
||||||
return () => window.removeEventListener('message', handleMessage);
|
return () => window.removeEventListener('message', handleMessage);
|
||||||
}, [inputRef, setInputMsg]);
|
}, [textarea]);
|
||||||
|
|
||||||
// Add a keydown listener that sends the "escapePressed" message to the parent window
|
// Add a keydown listener that sends the "escapePressed" message to the parent window
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
Reference in New Issue
Block a user