From ae25dc942819829d793e06657a03df7bf4fb4ab6 Mon Sep 17 00:00:00 2001 From: admin Date: Wed, 10 Jun 2026 21:48:13 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Objekt-Zuweisung=20+=20Audio-Nachholen?= =?UTF-8?q?=20in=20der=20Ver=C3=B6ffentlichen-Ansicht?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Pro Pair: erkannte Objekt-Wörter als 🔗-Chips, ein Klick weist das Wort in allen Sprachen dem Objekt zu ({{wort.o:objectId}}) - '🔊 Fehlende Audios generieren' bei unvollständigen Pairs - Bundle-Refresh nach Zuweisung/Audio-Fill Co-Authored-By: Claude Fable 5 --- src/pages/Publish.jsx | 74 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 6 deletions(-) diff --git a/src/pages/Publish.jsx b/src/pages/Publish.jsx index 6e97a3b..af4eecf 100644 --- a/src/pages/Publish.jsx +++ b/src/pages/Publish.jsx @@ -75,8 +75,11 @@ function ProgressCard({ pic, onRetry }) { // ─── Pair-Zeile (flach, 3 Sprachspalten, Flag-Toggle) ──────────────────────── -function PairRow({ pair, flagged, onToggleFlag }) { +const FIELD_LABELS = { sentence: 'Frage', positive_sentence: 'Positiv', negative_sentence: 'Negativ' }; + +function PairRow({ pair, flagged, onToggleFlag, objIndexById, onAssign, assigning }) { const rows = buildRows(pair.content); + const candidates = pair.candidates || []; return (
@@ -133,18 +136,37 @@ function PairRow({ pair, flagged, onToggleFlag }) { 🚩
+ + {/* Objekt-Zuweisung: erkannte Objekt-Wörter im Satz, ein Klick pro Fund */} + {!flagged && candidates.length > 0 && ( +
+ {candidates.map((c, i) => { + const key = `${c.source_id}|${c.source_field}|${c.word_id}|${c.object_id}`; + const objNo = (objIndexById[c.object_id] ?? 0) + 1; + return ( + + ); + })} +
+ )} ); } // ─── Bereit-Karte: ganzes Bild-Bundle ───────────────────────────────────────── -function ReadyCard({ bundle, onPublished }) { +function ReadyCard({ bundle, onPublished, onRefresh }) { const [excluded, setExcluded] = useState(() => new Set()); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [notReady, setNotReady] = useState(null); const [done, setDone] = useState(null); + const [assigning, setAssigning] = useState(null); + const [audioFilling, setAudioFilling] = useState(false); if (!bundle) { return ( @@ -170,6 +192,33 @@ function ReadyCard({ bundle, onPublished }) { }); } + const objIndexById = Object.fromEntries(bundle.objects.map((o, i) => [o.id, i])); + + async function assign(c) { + const key = `${c.source_id}|${c.source_field}|${c.word_id}|${c.object_id}`; + setAssigning(key); setError(null); + try { + const res = await apiPost('/pipeline/assign-object', { + source_table: c.source_table, source_id: c.source_id, source_field: c.source_field, + object_id: c.object_id, word_id: c.word_id, + }); + if (res.skipped_langs?.length) + setError(`Zugewiesen, aber in ${res.skipped_langs.join(', ')} nicht gefunden (abweichende Wortform) — ggf. dort manuell prüfen.`); + await onRefresh?.(); + } catch (e) { setError(e.payload?.error || e.message); } + finally { setAssigning(null); } + } + + async function fillAudio() { + setAudioFilling(true); setError(null); + try { + const res = await apiPost(`/pipeline/picture/${bundle.picture.id}/audio-fill`, {}); + if (res.failed) setError(`${res.generated} Audios erzeugt, ${res.failed} fehlgeschlagen: ${res.errors?.[0]?.error || ''}`); + await onRefresh?.(); + } catch (e) { setError(e.payload?.error || e.message); } + finally { setAudioFilling(false); } + } + async function publish() { setBusy(true); setError(null); setNotReady(null); try { @@ -212,8 +261,14 @@ function ReadyCard({ bundle, onPublished }) { {bundle.objects.length} Objekt{bundle.objects.length !== 1 ? 'e' : ''} · {allPairs.length} Pairs · {wordCount} Wörter {incompletePairs.length > 0 && ( -
- ⚠ {incompletePairs.length} Pair{incompletePairs.length !== 1 ? 's' : ''} unvollständig (Text/Audio) — flaggen oder erst vervollständigen +
+ + ⚠ {incompletePairs.length} Pair{incompletePairs.length !== 1 ? 's' : ''} unvollständig (Text/Audio) + +
)}
@@ -230,7 +285,8 @@ function ReadyCard({ bundle, onPublished }) {
{obj.pairs.map(p => ( - toggleFlag(p.id)} /> + toggleFlag(p.id)} + objIndexById={objIndexById} onAssign={assign} assigning={assigning} /> ))} {obj.pairs.length === 0 &&
Keine Pairs
}
@@ -300,6 +356,11 @@ export default function Publish() { catch (e) { setError(e.payload?.error || e.message); } } + async function refreshBundle(id) { + const b = await apiFetch(`/pipeline/picture/${id}/bundle`); + setBundles(prev => ({ ...prev, [id]: b })); + } + const rows = overview || []; const inFlight = rows.filter(p => ['queued', 'running'].includes(p.pipeline_status)); const failed = rows.filter(p => p.pipeline_status === 'failed'); @@ -334,7 +395,8 @@ export default function Publish() { ))} {ready.map(pic => ( - load()} /> + load()} + onRefresh={() => refreshBundle(pic.id)} /> ))} )}