Skip to content

Move the Tutor+ teacher gallery into the lesson-deep-dive package - #75105

Draft
stephenliang wants to merge 13 commits into
stagingfrom
stephen/ldd-gallery-mega
Draft

Move the Tutor+ teacher gallery into the lesson-deep-dive package#75105
stephenliang wants to merge 13 commits into
stagingfrom
stephen/ldd-gallery-mega

Conversation

@stephenliang

Copy link
Copy Markdown
Member

The Tutor+ teacher gallery lived in apps/ with a webpack entry, an HttpClient dependency and a Rails-embedded bootstrap payload; it now lives in frontend/packages/lesson-deep-dive as a host-agnostic component, the page fetches its bootstrap from a JSON route beside the page, and the Rails action does nothing but authorize and render a mount div.

This is one draft PR carrying the whole grow-in as an ordered commit series so each step can be read on its own. Apps stays the production host; a Studio route is a later, additive step that needs no edits under src/gallery/.

# Commit What it does
1 feat(dashboard): serve the Tutor+ gallery bootstrap as JSON at tutor/gallery_data New Lessons::TutorGalleryDataController#show, routed beside tutor/gallery in the unit routes so both URL grammars work.
2 fix(core): emit default-import interop in the CJS build Core's CJS output did require('ky').create; ky is ESM-only. First apps consumer of @code-dot-org/core/api hit i.create is not a function. One-line rollup interop: 'auto'.
3 refactor(gallery): call Rails through core's transport instead of HttpClient Four call sites, same URLs and validators, client injected via ApiClientProvider with a same-origin base URL (the singleton's absolute base URL bypasses the dev proxies).
4 refactor(gallery): move the Tutor+ gallery into the lesson-deep-dive package Pure git mv of 8 components, 2 SCSS modules and the avatar. Content differs only by forced import paths and the unused React default import. Package lint fails on this commit by design.
5 style(gallery): format the moved gallery files to the package's conventions Output of the package's eslint --fix and prettier --write, plus one named MouseEvent type import.
6 test(gallery): port the gallery suites from apps Jest to the package's Vitest Same assertions, real ApiClientProvider with a stub transport.
7 test(apps): transform core's ESM-only dependencies under Jest Regression fix: the move put a core import into the package's single-chunk build, which broke five student-flow Jest suites that import the barrel.
8 feat(gallery): fetch the bootstrap over JSON instead of embedding it in the page New TutorGalleryPage; the entry derives the lesson path from the URL; the Haml data attribute and the controller's payload build are gone.
9 docs(lesson-deep-dive): describe what the package now ships README intro.

