Skip to content

feat(exercises): show students their past AI task results, legibly (#581) - #582

Merged
guillermoscript merged 1 commit into
masterfrom
fix/lesson-task-result-legibility-581
Jul 28, 2026
Merged

feat(exercises): show students their past AI task results, legibly (#581)#582
guillermoscript merged 1 commit into
masterfrom
fix/lesson-task-result-legibility-581

Conversation

@guillermoscript

Copy link
Copy Markdown
Owner

Closes #581.

A student who came back to a finished lesson checkpoint or standalone exercise saw a bare score badge. The AI's feedback was never missing — it has always been written to lesson_checkpoint_attempts.evaluation and exercise_evaluations.ai_result. The loaders just never read those columns back, and every engine held its result in component state that a reload discarded. This was a display gap, not a storage gap.

This PR restores it, then fixes what an /impeccable critique found wrong with how it read.

Walkthrough

Same student, two surfaces: the standalone essay they passed, then the in-lesson checkpoint they did not. 390px.

Walkthrough

Restoring the result

Mobile · 390px Desktop · 1440px
essay mobile essay desktop
  • loadLessonCheckpoints now selects response and evaluation. Defensive parsers turn those two free-form jsonb columns back into the student's own answer plus the feedback, next-step hint and per-question explanations. Four writer shapes exist (ai / deterministic / fallback / external sync); anything unrecognised yields "nothing to show" rather than being trusted into the UI.
  • The standalone exercise page needed no new query. Artifact and essay rows were already in the exercise_evaluations result set fetched for the audio/video history, and were simply never passed to their components.
  • Per-question feedback rendered correctValue raw, so a multiple-choice miss read "Incorrect — 1" — the option index. It now maps to option text, and booleans to localized true/false.
  • Coding challenges write no evaluation row at all. They are graded by their test runner and store a hardcoded completion, so there is nothing to replay. The card says so explicitly instead of rendering an empty result. That is a storage gap and out of scope here.

Making it legible

Verified live at 390×844 and 1440×900, light and dark, /en and /es, on cold loads.

Accessibility. Six WCAG AA text-contrast failures in light and four in dark, measured canvas-sampled over the real composited background. All now clear; 14/14 elements pass in both themes.

Element Was Now
PASSED badge, white on emerald-500 2.47
/ 100, text-muted-foreground/60 2.31 light / 3.29 dark
STRENGTHS / AREAS TO IMPROVE 3.65 / 3.20
+ / - list markers 2.47 / 2.13
AI FEEDBACK, text-primary 3.49 dark

The AI Feedback label no longer uses text-primary at all: tenants override that token, so small text in it cannot be guaranteed to clear AA for any school. The icon keeps the accent — non-text UI only needs 3:1.

The artifact panel printed the score over the pass mark, twice. 62 / 70 reads as 89%, but 62 against a pass mark of 70 is a failure. That whole block was a near-copy of ExerciseResultSummary, so it now renders the shared card and the duplicate is gone.

artifact

A failed checkpoint showed a green tick, because its tone came from completed and a failed attempt is still completed. And the restored feedback sat behind a closed disclosure, so a returning student saw a score chip and nothing else. It now opens itself for the one state that asks something of them — a graded attempt they did not pass — and an explicit toggle still wins.

The retry field also sat between the answer and the grade for that answer, looking identical to the read-only answer above it. Order is now answer → result → retry, and the restored answer is borderless so it stops reading as an editable field.

checkpoint

A sticky-column regression I introduced. Putting the result card inside lg:sticky lg:top-6 took that container to 1078px against a 900px viewport. position: sticky does not pin an element taller than its scrollport, so the chat input scrolled away unreachable. The card is now a sibling above it (650px, sticks again) — which also puts the result first on mobile instead of 1157px down the page, behind instructions the student has already followed.

Measure and touch. Prose capped at 68ch (was 85–90). Header stacks below sm:, where the metadata line was being squeezed to 161px and two lines. Long unbroken input wraps. On phones the retry button is 36px tall (was 24) and inputs are 16px, so iOS Safari stops zooming the page on focus.

Verification

  • npm run build exit 0 · npm run typecheck clean · 669 unit tests pass (53 files, 23 new covering the parsers and the auto-expand rule)
  • npx impeccable detect clean on all touched files
  • Contrast audited programmatically in-page, both themes, 0 failures
  • All QA fixtures removed afterwards; lesson content restored byte-for-byte

Not in scope

The artifact page restores a grade while its own answer fields sit empty — checkpoints show "Your answer", the artifact engine has no equivalent. Noted in #581 as deserving its own issue.

)

A student returning to a finished lesson checkpoint or standalone exercise
saw a bare score badge. The AI's feedback was never missing — it was always
written to `lesson_checkpoint_attempts.evaluation` and `exercise_evaluations.
ai_result`; the loaders simply never read those columns back, and every
engine held its result in component state that a reload discarded.

Restore path
- `loadLessonCheckpoints` now selects `response` and `evaluation`, and
  defensive parsers turn those two free-form jsonb columns back into the
  student's own answer plus the feedback, next-step hint and per-question
  explanations. Four writer shapes exist (ai / deterministic / fallback /
  external sync); anything unrecognised yields "nothing to show" rather than
  being trusted into the UI.
- The standalone exercise page needed no new query: artifact and essay rows
  were already in the `exercise_evaluations` result set fetched for the
  audio/video history and were simply never passed to their components.
- Per-question feedback rendered `correctValue` raw, so a multiple-choice
  miss read "Incorrect — 1" (the option *index*). It now maps to option text
  and localized true/false.
- Coding challenges write no evaluation row at all — they are graded by their
  test runner and store a hardcoded completion. The card says so instead of
  rendering an empty result.

Legibility (issue #581, found by an /impeccable critique verified live at
390x844 and 1440x900, light and dark, en and es)
- Six WCAG AA contrast failures in light and four in dark are fixed; the card
  now measures clean in both themes. The worst was the PASSED badge at 2.47:1
  — white on emerald-500, the single most important word on the card. The
  "AI Feedback" label no longer uses `text-primary`: tenants override that
  token, so small text in it cannot be guaranteed to clear AA.
- The artifact panel printed the score over the PASS MARK, twice: "62 / 70"
  reads as 89% when 62 is a failure. That whole block was a near-copy of
  ExerciseResultSummary, so it now renders the shared card instead.
- A failed checkpoint showed a green tick, because its tone came from
  `completed` and a failed attempt is still completed.
- The checkpoint restored behind a closed disclosure. It now opens itself for
  the one state that asks something of the student — a graded attempt they did
  not pass — and an explicit toggle still wins.
- The retry field sat between the answer and the grade for that answer, and
  looked identical to the read-only answer above it. Order is now answer →
  result → retry, and the restored answer is borderless so it does not read
  as an editable field.
- Adding the result card inside `lg:sticky lg:top-6` took that container to
  1078px against a 900px viewport; `sticky` does not pin an element taller
  than its scrollport, so the chat input scrolled away unreachable. The card
  is now a sibling above it, which also puts the result first on mobile
  instead of 1157px down the page behind instructions already followed.
- Prose is capped at 68ch (was 85-90), the header stacks below `sm:`, long
  unbroken input wraps, and on phones the retry button is 36px tall and
  inputs are 16px so iOS Safari stops zooming the page on focus.

23 unit tests pin the parsers and the auto-expand rule.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JUBJNELGqnwbQbQNp5Qd9h
@guillermoscript
guillermoscript merged commit 84b22dc into master Jul 28, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Student-facing AI task results: contrast, hierarchy and a broken sticky column on mobile + desktop

1 participant