Spaces:
Configuration error
Configuration error
| import React, { useState, useEffect, useRef, useCallback } from 'react'; | |
| import { Chat } from '@google/genai'; | |
| import { Navbar } from './components/Layout/Navbar'; | |
| import { Sidebar } from './components/Sidebar'; | |
| import { DashboardView } from './components/Dashboard/DashboardView'; | |
| import { MessageBubble, stopAllMessageAudio } from './components/Chat/MessageBubble'; | |
| import { InputArea } from './components/Chat/InputArea'; | |
| import { ExamView } from './components/Exam/ExamView'; | |
| import { PlannerView } from './components/Planner/PlannerView'; | |
| import { FlashcardView } from './components/Flashcards/FlashcardView'; | |
| import { PastPaperGenerator } from './components/PastPapers/PastPaperGenerator'; | |
| import { WhiteboardCanvas, WhiteboardRef } from './components/Whiteboard/WhiteboardCanvas'; | |
| import { StatsView } from './components/Stats/StatsView'; | |
| import { SpellingVocabView } from './components/Spelling/SpellingVocabView'; | |
| import { SessionReplayView } from './components/Dashboard/SessionReplayView'; | |
| import { LandingPage } from './components/LandingPage'; | |
| import { OnboardingTour } from './components/Onboarding/OnboardingTour'; | |
| import { Icon } from './components/ui/Icon'; | |
| import { NeuralBackground } from './components/ui/NeuralBackground'; | |
| import { | |
| PERSONAS, | |
| JUNIOR_PERSONAS, | |
| INITIAL_DISCOVERY_SVG, | |
| INITIAL_DISCOVERY_MESSAGE, | |
| INITIAL_JUNIOR_SVG, | |
| INITIAL_JUNIOR_MESSAGE, | |
| INITIAL_MATH_SVG, | |
| INITIAL_MATH_MESSAGE, | |
| INITIAL_ENGLISH_SVG, | |
| INITIAL_ENGLISH_MESSAGE, | |
| INITIAL_WRITING_SVG, | |
| INITIAL_WRITING_MESSAGE, | |
| } from './constants'; | |
| import { createChatSession, sendMessageStream, generateProImage } from './services/geminiService'; | |
| import { playSound } from './services/audioEffects'; | |
| import { Message, Role, Persona, ViewMode, DetailedTopicAnalytics, TTSMode, PlatformLevel, StandardLevel, IllustrationMode, LearningSession } from './types'; | |
| const SVG_CANDIDATE_REGEX = /```(?:svg|xml|html)?\s*([\s\S]*?)```/gi; | |
| const PARTIAL_SVG_REGEX = /```(?:svg|xml|html)?\s*(<svg[\s\S]*)$/i; | |
| const AUDIO_TAG_REGEX = /\[{1,2}\s*AUDIO\s*[|:]\s*([\s\S]*?)\s*\]{1,2}/gi; | |
| interface SessionData { | |
| chatSession: Chat | null; | |
| messages: Message[]; | |
| latestSvg: string; | |
| whiteboardHistory: string[]; | |
| historyIndex: number; | |
| whiteboardImage: string | null; | |
| functionPlots: Array<{ formula: string, color: string, xRange: number[] }>; | |
| browserUrl: string | null; | |
| browserTitle: string | null; | |
| illustrationMode: IllustrationMode; | |
| } | |
| const getInitialContent = (personaId: string, platform: PlatformLevel) => { | |
| if (platform === 'JUNIOR') { | |
| if (personaId.includes('math')) return { svg: INITIAL_MATH_SVG, msg: INITIAL_MATH_MESSAGE }; | |
| return { svg: INITIAL_JUNIOR_SVG, msg: INITIAL_JUNIOR_MESSAGE }; | |
| } | |
| switch (personaId) { | |
| case 'math': return { svg: INITIAL_MATH_SVG, msg: INITIAL_MATH_MESSAGE }; | |
| case 'english': return { svg: INITIAL_ENGLISH_SVG, msg: INITIAL_ENGLISH_MESSAGE }; | |
| case 'writing-lab': return { svg: INITIAL_WRITING_SVG, msg: INITIAL_WRITING_MESSAGE }; | |
| default: return { svg: INITIAL_DISCOVERY_SVG, msg: INITIAL_DISCOVERY_MESSAGE }; | |
| } | |
| }; | |
| export function App() { | |
| const [isAuthenticated, setIsAuthenticated] = useState(false); | |
| const [platformLevel, setPlatformLevel] = useState<PlatformLevel>('SEA'); | |
| const [standardLevel, setStandardLevel] = useState<StandardLevel>(null); | |
| const [activePersona, setActivePersona] = useState<Persona>(PERSONAS[0]); | |
| const [currentView, setCurrentView] = useState<ViewMode>('whiteboard'); | |
| const [messages, setMessages] = useState<Message[]>([]); | |
| const [isProcessing, setIsProcessing] = useState(false); | |
| const [chatSession, setChatSession] = useState<Chat | null>(null); | |
| const [isDarkMode, setIsDarkMode] = useState(false); | |
| const [globalTheme, setGlobalTheme] = useState<string>('blue'); | |
| const [ttsMode, setTtsMode] = useState<TTSMode>('browser'); | |
| const [isMuted, setIsMuted] = useState(false); | |
| const [latestSvg, setLatestSvg] = useState<string>(INITIAL_DISCOVERY_SVG); | |
| const [whiteboardImage, setWhiteboardImage] = useState<string | null>(null); | |
| const [browserUrl, setBrowserUrl] = useState<string | null>(null); | |
| const [browserTitle, setBrowserTitle] = useState<string | null>(null); | |
| const [functionPlots, setFunctionPlots] = useState<Array<{ formula: string, color: string, xRange: number[] }>>([]); | |
| const [whiteboardMode, setWhiteboardMode] = useState<'split' | 'focus'>('split'); | |
| const [analyticsData, setAnalyticsData] = useState<DetailedTopicAnalytics[]>([]); | |
| const [whiteboardHistory, setWhiteboardHistory] = useState<string[]>([INITIAL_DISCOVERY_SVG]); | |
| const [historyIndex, setHistoryIndex] = useState(0); | |
| const [historicalSessions, setHistoricalSessions] = useState<LearningSession[]>([]); | |
| const [replaySession, setReplaySession] = useState<LearningSession | null>(null); | |
| const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false); | |
| const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); | |
| const [showTour, setShowTour] = useState(false); | |
| const [illustrationMode, setIllustrationMode] = useState<IllustrationMode>('standard'); | |
| const sessionsStore = useRef<Record<string, SessionData>>({}); | |
| const chatContainerRef = useRef<HTMLDivElement>(null); | |
| const messagesEndRef = useRef<HTMLDivElement>(null); | |
| const whiteboardRef = useRef<WhiteboardRef>(null); | |
| useEffect(() => { | |
| if (isDarkMode) document.documentElement.classList.add('dark'); | |
| else document.documentElement.classList.remove('dark'); | |
| }, [isDarkMode]); | |
| useEffect(() => { | |
| messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); | |
| }, [messages]); | |
| const archiveCurrentSession = useCallback(() => { | |
| if (messages.length <= 1) return; // Don't archive initial states with no interaction | |
| const lastMsg = messages[messages.length - 1]; | |
| const newSession: LearningSession = { | |
| id: Date.now().toString(), | |
| topic: messages[1]?.text.slice(0, 30) || "Dynamic Study", | |
| subject: activePersona.name.split(' ')[0], | |
| date: new Date(), | |
| mastery: Math.floor(Math.random() * 40) + 60, // Mock mastery calculation | |
| personaName: activePersona.name, | |
| personaColor: activePersona.color, | |
| messages: [...messages], | |
| slides: [...whiteboardHistory], | |
| summary: `In this session, Kerwin explored ${activePersona.name} concepts and demonstrated strong engagement with visual models.` | |
| }; | |
| setHistoricalSessions(prev => [newSession, ...prev]); | |
| }, [messages, whiteboardHistory, activePersona]); | |
| const processResponse = useCallback((text: string) => { | |
| let displayText = text; | |
| let foundSvg = undefined; | |
| let audioSummary = undefined; | |
| const audioMatch = Array.from(displayText.matchAll(AUDIO_TAG_REGEX)); | |
| if (audioMatch.length > 0) { | |
| audioSummary = audioMatch[0][1].trim(); | |
| displayText = displayText.replace(AUDIO_TAG_REGEX, ''); | |
| } | |
| const matches = [...displayText.matchAll(SVG_CANDIDATE_REGEX)]; | |
| if (matches.length > 0) { | |
| for (const m of matches) { | |
| const content = m[1].trim(); | |
| if (content.includes('<svg')) foundSvg = content; | |
| } | |
| } | |
| if (!foundSvg) { | |
| const partialMatch = displayText.match(PARTIAL_SVG_REGEX); | |
| if (partialMatch) foundSvg = partialMatch[1]; | |
| } | |
| displayText = displayText.replace(SVG_CANDIDATE_REGEX, '').replace(PARTIAL_SVG_REGEX, '').trim(); | |
| return { displayText, svgContent: foundSvg, audioSummary }; | |
| }, []); | |
| const handleLogin = (platform: PlatformLevel, standard?: StandardLevel) => { | |
| setPlatformLevel(platform); | |
| setStandardLevel(standard || null); | |
| let initialPersona: Persona; | |
| let initContent: { svg: string, msg: string }; | |
| if (platform === 'JUNIOR') { | |
| setGlobalTheme('orange'); | |
| initialPersona = JUNIOR_PERSONAS[0]; | |
| initContent = getInitialContent(initialPersona.id, 'JUNIOR'); | |
| } else { | |
| setGlobalTheme('blue'); | |
| initialPersona = PERSONAS[0]; | |
| initContent = getInitialContent(initialPersona.id, 'SEA'); | |
| } | |
| const { displayText, audioSummary } = processResponse(initContent.msg); | |
| const initialMessages: Message[] = [{ | |
| id: 'init-'+Date.now(), | |
| role: Role.MODEL, | |
| text: displayText, | |
| audioSummary, | |
| timestamp: new Date() | |
| }]; | |
| setActivePersona(initialPersona); | |
| setLatestSvg(initContent.svg); | |
| setMessages(initialMessages); | |
| setWhiteboardHistory([initContent.svg]); | |
| let instruction = initialPersona.systemInstruction; | |
| if (platform === 'JUNIOR' && standard) instruction += `\nCONTEXT: Standard ${standard}.`; | |
| const session = createChatSession(instruction, { illustrationMode: 'standard' }); | |
| setChatSession(session); | |
| setIsAuthenticated(true); | |
| const tourComplete = localStorage.getItem('prepair_tour_complete'); | |
| if (!tourComplete) setTimeout(() => setShowTour(true), 1500); | |
| }; | |
| const handleLogout = () => { | |
| setIsAuthenticated(false); | |
| setPlatformLevel('SEA'); | |
| setStandardLevel(null); | |
| sessionsStore.current = {}; | |
| }; | |
| const handleToggleMute = () => { | |
| setIsMuted(prev => !prev); | |
| stopAllMessageAudio(); | |
| }; | |
| const primaryUiColor = globalTheme; | |
| const handleNavigateHistory = (direction: 'prev' | 'next') => { | |
| let newIndex = historyIndex; | |
| if (direction === 'prev') newIndex = Math.max(0, historyIndex - 1); | |
| else newIndex = Math.min(whiteboardHistory.length - 1, historyIndex + 1); | |
| if (newIndex !== historyIndex) { | |
| setHistoryIndex(newIndex); | |
| setLatestSvg(whiteboardHistory[newIndex]); | |
| setWhiteboardImage(null); | |
| } | |
| }; | |
| const saveCurrentSession = useCallback(() => { | |
| sessionsStore.current[activePersona.id] = { | |
| chatSession, messages, latestSvg, whiteboardHistory, historyIndex, | |
| whiteboardImage, functionPlots, browserUrl, browserTitle, illustrationMode | |
| }; | |
| }, [activePersona.id, chatSession, messages, latestSvg, whiteboardHistory, historyIndex, whiteboardImage, functionPlots, browserUrl, browserTitle, illustrationMode]); | |
| const handlePersonaTransition = async (newPersona: Persona, deferredPrompt?: string) => { | |
| if (newPersona.id === activePersona.id) return; | |
| archiveCurrentSession(); | |
| saveCurrentSession(); | |
| const saved = sessionsStore.current[newPersona.id]; | |
| if (saved && !deferredPrompt) { | |
| setActivePersona(newPersona); | |
| setLatestSvg(saved.latestSvg); | |
| setMessages(saved.messages); | |
| setWhiteboardHistory(saved.whiteboardHistory); | |
| setHistoryIndex(saved.historyIndex); | |
| setChatSession(saved.chatSession); | |
| setWhiteboardImage(saved.whiteboardImage); | |
| setFunctionPlots(saved.functionPlots); | |
| setBrowserUrl(saved.browserUrl); | |
| setBrowserTitle(saved.browserTitle); | |
| setIllustrationMode(saved.illustrationMode); | |
| } else { | |
| const { svg, msg } = getInitialContent(newPersona.id, platformLevel); | |
| const { displayText, audioSummary } = processResponse(msg); | |
| const nextMessages: Message[] = [{ id: 'init-'+Date.now(), role: Role.MODEL, text: displayText, audioSummary, timestamp: new Date() }]; | |
| let instruction = newPersona.systemInstruction; | |
| if (platformLevel === 'JUNIOR' && standardLevel) instruction += `\nCONTEXT: Standard ${standardLevel}.`; | |
| const nextChatSession = createChatSession(instruction, { illustrationMode }); | |
| setActivePersona(newPersona); | |
| setLatestSvg(svg); | |
| setMessages(nextMessages); | |
| setWhiteboardHistory([svg]); | |
| setHistoryIndex(0); | |
| setChatSession(nextChatSession); | |
| setWhiteboardImage(null); | |
| setFunctionPlots([]); | |
| setBrowserUrl(null); | |
| setBrowserTitle(null); | |
| if (deferredPrompt) setTimeout(() => handleSendMessage(deferredPrompt, undefined, true, true), 100); | |
| } | |
| }; | |
| const handleContinueSession = (session: LearningSession) => { | |
| // 1. Identify Persona | |
| const personas = platformLevel === 'SEA' ? PERSONAS : JUNIOR_PERSONAS; | |
| const persona = personas.find(p => p.name === session.personaName) || personas[0]; | |
| // 2. Prepare state | |
| setActivePersona(persona); | |
| setMessages(session.messages); | |
| setWhiteboardHistory(session.slides); | |
| setHistoryIndex(session.slides.length - 1); | |
| setLatestSvg(session.slides[session.slides.length - 1]); | |
| // 3. Initialize fresh chat session with the persona context | |
| // Ideally we'd feed the history back into Gemini, but for this implementation | |
| // we reset the session and the user continues from the last visual state. | |
| let instruction = persona.systemInstruction; | |
| if (platformLevel === 'JUNIOR' && standardLevel) instruction += `\nCONTEXT: Standard ${standardLevel}.`; | |
| const nextChatSession = createChatSession(instruction, { illustrationMode }); | |
| setChatSession(nextChatSession); | |
| // 4. Transition view | |
| setReplaySession(null); | |
| setCurrentView('whiteboard'); | |
| playSound('success'); | |
| }; | |
| const handleViewTransition = async (newView: ViewMode) => { | |
| if (newView === currentView) return; | |
| if (currentView === 'whiteboard') { | |
| archiveCurrentSession(); | |
| saveCurrentSession(); | |
| } | |
| setCurrentView(newView); | |
| setIsMobileSidebarOpen(false); | |
| }; | |
| const handleSendMessage = useCallback(async (text: string, imageBase64?: string, isHidden = false, skipRouting = false) => { | |
| if (!chatSession) return; | |
| const personas = platformLevel === 'SEA' ? PERSONAS : JUNIOR_PERSONAS; | |
| const lowerText = text.toLowerCase(); | |
| // Enforce priority whiteboard updates and strict feedback protocol | |
| const visualDirective = `\n\n[DIRECTIVE: MANDATORY WHITEBOARD SYNC. START YOUR RESPONSE WITH THE SVG BLOCK. | |
| SVG Viewbox 960x540 update is REQUIRED. | |
| EXPLANATION PROTOCOL: If the user is right, explicitly state "Correct" and explain WHY. If wrong, explicitly state "Incorrect", explain WHY, and scaffold. | |
| MASTERY PROTOCOL: If the user demonstrates mastery (2 correct in a row), suggest moving to the next advanced syllabus area. | |
| Ask exactly one follow-up to confirm understanding. Chat text MUST be under 3 sentences.]`; | |
| const finalPrompt = text.includes('[ACTION:') ? text : text + visualDirective; | |
| if (!skipRouting && activePersona.id.includes('discovery')) { | |
| if (lowerText.match(/math|number|calculat|fraction|geometry/)) { | |
| handlePersonaTransition(personas.find(p => p.id.includes('math'))!, `[ACTION: INITIALIZE_VISUAL_MODULE] Topic: "${text}". Start interactive whiteboard lesson.`); | |
| return; | |
| } | |
| if (lowerText.match(/english|reading|word|grammar|vocab|spelling/)) { | |
| handlePersonaTransition(personas.find(p => p.id.includes('english') || p.id.includes('reading'))!, `[ACTION: INITIALIZE_VISUAL_MODULE] Topic: "${text}". Start interactive whiteboard lesson.`); | |
| return; | |
| } | |
| if (lowerText.match(/story|writing|narrative|coach/)) { | |
| handlePersonaTransition(personas.find(p => p.id.includes('write'))!, `[ACTION: INITIALIZE_VISUAL_MODULE] Topic: "${text}". Start interactive whiteboard lesson.`); | |
| return; | |
| } | |
| } | |
| const aiMsgId = (Date.now() + 1).toString(); | |
| if (!isHidden) setMessages(prev => [...prev, { id: Date.now().toString(), role: Role.USER, text, timestamp: new Date(), image: imageBase64 }]); | |
| setIsProcessing(true); | |
| setMessages(prev => [...prev, { id: aiMsgId, role: Role.MODEL, text: '', timestamp: new Date(), isLoading: true }]); | |
| let fullText = ''; | |
| let lastRender = 0; | |
| await sendMessageStream(chatSession, finalPrompt, imageBase64 ? { mimeType: 'image/png', data: imageBase64.split(',')[1] } : undefined, { | |
| onChunk: (chunk) => { | |
| fullText += chunk; | |
| const now = Date.now(); | |
| if (now - lastRender > 30) { | |
| const { displayText, svgContent, audioSummary } = processResponse(fullText); | |
| if (svgContent) { setLatestSvg(svgContent); setWhiteboardImage(null); } | |
| setMessages(prev => prev.map(m => m.id === aiMsgId ? { ...m, text: displayText, audioSummary, isLoading: true } : m)); | |
| lastRender = now; | |
| } | |
| }, | |
| onToolCall: (toolCall) => { | |
| if (toolCall.name === 'display_content') { | |
| const { url, title } = toolCall.args as any; | |
| setBrowserUrl(url); setBrowserTitle(title || "Interactive View"); | |
| } else if (toolCall.name === 'trigger_sound_effect') playSound((toolCall.args as any).effect); | |
| else if (toolCall.name === 'plot_function') { | |
| const { formula, color, xRange } = toolCall.args as any; | |
| setFunctionPlots(prev => [...prev, { formula, color: color || '#ef4444', xRange: xRange || [-12, 12] }]); | |
| } else if (toolCall.name === 'generate_illustration') handleGenerateAIImage((toolCall.args as any).prompt, (toolCall.args as any).size || '1K'); | |
| }, | |
| onComplete: () => { | |
| setIsProcessing(false); | |
| const { displayText, svgContent, audioSummary } = processResponse(fullText); | |
| if (svgContent) { | |
| setLatestSvg(svgContent); | |
| setWhiteboardImage(null); | |
| setWhiteboardHistory(prev => { | |
| if (prev[prev.length - 1] === svgContent) return prev; | |
| const next = [...prev, svgContent].slice(-20); | |
| setHistoryIndex(next.length - 1); | |
| return next; | |
| }); | |
| } | |
| setMessages(prev => prev.map(m => m.id === aiMsgId ? { ...m, text: displayText, audioSummary, isLoading: false } : m)); | |
| }, | |
| onError: (err) => { | |
| setIsProcessing(false); | |
| setMessages(prev => prev.map(m => m.id === aiMsgId ? { ...m, text: fullText + `\n\n[Note: Connection issue.]`, isError: true, isLoading: false } : m)); | |
| } | |
| }); | |
| }, [chatSession, activePersona, processResponse, platformLevel, handlePersonaTransition, illustrationMode, whiteboardHistory]); | |
| const handleGenerateAIImage = async (prompt: string, size: '1K' | '2K' | '4K') => { | |
| setIsProcessing(true); | |
| try { | |
| const aiMsgId = Date.now().toString(); | |
| setMessages(prev => [...prev, { id: aiMsgId, role: Role.MODEL, text: `Creating illustration...`, timestamp: new Date(), isLoading: true }]); | |
| const imageUrl = await generateProImage(prompt, size); | |
| if (imageUrl) { | |
| setWhiteboardImage(imageUrl); setLatestSvg(''); | |
| setMessages(prev => prev.map(m => m.id === aiMsgId ? { ...m, text: `Illustration ready.`, image: imageUrl, isLoading: false } : m)); | |
| } | |
| } catch (err) { | |
| setMessages(prev => [...prev, { id: Date.now().toString(), role: Role.MODEL, text: "Generation failed.", timestamp: new Date(), isError: true }]); | |
| } finally { setIsProcessing(false); } | |
| }; | |
| const handleModeChange = (newMode: IllustrationMode) => { | |
| setIllustrationMode(newMode); | |
| if (chatSession) { | |
| let instruction = activePersona.systemInstruction; | |
| if (platformLevel === 'JUNIOR' && standardLevel) instruction += `\nCONTEXT: Standard ${standardLevel}.`; | |
| const nextChatSession = createChatSession(instruction, { illustrationMode: newMode }); | |
| setChatSession(nextChatSession); | |
| const modeLabels = { eco: 'Eco Mode (Vector)', standard: 'Standard Mode (Hybrid)', studio: 'Studio Mode (High-Res)' }; | |
| handleSendMessage(`[ACTION: MODE_CHANGE] I have switched to ${modeLabels[newMode]}. Please adapt your visual explanations.`, undefined, true, true); | |
| } | |
| }; | |
| const handleWhiteboardInteract = (detail: any) => { | |
| if (detail.id) { | |
| if (detail.id === 'btn-next') { | |
| playSound('click'); | |
| handleSendMessage("[ACTION: ADVANCE_LESSON] Proceed to next challenge on whiteboard.", undefined, true, true); | |
| return; | |
| } | |
| if (detail.id.startsWith('btn-reveal-')) { | |
| playSound('pop'); | |
| const topic = detail.id.replace('btn-reveal-', ''); | |
| handleSendMessage(`[ACTION: REVEAL] Explain "${topic}" on board then test me immediately.`, undefined, true, true); | |
| return; | |
| } | |
| if (detail.id.startsWith('btn-topic-') || detail.id.startsWith('btn-math') || detail.id.startsWith('btn-english') || detail.id.startsWith('btn-writing')) { | |
| playSound('swoosh'); | |
| const topic = detail.id.replace('btn-topic-', '').replace('btn-', '').replace('-', ' '); | |
| handleSendMessage(`[ACTION: INITIALIZE_VISUAL_MODULE] Topic: "${topic}". Begin lesson with a diagnostic check.`, undefined, true, true); | |
| return; | |
| } | |
| const idMap: Record<string, string> = { | |
| 'math-number-sense': 'Number Sense', 'math-fractions': 'Fractions', 'math-geometry': 'Geometry', | |
| 'eng-grammar': 'Grammar', 'eng-reading': 'Reading', 'eng-vocab': 'Vocabulary', | |
| 'write-narrative': 'Narrative', 'write-descriptive': 'Descriptive', 'write-report': 'Reports', | |
| 'junior-numbers': 'Numbers', 'junior-stories': 'Stories', 'junior-nature': 'Nature' | |
| }; | |
| const topic = idMap[detail.id] || detail.id; | |
| handleSendMessage(`[ACTION: INITIALIZE_VISUAL_MODULE] Topic: "${topic}". Begin interactive lesson.`, undefined, true, true); | |
| } | |
| }; | |
| if (!isAuthenticated) return <LandingPage onLogin={handleLogin} isDarkMode={isDarkMode} toggleTheme={() => setIsDarkMode(!isDarkMode)} />; | |
| return ( | |
| <div className={`font-display bg-${primaryUiColor}-50/30 dark:bg-slate-950 text-slate-800 dark:text-slate-200 min-h-screen flex overflow-hidden relative transition-colors duration-500`}> | |
| {showTour && <OnboardingTour themeColor={primaryUiColor} onComplete={() => { | |
| setShowTour(false); | |
| localStorage.setItem('prepair_tour_complete', 'true'); | |
| }} />} | |
| {replaySession && <SessionReplayView session={replaySession} onClose={() => setReplaySession(null)} onContinue={handleContinueSession} themeColor={primaryUiColor} />} | |
| <NeuralBackground colorTheme={globalTheme} isDarkMode={isDarkMode} /> | |
| <Sidebar | |
| activePersona={activePersona} currentView={currentView} onSelectPersona={handlePersonaTransition} | |
| onSelectView={handleViewTransition} onSelectTopic={(topic) => handleSendMessage(`[ACTION: INITIALIZE_VISUAL_MODULE] Topic: "${topic}".`, undefined, true, true)} | |
| isMobileOpen={isMobileSidebarOpen} closeMobile={() => setIsMobileSidebarOpen(false)} | |
| isCollapsed={isSidebarCollapsed} toggleCollapse={() => setIsSidebarCollapsed(!isSidebarCollapsed)} | |
| onLogout={handleLogout} isDarkMode={isDarkMode} toggleTheme={() => setIsDarkMode(!isDarkMode)} | |
| themeColor={primaryUiColor} | |
| /> | |
| <div className="flex-1 flex flex-col h-screen overflow-hidden gap-0 relative transition-all"> | |
| <div className="shrink-0 flex justify-center z-50 pt-4 px-4"> | |
| <Navbar | |
| isDarkMode={isDarkMode} onToggleTheme={() => setIsDarkMode(!isDarkMode)} onNavigate={handleViewTransition} | |
| onSearch={(q) => handleSendMessage(`[ACTION: INITIALIZE_VISUAL_MODULE] Topic: "${q}".`, undefined, true, true)} | |
| currentTheme={globalTheme} onThemeChange={setGlobalTheme} onLogout={handleLogout} | |
| onRestartTour={() => setShowTour(true)} | |
| /> | |
| </div> | |
| <div className={`flex-1 flex min-h-0 relative px-4 pb-4 transition-all duration-500 ${whiteboardMode === 'focus' && currentView === 'whiteboard' ? 'gap-0' : 'gap-4'}`}> | |
| <div data-tour="whiteboard" className={`relative flex-1 flex flex-col min-w-0 my-4 bg-white/40 dark:bg-slate-900/40 backdrop-blur-md border border-${primaryUiColor}-200/50 dark:border-${primaryUiColor}-500/30 rounded-[32px] shadow-2xl shadow-${primaryUiColor}-900/5 overflow-hidden transition-all duration-500`}> | |
| {currentView === 'dashboard' && <div className="overflow-y-auto h-full scrollbar-hide"><DashboardView onSelectPersona={(p) => { handleViewTransition('whiteboard'); handlePersonaTransition(p); }} onNavigate={handleViewTransition} onStartTopic={(p,t,c) => { handleViewTransition('whiteboard'); handleSendMessage(`[ACTION: START_MISSION] Topic: "${t}".`, undefined, true, true); }} onRevisitSession={setReplaySession} progress={72} analyticsData={analyticsData} historicalSessions={historicalSessions} themeColor={primaryUiColor} platformLevel={platformLevel} /></div>} | |
| {currentView === 'whiteboard' && ( | |
| <WhiteboardCanvas | |
| ref={whiteboardRef} svgContent={latestSvg} imageContent={whiteboardImage} browserUrl={browserUrl} | |
| browserTitle={browserTitle} functionPlots={functionPlots} | |
| onCloseBrowser={() => { setBrowserUrl(null); handleSendMessage("Resume interactive lesson.", undefined, true, true); }} | |
| onInteract={handleWhiteboardInteract} historyIndex={historyIndex} historyLength={whiteboardHistory.length} | |
| onNavigateHistory={handleNavigateHistory} isStreaming={isProcessing} themeColor={primaryUiColor} | |
| illustrationMode={illustrationMode} onModeChange={handleModeChange} | |
| /> | |
| )} | |
| {currentView === 'exam' && <div className="overflow-y-auto h-full scrollbar-hide"><ExamView onDeepDive={(ctx) => handleSendMessage(`Analyze and test me on this: ${ctx}.`, undefined, false, true)} themeColor={primaryUiColor} /></div>} | |
| {currentView === 'planner' && <div className="overflow-y-auto h-full scrollbar-hide"><PlannerView themeColor={primaryUiColor} /></div>} | |
| {currentView === 'flashcards' && <div className="overflow-y-auto h-full scrollbar-hide"><FlashcardView themeColor={primaryUiColor} /></div>} | |
| {currentView === 'past-papers' && <div className="overflow-y-auto h-full scrollbar-hide"><PastPaperGenerator themeColor={primaryUiColor} /></div>} | |
| {currentView === 'stats' && <div className="overflow-y-auto h-full scrollbar-hide"><StatsView stats={analyticsData} themeColor={primaryUiColor} /></div>} | |
| {currentView === 'spelling' && <div className="overflow-y-auto h-full scrollbar-hide"><SpellingVocabView themeColor={primaryUiColor} /></div>} | |
| </div> | |
| {currentView === 'whiteboard' && ( | |
| <div data-tour="chat" className={`shrink-0 h-full transition-all duration-500 ${whiteboardMode === 'focus' ? 'lg:w-0 lg:opacity-0 lg:pointer-events-none' : 'lg:w-[400px] lg:opacity-100'} hidden lg:block`}> | |
| <div className={`w-full h-[calc(100%-2rem)] my-4 flex flex-col bg-${primaryUiColor}-50/90 dark:bg-slate-900/90 backdrop-blur-2xl rounded-[32px] border border-${primaryUiColor}-200/50 dark:border-${primaryUiColor}-500/30 shadow-2xl shadow-${primaryUiColor}-900/10 overflow-hidden relative z-30 transition-colors duration-500`}> | |
| <div className={`flex items-center justify-between p-4 border-b border-${primaryUiColor}-100/50 dark:border-white/5 shrink-0 gap-2`}> | |
| <div className="flex items-center gap-3 overflow-hidden"> | |
| <div className={`w-8 h-8 rounded-xl flex items-center justify-center bg-${activePersona.color}-500/10 text-${activePersona.color}-600 dark:text-${activePersona.color}-400 shadow-sm border border-${activePersona.color}-500/20 shrink-0`}><Icon name={activePersona.icon} size={16} /></div> | |
| <div className="flex flex-col min-w-0"><span className="text-[9px] font-black uppercase tracking-widest text-slate-400 mb-1">{activePersona.name}</span><span className="text-sm font-bold text-slate-900 dark:text-white truncate">Active Tutor</span></div> | |
| </div> | |
| <div className="flex items-center gap-2 shrink-0"> | |
| <button onClick={handleToggleMute} className={`w-8 h-8 rounded-full flex items-center justify-center transition-colors border ${isMuted ? 'bg-red-50 dark:bg-red-900/20 border-red-200 text-red-500' : 'bg-white dark:bg-slate-800 border-slate-200 dark:border-white/10 text-slate-400'}`}><Icon name={isMuted ? "VolumeX" : "Volume2"} size={16} /></button> | |
| <button onClick={() => setWhiteboardMode('focus')} className="w-8 h-8 rounded-full flex items-center justify-center bg-white dark:bg-slate-800 border border-slate-200 dark:border-white/10 text-slate-400"><Icon name="Minimize2" size={16} /></button> | |
| </div> | |
| </div> | |
| <div ref={chatContainerRef} className="flex-1 overflow-y-auto p-4 space-y-4 scrollbar-hide"> | |
| {messages.map((msg, i) => <MessageBubble key={msg.id} message={msg} personaColor={activePersona.color} isLatest={i === messages.length - 1} themeColor={primaryUiColor} isMuted={isMuted} ttsMode={ttsMode} />)} | |
| <div ref={messagesEndRef} /> | |
| </div> | |
| <div className={`p-3 lg:p-4 shrink-0 border-t border-${primaryUiColor}-100/20 dark:border-white/5 bg-${primaryUiColor}-50/50 dark:bg-slate-900/50`}><InputArea onSend={handleSendMessage} disabled={isProcessing} themeColor={primaryUiColor} placeholder={`Answer ${activePersona.name}...`} compactMode={true} /></div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| {whiteboardMode === 'focus' && <button onClick={() => setWhiteboardMode('split')} className="fixed top-8 right-8 hidden lg:flex p-4 bg-white/90 dark:bg-slate-900/90 backdrop-blur-md border border-slate-200 dark:border-white/10 rounded-full shadow-2xl text-cyan-500 hover:scale-110 transition-transform active:scale-95 z-[100]"><Icon name="MessageSquare" size={24} /></button>} | |
| </div> | |
| ); | |
| } | |