Compare commits

..

2 Commits

Author SHA1 Message Date
11e3ce8770 feat: Geführter Pair-Review-Flow (Wizard) pro Objekt
'🚀 Review-Flow starten' läuft die Pairs des Objekts der Reihe nach durch:
Inline bearbeiten + speichern oder 'Speichern & übersetzen' (Prüf-Grid),
Reviewed/Blocked — danach automatisch das nächste Pair, bis alle durch sind.
Orchestrierungs-Hülle um EditPairForm + PairReviewModal, keine Logik dupliziert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 22:14:37 +02:00
2a6d203d1c fix: Negativ-Zeile im Review-Modal immer zeigen (fehlt sichtbar machen)
question/word-Pairs zeigen die Negativ-Zeile jetzt auch wenn leer ('fehlt'
statt stilles Ausblenden), damit eine fehlende Negativ-Antwort auffaellt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 22:14:28 +02:00
2 changed files with 102 additions and 7 deletions

View File

@@ -42,7 +42,7 @@ function buildRows(content) {
} else if (type === 'word') { } else if (type === 'word') {
rows.push({ kind: 'lang', label: 'Positiv-Wörter', color: 'text-green-700', rows.push({ kind: 'lang', label: 'Positiv-Wörter', color: 'text-green-700',
cell: wordsCell(content.positive) }); cell: wordsCell(content.positive) });
if (content.negative?.words?.length) // 'word' braucht laut Datenmodell Negativ-Wörter → Zeile immer zeigen, fehlende sichtbar machen
rows.push({ kind: 'lang', label: 'Negativ-Wörter', color: 'text-red-600', rows.push({ kind: 'lang', label: 'Negativ-Wörter', color: 'text-red-600',
cell: wordsCell(content.negative) }); cell: wordsCell(content.negative) });
} else { } else {
@@ -50,9 +50,10 @@ function buildRows(content) {
if (content.positive) if (content.positive)
rows.push({ kind: 'lang', label: 'Positiv', color: 'text-green-700', rows.push({ kind: 'lang', label: 'Positiv', color: 'text-green-700',
cell: sentenceCell(content.positive, 'positive_sentence') }); cell: sentenceCell(content.positive, 'positive_sentence') });
const negHasContent = content.negative && // 'question' = Frage + Positiv + Negativ → Negativ-Zeile immer zeigen, auch wenn leer
LANGS.some(l => (content.negative.sentence?.[`negative_sentence_${l}`] || '').trim()); // ('fehlt' statt stilles Ausblenden, damit eine fehlende Negativ-Antwort auffällt).
if (negHasContent) // 'text' hat per Definition kein Negativ.
if (type === 'question')
rows.push({ kind: 'lang', label: 'Negativ', color: 'text-red-600', rows.push({ kind: 'lang', label: 'Negativ', color: 'text-red-600',
cell: sentenceCell(content.negative, 'negative_sentence') }); cell: sentenceCell(content.negative, 'negative_sentence') });
} }

View File

