feat: Pro-Pair Übersetzen-&-Prüfen mit Review-Modal
- PairReviewModal: zeigt Frage/Positiv/Negativ in de/en/sv (Wörter bei word-Typ) zum Gegenprüfen, mit Reviewed/Blocked-Buttons; bei 409 wird die missing-Liste inline angezeigt. - ContentCreation: pro Pair-Karte '🪄 Übersetzen & prüfen' (ruft /pairs/:id/translate, öffnet Modal); nach Review werden Pairs, Objekte und Bilder neu geladen. - api.js: Fehler-Payload (z.B. { missing }) wird am Error durchgereicht. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import { useEffect, useState, useRef, useCallback, useMemo } from 'react';
|
||||
import Layout from '../components/Layout';
|
||||
import { apiFetch, apiPost, apiPatch, apiLink, apiUnlink, apiDelete, getUserLang } from '../lib/api';
|
||||
import { STATUS_COLORS } from '../lib/tables';
|
||||
import PairReviewModal from '../components/PairReviewModal';
|
||||
|
||||
// ─── Word / placeholder helpers ───────────────────────────────────────────────
|
||||
|
||||
@@ -662,8 +663,11 @@ function EditPairForm({ pair, allObjects, onSaved, onCancel, onDeleted }) {
|
||||
onSaved();
|
||||
} catch (e) {
|
||||
// Server gibt bei fehlenden Übersetzungen 409 mit `missing: [...]` zurück
|
||||
const m = (e.message || '').replace(/^Übersetzung unvollständig\s*/, '');
|
||||
alert('Reviewed nicht möglich: ' + (m || e.message));
|
||||
const missing = e.payload?.missing;
|
||||
const detail = Array.isArray(missing) && missing.length
|
||||
? `\nFehlt: ${missing.join(', ')}`
|
||||
: '';
|
||||
alert('Reviewed nicht möglich: ' + e.message + detail);
|
||||
} finally { setSaving(false); }
|
||||
}
|
||||
|
||||
@@ -1170,8 +1174,20 @@ function AutoCreateAllButton({ currentPicture, objects }) {
|
||||
|
||||
// ─── Right panel: Pairs ───────────────────────────────────────────────────────
|
||||
|
||||
function PairsPanel({ selectedObject, allObjects, objectPairs, loadingPairs, onPairSaved, onPairsReload }) {
|
||||
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 }
|
||||
|
||||
async function handleTranslate(pair) {
|
||||
setTranslatingId(pair.id);
|
||||
try {
|
||||
const res = await apiPost(`/pairs/${pair.id}/translate`, {});
|
||||
setReviewData({ pair, content: res.content });
|
||||
} catch (e) {
|
||||
alert('Übersetzen fehlgeschlagen: ' + e.message);
|
||||
} finally { setTranslatingId(null); }
|
||||
}
|
||||
|
||||
if (!selectedObject) {
|
||||
return (
|
||||
@@ -1219,8 +1235,13 @@ function PairsPanel({ selectedObject, allObjects, objectPairs, loadingPairs, onP
|
||||
{LEVEL_LABELS[pair.difficulty_level] || pair.difficulty_level}
|
||||
</span>
|
||||
)}
|
||||
<button onClick={() => handleTranslate(pair)} disabled={translatingId === pair.id}
|
||||
className="ml-auto text-xs font-medium text-emerald-700 bg-emerald-50 hover:bg-emerald-100 px-2 py-0.5 rounded transition-colors disabled:opacity-50 disabled:cursor-wait"
|
||||
title="Fehlende Übersetzungen ergänzen und prüfen">
|
||||
{translatingId === pair.id ? 'Läuft …' : '🪄 Übersetzen & prüfen'}
|
||||
</button>
|
||||
<button onClick={() => setEditingId(pair.id)}
|
||||
className="ml-auto text-xs text-slate-400 hover:text-indigo-600 px-1.5 py-0.5 rounded hover:bg-indigo-50 transition-colors" title="Bearbeiten">✏️</button>
|
||||
className="text-xs text-slate-400 hover:text-indigo-600 px-1.5 py-0.5 rounded hover:bg-indigo-50 transition-colors" title="Bearbeiten">✏️</button>
|
||||
</div>
|
||||
{pair.question && (
|
||||
<p className="text-xs text-slate-600 truncate">
|
||||
@@ -1246,6 +1267,15 @@ function PairsPanel({ selectedObject, allObjects, objectPairs, loadingPairs, onP
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{reviewData && (
|
||||
<PairReviewModal
|
||||
pair={reviewData.pair}
|
||||
content={reviewData.content}
|
||||
onClose={() => setReviewData(null)}
|
||||
onDone={() => { setReviewData(null); (onReloadAll || onPairsReload)(); }}
|
||||
/>
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -1527,6 +1557,33 @@ export default function ContentCreation() {
|
||||
.finally(() => setLoadingPairs(false));
|
||||
}
|
||||
|
||||
function reloadObjects() {
|
||||
if (!currentPicture) return;
|
||||
apiFetch(`/objects?picture_id=${currentPicture.id}&limit=100`)
|
||||
.then(async data => {
|
||||
const objs = Array.isArray(data) ? data : [];
|
||||
const withWords = await Promise.all(objs.map(async obj => {
|
||||
try { const words = await apiFetch(`/objects/${obj.id}/words`); return { ...obj, _words: Array.isArray(words) ? words : [] }; }
|
||||
catch { return { ...obj, _words: [] }; }
|
||||
}));
|
||||
setObjects(withWords);
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
function reloadPictures() {
|
||||
apiFetch('/pictures?limit=500')
|
||||
.then(data => setPictures(Array.isArray(data) ? data : []))
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
// Nach Review eines Pairs: Pair-, Objekt- und Bild-Status (kaskadiert auf 'reviewed') auffrischen.
|
||||
function reloadAfterReview() {
|
||||
reloadPairs();
|
||||
reloadObjects();
|
||||
reloadPictures();
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout back="/content" fullHeight>
|
||||
<div className="h-full flex flex-col">
|
||||
@@ -1616,6 +1673,7 @@ export default function ContentCreation() {
|
||||
loadingPairs={loadingPairs}
|
||||
onPairSaved={pair => setObjectPairs(prev => [pair, ...prev])}
|
||||
onPairsReload={reloadPairs}
|
||||
onReloadAll={reloadAfterReview}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user