feat: add Claude proxy endpoint for auto pair generation

POST /api/claude/generate-pairs — proxies image + objects to Claude
Haiku and returns 30 structured pairs (text/yes_no/question, easy/medium)
as JSON. Keeps the Anthropic API key server-side via ANTHROPIC_API_KEY.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-28 21:00:29 +02:00
parent 7b3ce50a17
commit 24853f710f
3 changed files with 75 additions and 0 deletions

71
src/routes/claude.js Normal file
View File

@@ -0,0 +1,71 @@
const router = require('express').Router();
const ANTHROPIC_API_URL = 'https://api.anthropic.com/v1/messages';
// POST /api/claude/generate-pairs
// Body: { imageUrl, objects: [{id, words: [{titel_de, titel_en}], selections: [{points:[{x,y}]}]}, ...], selectedObjectId }
router.post('/generate-pairs', 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 { imageUrl, objects, selectedObjectId } = req.body;
if (!imageUrl) return res.status(400).json({ error: 'imageUrl fehlt' });
if (!Array.isArray(objects) || objects.length === 0) return res.status(400).json({ error: 'objects fehlt oder leer' });
if (!selectedObjectId) return res.status(400).json({ error: 'selectedObjectId fehlt' });
const objectsDesc = objects.map((obj, i) => {
const words = (obj.words || []).map(w => w.titel_de || w.titel_en).filter(Boolean).join(', ');
const isSelected = obj.id === selectedObjectId;
let posStr = '';
if (obj.selections?.[0]?.points?.length) {
const pts = obj.selections[0].points;
const cx = (pts.reduce((s, p) => s + p.x, 0) / pts.length * 100).toFixed(0);
const cy = (pts.reduce((s, p) => s + p.y, 0) / pts.length * 100).toFixed(0);
posStr = ` (ca. ${cx}% von links, ${cy}% von oben)`;
}
return `- Objekt ${i + 1}${isSelected ? ' [DIESES OBJEKT]' : ''}: ${words || '(unbenannt)'}${posStr}`;
}).join('\n');
const userPrompt = `Analysiere das Bild. Folgende Objekte sind markiert:\n${objectsDesc}\n\nErstelle Sprachlernmaterial für das Objekt [DIESES OBJEKT] auf Deutsch — natürliche Sätze wie in einem echten Gespräch.\n\nAntworte NUR mit gültigem JSON ohne Markdown:\n{"pairs":[...]}\n\nJedes Pair braucht: "type" und "difficulty". Feldregeln:\n- type "text": {"type":"text","difficulty":"...","positive":"Aussage."}\n- type "yes_no": {"type":"yes_no","difficulty":"...","question":"Frage?","answer":true}\n- type "question": {"type":"question","difficulty":"...","question":"Frage?","positive":"Positive Aussage.","negative":"Negative Aussage."}\n\nErstelle genau 30 Pairs:\n- 10 × type "text": 5 × difficulty "easy" (max 8 Wörter, für Kinder), 5 × "medium" (815 Wörter, für Teenager)\n- 10 × type "yes_no": 5 × "easy", 5 × "medium" — mix aus answer:true und answer:false\n- 10 × type "question": 5 × "easy", 5 × "medium"\n\nRegeln: Alle Sätze auf Deutsch. Sätze müssen natürlich klingen. Keine Wiederholungen.`;
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: 8000,
system: 'Du bist ein Deutsch-Sprachlernassistent. Antworte AUSSCHLIESSLICH mit gültigem JSON, ohne Markdown-Codeblöcke, ohne Erklärungen.',
messages: [{ role: 'user', content: [
{ type: 'image', source: { type: 'url', url: imageUrl } },
{ type: 'text', text: 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();
// Strip markdown code blocks if present
const mdMatch = rawText.match(/```(?:json)?\s*([\s\S]+?)\s*```/);
if (mdMatch) rawText = mdMatch[1];
const parsed = JSON.parse(rawText);
if (!Array.isArray(parsed.pairs)) return res.status(500).json({ error: 'Ungültiges JSON-Format von Claude' });
res.json({ pairs: parsed.pairs });
} catch (err) {
next(err);
}
});
module.exports = router;