The branch is based on stephen/ldd-shell-live-bootstrap (staging plus the teacher-gallery dev shell from #74914, the wire-types move, and the shell's live bootstrap fetch), so those commits appear here too.

Deviations from the written plan, each forced by a measurement: the "style prep in apps before the move" step was infeasible (apps compiles JSX with the classic runtime, pins prettier 2, and orders imports differently), so the move is two commits, a pure rename and a formatting pass. The core interop fix and the Jest transform allowlist were not planned; both are prerequisites the swap and the move exposed. The bootstrap endpoint is page-adjacent (tutor/gallery_data, precedent level_properties) rather than under /api/v1, because the page answers to two URL grammars and the entry can only derive its own path.

Links

Testing story

Parity was measured, not eyeballed. A DOM and computed-style fingerprint (tag, own text, alt/href/role, bounding rect, 27 computed properties for every element under the mount) was captured on the pre-change code and again after commits 3, 5 and 8, for seven states: section list, three teacher project views (scored video, unscored video, whiteboard deep link), the owner and peer project views as students, and the "My projects" empty state. All states compare IDENTICAL to the pre-change baseline at every step, ignoring only a 15 px scrollbar width between the headless baseline browser and the headed one. Class names are excluded by construction; they are Vite-hashed after the move.

Request sequence on the final page: GET .../tutor/gallery_data 200, then GET /challenge_responses?unit_id=&section_id= and GET /challenge_responses/unit_counts?section_id=. The page HTML has no data-tutorgallerydata attribute. One request more than before, a loading line before the sidebar, otherwise unchanged.

Mocked mode (VITE_API_MODE=msw yarn dev in the package, /gallery.html) covers the edge cases: owner and peer layouts via ?viewerRole=, a missing project via ?project=999, and the empty unit state.

Automated: package release:dryrun (build, lint, prettier, stylelint, typecheck, 7 Vitest files / 27 tests); apps typecheck; full apps Jest suite 1107 suites / 10504 tests after commit 7; Lessons::TutorGalleryDataControllerTest 4 tests. LessonsControllerTest's tutor_gallery cases that render the HTML layout need RAILS_ENV=test rake assets:precompile locally and are verified by CI.

To try it locally: run bin/dashboard-server, build apps (cd apps && yarn build), sign in as a teacher whose section has final challenge submissions, and open /s/<script>/lessons/<n>/tutor/gallery?enableExperiments=lesson-tutor-challenge. For the shell: cd frontend/packages/lesson-deep-dive && VITE_API_MODE=msw yarn dev then http://localhost:5173/gallery.html, or without VITE_API_MODE at http://localhost-studio.code.org:5173/gallery.html against a running Rails.

Deployment notes

The page now depends on GET .../tutor/gallery_data at runtime. Both ship in this PR; nothing to coordinate. The lesson-tutor-challenge experiment gate is unchanged and still in the webpack entry.

🤖 Generated with Claude Code

stephenliang and others added 13 commits August 26, 2026 11:40
Adds gallery.html/src/dev/gallery.tsx as a second dev-shell entry
rendering apps' Tutor+ project gallery (ChallengeGallery), mirroring how
main.tsx renders the student lesson deep dive. Extracts the shared
fonts/theme/Font-Awesome/50px-band chrome both entries need into
devPageChrome.tsx, with no change to main.tsx's rendered output.

Adds galleryFixtures.ts (tutorGalleryData bootstrap plus a plausible
class section's worth of video/whiteboard challenge responses, wire-typed
from ChallengeResponsesController/ChallengeResponse#summarize) and
galleryMocks.ts (msw GET handlers for the gallery's three endpoints,
gated behind a ?viewerRole= knob for the teacher/owner/peer project page
layouts), plus tiny real video/PNG fixtures in public/gallery/ for the
asset download_urls.
… them

ChallengeResponse#summarize adds student_feedback/evaluated_at/
evaluation_result/rubric as conditional hash keys (include_feedback/
include_evaluation), not nulled fields — Rails never sends them at all
to a peer, and never sends evaluation_result/rubric to an owner. The
GET /challenge_responses/:id mock was sending null/[] instead, which a
client distinguishing absent-key from null would not catch against the
shell. Build the response with conditional spreads instead.
…he package

challenge_responses and challenges share their wire types and validators
between the student ChallengeActivities flow and the teacher gallery, both
still partly in apps. Moving the types first, ahead of the gallery
components themselves, lets ChallengeActivities and the gallery views
staying behind in apps pull from one definition instead of two.

Moved: the ChallengeResponse/Challenge family and their validators from
lessonDeepDive/types.ts (ExplanationTypes, EvaluationStatus stay with
them); all of gallery/types.ts; assetUtils.ts, the one gallery leaf whose
runtime imports and scss are self-contained.

Deferred: GallerySidebar, ProjectCard, and ReactionChips share
challenge-gallery.module.scss with ChallengeGallery; ProjectDetailsCard and
ProjectStage share project-view.module.scss with ProjectView and
AssessmentPanel. Since ChallengeGallery, ProjectView, and AssessmentPanel
are out of scope for this slice and an scss module cannot split across the
apps/package boundary, none of those five move yet.

ReflectionValue, ReflectionData, LessonDeepDiveData, and the practice
problem types stay in apps/types.ts: they depend on
LessonObjectiveReflectionValues from generated-scripts, which is Ruby
codegen the package does not have a story for yet, and none of them are on
the gallery's dependency path.
…y_data endpoint

gallery.tsx now always fetches GET /api/v1/tutor_gallery_data (msw mocks
it; dashboard mode proxies it) before mounting ChallengeGallery, with
?script/?lessonPosition knobs, falling back to the checked-in fixture and
one console.info on any fetch failure so the page never renders blank
ahead of the Rails endpoint landing.
…gallery_data

The gallery page embeds its bootstrap payload in a data attribute, which
nothing but that page can read. A standalone dev shell, and later the page
itself, need the same payload over HTTP.

New Lessons::TutorGalleryDataController#show, routed as a sibling of
tutor/gallery inside the unit_routes block so both the /s/ and /courses/
URL grammars work. Same lesson resolution and availability 404 as the
page; class-level authorize! to grant exactly what the page grants. The
payload build is duplicated from LessonsController#tutor_gallery on
purpose and guarded by a test until the page stops embedding it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The CJS output compiled `import ky from 'ky'` to `require('ky').create`.
ky is ESM-only, so a CJS consumer such as the apps webpack bundle gets the
module namespace and `create` is undefined. Studio never saw it because
Vite takes the ESM output. Rollup's `interop: 'auto'` picks `.default`
when the required module carries __esModule, the same fix the
lesson-deep-dive package already carries.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…pClient

The Tutor+ gallery's four GETs went through apps' HttpClient, the last
host import standing between the components and the package. They now go
through a core API client that the host injects via ApiClientProvider,
with the same URLs, the same validators, and the same effect structure.

The client is built per host with a same-origin base URL, not the
singleton DashboardApiClient: the singleton's base URL is absolute and
would bypass the webpack and Vite dev proxies. ky's default 10 s timeout
is turned off because HttpClient had none.

apps' Jest cannot load the real @code-dot-org/core/api (ky is ESM-only
under its CJS transform), so the two fetching suites stand in a
context-only mock, the same shape as the HttpClient module mock they
replace.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…package

git mv of the eight components, two SCSS modules and the tutor avatar from
apps/src/aiTutor/views/gallery to the package's src/gallery. Content is
unchanged except what the new location forces: barrel imports become
relative, the avatar import becomes relative (Vite inlines it), and the
unused default React import goes because the package compiles JSX with the
automatic runtime under noUnusedLocals.

The barrel exports the components; the webpack entry and the five apps
Jest suites import them from there. A type-only reference to the component
library's themes module makes its MUI variant augmentation visible to the
dts build, which excludes the dev shell that used to carry it. Runtime
deps the moved code needs (core, MUI, classnames) become peer deps.

The three presentational Jest suites stub @code-dot-org/core/api with an
empty module: the package is one chunk, so importing any component loads
ChallengeGallery's core import, which Jest cannot parse. They move to
Vitest next.

Package lint and prettier fail on this commit by design; the next commit
reformats the moved files to the package's conventions so this one stays
a pure rename.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ntions

Output of the package's eslint --fix and prettier --write on the files the
previous commit moved: import grouping and order, trailing commas, and one
named MouseEvent type import in place of the default React import that
import-x/default rejects. No JSX, identifier, comment or logic changes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s Vitest

The five suites move next to the components they test. Runner syntax and
import paths change; assertions and test names do not. The two fetching
suites render inside the real ApiClientProvider with a stub transport, which
Vitest can do because it loads core's ESM dependencies; the Jest-only
module mocks go away with them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The gallery's move put an @code-dot-org/core/api import into the
lesson-deep-dive package's single-chunk build, so every apps Jest suite
that imports anything from that barrel now loads ky, camelcase-keys, its
nested camelcase and quick-lru, and map-obj. All five ship ESM only and
Jest's CJS transform refused them. Allowing them through babel-jest fixes
the five lesson deep dive suites this broke; it works now that core's CJS
output carries default-import interop.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…in the page

The Rails page no longer embeds the gallery's bootstrap in a data
attribute. A new TutorGalleryPage in the package fetches it from the
tutor/gallery_data route beside the page, shows the gallery's own loading
and error lines meanwhile, then renders ChallengeGallery. The webpack
entry derives the lesson path from the URL and mounts that page; the Rails
action shrinks to authorization, the availability 404, and the mount div.

The dev shell mounts the same page component, so its live mode is the
production code path; its fixture fallback and the drift-guard test that
compared the JSON to the embedded payload go away with the payload.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The README opened by saying the package ships nothing and the feature's
source still lives in apps. The gallery lives here now and the student
flow's leaf views have for a while.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.

1 participant