Skip to content

feat(regen): per-slide regeneration dialog with block toggles and title editing#410

Open
jaumemir wants to merge 41 commits intoTHU-MAIC:mainfrom
jaumemir:feat/regenerate-slide-pr
Open

feat(regen): per-slide regeneration dialog with block toggles and title editing#410
jaumemir wants to merge 41 commits intoTHU-MAIC:mainfrom
jaumemir:feat/regenerate-slide-pr

Conversation

@jaumemir
Copy link
Copy Markdown

Summary

Adds a per-slide regeneration dialog that lets users selectively regenerate parts of an existing slide without touching everything:

  • Slide block toggle (Modify slide switch): when OFF, slide content/actions generation is skipped and the existing slide is preserved. Includes a conflict-aware auto-override: if new media is requested but the slide has no existing media slot, the slide is regenerated anyway to create the placeholder element.
  • Editable title field: pre-loaded from outline.title, live-binds to <DialogTitle>. Only editable when the slide toggle is ON.
  • Audio block toggle (Modify narration switch): skip TTS generation when narration is unchanged.
  • AI narration generator: one-click LLM generation of narration text from the slide indication (/api/generate/narration-text).
  • Media block (Keep / None / Image / Video): independently toggle media regeneration with auto-generated prompts.
  • Theme selector: apply a different theme on regeneration.
  • Model controls in dialog footer (LLM + image/video model).
  • Review bar: after regeneration, Accept / Undo / Edit again workflow.
  • Catalan (ca) locale: new i18n keys added. Note: full Catalan locale support is already proposed in feat(i18n): add Catalan (ca) locale #403 — if that PR lands first, the ca.json additions here apply on top of it.

New files

