Skip to content

Sync with upstream ClassBuild (Codex redesign) + re-home Canvas features - #1

Merged
daTechGuy merged 38 commits into
mainfrom
sync-upstream-2026-10
Oct 3, 2026
Merged

daTechGuy merged 38 commits into
mainfrom
sync-upstream-2026-10

Conversation

@daTechGuy

@daTechGuy daTechGuy commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

What this is

Merges jtangen/classbuild@main (31 commits, May 28 – Jun 10) into the fork. Upstream shipped a full Codex design-system redesign that deleted the setup/syllabus component trees the fork's Canvas features were built on, so this is a merge plus a port, not just conflict resolution.

Upstream brought in

Codex UI redesign · Opus 4.8 adaptive thinking · OpenAI gpt-image-2 + ElevenLabs (Gemini removed) · project backup/import · multi-file publish · SCORM, IndexedDB content-loss and Opus-truncation fixes · stoppable batch generation · a11y/mobile polish.

Fork features re-homed on the new UI

Area Change
Setup Canvas template picker + outline upload; provider switch (Claude / Ollama Cloud), Ollama key + model, research backend (Claude / Tavily / Wikipedia), Tavily key, and Canvas-focused vs Everything outputs — all in the API keys modal. Begin is gated on the active provider's key.
Research runResearch backend dispatch + "backend not configured" gate / Skip to Build; upstream enrichDossier kept
Syllabus Canvas template title editor
Build New Canvas Module tab (single + generate-all, cancellable), advanced-mode tab gating, shortcuts follow visible tabs
Export Export for Canvas (.imscc), template-aware
Landing Demo-course loader, Canvas templates link, fork credit
services/llm Opus 4.8 adaptive thinking + AbortSignal ported into both backends; no retry on user cancel

Bugs found and fixed along the way

  • Ollama routing forwarded Claude model ids (MODELS.opus…) to Ollama. Claude ids are now ignored on that path.
  • CodexModal stole focus on every keystroke (effect depended on an inline onClose). Pasting worked, typing didn't. Regression test added.
  • IMSCC export could silently spend gpt-image-2 credits via the new image-driven PPTX exporter. slides.pptx is now only bundled when every slide is already rendered.
  • Two lint errors inherited from upstream (dead wavToMp3, setState-in-effect).

Verification

tsc -b clean · eslint 0 errors (7 pre-existing exhaustive-deps warnings) · vitest 265/265 · vite build OK · Setup page + keys modal checked in a browser, no console errors.

Tests: rewrote Setup/Research/Syllabus/Build/Export page tests and the PPTX exporter tests for the new UI; removed tests for deleted modules (gemini-tts, ApiKeyPanel).

Canvas export rewritten to the native format (ac264d6)

Importing the demo cartridge into a live Canvas (built on a Unraid VM for this) showed "imported, no issues" but only the syllabus and one discussion arrived — no modules, pages or quizzes. Cause: a manifest containing course_settings/canvas_export.txt makes Canvas use its native importer, which ignores plain Common Cartridge content. assembleImscc() now emits the exact layout Canvas's own exporter writes (copied from a reference course exported by the running Canvas): module_meta.xml modules, wiki_content/ Pages, quizzes as CC + native QTI with assessment_meta.xml (practice = ungraded, in-class / challenge = graded in an "Assignments" group), published discussions, web_resources/ files, syllabus.

