Skip to content

Commit 5028128

Browse files
igardevigardevngxson
authored andcommitted
server : (webui) Enable communication with parent html (if webui is in iframe) (ggml-org#11940)
* Webui: Enable communication with parent html (if webui is in iframe): - Listens for "setText" command from parent with "text" and "context" fields. "text" is set in inputMsg, "context" is used as hidden context on the following requests to the llama.cpp server - On pressing na Escape button sends command "escapePressed" to the parent Example handling from the parent html side: - Send command "setText" from parent html to webui in iframe: const iframe = document.getElementById('askAiIframe'); if (iframe) { iframe.contentWindow.postMessage({ command: 'setText', text: text, context: context }, '*'); } - Listen for Escape key from webui on parent html: // Listen for escape key event in the iframe window.addEventListener('keydown', (event) => { if (event.key === 'Escape') { // Process case when Escape is pressed inside webui } }); * Move the extraContext from storage to app.context. * Fix formatting. * add Message.extra * format + build * MessageExtraContext * build * fix display * rm console.log --------- Co-authored-by: igardev <[email protected]> Co-authored-by: Xuan Son Nguyen <[email protected]>
1 parent f64c3d0 commit 5028128

File tree

7 files changed

+147
-3
lines changed

7 files changed

+147
-3
lines changed

examples/server/public/index.html.gz

462 Bytes
Binary file not shown.

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

+29
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,35 @@ export default function ChatMessage({
159159
</div>
160160
</details>
161161
)}
162+
163+
{msg.extra && msg.extra.length > 0 && (
164+
<details
165+
className={classNames({
166+
'collapse collapse-arrow mb-4 bg-base-200': true,
167+
'bg-opacity-10': msg.role !== 'assistant',
168+
})}
169+
>
170+
<summary className="collapse-title">
171+
Extra content
172+
</summary>
173+
<div className="collapse-content">
174+
{msg.extra.map(
175+
(extra, i) =>
176+
extra.type === 'textFile' ? (
177+
<div key={extra.name}>
178+
<b>{extra.name}</b>
179+
<pre>{extra.content}</pre>
180+
</div>
181+
) : extra.type === 'context' ? (
182+
<div key={i}>
183+
<pre>{extra.content}</pre>
184+
</div>
185+
) : null // TODO: support other extra types
186+
)}
187+
</div>
188+
</details>
189+
)}
190+
162191
<MarkdownDisplay
163192
content={content}
164193
isGenerating={isPending}

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

+24-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { useEffect, useMemo, useState } from 'react';
1+
import { useEffect, useMemo, useRef, useState } from 'react';
22
import { CallbackGeneratedChunk, useAppContext } from '../utils/app.context';
33
import ChatMessage from './ChatMessage';
44
import { CanvasType, Message, PendingMessage } from '../utils/types';
55
import { classNames, throttle } from '../utils/misc';
66
import CanvasPyInterpreter from './CanvasPyInterpreter';
77
import StorageUtils from '../utils/storage';
8+
import { useVSCodeContext } from '../utils/llama-vscode';
89

910
/**
1011
* A message display is a message node with additional information for rendering.
@@ -81,6 +82,14 @@ export default function ChatScreen() {
8182
replaceMessageAndGenerate,
8283
} = useAppContext();
8384
const [inputMsg, setInputMsg] = useState('');
85+
const inputRef = useRef<HTMLTextAreaElement>(null);
86+
87+
const { extraContext, clearExtraContext } = useVSCodeContext(
88+
inputRef,
89+
setInputMsg
90+
);
91+
// TODO: improve this when we have "upload file" feature
92+
const currExtra: Message['extra'] = extraContext ? [extraContext] : undefined;
8493

8594
// keep track of leaf node for rendering
8695
const [currNodeId, setCurrNodeId] = useState<number>(-1);
@@ -115,10 +124,20 @@ export default function ChatScreen() {
115124
setCurrNodeId(-1);
116125
// get the last message node
117126
const lastMsgNodeId = messages.at(-1)?.msg.id ?? null;
118-
if (!(await sendMessage(currConvId, lastMsgNodeId, inputMsg, onChunk))) {
127+
if (
128+
!(await sendMessage(
129+
currConvId,
130+
lastMsgNodeId,
131+
inputMsg,
132+
currExtra,
133+
onChunk
134+
))
135+
) {
119136
// restore the input message if failed
120137
setInputMsg(lastInpMsg);
121138
}
139+
// OK
140+
clearExtraContext();
122141
};
123142

124143
const handleEditMessage = async (msg: Message, content: string) => {
@@ -129,6 +148,7 @@ export default function ChatScreen() {
129148
viewingChat.conv.id,
130149
msg.parent,
131150
content,
151+
msg.extra,
132152
onChunk
133153
);
134154
setCurrNodeId(-1);
@@ -143,6 +163,7 @@ export default function ChatScreen() {
143163
viewingChat.conv.id,
144164
msg.parent,
145165
null,
166+
msg.extra,
146167
onChunk
147168
);
148169
setCurrNodeId(-1);
@@ -203,6 +224,7 @@ export default function ChatScreen() {
203224
<textarea
204225
className="textarea textarea-bordered w-full"
205226
placeholder="Type a message (Shift+Enter to add a new line)"
227+
ref={inputRef}
206228
value={inputMsg}
207229
onChange={(e) => setInputMsg(e.target.value)}
208230
onKeyDown={(e) => {

examples/server/webui/src/utils/app.context.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@ interface AppContextValue {
2525
convId: string | null,
2626
leafNodeId: Message['id'] | null,
2727
content: string,
28+
extra: Message['extra'],
2829
onChunk: CallbackGeneratedChunk
2930
) => Promise<boolean>;
3031
stopGenerating: (convId: string) => void;
3132
replaceMessageAndGenerate: (
3233
convId: string,
3334
parentNodeId: Message['id'], // the parent node of the message to be replaced
3435
content: string | null,
36+
extra: Message['extra'],
3537
onChunk: CallbackGeneratedChunk
3638
) => Promise<void>;
3739

@@ -274,6 +276,7 @@ export const AppContextProvider = ({
274276
convId: string | null,
275277
leafNodeId: Message['id'] | null,
276278
content: string,
279+
extra: Message['extra'],
277280
onChunk: CallbackGeneratedChunk
278281
): Promise<boolean> => {
279282
if (isGenerating(convId ?? '') || content.trim().length === 0) return false;
@@ -298,6 +301,7 @@ export const AppContextProvider = ({
298301
convId,
299302
role: 'user',
300303
content,
304+
extra,
301305
parent: leafNodeId,
302306
children: [],
303307
},
@@ -324,6 +328,7 @@ export const AppContextProvider = ({
324328
convId: string,
325329
parentNodeId: Message['id'], // the parent node of the message to be replaced
326330
content: string | null,
331+
extra: Message['extra'],
327332
onChunk: CallbackGeneratedChunk
328333
) => {
329334
if (isGenerating(convId)) return;
@@ -339,6 +344,7 @@ export const AppContextProvider = ({
339344
convId,
340345
role: 'user',
341346
content,
347+
extra,
342348
parent: parentNodeId,
343349
children: [],
344350
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { useEffect, useState } from 'react';
2+
import { MessageExtraContext } from './types';
3+
4+
// Extra context when using llama.cpp WebUI from llama-vscode, inside an iframe
5+
// Ref: https://github.com/ggml-org/llama.cpp/pull/11940
6+
7+
interface SetTextEvData {
8+
text: string;
9+
context: string;
10+
}
11+
12+
/**
13+
* To test it:
14+
* window.postMessage({ command: 'setText', text: 'Spot the syntax error', context: 'def test()\n return 123' }, '*');
15+
*/
16+
17+
export const useVSCodeContext = (
18+
inputRef: React.RefObject<HTMLTextAreaElement>,
19+
setInputMsg: (text: string) => void
20+
) => {
21+
const [extraContext, setExtraContext] = useState<MessageExtraContext | null>(
22+
null
23+
);
24+
25+
// Accept setText message from a parent window and set inputMsg and extraContext
26+
useEffect(() => {
27+
const handleMessage = (event: MessageEvent) => {
28+
if (event.data?.command === 'setText') {
29+
const data: SetTextEvData = event.data;
30+
setInputMsg(data?.text);
31+
if (data?.context && data.context.length > 0) {
32+
setExtraContext({
33+
type: 'context',
34+
content: data.context,
35+
});
36+
}
37+
inputRef.current?.focus();
38+
}
39+
};
40+
41+
window.addEventListener('message', handleMessage);
42+
return () => window.removeEventListener('message', handleMessage);
43+
}, []);
44+
45+
// Add a keydown listener that sends the "escapePressed" message to the parent window
46+
useEffect(() => {
47+
const handleKeyDown = (event: KeyboardEvent) => {
48+
if (event.key === 'Escape') {
49+
window.parent.postMessage({ command: 'escapePressed' }, '*');
50+
}
51+
};
52+
53+
window.addEventListener('keydown', handleKeyDown);
54+
return () => window.removeEventListener('keydown', handleKeyDown);
55+
}, []);
56+
57+
return {
58+
extraContext,
59+
// call once the user message is sent, to clear the extra context
60+
clearExtraContext: () => setExtraContext(null),
61+
};
62+
};

examples/server/webui/src/utils/misc.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,23 @@ export const copyStr = (textToCopy: string) => {
5353

5454
/**
5555
* filter out redundant fields upon sending to API
56+
* also format extra into text
5657
*/
5758
export function normalizeMsgsForAPI(messages: Readonly<Message[]>) {
5859
return messages.map((msg) => {
60+
let newContent = '';
61+
62+
for (const extra of msg.extra ?? []) {
63+
if (extra.type === 'context') {
64+
newContent += `${extra.content}\n\n`;
65+
}
66+
}
67+
68+
newContent += msg.content;
69+
5970
return {
6071
role: msg.role,
61-
content: msg.content,
72+
content: newContent,
6273
};
6374
}) as APIMessage[];
6475
}

examples/server/webui/src/utils/types.ts

+14
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,25 @@ export interface Message {
4242
role: 'user' | 'assistant' | 'system';
4343
content: string;
4444
timings?: TimingReport;
45+
extra?: MessageExtra[];
4546
// node based system for branching
4647
parent: Message['id'];
4748
children: Message['id'][];
4849
}
4950

51+
type MessageExtra = MessageExtraTextFile | MessageExtraContext; // TODO: will add more in the future
52+
53+
export interface MessageExtraTextFile {
54+
type: 'textFile';
55+
name: string;
56+
content: string;
57+
}
58+
59+
export interface MessageExtraContext {
60+
type: 'context';
61+
content: string;
62+
}
63+
5064
export type APIMessage = Pick<Message, 'role' | 'content'>;
5165

5266
export interface Conversation {

0 commit comments

Comments
 (0)