diff --git a/.env.example b/.env.example index ff37954..35e67de 100644 --- a/.env.example +++ b/.env.example @@ -8,3 +8,6 @@ DB_SSL=false # API PORT=3000 + +# Anthropic (für Auto-Pairs-Generierung) +ANTHROPIC_API_KEY=sk-ant-... diff --git a/src/index.js b/src/index.js index 0a75a36..3894a57 100644 --- a/src/index.js +++ b/src/index.js @@ -40,6 +40,7 @@ app.use('/api/languages', auth, require('./routes/languages')); app.use('/api/user-names', auth, require('./routes/user-names')); app.use('/api/users-public', auth, require('./routes/users-public')); app.use('/api/users', auth, require('./routes/users')); +app.use('/api/claude', auth, require('./routes/claude')); // 404 app.use((req, res) => { diff --git a/src/routes/claude.js b/src/routes/claude.js new file mode 100644 index 0000000..1afb22f --- /dev/null +++ b/src/routes/claude.js @@ -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" (8–15 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;