Verified by import: 3 modules / 13 items, 2 published Pages, 5 published quizzes with correct answers, 3 graded assignments, published discussion, files, syllabus. tools/validate-imscc.py gained native-format rules (passes the new cartridge and Canvas's own export, fails the old one).

Template-based export tested against Canvas and fixed (582ab4f)

Built an instructor-style template inside the live Canvas (verbatim "do not publish" + "Begin Here" modules, an example-pattern module, a placeholder pattern module), exported it with Canvas's own exporter, ran it through our parser + template exporter, and imported the result. It "imported with no issues", but compared with a control import of the untouched template it exposed four exporter bugs and one parser bug, all now fixed with regression tests (each verified to fail against the old code):

  • Verbatim items lost data: modules were re-serialised from a small subset, dropping an external link's URL (Canvas then discarded the item), LTI link settings, and module prerequisites / completion requirements / sequential-progress. Verbatim modules now pass through byte-for-byte.
  • Dangling manifest references for external-link items → original manifest linkage is kept.
  • Resource <dependency> links and intendeduse were dropped (a verbatim quiz/discussion would lose its metadata) → preserved.
  • Stray duplicate pages: replaced pattern modules' files stayed in the archive and Canvas's native importer scans wiki_content/ directly → now removed with their metadata resources.
  • Parser: the example discussion was never extracted from real Canvas exports (their discussion resources have no href).

Verified by re-import: all 5 Begin-Here items incl. the link, 12 pages (was 16, no duplicates), Modules 1–3 with notes + published discussions, syllabus from the outline fields.

Needs a human eye

  1. Branding/palette — done (9898fd4): Canvas red/orange restored on the Codex tokens, light + dark.
  2. Importing a produced .imscc into real Canvas — done against a self-hosted Canvas (see above). Still not exercised: generation against live LLMs.
  3. Batch generation ignoring mode — done (0f88e16): "Draft all", per-chapter "Generate all" and the sidebar count now follow Canvas-focused vs Everything.
  4. Slides missing from IMSCC after a reload — done (f77004e): Export for Canvas now asks whether to render the missing deck images (spends OpenAI credits) or export without decks.
  5. PR Canvas test harness, TypeScript IMSCC validator in CI, real-Canvas fixtures, refreshed CLAUDE.md #2 to upstream (imscc-canvas-only) is stale/conflicting and should probably be closed or re-cut.

🤖 Generated with Claude Code

jtangen and others added 30 commits May 27, 2026 13:10
Standalone 1920×1080 marketing/pitch page combining the Codex parchment
palette, Newsreader + JetBrains Mono fonts, the colophon SVG mark, the
five-stage indicator strip, the seven-artifact aside, and the six
worked-example course thumbnails — designed for a single hero
screenshot. Asset paths are absolute so the page works both via the
Vite dev server and after Vercel deploy.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pitch-scoped copy of the six course thumbnails so the pitch page can
ship the newer, smaller renders without disturbing /courses/ that the
rest of the app still uses.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Refresh docs for the redesign: new Codex hero image; six chapter themes
(Press/Notebook/Almanac/Storybook/Studio/Terminal) replacing the old four;
OpenAI gpt-image-2 + ElevenLabs providers (Gemini removed); the seven
per-chapter deliverables; five-stage pipeline; and the weeklyChallenge
prompt builder in the library table.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the app onto the Codex parchment design system and six per-course
chapter themes (Press/Notebook/Almanac/Storybook/Studio/Terminal). Replace
Gemini with OpenAI gpt-image-2 (slide + chapter images) and ElevenLabs
(narration). Rebuild the Setup → Syllabus → Research → Build → Export flow
and the published course viewer, switch slides to image-first decks, and add
the themed quiz/challenge renderer in tools/. Refresh the favicon/icon set
(SVG + web manifest + apple-touch + OG card) and prune stale public assets
(Gemini-era voice samples, old hero and preview images). Remove the legacy
setup/syllabus components and the Gemini services.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The generated favicon PNGs and favicon.svg used the source mark's portrait
aspect (e.g. 16x19, 32x38) and a 100x120 SVG viewBox, so browsers squished
the tab icon. Regenerate the square favicons (16/32/48/96) from the 512px
square-colophon render, point favicon.svg at the square colophon variant, and
add a multi-resolution favicon.ico (+ <link ... sizes="any">) for default and
legacy requests. Verified all icon paths resolve in-browser with no 404s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bump MODELS.opus to claude-opus-4-8. Crossing from 4.6 triggers the 4.7
migration, which rejects manual extended thinking (budget_tokens) on Opus —
a raw model-ID swap would 400 every Opus call (syllabus, chapters).

Two non-obvious 4.7+ defaults shape the fix: adaptive thinking is OFF unless
explicitly enabled (effort alone does NOT make Opus think), and the thinking
summary text is omitted by default. So the Opus branch now sets
thinking:{type:'adaptive', display:'summarized'} alongside
output_config.effort (mapping the existing max/high/medium/low budgets onto
effort levels) — this preserves deep reasoning on syllabus/chapters and keeps
the streamed thinking our UI shows via onThinking. Sonnet 4.6 / Haiku 4.5
keep manual budget_tokens, which they still accept. No sampling params are
set, so that 4.7 breaking change doesn't apply. README + CLAUDE.md version
lines updated.

(display isn't typed in SDK 0.74.0 yet; the field still serializes, so cast.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Point the footer "The maker" link at tangenlab.com and "Code with Claude ·
Tokyo" at claude.com/code-with-claude/tokyo (were placeholder URLs), and
repoint the README hackathon credit from the old Google signup form to the
Code with Claude · Tokyo event page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
On Opus 4.8 the effort levels were recalibrated and 'max' overthinks — the
syllabus stalled ~7 min before the first token. Drop every Opus 'max' call to
'high' (Anthropic's recommended floor for intelligence-sensitive work, and what
the already-fast chapter build uses): fresh syllabus (SPA + CLI), practice quiz,
and in-class quiz.

Also surface Opus's reasoning on the syllabus page. display:'summarized' (added
with the 4.8 upgrade) makes Opus stream its planning, but SyllabusPage discarded
it — it had onText/onError but no onThinking. Now the pre-output wait shows a
live "Reasoning through the course architecture · 0:14" trace with an elapsed
clock instead of a frozen "Designing the course architecture…" label.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two bugs surfaced while generating materials on Opus 4.8:

1. Activities crash ("undefined is not an object — 'variations.length'"): the
   activity-detail model occasionally returns valid JSON that omits an array
   field (e.g. variations), and the `as ActivityDetail` cast doesn't validate
   it — so the detail render/export/copy crashed the whole Build phase, and the
   malformed object was persisted, so reselecting Activities re-crashed. Add
   normalizeActivityDetail() and apply it at parse (useChapterMaterials), render
   + copy (ActivitiesTab), and export (BuildPage). Pre-existing fragility, not a
   4.8 regression — but it now degrades gracefully and recovers stuck courses.

2. Weekly Challenge silently reverting: under adaptive thinking, thinking tokens
   count toward max_tokens. The old formula (budgetTokens + maxTokens) left too
   little output room at 'high', so a deep think truncated the JSON →
   parseJson threw → setTabError → button reverted with no output. Give Opus
   generous headroom above the intended output (maxTokens + budgetTokens*2,
   capped at 128k); keep the Sonnet/Haiku reservation where budget_tokens must
   fit inside max_tokens.

Not a code bug: slow slide generation is gpt-image-2 rendering 4K images, not
Opus thinking.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Audio "synthesis failed" box and the batch-confirm modal used
bg-cb-status-warning (solid #b07a1f gilt) as the container AND
text-cb-status-warning for the text — identical colour, so the error message
and modal heading rendered invisibly: an "ugly, meaningless" mustard box with
only a button. Switch the container to the tinted bg-cb-status-warning-bg,
matching the app convention (saturated text on a light tint), so the actual
message is readable.

The audio failure itself happens at ElevenLabs (invalid key / voice / quota)
and is captured via friendlyError into audioError — it was always there, just
painted invisible. Now legible so it can be acted on. Not migration-related
(transcript is Sonnet, narration is ElevenLabs — neither changed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Investigating the "SCORM export failed." banner (which appeared on the Build
page): SCORM packages the weekly challenge, and the challenge had been failing
to generate (the truncation bug fixed in f1c2791). Three rough edges hardened:

- ExportPage SCORM handler threw a cryptic "SCORM export failed." when the
  challenge data was present-but-incomplete (no questions), and silently did
  nothing when absent. It now validates questions up front and gives an
  actionable message ("generate the mastery challenge for this chapter first").

- buildWeeklyChallengeHtml threw on partial data (challengeData.metadata and
  sanitised.questions were assumed present). Guard both so a sparse challenge
  yields a (possibly empty) package instead of a cryptic throw.

- The uiStore error banner persisted across navigation, so a SCORM failure on
  the Export page haunted the Build page (the screenshot). Clear the transient
  error on route change in AppShell; errors raised while you stay on a page are
  unaffected.

Root cause is upstream (challenge generation, already fixed) — these make the
failure legible and self-contained.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
buildWeeklyChallengeHtml obfuscated answers with
Buffer.from(out, 'binary').toString('base64'). Buffer is a Node global with no
browser polyfill, so in the SPA Build page the template threw "Buffer is not
defined" the instant it encoded the first answer — and the throw was swallowed
(catch { /* template failed */ }), so the challenge parsed and saved its data
but rendered no HTML and showed no error: it just reverted to the "Generate
mastery challenge" button. The Node CLI has Buffer, which is why the example
courses have working challenges while the in-browser challenge never did. The
same template backs the SCORM export, so in-browser SCORM failed for the same
reason.

Switch to btoa (browser global; Node >= 16), masking each XOR byte with & 0xFF
so the output is byte-identical to Buffer's latin1 truncation (CLI output
unchanged) and btoa never sees a code point > 0xFF. The runtime decoder already
uses atob.

Also stop swallowing the template error in generateWeeklyChallenge — surface it
as a tab error so a render failure can never be silent again.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ction bug)

Empirically verified (curl, 2026-05) that ElevenLabs' TTS endpoint returns
permissive CORS (access-control-allow-origin: *) and a real HTTP 401 for a bad
key — so direct browser BYOK calls are allowed and CORS is NOT the blocker. A
*rejected* fetch (the catch path, vs an HTTP status response) therefore means
the request never reached ElevenLabs: almost always a local block of
api.elevenlabs.io (a Safari content/privacy blocker, "Prevent Cross-Site
Tracking", a browser extension, a VPN, or iCloud Private Relay). Anthropic and
OpenAI keep working because they are different hosts.

- Replace the misleading "Network error. Check your connection and retry." with
  guidance that names the real, user-fixable causes.
- Fail fast on a rejected fetch instead of retrying 3x (a host block isn't
  transient). The 429/5xx retry on real HTTP responses is unchanged.
- Surface the actionable message verbatim for a status-less ElevenLabsTtsError;
  keep friendlyError's mapped copy (key rejected / rate limit / overloaded) for
  errors that carry an HTTP status, so a genuine 401/422 is no longer masked as
  a generic "network" error.

No backend/proxy added — CORS is open, so a proxy would only break BYOK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- generateAllOutputs never queued the weekly challenge, so "Generate all
  materials for this chapter" built everything EXCEPT the Challenge (you had to
  open the tab and generate it by hand). Queue it alongside the others.

- The challenge preview's "Begin Challenge" did nothing in-app yet worked in the
  downloaded file. The challenge's SCORM API discovery walks up to window.parent;
  in the sandboxed preview iframe (no allow-same-origin) reading a property on
  the cross-origin parent throws a SecurityError, which aborted init before the
  start button was wired. Wrap discovery in try/catch so a missing/cross-origin
  LMS just means "run standalone" (also hardens real LMS embeds).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
"Publish · standalone HTML" built ONE self-contained .html with 4 student tabs
using a different, older viewer — so it lacked tabs and didn't match the polished
example courses at courses.classbuild.ai. Replace it with a multi-file course
SITE (.zip) that uses the SAME Codex viewer the examples use:

  index.html                index/sidebar/tabs/welcome (Codex chrome)
  chapters/NN.html          themed reading
  quizzes/NN.html           gamified practice quiz
  challenges/NN.html        weekly mastery challenge
  slides/chNN/slide-MM.jpg  rendered slide images
  audio/NN.mp3              audiobook narration

All 7 tabs (Reading · Practice · Challenge · Discussion · Activities · Audio ·
Slides) appear when their data exists; discussion + activities render natively in
the viewer. Keeping a multi-file layout means slide images and audio stay as
files, so the site stays light instead of ballooning a single HTML to 100MB+.

- Port the example-course viewer to src/templates/codexCourseSiteTemplate.ts
  (verbatim, minus the gallery "all courses" back-link, which a standalone
  published course has no use for).
- Rewrite publishExporter to assemble the zip: decode slide data URIs to .jpg,
  fetch audio blobs to .mp3, write themed chapter/quiz/challenge HTML, build the
  index via the Codex viewer.
- Export button: "Publish · course site (.zip)"; downloads <course>-site.zip.
- Remove the now-orphaned courseViewerTemplate.ts.

Verified the viewer output (tabs, file paths, theme tag, single-course auto-open)
with a smoke test; build is green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oads

ROOT CAUSE (confirmed via investigation): the persisted course snapshot included
the rendered 4K slide images (~13/chapter, ~1.3MB each as base64). Zustand
persist re-serializes + rewrites the ENTIRE course to IndexedDB on every state
change with no debounce, and slides are written one-at-a-time (a ~13x write
storm). Once a deck rendered, the multi-MB writes stalled without resolving OR
rejecting — leaving a deceptively green "Saved · 12m ago" while nothing actually
saved. Content generated after that point (audio, weekly challenge) never
reached disk, so a reload restored the stale pre-stall snapshot and they
vanished; the transcript survived only because it was written before the stall.
Audio was doubly doomed: it was kept solely as a blob: URL, which is stripped on
save and dies on reload regardless.

FIX:
- Keep the persisted snapshot lean: strip slide imageDataUri at the persist
  boundary (partialize). imagePrompt/title stay, so the deck re-renders next
  session. In-memory data is unchanged, so live preview/export/publish are
  unaffected in-session. Chapter reading figures stay inlined (smaller, written
  once, not cleanly re-renderable) so readings survive a reload intact.
- Make audio durable across reload: persist a size-capped (~6MB) base64 data URI
  (audioDataUri) alongside the in-session blob URL; rehydrate and all export /
  publish / download read sites fall back to it. Long chapters re-narrate from
  the surviving transcript instead of bloating the snapshot.
- Surface a hung write: idbStorage.setItem now races a 15s timeout, so an
  oversized/never-settling put becomes a visible persistError instead of a
  silently stale green "Saved".
- Audio download was mislabeled .wav — it is MP3; fixed the filename + button.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The audio-transcript prompt instructed inserting "[short pause]" pacing cues —
but ElevenLabs reads bracketed stage directions aloud verbatim ("short pause")
instead of pausing. Rewrite the pacing instruction to rely on punctuation and
sentence/paragraph structure and explicitly forbid bracketed/parenthetical
directions. Also sanitize at the TTS layer (stripStageDirections) so transcripts
already generated narrate cleanly without regenerating — strips any
bracketed/parenthetical group containing "pause"/"beat" and tidies whitespace,
while leaving real bracketed content like [H2O] or (correct) intact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…otices

- AbortController plumbed through streamMessage/sendMessage/streamWithRetry
  and ElevenLabs TTS; module-level abort registry so Stop works after
  navigation. Stop buttons on every drafting status line (all 8 tabs) and
  on the batch header; aborts are silent cancels, never error banners.
- Unified the two batch flows into one runBatch(mode) engine with
  fill-the-holes guards, so "Retry failed" re-runs only what's missing.
  Failures are collected per chapter/material and reported in an
  end-of-batch summary panel instead of console.error.
- Batch header now shows "Class k of N · drafting … · ~M min left" from a
  rolling per-chapter average; confirm-dialog counts match the real queue.
- Audio tab shows the transcript (word count + narrated minutes, editable,
  downloadable) before any ElevenLabs spend, and says so when a narration
  exceeds the persistable size cap instead of silently dropping it.
- Slides tab explains that rendered images don't survive reloads and shows
  per-row thumbnails; per-tab error banners gained a retry action.
- "Generate all materials" is a real button with remaining-count and a
  rough time hint; tab bar is a proper tablist with arrow-key switching.
- A successful full-state IndexedDB write clears any stale persistError.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Download the whole course (sans binaries and keys) from Export as a
portable project file; restore it from the landing page with a
replace-confirmation when a course is already in progress. One bad
IndexedDB write or a cleared browser profile no longer means losing
the course.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each drafted chapter row gains a quiet edit affordance (input/textarea,
Enter/Escape handling) backed by a new updateSyllabusChapter store action
that preserves the curriculum map, plus a small-caps "interactives" line
listing each planned widget with its rationale on hover.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AI-cited references that haven't been verified get a warning badge per
source (and an aggregate count on the Build page dossier strip), with a
footer reminding teachers to check them before teaching.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
aria-describedby/aria-invalid + role=alert on Input/Textarea errors,
labelled Select hints, a real label + aria-valuetext on Slider, and a
visible keyboard focus ring on Checkbox/Radio (the hidden native input's
opacity:0 was suppressing its own outline).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Setup: when the Anthropic key is missing (or failed verification) an
  "Add Anthropic key →" button sits beside the disabled Begin CTA instead
  of a buried footer link; key validation now uses the free countTokens
  endpoint; the length slider notes ≈ one chapter per week.
- "Begin · Syllabus" confirms before wiping an in-progress course, and
  Syllabus "Regenerate" confirms + clears built chapters/dossiers rather
  than silently orphaning them; both point at the project-file backup.
- Header: "Save failed" is now a retry button (a no-op set re-runs the
  full persist write; success clears the banner), and the new-course
  dialog mentions the project file.
- Reduced motion honoured app-wide (CSS media query + MotionConfig
  reducedMotion="user"); --cb-text-subtle nudged to clear WCAG AA on both
  parchment and vespers grounds.
- Chapter sidebar speaks one voice: "— complete / — 3 of 8 drafted /
  — awaiting"; header chip says "practice quiz" instead of "quiz".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The landing markup was inline-styled for the 1100px reading column and
broke badly on phones (674px of content in a 375px viewport — hero
squeezed against an off-screen panel, three-up grids crushed, four-column
footer unreadable). Now:

- Hero headline scales clamp(46px → 96px); panel stacks below the CTA
  (capped width) under 1020px.
- How-it-works, deliverables, CLI, and footer grids restack at 1020/860/
  720px; course cards hold two-up until 640px since they're designed
  narrow; principle rows put the description beneath the label on phones.
- Section kickers no longer wrap mid-phrase; anchored scrolls glide
  (scroll-behavior: smooth, reduced-motion safe) and land below the fixed
  header via scroll-margin; card links get real tap targets; closing CTA
  centres on phones; header padding tightens at phone widths.

Verified 375/768/1440 full-page via CDP — zero horizontal overflow at
every width.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Same changes as the classbuild-cli codexViewerTemplate (the two stay in
lockstep): welcome screen gains Begin/Browse CTAs and fluid type, the
sidebar gets a scrim + close-state toggle that no longer overlaps the
brand, the tab bar becomes one swipeable row on phones, .main picks up
min-width:0 against the flex min-content trap, plus theme-color/
description metas and reduced-motion support.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Viewer: welcome screen gains a "Begin Class 01" CTA and a Browse-classes
button (phone visitors were stranded with only a bare hamburger), fluid
clamp() type for long course titles, a tap-to-close scrim behind the
slide-out sidebar with an open/close toggle that no longer overlaps the
brand, a single swipeable tab row on phones, min-width:0 on .main (the
nowrap tab bar otherwise widened it past the viewport — flex min-content
trap), stacked activity meta rows, theme-color/description metas, and
prefers-reduced-motion support.

Gallery: theme-color/description metas, eager-load the first thumbnail,
reduced-motion support, tightened phone spacing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The machinery that builds courses.classbuild.ai was living only in this
working copy: build-staging.ts (themed rebuild of all six bundles into
tmp/staging-deploy + Codex gallery), build-image-pptx.ts (full-bleed
image decks), the render/rewrite image scripts, publish.ts's themed-dir
AssembleOptions + codex viewer path, and HANDOFF.md documenting the
theme-conversion workflow. Committed so tonight's deploy is reproducible
from git.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every stage past the landing page overflowed a 375px viewport by
140-160px. The two global culprits: the stage indicator's five cells
couldn't shrink below their labels' min-content (~514px), and CodexModal
ignored the viewport for its fixed width. Stage cells now use
minmax(0,1fr) and compress to a centred label-only strip on phones;
modals clamp to min(width, 100vw - 28px).

Per-stage: page banners (Syllabus/Research/Build) stack their title over
a wrapping action row instead of squeezing it beside three buttons —
Research's header previously wrapped one word per line and stamped its
buttons over the subtitle. Setup's five audience pills go two-up (labels
were chopped mid-glyph: "Undergr", "Professi"), starter briefs stack,
and the what-comes-next strip pairs up. Build's eight-tab bar becomes a
single swipeable row scrolling inside its own bar (plus min-width
hygiene), and the keyboard-shortcuts chip — meaningless on touch — is
hidden at phone widths. Export's six theme cards go two-up with the
preview chip wrapping below long names instead of clipping, and artifact
rows give chapter titles the full width with pills beneath.

