Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
3813c12
docs: add regenerate-slide implementation plan
Apr 12, 2026
5447f85
refactor: export generateAndStoreMedia from media-orchestrator
Apr 12, 2026
e1a8d65
feat: add media-prompt endpoint for auto-generating media prompts
Apr 12, 2026
ea83e52
feat: add scene-content-only endpoint for per-slide regeneration
Apr 12, 2026
aaa8d9b
fix: add slide type guard to scene-content-only endpoint
Apr 12, 2026
d41e9ec
refactor: improve media-orchestrator section comments and add JSDoc
Apr 12, 2026
89b7c20
fix: improve media-prompt validation, logging, and test coverage
Apr 12, 2026
cb42e26
fix: use resolveModelFromHeaders and improve type safety in scene-con…
Apr 12, 2026
c70660c
test: add 404 coverage and style fix for scene-content-only
Apr 12, 2026
7232f9a
feat: add useSceneRegenerator hook with 4-step incremental pipeline
Apr 12, 2026
4c2dbd6
fix: align useSceneRegenerator with spec — media guard and audio text…
Apr 12, 2026
f50f561
fix: code quality improvements in useSceneRegenerator hook
Apr 12, 2026
5edd756
feat: add regen button to SceneSidebar for active slide scenes
Apr 12, 2026
db0030a
feat: add RegenerateSlideDialog component with media auto-prompt and …
Apr 12, 2026
ceefb5e
fix: improve accessibility of regen button in SceneSidebar
Apr 12, 2026
6922ade
fix: add abort controller, accessibility improvements to RegenerateSl…
Apr 12, 2026
4affd0b
feat: add regenerate-slide state machine, review bar, and confirm mod…
Apr 12, 2026
31d8e6d
fix: address code quality issues in regenerate-slide state machine
Apr 12, 2026
057066f
fix: use local backup var in handleRegenerate to avoid stale closure …
Apr 12, 2026
a26503a
fix: widen regen dialog, propagate error message on generation failure
Apr 12, 2026
737b5e1
fix: widen regen dialog to max-w-4xl
Apr 12, 2026
d225642
fix: don't call onClose from handleSubmit to prevent dialog staying o…
Apr 12, 2026
32bb9d1
feat: add regen progress bar and disable regen button during regenera…
Apr 12, 2026
7aa7ccf
fix: enforce silent video generation — append no-audio suffix to prom…
Apr 12, 2026
3b6a105
fix(regen): restore outline on undo and disable Veo audio generation
Apr 12, 2026
fadb185
feat(regen): add 'Modify narration' toggle to skip TTS when audio unc…
Apr 12, 2026
986a40a
feat(regen): add 'Keep' media option and fix narration toggle showing…
Apr 12, 2026
6c53512
feat(regen): add theme selector and fix 'Keep' media submit button
Apr 12, 2026
fef995a
feat(regen): add AI narration generator button and model controls in …
Apr 12, 2026
5b27733
fix(regen): surface AI generation errors with model selection hint
Apr 12, 2026
594c45d
docs: add spec for regen slide block toggles and title editing
Apr 12, 2026
4c7d604
docs: add implementation plan for regen slide block toggles
Apr 12, 2026
011b588
feat(regen): add i18n keys for slide block toggle and title
Apr 12, 2026
4d43c50
feat(regen): add skipSlide to RegenerateParams and export resolveSkip…
Apr 12, 2026
0a16020
test(regen): add resolveSkipSlide unit tests
Apr 12, 2026
fb10725
feat(regen): implement skipSlide in regeneration hook and propagate o…
Apr 12, 2026
1dd4f7b
feat(regen): add slide block toggle, title input, and conflict warnin…
Apr 12, 2026
ed3b644
fix(regen): preserve outline fields when skipSlide and fix stage setL…
Apr 12, 2026
39bd858
fix(regen): patch canvas src after media generation to fix image not …
Apr 12, 2026
bcd6f65
chore: apply Prettier formatting to feature files
Apr 12, 2026
1b6a115
fix(regen): bust browser cache when patching regenerated media URL
Apr 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions app/api/generate/media-prompt/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* Auto-generates a media generation prompt from a slide indication text.
* Used when the user picks a media type not present in the original slide.
*/
import { NextRequest } from 'next/server';
import { createLogger } from '@/lib/logger';
import { apiError, apiSuccess } from '@/lib/server/api-response';
import { callLLM } from '@/lib/ai/llm';
import { resolveModelFromHeaders } from '@/lib/server/resolve-model';

