import React, { useState, useEffect, useRef } from 'react'; import { Settings as SettingsIcon, MessageSquare, Sparkles, Menu, X, Mic, ImagePlus, Send, Loader2, Volume2, Trash2, Plus, BookOpen, Brain, GraduationCap, Coffee, History, ChevronRight, ChevronLeft, Calendar } from 'lucide-react'; import ReactMarkdown from 'react-markdown'; import { TRANSLATIONS, DEFAULT_LANGUAGE } from './constants'; import { AppLanguage, ChatMode, Message, UserSettings, ChatSession, ChatScenario } from './types'; import { loadSettings, saveSettings, loadSessions, saveSessions, exportData, importData, clearData } from './services/storage'; import { streamChatResponse, transcribeAudio, generateSpeech } from './services/geminiService'; import Tools from './components/Tools'; const App: React.FC = () => { // State const [settings, setSettingsState] = useState(loadSettings()); const [sessions, setSessions] = useState([]); const [currentSessionId, setCurrentSessionId] = useState(null); // New State for Lazy Creation const [selectedScenario, setSelectedScenario] = useState(ChatScenario.GENERAL); // Track selected mode for the upcoming session const [selectedMode, setSelectedMode] = useState(ChatMode.STANDARD); const [input, setInput] = useState(''); const [activeView, setActiveView] = useState<'chat' | 'tools' | 'settings'>('chat'); // Sidebar States const [isLeftSidebarOpen, setIsLeftSidebarOpen] = useState(false); const [isRightSidebarOpen, setIsRightSidebarOpen] = useState(true); const [isProcessing, setIsProcessing] = useState(false); const [streamingContent, setStreamingContent] = useState(''); const [attachments, setAttachments] = useState<{mimeType: string, data: string, name?: string}[]>([]); const [isRecording, setIsRecording] = useState(false); // Refs const messagesEndRef = useRef(null); const fileInputRef = useRef(null); const mediaRecorderRef = useRef(null); const t = TRANSLATIONS[settings.language] || TRANSLATIONS[DEFAULT_LANGUAGE]; // Effects useEffect(() => { const loadedSessions = loadSessions(); setSessions(loadedSessions); // On load, do NOT select a session, let user start fresh or pick one }, []); // Responsive Sidebar Logic useEffect(() => { const handleResize = () => { if (window.innerWidth < 1024) { setIsRightSidebarOpen(false); } else { setIsRightSidebarOpen(true); } }; // Set initial state handleResize(); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); useEffect(() => { saveSettings(settings); }, [settings]); useEffect(() => { saveSessions(sessions); }, [sessions]); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [sessions, streamingContent, currentSessionId]); // Helpers const getCurrentSession = () => sessions.find(s => s.id === currentSessionId); // Instead of creating session immediately, we prepare the UI const handleScenarioSelect = (scenario: ChatScenario) => { setSelectedScenario(scenario); setCurrentSessionId(null); // Clear current session to show "New Chat" state setActiveView('chat'); setIsLeftSidebarOpen(false); // Close mobile menu if open }; const deleteSession = (e: React.MouseEvent, id: string) => { e.stopPropagation(); if (window.confirm(t.confirmDelete)) { setSessions(prev => prev.filter(s => s.id !== id)); if (currentSessionId === id) setCurrentSessionId(null); } }; const getScenarioIcon = (scenario?: ChatScenario) => { switch (scenario) { case ChatScenario.READING: return ; case ChatScenario.CONCEPT: return ; case ChatScenario.RESEARCH: return ; case ChatScenario.GENERAL: default: return ; } }; const SCENARIOS = [ ChatScenario.GENERAL, ChatScenario.READING, ChatScenario.CONCEPT, ChatScenario.RESEARCH ]; // Group Sessions by Date AND Filter by current Scenario const getGroupedSessions = () => { const groups: { [key: string]: ChatSession[] } = { 'Today': [], 'Yesterday': [], 'Previous 7 Days': [], 'Older': [] }; const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime(); const yesterday = today - 86400000; const lastWeek = today - 86400000 * 7; // Filter sessions to only show those belonging to the selected scenario const filteredSessions = sessions.filter(s => { // Default to GENERAL if scenario is undefined (legacy data) const sessionScenario = s.scenario || ChatScenario.GENERAL; return sessionScenario === selectedScenario; }); filteredSessions.forEach(s => { if (s.createdAt >= today) groups['Today'].push(s); else if (s.createdAt >= yesterday) groups['Yesterday'].push(s); else if (s.createdAt >= lastWeek) groups['Previous 7 Days'].push(s); else groups['Older'].push(s); }); return groups; }; // Handlers const handleSendMessage = async () => { if ((!input.trim() && attachments.length === 0) || isProcessing) return; let session = getCurrentSession(); let isNewSession = false; // LAZY CREATION LOGIC: // If no session exists, create one now if (!session) { isNewSession = true; const scenarioConfig = t.scenarios[selectedScenario]; const initialGreeting = scenarioConfig.greeting; const newSession: ChatSession = { id: Date.now().toString(), title: input.slice(0, 30) || t.newChat, // Set title immediately from first prompt messages: [ { id: (Date.now() - 100).toString(), role: 'model', content: initialGreeting, // Inject the greeting that was shown in UI timestamp: Date.now() - 100 } ], mode: selectedMode, // Use the selected mode scenario: selectedScenario, createdAt: Date.now() }; // Update local variable to use immediately session = newSession; } // Prepare User Message const userMsg: Message = { id: Date.now().toString(), role: 'user', content: input, timestamp: Date.now(), attachments: attachments.map(a => ({ type: 'image', ...a })) }; // Optimistic UI Update const updatedMessages = [...session!.messages, userMsg]; if (isNewSession) { setSessions(prev => [ { ...session!, messages: updatedMessages }, ...prev ]); setCurrentSessionId(session!.id); } else { setSessions(prev => prev.map(s => s.id === session!.id ? { ...s, messages: updatedMessages } : s)); } setInput(''); setAttachments([]); setIsProcessing(true); setStreamingContent(''); try { let fullResponse = ''; let groundingData: any = null; await streamChatResponse( updatedMessages, userMsg.content, session!.mode, settings.language, session!.scenario || ChatScenario.GENERAL, userMsg.attachments as any, (text, grounding) => { fullResponse += text; setStreamingContent(fullResponse); if (grounding) groundingData = grounding; } ); const modelMsg: Message = { id: (Date.now() + 1).toString(), role: 'model', content: fullResponse, timestamp: Date.now(), groundingMetadata: groundingData }; setSessions(prev => prev.map(s => s.id === session!.id ? { ...s, messages: [...updatedMessages, modelMsg] } : s)); } catch (err) { console.error(err); const errorMsg: Message = { id: Date.now().toString(), role: 'model', content: t.apiError, timestamp: Date.now() }; setSessions(prev => prev.map(s => s.id === session!.id ? { ...s, messages: [...updatedMessages, errorMsg] } : s)); } finally { setIsProcessing(false); setStreamingContent(''); } }; // ... (Keep existing helpers: handleFileUpload, handleRecordAudio, playTTS, handleImport) const handleFileUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onloadend = () => { const base64String = reader.result as string; const base64Data = base64String.split(',')[1]; setAttachments(prev => [...prev, { mimeType: file.type, data: base64Data, name: file.name }]); }; reader.readAsDataURL(file); e.target.value = ''; }; const handleRecordAudio = async () => { if (isRecording) { mediaRecorderRef.current?.stop(); setIsRecording(false); return; } try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const mediaRecorder = new MediaRecorder(stream); mediaRecorderRef.current = mediaRecorder; const chunks: BlobPart[] = []; mediaRecorder.ondataavailable = (e) => chunks.push(e.data); mediaRecorder.onstop = async () => { const blob = new Blob(chunks, { type: 'audio/webm' }); const reader = new FileReader(); reader.onloadend = async () => { const base64 = (reader.result as string).split(',')[1]; setIsProcessing(true); try { const text = await transcribeAudio(base64, 'audio/webm'); setInput(prev => prev + " " + text); } catch (e) { console.error(e); alert(t.transcriptionFail); } finally { setIsProcessing(false); } }; reader.readAsDataURL(blob); stream.getTracks().forEach(track => track.stop()); }; mediaRecorder.start(); setIsRecording(true); } catch (e) { console.error("Mic error", e); alert(t.micError); } }; const playTTS = async (text: string) => { try { const buffer = await generateSpeech(text); const ctx = new (window.AudioContext || (window as any).webkitAudioContext)(); const source = ctx.createBufferSource(); source.buffer = buffer; source.connect(ctx.destination); source.start(0); } catch (e) { console.error("TTS Error", e); } }; const handleImport = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { const success = await importData(file); if (success) { setSettingsState(loadSettings()); setSessions(loadSessions()); alert(t.importSuccess); } else { alert(t.importFail); } } }; const currentSession = getCurrentSession(); const groupedSessions = getGroupedSessions(); // Helper to get active mode for UI display const activeMode = currentSession ? currentSession.mode : selectedMode; return (
{/* 1. LEFT SIDEBAR (Navigation) */} {isLeftSidebarOpen && (
setIsLeftSidebarOpen(false)} /> )} {/* 2. MAIN CONTENT (Center) */}
{/* Header */}
{activeView === 'chat' && (
{getScenarioIcon(currentSession ? currentSession.scenario : selectedScenario)} {t.scenarios[(currentSession ? currentSession.scenario : selectedScenario) || ChatScenario.GENERAL].title}
)}
{/* Mode Switcher */} {activeView === 'chat' && (
)} {/* Toggle Right Sidebar - Only visible in Chat View */} {activeView === 'chat' && ( )}
{/* View Content */}
{activeView === 'chat' && (
{/* Messages Area */}
{!currentSession ? ( /* Welcome State (No session yet) */
{getScenarioIcon(selectedScenario)}

{t.scenarios[selectedScenario].title}

{t.scenarios[selectedScenario].greeting}

) : ( /* Active Session Messages */ <> {currentSession.messages.map((msg, idx) => (
{msg.role === 'model' && (
{getScenarioIcon(currentSession.scenario)}
)}
{msg.attachments?.map((att, i) => (
attachment
))}
{msg.content}
{new Date(msg.timestamp).toLocaleTimeString()} {msg.role === 'model' && ( <> {msg.groundingMetadata?.groundingChunks && msg.groundingMetadata.groundingChunks.length > 0 && ( {t.searchSources} )} )}
{msg.role === 'model' && msg.groundingMetadata?.groundingChunks && (

{t.searchSources}:

    {msg.groundingMetadata.groundingChunks.map((chunk, i) => chunk.web && (
  • {chunk.web.title}
  • ))}
)}
))} {isProcessing && streamingContent && (
{getScenarioIcon(currentSession.scenario)}
{streamingContent}
{currentSession?.mode === ChatMode.DEEP ? t.thinking : t.generating}
)}
)}
{/* Input Area */}
{attachments.length > 0 && (
{attachments.map((a, i) => (
preview
))}
)}