Verified at 375px via CDP sweep: zero horizontal overflow on every
stage, with an independent visual QA pass on the before/after captures.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jtangen and others added 2 commits June 10, 2026 12:37
Stepping back to Setup while a syllabus was drafting stranded you: the
stage strip showed Syllabus as unclickable "awaiting" (it's neither
completed nor the current route), so the only forward affordance was
"Begin · Syllabus" — which threatens to replace the course.

The stage strip now treats the store's currentStage as the journey's
high-water mark: any reached stage stays clickable, and the stage the
flow is actually at reads "↵ return" in the accent colour when viewed
from another page. Setup additionally gets a plain "← Back to <stage>"
button beside the primary CTA, which relabels to "Begin again ·
Syllabus →" when a course exists. The begin-confirm now also gates on
the journey (not just settled content), so a mid-draft Begin can't
silently race a second generation over the running one.

Verified headlessly: return via strip and via button land on /syllabus
with no modal and no reset; Begin again raises the replace-confirm.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Brings in 31 upstream commits (May 28 - Jun 10): the Codex design system
redesign, Opus 4.8 adaptive thinking, OpenAI gpt-image-2 + ElevenLabs
(replacing Gemini), project backup/import, multi-file publish, SCORM and
IndexedDB content-loss fixes, stoppable batch generation, a11y/mobile polish.

