edit message
This commit is contained in:
		
							parent
							
								
									e30336c00e
								
							
						
					
					
						commit
						a03d8b2ba9
					
				|  | @ -20,10 +20,13 @@ interface Props { | ||||||
|   onSend: (message: Message, isResend: boolean) => void; |   onSend: (message: Message, isResend: boolean) => void; | ||||||
|   onUpdateConversation: (conversation: Conversation, data: KeyValuePair) => void; |   onUpdateConversation: (conversation: Conversation, data: KeyValuePair) => void; | ||||||
|   onAcceptEnv: (accept: boolean) => void; |   onAcceptEnv: (accept: boolean) => void; | ||||||
|  |   onEditMessage: (message: Message, messageIndex: number) => void; | ||||||
|  |   onDeleteMessage: (message: Message, messageIndex: number) => void; | ||||||
|  |   onRegenerate: () => void; | ||||||
|   stopConversationRef: MutableRefObject<boolean>; |   stopConversationRef: MutableRefObject<boolean>; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const Chat: FC<Props> = ({ conversation, models, apiKey, isUsingEnv, messageIsStreaming, modelError, messageError, loading, lightMode, onSend, onUpdateConversation, onAcceptEnv, stopConversationRef }) => { | export const Chat: FC<Props> = ({ conversation, models, apiKey, isUsingEnv, messageIsStreaming, modelError, messageError, loading, lightMode, onSend, onUpdateConversation, onAcceptEnv, onEditMessage, onDeleteMessage, onRegenerate, stopConversationRef }) => { | ||||||
|   const [currentMessage, setCurrentMessage] = useState<Message>(); |   const [currentMessage, setCurrentMessage] = useState<Message>(); | ||||||
|   const [autoScrollEnabled, setAutoScrollEnabled] = useState(true); |   const [autoScrollEnabled, setAutoScrollEnabled] = useState(true); | ||||||
| 
 | 
 | ||||||
|  | @ -122,7 +125,10 @@ export const Chat: FC<Props> = ({ conversation, models, apiKey, isUsingEnv, mess | ||||||
|                   <ChatMessage |                   <ChatMessage | ||||||
|                     key={index} |                     key={index} | ||||||
|                     message={message} |                     message={message} | ||||||
|  |                     messageIndex={index} | ||||||
|                     lightMode={lightMode} |                     lightMode={lightMode} | ||||||
|  |                     onEditMessage={onEditMessage} | ||||||
|  |                     onDeleteMessage={onDeleteMessage} | ||||||
|                   /> |                   /> | ||||||
|                 ))} |                 ))} | ||||||
| 
 | 
 | ||||||
|  | @ -149,11 +155,12 @@ export const Chat: FC<Props> = ({ conversation, models, apiKey, isUsingEnv, mess | ||||||
|               stopConversationRef={stopConversationRef} |               stopConversationRef={stopConversationRef} | ||||||
|               textareaRef={textareaRef} |               textareaRef={textareaRef} | ||||||
|               messageIsStreaming={messageIsStreaming} |               messageIsStreaming={messageIsStreaming} | ||||||
|  |               model={conversation.model} | ||||||
|               onSend={(message) => { |               onSend={(message) => { | ||||||
|                 setCurrentMessage(message); |                 setCurrentMessage(message); | ||||||
|                 onSend(message, false); |                 onSend(message, false); | ||||||
|               }} |               }} | ||||||
|               model={conversation.model} |               onRegenerate={onRegenerate} | ||||||
|             /> |             /> | ||||||
|           )} |           )} | ||||||
|         </> |         </> | ||||||
|  |  | ||||||
|  | @ -1,11 +1,12 @@ | ||||||
| import { Message, OpenAIModel, OpenAIModelID } from "@/types"; | import { Message, OpenAIModel, OpenAIModelID } from "@/types"; | ||||||
| import { IconPlayerStop, IconSend } from "@tabler/icons-react"; | import { IconPlayerStop, IconSend } from "@tabler/icons-react"; | ||||||
| import { FC, KeyboardEvent, MutableRefObject, useEffect, useRef, useState } from "react"; | import { FC, KeyboardEvent, MutableRefObject, useEffect, useState } from "react"; | ||||||
| 
 | 
 | ||||||