const log = createLogger('MediaPrompt API');

export const maxDuration = 30;

export async function POST(req: NextRequest) {
try {
const body = await req.json();
const { indicationText, mediaType, language } = body as {
indicationText: string;
mediaType: 'image' | 'video';
language?: string;
};

if (!indicationText || !indicationText.trim()) {
return apiError('MISSING_REQUIRED_FIELD', 400, 'indicationText is required');
}
if (!mediaType) {
return apiError('MISSING_REQUIRED_FIELD', 400, 'mediaType is required');
}
if (mediaType !== 'image' && mediaType !== 'video') {
return apiError('INVALID_REQUEST', 400, 'mediaType must be "image" or "video"');
}

const { model: languageModel } = await resolveModelFromHeaders(req);

const mediaLabel = mediaType === 'image' ? 'image' : 'short video loop';
const langHint = language ? ` The course language is ${language}.` : '';

const result = await callLLM(
{
model: languageModel,
system: `You are a visual media prompt writer. Given a slide description, write a concise prompt (1–2 sentences, max 30 words) for generating a ${mediaLabel} that visually represents the slide content. Respond with ONLY the prompt text — no quotes, no explanation.${langHint}`,
prompt: indicationText,
maxOutputTokens: 150,
},
'media-prompt',
);

const prompt = result.text.trim();
log.info(
`Generated media prompt for ${mediaType}: "${prompt.length > 60 ? prompt.slice(0, 60) + '...' : prompt}"`,
);

return apiSuccess({ data: { prompt } });
} catch (error) {
log.error('media-prompt generation failed:', error);
return apiError('INTERNAL_ERROR', 500, error instanceof Error ? error.message : String(error));
}
}
54 changes: 54 additions & 0 deletions app/api/generate/narration-text/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Generates narration text for a slide from its indication (description + key points).
* Used by the "Regenerate narration" AI button in the slide regeneration dialog.
*/
import { NextRequest } from 'next/server';
import { createLogger } from '@/lib/logger';
import { apiError, apiSuccess, requireAuth } from '@/lib/server/api-response';

Check failure on line 7 in app/api/generate/narration-text/route.ts

View workflow job for this annotation

GitHub Actions / Lint, Typecheck & Unit Tests

Module '"@/lib/server/api-response"' has no exported member 'requireAuth'.
import { callLLM } from '@/lib/ai/llm';
import { resolveModelFromHeaders } from '@/lib/server/resolve-model';

const log = createLogger('NarrationText API');

export const maxDuration = 30;