Fork features re-homed onto the new Codex UI:
- Setup: Canvas template picker + outline upload; provider switch (Claude /
  Ollama Cloud), Ollama key + model, research backend (Claude / Tavily /
  Wikipedia) + Tavily key, and Canvas-focused vs Everything build outputs,
  all in the API keys modal. Begin is gated on the active provider's key.
- Research: runResearch backend dispatch + the "backend not configured"
  gate with Skip to Build; upstream's enrichDossier verification kept.
- Syllabus: Canvas template title editor.
- Build: new Canvas Module tab (single + "generate all", cancellable),
  advanced-mode tab gating, keyboard shortcuts follow visible tabs.
- Export: "Export for Canvas (.imscc)" action (template-aware).
- Landing: demo-course loader, Canvas templates link, fork credit.
- LLM layer (services/llm): Opus 4.8 adaptive thinking + AbortSignal ported
  into the Anthropic and Ollama backends; no retry on user cancel.

Fixes found along the way:
- Ollama routing forwarded Claude model ids (MODELS.opus etc.) to Ollama.
- CodexModal re-focused the dialog on every parent render, stealing focus
  from inputs while typing (effect depended on an inline onClose).
- IMSCC export no longer silently spends gpt-image-2 credits: slides.pptx is
  only bundled when every slide is already rendered.