File Purpose
app/api/generate/scene-content-only/route.ts Regenerate a single slide's content without persisting a new scene
app/api/generate/narration-text/route.ts LLM-generate narration text from slide indication
app/api/generate/media-prompt/route.ts Auto-generate a media prompt from slide indication
components/classroom/regenerate-slide-dialog.tsx Full regeneration dialog (slide + audio + media blocks)
components/generation/model-selector-popover.tsx Reusable two-level model selector popover
lib/hooks/use-scene-regenerator.ts 4-step incremental regeneration pipeline hook
lib/i18n/locales/ca.json Catalan locale keys (see #403)

Modified files

File Change
components/stage.tsx Regen state machine, backup management, review bar, confirm modal
components/stage/scene-sidebar.tsx ↺ Regen. button for active slide scenes
components/generation/generation-toolbar.tsx Extract ModelSelectorPopover to shared component
lib/media/media-orchestrator.ts Export generateAndStoreMedia for reuse
lib/i18n/locales/{en-US,zh-CN,ja-JP,ru-RU}.json i18n keys for new UI strings

Test plan

  • Open any slide scene → click ↺ Regen. in sidebar → dialog opens with pre-filled fields
  • Toggle "Modify slide" OFF → indication/title hidden, "Existing slide will be preserved" shown → submit → no content API calls made
  • Toggle OFF + new image + no existing media slot → amber warning shown → submit forces slide regen
  • Edit title → <DialogTitle> updates live → regenerated slide and sidebar show new title
  • "Modify narration" toggle + "Generate with AI" button fills narration textarea
  • Media: Keep / None / Image / Video each work; image/video prompt auto-generates
  • After regeneration: Accept (keeps new) / Undo (restores original) / Edit again (re-opens with last values)
  • Navigate away during review → confirmation modal appears
  • resolveSkipSlide unit tests: pnpm test -- tests/hooks/use-scene-regenerator.test.ts

🤖 Generated with Claude Code

Ubuntu and others added 30 commits April 12, 2026 11:28
8-task plan covering API endpoints, hook, dialog component, sidebar
button, Stage state machine, and manual verification steps.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
POST /api/generate/media-prompt calls a brief LLM turn to convert a
slide's indication text into a concise image/video generation prompt,
used when the user switches to a media type not present in the original
slide.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Synchronous POST /api/generate/scene-content-only that wraps
generateSceneContentFromInput and returns raw slide elements without
creating or persisting a scene. Loads stage metadata and outlines from
server storage so the client only needs to send the edited outline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Split the single "Internal" banner into "Exported core" (above
generateAndStoreMedia) and "Internal helpers" (above callImageApi /
callVideoApi / fetchAsBlob) to accurately reflect visibility.
Added JSDoc to generateAndStoreMedia matching the style of the other
two public functions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Issue 1: reject empty/whitespace indicationText (was bypassing !falsy check)
- Issue 3: only append "..." in log line when prompt exceeds 60 chars
- Issue 4: split mediaType validation into two checks with correct error codes
  (MISSING_REQUIRED_FIELD when absent, INVALID_REQUEST when wrong value)
- Issue 5: add beforeEach(vi.clearAllMocks) and test for invalid mediaType

Auth pattern unchanged: neither scene-actions nor image sibling routes use requireAuth.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tent-only

Replace manual x-api-key/x-base-url header reading with resolveModelFromHeaders
to follow the server-side key resolution chain. Also cast result.content to
GeneratedSlideContent instead of unknown[], add stageData null check (404),
improve error message wording, and add a missing stageId test case.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add 5th test case covering the loadStage-returns-null (404) guard path.
Refactored storage mock to use vi.fn() so mockReturnValueOnce works per-test.
Also apply themeId shorthand property fix in the route.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements per-slide regeneration orchestration: scene-content-only →
scene-actions → TTS audio (with splitLongSpeechActions + user override) →
media. Exports pure helpers (outlineToIndication, indicationToOutline,
buildMediaGenerations, applyAudioOverride) for use by RegenerateSlideDialog.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… commit

- Fix 1: Remove mediaPrompt guard from Step 3 (spec says only mediaType !== 'none' required)
- Fix 2: Commit overridden speech actions to store before TTS loop so text override is always persisted even if all TTS calls fail
- Fix 3: Type newContent as SceneContent instead of unknown, removing the as never cast

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace direct mutation of speechAction objects with index-based immutable updates
- Add ttsEnabled guard (matching use-scene-generator.ts pattern) before TTS loop
- Fix stale allOutlines in Step 4 outline sync — read fresh from store.getState()
- Change store variable from snapshot to store module (getState() pattern)
- Add guard for missing json.scene.content in Step 1b actions response

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…i18n strings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wrap the ↺ Unicode character in aria-hidden to prevent screen readers
from announcing it, add a title attribute matching the existing retry
button pattern, and document regenState prop intent in the interface.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ideDialog

- Add AbortController ref to cancel in-flight media-prompt fetches on media type switch or dialog close/unmount
- Pass indication as argument to generatePromptForType (remove stale closure dep)
- Add DialogDescription (sr-only) for screen readers
- Wrap ↺ decorative characters in aria-hidden spans
- Add htmlFor/id pairs to Label+Textarea (indication, audioText)
- Use role=group + aria-labelledby for the media type button group

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…al to Stage

Wire useSceneRegenerator hook into Stage component with full state machine
(idle → dialog_open → regenerating → review), a review bar with accept/undo/edit-again
actions, and a confirm AlertDialog when navigating away during review.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- C1: make regenerate() return boolean; handleRegenerate restores backup and reopens dialog on failure
- C2: replace dynamic import() of already-statically-imported module with static import
- I1: move RegenState type to module scope
- I2: read scene from getCurrentScene() in handleRegenerate to avoid stale closure
- I3: inline confirmRegenDiscard logic to remove cascade rememoisation
- I4: add onOpenChange to regen confirm AlertDialog so Escape key dismisses it

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Back up the SceneOutline before regeneration starts alongside the
  scene content, so 'Undo' restores the original indication text in
  the dialog (previously the outline was updated by Step 4 of the
  regenerator but never rolled back on discard/undo).
- Restore the outline backup in handleRegenUndo, confirmRegenDiscard,
  and the error-recovery path in handleRegenerate.
- Clear backupOutline on handleRegenAccept and confirmRegenKeep.
- Add providerOptions.google.generateAudio = false to Veo adapter so
  Veo 3+ models generate silent video (audio is handled via TTS).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…hanged

- Add modifyAudio toggle (Switch) to RegenerateSlideDialog; default off.
  When off, the narration textarea is hidden and skipAudio: true is passed
  to the regenerator, saving time when only slide content changes.
- In use-scene-regenerator: when skipAudio=true, capture existing speech
  actions before generating new content, then map old text + audioId +
  audioUrl onto new speech actions by index, skipping the TTS step entirely.
- Add modifyAudio field to RegenerateFormValues so state survives error retries.
- Fix veo-adapter: gate generateAudio=false to models that support it
  (Veo 3+ non-fast); fast and Veo 2.0 models reject this API parameter.
- Add stage.regen.modifyAudio and stage.regen.audioKeep i18n keys to all
  5 locales (en-US, ca, zh-CN, ja-JP, ru-RU).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… empty text

Media:
- Add 'keep' option to media selector ("No regenerar" in Catalan): preserves
  existing image/video from the slide without calling any media API.
  When 'keep' is selected, the regenerator reads existing image/video elements
  (type + src) from the scene canvas before updating, determines the media type
  for the outline (so LLM includes the layout slot), then injects the old src
  values back after content generation, skipping Step 3 entirely.
- Default media selection now prefers 'keep' when the outline already had media.
- Media prompt textarea is hidden when 'keep' is selected (no prompt needed).
- Add stage.regen.mediaKeep i18n key to all 5 locales.

Narration toggle fix:
- When skipAudio=true, lastRegenValues.audioText was stored as '' causing the
  textarea to appear empty when the user activated "Modify narration" on retry.
  Fix: capture current speech action text from the scene when skipAudio=true.

Veo audio (separate hotfix already committed):
- Guard generateAudio=false to non-fast Veo 3+ models only.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Theme selector:
- Add themeId field to RegenerateParams and RegenerateFormValues.
- Fetch /api/themes when the dialog opens; show a Select (color dot +
  name) only when the list is non-empty.
- Default to the current settingsStore.themeId (the theme selected at
  generation time). Persisted in lastRegenValues for retry sessions.
- Pass themeId to /api/generate/scene-content-only so LLM prompt
  instructions reflect the chosen theme.

Bug fix:
- isSubmitDisabled now also excludes mediaType='keep', so selecting
  'No regenerar' (Keep) no longer blocks the Regenerate button.

i18n: add stage.regen.theme key to all 5 locales.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…dialog footer

- New /api/generate/narration-text endpoint — LLM generates spoken narration
  text from the slide indication (description + key points)
- Extract ModelSelectorPopover to components/generation/model-selector-popover.tsx
  with a CompactModelSelector wrapper (store-connected, zero props needed)
- Regen dialog: "Generar amb IA" button under narration textarea (visible when
  Modify narration toggle is ON) — calls narration-text endpoint, auto-fills textarea
- Regen dialog footer left: CompactModelSelector + MediaPopover controls
  (LLM model, image/video model, TTS model) mirroring the generation toolbar
- i18n: generateNarration + generatingNarration keys in all 5 locales

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When narration-text or media-prompt API calls fail (e.g. no model
configured, bad deployment), show inline error message instead of
silently swallowing it. Error text includes hint pointing to the
LLM model controls in the dialog footer bottom-left.

i18n: add aiGenerationError + aiModelHint keys in all 5 locales.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Ubuntu and others added 11 commits April 12, 2026 11:32
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds four new translation keys across all five locale files:
- modifySlide: toggle for blocking slide regeneration
- slideTitle: label for the title input field
- slideKeep: message indicating slide will be preserved
- slideWarningMediaNeeded: warning about media requiring slide regeneration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…Slide helper

- Add skipSlide optional field to RegenerateParams interface
- Export resolveSkipSlide pure helper function that determines whether to skip
  slide regeneration based on media type and existing media slot availability

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add import of resolveSkipSlide from use-scene-regenerator
- Add 7 comprehensive unit tests covering:
  - skipSlide=false always returns false
  - mediaType='none' or 'keep' returns true when skipSlide=true
  - New media (image/video) only skips if existing media slot present
  - Returns false for new media when no existing slot

All tests passing (16 total: 9 existing + 7 new).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…utline.title

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…g to dialog

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…astRegenValues

- Add missing `title` and `regenerateSlide` fields to `setLastRegenValues` call in stage.tsx so RegenerateFormValues is fully populated on retry
- Move `hasExistingMedia`/`needsNewMedia`/`showSlideWarning` constants before `handleSubmit` in regenerate-slide-dialog.tsx
- Fix `updatedOutline` construction: when `!regenerateSlide` (and not forceSlideRegen), spread original outline to preserve all fields (title, description, keyPoints) instead of always calling indicationToOutline

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…showing

Two bugs fixed:

1. skipSlide=true + new media: existing canvas element has a real URL
   (not gen_img_1), so SlideRenderer never consults the media store.
   After media generation completes, directly patch the matching element
   src with the server URL via updateScene.

2. skipSlide=false + repeated regens: gen_img_1 is a shared store key.
   If a previous regen stored gen_img_1=old-url, enqueueTasks skips it
   and the new slide briefly flashes the wrong image. Fix: resetTask
   before enqueueTasks to create a fresh pending entry, then patch
   the canvas src after generation so gen_img_1 is never kept as a
   permanent placeholder (preventing cross-slide contamination).

Add resetTask(elementId) to MediaGenerationState for single-task removal.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After generateAndStoreMedia completes, the server URL is deterministic
(e.g. /api/stages/xxx/media/gen_img_1). Patching the canvas with this
URL caused the browser to serve the old cached image instead of the
newly generated one.

Fix: append ?t=<timestamp> to the URL when patching canvas elements,
forcing a fresh fetch on each regeneration cycle.

Co-Authored-By: Claude Sonnet 4.6 <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