export async function POST(req: NextRequest) {
const user = await requireAuth(req);
if ('status' in user && user instanceof Response) return user;

try {
const body = await req.json();
const { indicationText, language } = body as {
indicationText: string;
language?: string;
};

if (!indicationText || !indicationText.trim()) {
return apiError('MISSING_REQUIRED_FIELD', 400, 'indicationText is required');
}

const { model: languageModel } = await resolveModelFromHeaders(req);

const langHint = language
? ` The narration MUST be written in ${language} (match the language of the key points).`
: '';

const result = await callLLM(
{
model: languageModel,
system: `You are an expert educational narrator. Given a slide's description and key points, write a natural spoken narration for a teacher to deliver while presenting the slide. The narration should:\n- Be conversational and engaging, as if speaking directly to students\n- Cover the key points clearly without reading them verbatim\n- Be 2-4 sentences long (suitable for a 15-30 second voiceover)\n- NOT include stage directions, quotes, or explanations — only the spoken text itself${langHint}`,
prompt: indicationText,
maxOutputTokens: 300,
},
'narration-text',
);

const text = result.text.trim();
log.info(`Generated narration text (${text.length} chars)`);

return apiSuccess({ data: { text } });
} catch (error) {
log.error('narration-text generation failed:', error);
return apiError('INTERNAL_ERROR', 500, error instanceof Error ? error.message : String(error));
}
}
83 changes: 83 additions & 0 deletions app/api/generate/scene-content-only/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* Synchronous slide content generation — returns raw PPTElements without persisting a scene.
* Used by the per-slide regeneration flow.
*/
import { NextRequest } from 'next/server';
import { createLogger } from '@/lib/logger';
import { apiError, apiSuccess } from '@/lib/server/api-response';
import { generateSceneContentFromInput } from '@/lib/server/scene-content-generation';

Check failure on line 8 in app/api/generate/scene-content-only/route.ts

View workflow job for this annotation

GitHub Actions / Lint, Typecheck & Unit Tests

Cannot find module '@/lib/server/scene-content-generation' or its corresponding type declarations.
import { getStorageBackend } from '@/lib/server/storage';

Check failure on line 9 in app/api/generate/scene-content-only/route.ts

View workflow job for this annotation

GitHub Actions / Lint, Typecheck & Unit Tests

Cannot find module '@/lib/server/storage' or its corresponding type declarations.
import { resolveModelFromHeaders } from '@/lib/server/resolve-model';
import type { SceneOutline, GeneratedSlideContent } from '@/lib/types/generation';
import type { AgentInfo } from '@/lib/generation/generation-pipeline';

const log = createLogger('SceneContentOnly API');

export const maxDuration = 60;

export async function POST(req: NextRequest) {
try {
const body = await req.json();
const { outline, stageId, agents, themeId } = body as {
outline: SceneOutline;
stageId: string;
agents?: AgentInfo[];
themeId?: string;
};

if (!outline) {
return apiError('MISSING_REQUIRED_FIELD', 400, 'outline is required');
}
if (!stageId) {
return apiError('MISSING_REQUIRED_FIELD', 400, 'stageId is required');
}

if (outline.type !== 'slide') {
return apiError('INVALID_REQUEST', 400, 'Only slide-type outlines are supported');
}

// Load stage metadata and outlines from server storage
const backend = getStorageBackend();
const [stageData, savedOutlines] = await Promise.all([
backend.loadStage(stageId),
backend.loadOutlines(stageId),
]);

if (!stageData) {
return apiError('NOT_FOUND', 404, 'Stage not found');

Check failure on line 47 in app/api/generate/scene-content-only/route.ts

View workflow job for this annotation

GitHub Actions / Lint, Typecheck & Unit Tests

Argument of type '"NOT_FOUND"' is not assignable to parameter of type 'ApiErrorCode'.
}

const allOutlines = savedOutlines ?? [outline];
const stageInfo = {
name: stageData.stage.name ?? '',
description: stageData.stage.description,
language: stageData.stage.language,
style: stageData.stage.style,
themeId,
};

// ── Model resolution from request headers ──
const { modelString } = await resolveModelFromHeaders(req);

const result = await generateSceneContentFromInput({
outline,
allOutlines,
stageId,
stageInfo,
agents,
modelConfig: { modelString },
});

// Return only the slide content fields (elements + background)
const slideContent = result.content as GeneratedSlideContent;
return apiSuccess({
data: {
elements: slideContent.elements ?? [],
background: slideContent.background,
},
});
} catch (error) {
log.error('scene-content-only failed:', error);
return apiError('INTERNAL_ERROR', 500, error instanceof Error ? error.message : String(error));
}
}
Loading
Loading