- Lint errors inherited from upstream (dead wavToMp3, setState-in-effect).

Tests: rewrote Setup/Research/Syllabus/Build/Export page tests and the pptx
exporter tests for the new UI/exporter; removed tests for deleted modules
(gemini-tts, ApiKeyPanel). tsc, eslint (0 errors), vitest 265/265, build OK.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@codecov-commenter

Copy link
Copy Markdown

Welcome to Codecov 🎉

Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests.

ℹ️ You can also turn on project coverage checks and project coverage reporting on Pull Request comment

Thanks for integrating Codecov - We've got you covered ☂️

daTechGuy and others added 6 commits October 3, 2026 12:42
Override the Codex emphasis tokens (parchment + vespers modes), their
Tailwind aliases, and the legacy violet-* tokens with the Canvas palette
the fork used before the upstream redesign. Light mode uses #c72a26 for
text/CTA contrast on parchment; dark mode uses #ee6c4d. Exported course
themes are unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
In Canvas-focused mode the challenge/activities/audio/slides tabs are
hidden, but "Draft all chapters", per-chapter "Generate all", and the
retry path still generated (and billed for) them. They now only run when
"Everything" is on. The batch confirm dialog and the sidebar's "N of M
drafted" count reflect the active mode (4 materials vs 8).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Rendered slide images aren't persisted across reloads (upstream strips them
to keep IndexedDB writes small), so after a reload slides.pptx silently
dropped out of Canvas cartridges. Exporting now detects unrendered decks and
asks: render the images and include the decks (spends gpt-image-2 credits),
or export without them. Without an OpenAI key the dialog only offers the
latter. Rendered images are cached into the store as they arrive and the
single-render mutex is respected. Template-mode cartridges don't bundle decks
so they skip the prompt.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
tools/validate-imscc.py checks archive integrity, XML well-formedness,
manifest identifier/reference integrity, resource types, QTI shape and
Canvas course_settings extras. Structural only; a real Canvas import is
still the source of truth.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Importing the demo cartridge into a live Canvas showed it "imported with no
issues" but contained only the syllabus and one discussion: no modules, pages
or quizzes. Canvas's PackageIdentifier treats any package whose manifest has
course_settings/canvas_export.txt as a native Canvas export, and the native
converter only reads Canvas's own files (module_meta.xml, wiki_content/,
non_cc_assessments/) and ignores plain manifest content. The marker was added
earlier to get the syllabus recognised.

