Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion app/api/generate/scene-content/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ export async function POST(req: NextRequest) {
// Ensure outline has language from stageInfo (fallback for older outlines)
const outline: SceneOutline = {
...rawOutline,
language: rawOutline.language || (stageInfo?.language as 'zh-CN' | 'en-US') || 'zh-CN',
language:
rawOutline.language || (stageInfo?.language as 'zh-CN' | 'en-US' | 'de-DE') || 'zh-CN',
};

// ── Model resolution from request headers ──
Expand Down
18 changes: 15 additions & 3 deletions app/api/generate/scene-outlines-stream/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,11 @@ export async function POST(req: NextRequest) {

// Build prompt (same logic as generateSceneOutlinesFromRequirements)
let availableImagesText =
requirements.language === 'zh-CN' ? '无可用图片' : 'No images available';
requirements.language === 'zh-CN'
? '无可用图片'
: requirements.language === 'de-DE'
? 'Keine Bilder verfügbar'
: 'No images available';
let visionImages: Array<{ id: string; src: string }> | undefined;

if (pdfImages && pdfImages.length > 0) {
Expand Down Expand Up @@ -179,9 +183,17 @@ export async function POST(req: NextRequest) {
? pdfText.substring(0, MAX_PDF_CONTENT_CHARS)
: requirements.language === 'zh-CN'
? '无'
: 'None',
: requirements.language === 'de-DE'
? 'Keine'
: 'None',
availableImages: availableImagesText,
researchContext: researchContext || (requirements.language === 'zh-CN' ? '无' : 'None'),
researchContext:
researchContext ||
(requirements.language === 'zh-CN'
? '无'
: requirements.language === 'de-DE'
? 'Keine'
: 'None'),
mediaGenerationPolicy,
teacherContext,
});
Expand Down
17 changes: 14 additions & 3 deletions app/api/quiz-grade/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,29 @@ export async function POST(req: NextRequest) {
const { model: languageModel } = resolveModelFromHeaders(req);

const isZh = language === 'zh-CN';
const isDe = language === 'de-DE';

const systemPrompt = isZh
? `你是一位专业的教育评估专家。请根据题目和学生答案进行评分并给出简短评语。
必须以如下 JSON 格式回复(不要包含其他内容):
{"score": <0到${points}的整数>, "comment": "<一两句评语>"}`
: `You are a professional educational assessor. Grade the student's answer and provide brief feedback.
: isDe
? `Sie sind ein professioneller Bildungsprüfer. Bewerten Sie die Antwort des Schülers und geben Sie ein kurzes Feedback.
Sie müssen ausschließlich in folgendem JSON-Format antworten (kein anderer Inhalt):
{"score": <Ganze Zahl von 0 bis ${points}>, "comment": "<Ein oder zwei Sätze Feedback>"}`
: `You are a professional educational assessor. Grade the student's answer and provide brief feedback.
You must reply in the following JSON format only (no other content):
{"score": <integer from 0 to ${points}>, "comment": "<one or two sentences of feedback>"}`;

const userPrompt = isZh
? `题目:${question}
满分:${points}分
${commentPrompt ? `评分要点:${commentPrompt}\n` : ''}学生答案:${userAnswer}`
: `Question: ${question}
: isDe
? `Frage: ${question}
Volle Punktzahl: ${points} Punkte
${commentPrompt ? `Bewertungsrichtlinien: ${commentPrompt}\n` : ''}Antwort des Schülers: ${userAnswer}`
: `Question: ${question}
Full marks: ${points} points
${commentPrompt ? `Grading guidance: ${commentPrompt}\n` : ''}Student answer: ${userAnswer}`;

Expand Down Expand Up @@ -83,7 +92,9 @@ ${commentPrompt ? `Grading guidance: ${commentPrompt}\n` : ''}Student answer: ${
score: Math.round(points * 0.5),
comment: isZh
? '已作答,请参考标准答案。'
: 'Answer received. Please refer to the standard answer.',
: isDe
? 'Antwort erhalten. Bitte beachten Sie die Musterantwort.'
: 'Answer received. Please refer to the standard answer.',
};
}

Expand Down
14 changes: 12 additions & 2 deletions app/classroom/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,18 @@ export default function ClassroomDetailPage() {
// Resume media generation for any tasks not yet in IndexedDB.
// generateMediaForOutlines skips already-completed tasks automatically.
generationStartedRef.current = true;
generateMediaForOutlines(outlines, stage.id).catch((err) => {
log.warn('[Classroom] Media generation resume error:', err);

// Load generation params to get the imageMapping for manual uploads
const genParamsStr = sessionStorage.getItem('generationParams');
const params = genParamsStr ? JSON.parse(genParamsStr) : {};
const storageIds = (params.pdfImages || [])
.map((img: { storageId?: string }) => img.storageId)
.filter(Boolean);

loadImageMapping(storageIds).then((imageMapping) => {
generateMediaForOutlines(outlines, stage.id, undefined, imageMapping).catch((err) => {
log.warn('[Classroom] Media generation resume error:', err);
});
});
}
}, [loading, error, generateRemaining]);
Expand Down
46 changes: 42 additions & 4 deletions app/generation-preview/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -267,8 +267,8 @@ function GenerationPreviewContent() {
const updatedSession = {
...currentSession,
pdfText,
pdfImages,
imageStorageIds,
pdfImages: [...(currentSession.pdfImages || []), ...pdfImages],
imageStorageIds: [...(currentSession.imageStorageIds || []), ...imageStorageIds],
pdfStorageKey: undefined, // Clear so we don't re-parse
};
setSession(updatedSession);
Expand Down Expand Up @@ -339,11 +339,49 @@ function GenerationPreviewContent() {
activeSteps = getActiveSteps(currentSession);
}

// ── Handle manually uploaded images ──
const manualPdfImages: PdfImage[] = [];

if (
currentSession.uploadedImageStorageIds &&
currentSession.uploadedImageStorageIds.length > 0
) {
log.debug('Loading manually uploaded images');
currentSession.uploadedImageStorageIds.forEach((storageId, i) => {
const uploadedId = `uploaded_img_${i + 1}`;
manualPdfImages.push({
id: uploadedId,
src: '',
pageNumber: 1,
description: 'Manually uploaded image',
storageId,
});
});

// Add them to the session's pdfImages if not already there
const existingUploaded = (currentSession.pdfImages || []).some((img) =>
img.id.startsWith('uploaded_img_'),
);
if (!existingUploaded) {
currentSession = {
...currentSession,
pdfImages: [...(currentSession.pdfImages || []), ...manualPdfImages],
};
setSession(currentSession);
sessionStorage.setItem('generationSession', JSON.stringify(currentSession));
}
}

// Load imageMapping early (needed for both outline and scene generation)
let imageMapping: ImageMapping = {};
if (currentSession.imageStorageIds && currentSession.imageStorageIds.length > 0) {
const allImageStorageIds = [
...(currentSession.imageStorageIds || []),
...(currentSession.uploadedImageStorageIds || []),
];

if (allImageStorageIds.length > 0) {
log.debug('Loading images from IndexedDB');
imageMapping = await loadImageMapping(currentSession.imageStorageIds);
imageMapping = await loadImageMapping(allImageStorageIds);
} else if (
currentSession.imageMapping &&
Object.keys(currentSession.imageMapping).length > 0
Expand Down
2 changes: 2 additions & 0 deletions app/generation-preview/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export interface GenerationSessionState {
// Web search context
researchContext?: string;
researchSources?: Array<{ title: string; url: string }>;
// Manually uploaded images (along with PDF)
uploadedImageStorageIds?: string[];
}

export type GenerationStep = {
Expand Down
56 changes: 45 additions & 11 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,15 @@ const RECENT_OPEN_STORAGE_KEY = 'recentClassroomsOpen';

interface FormState {
pdfFile: File | null;
imageFiles: File[];
requirement: string;
language: 'zh-CN' | 'en-US';
language: 'zh-CN' | 'en-US' | 'de-DE';
webSearch: boolean;
}

const initialFormState: FormState = {
pdfFile: null,
imageFiles: [],
requirement: '',
language: 'zh-CN',
webSearch: false,
Expand Down Expand Up @@ -101,10 +103,15 @@ function HomePage() {
const savedLanguage = localStorage.getItem(LANGUAGE_STORAGE_KEY);
const updates: Partial<FormState> = {};
if (savedWebSearch === 'true') updates.webSearch = true;
if (savedLanguage === 'zh-CN' || savedLanguage === 'en-US') {
if (savedLanguage === 'zh-CN' || savedLanguage === 'en-US' || savedLanguage === 'de-DE') {
updates.language = savedLanguage;
} else {
const detected = navigator.language?.startsWith('zh') ? 'zh-CN' : 'en-US';
const lang = navigator.language;
const detected = lang?.startsWith('zh')
? 'zh-CN'
: lang?.startsWith('de')
? 'de-DE'
: 'en-US';
updates.language = detected;
}
if (Object.keys(updates).length > 0) {
Expand Down Expand Up @@ -251,13 +258,6 @@ function HomePage() {

try {
const userProfile = useUserProfileStore.getState();
const requirements: UserRequirements = {
requirement: form.requirement,
language: form.language,
userNickname: userProfile.nickname || undefined,
userBio: userProfile.bio || undefined,
webSearch: form.webSearch || undefined,
};

let pdfStorageKey: string | undefined;
let pdfFileName: string | undefined;
Expand All @@ -279,12 +279,31 @@ function HomePage() {
}
}

// Store manually uploaded images
const uploadedImageStorageIds: string[] = [];
for (const imageFile of form.imageFiles) {
const key = await storePdfBlob(imageFile); // use storePdfBlob for any binary data
uploadedImageStorageIds.push(key);
}

const requirements: UserRequirements = {
requirement: form.requirement,
language: form.language,
userNickname: userProfile.nickname || undefined,
userBio: userProfile.bio || undefined,
webSearch: form.webSearch || undefined,
uploadedImageStorageIds:
uploadedImageStorageIds.length > 0 ? uploadedImageStorageIds : undefined,
};

const sessionState = {
sessionId: nanoid(),
requirements,
pdfText: '',
pdfImages: [],
imageStorageIds: [],
uploadedImageStorageIds:
uploadedImageStorageIds.length > 0 ? uploadedImageStorageIds : undefined,
pdfStorageKey,
pdfFileName,
pdfProviderId,
Expand Down Expand Up @@ -338,7 +357,7 @@ function HomePage() {
}}
className="flex items-center gap-1 px-3 py-1.5 rounded-full text-xs font-bold text-gray-500 dark:text-gray-400 hover:bg-white dark:hover:bg-gray-700 hover:text-gray-800 dark:hover:text-gray-200 hover:shadow-sm transition-all"
>
{locale === 'zh-CN' ? 'CN' : 'EN'}
{locale === 'zh-CN' ? 'CN' : locale === 'de-DE' ? 'DE' : 'EN'}
</button>
{languageOpen && (
<div className="absolute top-full mt-2 right-0 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg overflow-hidden z-50 min-w-[120px]">
Expand Down Expand Up @@ -368,6 +387,19 @@ function HomePage() {
>
English
</button>
<button
onClick={() => {
setLocale('de-DE');
setLanguageOpen(false);
}}
className={cn(
'w-full px-4 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors',
locale === 'de-DE' &&
'bg-purple-50 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400',
)}
>
Deutsch
</button>
</div>
)}
</div>
Expand Down Expand Up @@ -558,6 +590,8 @@ function HomePage() {
}}
pdfFile={form.pdfFile}
onPdfFileChange={(f) => updateForm('pdfFile', f)}
imageFiles={form.imageFiles}
onImageFilesChange={(fs) => updateForm('imageFiles', fs)}
onPdfError={setError}
/>
</div>
Expand Down
Loading
Loading