mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2025-06-28 12:25:03 +00:00
server : (webui) Enable communication with parent html (if webui is in iframe) (#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 <ivailo.gardev@akros.ch> Co-authored-by: Xuan Son Nguyen <son@huggingface.co>
This commit is contained in:
Binary file not shown.
@ -159,6 +159,35 @@ export default function ChatMessage({
|
|||||||
</div>
|
</div>
|
||||||
</details>
|
</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
|
<MarkdownDisplay
|
||||||
content={content}
|
content={content}
|
||||||
isGenerating={isPending}
|
isGenerating={isPending}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { CallbackGeneratedChunk, useAppContext } from '../utils/app.context';
|
import { CallbackGeneratedChunk, useAppContext } from '../utils/app.context';
|
||||||
import ChatMessage from './ChatMessage';
|
import ChatMessage from './ChatMessage';
|
||||||
import { CanvasType, Message, PendingMessage } from '../utils/types';
|
import { CanvasType, Message, PendingMessage } from '../utils/types';
|
||||||
import { classNames, throttle } from '../utils/misc';
|
import { classNames, throttle } from '../utils/misc';
|
||||||
import CanvasPyInterpreter from './CanvasPyInterpreter';
|
import CanvasPyInterpreter from './CanvasPyInterpreter';
|
||||||
import StorageUtils from '../utils/storage';
|
import StorageUtils from '../utils/storage';
|
||||||
|
import { useVSCodeContext } from '../utils/llama-vscode';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A message display is a message node with additional information for rendering.
|
* A message display is a message node with additional information for rendering.
|
||||||
@ -81,6 +82,14 @@ export default function ChatScreen() {
|
|||||||
replaceMessageAndGenerate,
|
replaceMessageAndGenerate,
|
||||||
} = useAppContext();
|
} = useAppContext();
|
||||||
const [inputMsg, setInputMsg] = useState('');
|
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
|
// keep track of leaf node for rendering
|
||||||
const [currNodeId, setCurrNodeId] = useState<number>(-1);
|
const [currNodeId, setCurrNodeId] = useState<number>(-1);
|
||||||
@ -115,10 +124,20 @@ export default function ChatScreen() {
|
|||||||
setCurrNodeId(-1);
|
setCurrNodeId(-1);
|
||||||
// get the last message node
|
// get the last message node
|
||||||
const lastMsgNodeId = messages.at(-1)?.msg.id ?? null;
|
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
|
// restore the input message if failed
|
||||||
setInputMsg(lastInpMsg);
|
setInputMsg(lastInpMsg);
|
||||||
}
|
}
|
||||||
|
// OK
|
||||||
|
clearExtraContext();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditMessage = async (msg: Message, content: string) => {
|
const handleEditMessage = async (msg: Message, content: string) => {
|
||||||
@ -129,6 +148,7 @@ export default function ChatScreen() {
|
|||||||
viewingChat.conv.id,
|
viewingChat.conv.id,
|
||||||
msg.parent,
|
msg.parent,
|
||||||
content,
|
content,
|
||||||
|
msg.extra,
|
||||||
onChunk
|
onChunk
|
||||||
);
|
);
|
||||||
setCurrNodeId(-1);
|
setCurrNodeId(-1);
|
||||||
@ -143,6 +163,7 @@ export default function ChatScreen() {
|
|||||||
viewingChat.conv.id,
|
viewingChat.conv.id,
|
||||||
msg.parent,
|
msg.parent,
|
||||||
null,
|
null,
|
||||||
|
msg.extra,
|
||||||
onChunk
|
onChunk
|
||||||
);
|
);
|
||||||
setCurrNodeId(-1);
|
setCurrNodeId(-1);
|
||||||
@ -203,6 +224,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}
|
||||||
value={inputMsg}
|
value={inputMsg}
|
||||||
onChange={(e) => setInputMsg(e.target.value)}
|
onChange={(e) => setInputMsg(e.target.value)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
|
@ -25,6 +25,7 @@ interface AppContextValue {
|
|||||||
convId: string | null,
|
convId: string | null,
|
||||||
leafNodeId: Message['id'] | null,
|
leafNodeId: Message['id'] | null,
|
||||||
content: string,
|
content: string,
|
||||||
|
extra: Message['extra'],
|
||||||
onChunk: CallbackGeneratedChunk
|
onChunk: CallbackGeneratedChunk
|
||||||
) => Promise<boolean>;
|
) => Promise<boolean>;
|
||||||
stopGenerating: (convId: string) => void;
|
stopGenerating: (convId: string) => void;
|
||||||
@ -32,6 +33,7 @@ interface AppContextValue {
|
|||||||
convId: string,
|
convId: string,
|
||||||
parentNodeId: Message['id'], // the parent node of the message to be replaced
|
parentNodeId: Message['id'], // the parent node of the message to be replaced
|
||||||
content: string | null,
|
content: string | null,
|
||||||
|
extra: Message['extra'],
|
||||||
onChunk: CallbackGeneratedChunk
|
onChunk: CallbackGeneratedChunk
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
|
||||||
@ -274,6 +276,7 @@ export const AppContextProvider = ({
|
|||||||
convId: string | null,
|
convId: string | null,
|
||||||
leafNodeId: Message['id'] | null,
|
leafNodeId: Message['id'] | null,
|
||||||
content: string,
|
content: string,
|
||||||
|
extra: Message['extra'],
|
||||||
onChunk: CallbackGeneratedChunk
|
onChunk: CallbackGeneratedChunk
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
if (isGenerating(convId ?? '') || content.trim().length === 0) return false;
|
if (isGenerating(convId ?? '') || content.trim().length === 0) return false;
|
||||||
@ -298,6 +301,7 @@ export const AppContextProvider = ({
|
|||||||
convId,
|
convId,
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content,
|
content,
|
||||||
|
extra,
|
||||||
parent: leafNodeId,
|
parent: leafNodeId,
|
||||||
children: [],
|
children: [],
|
||||||
},
|
},
|
||||||
@ -324,6 +328,7 @@ export const AppContextProvider = ({
|
|||||||
convId: string,
|
convId: string,
|
||||||
parentNodeId: Message['id'], // the parent node of the message to be replaced
|
parentNodeId: Message['id'], // the parent node of the message to be replaced
|
||||||
content: string | null,
|
content: string | null,
|
||||||
|
extra: Message['extra'],
|
||||||
onChunk: CallbackGeneratedChunk
|
onChunk: CallbackGeneratedChunk
|
||||||
) => {
|
) => {
|
||||||
if (isGenerating(convId)) return;
|
if (isGenerating(convId)) return;
|
||||||
@ -339,6 +344,7 @@ export const AppContextProvider = ({
|
|||||||
convId,
|
convId,
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content,
|
content,
|
||||||
|
extra,
|
||||||
parent: parentNodeId,
|
parent: parentNodeId,
|
||||||
children: [],
|
children: [],
|
||||||
},
|
},
|
||||||
|
62
examples/server/webui/src/utils/llama-vscode.ts
Normal file
62
examples/server/webui/src/utils/llama-vscode.ts
Normal file
@ -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<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),
|
||||||
|
};
|
||||||
|
};
|
@ -53,12 +53,23 @@ export const copyStr = (textToCopy: string) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* filter out redundant fields upon sending to API
|
* filter out redundant fields upon sending to API
|
||||||
|
* also format extra into text
|
||||||
*/
|
*/
|
||||||
export function normalizeMsgsForAPI(messages: Readonly<Message[]>) {
|
export function normalizeMsgsForAPI(messages: Readonly<Message[]>) {
|
||||||
return messages.map((msg) => {
|
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 {
|
return {
|
||||||
role: msg.role,
|
role: msg.role,
|
||||||
content: msg.content,
|
content: newContent,
|
||||||
};
|
};
|
||||||
}) as APIMessage[];
|
}) as APIMessage[];
|
||||||
}
|
}
|
||||||
|
@ -42,11 +42,25 @@ export interface Message {
|
|||||||
role: 'user' | 'assistant' | 'system';
|
role: 'user' | 'assistant' | 'system';
|
||||||
content: string;
|
content: string;
|
||||||
timings?: TimingReport;
|
timings?: TimingReport;
|
||||||
|
extra?: MessageExtra[];
|
||||||
// node based system for branching
|
// node based system for branching
|
||||||
parent: Message['id'];
|
parent: Message['id'];
|
||||||
children: 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 type APIMessage = Pick<Message, 'role' | 'content'>;
|
||||||
|
|
||||||
export interface Conversation {
|
export interface Conversation {
|
||||||
|
Reference in New Issue
Block a user