Rework assembleImscc() to emit exactly what Canvas's own exporter writes
(taken from a reference course exported by the running Canvas):
- one published Module per chapter via module_meta.xml, plus a "Course
  resources" module for the curriculum matrix
- reading as a native Canvas Page (wiki_content/, scripts/styles stripped),
  with the original interactive HTML attached as a downloadable file
- every quiz as CC QTI + Canvas-native QTI (non_cc_assessments) +
  assessment_meta.xml; practice quiz is an ungraded practice_quiz, in-class
  quiz and weekly challenge are graded assignments in an "Assignments" group;
  all published, correct answers wired, feedback preserved
- discussions as imsdt topics + topicMeta, published
- slides/infographic/teaching DOCX as web_resources files + files_meta.xml
- course_settings.xml, assignment_groups.xml, syllabus (overview + outline)
- deterministic Canvas-style identifiers so re-exports match up

Verified by importing into Canvas 2026-04-22 (prod) via the native importer:
3 modules / 13 items, 2 published Pages, 5 published quizzes with correct
answers, 3 graded assignments, published discussion, files and syllabus.

tools/validate-imscc.py gains native-format rules; it passes this cartridge
and Canvas's own export and fails the old cartridge. Rewrote the exporter
tests for the new layout (module/manifest consistency, both QTI flavours,
graded vs practice, escaping, determinism).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Built an instructor-style template inside a live Canvas (verbatim "do not
publish" + "Begin Here" modules, an example-pattern module, a placeholder
pattern module), exported it with Canvas's own exporter, ran it through the
parser + template exporter, and imported the result. Canvas reported no
issues, but the import exposed four real bugs (a control import of the
untouched template showed the loss was ours):

