Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/app/[exam]/assess/[domain]/AssessmentClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Badge } from '@/components/ui/badge';
import { QuestionCard } from '@/components/assess/QuestionCard';
import { Clock, CheckCircle, XCircle, Target } from 'lucide-react';
import type { Question } from '@/types/domain';
import { MASTERY_THRESHOLD, APPROACHING_THRESHOLD } from '@/lib/constants';
import type { QuestionAnswer, AssessmentResult } from '@/types/assessment';

interface AssessmentClientProps {
Expand Down Expand Up @@ -153,8 +154,8 @@ export function AssessmentClient({ examId, domainId, topicId, questions }: Asses

<ProgressIndicator value={result.score} className="h-3 w-full max-w-md" />

<Badge variant={result.score >= 85 ? 'default' : result.score >= 60 ? 'secondary' : 'destructive'} className="text-base px-4 py-2">
{result.score >= 85 ? 'Excellent!' : result.score >= 60 ? 'Good Progress' : 'Needs Review'}
<Badge variant={result.score >= MASTERY_THRESHOLD ? 'default' : result.score >= APPROACHING_THRESHOLD ? 'secondary' : 'destructive'} className="text-base px-4 py-2">
{result.score >= MASTERY_THRESHOLD ? 'Excellent!' : result.score >= APPROACHING_THRESHOLD ? 'Good Progress' : 'Needs Review'}
</Badge>
</div>
</CardContent>
Expand Down
3 changes: 2 additions & 1 deletion src/components/dashboard/DashboardHero.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { MASTERY_THRESHOLD } from "@/lib/constants";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
Expand Down Expand Up @@ -30,7 +31,7 @@ function getCTA(examId: string, overall: OverallProgress, domains: DomainProgres
}

// Exam ready - 85%+ mastery
if (overall.masteryScore >= 85) {
if (overall.masteryScore >= MASTERY_THRESHOLD) {
return {
text: "Review Your Progress",
href: `/${examId}/progress`,
Expand Down
3 changes: 2 additions & 1 deletion src/components/progress/ReadinessCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ChevronDown, ChevronRight, Info } from 'lucide-react';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { ProgressIndicator } from '@/components/ui/progress-indicator';
import type { ReadinessEstimate, DomainReadiness } from '@/lib/progress/calculator';
import { MASTERY_THRESHOLD, APPROACHING_THRESHOLD } from '@/lib/constants';
import { clsx } from 'clsx';

interface ReadinessCardProps {
Expand Down Expand Up @@ -116,7 +117,7 @@ export function ReadinessCard({ estimate, examId }: ReadinessCardProps) {
function DomainBar({ domain }: { domain: DomainReadiness }) {
const [expanded, setExpanded] = useState(false);
const masteryRounded = Math.round(domain.mastery);
const colorClass = masteryRounded >= 85 ? 'text-green-600' : masteryRounded >= 60 ? 'text-amber-600' : 'text-red-600';
const colorClass = masteryRounded >= MASTERY_THRESHOLD ? 'text-green-600' : masteryRounded >= APPROACHING_THRESHOLD ? 'text-amber-600' : 'text-red-600';
const hasWeakTopics = domain.weakTopics.length > 0;

return (
Expand Down
5 changes: 3 additions & 2 deletions src/components/ui/progress-indicator.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client"

import { MASTERY_THRESHOLD, APPROACHING_THRESHOLD } from '@/lib/constants';
import { cn } from '@/lib/utils';
import * as ProgressPrimitive from "@radix-ui/react-progress";

Expand All @@ -16,8 +17,8 @@ interface ProgressIndicatorProps {
*/
export function ProgressIndicator({ value, className }: ProgressIndicatorProps) {
const indicatorColorClass =
value >= 85 ? 'bg-green-600' :
value >= 60 ? 'bg-amber-500' :
value >= MASTERY_THRESHOLD ? 'bg-green-600' :
value >= APPROACHING_THRESHOLD ? 'bg-amber-500' :
'bg-red-600';

return (
Expand Down
14 changes: 14 additions & 0 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/** Score at or above which a topic is considered "mastered" / "exam ready" */
export const MASTERY_THRESHOLD = 85;

/** Score at or above which a topic is considered "developing" */
export const APPROACHING_THRESHOLD = 60;

/** Score at or above which readiness level is "approaching" (vs "building") */
export const READINESS_APPROACHING_THRESHOLD = 65;

/** Score at or above which dashboard tips switch to exam-prep category */
export const EXAM_PREP_THRESHOLD = 75;

/** Decimal equivalent for SQL queries (mastery_level is 0-1 scale) */
export const MASTERY_THRESHOLD_DECIMAL = MASTERY_THRESHOLD / 100;
3 changes: 2 additions & 1 deletion src/lib/content/sidebar.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { MASTERY_THRESHOLD } from '@/lib/constants';
import { getAllDomains } from './loader';
import { parseTopicSections } from './parser';
import { getAllTopicWindowedMasteries } from '@/lib/progress/mastery';
Expand Down Expand Up @@ -111,7 +112,7 @@ export function getSidebarHierarchyWithProgress(examId: string): SidebarHierarch
if (topicResult && topicResult.attempts > 0) {
topicMasterySum += masteryScore;
topicsWithProgress++;
if (masteryScore >= 85) topicsCompleted++;
if (masteryScore >= MASTERY_THRESHOLD) topicsCompleted++;
}

return {
Expand Down
4 changes: 3 additions & 1 deletion src/lib/dashboard/tips.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { EXAM_PREP_THRESHOLD } from '@/lib/constants';

export interface Tip {
id: string;
category: "onboarding" | "assessment" | "study" | "feature" | "exam-prep";
Expand Down Expand Up @@ -136,7 +138,7 @@ function getTipCategory(state: UserState): Tip["category"] {
return "onboarding";
}

if (state.masteryScore >= 75) {
if (state.masteryScore >= EXAM_PREP_THRESHOLD) {
return "exam-prep";
}

Expand Down
3 changes: 2 additions & 1 deletion src/lib/llm/tool-handlers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { MASTERY_THRESHOLD } from '@/lib/constants';
import { getTutorProgressContext } from '@/lib/progress/tutor-context';
import { getAllDomains, getTopicById, getTopicQuestions } from '@/lib/content/loader';
import { getLabMeta } from '@/lib/content/experiments';
Expand Down Expand Up @@ -466,7 +467,7 @@ export function handleSuggestNextStudyTopic(_params: Record<string, unknown>, ex
}

// Skip mastered topics
if (mastery >= 85) continue;
if (mastery >= MASTERY_THRESHOLD) continue;

const finalScore = Math.round(baseScore * weightMultiplier);

Expand Down
11 changes: 6 additions & 5 deletions src/lib/progress/calculator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { cache } from "react";
import { db } from "@/lib/db/client";
import { getAllDomains, getTopicById } from "@/lib/content/loader";
import { MASTERY_THRESHOLD, MASTERY_THRESHOLD_DECIMAL, READINESS_APPROACHING_THRESHOLD } from "@/lib/constants";
import { getAllTopicWindowedMasteries } from './mastery';
import type { TopicMasteryResult } from './mastery';

Expand Down Expand Up @@ -132,7 +133,7 @@ export const getDomainProgress = cache((examId: string, domainId: string): Domai
const topicStats = db.prepare(`
SELECT
COUNT(*) as topics_with_progress,
SUM(CASE WHEN mastery_level >= 0.85 THEN 1 ELSE 0 END) as completed_topics
SUM(CASE WHEN mastery_level >= ${MASTERY_THRESHOLD_DECIMAL} THEN 1 ELSE 0 END) as completed_topics
FROM topic_progress
WHERE exam_id = ? AND domain_id = ?
`).get(examId, domainId) as { topics_with_progress: number; completed_topics: number };
Expand Down Expand Up @@ -336,7 +337,7 @@ export const getReadinessEstimate = cache((examId: string): ReadinessEstimate =>
}

// Topics below 85% are weak (including unstudied ones)
if (mastery < 85) {
if (mastery < MASTERY_THRESHOLD) {
weakTopics.push({
topicId: topic.meta.id,
topicName: topic.meta.shortName,
Expand Down Expand Up @@ -378,9 +379,9 @@ export const getReadinessEstimate = cache((examId: string): ReadinessEstimate =>

// Determine level
let level: 'ready' | 'approaching' | 'building';
if (overallMastery >= 85) {
if (overallMastery >= MASTERY_THRESHOLD) {
level = 'ready';
} else if (overallMastery >= 65) {
} else if (overallMastery >= READINESS_APPROACHING_THRESHOLD) {
level = 'approaching';
} else {
level = 'building';
Expand Down Expand Up @@ -413,7 +414,7 @@ function getAllDomainProgressBatch(examId: string): DomainProgress[] {
SELECT
domain_id,
COUNT(*) as topics_with_progress,
SUM(CASE WHEN mastery_level >= 0.85 THEN 1 ELSE 0 END) as completed_topics
SUM(CASE WHEN mastery_level >= ${MASTERY_THRESHOLD_DECIMAL} THEN 1 ELSE 0 END) as completed_topics
FROM topic_progress
WHERE exam_id = ?
GROUP BY domain_id
Expand Down
5 changes: 3 additions & 2 deletions src/lib/utils/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* Tailwind cannot handle dynamic class names (e.g., `border-${color}-500`),
* so we map domain color strings to hex values for use with style={{ }}.
*/
import { MASTERY_THRESHOLD, APPROACHING_THRESHOLD } from '@/lib/constants';

const DOMAIN_COLOR_HEX: Record<string, string> = {
blue: '#3b82f6',
Expand All @@ -25,8 +26,8 @@ export function getDomainColorHex(color: string): string {
* Uses Tailwind classes since these are static (known at build time).
*/
export function getMasteryDotColorClass(score: number): string {
if (score >= 85) return 'bg-green-500';
if (score >= 60) return 'bg-amber-500';
if (score >= MASTERY_THRESHOLD) return 'bg-green-500';
if (score >= APPROACHING_THRESHOLD) return 'bg-amber-500';
if (score > 0) return 'bg-red-500';
return 'bg-muted-foreground/30';
}
13 changes: 7 additions & 6 deletions src/lib/utils/mastery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
* Centralized mastery utilities for consistent display across the application.
* Consolidates mastery scoring, labeling, and formatting functions.
*/
import { MASTERY_THRESHOLD, APPROACHING_THRESHOLD } from '@/lib/constants';

/**
* Get mastery score color class based on threshold
*/
export function getMasteryColorClass(score: number): string {
if (score >= 85) return "text-green-600";
if (score >= 60) return "text-amber-600";
if (score >= MASTERY_THRESHOLD) return "text-green-600";
if (score >= APPROACHING_THRESHOLD) return "text-amber-600";
if (score > 0) return "text-red-600";
return "text-muted-foreground";
}
Expand All @@ -17,8 +18,8 @@ export function getMasteryColorClass(score: number): string {
* Get mastery label based on score
*/
export function getMasteryLabel(score: number): string {
if (score >= 85) return "Mastered";
if (score >= 60) return "Developing";
if (score >= MASTERY_THRESHOLD) return "Mastered";
if (score >= APPROACHING_THRESHOLD) return "Developing";
if (score > 0) return "In Progress";
return "Not Started";
}
Expand All @@ -30,8 +31,8 @@ export function getMasteryStatus(score: number): {
label: string;
variant: "default" | "secondary" | "destructive" | "outline";
} {
if (score >= 85) return { label: "Exam Ready", variant: "default" };
if (score >= 60) return { label: "Developing", variant: "secondary" };
if (score >= MASTERY_THRESHOLD) return { label: "Exam Ready", variant: "default" };
if (score >= APPROACHING_THRESHOLD) return { label: "Developing", variant: "secondary" };
if (score > 0) return { label: "Building Foundation", variant: "outline" };
return { label: "Not Started", variant: "outline" };
}
Expand Down