Download src/components/learn-anything/CoursePlanCard.tsx from asnannp/DocDoeAI: direct link, hf CLI and curl.
- Browser
- Download file 20.3 kB
-
https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/learn-anything/CoursePlanCard.tsx
- Command line
-
hf download hf://spaces/asnannp/DocDoeAI/src/components/learn-anything/CoursePlanCard.tsx
-
curl -L -o CoursePlanCard.tsx https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/learn-anything/CoursePlanCard.tsx
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> | |
| ); | |
| } | |