edit message
This commit is contained in:
parent
e30336c00e
commit
a03d8b2ba9
|
@ -20,10 +20,13 @@ interface Props {
|
|||
onSend: (message: Message, isResend: boolean) => void;
|
||||
onUpdateConversation: (conversation: Conversation, data: KeyValuePair) => void;
|
||||
onAcceptEnv: (accept: boolean) => void;
|
||||
onEditMessage: (message: Message, messageIndex: number) => void;
|
||||
onDeleteMessage: (message: Message, messageIndex: number) => void;
|
||||
onRegenerate: () => void;
|
||||
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 [autoScrollEnabled, setAutoScrollEnabled] = useState(true);
|
||||
|
||||
|
@ -122,7 +125,10 @@ export const Chat: FC<Props> = ({ conversation, models, apiKey, isUsingEnv, mess
|
|||
<ChatMessage
|
||||
key={index}
|
||||
message={message}
|
||||
messageIndex={index}
|
||||
lightMode={lightMode}
|
||||
onEditMessage={onEditMessage}
|
||||
onDeleteMessage={onDeleteMessage}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
@ -149,11 +155,12 @@ export const Chat: FC<Props> = ({ conversation, models, apiKey, isUsingEnv, mess
|
|||
stopConversationRef={stopConversationRef}
|
||||
textareaRef={textareaRef}
|
||||
messageIsStreaming={messageIsStreaming}
|
||||
model={conversation.model}
|
||||
onSend={(message) => {
|
||||
setCurrentMessage(message);
|
||||
onSend(message, false);
|
||||
}}
|
||||
model={conversation.model}
|
||||
onRegenerate={onRegenerate}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { Message, OpenAIModel, OpenAIModelID } from "@/types";
|
||||
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 {
|
||||
messageIsStreaming: boolean;
|
||||
onSend: (message: Message) => void;
|
||||
model: OpenAIModel;
|
||||
onSend: (message: Message) => void;
|
||||
onRegenerate: () => void;
|
||||
stopConversationRef: MutableRefObject<boolean>;
|
||||
textareaRef: MutableRefObject<HTMLTextAreaElement | null>;
|
||||
}
|
||||
|
@ -67,7 +68,6 @@ export const ChatInput: FC<Props> = ({ onSend, messageIsStreaming, model, stopCo
|
|||
}
|
||||
}, [content]);
|
||||
|
||||
|
||||
function handleStopConversation() {
|
||||
stopConversationRef.current = true;
|
||||
setTimeout(() => {
|
||||
|
|
|
@ -1,26 +1,119 @@
|
|||
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 remarkGfm from "remark-gfm";
|
||||
import { CodeBlock } from "../Markdown/CodeBlock";
|
||||
|
||||
interface Props {
|
||||
message: Message;
|
||||
messageIndex: number;
|
||||
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 (
|
||||
<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]"}`}
|
||||
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="prose dark:prose-invert mt-[-2px]">
|
||||
<div className="prose dark:prose-invert mt-[-2px] w-full">
|
||||
{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>
|
||||
)}
|
||||
|
||||
{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
|
||||
remarkPlugins={[remarkGfm]}
|
||||
|
|
|
@ -24,6 +24,7 @@ export default function Home() {
|
|||
const [messageError, setMessageError] = useState<boolean>(false);
|
||||
const [modelError, setModelError] = useState<boolean>(false);
|
||||
const [isUsingEnv, setIsUsingEnv] = useState<boolean>(false);
|
||||
const [currentMessage, setCurrentMessage] = useState<Message>();
|
||||
|
||||
const stopConversationRef = useRef<boolean>(false);
|
||||
|
||||
|
@ -352,6 +353,41 @@ export default function Home() {
|
|||
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(() => {
|
||||
if (window.innerWidth < 640) {
|
||||
setShowSidebar(false);
|
||||
|
@ -491,6 +527,9 @@ export default function Home() {
|
|||
onSend={handleSend}
|
||||
onUpdateConversation={handleUpdateConversation}
|
||||
onAcceptEnv={handleEnvChange}
|
||||
onEditMessage={handleEditMessage}
|
||||
onDeleteMessage={handleDeleteMessage}
|
||||
onRegenerate={handleRegenerate}
|
||||
stopConversationRef={stopConversationRef}
|
||||
/>
|
||||
</article>
|
||||
|
|
Loading…
Reference in New Issue