|
|
| import React, { useState, useEffect, useCallback, useRef } from 'react'; |
| import { Sidebar } from './components/Sidebar'; |
| import { ChatView } from './components/ChatView'; |
| import type { ChatSession, ChatMessage, UploadedFile } from './types'; |
| import { geminiService } from './services/geminiService'; |
| import { NEW_CHAT_ID, REEL_BOT_SYSTEM_INSTRUCTION } from './constants'; |
| import type { Chat } from '@google/genai'; |
| import { MenuIcon } from './components/icons'; |
|
|
| const App: React.FC = () => { |
| const [chats, setChats] = useState<Map<string, ChatSession>>(new Map()); |
| const [activeChatId, setActiveChatId] = useState<string | null>(null); |
| const [isLoading, setIsLoading] = useState(false); |
| const [error, setError] = useState<string | null>(null); |
| const [isSidebarOpen, setIsSidebarOpen] = useState(false); |
|
|
| const transientGeminiChatsRef = useRef<Map<string, Chat>>(new Map()); |
| const isCancelledRef = useRef(false); |
|
|
| useEffect(() => { |
| const storedChats = localStorage.getItem('reelCreatorChats'); |
| if (storedChats) { |
| try { |
| const parsedChatsArray: [string, ChatSession][] = JSON.parse(storedChats); |
| parsedChatsArray.forEach(([, chat]) => { |
| if (!chat.messages) { |
| chat.messages = []; |
| } |
| chat.messages.forEach(msg => { |
| if (msg.file && !msg.file.dataUrl && !msg.file.type.startsWith('image/')) { |
| |
| } |
| }); |
| }); |
| setChats(new Map(parsedChatsArray)); |
| if (parsedChatsArray.length > 0) { |
| |
| } else { |
| setActiveChatId(NEW_CHAT_ID); |
| } |
| } catch (e) { |
| console.error("Failed to parse chats from localStorage", e); |
| localStorage.removeItem('reelCreatorChats'); |
| setActiveChatId(NEW_CHAT_ID); |
| } |
| } else { |
| setActiveChatId(NEW_CHAT_ID); |
| } |
| }, []); |
|
|
| useEffect(() => { |
| if (chats.size > 0 || localStorage.getItem('reelCreatorChats')) { |
| const storableChatsArray = Array.from(chats.entries()).map(([id, session]) => { |
| const storableMessages = session.messages.map(msg => { |
| if (msg.file) { |
| const { ...fileToStore } = msg.file; |
| return { ...msg, file: fileToStore }; |
| } |
| return msg; |
| }); |
| return [id, { ...session, messages: storableMessages }]; |
| }); |
| localStorage.setItem('reelCreatorChats', JSON.stringify(storableChatsArray)); |
| } |
| }, [chats]); |
| |
| const getOrCreateGeminiChatInstance = useCallback(async (chatId: string): Promise<Chat> => { |
| if (transientGeminiChatsRef.current.has(chatId)) { |
| return transientGeminiChatsRef.current.get(chatId)!; |
| } |
| const chatSession = chats.get(chatId); |
| const history = chatSession |
| ? chatSession.messages |
| .filter(m => !m.error) |
| .map(m => { |
| let messageText = m.text; |
| if (m.file) { |
| |
| messageText = `${m.text} (User had attached ${m.file.type.startsWith('image/') ? 'image' : 'document'}: ${m.file.name})`; |
| } |
| return { |
| role: m.sender === 'user' ? 'user' : 'model', |
| parts: [{text: messageText}] |
| } |
| }) |
| : []; |
| |
| const newInstance = await geminiService.createChatSessionWithHistory(REEL_BOT_SYSTEM_INSTRUCTION, history); |
| transientGeminiChatsRef.current.set(chatId, newInstance); |
| return newInstance; |
| }, [chats]); |
|
|
| const handleStopGeneration = useCallback(() => { |
| isCancelledRef.current = true; |
| }, []); |
|
|
| const handleSendMessage = useCallback(async (userInput: string, file?: UploadedFile, isSuggestion: boolean = false) => { |
| if (!userInput.trim() && !file) return; |
| |
| isCancelledRef.current = false; |
| setIsLoading(true); |
| setError(null); |
|
|
| let currentChatId = activeChatId; |
| let currentChatSession: ChatSession | undefined; |
|
|
| const userMessage: ChatMessage = { |
| id: Date.now().toString(), |
| text: userInput, |
| sender: 'user', |
| timestamp: Date.now(), |
| file: file ? { name: file.name, type: file.type, size: file.size, dataUrl: file.dataUrl } : undefined, |
| }; |
|
|
| if (currentChatId === NEW_CHAT_ID || !currentChatId || !chats.has(currentChatId)) { |
| const newChatId = Date.now().toString(); |
| let chatName = "Nuevo Chat"; |
| const trimmedInput = userInput.trim(); |
|
|
| if (trimmedInput) { |
| const words = trimmedInput.split(' '); |
| chatName = words.slice(0, 5).join(' '); |
| if (chatName.length > 30) { |
| chatName = chatName.substring(0, 27) + "..."; |
| } |
| } else if (file) { |
| chatName = `Chat con ${file.name}`; |
| if (chatName.length > 30) { |
| chatName = chatName.substring(0, 27) + "..."; |
| } |
| } else { |
| chatName = `Nuevo Chat ${new Date().toLocaleTimeString()}`; |
| } |
| |
| currentChatSession = { |
| id: newChatId, |
| name: chatName, |
| messages: [userMessage], |
| createdAt: Date.now(), |
| }; |
| setChats(prev => new Map(prev).set(newChatId, currentChatSession!)); |
| setActiveChatId(newChatId); |
| currentChatId = newChatId; |
| } else { |
| currentChatSession = chats.get(currentChatId); |
| if (currentChatSession) { |
| const updatedMessages = [...currentChatSession.messages, userMessage]; |
| setChats(prev => new Map(prev).set(currentChatId!, { ...currentChatSession!, messages: updatedMessages })); |
| } |
| } |
| |
| if (!currentChatSession || !currentChatId) { |
| setError("Failed to create or find chat session."); |
| setIsLoading(false); |
| return; |
| } |
|
|
| const modelMessageId = Date.now().toString() + '_model'; |
| const initialModelMessage: ChatMessage = { |
| id: modelMessageId, |
| text: '', |
| sender: 'model', |
| timestamp: Date.now(), |
| isStreaming: true, |
| }; |
|
|
| setChats(prev => { |
| const updatedChats = new Map(prev); |
| const chat = updatedChats.get(currentChatId!); |
| if (chat) { |
| const updatedMessages = [...chat.messages, initialModelMessage]; |
| updatedChats.set(currentChatId!, { ...chat, messages: updatedMessages }); |
| } |
| return updatedChats; |
| }); |
| |
| let accumulatedText = ""; |
| let groundingChunks: ChatMessage['groundingChunks'] = []; |
| try { |
| const geminiChat = await getOrCreateGeminiChatInstance(currentChatId); |
| const imageFileToSend = (file?.type.startsWith('image/') && file.dataUrl) ? file : undefined; |
|
|
| const stream = await geminiService.sendMessageStream(geminiChat, userInput, imageFileToSend); |
| for await (const chunk of stream) { |
| if (isCancelledRef.current) { |
| console.log("Generation stopped by user."); |
| break; |
| } |
| accumulatedText += chunk.text; |
| if (chunk.groundingChunks && chunk.groundingChunks.length > 0) { |
| groundingChunks = [...(groundingChunks || []), ...chunk.groundingChunks]; |
| } |
| |
| setChats(prev => { |
| const updatedChats = new Map(prev); |
| const chat = updatedChats.get(currentChatId!); |
| if (chat) { |
| const msgIndex = chat.messages.findIndex(m => m.id === modelMessageId); |
| if (msgIndex !== -1) { |
| const newMessages = [...chat.messages]; |
| newMessages[msgIndex] = { |
| ...newMessages[msgIndex], |
| text: accumulatedText, |
| groundingChunks: groundingChunks.length > 0 ? groundingChunks : undefined, |
| isStreaming: true |
| }; |
| updatedChats.set(currentChatId!, { ...chat, messages: newMessages }); |
| } |
| } |
| return updatedChats; |
| }); |
| } |
| |
| setChats(prev => { |
| const updatedChats = new Map(prev); |
| const chat = updatedChats.get(currentChatId!); |
| if (chat) { |
| const msgIndex = chat.messages.findIndex(m => m.id === modelMessageId); |
| if (msgIndex !== -1) { |
| const newMessages = [...chat.messages]; |
| newMessages[msgIndex] = { |
| ...newMessages[msgIndex], |
| text: accumulatedText, |
| isStreaming: false, |
| groundingChunks: groundingChunks.length > 0 ? groundingChunks : undefined |
| }; |
| updatedChats.set(currentChatId!, { ...chat, messages: newMessages }); |
| } |
| } |
| return updatedChats; |
| }); |
|
|
| } catch (e: any) { |
| console.error("Error sending message to Gemini:", e); |
| const errorMessage = e.message || "An error occurred with the AI service."; |
| setError(errorMessage); |
| setChats(prev => { |
| const updatedChats = new Map(prev); |
| const chat = updatedChats.get(currentChatId!); |
| if (chat) { |
| const msgIndex = chat.messages.findIndex(m => m.id === modelMessageId); |
| if (msgIndex !== -1) { |
| const newMessages = [...chat.messages]; |
| newMessages[msgIndex] = { |
| ...newMessages[msgIndex], |
| text: accumulatedText || `Error: ${errorMessage}`, |
| isStreaming: false, |
| error: errorMessage |
| }; |
| updatedChats.set(currentChatId!, { ...chat, messages: newMessages }); |
| } else { |
| const errorMsgEntry: ChatMessage = { |
| id: modelMessageId, |
| text: `Error: ${errorMessage}`, |
| sender: 'model', |
| timestamp: Date.now(), |
| isStreaming: false, |
| error: errorMessage |
| }; |
| const newMessages = [...chat.messages, errorMsgEntry]; |
| updatedChats.set(currentChatId!, { ...chat, messages: newMessages }); |
| } |
| } |
| return updatedChats; |
| }); |
| } finally { |
| setIsLoading(false); |
| } |
| }, [activeChatId, chats, getOrCreateGeminiChatInstance]); |
|
|
| const handleSelectChat = useCallback((chatId: string | null) => { |
| setActiveChatId(chatId); |
| setIsSidebarOpen(false); |
| if (chatId && chatId !== NEW_CHAT_ID && chats.has(chatId)) { |
| getOrCreateGeminiChatInstance(chatId); |
| } |
| }, [chats, getOrCreateGeminiChatInstance]); |
|
|
| const handleCreateNewChat = useCallback(() => { |
| setActiveChatId(NEW_CHAT_ID); |
| setIsSidebarOpen(false); |
| }, []); |
| |
| const activeChatSession = activeChatId === NEW_CHAT_ID || !activeChatId ? null : chats.get(activeChatId) || null; |
|
|
| return ( |
| <div className="flex h-screen bg-slate-900 text-slate-100 overflow-hidden md:overflow-auto"> |
| <Sidebar |
| chats={Array.from(chats.values())} |
| activeChatId={activeChatId} |
| onSelectChat={handleSelectChat} |
| onCreateNewChat={handleCreateNewChat} |
| isOpen={isSidebarOpen} |
| onClose={() => setIsSidebarOpen(false)} |
| /> |
| |
| {isSidebarOpen && ( |
| <div |
| onClick={() => setIsSidebarOpen(false)} |
| className="fixed inset-0 z-30 bg-black/60 md:hidden" |
| aria-hidden="true" |
| /> |
| )} |
| |
| <div className="flex-1 flex flex-col h-full"> |
| <div className="p-3 border-b border-slate-700 md:hidden flex items-center justify-start bg-slate-900 sticky top-0 z-20"> |
| <button |
| onClick={() => setIsSidebarOpen(true)} |
| className="text-slate-300 hover:text-slate-100 p-2 rounded-md focus:outline-none focus:ring-2 focus:ring-cyan-500" |
| aria-label="Open menu" |
| > |
| <MenuIcon className="w-6 h-6" /> |
| </button> |
| {/* Title text and spacer div removed here */} |
| </div> |
| |
| <ChatView |
| activeChatSession={activeChatSession} |
| onSendMessage={handleSendMessage} |
| isLoading={isLoading} |
| error={error} |
| onStopGeneration={handleStopGeneration} |
| /> |
| </div> |
| </div> |
| ); |
| }; |
|
|
| export default App; |
|
|