Skip to content

Commit 1920cc9

Browse files
committed
add Message.extra
1 parent cd32872 commit 1920cc9

File tree

7 files changed

+134
-39
lines changed

7 files changed

+134
-39
lines changed

examples/server/public/index.html.gz

256 Bytes
Binary file not shown.

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,30 @@ 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((extra) =>
175+
extra.type === 'textFile' ? (
176+
<div key={extra.name}>
177+
<b>{extra.name}</b>
178+
<pre>{extra.content}</pre>
179+
</div>
180+
) : null // TODO: support other extra types
181+
)}
182+
</div>
183+
</details>
184+
)}
185+
162186
<MarkdownDisplay
163187
content={content}
164188
isGenerating={isPending}

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

Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ 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.
@@ -79,36 +80,16 @@ export default function ChatScreen() {
7980
pendingMessages,
8081
canvasData,
8182
replaceMessageAndGenerate,
82-
setExtraContext
8383
} = useAppContext();
8484
const [inputMsg, setInputMsg] = useState('');
8585
const inputRef = useRef<HTMLTextAreaElement>(null);
8686

87-
// Accept setText message from a parent window and set inputMsg and extraContext
88-
useEffect(() => {
89-
const handleMessage = (event: MessageEvent) => {
90-
if (event.data?.command === 'setText') {
91-
setInputMsg(event.data?.text);
92-
setExtraContext(event.data?.context)
93-
inputRef.current?.focus();
94-
}
95-
};
96-
97-
window.addEventListener('message', handleMessage);
98-
return () => window.removeEventListener('message', handleMessage);
99-
}, []);
100-
101-
// Add a keydown listener that sends the "escapePressed" message to the parent window
102-
useEffect(() => {
103-
const handleKeyDown = (event: KeyboardEvent) => {
104-
if (event.key === 'Escape') {
105-
window.parent.postMessage({ command: 'escapePressed' }, '*');
106-
}
107-
};
108-
109-
window.addEventListener('keydown', handleKeyDown);
110-
return () => window.removeEventListener('keydown', handleKeyDown);
111-
}, []);
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;
11293