- Verbatim modules were re-serialised from a small parsed subset, which
  dropped an external link's <url> (Canvas then discarded the item entirely),
  LTI link settings, and module prerequisites / completion requirements /
  sequential-progress flags (hard-coded away). Verbatim <module> blocks are now
  carried through byte-for-byte; only <position> is renumbered.
- Verbatim organization items were linked via the module_meta identifierref,
  which left dangling manifest references for external links. They now keep the
  template's original manifest linkage.
- Manifest resources were rebuilt without <dependency> links or intendeduse,
  so a verbatim quiz/discussion would lose its assessment_meta/topicMeta and the
  syllabus resource its tag. Both are parsed and re-emitted.
- Files of the replaced pattern modules stayed in the archive. Canvas's native
  importer scans wiki_content/ directly, so the old "Module N Overview" and
  example notes were imported as stray duplicate pages. Dropped resources, and
  dependency resources nothing else needs, are now removed with their files.

Parser: the example (few-shot) discussion was never extracted from real Canvas
exports, because discussion/quiz resources there have no href attribute (file
only in a <file> child). The resource map now falls back to it.

Tests: regression tests for each (verified to fail against the old code).
tools/validate-imscc.py now flags unclaimed wiki_content pages as errors in
native mode and no longer false-positives on sub-headers / inline external
links (both appear in Canvas's own exports).

Verified by import into Canvas: all 5 Begin-Here items incl. the external
link, no stray pages (12 vs 16), new Module 1-3 with notes + discussions,
syllabus from outline fields.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@daTechGuy
daTechGuy merged commit 86ddfff into main Oct 3, 2026
1 check passed
@daTechGuy
daTechGuy deleted the sync-upstream-2026-10 branch October 3, 2026 21:04
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.

3 participants