SEAPREPAIR2026 / App.tsx
AptlyDigital's picture
Upload 10 files
09b069c verified
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>
);
}