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