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:
@@ -744,10 +744,10 @@ function EditPairForm({ pair, allObjects, onSaved, onCancel, onDeleted }) {
|
|||||||
|
|
||||||
// ─── Left panel: Object list ──────────────────────────────────────────────────
|
// ─── Left panel: Object list ──────────────────────────────────────────────────
|
||||||
|
|
||||||
function ObjectListPanel({ objects, loadingObjects, mode, selectedObjectId, onAddObject, onSelectObject }) {
|
function ObjectListPanel({ objects, loadingObjects, mode, selectedObjectId, onAddObject, onSelectObject, currentPicture }) {
|
||||||
return (
|
return (
|
||||||
<aside className="w-1/5 min-w-[180px] border-r border-slate-200 bg-white flex flex-col overflow-hidden">
|
<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
|
<button
|
||||||
onClick={onAddObject}
|
onClick={onAddObject}
|
||||||
className={`w-full flex items-center justify-center gap-1.5 text-xs font-medium py-1.5 rounded-lg transition-colors
|
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
|
<span className="text-base leading-none">+</span> Objekt hinzufügen
|
||||||
</button>
|
</button>
|
||||||
|
{objects.length > 0 && (
|
||||||
|
<AutoCreateAllButton currentPicture={currentPicture} objects={objects} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-y-auto p-3 space-y-2">
|
<div className="flex-1 overflow-y-auto p-3 space-y-2">
|
||||||
{loadingObjects && (
|
{loadingObjects && (
|
||||||
@@ -887,58 +890,80 @@ async function callClaudeForPairs(picture, allObjects, selectedObjectId) {
|
|||||||
return res.pairs;
|
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 lang = getUserLang();
|
||||||
const [state, setState] = useState('idle'); // idle | loading | done | error
|
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('');
|
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');
|
setState('loading');
|
||||||
setProgress({ done: 0, total: 0 });
|
setTotalCreated(0);
|
||||||
|
setProgress({ objIdx: 0, objTotal: objects.length, pairDone: 0, pairTotal: 0 });
|
||||||
setErrorMsg('');
|
setErrorMsg('');
|
||||||
|
|
||||||
|
let total = 0;
|
||||||
try {
|
try {
|
||||||
const pairs = await callClaudeForPairs(currentPicture, allObjects, selectedObject.id);
|
for (let oi = 0; oi < objects.length; oi++) {
|
||||||
setProgress({ done: 0, total: pairs.length });
|
const obj = objects[oi];
|
||||||
|
setProgress({ objIdx: oi, objTotal: objects.length, pairDone: 0, pairTotal: 0 });
|
||||||
|
|
||||||
for (let i = 0; i < pairs.length; i++) {
|
const pairs = await callClaudeForPairs(currentPicture, objects, obj.id);
|
||||||
const p = pairs[i];
|
setProgress(prev => ({ ...prev, pairTotal: pairs.length }));
|
||||||
|
|
||||||
let questionId = null;
|
await savePairsForObject(pairs, obj.id, lang, null, (done, total) => {
|
||||||
if (p.type !== 'text' && p.question?.trim()) {
|
setProgress(prev => ({ ...prev, pairDone: done, pairTotal: total }));
|
||||||
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/${selectedObject.id}/pairs/${created.id}`);
|
|
||||||
onPairSaved(created);
|
total += pairs.length;
|
||||||
setProgress({ done: i + 1, total: pairs.length });
|
setTotalCreated(total);
|
||||||
}
|
}
|
||||||
|
|
||||||
setState('done');
|
setState('done');
|
||||||
setTimeout(() => setState('idle'), 4000);
|
setTimeout(() => setState('idle'), 5000);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setErrorMsg(e.message);
|
setErrorMsg(e.message);
|
||||||
setState('error');
|
setState('error');
|
||||||
@@ -946,15 +971,18 @@ function AutoCreateButton({ currentPicture, allObjects, selectedObject, onPairSa
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (state === 'loading') {
|
if (state === 'loading') {
|
||||||
|
const { objIdx, objTotal, pairDone, pairTotal } = progress;
|
||||||
return (
|
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">
|
<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">
|
<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"/>
|
<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"/>
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"/>
|
||||||
</svg>
|
</svg>
|
||||||
{progress.total === 0
|
<span>
|
||||||
? 'KI analysiert Bild…'
|
{pairTotal === 0
|
||||||
: `Erstelle Pairs… ${progress.done}/${progress.total}`}
|
? `Objekt ${objIdx + 1}/${objTotal} — KI analysiert…`
|
||||||
|
: `Objekt ${objIdx + 1}/${objTotal} — Pair ${pairDone}/${pairTotal}`}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -962,7 +990,7 @@ function AutoCreateButton({ currentPicture, allObjects, selectedObject, onPairSa
|
|||||||
if (state === 'done') {
|
if (state === 'done') {
|
||||||
return (
|
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">
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -982,7 +1010,7 @@ function AutoCreateButton({ currentPicture, allObjects, selectedObject, onPairSa
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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">
|
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
|
✨ Auto Pairs erstellen
|
||||||
</button>
|
</button>
|
||||||
@@ -991,7 +1019,7 @@ function AutoCreateButton({ currentPicture, allObjects, selectedObject, onPairSa
|
|||||||
|
|
||||||
// ─── Right panel: Pairs ───────────────────────────────────────────────────────
|
// ─── 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);
|
const [editingId, setEditingId] = useState(null);
|
||||||
|
|
||||||
if (!selectedObject) {
|
if (!selectedObject) {
|
||||||
@@ -1012,13 +1040,7 @@ function PairsPanel({ selectedObject, allObjects, currentPicture, objectPairs, l
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
<div className="p-3 border-b border-slate-100 space-y-2">
|
<div className="p-3 border-b border-slate-100">
|
||||||
<AutoCreateButton
|
|
||||||
currentPicture={currentPicture}
|
|
||||||
allObjects={allObjects}
|
|
||||||
selectedObject={selectedObject}
|
|
||||||
onPairSaved={onPairSaved}
|
|
||||||
/>
|
|
||||||
<PairForm objectId={selectedObject.id} allObjects={allObjects} onPairSaved={pair => onPairSaved(pair)} />
|
<PairForm objectId={selectedObject.id} allObjects={allObjects} onPairSaved={pair => onPairSaved(pair)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 space-y-3">
|
<div className="p-3 space-y-3">
|
||||||
@@ -1381,6 +1403,7 @@ export default function ContentCreation() {
|
|||||||
selectedObjectId={selectedObjectId}
|
selectedObjectId={selectedObjectId}
|
||||||
onAddObject={handleAddObject}
|
onAddObject={handleAddObject}
|
||||||
onSelectObject={handleSelectObject}
|
onSelectObject={handleSelectObject}
|
||||||
|
currentPicture={currentPicture}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ImageCanvas
|
<ImageCanvas
|
||||||
@@ -1412,7 +1435,6 @@ export default function ContentCreation() {
|
|||||||
<PairsPanel
|
<PairsPanel
|
||||||
selectedObject={selectedObjectWithIndex}
|
selectedObject={selectedObjectWithIndex}
|
||||||
allObjects={objects}
|
allObjects={objects}
|
||||||
currentPicture={currentPicture}
|
|
||||||
objectPairs={objectPairs}
|
objectPairs={objectPairs}
|
||||||
loadingPairs={loadingPairs}
|
loadingPairs={loadingPairs}
|
||||||
onPairSaved={pair => setObjectPairs(prev => [pair, ...prev])}
|
onPairSaved={pair => setObjectPairs(prev => [pair, ...prev])}
|
||||||
|
|||||||
Reference in New Issue
Block a user