"use client"; import { useCallback, useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { BookOpen, Brain, Moon, Target, X } from "lucide-react"; import { cn } from "@/lib/utils"; import { useAuth } from "@/lib/auth/useAuth"; import { hasLiveSession } from "@/lib/api/mode"; import { getPublicDemoMode } from "@/lib/env"; import { deleteDocument, listDocuments, reclassifyDocument } from "@/lib/api/documents"; import { getStudentDashboard } from "@/lib/api/dashboard"; import type { StudentDashboard } from "@/lib/api/dashboard"; import type { ApiDocument } from "@/lib/api/types"; import { WorkspaceTopBar } from "./WorkspaceTopBar"; import { SourcesPanel } from "./SourcesPanel"; import type { SourceDoc } from "./SourcesPanel"; import { AskDocDoeChat } from "./AskDocDoeChat"; import { RightPanel } from "./RightPanel"; import { ExamPathModule } from "./ExamPathModule"; import { isReadyStudySource } from "./SourceTrust"; import type { MaterialType } from "./workspaceTypes"; import { StudyVideoWizard } from "@/components/dashboard/StudyVideoWizard"; import { getOnboardingProfile, isOnboardingCompleted, type StudentStudyProfile } from "@/lib/onboarding"; const isLiveFrontendMode = getPublicDemoMode() === "live"; function mapToSourceDoc(doc: ApiDocument): SourceDoc { return { id: doc.id, title: doc.title, status: doc.status, subject: doc.subject ?? "", syllabus: doc.syllabus, file_type: doc.file_type ?? "", chunk_count: doc.chunk_count, material_type: (doc.material_type ?? "unknown") as MaterialType, source_type: doc.source_type ?? null, }; } function hasSyllabusTerm(syllabus: string | null | undefined, term: string | undefined) { if (!term) return true; if (!syllabus) return false; const syllabusTokens = new Set(syllabus.toLowerCase().split(/[\s,./-]+/).filter(Boolean)); return term .toLowerCase() .split(/[\s,./-]+/) .filter(Boolean) .every((token) => syllabusTokens.has(token)); } function matchesProfileContext(doc: SourceDoc, profile: StudentStudyProfile | null) { if (!profile || profile.onboardingSkipped) return true; return hasSyllabusTerm(doc.syllabus, profile.board) && hasSyllabusTerm(doc.syllabus, profile.classLevel); } export function StudentWorkspace() { const router = useRouter(); const { user, isLoading: isAuthLoading } = useAuth(); const [subject, setSubject] = useState("All subjects"); const [documents, setDocuments] = useState([]); const [activeDocId, setActiveDocId] = useState(null); const [docRefreshToken, setDocRefreshToken] = useState(0); const [docError, setDocError] = useState(null); const [isLoadingDocs, setIsLoadingDocs] = useState(false); const [prefillPrompt, setPrefillPrompt] = useState(undefined); const [focusMode, setFocusMode] = useState(false); const [dashboard, setDashboard] = useState(null); const [studyProfile, setStudyProfile] = useState(null); const [profileLoaded, setProfileLoaded] = useState(false); const [isStudyVideoWizardOpen, setIsStudyVideoWizardOpen] = useState(false); // Mobile drawer state const [mobilePanel, setMobilePanel] = useState<"sources" | "brain" | null>(null); // Auth guard useEffect(() => { if (!isAuthLoading && isLiveFrontendMode && !hasLiveSession()) { router.replace("/login?next=%2Fdashboard"); } }, [isAuthLoading, router]); useEffect(() => { const id = window.setTimeout(() => { const stored = isOnboardingCompleted() ? getOnboardingProfile() : null; setStudyProfile(stored); if (stored?.subjects?.[0]) setSubject(stored.subjects[0]); setProfileLoaded(true); }, 0); return () => window.clearTimeout(id); }, []); // Fetch documents useEffect(() => { const id = window.setTimeout(() => { setDocError(null); setIsLoadingDocs(true); listDocuments() .then((docs) => { const mapped = docs.map(mapToSourceDoc); setDocuments(mapped); setActiveDocId((prev) => { if (prev) return prev; const firstReady = mapped.find((d) => d.status === "ready"); return firstReady?.id ?? null; }); }) .catch(() => { setDocError("Your sources could not be loaded right now. Try again in a moment."); }) .finally(() => { setIsLoadingDocs(false); }); }, 0); return () => window.clearTimeout(id); }, [docRefreshToken]); // Fetch aggregated dashboard snapshot (graceful — failure leaves panel minimal) useEffect(() => { let cancelled = false; getStudentDashboard() .then((data) => { if (!cancelled) setDashboard(data); }) .catch(() => { if (!cancelled) setDashboard(null); }); return () => { cancelled = true; }; }, [docRefreshToken]); // Close mobile drawer on escape useEffect(() => { function onKeyDown(e: KeyboardEvent) { if (e.key === "Escape") setMobilePanel(null); } window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); }, []); const activeDoc = documents.find((d) => d.id === activeDocId) ?? null; const userName = user?.name ?? ""; const hasDocuments = documents.length > 0; const examActionsDisabled = !activeDoc || !isReadyStudySource(activeDoc); const pyqPaperCount = documents.filter( (doc) => doc.material_type === "question_paper" && (!activeDoc?.subject || doc.subject === activeDoc.subject) && (doc.id === activeDoc?.id || matchesProfileContext(doc, studyProfile)), ).length; const handleDocumentSelect = useCallback((doc: SourceDoc | null) => { setActiveDocId(doc?.id ?? null); if (doc?.subject?.trim()) { setSubject(doc.subject); } setMobilePanel(null); }, []); const handleRefreshDocuments = useCallback(() => { setDocRefreshToken((t) => t + 1); }, []); const handleReclassifySource = useCallback( async (materialType: MaterialType) => { if (!activeDoc) return; setDocError(null); try { await reclassifyDocument(activeDoc.id, materialType); handleRefreshDocuments(); } catch { setDocError("Could not update this source type. Try again."); } }, [activeDoc, handleRefreshDocuments], ); const handleRemoveSource = useCallback(async () => { if (!activeDoc) return; setDocError(null); try { await deleteDocument(activeDoc.id); setActiveDocId(null); handleRefreshDocuments(); } catch { setDocError("Could not remove this source. Try again."); } }, [activeDoc, handleRefreshDocuments]); const handleCardAction = useCallback( (prompt: string) => { if (activeDoc && !isReadyStudySource(activeDoc)) { setMobilePanel(null); return; } const resolved = prompt .replace(/\{source\}/g, activeDoc?.title ?? subject) .replace(/\{subject\}/g, activeDoc?.subject ?? subject); setPrefillPrompt(resolved); setMobilePanel(null); }, [activeDoc, subject], ); const handleReviewWeakArea = useCallback( (topic: string) => { setPrefillPrompt( `I'm weak in "${topic}". Explain it simply, create a few important practice questions, and give me a quick way to master it.`, ); setMobilePanel(null); }, [], ); const handleOpenStudyVideoWizard = useCallback(() => { setMobilePanel(null); setIsStudyVideoWizardOpen(true); }, []); const handleSearch = useCallback((query: string) => { setPrefillPrompt(query); }, []); return (
{/* Top bar */} setMobilePanel((p) => (p === "sources" ? null : "sources"))} onToggleStudyBrain={hasDocuments ? () => setMobilePanel((p) => (p === "brain" ? null : "brain")) : undefined} focusMode={focusMode} onToggleFocus={() => setFocusMode((v) => !v)} sourceTitle={activeDoc?.title} /> {/* Source load error */} {docError && (
{docError}
)} {profileLoaded ? ( router.push("/onboarding?edit=1")} onUpload={() => router.push("/upload")} onGenerate={() => handleCardAction( "Build my study path from the selected material. Rank repeated topics, weak areas, and what deserves my next hour.", ) } /> ) : null} {/* Main workspace */}
{/* Desktop Sources panel — hidden in focus mode */} {!focusMode && ( router.push("/upload")} /> )} {/* Center — Chat */} setPrefillPrompt(undefined)} dashboard={dashboard} showFirstUploadState={!isLoadingDocs && !hasDocuments} onOpenUpload={() => router.push("/upload")} onClassifySource={handleReclassifySource} onRemoveSource={handleRemoveSource} onCompleteProfile={() => router.push("/onboarding?edit=1")} onOpenStudyVideoWizard={handleOpenStudyVideoWizard} /> {/* Desktop Right panel — hidden in focus mode */} {!focusMode && hasDocuments && ( router.push("/upload")} pyqPaperCount={pyqPaperCount} onOpenStudyVideoWizard={handleOpenStudyVideoWizard} /> )} {/* ── Mobile Drawer Overlay ── */} {mobilePanel !== null && (
setMobilePanel(null)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") setMobilePanel(null); }} role="button" tabIndex={0} aria-label="Close panel" /> )} {/* ── Mobile Sources Drawer (left) ── */}
Sources {documents.length > 0 && ( {documents.length} )}
router.push("/upload")} />
{/* ── Mobile Study Brain Drawer (right) ── */}
Study actions
router.push("/upload")} pyqPaperCount={pyqPaperCount} onOpenStudyVideoWizard={handleOpenStudyVideoWizard} />
{/* Mobile bottom nav — hidden on desktop */}
{hasDocuments ? ( ) : null}
setIsStudyVideoWizardOpen(false)} onUploadClick={() => { setIsStudyVideoWizardOpen(false); router.push("/upload"); }} />
); }