'use client'; import { useState, useCallback, useEffect, useRef } from 'react'; import type { Topic, ProviderConfig, AuthUser, AppMode } from '@/types'; import { useAppState } from '@/hooks/useAppState'; import { useAI } from '@/hooks/useAI'; import { loadProviderConfig, saveProviderConfig, PROVIDER_MAP } from '@/lib/providers'; import { loadLocalSession, saveLocalSession, clearLocalSession } from '@/lib/localSession'; import { TOPICS } from '@/lib/topics'; import TopicSelector from './TopicSelector'; import EditorPane from './editor/EditorPane'; import LessonPane from './classroom/LessonPane'; import ChatPane from './chat/ChatPane'; import ProviderSettings from './ProviderSettings'; import AuthModal from './AuthModal'; export default function AppShell() { const [state, dispatch] = useAppState(); const [providerConfig, setProviderConfig] = useState(loadProviderConfig); const [showSettings, setShowSettings] = useState(false); const [authUser, setAuthUser] = useState(null); const [showAuth, setShowAuth] = useState(false); const [savedIndicator, setSavedIndicator] = useState(false); const [splitPercent, setSplitPercent] = useState(50); const containerRef = useRef(null); const dbSaveRef = useRef | null>(null); const savedTimerRef = useRef | null>(null); // Keep localStorage in sync whenever providerConfig changes useEffect(() => { saveProviderConfig(providerConfig); }, [providerConfig]); // Load persisted responseMode on mount useEffect(() => { if (typeof window === 'undefined') return; try { const raw = localStorage.getItem('professor_response_mode'); if (raw) dispatch({ type: 'SET_RESPONSE_MODE', payload: JSON.parse(raw) }); } catch { /* ignore */ } }, []); // eslint-disable-line react-hooks/exhaustive-deps // Persist responseMode whenever it changes useEffect(() => { if (typeof window === 'undefined') return; localStorage.setItem('professor_response_mode', JSON.stringify(state.responseMode)); }, [state.responseMode]); // Check auth status on mount useEffect(() => { fetch('/api/auth/me') .then((r) => r.json()) .then((data) => { if (data.user) setAuthUser(data.user as AuthUser); }) .catch(() => {}); }, []); // Restore session from localStorage on mount (immediate, works without an account) useEffect(() => { const local = loadLocalSession(); if (!local) return; const topic = TOPICS.find((t) => t.id === local.topicId); if (topic && local.task) { dispatch({ type: 'RESTORE_SESSION', payload: { topic, task: local.task, code: local.code, messages: local.messages, executionResult: local.executionResult, }, }); } }, []); // eslint-disable-line react-hooks/exhaustive-deps // When user logs in, fetch their DB session and prefer it over the local one useEffect(() => { if (!authUser) return; fetch('/api/session') .then((r) => r.json()) .then((data) => { if (!data.session) return; const { topicId, task, code, messages, executionResult } = data.session; const topic = TOPICS.find((t) => t.id === topicId); if (topic && task) { dispatch({ type: 'RESTORE_SESSION', payload: { topic, task, code: code ?? '', messages: messages ?? [], executionResult }, }); // Sync DB session down to local so they're consistent saveLocalSession({ topicId, task, code: code ?? '', messages: messages ?? [], executionResult }); } }) .catch(() => {}); }, [authUser, dispatch]); // Auto-save session whenever relevant state changes useEffect(() => { if (state.phase === 'selecting' || !state.topic || !state.task) return; const sessionData = { topicId: state.topic.id, task: state.task, code: state.code, messages: state.messages, executionResult: state.executionResult, }; // Always save to localStorage immediately saveLocalSession(sessionData); // Flash "Saved" indicator if (savedTimerRef.current) clearTimeout(savedTimerRef.current); setSavedIndicator(true); savedTimerRef.current = setTimeout(() => setSavedIndicator(false), 1500); // Debounced DB sync when logged in if (authUser) { if (dbSaveRef.current) clearTimeout(dbSaveRef.current); dbSaveRef.current = setTimeout(() => { fetch('/api/session', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(sessionData), }).catch(() => {}); }, 1000); } }, [state.topic, state.task, state.code, state.messages, state.executionResult, authUser]); const { generateTask, reviewCode, sendMessage, generateLesson, sendClassroomMessage } = useAI(dispatch, providerConfig); const handleTopicSelect = useCallback( (topic: Topic) => { clearLocalSession(); dispatch({ type: 'SELECT_TOPIC', payload: topic }); generateTask(topic); }, [dispatch, generateTask] ); const handleSubmit = useCallback(() => { reviewCode(state); }, [reviewCode, state]); const handleSendMessage = useCallback( (text: string) => { sendMessage(state, text); }, [sendMessage, state] ); const handleSendClassroomMessage = useCallback( (text: string) => { sendClassroomMessage(state, text); }, [sendClassroomMessage, state] ); const handleSetAppMode = useCallback( (mode: AppMode) => { dispatch({ type: 'SET_APP_MODE', payload: mode }); // Lazily generate the lesson the first time the user enters Classroom mode if (mode === 'classroom' && !state.lessonContent && !state.isStreaming && state.topic) { generateLesson(state.topic); } }, [dispatch, state.lessonContent, state.isStreaming, state.topic, generateLesson] ); const handleReset = useCallback(() => { dispatch({ type: 'RESET' }); clearLocalSession(); // Clear DB session if logged in if (authUser) { fetch('/api/session', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ topicId: null, task: null, code: null, messages: [], executionResult: null }), }).catch(() => {}); } }, [dispatch, authUser]); const handleSaveProvider = useCallback((config: ProviderConfig) => { setProviderConfig(config); }, []); const handleAuthSuccess = useCallback((user: AuthUser) => { setAuthUser(user); setShowAuth(false); }, []); const handleLogout = useCallback(async () => { await fetch('/api/auth/logout', { method: 'POST' }); setAuthUser(null); // Local session stays intact โ€” user can continue without an account }, []); const handlePaneDragStart = useCallback((e: React.MouseEvent) => { e.preventDefault(); const container = containerRef.current; if (!container) return; const onMove = (e: MouseEvent) => { const rect = container.getBoundingClientRect(); const isHorizontal = rect.width >= 1024; // lg breakpoint const pct = isHorizontal ? ((e.clientX - rect.left) / rect.width) * 100 : ((e.clientY - rect.top) / rect.height) * 100; setSplitPercent(Math.min(Math.max(pct, 20), 80)); }; const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); }; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); }, []); const providerDef = PROVIDER_MAP[providerConfig.provider]; if (state.phase === 'selecting') { return ( <> setShowSettings(true)} /> {showSettings && ( setShowSettings(false)} /> )} {showAuth && ( setShowAuth(false)} /> )} ); } return ( <>
{/* Left pane โ€” editor (Homework) or lesson viewer (Classroom) */}
{state.appMode === 'classroom' ? ( ) : ( setShowSettings(true)} savedIndicator={savedIndicator} authUser={authUser} onShowAuth={() => setShowAuth(true)} onLogout={handleLogout} /> )}
{/* Drag handle */}
{/* Right pane โ€” chat */}
{showSettings && ( setShowSettings(false)} /> )} {showAuth && ( setShowAuth(false)} /> )} ); }