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)} />
))}
)}