mirror of
				https://github.com/ggml-org/llama.cpp.git
				synced 2025-10-30 08:42:00 +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
	 igardev
					igardev