feat: implement curriculum management system including automated generation, enrichment, and versioning workflows
This commit is contained in:
@@ -178,8 +178,8 @@ const Admin = () => {
|
||||
|
||||
{activeTab === 'curriculum' && (
|
||||
<div className="animate-in fade-in duration-300 max-w-5xl mx-auto">
|
||||
<h1 className="text-3xl text-teal mb-2">Annual Curriculum</h1>
|
||||
<p className="text-fg-muted mb-8">Plan and manage the 52-week learning schedule. All employees follow the same weekly topic.</p>
|
||||
<h1 className="text-3xl text-teal mb-2">Curriculum</h1>
|
||||
<p className="text-fg-muted mb-8">AI-generated 26-week learning cycle. Review and activate curriculum versions.</p>
|
||||
<CurriculumManager />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -6,7 +6,7 @@ import Button from '../components/ui/Button';
|
||||
import Tag from '../components/ui/Tag';
|
||||
import * as db from '../lib/db';
|
||||
import { getAssignedTopic } from '../lib/learningService';
|
||||
import { getYearProgress, getQuarterName, getQuarterForWeek, hasCurriculum as checkHasCurriculum } from '../lib/curriculumService';
|
||||
import { getYearProgress, getCurriculumCycle, getCurriculumWeek, getActiveVersion } from '../lib/curriculumService';
|
||||
|
||||
const Dashboard = () => {
|
||||
const { state } = useApp();
|
||||
@@ -22,6 +22,7 @@ const Dashboard = () => {
|
||||
activity: [],
|
||||
yearProgress: null,
|
||||
hasCurriculum: false,
|
||||
theme: '',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -57,23 +58,35 @@ const Dashboard = () => {
|
||||
let yearProgress = null;
|
||||
let curriculumExists = false;
|
||||
try {
|
||||
curriculumExists = await checkHasCurriculum();
|
||||
const activeVersion = await getActiveVersion();
|
||||
curriculumExists = !!activeVersion;
|
||||
if (curriculumExists) {
|
||||
yearProgress = await getYearProgress(currentUser.id);
|
||||
yearProgress = await getYearProgress(currentUser.id, weekNumber);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Dashboard] Could not load curriculum data:', e.message);
|
||||
}
|
||||
|
||||
setDashData({ topic, learnDone, testResult, top3, myRank, myPoints, activity, yearProgress, hasCurriculum: curriculumExists });
|
||||
setDashData({
|
||||
topic,
|
||||
learnDone,
|
||||
testResult,
|
||||
top3,
|
||||
myRank,
|
||||
myPoints,
|
||||
activity,
|
||||
yearProgress,
|
||||
hasCurriculum: curriculumExists,
|
||||
theme: topic?.theme || '',
|
||||
});
|
||||
};
|
||||
|
||||
load();
|
||||
}, [currentUser, weekNumber]);
|
||||
|
||||
const { topic, learnDone, testResult, top3, myRank, myPoints, activity, yearProgress, hasCurriculum: curriculumActive } = dashData;
|
||||
const currentQuarter = getQuarterForWeek(weekNumber);
|
||||
const quarterName = getQuarterName(weekNumber);
|
||||
const { topic, learnDone, testResult, top3, myRank, myPoints, activity, yearProgress, hasCurriculum: curriculumActive, theme } = dashData;
|
||||
const currentCycle = getCurriculumCycle(weekNumber);
|
||||
const currWeek = getCurriculumWeek(weekNumber);
|
||||
|
||||
return (
|
||||
<div className="p-6 md:p-10 space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
@@ -81,12 +94,12 @@ const Dashboard = () => {
|
||||
<h1 className="text-3xl md:text-5xl mb-2">Welcome, {currentUser?.name}</h1>
|
||||
<p className="text-fg-muted text-lg">
|
||||
{curriculumActive
|
||||
? `Week ${weekNumber} · ${quarterName}`
|
||||
? `Cycle ${currentCycle} · Week ${currWeek} of 26`
|
||||
: `Here is your overview for week ${weekNumber}.`}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{/* Annual Progress Bar (only when curriculum exists) */}
|
||||
{/* Cycle Progress Bar (only when curriculum exists) */}
|
||||
{curriculumActive && yearProgress && (
|
||||
<Card className="border border-bg-warm">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
@@ -107,21 +120,21 @@ const Dashboard = () => {
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-lg">Annual Progress</h3>
|
||||
<h3 className="font-bold text-lg">Cycle Progress</h3>
|
||||
<p className="text-sm text-fg-muted">{yearProgress.completed} of {yearProgress.total} weeks completed</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Tag variant="dark" className="text-xs">Q{currentQuarter}</Tag>
|
||||
<span className="text-sm text-fg-muted">{52 - weekNumber} weeks remaining</span>
|
||||
<Tag variant="dark" className="text-xs">Cycle {currentCycle}</Tag>
|
||||
<span className="text-sm text-fg-muted">{26 - currWeek} weeks remaining</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* Visual week progress bar */}
|
||||
<div className="mt-4 flex gap-[2px] h-2 rounded-full overflow-hidden">
|
||||
{Array.from({ length: 52 }, (_, i) => {
|
||||
{Array.from({ length: 26 }, (_, i) => {
|
||||
const w = i + 1;
|
||||
const isCurrent = w === weekNumber;
|
||||
const isPast = w < weekNumber;
|
||||
const isCurrent = w === currWeek;
|
||||
const isPast = w < currWeek;
|
||||
return (
|
||||
<div
|
||||
key={w}
|
||||
|
||||
@@ -8,7 +8,7 @@ import Tag from '../components/ui/Tag';
|
||||
import LearningContentViewer from '../components/ui/LearningContentViewer';
|
||||
import { useApp } from '../store/AppContext';
|
||||
import { getAssignedTopic, generateLearningContent, getCachedContent } from '../lib/learningService';
|
||||
import { getUpcomingWeeks, getQuarterProgress, getYearProgress, getQuarterName, getQuarterForWeek, hasCurriculum as checkHasCurriculum } from '../lib/curriculumService';
|
||||
import { getYearProgress, getCurriculumCycle, getCurriculumWeek, getActiveVersion, getCurrentWeekContent } from '../lib/curriculumService';
|
||||
import * as db from '../lib/db';
|
||||
|
||||
const Leren = () => {
|
||||
@@ -17,7 +17,7 @@ const Leren = () => {
|
||||
const [allTopics, setAllTopics] = useState([]);
|
||||
|
||||
// View state
|
||||
const [view, setView] = useState('overview'); // overview, detail, creating
|
||||
const [view, setView] = useState('overview'); // overview, detail
|
||||
const [activeTopic, setActiveTopic] = useState(null);
|
||||
const [content, setContent] = useState(null);
|
||||
|
||||
@@ -25,9 +25,6 @@ const Leren = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
// Custom Topic
|
||||
const [customTopicQuery] = useState('');
|
||||
|
||||
// Weekly status
|
||||
const [weeklyDone, setWeeklyDone] = useState(false);
|
||||
const [sessionDone, setSessionDone] = useState(false);
|
||||
@@ -39,9 +36,8 @@ const Leren = () => {
|
||||
|
||||
// Curriculum state
|
||||
const [hasCurriculum, setHasCurriculum] = useState(false);
|
||||
const [upcoming, setUpcoming] = useState([]);
|
||||
const [quarterProgress, setQuarterProgress] = useState(null);
|
||||
const [yearProgress, setYearProgress] = useState(null);
|
||||
const [weekContent, setWeekContent] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.currentUser) {
|
||||
@@ -57,17 +53,15 @@ const Leren = () => {
|
||||
|
||||
// Load curriculum data
|
||||
try {
|
||||
const currExists = await checkHasCurriculum();
|
||||
setHasCurriculum(currExists);
|
||||
if (currExists) {
|
||||
const [upcomingData, qProgress, yProgress] = await Promise.all([
|
||||
getUpcomingWeeks(state.weekNumber, 4),
|
||||
getQuarterProgress(state.currentUser.id, getQuarterForWeek(state.weekNumber)),
|
||||
getYearProgress(state.currentUser.id),
|
||||
const activeVersion = await getActiveVersion();
|
||||
setHasCurriculum(!!activeVersion);
|
||||
if (activeVersion) {
|
||||
const [yProgress, wc] = await Promise.all([
|
||||
getYearProgress(state.currentUser.id, state.weekNumber),
|
||||
getCurrentWeekContent(state.weekNumber),
|
||||
]);
|
||||
setUpcoming(upcomingData);
|
||||
setQuarterProgress(qProgress);
|
||||
setYearProgress(yProgress);
|
||||
setWeekContent(wc);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Learn] Could not load curriculum data:', e.message);
|
||||
@@ -262,21 +256,10 @@ const Leren = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// ── Creating Topic Loading ─────────────────────────────────
|
||||
if (view === 'creating') {
|
||||
return (
|
||||
<div className="p-4 md:p-8 max-w-2xl mx-auto text-center py-20">
|
||||
<Loader size={48} className="mx-auto text-teal animate-spin mb-4" />
|
||||
<h1 className="text-2xl font-bold text-teal mb-2">Architecting New Topic</h1>
|
||||
<p className="text-fg-muted">The AI is creating a structure for "{customTopicQuery}"...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Overview ──────────────────────────────────────────────
|
||||
const otherTopics = allTopics.filter(t => t.id !== assignedTopic?.id && t.type !== 'fact' && t.learning_relevance !== 'exclude');
|
||||
const currentQuarter = getQuarterForWeek(state.weekNumber);
|
||||
const currentQuarterName = getQuarterName(state.weekNumber);
|
||||
const currentCycle = getCurriculumCycle(state.weekNumber);
|
||||
const currWeek = getCurriculumWeek(state.weekNumber);
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-8 max-w-5xl mx-auto pb-24 md:pb-8 animate-in fade-in duration-300">
|
||||
@@ -284,7 +267,7 @@ const Leren = () => {
|
||||
<h1 className="text-3xl md:text-4xl font-bold text-teal mb-3">Learning Station</h1>
|
||||
<p className="text-fg-muted text-lg">
|
||||
{hasCurriculum
|
||||
? `Week ${state.weekNumber} · ${currentQuarterName}`
|
||||
? `Cycle ${currentCycle} · Week ${currWeek} of 26`
|
||||
: 'Complete at least 1 topic per week. Explore more from the library!'}
|
||||
</p>
|
||||
</div>
|
||||
@@ -296,9 +279,9 @@ const Leren = () => {
|
||||
)}
|
||||
|
||||
{/* Progress Cards (only shown when curriculum exists) */}
|
||||
{hasCurriculum && yearProgress && quarterProgress && (
|
||||
{hasCurriculum && yearProgress && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||
{/* Year Progress */}
|
||||
{/* Cycle Progress */}
|
||||
<Card className="border border-bg-warm text-center p-4">
|
||||
<div className="relative w-16 h-16 mx-auto mb-2">
|
||||
<svg viewBox="0 0 36 36" className="w-full h-full -rotate-90">
|
||||
@@ -315,45 +298,22 @@ const Leren = () => {
|
||||
{yearProgress.percentage}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-fg-muted">Annual Progress</div>
|
||||
<div className="text-xs text-fg-muted">Cycle Progress</div>
|
||||
<div className="text-[10px] text-fg-muted">{yearProgress.completed}/{yearProgress.total} weeks</div>
|
||||
</Card>
|
||||
|
||||
{/* Quarter Progress */}
|
||||
<Card className="border border-bg-warm text-center p-4">
|
||||
<div className="relative w-16 h-16 mx-auto mb-2">
|
||||
<svg viewBox="0 0 36 36" className="w-full h-full -rotate-90">
|
||||
<circle cx="18" cy="18" r="15.5" fill="none" stroke="var(--color-bg-warm)" strokeWidth="3" />
|
||||
<circle
|
||||
cx="18" cy="18" r="15.5" fill="none"
|
||||
stroke="#7c3aed" strokeWidth="3"
|
||||
strokeDasharray={`${quarterProgress.percentage} 100`}
|
||||
strokeLinecap="round"
|
||||
className="transition-all duration-700"
|
||||
/>
|
||||
</svg>
|
||||
<span className="absolute inset-0 flex items-center justify-center text-sm font-bold">
|
||||
{quarterProgress.percentage}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-fg-muted">Q{currentQuarter} Progress</div>
|
||||
<div className="text-[10px] text-fg-muted">{quarterProgress.completed}/{quarterProgress.total} weeks</div>
|
||||
</Card>
|
||||
|
||||
{/* Current Week */}
|
||||
<Card className="border border-bg-warm text-center p-4 flex flex-col items-center justify-center">
|
||||
<Calendar size={24} className="text-teal mb-1" />
|
||||
<div className="text-2xl font-bold text-teal">{state.weekNumber}</div>
|
||||
<div className="text-2xl font-bold text-teal">{currWeek}</div>
|
||||
<div className="text-xs text-fg-muted">Current Week</div>
|
||||
</Card>
|
||||
|
||||
{/* Status */}
|
||||
<Card className="border border-bg-warm text-center p-4 flex flex-col items-center justify-center">
|
||||
<TrendingUp size={24} className={`mb-1 ${weeklyDone ? 'text-teal' : 'text-fg-muted'}`} />
|
||||
<div className={`text-lg font-bold ${weeklyDone ? 'text-teal' : 'text-fg-muted'}`}>
|
||||
{weeklyDone ? 'Complete' : 'In Progress'}
|
||||
</div>
|
||||
<div className="text-xs text-fg-muted">This Week</div>
|
||||
{/* Theme */}
|
||||
<Card className="border border-bg-warm text-center p-4 flex flex-col items-center justify-center col-span-2">
|
||||
<BookOpen size={24} className="text-purple-600 mb-1" />
|
||||
<div className="text-lg font-bold text-purple-700">{weekContent?.theme || 'General'}</div>
|
||||
<div className="text-xs text-fg-muted">Current Theme</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
@@ -376,7 +336,7 @@ const Leren = () => {
|
||||
{weeklyDone ? 'Completed' : 'Required'}
|
||||
</Tag>
|
||||
{hasCurriculum && (
|
||||
<Tag variant="dark" className="text-[10px]">Week {state.weekNumber}</Tag>
|
||||
<Tag variant="dark" className="text-[10px]">Theme: {weekContent?.theme || 'General'}</Tag>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-teal">{assignedTopic.label}</h3>
|
||||
@@ -390,35 +350,6 @@ const Leren = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Upcoming Schedule (only when curriculum exists) */}
|
||||
{hasCurriculum && upcoming.length > 0 && (
|
||||
<div className="mb-8">
|
||||
<h2 className="text-xl font-bold mb-4 flex items-center gap-2">
|
||||
<Calendar size={20} className="text-fg-muted" /> Coming Up
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
{upcoming.map(week => (
|
||||
<Card key={week.week_number} className="border border-bg-warm p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Tag variant="dark" className="text-[10px] font-mono">Week {week.week_number}</Tag>
|
||||
{week.is_review_week && <Tag variant="accent" className="text-[10px]">Review</Tag>}
|
||||
</div>
|
||||
{week.topic ? (
|
||||
<>
|
||||
<h4 className="font-medium text-sm leading-tight">{week.topic.label}</h4>
|
||||
<p className="text-xs text-fg-muted mt-1">{week.theme}</p>
|
||||
</>
|
||||
) : week.is_review_week ? (
|
||||
<h4 className="font-medium text-sm text-purple-600">{week.theme}</h4>
|
||||
) : (
|
||||
<h4 className="text-sm text-fg-muted italic">Unassigned</h4>
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Other Available Topics */}
|
||||
{otherTopics.length > 0 && (
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user