Download src/components/library/ProgressView.tsx from asnannp/DocDoeAI: direct link, hf CLI and curl.
- Browser
- Download file 10.7 kB
-
https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/library/ProgressView.tsx
- Command line
-
hf download hf://spaces/asnannp/DocDoeAI/src/components/library/ProgressView.tsx
-
curl -L -o ProgressView.tsx https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/library/ProgressView.tsx
10.7 kB
| "use client"; | |
| import { useEffect, useMemo, useState } from "react"; | |
| import Link from "next/link"; | |
| import { | |
| BookOpen, | |
| Brain, | |
| CheckCircle2, | |
| FileText, | |
| LineChart, | |
| MessageSquareText, | |
| Play, | |
| Target, | |
| } from "lucide-react"; | |
| import { hydratePhase3Activity, readPhase3Activity, type Phase3Activity } from "@/lib/phase3-learning"; | |
| import { | |
| buildStudentProgressSummary, | |
| listActivities, | |
| type ActivityEvent, | |
| } from "@/lib/student-activity"; | |
| import { getLocalStudentContext, loadStudentContext, type StudentContext } from "@/lib/student-context"; | |
| import { useStudentActivityVersion } from "@/hooks/useStudentActivity"; | |
| // ============================================================================= | |
| // Truthful progress: every number traces to something the student actually did | |
| // (the student-activity ledger) or real weak-topic evidence (phase3 aggregate, | |
| // fed by quiz mistakes). No sample metrics. Extracted from the /progress route | |
| // so /library can compose it as a tab. | |
| // ============================================================================= | |
| const KIND_META: Record<ActivityEvent["kind"], { label: string; icon: typeof Play }> = { | |
| question_asked: { label: "Asked in StudyChat", icon: MessageSquareText }, | |
| lesson_started: { label: "Lesson started", icon: Play }, | |
| lesson_completed: { label: "Lesson completed", icon: CheckCircle2 }, | |
| note_saved: { label: "Note saved", icon: FileText }, | |
| quiz_attempted: { label: "Quiz attempted", icon: Brain }, | |
| demo_class_completed: { label: "Demo class completed", icon: CheckCircle2 }, | |
| }; | |
| export function ProgressView() { | |
| const [events, setEvents] = useState<ActivityEvent[]>([]); | |
| const [phase3, setPhase3] = useState<Phase3Activity>({}); | |
| const [ctx, setCtx] = useState<StudentContext>(() => getLocalStudentContext()); | |
| // Live token: re-read the ledger whenever activity changes (this tab or another). | |
| const activityVersion = useStudentActivityVersion(); | |
| useEffect(() => { | |
| let cancelled = false; | |
| void Promise.resolve().then(() => { | |
| if (cancelled) return; | |
| setEvents(listActivities(30)); | |
| setPhase3(readPhase3Activity()); | |
| }); | |
| return () => { | |
| cancelled = true; | |
| }; | |
| }, [activityVersion]); | |
| useEffect(() => { | |
| let cancelled = false; | |
| void loadStudentContext().then((merged) => { | |
| if (!cancelled) setCtx(merged); | |
| }); | |
| // Pull the saved weak-topic aggregate from the backend on fresh devices. | |
| void hydratePhase3Activity().then((remote) => { | |
| if (!cancelled && remote) setPhase3(readPhase3Activity()); | |
| }); | |
| return () => { | |
| cancelled = true; | |
| }; | |
| }, []); | |
| const summary = useMemo(() => buildStudentProgressSummary(events, phase3), [events, phase3]); | |
| return ( | |
| <div className="text-white"> | |
| <header className="mb-8"> | |
| <h2 className="text-2xl font-semibold text-white sm:text-3xl">Study progress</h2> | |
| <p className="mt-2 max-w-3xl text-sm leading-6 text-white/52"> | |
| Built only from what you actually did β questions, lessons, notes, and quiz results on this device. | |
| </p> | |
| {ctx.onboardingComplete ? ( | |
| <div className="mt-3 space-y-1 text-[12px] text-white/55"> | |
| <p className="inline-flex items-center gap-1.5"> | |
| <CheckCircle2 className="size-3.5 text-white/70" /> | |
| Onboarding completed{ctx.classLevel ? ` Β· ${ctx.classLevel}` : ""}{ctx.board ? ` Β· ${ctx.board}` : ""} | |
| </p> | |
| <p>{ctx.synced ? "Account profile synced." : "Local profile active Β· account sync unavailable."}</p> | |
| </div> | |
| ) : ( | |
| <p className="mt-3 text-[12px] text-white/55"> | |
| <Link href="/onboarding" className="underline hover:text-white"> | |
| Complete onboarding | |
| </Link>{" "} | |
| to set your study focus. | |
| </p> | |
| )} | |
| </header> | |
| {!summary.hasActivity ? ( | |
| <div className="rounded-2xl border border-dashed border-white/[0.12] bg-black/30 p-10 text-center"> | |
| <LineChart className="mx-auto mb-4 size-8 text-white/40" /> | |
| <p className="text-base font-semibold text-white"> | |
| Start a lesson, ask a question, or take a quiz to build your progress. | |
| </p> | |
| <div className="mt-6 flex flex-wrap justify-center gap-2"> | |
| <Link href="/learn-anything" className="inline-flex rounded-xl bg-white px-5 py-2.5 text-sm font-semibold text-black"> | |
| Start a lesson | |
| </Link> | |
| <Link href="/study-chat" className="inline-flex rounded-xl border border-white/[0.14] px-5 py-2.5 text-sm font-semibold text-white/75 hover:text-white"> | |
| Ask a question | |
| </Link> | |
| <Link href="/library?tab=quizzes" className="inline-flex rounded-xl border border-white/[0.14] px-5 py-2.5 text-sm font-semibold text-white/75 hover:text-white"> | |
| Take a quiz | |
| </Link> | |
| </div> | |
| </div> | |
| ) : ( | |
| <> | |
| <div className="dd-stagger grid gap-5 sm:grid-cols-2 lg:grid-cols-4"> | |
| {summary.metrics.map((metric) => ( | |
| <div key={metric.label} className="rounded-2xl border border-white/[0.08] bg-[#0B0B0B] p-5"> | |
| <p className="text-[10px] font-bold uppercase tracking-[0.14em] text-white/34">{metric.label}</p> | |
| <p className="mt-3 text-2xl font-semibold tabular-nums text-white">{metric.value}</p> | |
| </div> | |
| ))} | |
| </div> | |
| {summary.sampleActivityCount > 0 ? ( | |
| <p className="mt-4 rounded-xl border border-white/[0.08] bg-white/[0.03] px-4 py-3 text-xs text-white/55"> | |
| Demo activity is labelled below and does not represent full tuition progress. | |
| </p> | |
| ) : null} | |
| <div className="mt-6 grid gap-5 lg:grid-cols-[minmax(0,1fr)_320px]"> | |
| <div className="space-y-5"> | |
| {/* ββ Recent activity (real ledger) ββ */} | |
| <section className="rounded-2xl border border-white/[0.08] bg-[#0B0B0B] p-5"> | |
| <h3 className="mb-4 text-lg font-semibold text-white">Recent activity</h3> | |
| <div className="space-y-2.5"> | |
| {summary.recentActivities.slice(0, 10).map((event) => { | |
| const meta = KIND_META[event.kind]; | |
| const Icon = meta.icon; | |
| return ( | |
| <div key={event.id} className="flex items-center gap-3 rounded-xl border border-white/[0.06] bg-black/25 px-4 py-3"> | |
| <Icon className="size-4 shrink-0 text-white/50" /> | |
| <div className="min-w-0 flex-1"> | |
| <p className="truncate text-sm font-medium text-white/85">{event.title}</p> | |
| <p className="text-[11px] text-white/40"> | |
| {meta.label} | |
| {event.detail ? ` Β· ${event.detail}` : ""} | |
| {event.sample ? " Β· demo/sample" : ""} Β· {new Date(event.at).toLocaleString()} | |
| </p> | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| {summary.recentActivities.length === 0 ? ( | |
| <p className="text-sm text-white/50">No activity recorded yet.</p> | |
| ) : null} | |
| </div> | |
| </section> | |
| {/* ββ Weak areas (evidence only) ββ */} | |
| <section className="rounded-2xl border border-white/[0.08] bg-[#0B0B0B] p-5"> | |
| <div className="mb-3 flex items-center gap-2"> | |
| <Target className="size-5 text-white/56" /> | |
| <h3 className="text-lg font-semibold text-white">Weak topics</h3> | |
| </div> | |
| {summary.weakTopics.length > 0 ? ( | |
| <div className="grid gap-3 sm:grid-cols-2"> | |
| {summary.weakTopics.map((topic) => ( | |
| <div key={topic} className="rounded-xl border border-white/[0.07] bg-black/25 p-4"> | |
| <p className="text-sm font-semibold text-white">{topic}</p> | |
| <p className="mt-1 text-xs leading-5 text-white/44">From a missed quiz question or saved feedback.</p> | |
| </div> | |
| ))} | |
| </div> | |
| ) : ( | |
| <p className="text-sm text-white/50"> | |
| No weak topics yet β they appear here when you miss a quiz question. | |
| </p> | |
| )} | |
| </section> | |
| </div> | |
| <aside className="space-y-5"> | |
| {/* ββ Revision queue ββ */} | |
| <section className="rounded-2xl border border-white/[0.08] bg-[#0B0B0B] p-5"> | |
| <h3 className="mb-3 text-lg font-semibold text-white">Revision queue</h3> | |
| {summary.revisionQueue.length > 0 ? ( | |
| <div className="space-y-2"> | |
| {summary.revisionQueue.map((topic) => ( | |
| <p key={topic} className="rounded-lg bg-white/[0.03] px-3 py-2 text-sm text-white/70"> | |
| {topic} | |
| </p> | |
| ))} | |
| </div> | |
| ) : ( | |
| <p className="text-sm text-white/50">Nothing saved for revision yet.</p> | |
| )} | |
| </section> | |
| {/* ββ Next actions ββ */} | |
| <section className="rounded-2xl border border-white/[0.08] bg-[#0B0B0B] p-5"> | |
| <div className="mb-4 flex items-center gap-2"> | |
| <BookOpen className="size-5 text-white/56" /> | |
| <h3 className="text-lg font-semibold text-white">Next actions</h3> | |
| </div> | |
| <div className="space-y-2"> | |
| {[ | |
| { label: "Take a quiz", href: "/library?tab=quizzes" }, | |
| { label: "Review your notes", href: "/library?tab=notes" }, | |
| { label: "Ask in Study Chat", href: "/study-chat" }, | |
| ].map((action) => ( | |
| <Link | |
| key={action.href} | |
| href={action.href} | |
| className="dd-lift dd-press flex min-h-11 items-center justify-between rounded-xl border border-white/[0.08] bg-white/[0.03] px-3 text-sm font-semibold text-white/64 hover:text-white" | |
| > | |
| {action.label} | |
| <span aria-hidden="true">β</span> | |
| </Link> | |
| ))} | |
| </div> | |
| </section> | |
| </aside> | |
| </div> | |
| </> | |
| )} | |
| </div> | |
| ); | |
| } | |