feat: Status-Pipeline (reviewed), Audio-Verknüpfung+Coverage, EP-Fortschritt, Wort-Generierung
- reviewed-Status für objects/questions/statements/pairs (Constraints) - feed: nur fertige Inhalte (published + Bild + Audio-Gate), audio_url - pairs: Publish-Gating (draft→published = 409) - audios: source_table/source_id/source_field/language + Unique-Index; generate-for, generate-batch, GET /coverage; voices.js (Voice je Sprache) - auth: POST /auth/progress, /auth/me mit total_ep/streak/level; users_public EP-Spalten + user_pair_progress.earned_points - claude: POST /generate-words; words POST akzeptiert status Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -68,4 +68,68 @@ router.post('/generate-pairs', async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/claude/generate-words
|
||||
// Body: { topic, count=15, difficulty? }
|
||||
// Liefert eine Vorschau-Liste neuer Wörter (de/en/sv) — schreibt NICHTS in die DB.
|
||||
router.post('/generate-words', async (req, res, next) => {
|
||||
try {
|
||||
const apiKey = process.env.ANTHROPIC_API_KEY;
|
||||
if (!apiKey) return res.status(500).json({ error: 'ANTHROPIC_API_KEY nicht konfiguriert' });
|
||||
|
||||
const topic = (req.body.topic || '').toString().trim();
|
||||
if (!topic) return res.status(400).json({ error: 'topic fehlt' });
|
||||
const count = Math.min(Math.max(parseInt(req.body.count) || 15, 1), 50);
|
||||
const difficulty = req.body.difficulty ? ` Schwierigkeitsgrad: ${req.body.difficulty}.` : '';
|
||||
|
||||
const userPrompt = `Erstelle ${count} einzelne Vokabeln zum Thema/zur Kategorie: "${topic}".${difficulty}\n\n` +
|
||||
`Es sollen lernbare Einzelwörter sein: Nomen, Verben oder Adjektive. ` +
|
||||
`KEINE Pronomen, Artikel, Präpositionen oder Funktionswörter (kein der/die/das/ein/ich/wir/man/und/oder). ` +
|
||||
`Keine Mehrwortausdrücke, keine Duplikate.\n\n` +
|
||||
`Gib für jedes Wort die Übersetzung auf Deutsch, Englisch und Schwedisch an.\n\n` +
|
||||
`Antworte NUR mit gültigem JSON ohne Markdown:\n` +
|
||||
`{"words":[{"titel_de":"Apfel","titel_en":"apple","titel_sv":"äpple"}, ...]}`;
|
||||
|
||||
const anthropicRes = await fetch(ANTHROPIC_API_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': apiKey,
|
||||
'anthropic-version': '2023-06-01',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'claude-haiku-4-5-20251001',
|
||||
max_tokens: 4000,
|
||||
system: 'Du bist ein Vokabel-Assistent. Antworte AUSSCHLIESSLICH mit gültigem JSON, ohne Markdown-Codeblöcke, ohne Erklärungen.',
|
||||
messages: [{ role: 'user', content: userPrompt }],
|
||||
}),
|
||||
});
|
||||
|
||||
if (!anthropicRes.ok) {
|
||||
const err = await anthropicRes.json().catch(() => ({}));
|
||||
return res.status(anthropicRes.status).json({ error: err.error?.message || `Claude API Fehler ${anthropicRes.status}` });
|
||||
}
|
||||
|
||||
const data = await anthropicRes.json();
|
||||
let rawText = data.content[0].text.trim();
|
||||
const mdMatch = rawText.match(/```(?:json)?\s*([\s\S]+?)\s*```/);
|
||||
if (mdMatch) rawText = mdMatch[1];
|
||||
|
||||
const parsed = JSON.parse(rawText);
|
||||
if (!Array.isArray(parsed.words)) return res.status(500).json({ error: 'Ungültiges JSON-Format von Claude' });
|
||||
|
||||
// Nur saubere Einträge mit mindestens einer Übersetzung zurückgeben
|
||||
const words = parsed.words
|
||||
.map(w => ({
|
||||
titel_de: (w.titel_de || '').toString().trim(),
|
||||
titel_en: (w.titel_en || '').toString().trim(),
|
||||
titel_sv: (w.titel_sv || '').toString().trim(),
|
||||
}))
|
||||
.filter(w => w.titel_de || w.titel_en || w.titel_sv);
|
||||
|
||||
res.json({ words });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
Reference in New Issue
Block a user