DocDoeAI / src /components /study-chat /StudyGenerator.tsx
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
65d68f5
Raw History Blame Contribute Delete
32.8 kB
"use client";
import { useCallback, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import {
ArrowLeft, ArrowRight, BookOpen, Check, ChevronDown,
ClipboardList, FileQuestion, FileText, GraduationCap,
Layers3, LoaderCircle, Moon, RefreshCw, Sparkles, Upload, Video, X,
} from "lucide-react";
import { cn } from "@/lib/utils";
import {
generateBrainNotes, generateBrainQuiz, generateBrainFlashcards,
generateBrainVideoPlan, generateBrainPyqAnalysis,
} from "@/lib/api/brain";
import { uploadDocument } from "@/lib/api/documents";
import type { ApiDocument, BrainGeneratePayload, BrainVideoPlanPayload, BrainVideoPlanResponse } from "@/lib/api/types";
import type { NotesOutput, QuizQuestionResponse, FlashcardResponseItem } from "@/lib/api/types";
import type { BrainPyqAnalysisOutput } from "@/lib/api/types";
import type { VideoMode } from "@/lib/api/types";
import { MATERIAL_UPLOAD_ACCEPT } from "@/lib/uploads/document-upload-contract";
import { validateStudyFile } from "./upload-guards";
type GeneratorStep = "upload" | "context" | "generating" | "results";
type StudyKit = {
notes: NotesOutput | null;
quiz: { questions: QuizQuestionResponse[] } | null;
flashcards: { cards: FlashcardResponseItem[] } | null;
videoPlan: BrainVideoPlanResponse | null;
pyqAnalysis: BrainPyqAnalysisOutput | null;
generatedAt: string;
};
type ContextFields = {
classLevel: string;
board: string;
subject: string;
chapter: string;
materialType: string;
goal: string;
availableTime: string;
outputs: string[];
};
const OUTPUT_OPTIONS = [
{ id: "notes", label: "Smart Notes", icon: ClipboardList },
{ id: "quiz", label: "Practice Quiz", icon: FileQuestion },
{ id: "flashcards", label: "Flashcards", icon: Layers3 },
{ id: "video", label: "Study Video", icon: Video },
{ id: "pyq", label: "PYQ Analysis", icon: GraduationCap },
] as const;
const CLASS_OPTIONS = ["Plus One", "Plus Two", "Class 10", "Class 9"];
const BOARD_OPTIONS = ["Kerala HSE", "CBSE", "ICSE", "Other"];
const SUBJECT_OPTIONS = ["Physics", "Chemistry", "Mathematics", "Biology", "Computer Science", "English"];
const GOAL_OPTIONS = [
{ value: "exam", label: "Board exam prep" },
{ value: "revision", label: "Quick revision" },
{ value: "doubt", label: "Doubt solving" },
{ value: "video-only", label: "Watch study video" },
{ value: "deep-learn", label: "Deep learning" },
];
const TIME_OPTIONS = [
{ value: "30min", label: "30 minutes" },
{ value: "1h", label: "1 hour" },
{ value: "2h", label: "2 hours" },
{ value: "5h", label: "5 hours" },
{ value: "tonight", label: "All night" },
{ value: "tomorrow", label: "Tomorrow" },
{ value: "week", label: "A week+" },
];
function buildPayload(topic: string, subject: string, sourceIds: string[], extra?: Record<string, unknown>): BrainGeneratePayload {
return {
topic,
source_id: sourceIds[0] ?? null,
source_ids: sourceIds,
subject,
study_profile_id: null,
level: null,
language_preference: null,
...extra,
} as BrainGeneratePayload;
}
function estimateStudyTime(context: ContextFields): string {
const t = context.availableTime;
if (t === "30min") return "30 minutes";
if (t === "1h") return "1 hour";
if (t === "2h") return "2 hours";
if (t === "5h") return "5 hours";
if (t === "tonight") return "3-4 hours";
if (t === "tomorrow") return "5-6 hours";
if (t === "week") return "8-10 hours across the week";
return "1-2 hours";
}
function sourceTitle(doc: ApiDocument): string {
return doc.title || doc.file_name || "Untitled source";
}
export function StudyGenerator() {
const router = useRouter();
const [step, setStep] = useState<GeneratorStep>("upload");
const [uploadedDocs, setUploadedDocs] = useState<ApiDocument[]>([]);
const [isUploading, setIsUploading] = useState(false);
const [uploadError, setUploadError] = useState<string | null>(null);
const [dragOver, setDragOver] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const [context, setContext] = useState<ContextFields>({
classLevel: "", board: "", subject: "", chapter: "",
materialType: "", goal: "", availableTime: "", outputs: [],
});
const [questionStep, setQuestionStep] = useState(0);
const [kit, setKit] = useState<StudyKit | null>(null);
const [genProgress, setGenProgress] = useState("");
const [genError, setGenError] = useState<string | null>(null);
const [expandedSection, setExpandedSection] = useState<string | null>(null);
const missingQuestions: Array<{
key: keyof ContextFields;
label: string;
options?: readonly string[] | readonly { value: string; label: string }[];
type?: "select" | "multiselect";
}> = [];
if (!context.classLevel) missingQuestions.push({ key: "classLevel", label: "Which class?", options: CLASS_OPTIONS });
if (!context.board) missingQuestions.push({ key: "board", label: "Which syllabus/board?", options: BOARD_OPTIONS });
if (!context.subject) missingQuestions.push({ key: "subject", label: "Which subject?", options: SUBJECT_OPTIONS });
if (!context.chapter) missingQuestions.push({ key: "chapter", label: "Which chapter or topic?" });
if (!context.goal) missingQuestions.push({ key: "goal", label: "What's your goal?", options: GOAL_OPTIONS as unknown as readonly { value: string; label: string }[] });
if (!context.availableTime) missingQuestions.push({ key: "availableTime", label: "How much time do you have?", options: TIME_OPTIONS as unknown as readonly { value: string; label: string }[] });
if (context.outputs.length === 0) missingQuestions.push({ key: "outputs", label: "What output do you want?", options: OUTPUT_OPTIONS.map(o => ({ value: o.id, label: o.label })), type: "multiselect" });
const currentQuestion = questionStep < missingQuestions.length ? missingQuestions[questionStep] : null;
const allContextCollected = missingQuestions.length === 0;
const handleFile = useCallback(async (file: File) => {
const validation = validateStudyFile(file, "material");
if (!validation.ok) { setUploadError(validation.message); return; }
setIsUploading(true);
setUploadError(null);
try {
const doc = await uploadDocument({ file, title: file.name.replace(/\.[^/.]+$/, "") });
setUploadedDocs(prev => [...prev, doc]);
} catch { setUploadError("Upload failed. Try again."); }
finally { setIsUploading(false); }
}, []);
const handleDrop = useCallback((e: React.DragEvent) => {
e.preventDefault();
setDragOver(false);
const file = e.dataTransfer.files?.[0];
if (file) handleFile(file);
}, [handleFile]);
const handleContextSelect = (key: keyof ContextFields, value: string | string[]) => {
setContext(prev => ({ ...prev, [key]: value }));
setQuestionStep(s => s + 1);
};
const handleOutputToggle = useCallback((id: string) => {
setContext(prev => ({
...prev,
outputs: prev.outputs.includes(id) ? prev.outputs.filter(o => o !== id) : [...prev.outputs, id],
}));
}, []);
const handleGenerate = useCallback(async () => {
setGenError(null);
setStep("generating");
const topic = context.chapter || uploadedDocs[0]?.chapter || "this topic";
const subject = context.subject || uploadedDocs[0]?.subject || "General";
const sourceIds = uploadedDocs.map(d => d.id);
const results: StudyKit = { notes: null, quiz: null, flashcards: null, videoPlan: null, pyqAnalysis: null, generatedAt: new Date().toISOString() };
try {
if (context.outputs.includes("notes") || context.outputs.length === 0) {
setGenProgress("Creating study summary and smart notes...");
try {
const notes = await generateBrainNotes(buildPayload(topic, subject, sourceIds, { options: { mode: "smart_notes" } }));
results.notes = notes.output;
} catch { /* notes unavailable */ }
}
if (context.outputs.includes("quiz") || context.outputs.length === 0) {
setGenProgress("Building practice quiz...");
try {
const quiz = await generateBrainQuiz(buildPayload(topic, subject, sourceIds, { options: { num_questions: 8 } }));
results.quiz = quiz.output;
} catch { /* quiz unavailable */ }
}
if (context.outputs.includes("flashcards") || context.outputs.length === 0) {
setGenProgress("Making flashcards...");
try {
const cards = await generateBrainFlashcards(buildPayload(topic, subject, sourceIds, { options: { card_count: 10 } }));
results.flashcards = cards.output;
} catch { /* flashcards unavailable */ }
}
if (context.outputs.includes("video") || context.outputs.length === 0) {
setGenProgress("Planning study video...");
try {
const videoPayload: BrainVideoPlanPayload = {
topic,
video_mode: "exam_focus" as VideoMode,
teaching_style: "normal_teacher" as const,
format: "16:9" as const,
source_id: sourceIds[0] ?? null,
source_ids: sourceIds,
level: null,
};
const videoPlan = await generateBrainVideoPlan(videoPayload);
results.videoPlan = videoPlan;
} catch { /* video plan unavailable */ }
}
if (context.outputs.includes("pyq")) {
setGenProgress("Analyzing question patterns...");
try {
const pyq = await generateBrainPyqAnalysis(buildPayload(topic, subject, sourceIds, { options: { raw_text: `Analyze PYQ patterns for ${topic} - ${subject} ${context.board}` } }));
results.pyqAnalysis = pyq.output;
} catch { /* pyq unavailable */ }
}
setKit(results);
setStep("results");
} catch (err) {
setGenError(err instanceof Error ? err.message : "Generation failed. Try again.");
setStep("upload");
} finally {
setGenProgress("");
}
}, [context, uploadedDocs]);
// triggered directly when context collection completes
const renderUpload = () => (
<div className="mx-auto flex w-full max-w-2xl flex-col gap-6 py-8">
<div className="text-center">
<h1 className="text-3xl font-bold tracking-tight text-white">Study Generator</h1>
<p className="mt-2 text-sm text-white/60">Upload notes, textbook pages, question papers, or any study material. DocDoe generates your complete study kit.</p>
</div>
<div
onDragOver={e => { e.preventDefault(); setDragOver(true); }}
onDragLeave={() => setDragOver(false)}
onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()}
className={cn(
"flex cursor-pointer flex-col items-center gap-4 rounded-2xl border-2 border-dashed p-12 transition",
dragOver ? "border-white/40 bg-white/[0.06]" : "border-white/[0.12] bg-white/[0.02] hover:border-white/25 hover:bg-white/[0.04]",
)}
>
<div className="grid size-16 place-items-center rounded-2xl bg-white/[0.06]">
<Upload className="size-7 text-white/60" />
</div>
<div className="text-center">
<p className="font-semibold text-white/80">Drop your file here or click to browse</p>
<p className="mt-1 text-xs text-white/40">Selectable-text PDF or TXT &middot; Max 20 MB</p>
</div>
<input ref={fileInputRef} type="file" accept={MATERIAL_UPLOAD_ACCEPT} className="hidden"
onChange={e => { const f = e.target.files?.[0]; if (f) handleFile(f); e.target.value = ""; }} />
</div>
{isUploading && <div className="flex items-center justify-center gap-2 text-sm text-white/60"><LoaderCircle className="size-4 animate-spin" /> Uploading...</div>}
{uploadError && <div className="rounded-xl border border-red-500/20 bg-red-500/10 px-4 py-3 text-sm text-red-300">{uploadError}</div>}
{uploadedDocs.length > 0 && (
<div className="space-y-2">
<h3 className="text-xs font-semibold uppercase tracking-[0.12em] text-white/40">Uploaded ({uploadedDocs.length})</h3>
{uploadedDocs.map(doc => (
<div key={doc.id} className="flex items-center gap-3 rounded-xl border border-white/[0.08] bg-white/[0.03] px-4 py-3">
<FileText className="size-4 shrink-0 text-white/40" />
<div className="min-w-0 flex-1"><p className="truncate text-sm font-medium text-white/80">{sourceTitle(doc)}</p><p className="text-xs text-white/40">{doc.file_type || "document"}</p></div>
<button onClick={() => setUploadedDocs(prev => prev.filter(d => d.id !== doc.id))} className="shrink-0 rounded-lg p-1.5 text-white/30 hover:bg-white/[0.06] hover:text-white/60"><X className="size-4" /></button>
</div>
))}
</div>
)}
{uploadedDocs.length > 0 && (
<div className="flex flex-col gap-3 sm:flex-row">
<button onClick={() => setStep("context")} className="flex flex-1 items-center justify-center gap-2 rounded-xl bg-white px-5 py-3 font-semibold text-black transition hover:bg-white/90">Continue <ArrowRight className="size-4" /></button>
<button onClick={handleGenerate} className="flex flex-1 items-center justify-center gap-2 rounded-xl border border-white/[0.12] px-5 py-3 font-semibold text-white/80 transition hover:bg-white/[0.06]"><Sparkles className="size-4" /> Generate &mdash; no questions</button>
</div>
)}
<div className="relative my-4"><div className="absolute inset-0 flex items-center"><div className="w-full border-t border-white/[0.06]" /></div><div className="relative flex justify-center text-xs text-white/30"><span className="bg-[#0a0a0a] px-3">or just type a topic</span></div></div>
<button onClick={() => { setContext(prev => ({ ...prev, chapter: "" })); setQuestionStep(0); setStep("context"); }} className="flex items-center justify-center gap-2 rounded-xl border border-white/[0.08] px-5 py-3 text-sm font-medium text-white/60 transition hover:bg-white/[0.04] hover:text-white/80"><BookOpen className="size-4" /> Start with topic name only</button>
</div>
);
const renderContext = () => {
if (!currentQuestion && allContextCollected) return null;
return (
<div className="mx-auto flex w-full max-w-lg flex-col gap-6 py-12">
<button onClick={() => { setQuestionStep(0); setStep("upload"); }} className="flex w-fit items-center gap-1.5 text-xs text-white/40 transition hover:text-white/60"><ArrowLeft className="size-3.5" /> Back</button>
<div className="flex gap-1.5">{missingQuestions.slice(0, 7).map((q, i) => (<div key={q.key} className={cn("h-1 flex-1 rounded-full transition", i < questionStep ? "bg-white/40" : i === questionStep ? "bg-white/70" : "bg-white/[0.08]")} />))}</div>
<div className="space-y-6">
<h2 className="text-xl font-bold text-white">{currentQuestion?.label || "One more thing..."}</h2>
{currentQuestion?.key === "chapter" && (
<div className="space-y-3">
<input autoFocus type="text" placeholder="e.g. Electromagnetic Induction" className="w-full rounded-xl border border-white/[0.12] bg-white/[0.04] px-4 py-3 text-sm text-white placeholder:text-white/30 focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20"
onKeyDown={e => { if (e.key === "Enter" && (e.target as HTMLInputElement).value.trim()) handleContextSelect("chapter", (e.target as HTMLInputElement).value.trim()); }} />
<button onClick={() => { const inp = document.querySelector<HTMLInputElement>('input[placeholder="e.g. Electromagnetic Induction"]'); if (inp?.value.trim()) handleContextSelect("chapter", inp.value.trim()); }} className="w-full rounded-xl bg-white px-4 py-2.5 text-sm font-semibold text-black transition hover:bg-white/90">Next</button>
<button onClick={() => setQuestionStep(s => Math.min(s + 1, missingQuestions.length))} className="w-full text-xs text-white/30 hover:text-white/50">Skip for now</button>
</div>
)}
{currentQuestion?.options && currentQuestion.type !== "multiselect" && (
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3">
{(currentQuestion.options as readonly string[] | readonly { value: string; label: string }[]).map(opt => {
const value = typeof opt === "string" ? opt : opt.value;
const label = typeof opt === "string" ? opt : opt.label;
const selected = context[currentQuestion!.key] === value;
return (<button key={value} onClick={() => handleContextSelect(currentQuestion!.key, value)} className={cn("rounded-xl border px-4 py-3 text-sm font-medium transition", selected ? "border-white/30 bg-white/[0.08] text-white" : "border-white/[0.08] text-white/60 hover:border-white/20 hover:text-white/80")}>{label}</button>);
})}
</div>
)}
{currentQuestion?.type === "multiselect" && currentQuestion.options && (
<div className="space-y-3">
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3">
{(currentQuestion.options as readonly { value: string; label: string }[]).map(opt => {
const Icon = OUTPUT_OPTIONS.find(o => o.id === opt.value)?.icon || FileText;
const selected = context.outputs.includes(opt.value);
return (<button key={opt.value} onClick={() => handleOutputToggle(opt.value)} className={cn("flex flex-col items-center gap-2 rounded-xl border px-4 py-4 text-sm font-medium transition", selected ? "border-white/30 bg-white/[0.08] text-white" : "border-white/[0.08] text-white/50 hover:border-white/20 hover:text-white/70")}><Icon className="size-5" /><span>{opt.label}</span>{selected && <Check className="size-3.5 text-white/60" />}</button>);
})}
</div>
<button onClick={() => setQuestionStep(s => Math.min(s + 1, missingQuestions.length))} className="w-full rounded-xl bg-white px-4 py-2.5 text-sm font-semibold text-black transition hover:bg-white/90">{context.outputs.length > 0 ? `Done (${context.outputs.length} selected)` : "Skip — generate all"}</button>
</div>
)}
</div>
<div className="mt-8 rounded-xl border border-white/[0.06] bg-white/[0.02] p-4">
<p className="mb-2 text-[10px] font-semibold uppercase tracking-[0.14em] text-white/30">Your context</p>
<div className="flex flex-wrap gap-2">
{context.classLevel && <span className="rounded-full border border-white/[0.08] bg-white/[0.04] px-2.5 py-1 text-[11px] text-white/60">{context.classLevel}</span>}
{context.board && <span className="rounded-full border border-white/[0.08] bg-white/[0.04] px-2.5 py-1 text-[11px] text-white/60">{context.board}</span>}
{context.subject && <span className="rounded-full border border-white/[0.08] bg-white/[0.04] px-2.5 py-1 text-[11px] text-white/60">{context.subject}</span>}
{context.chapter && <span className="rounded-full border border-white/[0.08] bg-white/[0.04] px-2.5 py-1 text-[11px] text-white/60">{context.chapter}</span>}
{context.goal && <span className="rounded-full border border-white/[0.08] bg-white/[0.04] px-2.5 py-1 text-[11px] text-white/60">{GOAL_OPTIONS.find(g => g.value === context.goal)?.label || context.goal}</span>}
{context.availableTime && <span className="rounded-full border border-white/[0.08] bg-white/[0.04] px-2.5 py-1 text-[11px] text-white/60">{TIME_OPTIONS.find(t => t.value === context.availableTime)?.label || context.availableTime}</span>}
{context.outputs.map(o => <span key={o} className="rounded-full border border-white/[0.08] bg-white/[0.04] px-2.5 py-1 text-[11px] text-white/60">{OUTPUT_OPTIONS.find(op => op.id === o)?.label || o}</span>)}
</div>
</div>
</div>
);
};
const renderGenerating = () => (
<div className="mx-auto flex w-full max-w-lg flex-col items-center gap-6 py-24 text-center">
<div className="grid size-20 place-items-center rounded-full bg-white/[0.06]"><LoaderCircle className="size-10 animate-spin text-white/60" /></div>
<div><h2 className="text-lg font-bold text-white">Generating your study kit</h2><p className="mt-1 text-sm text-white/50">{genProgress}</p></div>
<div className="flex flex-wrap justify-center gap-2">
{(context.outputs.length > 0 ? context.outputs : OUTPUT_OPTIONS.map(o => o.id)).map(o => {
const opt = OUTPUT_OPTIONS.find(op => op.id === o);
return <span key={o} className="rounded-full border border-white/[0.08] bg-white/[0.04] px-3 py-1 text-xs text-white/50">{opt?.label || o}</span>;
})}
</div>
{genError && <div className="mt-4 rounded-xl border border-red-500/20 bg-red-500/10 px-4 py-3 text-sm text-red-300">{genError}</div>}
</div>
);
const renderResults = () => {
if (!kit) return null;
const sections: Array<{ id: string; label: string; icon: typeof Sparkles; content: React.ReactNode; }> = [];
if (kit.notes) {
sections.push({
id: "notes", label: "Smart Notes", icon: ClipboardList,
content: (
<div className="space-y-3 text-sm text-white/70">
{kit.notes.key_points?.length > 0 && (
<div className="rounded-xl border border-white/[0.06] bg-white/[0.02] p-4">
<h4 className="mb-2 font-semibold text-white/80">Key Points</h4>
<ul className="list-inside list-disc space-y-1">
{kit.notes.key_points.slice(0, 8).map((kp: string, i: number) => (
<li key={i} className="leading-6 text-white/70">{kp}</li>
))}
</ul>
</div>
)}
{kit.notes.summary && (
<div className="rounded-xl border border-white/[0.06] bg-white/[0.02] p-4">
<h4 className="mb-2 font-semibold text-white/80">Summary</h4>
<p className="leading-7">{kit.notes.summary}</p>
</div>
)}
{kit.notes.exam_keywords && kit.notes.exam_keywords.length > 0 && (
<div className="rounded-xl border border-white/[0.06] bg-white/[0.02] p-4">
<h4 className="mb-2 font-semibold text-white/80">Exam Keywords</h4>
<div className="flex flex-wrap gap-1.5">
{kit.notes.exam_keywords.slice(0, 10).map((kw: string, i: number) => (
<span key={i} className="rounded-full border border-white/[0.08] bg-white/[0.04] px-2 py-1 text-xs text-white/60">{kw}</span>
))}
</div>
</div>
)}
{!kit.notes.key_points && !kit.notes.summary && <p className="text-white/50">Notes generated.</p>}
</div>
),
});
}
if (kit.quiz) {
sections.push({
id: "quiz", label: "Practice Quiz", icon: FileQuestion,
content: (
<div className="space-y-3">
{(kit.quiz.questions || []).slice(0, 5).map((q, i) => (
<div key={i} className="rounded-xl border border-white/[0.06] bg-white/[0.02] p-4">
<div className="flex items-start gap-3">
<span className="grid size-7 shrink-0 place-items-center rounded-lg border border-white/[0.08] bg-black/24 text-xs font-bold text-white/50">{i + 1}</span>
<div><p className="text-sm font-medium text-white/80">{q.question}</p>
<div className="mt-2 grid gap-1.5 sm:grid-cols-2">
{q.options?.slice(0, 4).map((opt, oi) => (
<div key={oi} className="rounded-lg border border-white/[0.06] bg-white/[0.02] px-3 py-2 text-xs text-white/60">{opt}</div>
))}
</div>
</div>
</div>
</div>
))}
{(!kit.quiz.questions || kit.quiz.questions.length === 0) && <p className="text-sm text-white/50">Quiz questions will be ready in the full session.</p>}
</div>
),
});
}
if (kit.flashcards) {
sections.push({
id: "flashcards", label: "Flashcards", icon: Layers3,
content: (
<div className="grid gap-2 sm:grid-cols-2">
{(kit.flashcards.cards || []).slice(0, 6).map((card: { front: string; back: string }, i: number) => (
<div key={i} className="rounded-xl border border-white/[0.06] bg-white/[0.02] p-4">
<p className="text-xs font-semibold text-white/40">FRONT</p>
<p className="mt-1 text-sm text-white/80">{card.front}</p>
<p className="mt-2 text-xs font-semibold text-white/40">BACK</p>
<p className="mt-1 text-sm text-white/60">{card.back}</p>
</div>
))}
{(!kit.flashcards.cards || kit.flashcards.cards.length === 0) && <p className="col-span-2 text-sm text-white/50">Flashcards will be created in the full session.</p>}
</div>
),
});
}
if (kit.videoPlan) {
sections.push({
id: "video", label: "Study Video Plan", icon: Video,
content: (
<div className="space-y-3">
<div className="grid gap-3 sm:grid-cols-3">
<div className="rounded-xl border border-white/[0.06] bg-white/[0.02] p-4">
<p className="text-[10px] font-semibold uppercase text-white/40">Title</p>
<p className="mt-1 text-sm font-medium text-white/80">{kit.videoPlan.title || "Study video"}</p>
</div>
<div className="rounded-xl border border-white/[0.06] bg-white/[0.02] p-4">
<p className="text-[10px] font-semibold uppercase text-white/40">Scenes</p>
<p className="mt-1 text-sm text-white/80">{kit.videoPlan.scenes?.length || 0} beats</p>
</div>
<div className="rounded-xl border border-white/[0.06] bg-white/[0.02] p-4">
<p className="text-[10px] font-semibold uppercase text-white/40">Duration</p>
<p className="mt-1 text-sm text-white/80">{kit.videoPlan.target_duration_range || "~10 min"}</p>
</div>
</div>
{(kit.videoPlan.scenes || []).slice(0, 5).map((scene: { title?: string; duration?: string; narration?: string }, i: number) => (
<div key={i} className="rounded-xl border border-white/[0.06] bg-white/[0.02] p-4">
<div className="flex items-center gap-3">
<span className="grid size-7 shrink-0 place-items-center rounded-lg bg-white/[0.06] text-xs font-bold text-white/50">{i + 1}</span>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-white/80">{scene.title || `Scene ${i + 1}`}</p>
{scene.duration && <p className="text-xs text-white/40">{scene.duration}</p>}
</div>
</div>
{scene.narration && <p className="mt-2 text-xs leading-6 text-white/50 line-clamp-2">{scene.narration}</p>}
</div>
))}
</div>
),
});
}
if (kit.pyqAnalysis) {
sections.push({
id: "pyq", label: "PYQ Pattern Insights", icon: GraduationCap,
content: (
<div className="space-y-3">
<div className="rounded-xl border border-white/[0.06] bg-white/[0.02] p-4 text-sm leading-7 text-white/70">
<p>{kit.pyqAnalysis.summary}</p>
</div>
<div className="grid gap-3 sm:grid-cols-2">
{(kit.pyqAnalysis.repeated_topics || []).slice(0, 5).map((t: { topic: string; chapter: string; count: number }, i: number) => (
<div key={i} className="rounded-xl border border-white/[0.06] bg-white/[0.02] p-3">
<p className="text-xs font-semibold text-white/40">REPEATED TOPIC</p>
<p className="mt-1 text-sm text-white/80">{t.topic}</p>
{t.count > 0 && <p className="mt-0.5 text-xs text-white/40">Appeared {t.count}×</p>}
</div>
))}
</div>
{!kit.pyqAnalysis.repeated_topics && <p className="text-sm text-white/50">PYQ analysis uses uploaded papers. Add question papers for deeper pattern detection.</p>}
</div>
),
});
}
return (
<div className="mx-auto flex w-full max-w-2xl flex-col gap-6 py-8">
<div className="flex items-start justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-white">Your Study Kit</h1>
<p className="mt-1 text-sm text-white/50">
{[context.subject, context.chapter, context.board, context.classLevel].filter(Boolean).join(" · ")}
</p>
<p className="mt-0.5 text-xs text-white/30">Estimated study time: {estimateStudyTime(context)} &middot; {uploadedDocs.length} source{uploadedDocs.length !== 1 ? "s" : ""}</p>
</div>
<button onClick={() => { setKit(null); setStep("upload"); }} className="flex shrink-0 items-center gap-1.5 rounded-xl border border-white/[0.1] px-3.5 py-2 text-xs font-medium text-white/50 transition hover:bg-white/[0.06] hover:text-white/70"><RefreshCw className="size-3.5" /> New kit</button>
</div>
{uploadedDocs.length > 0 && (
<div className="rounded-xl border border-white/[0.06] bg-white/[0.02] px-4 py-3">
<p className="text-[10px] font-semibold uppercase tracking-[0.14em] text-white/30">Sources used</p>
<div className="mt-2 flex flex-wrap gap-2">
{uploadedDocs.map(doc => (
<span key={doc.id} className="inline-flex items-center gap-1.5 rounded-full border border-white/[0.08] bg-white/[0.04] px-2.5 py-1 text-[11px] text-white/50"><FileText className="size-3" /> {sourceTitle(doc)}</span>
))}
</div>
</div>
)}
{sections.map(section => (
<div key={section.id} className="overflow-hidden rounded-2xl border border-white/[0.08] bg-white/[0.02]">
<button onClick={() => setExpandedSection(expandedSection === section.id ? null : section.id)} className="flex w-full items-center justify-between px-5 py-4 text-left transition hover:bg-white/[0.02]">
<div className="flex items-center gap-3"><section.icon className="size-4 text-white/50" /><span className="font-semibold text-white/80">{section.label}</span></div>
<ChevronDown className={cn("size-4 text-white/30 transition", expandedSection === section.id && "rotate-180")} />
</button>
{expandedSection === section.id && <div className="border-t border-white/[0.06] px-5 py-4">{section.content}</div>}
</div>
))}
<div className="rounded-2xl border border-white/[0.08] bg-white/[0.02] p-5">
<div className="flex items-center gap-2 mb-4"><Moon className="size-4 text-white/50" /><h3 className="font-semibold text-white/80">Revision Checklist</h3></div>
<div className="space-y-3 text-sm text-white/60">
{["Review summary and key concepts", "Practice with generated quiz questions", "Review flashcards for active recall", "Watch or plan study video", "Review PYQ patterns for exam strategy", "Self-test with closed-book recall"].map((item, i) => (
<label key={i} className="flex items-start gap-3"><input type="checkbox" className="mt-0.5 size-4 accent-white/60" /><span>{item}</span></label>
))}
</div>
</div>
<p className="text-center text-[10px] text-white/20">Generated {new Date(kit.generatedAt).toLocaleTimeString()} &middot; Sources: {uploadedDocs.length} &middot; ~{estimateStudyTime(context)}</p>
</div>
);
};
return (
<div className="mx-auto min-h-[calc(100vh-4rem)] w-full max-w-5xl px-4 py-6 sm:px-6 lg:px-8">
{step === "results" && (
<div className="mb-6 flex items-center gap-3">
<button onClick={() => router.push("/home")} className="rounded-xl border border-white/[0.08] p-2 text-white/40 transition hover:bg-white/[0.06] hover:text-white/60"><ArrowLeft className="size-4" /></button>
<div><p className="text-xs font-semibold uppercase tracking-[0.14em] text-white/30">Study Generator</p><p className="text-sm text-white/60">Study kit ready</p></div>
</div>
)}
{step === "upload" && renderUpload()}
{step === "context" && renderContext()}
{step === "generating" && renderGenerating()}
{step === "results" && renderResults()}
</div>
);
}