Add edit functionality to PlaygroundMessage component
This commit is contained in:
		
							parent
							
								
									5b04e55a03
								
							
						
					
					
						commit
						78c44e13b0
					
				
							
								
								
									
										62
									
								
								src/components/Common/Playground/EditMessageForm.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								src/components/Common/Playground/EditMessageForm.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,62 @@ | ||||
| import { useForm } from "@mantine/form" | ||||
| import React from "react" | ||||
| import useDynamicTextareaSize from "~hooks/useDynamicTextareaSize" | ||||
| 
 | ||||
| type Props = { | ||||
|   value: string | ||||
|   onSumbit: (value: string) => void | ||||
|   onClose: () => void | ||||
|   isBot: boolean | ||||
| } | ||||
| 
 | ||||
| export const EditMessageForm = (props: Props) => { | ||||
|   const [isComposing, setIsComposing] = React.useState(false) | ||||
|   const textareaRef = React.useRef<HTMLTextAreaElement>(null) | ||||
| 
 | ||||
|   const form = useForm({ | ||||
|     initialValues: { | ||||
|       message: props.value | ||||
|     } | ||||
|   }) | ||||
|   useDynamicTextareaSize(textareaRef, form.values.message, 300) | ||||
| 
 | ||||
|   React.useEffect(() => { | ||||
|     form.setFieldValue("message", props.value) | ||||
|   }, [props.value]) | ||||
| 
 | ||||
|   return ( | ||||
|     <form | ||||
|       onSubmit={form.onSubmit((data) => { | ||||
|         if (isComposing) return | ||||
|         props.onClose() | ||||
|         props.onSumbit(data.message) | ||||
|       })} | ||||
|       className="flex flex-col gap-2"> | ||||
|       <textarea | ||||
|         {...form.getInputProps("message")} | ||||
|         onCompositionStart={() => setIsComposing(true)} | ||||
|         onCompositionEnd={() => setIsComposing(false)} | ||||
|         required | ||||
|         rows={1} | ||||
|         style={{ minHeight: "60px" }} | ||||
|         tabIndex={0} | ||||
|         placeholder="Type a message..." | ||||
|         ref={textareaRef} | ||||
|         className="w-full  bg-transparent focus-within:outline-none focus:ring-0 focus-visible:ring-0 ring-0 dark:ring-0 border-0 dark:text-gray-100" | ||||
|       /> | ||||
|       <div className="flex justify-center space-x-2 mt-2"> | ||||
|         <button | ||||
|           aria-label="Save" | ||||
|           className="bg-white dark:bg-black px-2.5 py-2 rounded-md text-gray-700 dark:text-gray-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-500 hover:bg-gray-100 dark:hover:bg-gray-900"> | ||||
|           {props.isBot ? "Save" : "Save & Submit"} | ||||
|         </button> | ||||
|         <button | ||||
|           onClick={props.onClose} | ||||
|           aria-label="Cancel" | ||||
|           className="border dark:border-gray-600 px-2.5 py-2 rounded-md text-gray-700 dark:text-gray-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-500 hover:bg-gray-100 dark:hover:bg-gray-900"> | ||||
|           Cancel | ||||
|         </button> | ||||
|       </div> | ||||
|     </form> | ||||
|   ) | ||||
| } | ||||
| @ -3,6 +3,7 @@ import React from "react" | ||||
| import { Image, Tooltip } from "antd" | ||||
| import { WebSearch } from "./WebSearch" | ||||
| import { CheckIcon, ClipboardIcon, Pen, RotateCcw } from "lucide-react" | ||||
| import { EditMessageForm } from "./EditMessageForm" | ||||
| 
 | ||||
