Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect, useCallback } from 'react'; | |
| import { Chat, Message, ChatState } from './types'; | |
| import { createChat, getChats, getMessages, sendMessage, deleteChat } from './services/api'; | |
| import ChatSidebar from './components/ChatSidebar'; | |
| import ChatWindow from './components/ChatWindow'; | |
| import DocumentsModal from './components/DocumentsModal'; | |
| const App: React.FC = () => { | |
| const [state, setState] = useState<ChatState>({ | |
| chats: [], | |
| activeChat: null, | |
| messages: [], | |
| isLoading: false, | |
| isSending: false, | |
| error: null, | |
| }); | |
| const [isDocsModalOpen, setIsDocsModalOpen] = useState(false); | |
| // Listen for custom event to open docs modal | |
| useEffect(() => { | |
| const handleOpenModal = () => setIsDocsModalOpen(true); | |
| window.addEventListener('open-documents-modal', handleOpenModal); | |
| return () => window.removeEventListener('open-documents-modal', handleOpenModal); | |
| }, []); | |
| // Load chats on mount | |
| useEffect(() => { | |
| loadChats(); | |
| }, []); | |
| // Load all chats from API | |
| const loadChats = async () => { | |
| try { | |
| setState(prev => ({ ...prev, isLoading: true, error: null })); | |
| const chats = await getChats(); | |
| setState(prev => ({ ...prev, chats, isLoading: false })); | |
| } catch (err) { | |
| setState(prev => ({ | |
| ...prev, | |
| isLoading: false, | |
| error: err instanceof Error ? err.message : 'Failed to load chats', | |
| })); | |
| } | |
| }; | |
| // Load messages for a chat | |
| const loadMessages = async (chatId: string) => { | |
| try { | |
| setState(prev => ({ ...prev, isLoading: true, error: null })); | |
| const messages = await getMessages(chatId); | |
| setState(prev => ({ ...prev, messages, isLoading: false })); | |
| } catch (err) { | |
| setState(prev => ({ | |
| ...prev, | |
| isLoading: false, | |
| error: err instanceof Error ? err.message : 'Failed to load messages', | |
| })); | |
| } | |
| }; | |
| // Handle selecting a chat | |
| const handleSelectChat = useCallback(async (chat: Chat) => { | |
| setState(prev => ({ ...prev, activeChat: chat, messages: [] })); | |
| await loadMessages(chat.id); | |
| }, []); | |
| // Handle creating a new chat | |
| const handleNewChat = useCallback(async () => { | |
| try { | |
| setState(prev => ({ ...prev, isLoading: true, error: null })); | |
| const chat = await createChat(); | |
| setState(prev => ({ | |
| ...prev, | |
| chats: [chat, ...prev.chats], | |
| activeChat: chat, | |
| messages: [], | |
| isLoading: false, | |
| })); | |
| } catch (err) { | |
| setState(prev => ({ | |
| ...prev, | |
| isLoading: false, | |
| error: err instanceof Error ? err.message : 'Failed to create chat', | |
| })); | |
| } | |
| }, []); | |
| // Handle deleting a chat | |
| const handleDeleteChat = useCallback(async (chatId: string) => { | |
| try { | |
| await deleteChat(chatId); | |
| setState(prev => { | |
| const newChats = prev.chats.filter(c => c.id !== chatId); | |
| const newActiveChat = prev.activeChat?.id === chatId ? null : prev.activeChat; | |
| const newMessages = prev.activeChat?.id === chatId ? [] : prev.messages; | |
| return { | |
| ...prev, | |
| chats: newChats, | |
| activeChat: newActiveChat, | |
| messages: newMessages, | |
| }; | |
| }); | |
| } catch (err) { | |
| setState(prev => ({ | |
| ...prev, | |
| error: err instanceof Error ? err.message : 'Failed to delete chat', | |
| })); | |
| } | |
| }, []); | |
| // Handle sending a message | |
| const handleSendMessage = useCallback(async (content: string) => { | |
| if (!state.activeChat) return; | |
| const chatId = state.activeChat.id; | |
| // Optimistically add user message | |
| const tempUserMsg: Message = { | |
| id: `temp-${Date.now()}`, | |
| chat_id: chatId, | |
| role: 'user', | |
| content, | |
| timestamp: new Date().toISOString(), | |
| }; | |
| setState(prev => ({ | |
| ...prev, | |
| messages: [...prev.messages, tempUserMsg], | |
| isSending: true, | |
| error: null, | |
| })); | |
| try { | |
| const response = await sendMessage(chatId, content); | |
| setState(prev => { | |
| // Replace temp message with real messages | |
| const messagesWithoutTemp = prev.messages.filter(m => m.id !== tempUserMsg.id); | |
| const newMessages = [ | |
| ...messagesWithoutTemp, | |
| response.user_message, | |
| response.assistant_message, | |
| ]; | |
| // Update chat title if it changed (first message) | |
| const updatedChats = prev.chats.map(chat => { | |
| if (chat.id === chatId && chat.title === 'New Chat') { | |
| const newTitle = content.length > 50 ? content.slice(0, 50) + '...' : content; | |
| return { ...chat, title: newTitle }; | |
| } | |
| return chat; | |
| }); | |
| // Update active chat title too | |
| const updatedActiveChat = prev.activeChat && prev.activeChat.title === 'New Chat' | |
| ? { ...prev.activeChat, title: content.length > 50 ? content.slice(0, 50) + '...' : content } | |
| : prev.activeChat; | |
| return { | |
| ...prev, | |
| messages: newMessages, | |
| chats: updatedChats, | |
| activeChat: updatedActiveChat, | |
| isSending: false, | |
| }; | |
| }); | |
| } catch (err) { | |
| setState(prev => ({ | |
| ...prev, | |
| messages: prev.messages.filter(m => m.id !== tempUserMsg.id), | |
| isSending: false, | |
| error: err instanceof Error ? err.message : 'Failed to send message', | |
| })); | |
| } | |
| }, [state.activeChat]); | |
| return ( | |
| <div className="h-screen flex bg-gray-100 overflow-hidden"> | |
| {/* Sidebar */} | |
| <ChatSidebar | |
| chats={state.chats} | |
| activeChat={state.activeChat} | |
| onSelectChat={handleSelectChat} | |
| onNewChat={handleNewChat} | |
| onDeleteChat={handleDeleteChat} | |
| isLoading={state.isLoading} | |
| /> | |
| {/* Main Chat Area */} | |
| <ChatWindow | |
| activeChat={state.activeChat} | |
| messages={state.messages} | |
| onSendMessage={handleSendMessage} | |
| isSending={state.isSending} | |
| isLoading={state.isLoading} | |
| /> | |
| {/* Error Toast */} | |
| {state.error && ( | |
| <div className="fixed bottom-4 right-4 max-w-md bg-red-500 text-white px-6 py-4 rounded-xl shadow-2xl flex items-center gap-3 animate-in slide-in-from-bottom-4 duration-300"> | |
| <svg className="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> | |
| </svg> | |
| <span className="text-sm font-medium">{state.error}</span> | |
| <button | |
| onClick={() => setState(prev => ({ ...prev, error: null }))} | |
| className="ml-2 hover:bg-red-600 p-1 rounded transition-colors" | |
| > | |
| <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> | |
| </svg> | |
| </button> | |
| </div> | |
| )} | |
| {/* Documents Management Modal */} | |
| <DocumentsModal | |
| isOpen={isDocsModalOpen} | |
| onClose={() => setIsDocsModalOpen(false)} | |
| /> | |
| </div> | |
| ); | |
| }; | |
| export default App; | |