diff --git a/examples/server/public/index.html.gz b/examples/server/public/index.html.gz index 1925b334b..3acd603ab 100644 Binary files a/examples/server/public/index.html.gz and b/examples/server/public/index.html.gz differ diff --git a/examples/server/webui/src/components/ChatMessage.tsx b/examples/server/webui/src/components/ChatMessage.tsx index 68be7c751..40ea74711 100644 --- a/examples/server/webui/src/components/ChatMessage.tsx +++ b/examples/server/webui/src/components/ChatMessage.tsx @@ -159,6 +159,35 @@ export default function ChatMessage({ )} + + {msg.extra && msg.extra.length > 0 && ( + + + Extra content + + + {msg.extra.map( + (extra, i) => + extra.type === 'textFile' ? ( + + {extra.name} + {extra.content} + + ) : extra.type === 'context' ? ( + + {extra.content} + + ) : null // TODO: support other extra types + )} + + + )} + (null); + + const { extraContext, clearExtraContext } = useVSCodeContext( + inputRef, + setInputMsg + ); + // TODO: improve this when we have "upload file" feature + const currExtra: Message['extra'] = extraContext ? [extraContext] : undefined; // keep track of leaf node for rendering const [currNodeId, setCurrNodeId] = useState(-1); @@ -115,10 +124,20 @@ export default function ChatScreen() { setCurrNodeId(-1); // get the last message node const lastMsgNodeId = messages.at(-1)?.msg.id ?? null; - if (!(await sendMessage(currConvId, lastMsgNodeId, inputMsg, onChunk))) { + if ( + !(await sendMessage( + currConvId, + lastMsgNodeId, + inputMsg, + currExtra, + onChunk + )) + ) { // restore the input message if failed setInputMsg(lastInpMsg); } + // OK + clearExtraContext(); }; const handleEditMessage = async (msg: Message, content: string) => { @@ -129,6 +148,7 @@ export default function ChatScreen() { viewingChat.conv.id, msg.parent, content, + msg.extra, onChunk ); setCurrNodeId(-1); @@ -143,6 +163,7 @@ export default function ChatScreen() { viewingChat.conv.id, msg.parent, null, + msg.extra, onChunk ); setCurrNodeId(-1); @@ -203,6 +224,7 @@ export default function ChatScreen() { setInputMsg(e.target.value)} onKeyDown={(e) => { diff --git a/examples/server/webui/src/utils/app.context.tsx b/examples/server/webui/src/utils/app.context.tsx index f2c935e1f..54bb65b6e 100644 --- a/examples/server/webui/src/utils/app.context.tsx +++ b/examples/server/webui/src/utils/app.context.tsx @@ -25,6 +25,7 @@ interface AppContextValue { convId: string | null, leafNodeId: Message['id'] | null, content: string, + extra: Message['extra'], onChunk: CallbackGeneratedChunk ) => Promise; stopGenerating: (convId: string) => void; @@ -32,6 +33,7 @@ interface AppContextValue { convId: string, parentNodeId: Message['id'], // the parent node of the message to be replaced content: string | null, + extra: Message['extra'], onChunk: CallbackGeneratedChunk ) => Promise; @@ -274,6 +276,7 @@ export const AppContextProvider = ({ convId: string | null, leafNodeId: Message['id'] | null, content: string, + extra: Message['extra'], onChunk: CallbackGeneratedChunk ): Promise => { if (isGenerating(convId ?? '') || content.trim().length === 0) return false; @@ -298,6 +301,7 @@ export const AppContextProvider = ({ convId, role: 'user', content, + extra, parent: leafNodeId, children: [], }, @@ -324,6 +328,7 @@ export const AppContextProvider = ({ convId: string, parentNodeId: Message['id'], // the parent node of the message to be replaced content: string | null, + extra: Message['extra'], onChunk: CallbackGeneratedChunk ) => { if (isGenerating(convId)) return; @@ -339,6 +344,7 @@ export const AppContextProvider = ({ convId, role: 'user', content, + extra, parent: parentNodeId, children: [], }, diff --git a/examples/server/webui/src/utils/llama-vscode.ts b/examples/server/webui/src/utils/llama-vscode.ts new file mode 100644 index 000000000..6c23221c4 --- /dev/null +++ b/examples/server/webui/src/utils/llama-vscode.ts @@ -0,0 +1,62 @@ +import { useEffect, useState } from 'react'; +import { MessageExtraContext } from './types'; + +// Extra context when using llama.cpp WebUI from llama-vscode, inside an iframe +// Ref: https://github.com/ggml-org/llama.cpp/pull/11940 + +interface SetTextEvData { + text: string; + context: string; +} + +/** + * To test it: + * window.postMessage({ command: 'setText', text: 'Spot the syntax error', context: 'def test()\n return 123' }, '*'); + */ + +export const useVSCodeContext = ( + inputRef: React.RefObject, + setInputMsg: (text: string) => void +) => { + const [extraContext, setExtraContext] = useState( + null + ); + + // Accept setText message from a parent window and set inputMsg and extraContext + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + if (event.data?.command === 'setText') { + const data: SetTextEvData = event.data; + setInputMsg(data?.text); + if (data?.context && data.context.length > 0) { + setExtraContext({ + type: 'context', + content: data.context, + }); + } + inputRef.current?.focus(); + } + }; + + window.addEventListener('message', handleMessage); + return () => window.removeEventListener('message', handleMessage); + }, []); + + // Add a keydown listener that sends the "escapePressed" message to the parent window + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + window.parent.postMessage({ command: 'escapePressed' }, '*'); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, []); + + return { + extraContext, + // call once the user message is sent, to clear the extra context + clearExtraContext: () => setExtraContext(null), + }; +}; diff --git a/examples/server/webui/src/utils/misc.ts b/examples/server/webui/src/utils/misc.ts index d7f81d0e2..d46322862 100644 --- a/examples/server/webui/src/utils/misc.ts +++ b/examples/server/webui/src/utils/misc.ts @@ -53,12 +53,23 @@ export const copyStr = (textToCopy: string) => { /** * filter out redundant fields upon sending to API + * also format extra into text */ export function normalizeMsgsForAPI(messages: Readonly) { return messages.map((msg) => { + let newContent = ''; + + for (const extra of msg.extra ?? []) { + if (extra.type === 'context') { + newContent += `${extra.content}\n\n`; + } + } + + newContent += msg.content; + return { role: msg.role, - content: msg.content, + content: newContent, }; }) as APIMessage[]; } diff --git a/examples/server/webui/src/utils/types.ts b/examples/server/webui/src/utils/types.ts index e85049f20..0eb774001 100644 --- a/examples/server/webui/src/utils/types.ts +++ b/examples/server/webui/src/utils/types.ts @@ -42,11 +42,25 @@ export interface Message { role: 'user' | 'assistant' | 'system'; content: string; timings?: TimingReport; + extra?: MessageExtra[]; // node based system for branching parent: Message['id']; children: Message['id'][]; } +type MessageExtra = MessageExtraTextFile | MessageExtraContext; // TODO: will add more in the future + +export interface MessageExtraTextFile { + type: 'textFile'; + name: string; + content: string; +} + +export interface MessageExtraContext { + type: 'context'; + content: string; +} + export type APIMessage = Pick; export interface Conversation {
{extra.content}