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:
@@ -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 ───────────────────────────────────────────────────────
|
||||
|
||||
function PairsPanel({ selectedObject, allObjects, objectPairs, loadingPairs, onPairSaved, onPairsReload, onReloadAll }) {
|
||||
const [editingId, setEditingId] = useState(null);
|
||||
const [translatingId, setTranslatingId] = useState(null);
|
||||
const [reviewData, setReviewData] = useState(null); // { pair, content }
|
||||
const [wizardOpen, setWizardOpen] = useState(false);
|
||||
|
||||
async function handleTranslate(pair) {
|
||||
setTranslatingId(pair.id);
|
||||
@@ -1214,10 +1292,17 @@ function PairsPanel({ selectedObject, allObjects, objectPairs, loadingPairs, onP
|
||||
|
||||
return (
|
||||
<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">
|
||||
Pairs — Objekt #{selectedObject._index + 1}
|
||||
</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 className="flex-1 overflow-y-auto">
|
||||
<div className="p-3 border-b border-slate-100">
|
||||
@@ -1291,6 +1376,15 @@ function PairsPanel({ selectedObject, allObjects, objectPairs, loadingPairs, onP
|
||||
onRetranslate={() => handleRetranslate(reviewData.pair)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{wizardOpen && (
|
||||
<PairReviewWizard
|
||||
pairs={objectPairs}
|
||||
allObjects={allObjects}
|
||||
onClose={() => { setWizardOpen(false); (onReloadAll || onPairsReload)(); }}
|
||||
onPairsReload={onPairsReload}
|
||||
/>
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user