DocDoeAI / src /components /learn-anything /CoursePlanCard.tsx
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
65d68f5
Raw History Blame Contribute Delete
20.3 kB
"use client";
import { useMemo } from "react";
import type { CoursePlanResult } from "./learnAnythingTypes";
interface CoursePlanCardProps {
coursePlan: CoursePlanResult;
}
/**
* Display the backend-generated course plan.
* Must show all available fields from CoursePlanResult.
* Must respect truth rules: no fake claims, honest source labels.
*/
export function CoursePlanCard({ coursePlan }: CoursePlanCardProps) {
const hasUploadedSource = coursePlan.data_source_label === "Uploaded source";
const contextItems = [
["Class", coursePlan.student_context.class_level],
["Syllabus", coursePlan.student_context.syllabus],
["Subject", coursePlan.student_context.subject],
["Chapter", coursePlan.student_context.chapter],
["Exam", coursePlan.student_context.exam_date],
["Daily time", coursePlan.student_context.daily_study_time],
["Goal", coursePlan.student_context.goal],
].filter(([, value]) => value);
// Format duration
const formatDuration = (minutes: number) => {
if (minutes < 60) return `${minutes} min`;
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
if (remainingMinutes === 0) return hours === 1 ? "1 hour" : `${hours} hours`;
return hours === 1 ? `1 hour ${remainingMinutes} min` : `${hours} hours ${remainingMinutes} min`;
};
// Format course type label
const courseTypeLabel = useMemo(() => {
if (coursePlan.source_basis === "source_upload") return "Source-backed course";
if (coursePlan.source_basis === "catalog_seed") return "Catalog-based course";
if (coursePlan.source_basis === "syllabus") return "Syllabus-based course";
return "Starter course plan";
}, [coursePlan.source_basis]);
return (
<div className="w-full rounded-2xl border border-white/15 bg-white/[0.03] overflow-hidden">
{/* Header */}
<div className="border-b border-white/10 p-6">
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h2 className="text-xl font-semibold text-white line-clamp-2">{coursePlan.title}</h2>
{/* Source Badge */}
<span className={`text-xs font-medium px-2 py-0.5 rounded-full ${
hasUploadedSource
? "bg-green-500/10 text-green-400 border border-green-500/20"
: "bg-white/10 text-white/40 border border-white/15"
}`}>
{coursePlan.data_source_label}
</span>
</div>
<div className="flex flex-wrap items-center gap-4 text-sm">
<span className="text-white/70">
Level: <span className="text-white">{coursePlan.learner_level || "General"}</span>
</span>
<span className="text-white/70">
Subject: <span className="text-white">{coursePlan.subject_area || "General"}</span>
</span>
<span className="text-white/70">
Total: <span className="text-white">{coursePlan.lessons_total} lessons</span> / {formatDuration(coursePlan.estimated_total_minutes)}
</span>
{coursePlan.estimated_study_time && (
<span className="text-white/70">
Study time: <span className="text-white">{coursePlan.estimated_study_time}</span>
</span>
)}
</div>
{/* Trust Notes - always visible if present */}
{coursePlan.trust_notes && coursePlan.trust_notes.length > 0 && (
<div className="mt-4 flex flex-wrap gap-2">
{coursePlan.trust_notes.map((note, index) => (
<span
key={index}
className="text-xs text-white/50 bg-white/5 px-2 py-1 rounded-lg border border-white/10"
>
{note}
</span>
))}
</div>
)}
</div>
</div>
</div>
{/* Main Content */}
<div className="p-6 space-y-6">
{/* Confidence */}
{coursePlan.confidence > 0 && (
<div className="flex items-center gap-2 text-sm">
<span className="text-white/60">Confidence:</span>
<div className="flex items-center gap-1">
<div className="w-20 h-2 bg-white/10 rounded-full overflow-hidden">
<div
className="h-full bg-white/30 rounded-full transition-all"
style={{ width: `${Math.min(coursePlan.confidence * 100, 100)}%` }}
/>
</div>
<span className="text-white/70 text-xs">{(coursePlan.confidence * 100).toFixed(0)}%</span>
</div>
</div>
)}
{contextItems.length > 0 && (
<section>
<h3 className="text-sm font-medium text-white/70 mb-2 uppercase tracking-wider">
Student Context
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{contextItems.map(([label, value]) => (
<div key={label} className="rounded-lg border border-white/10 bg-white/[0.02] px-3 py-2">
<p className="text-[11px] uppercase tracking-wider text-white/35">{label}</p>
<p className="text-sm text-white/80">{value}</p>
</div>
))}
</div>
</section>
)}
{coursePlan.lesson_outline.length > 0 && (
<section>
<h3 className="text-sm font-medium text-white/70 mb-2 uppercase tracking-wider">
Lesson Outline
</h3>
<div className="space-y-2">
{coursePlan.lesson_outline.map((item, index) => (
<div key={`${item}-${index}`} className="flex items-start gap-3 rounded-lg border border-white/10 bg-white/[0.02] p-3">
<span className="mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-white/15 text-xs text-white/50">
{index + 1}
</span>
<p className="text-sm text-white/78">{item}</p>
</div>
))}
</div>
</section>
)}
{coursePlan.prerequisite_check.length > 0 && (
<section>
<h3 className="text-sm font-medium text-white/70 mb-2 uppercase tracking-wider">
Prerequisite Check
</h3>
<ul className="space-y-2">
{coursePlan.prerequisite_check.map((item, index) => (
<li key={`${item}-${index}`} className="rounded-lg border border-white/10 bg-white/[0.02] px-3 py-2 text-sm text-white/78">
{item}
</li>
))}
</ul>
</section>
)}
{coursePlan.concept_explanation && (
<section>
<h3 className="text-sm font-medium text-white/70 mb-2 uppercase tracking-wider">
Concept Explanation
</h3>
<div className="rounded-xl border border-white/10 bg-white/[0.02] p-4">
<p className="text-base font-semibold text-white">{coursePlan.concept_explanation.title}</p>
<p className="mt-2 text-sm leading-6 text-white/78">{coursePlan.concept_explanation.explanation}</p>
{coursePlan.concept_explanation.board_exam_focus && (
<p className="mt-3 rounded-lg bg-white/[0.04] px-3 py-2 text-xs leading-5 text-white/60">
{coursePlan.concept_explanation.board_exam_focus}
</p>
)}
</div>
</section>
)}
{coursePlan.derivation_or_problem_steps.length > 0 && (
<section>
<h3 className="text-sm font-medium text-white/70 mb-2 uppercase tracking-wider">
Derivation / Problem Steps
</h3>
<div className="space-y-3">
{coursePlan.derivation_or_problem_steps.map((item) => (
<div key={item.title} className="rounded-xl border border-white/10 bg-white/[0.02] p-4">
<p className="text-sm font-semibold text-white">{item.title}</p>
<ol className="mt-3 space-y-2">
{item.steps.map((step, index) => (
<li key={step} className="flex gap-3 text-sm text-white/78">
<span className="text-white/35">{index + 1}.</span>
<span>{step}</span>
</li>
))}
</ol>
{item.common_mistake && (
<p className="mt-3 text-xs text-white/45">Common mark loss: {item.common_mistake}</p>
)}
</div>
))}
</div>
</section>
)}
{/* Prerequisites */}
{coursePlan.prerequisites && coursePlan.prerequisites.length > 0 && (
<section>
<h3 className="text-sm font-medium text-white/70 mb-2 uppercase tracking-wider">
Prerequisites
</h3>
<div className="flex flex-wrap gap-2">
{coursePlan.prerequisites.map((prereq, index) => (
<span
key={index}
className="text-sm text-white/80 bg-white/5 px-3 py-1 rounded-lg border border-white/10"
>
{prereq}
</span>
))}
</div>
</section>
)}
{/* Final Outcome */}
{coursePlan.final_outcome && (
<section>
<h3 className="text-sm font-medium text-white/70 mb-2 uppercase tracking-wider">
Final Outcome
</h3>
<p className="text-sm text-white/80">{coursePlan.final_outcome}</p>
</section>
)}
{/* Next Action */}
{coursePlan.next_action && (
<section>
<h3 className="text-sm font-medium text-white/70 mb-2 uppercase tracking-wider">
Next Action
</h3>
<p className="text-sm text-white/80">{coursePlan.next_action}</p>
</section>
)}
{/* Key Concepts */}
{coursePlan.key_concepts && coursePlan.key_concepts.length > 0 && (
<section>
<h3 className="text-sm font-medium text-white/70 mb-2 uppercase tracking-wider">
Key Concepts
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{coursePlan.key_concepts.map((concept, index) => (
<span
key={index}
className="text-sm text-white/80 bg-white/5 px-3 py-2 rounded-lg border border-white/10"
>
{concept}
</span>
))}
</div>
</section>
)}
{coursePlan.practice_questions.length > 0 && (
<QuestionSection title="Practice Questions" questions={coursePlan.practice_questions} />
)}
{coursePlan.pyq_style_questions.length > 0 && (
<QuestionSection title="PYQ-Style Questions" questions={coursePlan.pyq_style_questions} />
)}
{coursePlan.weak_topic_repairs.length > 0 && (
<section>
<h3 className="text-sm font-medium text-white/70 mb-2 uppercase tracking-wider">
Weak Topic Repair
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{coursePlan.weak_topic_repairs.map((repair) => (
<div key={repair.topic} className="rounded-lg border border-white/10 bg-white/[0.02] p-3">
<p className="text-sm font-medium text-white">{repair.topic}</p>
<p className="mt-1 text-xs leading-5 text-white/50">{repair.symptom}</p>
<p className="mt-2 text-xs leading-5 text-white/70">{repair.repair_task}</p>
</div>
))}
</div>
</section>
)}
{coursePlan.revision_plan.length > 0 && (
<section>
<h3 className="text-sm font-medium text-white/70 mb-2 uppercase tracking-wider">
Revision Plan
</h3>
<div className="space-y-2">
{coursePlan.revision_plan.map((item) => (
<div key={`${item.timing}-${item.task}`} className="rounded-lg border border-white/10 bg-white/[0.02] p-3">
<div className="flex flex-wrap items-center justify-between gap-2">
<p className="text-sm font-medium text-white">{item.timing}</p>
<span className="text-xs text-white/45">{item.estimated_minutes} min</span>
</div>
<p className="mt-1 text-sm text-white/78">{item.task}</p>
<p className="mt-1 text-xs text-white/45">{item.purpose}</p>
</div>
))}
</div>
</section>
)}
{/* Modules - Main Content */}
{coursePlan.modules && coursePlan.modules.length > 0 && (
<section>
<h3 className="text-sm font-medium text-white mb-2 uppercase tracking-wider">
Course Modules
</h3>
<div className="space-y-4">
{coursePlan.modules.map((module, moduleIndex) => (
<div
key={moduleIndex}
className="rounded-xl border border-white/10 bg-white/[0.02] p-4"
>
<div className="flex items-start gap-3 mb-3">
<span className="flex-shrink-0 w-8 h-8 flex items-center justify-center rounded-full border border-white/15 bg-white/[0.05] text-sm text-white/50 font-medium">
{moduleIndex + 1}
</span>
<div className="flex-1">
<h4 className="text-lg font-semibold text-white">{module.title}</h4>
{module.estimated_minutes > 0 && (
<p className="text-sm text-white/50">
~{formatDuration(module.estimated_minutes)}
</p>
)}
<p className="text-xs text-white/40 mt-1">
Source: {module.source_basis}
</p>
</div>
</div>
{/* Module Lessons */}
<div className="space-y-2 ml-11">
{module.lessons.map((lesson, lessonIndex) => (
<div
key={lessonIndex}
className="flex items-center gap-3 text-sm"
>
<span className="w-2 h-2 rounded-full bg-white/20 flex-shrink-0" />
<div className="flex-1">
<p className="text-white/90">{lesson.title}</p>
<p className="text-xs text-white/40 line-clamp-1">{lesson.task}</p>
</div>
<span className="text-xs text-white/50 flex-shrink-0">
{lesson.duration_minutes} min
</span>
</div>
))}
</div>
</div>
))}
</div>
</section>
)}
{/* Practice Tasks */}
{coursePlan.practice_tasks && coursePlan.practice_tasks.length > 0 && (
<section>
<h3 className="text-sm font-medium text-white/70 mb-2 uppercase tracking-wider">
Practice Tasks
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{coursePlan.practice_tasks.map((task, index) => (
<div
key={index}
className="rounded-lg border border-white/10 bg-white/[0.02] p-3"
>
<p className="text-sm font-medium text-white">{task.title}</p>
<p className="text-xs text-white/50 mt-1">{task.description || task.task_type}</p>
<p className="text-xs text-white/40 mt-1">Type: {task.task_type}</p>
</div>
))}
</div>
</section>
)}
{/* Quizzes */}
{coursePlan.quizzes && coursePlan.quizzes.length > 0 && (
<section>
<h3 className="text-sm font-medium text-white/70 mb-2 uppercase tracking-wider">
Quizzes
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{coursePlan.quizzes.map((quiz, index) => (
<div
key={index}
className="rounded-lg border border-white/10 bg-white/[0.02] p-3"
>
<p className="text-sm font-medium text-white">{quiz.title}</p>
<p className="text-xs text-white/50 mt-1">
{quiz.question_count} questions
</p>
{quiz.focus_areas && quiz.focus_areas.length > 0 && (
<p className="text-xs text-white/40 mt-1 line-clamp-1">
Focus: {quiz.focus_areas.join(", ")}
</p>
)}
</div>
))}
</div>
</section>
)}
{/* Revision Checkpoints */}
{coursePlan.revision_checkpoints && coursePlan.revision_checkpoints.length > 0 && (
<section>
<h3 className="text-sm font-medium text-white/70 mb-2 uppercase tracking-wider">
Revision Checkpoints
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{coursePlan.revision_checkpoints.map((checkpoint, index) => (
<div
key={index}
className="rounded-lg border border-white/10 bg-white/[0.02] p-3"
>
<p className="text-sm font-medium text-white">{checkpoint.title}</p>
<p className="text-xs text-white/50 mt-1">
{checkpoint.estimated_minutes} minutes
</p>
{checkpoint.focus_areas && checkpoint.focus_areas.length > 0 && (
<p className="text-xs text-white/40 mt-1 line-clamp-1">
Focus: {checkpoint.focus_areas.join(", ")}
</p>
)}
</div>
))}
</div>
</section>
)}
</div>
{/* Footer */}
<div className="border-t border-white/10 p-4">
<p className="text-xs text-white/40 text-center">
Generated by DocDoe AI | {courseTypeLabel}
{!hasUploadedSource && " | No uploaded source used"}
</p>
</div>
</div>
);
}
function QuestionSection({
title,
questions,
}: {
title: string;
questions: CoursePlanResult["practice_questions"];
}) {
return (
<section>
<h3 className="text-sm font-medium text-white/70 mb-2 uppercase tracking-wider">
{title}
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{questions.map((question) => (
<div key={`${question.question}-${question.marks}`} className="rounded-lg border border-white/10 bg-white/[0.02] p-3">
<div className="mb-2 flex items-center justify-between gap-2">
<span className="rounded-full border border-white/10 px-2 py-0.5 text-[11px] text-white/45">
{question.marks} marks
</span>
<span className="text-[11px] uppercase tracking-wider text-white/35">
{question.question_type.replaceAll("_", " ")}
</span>
</div>
<p className="text-sm font-medium leading-5 text-white">{question.question}</p>
{question.answer_hint && (
<p className="mt-2 text-xs leading-5 text-white/50">{question.answer_hint}</p>
)}
</div>
))}
</div>
</section>
);
}