import { useEffect, useState } from 'react'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import Dialog from '@mui/material/Dialog'; import DialogTitle from '@mui/material/DialogTitle'; import DialogContent from '@mui/material/DialogContent'; import DialogActions from '@mui/material/DialogActions'; import TextField from '@mui/material/TextField'; import Select from '@mui/material/Select'; import MenuItem from '@mui/material/MenuItem'; import InputLabel from '@mui/material/InputLabel'; import FormControl from '@mui/material/FormControl'; import Typography from '@mui/material/Typography'; import Alert from '@mui/material/Alert'; import CircularProgress from '@mui/material/CircularProgress'; import { Check, X, Wrench, Merge, Split, EyeOff } from 'lucide-react'; import type { Annotation, ReviewAction, ReviewActionResponse, ReviewTask } from '../../types/labeleval'; import { postReviewAction } from '../../services/labeleval'; import { SectionCard, GateLineList, StatusChip } from './shared'; const CLASSES = ['pedestrian', 'cyclist', 'vehicle', 'motorcycle', 'truck']; const BBOX_FIELDS = ['x', 'y', 'z', 'l', 'w', 'h', 'yaw'] as const; export default function RelabelingPanel({ task, annotation, onResolved, }: { task: ReviewTask | null; annotation: Annotation | null; onResolved: () => void; }) { const [busy, setBusy] = useState(null); const [errorMsg, setErrorMsg] = useState(null); const [result, setResult] = useState(null); const [correctOpen, setCorrectOpen] = useState(false); const [bbox, setBbox] = useState([0, 0, 0, 4, 2, 1.6, 0]); const [klass, setKlass] = useState('vehicle'); const [mergeOpen, setMergeOpen] = useState(false); const [mergeTrackId, setMergeTrackId] = useState(''); // Clear the previous re-validation result when switching tasks. useEffect(() => { setResult(null); setErrorMsg(null); }, [task?.task_id]); const submit = async (action: ReviewAction, extra?: { corrected_bbox_3d?: number[]; corrected_class?: string; merge_with_track_id?: string }) => { if (!task) return; setBusy(action); setErrorMsg(null); try { const res = await postReviewAction(task.task_id, { action, ...extra }); setResult(res); setCorrectOpen(false); setMergeOpen(false); onResolved(); } catch (err) { setErrorMsg(err instanceof Error ? err.message : String(err)); } finally { setBusy(null); } }; const openCorrect = () => { setBbox(annotation?.bbox_3d ? [...annotation.bbox_3d] : [0, 0, 0, 4, 2, 1.6, 0]); setKlass(annotation?.class_name && CLASSES.includes(annotation.class_name) ? annotation.class_name : 'vehicle'); setCorrectOpen(true); }; const revalidationPassed = result?.revalidation.validation.passed ?? null; return ( Corrected labels are never verified without re-running the quality gates — every action below triggers re-validation. {errorMsg ? ( setErrorMsg(null)} sx={{ mb: 1 }}> {errorMsg} ) : null} {result ? ( {result.message} {result.task.resolution?.final_status ? : null} Re-validation re-ran the quality gates:{' '} {revalidationPassed ? 'PASSED → VERIFIED' : 'FAILED'} {!revalidationPassed && result.revalidation.decision?.primary_failure_reason ? ` — ${result.revalidation.decision.primary_failure_reason.replace(/_/g, ' ')}` : ''} ) : null} {/* Correct dialog */} setCorrectOpen(false)} maxWidth="xs" fullWidth> Correct annotation {task?.annotation_id} Class {BBOX_FIELDS.map((field, i) => ( { const next = [...bbox]; next[i] = Number(e.target.value); setBbox(next); }} /> ))} {/* Merge dialog */} setMergeOpen(false)} maxWidth="xs" fullWidth> Merge tracks setMergeTrackId(e.target.value)} helperText={annotation?.track_id ? `Current track: ${annotation.track_id}` : undefined} /> ); }