DocDoeAI / src /components /library /ProgressView.tsx
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
65d68f5
Raw History Blame Contribute Delete
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>
);
}