DocDoeAI / src /components /study-chat /DeepChapterCards.tsx
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
65d68f5
Raw History Blame Contribute Delete
18.3 kB
"use client";
import {
BookOpen,
Calculator,
ChevronDown,
ChevronUp,
Lightbulb,
Target,
Zap,
} from "lucide-react";
import { useState } from "react";
import type { DeepChapterOutput, VisualLessonOutput } from "@/lib/api/types";
export function DeepChapterCard({ data, topic }: { data: DeepChapterOutput; topic: string }) {
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set(["concepts"]));
const toggle = (key: string) => {
setExpandedSections((prev) => {
const next = new Set(prev);
if (next.has(key)) next.delete(key);
else next.add(key);
return next;
});
};
const dataLabel = data.data_source_label;
const pyqPatterns = data.pyq_patterns ?? [];
const hasRealPyq = pyqPatterns.some((p) => p.question && p.source === "Seen in uploaded PYQs");
return (
<article className="animate-msg-assistant w-full rounded-xl border border-white/[0.06] bg-[#080809] p-5">
{/* Header */}
<div className="mb-4 flex items-center gap-2.5">
<BookOpen className="size-4 shrink-0 text-blue-400/70" />
<div>
<span className="text-[10px] font-medium uppercase tracking-[0.08em] text-white/22">Deep Chapter Teaching</span>
<p className="text-[15px] font-semibold tracking-[-0.01em] text-white/95">{data.chapter_title || topic}</p>
</div>
</div>
{/* Data source label */}
{dataLabel && (
<div className="mb-2 inline-flex items-center gap-1.5 rounded-md border border-white/[0.06] bg-white/[0.03] px-2 py-1">
<span className="size-1.5 rounded-full bg-emerald-400/60" />
<span className="text-[10px] text-white/40">{dataLabel}</span>
</div>
)}
{/* Syllabus position */}
<p className="mb-3 text-[11px] text-white/35">{data.syllabus_position}</p>
{/* What this chapter is about */}
{data.what_this_chapter_is_about && (
<div className="mb-3 rounded-lg border border-white/[0.06] bg-white/[0.02] px-3 py-2.5">
<p className="text-[13px] leading-[1.7] text-white/75">{data.what_this_chapter_is_about}</p>
</div>
)}
{/* Prerequisites */}
{data.prerequisite_topics?.length > 0 && (
<CollapsibleSection
title="Prerequisites"
icon={<Target className="size-3" />}
expanded={expandedSections.has("prereqs")}
onToggle={() => toggle("prereqs")}
count={data.prerequisite_topics.length}
>
<ul className="space-y-1">
{data.prerequisite_topics.map((p, i) => (
<li key={i} className="flex items-start gap-2 text-[12px] text-white/60">
<span className="mt-1 size-1 shrink-0 rounded-full bg-blue-400/50" />
{p}
</li>
))}
</ul>
</CollapsibleSection>
)}
{/* Deep concept sections */}
{data.deep_concept_sections?.length > 0 && (
<CollapsibleSection
title="Deep Teaching"
icon={<Lightbulb className="size-3" />}
expanded={expandedSections.has("concepts")}
onToggle={() => toggle("concepts")}
count={data.deep_concept_sections.length}
>
<div className="space-y-3">
{data.deep_concept_sections.map((concept, i) => (
<div key={i} className="rounded-lg border border-white/[0.04] bg-white/[0.01] p-3">
<p className="text-[13px] font-semibold text-white/85">{concept.name}</p>
<p className="mt-1 text-[12px] leading-[1.6] text-white/60">{concept.simple_explanation}</p>
{concept.deep_explanation && (
<p className="mt-1 text-[12px] leading-[1.6] text-white/50">{concept.deep_explanation}</p>
)}
{concept.equation && (
<p className="mt-1 rounded bg-black/30 px-2 py-1 text-[12px] font-mono text-emerald-400/80">{concept.equation}</p>
)}
{concept.real_example && (
<p className="mt-1 text-[11px] text-white/40">Example: {concept.real_example}</p>
)}
{concept.common_mistake && (
<p className="mt-1 text-[11px] text-amber-400/60">Mistake: {concept.common_mistake}</p>
)}
{concept.practice_question && (
<p className="mt-1 text-[11px] text-white/55">Try: {concept.practice_question}</p>
)}
</div>
))}
</div>
</CollapsibleSection>
)}
{/* Derivations */}
{data.derivations?.length > 0 && (
<CollapsibleSection
title="Derivations"
icon={<Calculator className="size-3" />}
expanded={expandedSections.has("derivations")}
onToggle={() => toggle("derivations")}
count={data.derivations.length}
>
<div className="space-y-3">
{data.derivations.map((d, i) => (
<div key={i} className="rounded-lg border border-emerald-500/10 bg-emerald-500/[0.03] p-3">
<p className="text-[13px] font-semibold text-white/85">{d.name}</p>
{d.symbols?.length > 0 && (
<p className="mt-1 text-[11px] text-white/40">Symbols: {d.symbols.join(", ")}</p>
)}
{d.steps?.length > 0 && (
<ol className="mt-2 space-y-1">
{d.steps.map((step, j) => (
<li key={j} className="flex items-start gap-2 text-[12px] text-white/60">
<span className="mt-0.5 text-[10px] text-white/30">{j + 1}.</span>
{step}
</li>
))}
</ol>
)}
{d.final_formula && (
<p className="mt-2 rounded bg-black/30 px-2 py-1 text-[12px] font-mono text-emerald-400/80">{d.final_formula}</p>
)}
{d.unit_check && <p className="mt-1 text-[11px] text-white/40">{d.unit_check}</p>}
</div>
))}
</div>
</CollapsibleSection>
)}
{/* Formulas */}
{data.formulas?.length > 0 && (
<CollapsibleSection
title="Formulas"
icon={<Zap className="size-3" />}
expanded={expandedSections.has("formulas")}
onToggle={() => toggle("formulas")}
count={data.formulas.length}
>
<div className="space-y-1.5">
{data.formulas.map((f, i) => (
<div key={i} className="flex items-start gap-2 rounded-lg border border-white/[0.04] bg-white/[0.01] px-3 py-2">
<span className="min-w-0 flex-1 text-[12px] text-white/70">{f.name}</span>
<span className="shrink-0 font-mono text-[11px] text-emerald-400/70">{f.formula}</span>
<span className="shrink-0 text-[10px] text-white/30">[{f.unit}]</span>
</div>
))}
</div>
</CollapsibleSection>
)}
{/* Common mistakes */}
{data.common_mistakes?.length > 0 && (
<CollapsibleSection
title="Common Mistakes"
expanded={expandedSections.has("mistakes")}
onToggle={() => toggle("mistakes")}
count={data.common_mistakes.length}
>
<ul className="space-y-1">
{data.common_mistakes.map((m, i) => (
<li key={i} className="flex items-start gap-2 text-[12px] text-amber-400/60">
<span className="mt-1 size-1 shrink-0 rounded-full bg-amber-400/50" />
{m}
</li>
))}
</ul>
</CollapsibleSection>
)}
{/* Important questions */}
{data.important_questions?.length > 0 && (
<CollapsibleSection
title="Important Questions"
expanded={expandedSections.has("important")}
onToggle={() => toggle("important")}
count={data.important_questions.length}
>
<ul className="space-y-1">
{data.important_questions.map((q, i) => (
<li key={i} className="flex items-start gap-2 text-[12px] text-white/60">
<span className="mt-1 size-1 shrink-0 rounded-full bg-white/45" />
{q}
</li>
))}
</ul>
</CollapsibleSection>
)}
{/* PYQ */}
{hasRealPyq ? (
<CollapsibleSection
title="PYQ Patterns (Uploaded Papers)"
expanded={expandedSections.has("pyq")}
onToggle={() => toggle("pyq")}
count={pyqPatterns.filter((p) => p.question).length}
>
<div className="space-y-1.5">
{pyqPatterns.filter((p) => p.question).map((p, i) => (
<div key={i} className="rounded-lg border border-white/[0.04] bg-white/[0.01] px-3 py-2">
<p className="text-[12px] text-white/70">{p.question}</p>
<div className="mt-0.5 flex items-center gap-2">
<span className="text-[10px] text-white/35">{p.marks} marks — {p.year}</span>
{p.source === "Seen in uploaded PYQs" && (
<span className="rounded bg-emerald-500/10 px-1.5 py-0.5 text-[9px] text-emerald-400/70">Uploaded PYQ</span>
)}
</div>
</div>
))}
</div>
</CollapsibleSection>
) : (
<div className="mt-3 rounded-lg border border-amber-500/10 bg-amber-500/[0.03] px-3 py-2.5">
<p className="text-[11px] font-medium text-amber-400/70">Upload previous year papers to unlock pattern analysis across them.</p>
<p className="mt-1 text-[10px] text-amber-400/50">Upload previous year question papers to see repeated topics, marks distribution, and evidence-backed insights.</p>
</div>
)}
{/* Quick revision */}
{data.quick_revision_box?.length > 0 && (
<div className="mt-3 rounded-lg border border-blue-500/10 bg-blue-500/[0.03] px-3 py-2.5">
<p className="mb-1.5 text-[10px] font-medium uppercase tracking-[0.08em] text-blue-400/50">Quick Revision</p>
<ul className="space-y-1">
{data.quick_revision_box.map((r, i) => (
<li key={i} className="text-[12px] text-white/60">{r}</li>
))}
</ul>
</div>
)}
{/* Practice */}
{data.practice_questions?.length > 0 && (
<div className="mt-3 rounded-lg border border-white/[0.1] bg-white/[0.03] px-3 py-2.5">
<p className="mb-1.5 text-[10px] font-medium uppercase tracking-[0.08em] text-white/50">Practice</p>
<ul className="space-y-1">
{data.practice_questions.map((q, i) => (
<li key={i} className="text-[12px] text-white/60">{q}</li>
))}
</ul>
</div>
)}
{/* Next action */}
{data.next_best_action && (
<div className="mt-3 rounded-lg border border-emerald-500/10 bg-emerald-500/[0.03] px-3 py-2.5">
<p className="text-[12px] text-emerald-400/70">{data.next_best_action}</p>
</div>
)}
{/* Source */}
{data.source_used && (
<p className="mt-3 text-[10px] text-white/25">Source: {data.source_used}</p>
)}
</article>
);
}
export function VisualLessonCard({ data, topic }: { data: VisualLessonOutput; topic: string }) {
const [expandedFrames, setExpandedFrames] = useState<Set<string>>(new Set(["derivation"]));
const toggle = (key: string) => {
setExpandedFrames((prev) => {
const next = new Set(prev);
if (next.has(key)) next.delete(key);
else next.add(key);
return next;
});
};
const dataLabel = data.data_source_label;
return (
<article className="animate-msg-assistant w-full rounded-xl border border-white/[0.06] bg-[#080809] p-5">
<div className="mb-4 flex items-center gap-2.5">
<BookOpen className="size-4 shrink-0 text-white/70" />
<div>
<span className="text-[10px] font-medium uppercase tracking-[0.08em] text-white/22">Visual Lesson Plan</span>
<p className="text-[15px] font-semibold tracking-[-0.01em] text-white/95">{data.lesson_title || topic}</p>
</div>
</div>
{/* Data source label */}
{dataLabel && (
<div className="mb-2 inline-flex items-center gap-1.5 rounded-md border border-white/[0.06] bg-white/[0.03] px-2 py-1">
<span className="size-1.5 rounded-full bg-emerald-400/60" />
<span className="text-[10px] text-white/40">{dataLabel}</span>
</div>
)}
<p className="mb-2 text-[11px] text-white/35">{data.syllabus_position}</p>
<p className="mb-3 text-[12px] text-white/50">{data.learning_goal}</p>
{/* Stats */}
<div className="mb-4 flex gap-3">
<span className="rounded bg-white/[0.04] px-2 py-1 text-[11px] text-white/50">{data.total_frames} frames</span>
<span className="rounded bg-white/[0.04] px-2 py-1 text-[11px] text-white/50">{Math.round(data.total_estimated_seconds / 60)} min</span>
<span className="rounded bg-white/[0.04] px-2 py-1 text-[11px] text-white/50">{data.visual_style}</span>
</div>
{/* Concept flow */}
{data.concept_flow?.length > 0 && (
<div className="mb-4">
<p className="mb-1.5 text-[10px] font-medium uppercase tracking-[0.08em] text-white/22">Concept Flow</p>
<div className="flex flex-wrap gap-1.5">
{data.concept_flow.map((step, i) => (
<span key={i} className="rounded-md border border-white/[0.06] bg-white/[0.02] px-2 py-1 text-[11px] text-white/50">
{i + 1}. {step}
</span>
))}
</div>
</div>
)}
{/* Derivation frames */}
{data.derivation_frames?.length > 0 && (
<CollapsibleSection
title="Derivation Frames"
icon={<Calculator className="size-3" />}
expanded={expandedFrames.has("derivation")}
onToggle={() => toggle("derivation")}
count={data.derivation_frames.length}
>
<div className="space-y-3">
{data.derivation_frames.map((frame) => (
<FrameCard key={frame.frame_number} frame={frame} />
))}
</div>
</CollapsibleSection>
)}
{/* Diagram frames */}
{data.diagram_frames?.length > 0 && (
<CollapsibleSection
title="Diagram Frames"
expanded={expandedFrames.has("diagram")}
onToggle={() => toggle("diagram")}
count={data.diagram_frames.length}
>
<div className="space-y-3">
{data.diagram_frames.map((frame) => (
<FrameCard key={frame.frame_number} frame={frame} />
))}
</div>
</CollapsibleSection>
)}
{/* Numerical frames */}
{data.numerical_frames?.length > 0 && (
<CollapsibleSection
title="Numerical Frames"
icon={<Calculator className="size-3" />}
expanded={expandedFrames.has("numerical")}
onToggle={() => toggle("numerical")}
count={data.numerical_frames.length}
>
<div className="space-y-3">
{data.numerical_frames.map((frame) => (
<FrameCard key={frame.frame_number} frame={frame} />
))}
</div>
</CollapsibleSection>
)}
{/* Narration script */}
{data.narration_script && (
<div className="mt-3 rounded-lg border border-white/[0.04] bg-white/[0.01] px-3 py-2.5">
<p className="mb-1 text-[10px] font-medium uppercase tracking-[0.08em] text-white/20">Narration Script</p>
<p className="text-[12px] leading-[1.6] text-white/50 whitespace-pre-wrap">{data.narration_script.substring(0, 500)}{data.narration_script.length > 500 ? "..." : ""}</p>
</div>
)}
</article>
);
}
function FrameCard({ frame }: { frame: { frame_number: number; teaching_purpose: string; visual_type: string; on_screen_text: string; narration: string; why_this_helps: string; exam_connection: string; duration_seconds: number; common_misunderstanding_fixed?: string } }) {
return (
<div className="rounded-lg border border-white/[0.04] bg-white/[0.01] p-3">
<div className="flex items-center justify-between gap-2">
<span className="text-[11px] font-medium text-white/50">Frame {frame.frame_number}</span>
<span className="rounded bg-white/[0.04] px-1.5 py-0.5 text-[10px] text-white/35">{frame.visual_type}</span>
<span className="text-[10px] text-white/25">{frame.duration_seconds}s</span>
</div>
<p className="mt-1 text-[12px] font-medium text-white/70">{frame.teaching_purpose}</p>
<p className="mt-1 rounded bg-black/30 px-2 py-1 text-[11px] font-mono text-emerald-400/70 whitespace-pre-wrap">{frame.on_screen_text}</p>
<p className="mt-1 text-[11px] text-white/45">{frame.narration}</p>
{frame.common_misunderstanding_fixed && (
<p className="mt-1 text-[10px] text-amber-400/50">Fixes: {frame.common_misunderstanding_fixed}</p>
)}
<p className="mt-1 text-[10px] text-white/50">Exam: {frame.exam_connection}</p>
</div>
);
}
function CollapsibleSection({
title,
icon,
expanded,
onToggle,
count,
children,
}: {
title: string;
icon?: React.ReactNode;
expanded: boolean;
onToggle: () => void;
count: number;
children: React.ReactNode;
}) {
return (
<div className="mt-3">
<button
type="button"
onClick={onToggle}
className="flex w-full items-center justify-between gap-2 rounded-lg border border-white/[0.06] bg-white/[0.02] px-3 py-2 transition hover:bg-white/[0.04]"
>
<span className="flex items-center gap-1.5 text-[11px] font-medium text-white/50">
{icon}
{title}
<span className="text-[10px] text-white/25">({count})</span>
</span>
{expanded ? <ChevronUp className="size-3 text-white/30" /> : <ChevronDown className="size-3 text-white/30" />}
</button>
{expanded && <div className="mt-2 pl-1">{children}</div>}
</div>
);
}