mirror of
				https://github.com/ggml-org/llama.cpp.git
				synced 2025-10-31 08:51:55 +00:00 
			
		
		
		
	server : (webui) revamp the input area, plus many small UI improvements (#13365)
* rework the input area * process selected file * change all icons to heroicons * fix thought process collapse * move conversation more menu to sidebar * sun icon --> moon icon * rm default system message * stricter upload file check, only allow image if server has mtmd * build it * add renaming * better autoscroll * build * add conversation group * fix scroll * extra context first, then user input in the end * fix <hr> tag * clean up a bit * build * add mb-3 for <pre> * throttle adjustTextareaHeight to make it less laggy * (nits) missing padding in sidebar * rm stray console log
This commit is contained in:
		| @@ -1,12 +1,25 @@ | ||||
| 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, cleanCurrentUrl, throttle } from '../utils/misc'; | ||||
| import { classNames, cleanCurrentUrl } from '../utils/misc'; | ||||
| import CanvasPyInterpreter from './CanvasPyInterpreter'; | ||||
| import StorageUtils from '../utils/storage'; | ||||
| import { useVSCodeContext } from '../utils/llama-vscode'; | ||||
| import { useChatTextarea, ChatTextareaApi } from './useChatTextarea.ts'; | ||||
| import { | ||||
|   ArrowUpIcon, | ||||
|   StopIcon, | ||||
|   PaperClipIcon, | ||||
| } from '@heroicons/react/24/solid'; | ||||
| import { | ||||
|   ChatExtraContextApi, | ||||
|   useChatExtraContext, | ||||
| } from './useChatExtraContext.tsx'; | ||||
| import Dropzone from 'react-dropzone'; | ||||
| import toast from 'react-hot-toast'; | ||||
| import ChatInputExtraContextItem from './ChatInputExtraContextItem.tsx'; | ||||
| import { scrollToBottom, useChatScroll } from './useChatScroll.tsx'; | ||||
|  | ||||
| /** | ||||
|  * A message display is a message node with additional information for rendering. | ||||
| @@ -72,24 +85,6 @@ function getListMessageDisplay( | ||||
|   return res; | ||||
| } | ||||
|  | ||||
| const scrollToBottom = throttle( | ||||
|   (requiresNearBottom: boolean, delay: number = 80) => { | ||||
|     const mainScrollElem = document.getElementById('main-scroll'); | ||||
|     if (!mainScrollElem) return; | ||||
|     const spaceToBottom = | ||||
|       mainScrollElem.scrollHeight - | ||||
|       mainScrollElem.scrollTop - | ||||
|       mainScrollElem.clientHeight; | ||||
|     if (!requiresNearBottom || spaceToBottom < 50) { | ||||
|       setTimeout( | ||||
|         () => mainScrollElem.scrollTo({ top: mainScrollElem.scrollHeight }), | ||||
|         delay | ||||
|       ); | ||||
|     } | ||||
|   }, | ||||
|   80 | ||||
| ); | ||||
|  | ||||
| export default function ChatScreen() { | ||||
|   const { | ||||
|     viewingChat, | ||||
| @@ -102,10 +97,11 @@ export default function ChatScreen() { | ||||
|   } = useAppContext(); | ||||
|  | ||||
|   const textarea: ChatTextareaApi = useChatTextarea(prefilledMsg.content()); | ||||
|   const extraContext = useChatExtraContext(); | ||||
|   useVSCodeContext(textarea, extraContext); | ||||
|  | ||||
|   const { extraContext, clearExtraContext } = useVSCodeContext(textarea); | ||||
|   // TODO: improve this when we have "upload file" feature | ||||
|   const currExtra: Message['extra'] = extraContext ? [extraContext] : undefined; | ||||
|   const msgListRef = useRef<HTMLDivElement>(null); | ||||
|   useChatScroll(msgListRef); | ||||
|  | ||||
|   // keep track of leaf node for rendering | ||||
|   const [currNodeId, setCurrNodeId] = useState<number>(-1); | ||||
| @@ -129,13 +125,15 @@ export default function ChatScreen() { | ||||
|     if (currLeafNodeId) { | ||||
|       setCurrNodeId(currLeafNodeId); | ||||
|     } | ||||
|     scrollToBottom(true); | ||||
|     // useChatScroll will handle the auto scroll | ||||
|   }; | ||||
|  | ||||
|   const sendNewMessage = async () => { | ||||
|     const lastInpMsg = textarea.value(); | ||||
|     if (lastInpMsg.trim().length === 0 || isGenerating(currConvId ?? '')) | ||||
|     if (lastInpMsg.trim().length === 0 || isGenerating(currConvId ?? '')) { | ||||
|       toast.error('Please enter a message'); | ||||
|       return; | ||||
|     } | ||||
|     textarea.setValue(''); | ||||
|     scrollToBottom(false); | ||||
|     setCurrNodeId(-1); | ||||
| @@ -146,7 +144,7 @@ export default function ChatScreen() { | ||||
|         currConvId, | ||||
|         lastMsgNodeId, | ||||
|         lastInpMsg, | ||||
|         currExtra, | ||||
|         extraContext.items, | ||||
|         onChunk | ||||
|       )) | ||||
|     ) { | ||||
| @@ -154,7 +152,7 @@ export default function ChatScreen() { | ||||
|       textarea.setValue(lastInpMsg); | ||||
|     } | ||||
|     // OK | ||||
|     clearExtraContext(); | ||||
|     extraContext.clearItems(); | ||||
|   }; | ||||
|  | ||||
|   // for vscode context | ||||
| @@ -234,10 +232,17 @@ export default function ChatScreen() { | ||||
|         })} | ||||
|       > | ||||
|         {/* chat messages */} | ||||
|         <div id="messages-list" className="grow"> | ||||
|           <div className="mt-auto flex justify-center"> | ||||
|         <div id="messages-list" className="grow" ref={msgListRef}> | ||||
|           <div className="mt-auto flex flex-col items-center"> | ||||
|             {/* placeholder to shift the message to the bottom */} | ||||
|             {viewingChat ? '' : 'Send a message to start'} | ||||
|             {viewingChat ? ( | ||||
|               '' | ||||
|             ) : ( | ||||
|               <> | ||||
|                 <div className="mb-4">Send a message to start</div> | ||||
|                 <ServerInfo /> | ||||
|               </> | ||||
|             )} | ||||
|           </div> | ||||
|           {[...messages, ...pendingMsgDisplay].map((msg) => ( | ||||
|             <ChatMessage | ||||
| @@ -248,46 +253,19 @@ export default function ChatScreen() { | ||||
|               onRegenerateMessage={handleRegenerateMessage} | ||||
|               onEditMessage={handleEditMessage} | ||||
|               onChangeSibling={setCurrNodeId} | ||||
|               isPending={msg.isPending} | ||||
|             /> | ||||
|           ))} | ||||
|         </div> | ||||
|  | ||||
|         {/* chat input */} | ||||
|         <div className="flex flex-row items-end pt-8 pb-6 sticky bottom-0 bg-base-100"> | ||||
|           <textarea | ||||
|             // Default (mobile): Enable vertical resize, overflow auto for scrolling if needed | ||||
|             // Large screens (lg:): Disable manual resize, apply max-height for autosize limit | ||||
|             className="textarea textarea-bordered w-full resize-vertical lg:resize-none lg:max-h-48 lg:overflow-y-auto" // Adjust lg:max-h-48 as needed (e.g., lg:max-h-60) | ||||
|             placeholder="Type a message (Shift+Enter to add a new line)" | ||||
|             ref={textarea.ref} | ||||
|             onInput={textarea.onInput} // Hook's input handler (will only resize height on lg+ screens) | ||||
|             onKeyDown={(e) => { | ||||
|               if (e.nativeEvent.isComposing || e.keyCode === 229) return; | ||||
|               if (e.key === 'Enter' && !e.shiftKey) { | ||||
|                 e.preventDefault(); | ||||
|                 sendNewMessage(); | ||||
|               } | ||||
|             }} | ||||
|             id="msg-input" | ||||
|             dir="auto" | ||||
|             // Set a base height of 2 rows for mobile views | ||||
|             // On lg+ screens, the hook will calculate and set the initial height anyway | ||||
|             rows={2} | ||||
|           ></textarea> | ||||
|  | ||||
|           {isGenerating(currConvId ?? '') ? ( | ||||
|             <button | ||||
|               className="btn btn-neutral ml-2" | ||||
|               onClick={() => stopGenerating(currConvId ?? '')} | ||||
|             > | ||||
|               Stop | ||||
|             </button> | ||||
|           ) : ( | ||||
|             <button className="btn btn-primary ml-2" onClick={sendNewMessage}> | ||||
|               Send | ||||
|             </button> | ||||
|           )} | ||||
|         </div> | ||||
|         <ChatInput | ||||
|           textarea={textarea} | ||||
|           extraContext={extraContext} | ||||
|           onSend={sendNewMessage} | ||||
|           onStop={() => stopGenerating(currConvId ?? '')} | ||||
|           isGenerating={isGenerating(currConvId ?? '')} | ||||
|         /> | ||||
|       </div> | ||||
|       <div className="w-full sticky top-[7em] h-[calc(100vh-9em)]"> | ||||
|         {canvasData?.type === CanvasType.PY_INTERPRETER && ( | ||||
| @@ -297,3 +275,129 @@ export default function ChatScreen() { | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function ServerInfo() { | ||||
|   const { serverProps } = useAppContext(); | ||||
|   return ( | ||||
|     <div className="card card-sm shadow-sm border-1 border-base-content/20 text-base-content/70 mb-6"> | ||||
|       <div className="card-body"> | ||||
|         <b>Server Info</b> | ||||
|         <p> | ||||
|           <b>Model</b>: {serverProps?.model_path?.split(/(\\|\/)/).pop()} | ||||
|           <br /> | ||||
|           <b>Build</b>: {serverProps?.build_info} | ||||
|           <br /> | ||||
|         </p> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function ChatInput({ | ||||
|   textarea, | ||||
|   extraContext, | ||||
|   onSend, | ||||
|   onStop, | ||||
|   isGenerating, | ||||
| }: { | ||||
|   textarea: ChatTextareaApi; | ||||
|   extraContext: ChatExtraContextApi; | ||||
|   onSend: () => void; | ||||
|   onStop: () => void; | ||||
|   isGenerating: boolean; | ||||
| }) { | ||||
|   const [isDrag, setIsDrag] = useState(false); | ||||
|  | ||||
|   return ( | ||||
|     <div | ||||
|       className={classNames({ | ||||
|         'flex items-end pt-8 pb-6 sticky bottom-0 bg-base-100': true, | ||||
|         'opacity-50': isDrag, // simply visual feedback to inform user that the file will be accepted | ||||
|       })} | ||||
|     > | ||||
|       <Dropzone | ||||
|         noClick | ||||
|         onDrop={(files: File[]) => { | ||||
|           setIsDrag(false); | ||||
|           extraContext.onFileAdded(files); | ||||
|         }} | ||||
|         onDragEnter={() => setIsDrag(true)} | ||||
|         onDragLeave={() => setIsDrag(false)} | ||||
|         multiple={true} | ||||
|       > | ||||
|         {({ getRootProps, getInputProps }) => ( | ||||
|           <div | ||||
|             className="flex flex-col rounded-xl border-1 border-base-content/30 p-3 w-full" | ||||
|             {...getRootProps()} | ||||
|           > | ||||
|             {!isGenerating && ( | ||||
|               <ChatInputExtraContextItem | ||||
|                 items={extraContext.items} | ||||
|                 removeItem={extraContext.removeItem} | ||||
|               /> | ||||
|             )} | ||||
|  | ||||
|             <div className="flex flex-row w-full"> | ||||
|               <textarea | ||||
|                 // Default (mobile): Enable vertical resize, overflow auto for scrolling if needed | ||||
|                 // Large screens (lg:): Disable manual resize, apply max-height for autosize limit | ||||
|                 className="text-md outline-none border-none w-full resize-vertical lg:resize-none lg:max-h-48 lg:overflow-y-auto" // Adjust lg:max-h-48 as needed (e.g., lg:max-h-60) | ||||
|                 placeholder="Type a message (Shift+Enter to add a new line)" | ||||
|                 ref={textarea.ref} | ||||
|                 onInput={textarea.onInput} // Hook's input handler (will only resize height on lg+ screens) | ||||
|                 onKeyDown={(e) => { | ||||
|                   if (e.nativeEvent.isComposing || e.keyCode === 229) return; | ||||
|                   if (e.key === 'Enter' && !e.shiftKey) { | ||||
|                     e.preventDefault(); | ||||
|                     onSend(); | ||||
|                   } | ||||
|                 }} | ||||
|                 id="msg-input" | ||||
|                 dir="auto" | ||||
|                 // Set a base height of 2 rows for mobile views | ||||
|                 // On lg+ screens, the hook will calculate and set the initial height anyway | ||||
|                 rows={2} | ||||
|               ></textarea> | ||||
|  | ||||
|               {/* buttons area */} | ||||
|               <div className="flex flex-row gap-2 ml-2"> | ||||
|                 <label | ||||
|                   htmlFor="file-upload" | ||||
|                   className={classNames({ | ||||
|                     'btn w-8 h-8 p-0 rounded-full': true, | ||||
|                     'btn-disabled': isGenerating, | ||||
|                   })} | ||||
|                 > | ||||
|                   <PaperClipIcon className="h-5 w-5" /> | ||||
|                 </label> | ||||
|                 <input | ||||
|                   id="file-upload" | ||||
|                   type="file" | ||||
|                   className="hidden" | ||||
|                   disabled={isGenerating} | ||||
|                   {...getInputProps()} | ||||
|                   hidden | ||||
|                 /> | ||||
|                 {isGenerating ? ( | ||||
|                   <button | ||||
|                     className="btn btn-neutral w-8 h-8 p-0 rounded-full" | ||||
|                     onClick={onStop} | ||||
|                   > | ||||
|                     <StopIcon className="h-5 w-5" /> | ||||
|                   </button> | ||||
|                 ) : ( | ||||
|                   <button | ||||
|                     className="btn btn-primary w-8 h-8 p-0 rounded-full" | ||||
|                     onClick={onSend} | ||||
|                   > | ||||
|                     <ArrowUpIcon className="h-5 w-5" /> | ||||
|                   </button> | ||||
|                 )} | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|         )} | ||||
|       </Dropzone> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Xuan-Son Nguyen
					Xuan-Son Nguyen