| type Props = { | ||||
|   message: string | ||||
| @ -15,6 +16,7 @@ type Props = { | ||||
|   currentMessageIndex: number | ||||
|   totalMessages: number | ||||
|   onRengerate: () => void | ||||
|   onEditFormSubmit: (value: string) => void | ||||
|   isProcessing: boolean | ||||
|   webSearch?: {} | ||||
|   isSearchingInternet?: boolean | ||||
| @ -23,6 +25,7 @@ type Props = { | ||||
| 
 | ||||
| export const PlaygroundMessage = (props: Props) => { | ||||
|   const [isBtnPressed, setIsBtnPressed] = React.useState(false) | ||||
|   const [editMode, setEditMode] = React.useState(false) | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="group w-full text-gray-800 dark:text-gray-100"> | ||||
| @ -55,7 +58,16 @@ export const PlaygroundMessage = (props: Props) => { | ||||
|             ) : null} | ||||
| 
 | ||||
|             <div className="flex flex-grow flex-col"> | ||||
|               {!editMode ? ( | ||||
|                 <Markdown message={props.message} /> | ||||
|               ) : ( | ||||
|                 <EditMessageForm | ||||
|                   value={props.message} | ||||
|                   onSumbit={props.onEditFormSubmit} | ||||
|                   onClose={() => setEditMode(false)} | ||||
|                   isBot={props.isBot} | ||||
|                 /> | ||||
|               )} | ||||
|             </div> | ||||
|             {/* source if aviable */} | ||||
|             {props.images && | ||||
| @ -89,7 +101,7 @@ export const PlaygroundMessage = (props: Props) => { | ||||
|                 ))} | ||||
|               </div> | ||||
|             )} | ||||
|             {!props.isProcessing && ( | ||||
|             {!props.isProcessing && !editMode && ( | ||||
|               <div | ||||
|                 className={`space-x-2 gap-2 mt-3 ${ | ||||
|                   props.currentMessageIndex !== props.totalMessages - 1 | ||||
| @ -130,7 +142,9 @@ export const PlaygroundMessage = (props: Props) => { | ||||
|                   </> | ||||
|                 )} | ||||
|                 <Tooltip title="Edit"> | ||||
|                   <button className="flex items-center justify-center w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"> | ||||
|                   <button | ||||
|                     onClick={() => setEditMode(true)} | ||||
|                     className="flex items-center justify-center w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"> | ||||
|                     <Pen className="w-3 h-3 text-gray-400 group-hover:text-gray-500" /> | ||||
|                   </button> | ||||
|                 </Tooltip> | ||||
|  | ||||
| @ -4,7 +4,13 @@ import { PlaygroundEmpty } from "./PlaygroundEmpty" | ||||
| import { PlaygroundMessage } from "~components/Common/Playground/Message" | ||||
| 
 | ||||
| export const PlaygroundChat = () => { | ||||
|   const { messages, streaming, regenerateLastMessage, isSearchingInternet } = useMessageOption() | ||||
|   const { | ||||
|     messages, | ||||
|     streaming, | ||||
|     regenerateLastMessage, | ||||
|     isSearchingInternet, | ||||
|     editMessage | ||||
|   } = useMessageOption() | ||||
|   const divRef = React.useRef<HTMLDivElement>(null) | ||||
|   React.useEffect(() => { | ||||
|     if (divRef.current) { | ||||
| @ -32,6 +38,9 @@ export const PlaygroundChat = () => { | ||||
|           isProcessing={streaming} | ||||
|           isSearchingInternet={isSearchingInternet} | ||||
|           sources={message.sources} | ||||
|           onEditFormSubmit={(value) => { | ||||
|             editMessage(index, value, !message.isBot) | ||||
|           }} | ||||
|         /> | ||||
|       ))} | ||||
|       {messages.length > 0 && ( | ||||
|  | ||||
| @ -16,6 +16,7 @@ export const SidePanelBody = () => { | ||||
|       {messages.length === 0 && <EmptySidePanel />} | ||||
|       {messages.map((message, index) => ( | ||||
|         <PlaygroundMessage | ||||
|         onEditFormSubmit={(value) => {}} | ||||
|           key={index} | ||||
|           isBot={message.isBot} | ||||
|           message={message.message} | ||||
|  | ||||
| @ -15,10 +15,12 @@ import { | ||||
| } from "@langchain/core/messages" | ||||
| import { useStoreMessageOption } from "~store/option" | ||||
| import { | ||||
|   deleteChatForEdit, | ||||
|   getPromptById, | ||||
|   removeMessageUsingHistoryId, | ||||
|   saveHistory, | ||||
|   saveMessage | ||||
|   saveMessage, | ||||
|   updateMessageByIndex | ||||
| } from "~libs/db" | ||||
| import { useNavigate } from "react-router-dom" | ||||
| import { notification } from "antd" | ||||
| @ -114,6 +116,8 @@ export const useMessageOption = () => { | ||||
|     setSelectedSystemPrompt | ||||
|   } = useStoreMessageOption() | ||||
| 
 | ||||
|   // const { notification } = App.useApp()
 | ||||
| 
 | ||||
|   const navigate = useNavigate() | ||||
|   const textareaRef = React.useRef<HTMLTextAreaElement>(null) | ||||
| 
 | ||||
| @ -134,7 +138,9 @@ export const useMessageOption = () => { | ||||
|   const searchChatMode = async ( | ||||
|     message: string, | ||||
|     image: string, | ||||
|     isRegenerate: boolean | ||||
|     isRegenerate: boolean, | ||||
|     messages: Message[], | ||||
|     history: ChatHistory | ||||
|   ) => { | ||||
|     const url = await getOllamaURL() | ||||
| 
 | ||||
| @ -387,7 +393,9 @@ export const useMessageOption = () => { | ||||
|   const normalChatMode = async ( | ||||
|     message: string, | ||||
|     image: string, | ||||
|     isRegenerate: boolean | ||||
|     isRegenerate: boolean, | ||||
|     messages: Message[], | ||||
|     history: ChatHistory | ||||
|   ) => { | ||||
|     const url = await getOllamaURL() | ||||
| 
 | ||||
| @ -625,21 +633,42 @@ export const useMessageOption = () => { | ||||
|   const onSubmit = async ({ | ||||
|     message, | ||||
|     image, | ||||
|     isRegenerate = false | ||||
|     isRegenerate = false, | ||||
|     messages: chatHistory, | ||||
|     memory | ||||
|   }: { | ||||
|     message: string | ||||
|     image: string | ||||
|     isRegenerate?: boolean | ||||
|     messages?: Message[] | ||||
|     memory?: ChatHistory | ||||
|   }) => { | ||||
|     setStreaming(true) | ||||
|     if (webSearch) { | ||||
|       await searchChatMode(message, image, isRegenerate) | ||||
|       await searchChatMode( | ||||
|         message, | ||||
|         image, | ||||
|         isRegenerate, | ||||
|         chatHistory || messages, | ||||
|         memory || history | ||||
|       ) | ||||
|     } else { | ||||
|       await normalChatMode(message, image, isRegenerate) | ||||
|       await normalChatMode( | ||||
|         message, | ||||
|         image, | ||||
|         isRegenerate, | ||||
|         chatHistory || messages, | ||||
|         memory || history | ||||
|       ) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   const regenerateLastMessage = async () => { | ||||
|     const isOk = validateBeforeSubmit() | ||||
| 
 | ||||
|     if (!isOk) { | ||||
|       return | ||||
|     } | ||||
|     if (history.length > 0) { | ||||
|       const lastMessage = history[history.length - 2] | ||||
|       let newHistory = history | ||||
| @ -653,7 +682,8 @@ export const useMessageOption = () => { | ||||
|         await onSubmit({ | ||||
|           message: lastMessage.content, | ||||
|           image: lastMessage.image || "", | ||||
|           isRegenerate: true | ||||
|           isRegenerate: true, | ||||
|           memory: newHistory | ||||
|         }) | ||||
|       } | ||||
|     } | ||||
| @ -666,7 +696,61 @@ export const useMessageOption = () => { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   const validateBeforeSubmit = () => { | ||||
|     if (!selectedModel || selectedModel?.trim()?.length === 0) { | ||||
|       notification.error({ | ||||
|         message: "Error", | ||||
|         description: "Please select a model to continue" | ||||
|       }) | ||||
|       return false | ||||
|     } | ||||
| 
 | ||||
|     return true | ||||
|   } | ||||
| 
 | ||||
|   const editMessage = async ( | ||||
|     index: number, | ||||
|     message: string, | ||||
|     isHuman: boolean | ||||
|   ) => { | ||||
|     // update message and history by index
 | ||||
|     let newMessages = messages | ||||
|     let newHistory = history | ||||
| 
 | ||||
|     if (isHuman) { | ||||
|       const isOk = validateBeforeSubmit() | ||||
| 
 | ||||
|       if (!isOk) { | ||||
|         return | ||||
|       } | ||||
| 
 | ||||
|       const currentHumanMessage = newMessages[index] | ||||
|       newMessages[index].message = message | ||||
|       newHistory[index].content = message | ||||
|       const previousMessages = newMessages.slice(0, index + 1) | ||||
|       setMessages(previousMessages) | ||||
|       const previousHistory = newHistory.slice(0, index + 1) | ||||
|       setHistory(previousHistory) | ||||
|       await updateMessageByIndex(historyId, index, message) | ||||
|       await deleteChatForEdit(historyId, index) | ||||
|       await onSubmit({ | ||||
|         message: message, | ||||
|         image: currentHumanMessage.images[0] || "", | ||||
|         isRegenerate: true, | ||||
|         messages: previousMessages, | ||||
|         memory: previousHistory | ||||
|       }) | ||||
|     } else { | ||||
|       newMessages[index].message = message | ||||
|       setMessages(newMessages) | ||||
|       newHistory[index].content = message | ||||
|       setHistory(newHistory) | ||||
|       await updateMessageByIndex(historyId, index, message) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   return { | ||||
|     editMessage, | ||||
|     messages, | ||||
|     setMessages, | ||||
|     onSubmit, | ||||
|  | ||||
| @ -299,6 +299,22 @@ export const getAllPrompts = async () => { | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| export const updateMessageByIndex = async (history_id: string, index: number, message: string) => { | ||||
|   const db = new PageAssitDatabase() | ||||
|   const chatHistory = (await db.getChatHistory(history_id)).reverse() | ||||
|   chatHistory[index].content = message | ||||
|   await db.db.set({ [history_id]: chatHistory.reverse() }) | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export const deleteChatForEdit = async (history_id: string, index: number) => { | ||||
|   const db = new PageAssitDatabase() | ||||
|   const chatHistory = (await db.getChatHistory(history_id)).reverse() | ||||
|   const previousHistory = chatHistory.slice(0, index + 1) | ||||
|   // console.log(previousHistory)
 | ||||
|   await db.db.set({ [history_id]: previousHistory.reverse() }) | ||||
| } | ||||
| 
 | ||||
| export const savePrompt = async ({ content, title, is_system = false }: { title: string, content: string, is_system: boolean }) => { | ||||
|   const db = new PageAssitDatabase() | ||||
|   const id = generateID() | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user