From 790947612dd1e2040c1f380741ab6b3ca2dfb5f4 Mon Sep 17 00:00:00 2001 From: Jackson Chen <541898146chen@gmail.com> Date: Sat, 2 Nov 2024 04:30:47 -0500 Subject: [PATCH 01/10] feat: Add chatListUpdated to fix sync problem --- backend/package.json | 4 +- frontend/src/app/RootLayout.tsx | 98 +++++++++ frontend/src/app/RootProvider.tsx | 3 +- frontend/src/app/[id]/page.tsx | 179 +++------------ frontend/src/app/hooks/useChatList.ts | 42 ++++ .../useChatStream.ts} | 164 +++++--------- frontend/src/app/hooks/useModels.ts | 28 ++- frontend/src/app/layout.tsx | 2 +- frontend/src/app/page.tsx | 39 +++- frontend/src/components/chat/chat-layout.tsx | 127 ----------- frontend/src/components/chat/chat-list.tsx | 162 +++++++------- frontend/src/components/chat/chat-topbar.tsx | 58 +---- frontend/src/components/chat/chat.tsx | 39 ++-- frontend/src/components/sidebar-item.tsx | 161 ++++++++++++++ frontend/src/components/sidebar.tsx | 205 ++++-------------- package.json | 2 +- 16 files changed, 600 insertions(+), 713 deletions(-) create mode 100644 frontend/src/app/RootLayout.tsx create mode 100644 frontend/src/app/hooks/useChatList.ts rename frontend/src/app/{HomeContent.tsx => hooks/useChatStream.ts} (62%) delete mode 100644 frontend/src/components/chat/chat-layout.tsx create mode 100644 frontend/src/components/sidebar-item.tsx diff --git a/backend/package.json b/backend/package.json index 7fec1b61..bfebe96e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -5,7 +5,7 @@ "author": "", "private": true, "license": "UNLICENSED", - "packageManager": "pnpm@9.1.2", + "packageManager": "pnpm@9.1.0", "scripts": { "build": "nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", @@ -74,4 +74,4 @@ "tsconfig-paths": "^4.2.0", "typescript": "^5.1.3" } -} +} \ No newline at end of file diff --git a/frontend/src/app/RootLayout.tsx b/frontend/src/app/RootLayout.tsx new file mode 100644 index 00000000..6310150a --- /dev/null +++ b/frontend/src/app/RootLayout.tsx @@ -0,0 +1,98 @@ +'use client'; + +import React, { useEffect, useState } from 'react'; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from '@/components/ui/resizable'; +import { cn } from '@/lib/utils'; +import { usePathname } from 'next/navigation'; +import { useChatList } from './hooks/useChatList'; +import Sidebar from '@/components/sidebar'; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + const [isCollapsed, setIsCollapsed] = useState(false); + const [isMobile, setIsMobile] = useState(false); + const defaultLayout = [30, 160]; + const navCollapsedSize = 10; + + const pathname = usePathname(); + const currentChatId = pathname.split('/')[1] || ''; + + const { + chats, + loading, + error, + chatListUpdated, + setChatListUpdated, + refetchChats, + } = useChatList(); + + useEffect(() => { + const checkScreenWidth = () => { + setIsMobile(window.innerWidth <= 1023); + }; + checkScreenWidth(); + window.addEventListener('resize', checkScreenWidth); + return () => { + window.removeEventListener('resize', checkScreenWidth); + }; + }, []); + + return ( +
+ { + document.cookie = `react-resizable-panels:layout=${JSON.stringify(sizes)}`; + }} + className="h-screen items-stretch" + > + { + setIsCollapsed(true); + document.cookie = `react-resizable-panels:collapsed=${JSON.stringify(true)}`; + }} + onExpand={() => { + setIsCollapsed(false); + document.cookie = `react-resizable-panels:collapsed=${JSON.stringify(false)}`; + }} + className={cn( + isCollapsed + ? 'min-w-[50px] md:min-w-[70px] transition-all duration-300 ease-in-out' + : 'hidden md:block' + )} + > + + + + + {children} + + +
+ ); +} diff --git a/frontend/src/app/RootProvider.tsx b/frontend/src/app/RootProvider.tsx index b7471152..4f437ea5 100644 --- a/frontend/src/app/RootProvider.tsx +++ b/frontend/src/app/RootProvider.tsx @@ -5,13 +5,14 @@ import { ApolloProvider } from '@apollo/client'; import { ThemeProvider } from 'next-themes'; import { Toaster } from 'sonner'; import { AuthProvider } from './AuthProvider'; +import RootLayout from './RootLayout'; export const RootProvider = ({ children }) => { return ( - {children} + {children} diff --git a/frontend/src/app/[id]/page.tsx b/frontend/src/app/[id]/page.tsx index 48220b4c..3825ea5f 100644 --- a/frontend/src/app/[id]/page.tsx +++ b/frontend/src/app/[id]/page.tsx @@ -1,178 +1,63 @@ 'use client'; -import { ChatLayout } from '@/components/chat/chat-layout'; +import { useEffect, useRef, useState } from 'react'; +import { useParams } from 'next/navigation'; import { Message } from '@/components/types'; -import { CHAT_STREAM_SUBSCRIPTION, GET_CHAT_HISTORY } from '@/graphql/request'; -import { useQuery, useSubscription } from '@apollo/client'; -import React, { useRef, useState } from 'react'; +import { useModels } from '@/app/hooks/useModels'; +import ChatContent from '@/components/chat/chat'; +import { useChatStream } from '../hooks/useChatStream'; +import { useQuery } from '@apollo/client'; +import { GET_CHAT_HISTORY } from '@/graphql/request'; import { toast } from 'sonner'; -import useChatStore from '../hooks/useChatStore'; -import { useModels } from '../hooks/useModels'; -// Define stream states for chat flow -enum StreamStatus { - IDLE = 'IDLE', - STREAMING = 'STREAMING', - DONE = 'DONE', -} - -interface PageProps { - params: { - id: string; - }; -} +export default function ChatPage() { + const params = useParams(); + const chatId = params.id as string; -export default function Page({ params }: PageProps) { // Core message states const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); const formRef = useRef(null); - // Loading and stream control states - const [loadingSubmit, setLoadingSubmit] = useState(false); - const [streamStatus, setStreamStatus] = useState( - StreamStatus.IDLE - ); - - // Model selection state const { models } = useModels(); const [selectedModel, setSelectedModel] = useState( models[0] || 'Loading models' ); - // Image handling from global store - const setBase64Images = useChatStore((state) => state.setBase64Images); - - // Load chat history - const { data: historyData } = useQuery(GET_CHAT_HISTORY, { + useQuery(GET_CHAT_HISTORY, { variables: { chatId: params.id }, onCompleted: (data) => { if (data?.getChatHistory) { setMessages(data.getChatHistory); - setStreamStatus(StreamStatus.IDLE); } }, onError: (error) => { - console.error('Error loading chat history:', error); toast.error('Failed to load chat history'); - setStreamStatus(StreamStatus.IDLE); - }, - }); - - // Subscribe to chat stream - const { data: streamData } = useSubscription(CHAT_STREAM_SUBSCRIPTION, { - variables: { - input: { - message: input, - chatId: params.id, - model: selectedModel, - }, - }, - skip: !input.trim(), - onSubscriptionData: ({ subscriptionData }) => { - if (!subscriptionData.data) return; - - const chunk = subscriptionData.data.chatStream; - const content = chunk.choices[0]?.delta?.content; - - // Handle first data arrival - if (streamStatus !== StreamStatus.STREAMING) { - setStreamStatus(StreamStatus.STREAMING); - setLoadingSubmit(false); - } - - // Update message content - if (content) { - setMessages((prev) => { - const lastMsg = prev[prev.length - 1]; - if (lastMsg?.role === 'assistant') { - // Append to existing assistant message - return [ - ...prev.slice(0, -1), - { - ...lastMsg, - content: lastMsg.content + content, - }, - ]; - } else { - // Create new assistant message - return [ - ...prev, - { - id: chunk.id, - role: 'assistant', - content, - createdAt: new Date(chunk.created * 1000).toISOString(), - }, - ]; - } - }); - } - - // Handle stream completion - if (chunk.choices[0]?.finish_reason === 'stop') { - setStreamStatus(StreamStatus.DONE); - setLoadingSubmit(false); - setBase64Images(null); - } - }, - onError: (error) => { - console.error('Subscription error:', error); - toast.error('Connection error. Please try again.'); - setStreamStatus(StreamStatus.IDLE); - setLoadingSubmit(false); }, }); - const handleInputChange = (e: React.ChangeEvent) => { - setInput(e.target.value); - }; - - // Stop message generation - const stop = () => { - if (streamStatus === StreamStatus.STREAMING) { - setStreamStatus(StreamStatus.IDLE); - setLoadingSubmit(false); - toast.info('Stopping message generation...'); - } - }; - - // Handle message submission - const onSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (!input.trim() || loadingSubmit) return; - - setLoadingSubmit(true); - - // Add user message immediately - const newMessage: Message = { - id: params.id, - role: 'user', - content: input, - createdAt: new Date().toISOString(), - }; - - setMessages((prev) => [...prev, newMessage]); - setInput(''); - }; + const { loadingSubmit, handleSubmit, handleInputChange, stop } = + useChatStream({ + chatId, + input, + setInput, + setMessages, + selectedModel, + }); return ( -
- -
+ ); } diff --git a/frontend/src/app/hooks/useChatList.ts b/frontend/src/app/hooks/useChatList.ts new file mode 100644 index 00000000..9b3b8e7d --- /dev/null +++ b/frontend/src/app/hooks/useChatList.ts @@ -0,0 +1,42 @@ +import { useQuery } from '@apollo/client'; +import { GET_USER_CHATS } from '@/graphql/request'; +import { Chat } from '@/graphql/type'; +import { useState, useCallback, useMemo } from 'react'; + +export function useChatList() { + const [chatListUpdated, setChatListUpdated] = useState(false); + + const { + data: chatData, + loading, + error, + refetch, + } = useQuery<{ getUserChats: Chat[] }>(GET_USER_CHATS, { + fetchPolicy: chatListUpdated ? 'network-only' : 'cache-first', + }); + + const handleRefetch = useCallback(() => { + refetch(); + }, [refetch]); + + const handleChatListUpdate = useCallback((value: boolean) => { + setChatListUpdated(value); + }, []); + + const sortedChats = useMemo(() => { + const chats = chatData?.getUserChats || []; + return [...chats].sort( + (a: Chat, b: Chat) => + new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + ); + }, [chatData?.getUserChats]); + + return { + chats: sortedChats, + loading, + error, + chatListUpdated, + setChatListUpdated: handleChatListUpdate, + refetchChats: handleRefetch, + }; +} diff --git a/frontend/src/app/HomeContent.tsx b/frontend/src/app/hooks/useChatStream.ts similarity index 62% rename from frontend/src/app/HomeContent.tsx rename to frontend/src/app/hooks/useChatStream.ts index 2d1a6602..ba87e5ff 100644 --- a/frontend/src/app/HomeContent.tsx +++ b/frontend/src/app/hooks/useChatStream.ts @@ -1,20 +1,9 @@ -'use client'; - -import React, { useEffect, useRef, useState } from 'react'; -import { useMutation, useSubscription, gql } from '@apollo/client'; -import { ChatLayout } from '@/components/chat/chat-layout'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogDescription, -} from '@/components/ui/dialog'; -import UsernameForm from '@/components/username-form'; -import { toast } from 'sonner'; -import { Message } from '@/components/types'; -import { useModels } from './hooks/useModels'; +import { useState, useCallback } from 'react'; +import { useMutation, useSubscription } from '@apollo/client'; import { CHAT_STREAM, CREATE_CHAT, TRIGGER_CHAT } from '@/graphql/request'; +import { Message } from '@/components/types'; +import { toast } from 'sonner'; +import { useRouter } from 'next/navigation'; // Define stream states to manage chat flow enum StreamStatus { @@ -37,35 +26,34 @@ interface SubscriptionState { } | null; } -export default function HomeContent() { - // Core message states - const [messages, setMessages] = useState([]); - const [input, setInput] = useState(''); - const formRef = useRef(null); +interface UseChatStreamProps { + chatId: string; + input: string; + setInput: (input: string) => void; + setMessages: React.Dispatch>; + selectedModel: string; +} - // Loading and stream control states +export function useChatStream({ + chatId, + input, + setInput, + setMessages, + selectedModel, +}: UseChatStreamProps) { + const router = useRouter(); const [loadingSubmit, setLoadingSubmit] = useState(false); const [streamStatus, setStreamStatus] = useState( StreamStatus.IDLE ); - // Chat session states - const [chatId, setChatId] = useState(''); - const { models } = useModels(); - const [selectedModel, setSelectedModel] = useState( - models[0] || 'Loading models' - ); - const [chatListUpdated, setChatListUpdated] = useState(false); - - // Welcome dialog state - const [open, setOpen] = useState(false); - // Subscription state management const [subscription, setSubscription] = useState({ enabled: false, variables: null, }); + // Initialize trigger chat mutation const [triggerChat] = useMutation(TRIGGER_CHAT, { onCompleted: () => { setStreamStatus(StreamStatus.STREAMING); @@ -76,8 +64,22 @@ export default function HomeContent() { }, }); + // Create new chat session mutation + const [createChat] = useMutation(CREATE_CHAT, { + onCompleted: async (data) => { + const newChatId = data.createChat.id; + router.push(`/${newChatId}`); + await startChatStream(newChatId, input); + }, + onError: () => { + toast.error('Failed to create chat'); + setStreamStatus(StreamStatus.IDLE); + setLoadingSubmit(false); + }, + }); + // Subscribe to chat stream - const { error: subError } = useSubscription(CHAT_STREAM, { + useSubscription(CHAT_STREAM, { skip: !subscription.enabled || !subscription.variables, variables: subscription.variables, onSubscriptionData: ({ subscriptionData }) => { @@ -161,23 +163,8 @@ export default function HomeContent() { } }; - // Create new chat session - const [createChat] = useMutation(CREATE_CHAT, { - onCompleted: async (data) => { - const newChatId = data.createChat.id; - setChatId(newChatId); - setChatListUpdated(true); - await startChatStream(newChatId, input); - }, - onError: () => { - toast.error('Failed to create chat'); - setStreamStatus(StreamStatus.IDLE); - setLoadingSubmit(false); - }, - }); - // Reset states after response completion - const finishChatResponse = () => { + const finishChatResponse = useCallback(() => { setLoadingSubmit(false); setSubscription({ enabled: false, @@ -186,14 +173,18 @@ export default function HomeContent() { if (streamStatus === StreamStatus.DONE) { setStreamStatus(StreamStatus.IDLE); } - }; + }, [streamStatus]); - const handleInputChange = (e: React.ChangeEvent) => { - setInput(e.target.value); - }; + // Handle input change + const handleInputChange = useCallback( + (e: React.ChangeEvent) => { + setInput(e.target.value); + }, + [setInput] + ); - // Handle message submission - const onSubmit = async (e: React.FormEvent) => { + // Handle form submission + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!input.trim() || loadingSubmit) return; @@ -230,7 +221,7 @@ export default function HomeContent() { }; // Stop message generation - const stop = () => { + const stop = useCallback(() => { if (streamStatus === StreamStatus.STREAMING) { setSubscription({ enabled: false, @@ -240,56 +231,13 @@ export default function HomeContent() { setLoadingSubmit(false); toast.info('Message generation stopped'); } + }, [streamStatus]); + + return { + loadingSubmit, + handleSubmit, + handleInputChange, + stop, + isStreaming: streamStatus === StreamStatus.STREAMING, }; - - // Handle welcome dialog - const onOpenChange = (isOpen: boolean) => { - const username = localStorage.getItem('ollama_user'); - if (username) return setOpen(isOpen); - - localStorage.setItem('ollama_user', 'Anonymous'); - window.dispatchEvent(new Event('storage')); - setOpen(isOpen); - }; - - // Monitor subscription errors - useEffect(() => { - if (subError) { - console.error('Subscription error:', subError); - } - }, [subError]); - - return ( -
- - - - - Welcome to Ollama! - - Enter your name to get started. This is just to personalize your - experience. - - - - - -
- ); } diff --git a/frontend/src/app/hooks/useModels.ts b/frontend/src/app/hooks/useModels.ts index eac020d6..1ac03e9b 100644 --- a/frontend/src/app/hooks/useModels.ts +++ b/frontend/src/app/hooks/useModels.ts @@ -1,5 +1,6 @@ import { gql, useQuery } from '@apollo/client'; import { toast } from 'sonner'; +import { useState, useEffect } from 'react'; import { LocalStore } from '@/lib/storage'; interface ModelsCache { @@ -10,11 +11,14 @@ interface ModelsCache { const CACHE_DURATION = 30 * 60 * 1000; export const useModels = () => { + const [selectedModel, setSelectedModel] = useState( + undefined + ); + const shouldUpdateCache = (): boolean => { try { const cachedData = sessionStorage.getItem(LocalStore.models); if (!cachedData) return true; - const { lastUpdate } = JSON.parse(cachedData) as ModelsCache; const now = Date.now(); return now - lastUpdate > CACHE_DURATION; @@ -27,7 +31,6 @@ export const useModels = () => { try { const cachedData = sessionStorage.getItem(LocalStore.models); if (!cachedData) return []; - const { models } = JSON.parse(cachedData) as ModelsCache; return models; } catch { @@ -65,17 +68,22 @@ export const useModels = () => { toast.error('Failed to load models'); } - if (!shouldUpdateCache()) { - return { - models: getCachedModels(), - loading: false, - error: null, - }; - } + const currentModels = !shouldUpdateCache() + ? getCachedModels() + : data?.getAvailableModelTags || getCachedModels(); + + // Update selectedModel when models are loaded + useEffect(() => { + if (currentModels.length > 0 && !selectedModel) { + setSelectedModel(currentModels[0]); + } + }, [currentModels, selectedModel]); return { - models: data?.getAvailableModelTags || getCachedModels(), + models: currentModels, loading, error, + selectedModel, + setSelectedModel, }; }; diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index eb4a9bae..b824baee 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -17,7 +17,7 @@ export const viewport = { userScalable: 1, }; -export default function RootLayout({ +export default function Layout({ children, }: Readonly<{ children: React.ReactNode; diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index b3bb90ed..3be94494 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,5 +1,40 @@ -import HomeContent from './HomeContent'; +'use client'; + +import { useRef, useState } from 'react'; +import { Message } from '@/components/types'; +import { useModels } from './hooks/useModels'; +import ChatContent from '@/components/chat/chat'; +import { useChatStream } from './hooks/useChatStream'; export default function Home() { - return ; + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(''); + const formRef = useRef(null); + + const { selectedModel, setSelectedModel } = useModels(); + + const { loadingSubmit, handleSubmit, handleInputChange, stop, isStreaming } = + useChatStream({ + chatId: '', + input, + setInput, + setMessages, + selectedModel, + }); + + return ( + + ); } diff --git a/frontend/src/components/chat/chat-layout.tsx b/frontend/src/components/chat/chat-layout.tsx deleted file mode 100644 index ca581606..00000000 --- a/frontend/src/components/chat/chat-layout.tsx +++ /dev/null @@ -1,127 +0,0 @@ -'use client'; -import React, { useEffect, useState, Dispatch, SetStateAction } from 'react'; -import { - ResizableHandle, - ResizablePanel, - ResizablePanelGroup, -} from '@/components/ui/resizable'; -import { cn } from '@/lib/utils'; -import { Sidebar } from '../sidebar'; -import Chat from './chat'; -import { Message } from '../types'; - -interface ChatLayoutProps { - defaultLayout?: number[]; - defaultCollapsed?: boolean; - navCollapsedSize: number; - chatId: string; - messages: Message[]; - input: string; - handleInputChange: (e: React.ChangeEvent) => void; - handleSubmit: (e: React.FormEvent) => void; - loadingSubmit: boolean; - stop: () => void; - setSelectedModel: Dispatch>; - formRef: React.RefObject; - setMessages: Dispatch>; - setInput: Dispatch>; - chatListUpdated: boolean; - setChatListUpdated: React.Dispatch>; -} - -export function ChatLayout({ - defaultLayout = [30, 160], - defaultCollapsed = false, - navCollapsedSize, - messages, - input, - handleInputChange, - handleSubmit, - stop, - chatId, - setSelectedModel, - loadingSubmit, - formRef, - setMessages, - setInput, - chatListUpdated, - setChatListUpdated, -}: ChatLayoutProps) { - const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed); - const [isMobile, setIsMobile] = useState(false); - - useEffect(() => { - const checkScreenWidth = () => { - setIsMobile(window.innerWidth <= 1023); - }; - checkScreenWidth(); - window.addEventListener('resize', checkScreenWidth); - return () => { - window.removeEventListener('resize', checkScreenWidth); - }; - }, []); - - return ( - { - document.cookie = `react-resizable-panels:layout=${JSON.stringify( - sizes - )}`; - }} - className="h-screen items-stretch" - > - { - setIsCollapsed(true); - document.cookie = `react-resizable-panels:collapsed=${JSON.stringify( - true - )}`; - }} - onExpand={() => { - setIsCollapsed(false); - document.cookie = `react-resizable-panels:collapsed=${JSON.stringify( - false - )}`; - }} - className={cn( - isCollapsed - ? 'min-w-[50px] md:min-w-[70px] transition-all duration-300 ease-in-out' - : 'hidden md:block' - )} - > - - - - - - - - ); -} diff --git a/frontend/src/components/chat/chat-list.tsx b/frontend/src/components/chat/chat-list.tsx index 5f84cff0..b2a15e98 100644 --- a/frontend/src/components/chat/chat-list.tsx +++ b/frontend/src/components/chat/chat-list.tsx @@ -4,65 +4,52 @@ import React, { useRef, useEffect } from 'react'; import { motion } from 'framer-motion'; import { cn } from '@/lib/utils'; import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar'; -import { ChatProps } from './chat'; -import Image from 'next/image'; -import CodeDisplayBlock from '../code-display-block'; import Markdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; -import { INITIAL_QUESTIONS } from '@/utils/initial-questions'; -import { Button } from '../ui/button'; +import CodeDisplayBlock from '../code-display-block'; import { Message } from '../types'; import { useAuth } from '@/app/hooks/useAuth'; +import { Button } from '../ui/button'; +import { Pencil } from 'lucide-react'; -// Helper function to determine if a message is from the user -const isUserMessage = (role: string) => - role.toLowerCase() === 'user' || role.toLowerCase() === 'User'; +interface ChatListProps { + messages: Message[]; + loadingSubmit?: boolean; + onMessageEdit?: (messageId: string, newContent: string) => void; +} -// Helper function to determine if a message is from the assistant/model -const isAssistantMessage = (role: string) => - role.toLowerCase() === 'assistant' || - role.toLowerCase() === 'model' || - role.toLowerCase() === 'Model'; +const isUserMessage = (role: string) => role.toLowerCase() === 'user'; export default function ChatList({ messages, - input, - handleInputChange, - handleSubmit, - stop, loadingSubmit, - formRef, - isMobile, -}: ChatProps) { + onMessageEdit, +}: ChatListProps) { const bottomRef = useRef(null); - const [initialQuestions, setInitialQuestions] = React.useState([]); const { user } = useAuth(); - - const scrollToBottom = () => { - if (bottomRef.current) { - bottomRef.current.scrollIntoView({ - behavior: 'smooth', - block: 'end', - }); - } - }; + const [editingMessageId, setEditingMessageId] = React.useState( + null + ); + const [editContent, setEditContent] = React.useState(''); useEffect(() => { - const timeoutId = setTimeout(scrollToBottom, 100); + const timeoutId = setTimeout(() => { + bottomRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' }); + }, 100); return () => clearTimeout(timeoutId); }, [messages]); - const onClickQuestion = (content: string, e: React.MouseEvent) => { - e.preventDefault(); - handleInputChange({ - target: { value: content }, - } as React.ChangeEvent); + const handleEditStart = (message: Message) => { + setEditingMessageId(message.id); + setEditContent(message.content); + }; - requestAnimationFrame(() => { - formRef.current?.dispatchEvent( - new Event('submit', { cancelable: true, bubbles: true }) - ); - }); + const handleEditSubmit = (messageId: string) => { + if (onMessageEdit) { + onMessageEdit(messageId, editContent); + } + setEditingMessageId(null); + setEditContent(''); }; const renderMessageContent = (content: string) => { @@ -85,46 +72,15 @@ export default function ChatList({ if (messages.length === 0) { return (
-
-
- AI -

- How can I help you today? -

-
- -
- {initialQuestions.map((message) => { - const delay = Math.random() * 0.25; - return ( - - - - ); - })} -
+
+ AI +

+ How can I help you today? +

); @@ -135,6 +91,8 @@ export default function ChatList({
{messages.map((message, index) => { const isUser = isUserMessage(message.role); + const isEditing = message.id === editingMessageId; + return (
{isUser ? (
-
-

{message.content}

+
+ {isEditing ? ( +
+