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>(new Map()); const [activeChatId, setActiveChatId] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [isSidebarOpen, setIsSidebarOpen] = useState(false); const transientGeminiChatsRef = useRef>(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/')) { // Potentially handle old document structures } }); }); setChats(new Map(parsedChatsArray)); if (parsedChatsArray.length > 0) { // setActiveChatId(parsedChatsArray[0][0]); // Keep previous logic or decide default } 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 { /* rawFile, */ ...fileToStore } = msg.file; // rawFile removed from example 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 => { 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) { // Keeping simplified logic for history 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); // Close sidebar on chat selection (mobile) if (chatId && chatId !== NEW_CHAT_ID && chats.has(chatId)) { getOrCreateGeminiChatInstance(chatId); } }, [chats, getOrCreateGeminiChatInstance]); const handleCreateNewChat = useCallback(() => { setActiveChatId(NEW_CHAT_ID); setIsSidebarOpen(false); // Close sidebar on new chat (mobile) }, []); const activeChatSession = activeChatId === NEW_CHAT_ID || !activeChatId ? null : chats.get(activeChatId) || null; return (
setIsSidebarOpen(false)} /> {isSidebarOpen && (
setIsSidebarOpen(false)} className="fixed inset-0 z-30 bg-black/60 md:hidden" aria-hidden="true" /> )}
{/* Title text and spacer div removed here */}
); }; export default App;