| interface Props { | interface Props { | ||||||
|   messageIsStreaming: boolean; |   messageIsStreaming: boolean; | ||||||
|   onSend: (message: Message) => void; |  | ||||||
|   model: OpenAIModel; |   model: OpenAIModel; | ||||||
|  |   onSend: (message: Message) => void; | ||||||
|  |   onRegenerate: () => void; | ||||||
|   stopConversationRef: MutableRefObject<boolean>; |   stopConversationRef: MutableRefObject<boolean>; | ||||||
|   textareaRef: MutableRefObject<HTMLTextAreaElement | null>; |   textareaRef: MutableRefObject<HTMLTextAreaElement | null>; | ||||||
| } | } | ||||||
|  | @ -67,7 +68,6 @@ export const ChatInput: FC<Props> = ({ onSend, messageIsStreaming, model, stopCo | ||||||
|     } |     } | ||||||
|   }, [content]); |   }, [content]); | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|   function handleStopConversation() { |   function handleStopConversation() { | ||||||
|     stopConversationRef.current = true; |     stopConversationRef.current = true; | ||||||
|     setTimeout(() => { |     setTimeout(() => { | ||||||
|  |  | ||||||
|  | @ -1,26 +1,119 @@ | ||||||
| import { Message } from "@/types"; | import { Message } from "@/types"; | ||||||
| import { FC } from "react"; | import { IconEdit } from "@tabler/icons-react"; | ||||||
|  | import { FC, useEffect, useRef, useState } from "react"; | ||||||
| import ReactMarkdown from "react-markdown"; | import ReactMarkdown from "react-markdown"; | ||||||
| import remarkGfm from "remark-gfm"; | import remarkGfm from "remark-gfm"; | ||||||
| import { CodeBlock } from "../Markdown/CodeBlock"; | import { CodeBlock } from "../Markdown/CodeBlock"; | ||||||
| 
 | 
 | ||||||
| interface Props { | interface Props { | ||||||
|   message: Message; |   message: Message; | ||||||
|  |   messageIndex: number; | ||||||
|   lightMode: "light" | "dark"; |   lightMode: "light" | "dark"; | ||||||
|  |   onEditMessage: (message: Message, messageIndex: number) => void; | ||||||
|  |   onDeleteMessage: (message: Message, messageIndex: number) => void; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const ChatMessage: FC<Props> = ({ message, lightMode }) => { | export const ChatMessage: FC<Props> = ({ message, messageIndex, lightMode, onEditMessage, onDeleteMessage }) => { | ||||||
|  |   const [isEditing, setIsEditing] = useState<boolean>(false); | ||||||
|  |   const [isHovering, setIsHovering] = useState<boolean>(false); | ||||||
|  |   const [messageContent, setMessageContent] = useState(message.content); | ||||||
|  | 
 | ||||||
|  |   const textareaRef = useRef<HTMLTextAreaElement>(null); | ||||||
|  | 
 | ||||||
|  |   const toggleEditing = () => { | ||||||
|  |     setIsEditing(!isEditing); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handleInputChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => { | ||||||
|  |     setMessageContent(event.target.value); | ||||||
|  |     if (textareaRef.current) { | ||||||
|  |       textareaRef.current.style.height = "inherit"; | ||||||
|  |       textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`; | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handleEditMessage = () => { | ||||||
|  |     onEditMessage({ ...message, content: messageContent }, messageIndex); | ||||||
|  |     setIsEditing(false); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handlePressEnter = (e: React.KeyboardEvent<HTMLTextAreaElement>) => { | ||||||
|  |     if (e.key === "Enter" && !e.shiftKey) { | ||||||
|  |       e.preventDefault(); | ||||||
|  |       handleEditMessage(); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (textareaRef.current) { | ||||||
|  |       textareaRef.current.style.height = "inherit"; | ||||||
|  |       textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`; | ||||||
|  |     } | ||||||
|  |   }, [isEditing]); | ||||||
|  | 
 | ||||||
|   return ( |   return ( | ||||||
|     <div |     <div | ||||||
|       className={`group ${message.role === "assistant" ? "text-gray-800 dark:text-gray-100 border-b border-black/10 dark:border-gray-900/50 bg-gray-50 dark:bg-[#444654]" : "text-gray-800 dark:text-gray-100 border-b border-black/10 dark:border-gray-900/50 bg-white dark:bg-[#343541]"}`} |       className={`group ${message.role === "assistant" ? "text-gray-800 dark:text-gray-100 border-b border-black/10 dark:border-gray-900/50 bg-gray-50 dark:bg-[#444654]" : "text-gray-800 dark:text-gray-100 border-b border-black/10 dark:border-gray-900/50 bg-white dark:bg-[#343541]"}`} | ||||||
|       style={{ overflowWrap: "anywhere" }} |       style={{ overflowWrap: "anywhere" }} | ||||||
|  |       onMouseEnter={() => setIsHovering(true)} | ||||||
|  |       onMouseLeave={() => setIsHovering(false)} | ||||||
|     > |     > | ||||||
|       <div className="text-base gap-4 md:gap-6 md:max-w-2xl lg:max-w-2xl xl:max-w-3xl p-4 md:py-6 flex lg:px-0 m-auto"> |       <div className="text-base gap-4 md:gap-6 md:max-w-2xl lg:max-w-2xl xl:max-w-3xl p-4 md:py-6 flex lg:px-0 m-auto relative"> | ||||||
|         <div className="font-bold min-w-[40px]">{message.role === "assistant" ? "AI:" : "You:"}</div> |         <div className="font-bold min-w-[40px]">{message.role === "assistant" ? "AI:" : "You:"}</div> | ||||||
| 
 | 
 | ||||||
|         <div className="prose dark:prose-invert mt-[-2px]"> |         <div className="prose dark:prose-invert mt-[-2px] w-full"> | ||||||
|           {message.role === "user" ? ( |           {message.role === "user" ? ( | ||||||
|  |             <div className="flex w-full"> | ||||||
|  |               {isEditing ? ( | ||||||
|  |                 <div className="flex flex-col w-full"> | ||||||
|  |                   <textarea | ||||||
|  |                     ref={textareaRef} | ||||||
|  |                     className="w-full dark:bg-[#343541] border-none resize-none outline-none whitespace-pre-wrap" | ||||||
|  |                     value={messageContent} | ||||||
|  |                     onChange={handleInputChange} | ||||||
|  |                     onKeyDown={handlePressEnter} | ||||||
|  |                     style={{ | ||||||
|  |                       fontFamily: "inherit", | ||||||
|  |                       fontSize: "inherit", | ||||||
|  |                       lineHeight: "inherit", | ||||||
|  |                       padding: "0", | ||||||
|  |                       margin: "0", | ||||||
|  |                       overflow: "hidden" | ||||||
|  |                     }} | ||||||
|  |                   /> | ||||||
|  | 
 | ||||||
|  |                   <div className="flex mt-10 justify-center space-x-4"> | ||||||
|  |                     <button | ||||||
|  |                       className="h-[40px] bg-blue-500 text-white rounded-md px-4 py-1 text-sm font-medium hover:bg-blue-600" | ||||||
|  |                       onClick={handleEditMessage} | ||||||
|  |                     > | ||||||
|  |                       Save & Submit | ||||||
|  |                     </button> | ||||||
|  |                     <button | ||||||
|  |                       className="h-[40px] border border-neutral-300 dark:border-neutral-700 rounded-md px-4 py-1 text-sm font-medium text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-800" | ||||||
|  |                       onClick={() => { | ||||||
|  |                         setMessageContent(message.content); | ||||||
|  |                         setIsEditing(false); | ||||||
|  |                       }} | ||||||
|  |                     > | ||||||
|  |                       Cancel | ||||||
|  |                     </button> | ||||||
|  |                   </div> | ||||||
|  |                 </div> | ||||||
|  |               ) : ( | ||||||
|                 <div className="prose dark:prose-invert whitespace-pre-wrap">{message.content}</div> |                 <div className="prose dark:prose-invert whitespace-pre-wrap">{message.content}</div> | ||||||
|  |               )} | ||||||
|  | 
 | ||||||
|  |               {isHovering && !isEditing && ( | ||||||
|  |                 <button className="absolute right-[-20px] top-[26px]"> | ||||||
|  |                   <IconEdit | ||||||
|  |                     size={20} | ||||||
|  |                     className="text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300" | ||||||
|  |                     onClick={toggleEditing} | ||||||
|  |                   /> | ||||||
|  |                 </button> | ||||||
|  |               )} | ||||||
|  |             </div> | ||||||
|           ) : ( |           ) : ( | ||||||
|             <ReactMarkdown |             <ReactMarkdown | ||||||
|               remarkPlugins={[remarkGfm]} |               remarkPlugins={[remarkGfm]} | ||||||
|  |  | ||||||
|  | @ -24,6 +24,7 @@ export default function Home() { | ||||||
|   const [messageError, setMessageError] = useState<boolean>(false); |   const [messageError, setMessageError] = useState<boolean>(false); | ||||||
|   const [modelError, setModelError] = useState<boolean>(false); |   const [modelError, setModelError] = useState<boolean>(false); | ||||||
|   const [isUsingEnv, setIsUsingEnv] = useState<boolean>(false); |   const [isUsingEnv, setIsUsingEnv] = useState<boolean>(false); | ||||||
|  |   const [currentMessage, setCurrentMessage] = useState<Message>(); | ||||||
| 
 | 
 | ||||||
|   const stopConversationRef = useRef<boolean>(false); |   const stopConversationRef = useRef<boolean>(false); | ||||||
| 
 | 
 | ||||||
|  | @ -352,6 +353,41 @@ export default function Home() { | ||||||
|     localStorage.removeItem("isUsingEnv"); |     localStorage.removeItem("isUsingEnv"); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|  |   const handleEditMessage = (message: Message, messageIndex: number) => { | ||||||
|  |     if (selectedConversation) { | ||||||
|  |       const updatedMessages = selectedConversation.messages | ||||||
|  |         .map((m, i) => { | ||||||
|  |           if (i < messageIndex) { | ||||||
|  |             return m; | ||||||
|  |           } | ||||||
|  |         }) | ||||||
|  |         .filter((m) => m) as Message[]; | ||||||
|  | 
 | ||||||
|  |       const updatedConversation = { | ||||||
|  |         ...selectedConversation, | ||||||
|  |         messages: updatedMessages | ||||||
|  |       }; | ||||||
|  | 
 | ||||||
|  |       const { single, all } = updateConversation(updatedConversation, conversations); | ||||||
|  | 
 | ||||||
|  |       setSelectedConversation(single); | ||||||
|  |       setConversations(all); | ||||||
|  | 
 | ||||||
|  |       setCurrentMessage(message); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handleDeleteMessage = (message: Message, messageIndex: number) => {}; | ||||||
|  | 
 | ||||||
|  |   const handleRegenerate = () => {}; | ||||||
|  | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (currentMessage) { | ||||||
|  |       handleSend(currentMessage, false); | ||||||
|  |       setCurrentMessage(undefined); | ||||||
|  |     } | ||||||
|  |   }, [currentMessage]); | ||||||
|  | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     if (window.innerWidth < 640) { |     if (window.innerWidth < 640) { | ||||||
|       setShowSidebar(false); |       setShowSidebar(false); | ||||||
|  | @ -491,6 +527,9 @@ export default function Home() { | ||||||
|               onSend={handleSend} |               onSend={handleSend} | ||||||
|               onUpdateConversation={handleUpdateConversation} |               onUpdateConversation={handleUpdateConversation} | ||||||
|               onAcceptEnv={handleEnvChange} |               onAcceptEnv={handleEnvChange} | ||||||
|  |               onEditMessage={handleEditMessage} | ||||||
|  |               onDeleteMessage={handleDeleteMessage} | ||||||
|  |               onRegenerate={handleRegenerate} | ||||||
|               stopConversationRef={stopConversationRef} |               stopConversationRef={stopConversationRef} | ||||||
|             /> |             /> | ||||||
|           </article> |           </article> | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue