Files
nextcraft/apps/web/app/(learner)/dashboard/page.tsx
T
CIAgent 88a1dab810 docs(milestone): complete v0.2-ai-tutor-architecture
---ci---
phase: 7
milestone: v0.2
status: complete
requirements:
  covered: [REQ-2-001, REQ-2-002, REQ-2-003, REQ-2-004, REQ-2-005, REQ-2-006, REQ-2-007, REQ-2-008, REQ-2-009, REQ-2-010, REQ-2-011, REQ-2-012]
  partial: []
---/ci---

Milestone v0.2 (ai-tutor-architecture) merged to main.

Escalation record (audit remediation, durable): P1 executor
delegation failed twice (empty subagent results, zero files
created); auto-resolved at full autonomy to inline execution with
identical plan fidelity (commit 3271373, reflog-only after phase
branch squash-delete).
2026-09-11 17:34:50 +00:00

295 lines
11 KiB
TypeScript

import Link from 'next/link';
import {
ArrowRight,
Award,
Calendar,
Code,
FileText,
PenTool,
Sparkles,
Bot,
} from 'lucide-react';
import { Card, CardBody, CardHeader, Badge, Avatar, Button } from '@nextcraft/ui';
import {
primaryLearner,
competencyStacks,
allCompetencies,
learnerArtifacts,
upcomingDefenses,
learnerMicrocredentials,
} from '@nextcraft/mock-data';
import { AiTutorChat } from '../../../components/learner/ai-tutor-chat';
import { MentorPanel } from '../../../components/learner/mentor-panel';
import { ProgressGraph } from '../../../components/learner/progress-graph';
const ACTIVE_COMPETENCY_IDS = [
'stack-orchestration-c003',
'stack-orchestration-c008',
'stack-safety-c019',
'stack-safety-c023',
];
const ACTIVE_PROGRESS: Record<string, number> = {
'stack-orchestration-c003': 45,
'stack-orchestration-c008': 38,
'stack-safety-c019': 52,
'stack-safety-c023': 30,
};
const ARTIFACT_ICON = {
code: Code,
design: PenTool,
simulation: Sparkles,
document: FileText,
} as const;
function formatDate(iso: string): string {
const d = new Date(iso);
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
}
export default function DashboardPage() {
const learner = primaryLearner;
const activeStack = competencyStacks.find((s) => s.id === 'stack-orchestration')!;
const activeCompetencies = ACTIVE_COMPETENCY_IDS
.map((id) => allCompetencies.find((c) => c.id === id))
.filter((c): c is NonNullable<typeof c> => Boolean(c));
const stackProgress = learner.progress[activeStack.id] ?? 0;
const masteredInStack = activeStack.competencies.filter((c) => c.status === 'mastered').length;
const totalInStack = activeStack.competencies.length;
const milestones = Array.from({ length: totalInStack }, (_, i) => i < masteredInStack);
return (
<div className="flex flex-col gap-6">
{/* Header */}
<header className="flex items-center gap-4">
<Avatar name={learner.name} src={learner.avatar} size="lg" />
<div>
<h1 className="text-2xl font-bold tracking-tight text-slate-900 dark:text-slate-100">
Welcome back, {learner.name.split(' ')[0]}
</h1>
<p className="text-sm text-slate-500 dark:text-slate-400">
You&apos;re {stackProgress}% through the {activeStack.name} stack.
</p>
</div>
</header>
{/* Top row: active competencies + milestone tracker */}
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
{/* Active competencies (spans 2) */}
<Card className="lg:col-span-2">
<CardHeader>
<h2 className="text-base font-semibold text-slate-900 dark:text-slate-100">
Active competencies
</h2>
<p className="text-sm text-slate-500 dark:text-slate-400">
{activeStack.name}
</p>
</CardHeader>
<CardBody className="flex flex-col gap-4">
{activeCompetencies.map((c) => {
const pct = ACTIVE_PROGRESS[c.id] ?? 0;
return (
<div key={c.id} className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<Link
href={`/learn/${c.id}`}
className="text-sm font-medium text-slate-800 hover:text-primary-700 dark:text-slate-100 dark:hover:text-primary-300"
>
{c.name}
</Link>
<span className="text-xs text-slate-500 dark:text-slate-400">{pct}%</span>
</div>
<div className="h-2 overflow-hidden rounded-full bg-slate-100 dark:bg-slate-800">
<div
className="h-full rounded-full bg-amber-500"
style={{ width: `${pct}%` }}
/>
</div>
</div>
);
})}
</CardBody>
</Card>
{/* Milestone tracker */}
<Card>
<CardHeader>
<h2 className="text-base font-semibold text-slate-900 dark:text-slate-100">
Milestone tracker
</h2>
<p className="text-sm text-slate-500 dark:text-slate-400">
{masteredInStack}/{totalInStack} competencies mastered
</p>
</CardHeader>
<CardBody>
<ol className="flex flex-wrap gap-2">
{milestones.map((done, i) => (
<li
key={i}
className={`flex h-7 w-7 items-center justify-center rounded-full text-xs font-medium ${
done
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300'
: 'bg-slate-100 text-slate-400 dark:bg-slate-800 dark:text-slate-500'
}`}
title={done ? 'Mastered' : 'Not yet mastered'}
>
{done ? <Award className="h-3.5 w-3.5" /> : i + 1}
</li>
))}
</ol>
<div className="mt-4">
<div className="h-2 overflow-hidden rounded-full bg-slate-100 dark:bg-slate-800">
<div
className="h-full rounded-full bg-accent-500"
style={{ width: `${stackProgress}%` }}
/>
</div>
<p className="mt-1 text-xs text-slate-500 dark:text-slate-400">
{stackProgress}% overall stack progress
</p>
</div>
</CardBody>
</Card>
</div>
{/* Progress graph (full width) */}
<Card>
<CardHeader>
<h2 className="text-base font-semibold text-slate-900 dark:text-slate-100">
Mastery progress
</h2>
<p className="text-sm text-slate-500 dark:text-slate-400">
Last 8 weeks · AI Orchestration stack
</p>
</CardHeader>
<CardBody>
<ProgressGraph />
</CardBody>
</Card>
{/* Bottom row: recent artifacts + upcoming defenses */}
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
{/* Recent artifacts */}
<Card>
<CardHeader>
<h2 className="text-base font-semibold text-slate-900 dark:text-slate-100">
Recent artifacts
</h2>
<p className="text-sm text-slate-500 dark:text-slate-400">
{learnerArtifacts.length} submitted
</p>
</CardHeader>
<CardBody className="flex flex-col gap-3">
{learnerArtifacts.slice(0, 4).map((a) => {
const Icon = ARTIFACT_ICON[a.type] ?? FileText;
return (
<div
key={a.id}
className="flex items-start gap-3 rounded-md border border-slate-200 p-3 dark:border-slate-800"
>
<span className="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-primary-100 text-primary-700 dark:bg-primary-900/40 dark:text-primary-300">
<Icon className="h-4 w-4" />
</span>
<div className="flex-1">
<div className="flex items-center justify-between gap-2">
<p className="text-sm font-medium text-slate-900 dark:text-slate-100">
{a.name}
</p>
<span className="text-xs text-slate-400">{formatDate(a.createdAt)}</span>
</div>
<p className="mt-0.5 text-xs text-slate-500 dark:text-slate-400">
{a.type}
</p>
</div>
</div>
);
})}
</CardBody>
</Card>
{/* Upcoming defenses */}
<Card>
<CardHeader>
<h2 className="text-base font-semibold text-slate-900 dark:text-slate-100">
Upcoming defenses
</h2>
<p className="text-sm text-slate-500 dark:text-slate-400">
Oral exams on the calendar
</p>
</CardHeader>
<CardBody className="flex flex-col gap-3">
{upcomingDefenses.map((d) => {
const comp = allCompetencies.find((c) => c.id === d.competencyId);
if (!comp) return null;
return (
<div
key={d.id}
className="flex items-center justify-between gap-3 rounded-md border border-slate-200 p-3 dark:border-slate-800"
>
<div className="flex items-start gap-3">
<span className="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300">
<Calendar className="h-4 w-4" />
</span>
<div>
<p className="text-sm font-medium text-slate-900 dark:text-slate-100">
{comp.name}
</p>
<p className="text-xs text-slate-500 dark:text-slate-400">
{d.status === 'scheduled' ? 'Scheduled' : 'Pending scheduling'}
</p>
</div>
</div>
<Link href={`/defend/${comp.id}`}>
<Button size="sm" variant="outline">
Prepare
</Button>
</Link>
</div>
);
})}
</CardBody>
</Card>
</div>
{/* AI Tutor chat */}
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<span className="flex h-8 w-8 items-center justify-center rounded-full bg-primary-100 text-primary-700 dark:bg-primary-900/40 dark:text-primary-300">
<Bot className="h-4 w-4" />
</span>
<div>
<h2 className="text-base font-semibold text-slate-900 dark:text-slate-100">
AI Tutor
</h2>
<p className="text-sm text-slate-500 dark:text-slate-400">
Coach and Socratic tutor · live streaming
</p>
</div>
</div>
</CardHeader>
<CardBody>
<AiTutorChat />
</CardBody>
</Card>
{/* Mentor — long-horizon career narrative */}
<Card>
<CardHeader>
<h2 className="text-base font-semibold text-slate-900 dark:text-slate-100">
Mentor
</h2>
<p className="text-sm text-slate-500 dark:text-slate-400">
Long-horizon career trajectory · live streaming
</p>
</CardHeader>
<CardBody>
<MentorPanel />
</CardBody>
</Card>
</div>
);
}