88a1dab810
---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).
295 lines
11 KiB
TypeScript
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'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>
|
|
);
|
|
} |