11394
// keep track of leaf node for rendering
11495
const [currNodeId, setCurrNodeId] = useState<number>(-1);
@@ -143,10 +124,20 @@ export default function ChatScreen() {
143124
setCurrNodeId(-1);
144125
// get the last message node
145126
const lastMsgNodeId = messages.at(-1)?.msg.id ?? null;
146-
if (!(await sendMessage(currConvId, lastMsgNodeId, inputMsg, onChunk))) {
127+
if (
128+
!(await sendMessage(
129+
currConvId,
130+
lastMsgNodeId,
131+
inputMsg,
132+
currExtra,
133+
onChunk
134+
))
135+
) {
147136
// restore the input message if failed
148137
setInputMsg(lastInpMsg);
149138
}
139+
// OK
140+
clearExtraContext();
150141
};
151142

152143
const handleEditMessage = async (msg: Message, content: string) => {
@@ -157,6 +148,7 @@ export default function ChatScreen() {
157148
viewingChat.conv.id,
158149
msg.parent,
159150
content,
151+
msg.extra,
160152
onChunk
161153
);
162154
setCurrNodeId(-1);
@@ -171,6 +163,7 @@ export default function ChatScreen() {
171163
viewingChat.conv.id,
172164
msg.parent,
173165
null,
166+
msg.extra,
174167
onChunk
175168
);
176169
setCurrNodeId(-1);

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

Lines changed: 6 additions & 11 deletions
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

@@ -44,10 +46,6 @@ interface AppContextValue {
4446
saveConfig: (config: typeof CONFIG_DEFAULT) => void;
4547
showSettings: boolean;
4648
setShowSettings: (show: boolean) => void;
47-
48-
// extra context
49-
extraContext: string;
50-
setExtraContext: (extraCtx: string) => void;
5149
}
5250

5351
// this callback is used for scrolling to the bottom of the chat and switching to the last node
@@ -86,7 +84,6 @@ export const AppContextProvider = ({
8684
const [config, setConfig] = useState(StorageUtils.getConfig());
8785
const [canvasData, setCanvasData] = useState<CanvasData | null>(null);
8886
const [showSettings, setShowSettings] = useState(false);
89-
const [extraContext, setExtraContext] = useState("");
9087

9188
// handle change when the convId from URL is changed
9289
useEffect(() => {
@@ -179,10 +176,6 @@ export const AppContextProvider = ({
179176
: [{ role: 'system', content: config.systemMessage } as APIMessage]),
180177
...normalizeMsgsForAPI(currMessages),
181178
];
182-
if (extraContext && extraContext != ""){
183-
// insert extra context just before the user messages
184-
messages.splice(config.systemMessage.length === 0 ? 0 : 1, 0, { role: 'user', content:extraContext } as APIMessage)
185-
}
186179
if (config.excludeThoughtOnReq) {
187180
messages = filterThoughtFromMsgs(messages);
188181
}
@@ -283,6 +276,7 @@ export const AppContextProvider = ({
283276
convId: string | null,
284277
leafNodeId: Message['id'] | null,
285278
content: string,
279+
extra: Message['extra'],
286280
onChunk: CallbackGeneratedChunk
287281
): Promise<boolean> => {
288282
if (isGenerating(convId ?? '') || content.trim().length === 0) return false;
@@ -307,6 +301,7 @@ export const AppContextProvider = ({
307301
convId,
308302
role: 'user',
309303
content,
304+
extra,
310305
parent: leafNodeId,
311306
children: [],
312307
},
@@ -333,6 +328,7 @@ export const AppContextProvider = ({
333328
convId: string,
334329
parentNodeId: Message['id'], // the parent node of the message to be replaced
335330
content: string | null,
331+
extra: Message['extra'],
336332
onChunk: CallbackGeneratedChunk
337333
) => {
338334
if (isGenerating(convId)) return;
@@ -348,6 +344,7 @@ export const AppContextProvider = ({
348344
convId,
349345
role: 'user',
350346
content,
347+
extra,
351348
parent: parentNodeId,
352349
children: [],
353350
},
@@ -380,8 +377,6 @@ export const AppContextProvider = ({
380377
saveConfig,
381378
showSettings,
382379
setShowSettings,
383-
extraContext,
384-
setExtraContext,
385380
}}
386381
>
387382
{children}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { useEffect, useState } from 'react';
2+
import { MessageExtraTextFile } 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<MessageExtraTextFile | null>(
22+
null
23+
);
24+
console.log({ extraContext });
25+
26+
// Accept setText message from a parent window and set inputMsg and extraContext
27+
useEffect(() => {
28+
const handleMessage = (event: MessageEvent) => {
29+
if (event.data?.command === 'setText') {
30+
const data: SetTextEvData = event.data;
31+
setInputMsg(data?.text);
32+
setExtraContext({
33+
type: 'textFile',
34+
name: '(context)', // TODO: set filename
35+
content: data?.context ?? '(no context)',
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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,24 @@ 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 === 'textFile') {
64+
// TODO: allow user to customize this via Settings
65+
newContent += `\n===\nExtra file: ${extra.name}\n${extra.content}\n\n===\n`;
66+
}
67+
}
68+
69+
newContent += msg.content;
70+
5971
return {
6072
role: msg.role,
61-
content: msg.content,
73+
content: newContent,
6274
};
6375
}) as APIMessage[];
6476
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,20 @@ 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; // TODO: will add more in the future
52+
53+
export interface MessageExtraTextFile {
54+
type: 'textFile';
55+
name: string;
56+
content: string;
57+
}
58+
5059
export type APIMessage = Pick<Message, 'role' | 'content'>;
5160

5261
export interface Conversation {

0 commit comments

Comments
 (0)