refactor: auto pairs now loops all objects, no selection needed

Button moves to ObjectListPanel (visible when ≥1 object exists).
One Claude call per object — image + that object's words & coordinates.
Progress shows "Objekt 2/3 — Pair 12/30". Pair saving logic extracted
into shared savePairsForObject() helper.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-28 21:15:01 +02:00
parent d7ba2c2c47
commit b39a3cca9f

View File

@@ -744,10 +744,10 @@ function EditPairForm({ pair, allObjects, onSaved, onCancel, onDeleted }) {
// ─── Left panel: Object list ──────────────────────────────────────────────────
function ObjectListPanel({ objects, loadingObjects, mode, selectedObjectId, onAddObject, onSelectObject }) {
function ObjectListPanel({ objects, loadingObjects, mode, selectedObjectId, onAddObject, onSelectObject, currentPicture }) {
return (
<aside className="w-1/5 min-w-[180px] border-r border-slate-200 bg-white flex flex-col overflow-hidden">
<div className="px-3 py-2.5 border-b border-slate-100 bg-slate-50 flex-shrink-0">
<div className="px-3 py-2.5 border-b border-slate-100 bg-slate-50 flex-shrink-0 space-y-1.5">
<button
onClick={onAddObject}
className={`w-full flex items-center justify-center gap-1.5 text-xs font-medium py-1.5 rounded-lg transition-colors
@@ -757,6 +757,9 @@ function ObjectListPanel({ objects, loadingObjects, mode, selectedObjectId, onAd
>
<span className="text-base leading-none"></span> Objekt hinzufügen
</button>
{objects.length > 0 && (
<AutoCreateAllButton currentPicture={currentPicture} objects={objects} />
)}
</div>
<div className="flex-1 overflow-y-auto p-3 space-y-2">
{loadingObjects && (
@@ -887,58 +890,80 @@ async function callClaudeForPairs(picture, allObjects, selectedObjectId) {
return res.pairs;
}
function AutoCreateButton({ currentPicture, allObjects, selectedObject, onPairSaved }) {
async function savePairsForObject(pairs, objectId, lang, onPairSaved, onProgress) {
for (let i = 0; i < pairs.length; i++) {
const p = pairs[i];
let questionId = null;
if (p.type !== 'text' && p.question?.trim()) {
const q = await apiPost('/questions', { [`sentence_${lang}`]: p.question.trim(), status: 'draft' });
questionId = q.id;
}
let posStmtId = null;
if ((p.type === 'text' || p.type === 'question') && p.positive?.trim()) {
const s = await apiPost('/statements', { status: 'draft', [`positive_sentence_${lang}`]: p.positive.trim() });
posStmtId = s.id;
} else if (p.type === 'yes_no') {
const s = await apiPost('/statements', { status: 'draft', answer: p.answer ?? null });
posStmtId = s.id;
}
let negStmtId = null;
if (p.type === 'question' && p.negative?.trim()) {
const s = await apiPost('/statements', { status: 'draft', [`negative_sentence_${lang}`]: p.negative.trim() });
negStmtId = s.id;
}
const created = await apiPost('/pairs', {
answer_type: p.type,
question_id: questionId,
positive_statement_id: posStmtId,
negative_statement_id: negStmtId,
status: 'draft',
});
await apiLink(`/objects/${objectId}/pairs/${created.id}`);
onPairSaved?.(created, objectId);
onProgress?.(i + 1, pairs.length);
}
}
function AutoCreateAllButton({ currentPicture, objects }) {
const lang = getUserLang();
const [state, setState] = useState('idle'); // idle | loading | done | error
const [progress, setProgress] = useState({ done: 0, total: 0 });
const [progress, setProgress] = useState({ objIdx: 0, objTotal: 0, pairDone: 0, pairTotal: 0 });
const [totalCreated, setTotalCreated] = useState(0);
const [errorMsg, setErrorMsg] = useState('');
async function handleAutoCreate() {
async function handleAutoCreateAll() {
if (!currentPicture?.picture_link) {
alert('Dieses Bild hat keinen Link für die KI-Analyse');
return;
}
setState('loading');
setProgress({ done: 0, total: 0 });
setTotalCreated(0);
setProgress({ objIdx: 0, objTotal: objects.length, pairDone: 0, pairTotal: 0 });
setErrorMsg('');
let total = 0;
try {
const pairs = await callClaudeForPairs(currentPicture, allObjects, selectedObject.id);
setProgress({ done: 0, total: pairs.length });
for (let oi = 0; oi < objects.length; oi++) {
const obj = objects[oi];
setProgress({ objIdx: oi, objTotal: objects.length, pairDone: 0, pairTotal: 0 });
for (let i = 0; i < pairs.length; i++) {
const p = pairs[i];
const pairs = await callClaudeForPairs(currentPicture, objects, obj.id);
setProgress(prev => ({ ...prev, pairTotal: pairs.length }));
let questionId = null;
if (p.type !== 'text' && p.question?.trim()) {
const q = await apiPost('/questions', { [`sentence_${lang}`]: p.question.trim(), status: 'draft' });
questionId = q.id;
}
let posStmtId = null;
if ((p.type === 'text' || p.type === 'question') && p.positive?.trim()) {
const s = await apiPost('/statements', { status: 'draft', [`positive_sentence_${lang}`]: p.positive.trim() });
posStmtId = s.id;
} else if (p.type === 'yes_no') {
const s = await apiPost('/statements', { status: 'draft', answer: p.answer ?? null });
posStmtId = s.id;
}
let negStmtId = null;
if (p.type === 'question' && p.negative?.trim()) {
const s = await apiPost('/statements', { status: 'draft', [`negative_sentence_${lang}`]: p.negative.trim() });
negStmtId = s.id;
}
const created = await apiPost('/pairs', {
answer_type: p.type,
question_id: questionId,
positive_statement_id: posStmtId,
negative_statement_id: negStmtId,
status: 'draft',
await savePairsForObject(pairs, obj.id, lang, null, (done, total) => {
setProgress(prev => ({ ...prev, pairDone: done, pairTotal: total }));
});
await apiLink(`/objects/${selectedObject.id}/pairs/${created.id}`);
onPairSaved(created);
setProgress({ done: i + 1, total: pairs.length });
total += pairs.length;
setTotalCreated(total);
}
setState('done');
setTimeout(() => setState('idle'), 4000);
setTimeout(() => setState('idle'), 5000);
} catch (e) {
setErrorMsg(e.message);
setState('error');
@@ -946,15 +971,18 @@ function AutoCreateButton({ currentPicture, allObjects, selectedObject, onPairSa
}
if (state === 'loading') {
const { objIdx, objTotal, pairDone, pairTotal } = progress;
return (
<div className="w-full py-2 px-3 text-xs text-violet-700 bg-violet-50 border border-violet-200 rounded-lg flex items-center gap-2">
<svg className="animate-spin h-3 w-3 text-violet-500 shrink-0" viewBox="0 0 24 24" fill="none">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"/>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"/>
</svg>
{progress.total === 0
? 'KI analysiert Bild'
: `Erstelle Pairs… ${progress.done}/${progress.total}`}
<span>
{pairTotal === 0
? `Objekt ${objIdx + 1}/${objTotal} — KI analysiert…`
: `Objekt ${objIdx + 1}/${objTotal} — Pair ${pairDone}/${pairTotal}`}
</span>
</div>
);
}
@@ -962,7 +990,7 @@ function AutoCreateButton({ currentPicture, allObjects, selectedObject, onPairSa
if (state === 'done') {
return (
<div className="w-full py-2 px-3 text-xs text-green-700 bg-green-50 border border-green-200 rounded-lg font-medium text-center">
✓ {progress.total} Pairs automatisch erstellt
✓ {totalCreated} Pairs für {objects.length} Objekte erstellt
</div>
);
}
@@ -982,7 +1010,7 @@ function AutoCreateButton({ currentPicture, allObjects, selectedObject, onPairSa
}
return (
<button onClick={handleAutoCreate}
<button onClick={handleAutoCreateAll}
className="w-full py-1.5 text-xs font-medium rounded-lg bg-violet-600 hover:bg-violet-700 text-white transition-colors flex items-center justify-center gap-1.5">
✨ Auto Pairs erstellen
</button>
@@ -991,7 +1019,7 @@ function AutoCreateButton({ currentPicture, allObjects, selectedObject, onPairSa
// ─── Right panel: Pairs ───────────────────────────────────────────────────────
function PairsPanel({ selectedObject, allObjects, currentPicture, objectPairs, loadingPairs, onPairSaved, onPairsReload }) {
function PairsPanel({ selectedObject, allObjects, objectPairs, loadingPairs, onPairSaved, onPairsReload }) {
const [editingId, setEditingId] = useState(null);
if (!selectedObject) {
@@ -1012,13 +1040,7 @@ function PairsPanel({ selectedObject, allObjects, currentPicture, objectPairs, l
</h2>
</div>
<div className="flex-1 overflow-y-auto">
<div className="p-3 border-b border-slate-100 space-y-2">
<AutoCreateButton
currentPicture={currentPicture}
allObjects={allObjects}
selectedObject={selectedObject}
onPairSaved={onPairSaved}
/>
<div className="p-3 border-b border-slate-100">
<PairForm objectId={selectedObject.id} allObjects={allObjects} onPairSaved={pair => onPairSaved(pair)} />
</div>
<div className="p-3 space-y-3">
@@ -1381,6 +1403,7 @@ export default function ContentCreation() {
selectedObjectId={selectedObjectId}
onAddObject={handleAddObject}
onSelectObject={handleSelectObject}
currentPicture={currentPicture}
/>
<ImageCanvas
@@ -1412,7 +1435,6 @@ export default function ContentCreation() {
<PairsPanel
selectedObject={selectedObjectWithIndex}
allObjects={objects}
currentPicture={currentPicture}
objectPairs={objectPairs}
loadingPairs={loadingPairs}
onPairSaved={pair => setObjectPairs(prev => [pair, ...prev])}