@@ -1180,12 +1180,90 @@ function AutoCreateAllButton({ currentPicture, objects }) {
); );
} }
// ─── Geführter Review-Flow (Wizard) ──────────────────────────────────────────
// Läuft die Pairs des ausgewählten Objekts der Reihe nach durch. Wiederverwendung:
// EditPairForm (Editor + alle Aktionen) und PairReviewModal (Übersetzungs-Prüf-Grid).
function PairReviewWizard({ pairs, allObjects, onClose, onPairsReload }) {
const [queue] = useState(() => pairs); // Snapshot — stabile Navigation
const [index, setIndex] = useState(0);
const [reviewData, setReviewData] = useState(null); // { pair, content } | null
const pair = queue[index];
function advance() {
if (index + 1 < queue.length) { setIndex(index + 1); setReviewData(null); }
else onClose();
}
async function handleTranslate(p) {
const res = await apiPost(`/pairs/${p.id}/translate`, {});
setReviewData({ pair: p, content: res.content });
}
async function handleRetranslate(p) {
const res = await apiPost(`/pairs/${p.id}/translate`, { overwrite: true });
setReviewData({ pair: p, content: res.content });
}
if (!pair) { onClose(); return null; }
return (
<div className="fixed inset-0 z-40 flex items-start justify-center bg-black/40 backdrop-blur-sm p-4 overflow-y-auto">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-lg my-8 flex flex-col">
{/* Header mit Fortschritt + Navigation */}
<div className="flex items-center gap-2 px-5 py-3 border-b border-slate-200">
<span className="text-lg">🚀</span>
<span className="font-semibold text-slate-800 text-sm">
Pair {index + 1}/{queue.length}
</span>
<span className="text-xs bg-slate-200 text-slate-600 rounded px-1.5 py-0.5">{pair.answer_type}</span>
<span className="font-mono text-xs text-slate-400">{pair.id?.slice(0, 8)}…</span>
<div className="ml-auto flex items-center gap-1.5">
<button onClick={() => setIndex(i => Math.max(0, i - 1))} disabled={index === 0}
className="text-xs px-2 py-1 rounded-lg text-slate-500 hover:bg-slate-100 disabled:opacity-30"
title="Vorheriges Pair"> Zurück</button>
<button onClick={advance}
className="text-xs px-2 py-1 rounded-lg text-slate-500 hover:bg-slate-100"
title="Ohne Änderung zum nächsten Pair">Überspringen →</button>
<button onClick={onClose}
className="text-slate-400 hover:text-slate-600 text-xl leading-none px-1" aria-label="Flow beenden">×</button>
</div>
</div>
{/* Body — vollständiger Editor pro Pair (remountet via key) */}
<div className="px-5 py-4 overflow-y-auto">
<EditPairForm
key={pair.id}
pair={pair}
allObjects={allObjects}
onSaved={() => { onPairsReload(); advance(); }}
onCancel={onClose}
onDeleted={() => { onPairsReload(); advance(); }}
onSavedAndTranslate={(savedPair) => handleTranslate(savedPair)}
/>
</div>
</div>
{reviewData && (
<PairReviewModal
pair={reviewData.pair}
content={reviewData.content}
onClose={() => setReviewData(null)}
onDone={() => { onPairsReload(); advance(); }}
onRetranslate={() => handleRetranslate(reviewData.pair)}
/>
)}
</div>
);
}
// ─── Right panel: Pairs ─────────────────────────────────────────────────────── // ─── Right panel: Pairs ───────────────────────────────────────────────────────
function PairsPanel({ selectedObject, allObjects, objectPairs, loadingPairs, onPairSaved, onPairsReload, onReloadAll }) { function PairsPanel({ selectedObject, allObjects, objectPairs, loadingPairs, onPairSaved, onPairsReload, onReloadAll }) {
const [editingId, setEditingId] = useState(null); const [editingId, setEditingId] = useState(null);
const [translatingId, setTranslatingId] = useState(null); const [translatingId, setTranslatingId] = useState(null);
const [reviewData, setReviewData] = useState(null); // { pair, content } const [reviewData, setReviewData] = useState(null); // { pair, content }
const [wizardOpen, setWizardOpen] = useState(false);
async function handleTranslate(pair) { async function handleTranslate(pair) {
setTranslatingId(pair.id); setTranslatingId(pair.id);
@@ -1214,10 +1292,17 @@ function PairsPanel({ selectedObject, allObjects, objectPairs, loadingPairs, onP
return ( return (
<aside className="w-2/5 border-l border-slate-200 bg-white flex flex-col overflow-hidden"> <aside className="w-2/5 border-l 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 flex items-center gap-2">
<h2 className="text-xs font-bold text-slate-500 uppercase tracking-wider"> <h2 className="text-xs font-bold text-slate-500 uppercase tracking-wider">
Pairs — Objekt #{selectedObject._index + 1} Pairs — Objekt #{selectedObject._index + 1}
</h2> </h2>
{objectPairs.length > 0 && (
<button onClick={() => setWizardOpen(true)}
className="ml-auto text-xs font-medium text-violet-700 bg-violet-50 hover:bg-violet-100 px-2 py-0.5 rounded transition-colors"
title="Alle Pairs dieses Objekts nacheinander prüfen">
🚀 Review-Flow starten
</button>
)}
</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"> <div className="p-3 border-b border-slate-100">
@@ -1291,6 +1376,15 @@ function PairsPanel({ selectedObject, allObjects, objectPairs, loadingPairs, onP
onRetranslate={() => handleRetranslate(reviewData.pair)} onRetranslate={() => handleRetranslate(reviewData.pair)}
/> />
)} )}
{wizardOpen && (
<PairReviewWizard
pairs={objectPairs}
allObjects={allObjects}
onClose={() => { setWizardOpen(false); (onReloadAll || onPairsReload)(); }}
onPairsReload={onPairsReload}
/>
)}
</aside> </aside>
); );
} }