// app/api/generate/route.js — the generation pipeline. // The client calls this once per stage so the progress UI is honest: // stage "analyze" -> content map of the source // stage "generate" -> full assignment (with one automatic JSON-repair retry) // stage "verify" -> per-question accuracy verdicts // stage "question" -> regenerate one question / add a new one import { NextResponse } from "next/server"; import { getSettings } from "@/lib/store"; import { chat } from "@/lib/providers"; import { resolveGeneration } from "@/lib/model-caps"; import { extractJson } from "@/lib/json-utils"; import { analyzePrompt, generatePrompt, verifyPrompt, questionPrompt } from "@/lib/prompts"; import { normalizeAssignment, normalizeQuestion } from "@/lib/schema"; export const dynamic = "force-dynamic"; export const maxDuration = 600; async function chatJson(settings, prompt, opts = {}) { const raw = await chat(settings, { ...prompt, expectJson: true, ...opts }); try { return extractJson(raw); } catch (firstErr) { // One repair attempt: ask the same model to re-emit valid JSON. const fixed = await chat(settings, { system: "You convert text into strictly valid JSON. Output ONLY the corrected JSON with no commentary and no code fences.", user: "The following was supposed to be a single valid JSON object but is malformed. Re-emit it as strictly valid JSON, preserving all content:\n\n" + String(raw).slice(0, 60000), expectJson: true, temperature: 0, }); try { return extractJson(fixed); } catch { throw firstErr; } } } export async function POST(request) { try { const body = await request.json(); const { stage, source, config } = body; const settings = getSettings(); // Auto mode sizes the source budget to the selected model's context window. const { maxSourceChars } = await resolveGeneration(settings); if (!source || String(source).trim().length < 100) { throw new Error("The source material is too short to build a quality assignment from (minimum ~100 characters)."); } if (stage === "analyze") { const prompt = analyzePrompt({ source, config, maxSourceChars }); const analysis = await chatJson(settings, prompt, { maxTokens: 2500 }); return NextResponse.json({ analysis }); } if (stage === "generate") { const prompt = generatePrompt({ source, analysis: body.analysis, config, maxSourceChars }); const raw = await chatJson(settings, prompt); const assignment = normalizeAssignment(raw, config); if (!assignment.questions.length) { throw new Error("The model did not return any usable questions. Try again, or switch to a stronger model in Settings."); } return NextResponse.json({ assignment }); } if (stage === "verify") { const questions = body.questions || []; if (!questions.length) throw new Error("No questions to verify."); const prompt = verifyPrompt({ source, questions, config, maxSourceChars }); const raw = await chatJson(settings, prompt, { temperature: 0.1 }); const results = Array.isArray(raw?.results) ? raw.results : []; const byId = {}; for (const r of results) { if (!r || !r.id) continue; const verdict = r.verdict === "pass" ? "pass" : "warn"; const note = [r.issue, r.suggestedFix ? "Suggested fix: " + r.suggestedFix : ""].filter(Boolean).join(" ").trim(); byId[r.id] = { status: verdict, note: verdict === "pass" ? "" : (note || "The reviewer flagged this question — double-check it.") }; } return NextResponse.json({ verifications: byId }); } if (stage === "question") { const { type, note, existingQuestions, replacing } = body; const prompt = questionPrompt({ source, config, existingQuestions, type, note, replacing, maxSourceChars }); const raw = await chatJson(settings, prompt, { maxTokens: 2500 }); let candidate = raw; if (Array.isArray(raw)) candidate = raw[0]; else if (Array.isArray(raw?.questions) && raw.questions.length) candidate = raw.questions[0]; else if (raw?.question && typeof raw.question === "object") candidate = raw.question; const question = normalizeQuestion(candidate); if (!question) throw new Error("The model returned a question in an unexpected shape. Try again."); return NextResponse.json({ question }); } throw new Error("Unknown stage: " + stage); } catch (e) { return NextResponse.json({ error: String(e.message || e) }, { status: 400 }); } }