diff --git a/app/[locale]/dashboard/student/courses/[courseId]/exercises/[exerciseId]/page.tsx b/app/[locale]/dashboard/student/courses/[courseId]/exercises/[exerciseId]/page.tsx index 7d7fe654..992eeb14 100644 --- a/app/[locale]/dashboard/student/courses/[courseId]/exercises/[exerciseId]/page.tsx +++ b/app/[locale]/dashboard/student/courses/[courseId]/exercises/[exerciseId]/page.tsx @@ -267,7 +267,8 @@ export default async function ExercisePage({ params }: PageProps) { const t = await getTranslations('exercises.audio') const otherExercisesSection = otherExercises && otherExercises.length > 0 ? ( <> -

{t('moreExercises')}

+ {/* Full-strength muted, not /70: the faded variant measured 2.76:1. */} +

{t('moreExercises')}

{otherExercises.map((ex) => ( {chatComponent} diff --git a/components/exercises/artifact-exercise.tsx b/components/exercises/artifact-exercise.tsx index 0479b2ca..bbf20d17 100644 --- a/components/exercises/artifact-exercise.tsx +++ b/components/exercises/artifact-exercise.tsx @@ -1,26 +1,15 @@ 'use client' import { useState, useRef, useCallback, useEffect } from 'react' -import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import ExerciseResultSummary from '@/components/exercises/exercise-result-summary' -import { cn } from '@/lib/utils' -import Markdown from 'react-markdown' +import ExerciseBrief from '@/components/exercises/exercise-brief' +import ExerciseHeader from '@/components/exercises/exercise-header' +import ExerciseWorkspace, { initialWorkspacePanel } from '@/components/exercises/exercise-workspace' import { useTranslations } from 'next-intl' import confetti from 'canvas-confetti' import { toast } from 'sonner' -import { - IconCheck, - IconClock, - IconFlame, - IconInfoCircle, - IconSparkles, - IconLoader2, - IconAlertTriangle, - IconArrowRight, - IconTrophy, - IconCode, -} from '@tabler/icons-react' +import { IconLoader2, IconAlertTriangle, IconArrowRight } from '@tabler/icons-react' interface ArtifactExerciseProps { exercise: { @@ -64,7 +53,7 @@ export default function ArtifactExercise({ initialEvaluation = null, }: ArtifactExerciseProps) { const t = useTranslations('exercises.artifact') - const tAudio = useTranslations('exercises.audio') + const tWorkspace = useTranslations('exercises.workspace') const tGamification = useTranslations('gamification') const config = exercise.exercise_config ?? {} @@ -76,25 +65,8 @@ export default function ArtifactExercise({ const [passed, setPassed] = useState(isExerciseCompleted) const [errorMsg, setErrorMsg] = useState(null) const [rateLimited, setRateLimited] = useState(false) - - const completionData = exercise.exercise_completions?.[0] - const completionScore = completionData?.score ?? (evaluation?.score || 0) - const completionDate = completionData?.completed_at - - const difficultyLabels: Record = { - easy: tAudio('beginner'), - medium: tAudio('intermediate'), - hard: tAudio('advanced'), - } - const difficultyConfig: Record = { - easy: { color: 'text-emerald-600 bg-emerald-500/10 border-emerald-500/20', icon: IconSparkles }, - medium: { color: 'text-amber-600 bg-amber-500/10 border-amber-500/20', icon: IconFlame }, - hard: { color: 'text-rose-600 bg-rose-500/10 border-rose-500/20', icon: IconFlame }, - } - - const difficulty = difficultyConfig[exercise.difficulty_level] || difficultyConfig.easy - const difficultyLabel = difficultyLabels[exercise.difficulty_level] || difficultyLabels.easy - const DifficultyIcon = difficulty.icon + /** Bumped on every fresh grade so the workspace can surface the result panel. */ + const [gradedNonce, setGradedNonce] = useState(0) const handleSubmit = useCallback(async (content: string, metadata: Record = {}) => { setSubmitState('evaluating') @@ -126,6 +98,7 @@ export default function ArtifactExercise({ setEvaluation(result) setPassed(result.passed) setSubmitState('done') + setGradedNonce((n) => n + 1) // Send feedback back to iframe iframeRef.current?.contentWindow?.postMessage( @@ -171,174 +144,103 @@ export default function ArtifactExercise({ setEvaluation(null) } - return ( -
- {/* Header */} -
-
- - - - - {exercise.time_limit && ( - - - )} - {(isExerciseCompleted || passed) && ( - - - )} + const taskPanel = ( +
+ {rateLimited && ( +
+

+

- -

- {exercise.title} -

-
- - {/* Main Layout */} -
- {/* Left: Instructions */} -
-
-
-

-

-
-
-
- {exercise.instructions} -
-
-
- - {isExerciseCompletedSection && ( -
{isExerciseCompletedSection}
- )} + )} + + {submitState === 'evaluating' && ( +
+
+ )} - {/* Right: Artifact + Results */} -
-
- {/* Completion summary */} - {(isExerciseCompleted || passed) && ( -
-
-
-

- - {tAudio('completedScore')} -

- {completionDate && ( -

- {tAudio('completedOn', { - date: new Date(completionDate).toLocaleDateString(undefined, { - month: 'long', day: 'numeric', year: 'numeric', - }), - })} -

- )} -
-
- - {Math.round(completionScore)} - - / - 100 -
-
-

- {tAudio('exerciseMarkedComplete')} -

-
- )} - - {/* Rate limit banner */} - {rateLimited && ( -
-
-
- -
-
-

{t('rateLimited')}

-
-
-
- )} - - {/* Evaluating state */} - {submitState === 'evaluating' && ( -
- -
-

{t('evaluating')}

-
-
- )} - - {/* Error */} - {errorMsg && ( -
- {errorMsg} -
- )} - - {/* Sandboxed iframe */} - {artifactHtml && ( -
-