</div>
</details>
)}
+
+ {msg.extra && msg.extra.length > 0 && (
+ <details
+ className={classNames({
+ 'collapse collapse-arrow mb-4 bg-base-200': true,
+ 'bg-opacity-10': msg.role !== 'assistant',
+ })}
+ >
+ <summary className="collapse-title">
+ Extra content
+ </summary>
+ <div className="collapse-content">
+ {msg.extra.map(
+ (extra, i) =>
+ extra.type === 'textFile' ? (
+ <div key={extra.name}>
+ <b>{extra.name}</b>
+ <pre>{extra.content}</pre>
+ </div>
+ ) : extra.type === 'context' ? (
+ <div key={i}>
+ <pre>{extra.content}</pre>
+ </div>
+ ) : null // TODO: support other extra types
+ )}
+ </div>
+ </details>
+ )}
+
<MarkdownDisplay
content={content}
isGenerating={isPending}
-import { useEffect, useMemo, useState } from 'react';
+import { useEffect, useMemo, useRef, useState } from 'react';
import { CallbackGeneratedChunk, useAppContext } from '../utils/app.context';
import ChatMessage from './ChatMessage';
import { CanvasType, Message, PendingMessage } from '../utils/types';
import { classNames, throttle } from '../utils/misc';
import CanvasPyInterpreter from './CanvasPyInterpreter';
import StorageUtils from '../utils/storage';
+import { useVSCodeContext } from '../utils/llama-vscode';
/**
* A message display is a message node with additional information for rendering.
replaceMessageAndGenerate,
} = useAppContext();
const [inputMsg, setInputMsg] = useState('');
+ const inputRef = useRef<HTMLTextAreaElement>(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<number>(-1);
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) => {
viewingChat.conv.id,
msg.parent,
content,
+ msg.extra,
onChunk
);
setCurrNodeId(-1);
viewingChat.conv.id,
msg.parent,
null,
+ msg.extra,
onChunk
);
setCurrNodeId(-1);
<textarea
className="textarea textarea-bordered w-full"
placeholder="Type a message (Shift+Enter to add a new line)"
+ ref={inputRef}
value={inputMsg}
onChange={(e) => setInputMsg(e.target.value)}
onKeyDown={(e) => {
convId: string | null,
leafNodeId: Message['id'] | null,
content: string,
+ extra: Message['extra'],
onChunk: CallbackGeneratedChunk
) => Promise<boolean>;
stopGenerating: (convId: string) => void;
convId: string,
parentNodeId: Message['id'], // the parent node of the message to be replaced
content: string | null,
+ extra: Message['extra'],
onChunk: CallbackGeneratedChunk
) => Promise<void>;
convId: string | null,
leafNodeId: Message['id'] | null,
content: string,
+ extra: Message['extra'],
onChunk: CallbackGeneratedChunk
): Promise<boolean> => {
if (isGenerating(convId ?? '') || content.trim().length === 0) return false;
convId,
role: 'user',
content,
+ extra,
parent: leafNodeId,
children: [],
},
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;
convId,
role: 'user',
content,
+ extra,
parent: parentNodeId,
children: [],
},
--- /dev/null
+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<HTMLTextAreaElement>,
+ setInputMsg: (text: string) => void
+) => {
+ const [extraContext, setExtraContext] = useState<MessageExtraContext | null>(
+ 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),
+ };
+};
/**
* filter out redundant fields upon sending to API
+ * also format extra into text
*/
export function normalizeMsgsForAPI(messages: Readonly<Message[]>) {
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[];
}
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<Message, 'role' | 'content'>;
export interface Conversation {