"use client"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetFooter } from "@/components/animate-ui/primitives/radix/sheet"; import { LiquidButton } from "@/components/animate-ui/primitives/buttons/liquid-button"; import { ArrowLeft, BookOpen, FileImage, FileText, LoaderCircle, Search, Upload, } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion } from "framer-motion"; import { useEffect, useMemo, useRef, useState, type DragEvent, type ReactNode, type RefObject } from "react"; import { DocDoeLogoMark } from "@/components/landing/DocDoeLogoMark"; import { uploadDocument } from "@/lib/api/documents"; import { saveStudyProfile } from "@/lib/api/brain"; import { normalizeApiError } from "@/lib/api/client"; import { hasLiveSession } from "@/lib/api/mode"; import { completeOnboarding, emptyStudyProfile, getOnboardingProfile, isOnboardingCompleted, labelForOnboardingGoal, saveOnboardingProfile, saveStudyProfileId, skipOnboarding, type BoardChoice, type ClassLevelChoice, type FirstUploadStatus, type OnboardingGoalChoice, type PrimaryNeedChoice, type StreamChoice, type StudentStudyProfile, } from "@/lib/onboarding"; import { cn } from "@/lib/utils"; import { MATERIAL_UPLOAD_ACCEPT, validateStudentUpload, } from "@/lib/uploads/document-upload-contract"; type SetupScreen = "welcome" | "context" | "subjects" | "exam-mode" | "material" | "ready"; type MaterialKind = "notes" | "chapter" | "question-paper"; const progression: Array<{ key: Exclude; label: string }> = [ { key: "context", label: "Context" }, { key: "subjects", label: "Subjects" }, { key: "exam-mode", label: "Exam mode" }, { key: "material", label: "Material" }, { key: "ready", label: "Ready" }, ]; const boardOptions: BoardChoice[] = ["Kerala HSE", "Kerala SSLC", "CBSE", "ICSE", "Other State Board", "Other"]; const classOptions: ClassLevelChoice[] = ["SSLC / Class 10", "+1 / Class 11", "+2 / Class 12", "College", "Other"]; const streamOptions: StreamChoice[] = ["General", "Science", "Commerce", "Humanities", "Computer Science", "Other"]; const subjectOptions = [ "Physics", "Chemistry", "Biology", "Mathematics", "Computer Science", "English", "Malayalam", "Accountancy", "Business Studies", "Economics", ] as const; const subjectCombinations: Array<{ label: string; subjects: readonly string[] }> = [ { label: "Science Stream", subjects: ["Physics", "Chemistry", "Mathematics", "Computer Science"] }, { label: "Commerce Stream", subjects: ["Accountancy", "Business Studies", "Economics", "Mathematics"] }, { label: "Humanities Stream", subjects: ["English", "Malayalam", "Economics", "Computer Science"] }, ]; const examGoals: Array<{ id: OnboardingGoalChoice; title: string; text: string }> = [ { id: "last_night_prep", title: "Last-night prep", text: "Fastest path when the exam is close." }, { id: "pyq_intelligence", title: "PYQ intelligence", text: "Repeated topics, high-mark chapters, and scoring patterns.", }, { id: "understand_chapters", title: "Understand chapters", text: "Hard concepts explained without extra noise." }, { id: "practice_mode", title: "Practice mode", text: "Quizzes, flashcards, and revision checks." }, { id: "full_study_path", title: "Full study path", text: "A clear order for what to study next." }, ]; const materialKinds: Array<{ id: MaterialKind; title: string; description: string; icon: typeof FileText }> = [ { id: "notes", title: "Notes", description: "PDF or plain-text export", icon: FileImage }, { id: "chapter", title: "Chapter PDF", description: "A chapter or portion", icon: BookOpen }, { id: "question-paper", title: "Question paper", description: "Past paper or PYQ", icon: FileText }, ]; const primaryNeedForGoal: Record = { last_night_prep: "last_night_plan", pyq_intelligence: "pyq_intelligence", understand_chapters: "scoring_notes", practice_mode: "quiz_me", full_study_path: "what_to_study", }; const screenIndex: Record = { welcome: 0, context: 0, subjects: 1, "exam-mode": 2, material: 3, ready: 4, }; const ease = [0.22, 1, 0.36, 1] as const; function createProfile(): StudentStudyProfile { const now = new Date().toISOString(); return { ...emptyStudyProfile, createdAt: now, updatedAt: now }; } function profilePayload(profile: StudentStudyProfile) { return { exam: profile.board || null, board: profile.board || null, grade: profile.classLevel || profile.grade || null, subject: profile.subjects?.[0] || profile.subject || null, chapter: null, topic: null, goal: labelForOnboardingGoal(profile.primaryGoal), time_left: null, level: "intermediate" as const, language_preference: "English", primary_need: profile.primaryNeed, weak_areas: [], onboarding_completed: Boolean(profile.onboardingCompleted), extra: { onboardingSkipped: Boolean(profile.onboardingSkipped), stream: profile.stream || null, subjects: profile.subjects ?? [], primaryGoal: profile.primaryGoal || null, }, }; } function documentStatus(status: string): FirstUploadStatus { if (status === "ready") return "ready"; return status === "processing" || status === "uploaded" ? "processing" : "received"; } export function StudySetupFlow({ className }: { className?: string }) { const router = useRouter(); const reduceMotion = Boolean(useReducedMotion()); const [mounted, setMounted] = useState(false); const [screen, setScreen] = useState("welcome"); const [profile, setProfile] = useState(() => createProfile()); const [customSubject, setCustomSubject] = useState(""); const [selectedMaterialKind, setSelectedMaterialKind] = useState("notes"); const [selectedFile, setSelectedFile] = useState(null); const [isDragging, setIsDragging] = useState(false); const [isUploading, setIsUploading] = useState(false); const [uploadNotice, setUploadNotice] = useState(null); const [showSkipSheet, setShowSkipSheet] = useState(false); const fileInputRef = useRef(null); // Calculate completed steps for progress bar const completedSteps = useMemo(() => { let count = 0; if (profile.board) count++; if (profile.subjects?.length) count++; if (profile.primaryGoal) count++; if (profile.firstUploadDocumentId) count++; return count; }, [profile]); useEffect(() => { const id = window.setTimeout(() => { const stored = getOnboardingProfile(); const editing = new URLSearchParams(window.location.search).get("edit") === "1"; if (isOnboardingCompleted() && !editing) { router.replace("/home"); return; } if (stored) setProfile({ ...emptyStudyProfile, ...stored }); if (editing) setScreen("context"); setMounted(true); }, 0); return () => window.clearTimeout(id); }, [router]); // Keyboard navigation useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") { setShowSkipSheet(true); return; } if (event.key === "Enter" && event.ctrlKey) { event.preventDefault(); // Navigate to next screen based on current screen const nextMap: Partial> = { welcome: "context", context: "subjects", subjects: "exam-mode", "exam-mode": "material", material: "ready", }; const next = nextMap[screen]; if (next) goNext(next); } if (event.key === "Backspace" && !event.ctrlKey && !event.metaKey) { const target = event.target as HTMLElement; if (target.tagName !== "INPUT" && target.tagName !== "TEXTAREA") { event.preventDefault(); const backMap: Partial> = { context: "welcome", subjects: "context", "exam-mode": "subjects", material: "exam-mode", ready: "material", }; const back = backMap[screen]; if (back) goNext(back); } } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [screen]); function updateProfile(patch: Partial) { setProfile((current) => { const updated = saveOnboardingProfile({ ...current, ...patch }); return updated; }); } function goNext(next: SetupScreen) { setScreen(next); } function persistCompletedProfile(nextProfile: StudentStudyProfile) { const completed = completeOnboarding(nextProfile) ?? nextProfile; setProfile(completed); if (!hasLiveSession()) return; void saveStudyProfile(profilePayload(completed)) .then((saved) => saveStudyProfileId(saved.id)) .catch(() => { // Local profile remains usable when the API is unavailable during beta. }); } async function handleSkipSetup() { const skippedItems = []; if (!profile.board) skippedItems.push("Personalized exam signals"); if (!profile.subjects?.length) skippedItems.push("Subject-specific recommendations"); if (!profile.primaryGoal) skippedItems.push("Tailored study modes"); if (!profile.firstUploadDocumentId) skippedItems.push("First study material analysis"); if (skippedItems.length > 2) { setShowSkipSheet(true); return; } await completeSkip(); } async function completeSkip() { const skipped = skipOnboarding(profile); if (hasLiveSession()) { try { const saved = await saveStudyProfile(profilePayload(skipped)); saveStudyProfileId(saved.id); } catch { // Skipping must never trap a student behind network availability. } } router.push("/home"); } function selectSubject(subject: string) { const current = profile.subjects ?? []; const subjects = current.includes(subject) ? current.filter((item) => item !== subject) : [...current, subject]; updateProfile({ subjects, subject: subjects[0] ?? "" }); } function addCustomSubject() { const subject = customSubject.trim(); if (!subject) return; const current = profile.subjects ?? []; if (!current.includes(subject)) updateProfile({ subjects: [...current, subject], subject: current[0] ?? subject }); setCustomSubject(""); } function toggleGoal(goal: OnboardingGoalChoice) { const current = profile.selectedGoals ?? []; if (current.includes(goal)) { const selectedGoals = current.filter((item) => item !== goal); const primaryGoal = profile.primaryGoal === goal ? selectedGoals[0] ?? "" : profile.primaryGoal ?? ""; updateProfile({ selectedGoals, primaryGoal, primaryNeed: primaryGoal ? primaryNeedForGoal[primaryGoal] : "what_to_study", }); return; } const selectedGoals = [...current, goal]; const primaryGoal = profile.primaryGoal || goal; updateProfile({ selectedGoals, primaryGoal, primaryNeed: primaryNeedForGoal[primaryGoal] }); } function makePrimaryGoal(goal: OnboardingGoalChoice) { updateProfile({ primaryGoal: goal, primaryNeed: primaryNeedForGoal[goal] }); } function handleFile(file: File | undefined) { if (!file) return; const validation = validateStudentUpload(file, "material"); if (!validation.ok) { setUploadNotice(validation.message); return; } setSelectedFile(file); setUploadNotice(null); } function handleDrop(event: DragEvent) { event.preventDefault(); setIsDragging(false); handleFile(event.dataTransfer.files[0]); } function finishWithoutUpload() { const nextProfile = saveOnboardingProfile({ ...profile, sourceMode: "skip", firstUploadStatus: "skipped", }); persistCompletedProfile(nextProfile); setScreen("ready"); } async function uploadFirstMaterial() { if (!selectedFile) return; setIsUploading(true); setUploadNotice(null); try { // Simulate progress for better UX setUploadNotice("Reading your material..."); await new Promise(resolve => setTimeout(resolve, 500)); setUploadNotice("Finding study text..."); const document = await uploadDocument({ file: selectedFile, title: selectedFile.name.replace(/\.[^/.]+$/, ""), subject: profile.subjects?.[0] || undefined, syllabus: profile.board || undefined, }); setUploadNotice("Preparing study signals..."); await new Promise(resolve => setTimeout(resolve, 300)); const nextProfile = saveOnboardingProfile({ ...profile, sourceMode: "upload", sourceId: document.id, sourceTitle: document.title, firstUploadDocumentId: document.id, firstUploadStatus: documentStatus(document.status), }); setUploadNotice("Material received. DocDoe will use it inside your study space."); persistCompletedProfile(nextProfile); setScreen("ready"); } catch (error) { const errorMessage = normalizeApiError(error) || "Could not upload this material right now."; setUploadNotice(`${errorMessage} You can retry, skip, or try a different file.`); } finally { setIsUploading(false); } } const stage = screenIndex[screen]; const subjects = profile.subjects ?? []; const selectedGoals = profile.selectedGoals ?? []; if (!mounted) { return (
); } return (
DocDoe AI
{screen === "welcome" ? ( goNext("context")} onSkip={() => void handleSkipSetup()} /> ) : null} {screen === "context" ? ( goNext("subjects")} onBack={() => goNext("welcome")} /> ) : null} {screen === "subjects" ? ( { const current = profile.subjects ?? []; const newSubjects = [...new Set([...current, ...combinationSubjects])]; updateProfile({ subjects: newSubjects, subject: newSubjects[0] ?? "" }); }} onContinue={() => goNext("exam-mode")} onBack={() => goNext("context")} /> ) : null} {screen === "exam-mode" ? ( goNext("material")} onBack={() => goNext("subjects")} /> ) : null} {screen === "material" ? ( setSelectedFile(null)} onDragEnter={() => setIsDragging(true)} onDragLeave={() => setIsDragging(false)} onDrop={handleDrop} onUpload={() => void uploadFirstMaterial()} onSkip={finishWithoutUpload} onBack={() => goNext("exam-mode")} /> ) : null} {screen === "ready" ? ( router.push("/home")} onUploadAnother={() => router.push("/upload")} /> ) : null}
Skip setup? You can always set this up later, but DocDoe works best with your exam context. setShowSkipSheet(false)}> Continue setup { setShowSkipSheet(false); completeSkip(); }}> Skip anyway
); } function StepHeading({ title, body, eyebrow }: { title: string; body: string; eyebrow?: string }) { return (
{eyebrow ? (

{eyebrow}

) : (

Build your exam path

)}

{title}

{body}

); } function WelcomeScreen({ onStart, onSkip }: { onStart: () => void; onSkip: () => void }) { return (
{["Material", "Signals", "Study path"].map((label, index) => (
{label} {index < 2 ?
))}
Start setup Skip for now
); } function ContextScreen({ profile, onChange, onContinue, onBack, }: { profile: StudentStudyProfile; onChange: (patch: Partial) => void; onContinue: () => void; onBack: () => void; }) { const valid = Boolean(profile.board && profile.classLevel && profile.stream); return (
onChange({ board, exam: board })} recommendedValue="Kerala HSE" helper="Your exam board determines the syllabus and question patterns." /> onChange({ classLevel, grade: classLevel })} helper="Your current academic level helps tailor difficulty appropriately." /> onChange({ stream })} helper="Your subject stream determines available electives and focus areas." />
{!valid && (

Choose your board, class, and stream to continue.

)}
); } function SubjectsScreen({ subjects, customSubject, onSubjectChange, onCustomChange, onCustomAdd, onCombinationAdd, onContinue, onBack, }: { subjects: string[]; customSubject: string; onSubjectChange: (subject: string) => void; onCustomChange: (value: string) => void; onCustomAdd: () => void; onCombinationAdd: (combinationSubjects: readonly string[]) => void; onContinue: () => void; onBack: () => void; }) { const hasSubjects = subjects.length > 0; const [search, setSearch] = useState(""); const filteredSubjects = subjectOptions.filter((subject) => subject.toLowerCase().includes(search.trim().toLowerCase()), ); function addCombination(combinationSubjects: readonly string[]) { onCombinationAdd(combinationSubjects); } return (
{subjectCombinations.map((combo) => ( ))}
{filteredSubjects.map((subject) => ( ))} {filteredSubjects.length === 0 ? (

No match. Add your subject below.

) : null}
{!hasSubjects && (

Choose at least one subject to continue.

)}
onCustomChange(event.target.value)} onKeyDown={(event) => { if (event.key === "Enter") { event.preventDefault(); onCustomAdd(); } }} placeholder="Add another subject" className="h-12 min-w-0 flex-1 rounded-xl border border-white/[0.1] bg-[#080808] px-4 text-[13px] text-white outline-none placeholder:text-white/30 transition-all duration-200 ease-out focus:border-white/40 focus:shadow-[0_0_0_3px_rgba(255,255,255,0.12)] focus:bg-white/[0.02]" />
); } function ExamModeScreen({ selected, primary, onToggle, onPrimary, onContinue, onBack, }: { selected: OnboardingGoalChoice[]; primary: OnboardingGoalChoice | ""; onToggle: (goal: OnboardingGoalChoice) => void; onPrimary: (goal: OnboardingGoalChoice) => void; onContinue: () => void; onBack: () => void; }) { return (
{examGoals.map((goal) => { const checked = selected.includes(goal.id); const isPrimary = primary === goal.id; return (
) : null}
); })}
); } function MaterialScreen({ materialKind, file, inputRef, dragging, uploading, notice, onKindSelect, onFile, onFileRemove, onDragEnter, onDragLeave, onDrop, onUpload, onSkip, onBack, }: { materialKind: MaterialKind; file: File | null; inputRef: RefObject; dragging: boolean; uploading: boolean; notice: string | null; onKindSelect: (kind: MaterialKind) => void; onFile: (file: File | undefined) => void; onFileRemove: () => void; onDragEnter: () => void; onDragLeave: () => void; onDrop: (event: DragEvent) => void; onUpload: () => void; onSkip: () => void; onBack: () => void; }) { return (
{materialKinds.map((option) => { const Icon = option.icon; const active = option.id === materialKind; return ( ); })}
{ onFile(event.target.files?.[0]); event.target.value = ""; }} />
event.preventDefault()} onDragEnter={onDragEnter} onDragLeave={onDragLeave} onDrop={onDrop} className={cn( "mt-4 rounded-[20px] border border-dashed px-5 py-7 transition", dragging ? "border-white/28 bg-white/[0.045]" : "border-white/[0.12] bg-white/[0.018]", )} > {file ? (

{file.name}

{(file.size / 1024 / 1024).toFixed(2)} MB · Selectable-text PDF or TXT

) : (
)}
{notice ?

{notice}

: null}
Back {file ? ( {uploading ? "Receiving material..." : "Upload and continue"} ) : null}
); } function ReadyScreen({ profile, onDashboard, onUploadAnother, }: { profile: StudentStudyProfile; onDashboard: () => void; onUploadAnother: () => void; }) { const summaries = [ ["Board", profile.board || "Not selected"], ["Class", profile.classLevel || "Not selected"], ["Stream", profile.stream || "Not selected"], ["Subjects", profile.subjects?.join(", ") || "Add later"], ["Main focus", labelForOnboardingGoal(profile.primaryGoal)], [ "First material", profile.firstUploadStatus === "ready" || profile.firstUploadStatus === "received" ? "Material received" : profile.firstUploadStatus === "processing" ? "Scanning pending" : "Upload from Sources", ], ]; const hasMaterial = profile.firstUploadStatus === "ready" || profile.firstUploadStatus === "received"; return (
{summaries.map(([label, value]) => (
{label} {value}
))}
Open DocDoe {hasMaterial ? "Upload another material" : "Upload material"}
); } function SignalPreview({ profile, screen }: { profile: StudentStudyProfile; screen: SetupScreen }) { const contextValue = profile.board ? `${profile.board} • ${profile.classLevel || "Class pending"}` : "Not set"; const subjectLabel = profile.subjects?.slice(0, 2).join(", ") || "Awaiting subjects"; const examModeValue = labelForOnboardingGoal(profile.primaryGoal); const materialValue = profile.firstUploadDocumentId ? "Material received" : screen === "material" ? "Ready for upload" : "Pending"; const steps = [ { label: "Context set", done: Boolean(profile.board) }, { label: "Subjects chosen", done: Boolean(profile.subjects?.length) }, { label: "Focus chosen", done: Boolean(profile.primaryGoal) }, { label: "Material ready", done: Boolean(profile.firstUploadDocumentId) }, ]; const completedSteps = steps.filter((s) => s.done).length; const progressLabel = completedSteps === 4 ? "Signal found" : "Ready for signal"; return ( ); } function OptionGroup({ title, values, selected, onSelect, recommendedValue, helper, }: { title: string; values: readonly T[]; selected: string; onSelect: (value: T) => void; recommendedValue?: T; helper?: string; }) { return (
{title}
{values.map((value) => { const isRecommended = value === recommendedValue; return ( ); })}
{helper && (

{helper}

)}
); } function StepActions({ onBack, onNext, disabled = false }: { onBack: () => void; onNext: () => void; disabled?: boolean }) { return (
Back Continue
); } function SecondaryAction({ children, onClick, icon, }: { children: ReactNode; onClick: () => void; icon?: "back"; }) { return ( ); }