From 64d1aad2bbde4bea9ab3003e9a7d08c1b53585db Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 5 Dec 2024 15:29:33 +0300 Subject: [PATCH] feat: edit and resubmit messages (#592) --- app/(chat)/actions.ts | 13 ++++ app/(chat)/api/chat/route.ts | 9 ++- components/block-messages.tsx | 12 +++- components/block.tsx | 6 ++ components/chat.tsx | 5 ++ components/message-editor.tsx | 113 ++++++++++++++++++++++++++++++++ components/message.tsx | 95 +++++++++++++++++++++------ components/messages.tsx | 12 +++- components/multimodal-input.tsx | 2 +- components/use-block-stream.tsx | 18 ++++- hooks/use-user-message-id.ts | 10 +++ lib/db/queries.ts | 32 ++++++++- 12 files changed, 302 insertions(+), 25 deletions(-) create mode 100644 components/message-editor.tsx create mode 100644 hooks/use-user-message-id.ts diff --git a/app/(chat)/actions.ts b/app/(chat)/actions.ts index 0d4b2c9..da4c5ea 100644 --- a/app/(chat)/actions.ts +++ b/app/(chat)/actions.ts @@ -4,6 +4,10 @@ import { type CoreUserMessage, generateText } from 'ai'; import { cookies } from 'next/headers'; import { customModel } from '@/lib/ai'; +import { + deleteMessagesByChatIdAfterTimestamp, + getMessageById, +} from '@/lib/db/queries'; export async function saveModelId(model: string) { const cookieStore = await cookies(); @@ -27,3 +31,12 @@ export async function generateTitleFromUserMessage({ return title; } + +export async function deleteTrailingMessages({ id }: { id: string }) { + const [message] = await getMessageById({ id }); + + await deleteMessagesByChatIdAfterTimestamp({ + chatId: message.chatId, + timestamp: message.createdAt, + }); +} diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index b5efc43..8452f3b 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -81,14 +81,21 @@ export async function POST(request: Request) { await saveChat({ id, userId: session.user.id, title }); } + const userMessageId = generateUUID(); + await saveMessages({ messages: [ - { ...userMessage, id: generateUUID(), createdAt: new Date(), chatId: id }, + { ...userMessage, id: userMessageId, createdAt: new Date(), chatId: id }, ], }); const streamingData = new StreamData(); + streamingData.append({ + type: 'user-message-id', + content: userMessageId, + }); + const result = streamText({ model: customModel(model.apiIdentifier), system: systemPrompt, diff --git a/components/block-messages.tsx b/components/block-messages.tsx index 47f263f..2d83068 100644 --- a/components/block-messages.tsx +++ b/components/block-messages.tsx @@ -3,7 +3,7 @@ import { UIBlock } from './block'; import { PreviewMessage } from './message'; import { useScrollToBottom } from './use-scroll-to-bottom'; import { Vote } from '@/lib/db/schema'; -import { Message } from 'ai'; +import { ChatRequestOptions, Message } from 'ai'; interface BlockMessagesProps { chatId: string; @@ -12,6 +12,12 @@ interface BlockMessagesProps { isLoading: boolean; votes: Array | undefined; messages: Array; + setMessages: ( + messages: Message[] | ((messages: Message[]) => Message[]), + ) => void; + reload: ( + chatRequestOptions?: ChatRequestOptions, + ) => Promise; } function PureBlockMessages({ @@ -21,6 +27,8 @@ function PureBlockMessages({ isLoading, votes, messages, + setMessages, + reload, }: BlockMessagesProps) { const [messagesContainerRef, messagesEndRef] = useScrollToBottom(); @@ -43,6 +51,8 @@ function PureBlockMessages({ ? votes.find((vote) => vote.messageId === message.id) : undefined } + setMessages={setMessages} + reload={reload} /> ))} diff --git a/components/block.tsx b/components/block.tsx index 7f8cd75..d426cf9 100644 --- a/components/block.tsx +++ b/components/block.tsx @@ -58,6 +58,7 @@ function PureBlock({ setBlock, messages, setMessages, + reload, votes, }: { chatId: string; @@ -82,6 +83,9 @@ function PureBlock({ }, chatRequestOptions?: ChatRequestOptions, ) => void; + reload: ( + chatRequestOptions?: ChatRequestOptions, + ) => Promise; }) { const { data: documents, @@ -286,6 +290,8 @@ function PureBlock({ setBlock={setBlock} votes={votes} messages={messages} + setMessages={setMessages} + reload={reload} />
diff --git a/components/chat.tsx b/components/chat.tsx index ad65857..0eee5a8 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -36,8 +36,10 @@ export function Chat({ append, isLoading, stop, + reload, data: streamingData, } = useChat({ + id, body: { id, modelId: selectedModelId }, initialMessages, onFinish: () => { @@ -81,6 +83,8 @@ export function Chat({ isLoading={isLoading} votes={votes} messages={messages} + setMessages={setMessages} + reload={reload} /> @@ -116,6 +120,7 @@ export function Chat({ setBlock={setBlock} messages={messages} setMessages={setMessages} + reload={reload} votes={votes} /> )} diff --git a/components/message-editor.tsx b/components/message-editor.tsx new file mode 100644 index 0000000..35fbcf9 --- /dev/null +++ b/components/message-editor.tsx @@ -0,0 +1,113 @@ +'use client'; + +import { ChatRequestOptions, Message } from 'ai'; +import { Button } from './ui/button'; +import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'; +import { Textarea } from './ui/textarea'; +import { deleteTrailingMessages } from '@/app/(chat)/actions'; +import { toast } from 'sonner'; +import { useUserMessageId } from '@/hooks/use-user-message-id'; + +export type MessageEditorProps = { + message: Message; + setMode: Dispatch>; + setMessages: ( + messages: Message[] | ((messages: Message[]) => Message[]), + ) => void; + reload: ( + chatRequestOptions?: ChatRequestOptions, + ) => Promise; +}; + +export function MessageEditor({ + message, + setMode, + setMessages, + reload, +}: MessageEditorProps) { + const { userMessageIdFromServer } = useUserMessageId(); + const [isSubmitting, setIsSubmitting] = useState(false); + + const [draftContent, setDraftContent] = useState(message.content); + const textareaRef = useRef(null); + + useEffect(() => { + if (textareaRef.current) { + adjustHeight(); + } + }, []); + + const adjustHeight = () => { + if (textareaRef.current) { + textareaRef.current.style.height = 'auto'; + textareaRef.current.style.height = `${textareaRef.current.scrollHeight + 2}px`; + } + }; + + const handleInput = (event: React.ChangeEvent) => { + setDraftContent(event.target.value); + adjustHeight(); + }; + + return ( +
+