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>
This commit is contained in:
2026-06-05 22:14:37 +02:00
parent 2a6d203d1c
commit 11e3ce8770

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