refactor: route Claude API call through backend proxy
Removes direct browser→Anthropic call (CORS issue). Now calls /api/claude/generate-pairs on the snakkimo-API, which holds the API key server-side. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -870,56 +870,21 @@ function ObjectAddPanel({ currentPolygon, savedSelections, objectWords, onAddObj
|
|||||||
// ─── Auto Create Pairs ───────────────────────────────────────────────────────
|
// ─── Auto Create Pairs ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function callClaudeForPairs(picture, allObjects, selectedObjectId) {
|
async function callClaudeForPairs(picture, allObjects, selectedObjectId) {
|
||||||
const apiKey = import.meta.env.VITE_ANTHROPIC_API_KEY;
|
|
||||||
if (!apiKey) throw new Error('VITE_ANTHROPIC_API_KEY nicht gesetzt');
|
|
||||||
if (!picture?.picture_link) throw new Error('Dieses Bild hat keinen Link für die KI-Analyse');
|
if (!picture?.picture_link) throw new Error('Dieses Bild hat keinen Link für die KI-Analyse');
|
||||||
|
|
||||||
const objectsDesc = allObjects.map((obj, i) => {
|
const objects = allObjects.map(obj => ({
|
||||||
const words = (obj._words || []).map(w => w.titel_de || w.titel_en).filter(Boolean).join(', ');
|
id: obj.id,
|
||||||
const isSelected = obj.id === selectedObjectId;
|
words: obj._words || [],
|
||||||
let posStr = '';
|
selections: obj.selections || [],
|
||||||
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 res = await apiFetch('/claude/generate-pairs', {
|
||||||
|
|
||||||
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
body: JSON.stringify({ imageUrl: picture.picture_link, objects, selectedObjectId }),
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'x-api-key': apiKey,
|
|
||||||
'anthropic-version': '2023-06-01',
|
|
||||||
'anthropic-dangerous-request-browser': 'true',
|
|
||||||
},
|
|
||||||
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: picture.picture_link } },
|
|
||||||
{ type: 'text', text: userPrompt },
|
|
||||||
]}],
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!Array.isArray(res?.pairs)) throw new Error('Ungültiges Format vom Server');
|
||||||
const err = await res.json().catch(() => ({}));
|
return res.pairs;
|
||||||
throw new Error(err.error?.message || `Claude API Fehler ${res.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await res.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.pairs)) throw new Error('Ungültiges JSON-Format: "pairs" fehlt');
|
|
||||||
return parsed.pairs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function AutoCreateButton({ currentPicture, allObjects, selectedObject, onPairSaved }) {
|
function AutoCreateButton({ currentPicture, allObjects, selectedObject, onPairSaved }) {
|
||||||
|
|||||||
Reference in New Issue
Block a user