import { useState, useEffect, useCallback } from 'react' import { useTranslation } from 'react-i18next' import { api, endpoints } from '../../api' import { useDebounce, useResizablePanel } from '../../hooks' import { useFlight, DetectionClasses, ConfirmDialog } from '../../components' import CanvasEditor from '../annotations/CanvasEditor' import type { DatasetItem, PaginatedResponse, ClassDistributionItem, AnnotationListItem, Detection, Media } from '../../types' import { AnnotationStatus } from '../../types' type Tab = 'annotations' | 'editor' | 'distribution' export default function DatasetPage() { const { t } = useTranslation() const { selectedFlight } = useFlight() const leftPanel = useResizablePanel(250, 200, 400) const [items, setItems] = useState([]) const [totalCount, setTotalCount] = useState(0) const [page, setPage] = useState(1) const [pageSize] = useState(20) const [fromDate, setFromDate] = useState('') const [toDate, setToDate] = useState('') const [statusFilter, setStatusFilter] = useState(null) const [objectsOnly, setObjectsOnly] = useState(false) const [search, setSearch] = useState('') const debouncedSearch = useDebounce(search, 400) const [selectedClassNum, setSelectedClassNum] = useState(0) const [photoMode, setPhotoMode] = useState(0) const [selectedIds, setSelectedIds] = useState>(new Set()) const [tab, setTab] = useState('annotations') const [editorAnnotation, setEditorAnnotation] = useState(null) const [editorDetections, setEditorDetections] = useState([]) const [distribution, setDistribution] = useState([]) const fetchItems = useCallback(async () => { const params = new URLSearchParams({ page: String(page), pageSize: String(pageSize) }) if (fromDate) params.set('fromDate', fromDate) if (toDate) params.set('toDate', toDate) if (selectedFlight) params.set('flightId', selectedFlight.id) if (statusFilter !== null) params.set('status', String(statusFilter)) if (selectedClassNum) params.set('classNum', String(selectedClassNum)) if (objectsOnly) params.set('hasDetections', 'true') if (debouncedSearch) params.set('name', debouncedSearch) try { const res = await api.get>(endpoints.annotations.dataset(params.toString())) setItems(res.items) setTotalCount(res.totalCount) } catch {} }, [page, pageSize, fromDate, toDate, selectedFlight, statusFilter, selectedClassNum, objectsOnly, debouncedSearch]) useEffect(() => { fetchItems() }, [fetchItems]) const handleDoubleClick = async (item: DatasetItem) => { try { const ann = await api.get(endpoints.annotations.datasetItem(item.annotationId)) setEditorAnnotation(ann) setEditorDetections(ann.detections) setTab('editor') } catch {} } const handleValidate = async () => { if (selectedIds.size === 0) return await api.post(endpoints.annotations.datasetBulkStatus(), { annotationIds: Array.from(selectedIds), status: AnnotationStatus.Validated, }) setSelectedIds(new Set()) fetchItems() } const loadDistribution = useCallback(async () => { try { const data = await api.get(endpoints.annotations.datasetClassDistribution()) setDistribution(data) } catch {} }, []) useEffect(() => { if (tab === 'distribution') loadDistribution() }, [tab, loadDistribution]) const maxDistCount = Math.max(...distribution.map(d => d.count), 1) const totalPages = Math.ceil(totalCount / pageSize) const editorMedia: Media | null = editorAnnotation ? { id: editorAnnotation.mediaId, name: '', path: '', mediaType: 1, mediaStatus: 0, duration: null, annotationCount: 0, waypointId: null, userId: '', } : null const statusButtons = [ { label: 'All', value: null }, { label: t('dataset.status.created'), value: AnnotationStatus.Created }, { label: t('dataset.status.edited'), value: AnnotationStatus.Edited }, { label: t('dataset.status.validated'), value: AnnotationStatus.Validated }, ] return (
{/* Left panel */}
setSearch(e.target.value)} placeholder={t('dataset.search')} className="w-full bg-az-bg border border-az-border rounded px-2 py-1 text-xs text-az-text outline-none" />
{/* Main area */}
{/* Filter bar */}
setFromDate(e.target.value)} className="bg-az-bg border border-az-border rounded px-2 py-1 text-az-text" /> setToDate(e.target.value)} className="bg-az-bg border border-az-border rounded px-2 py-1 text-az-text" /> {statusButtons.map(sb => ( ))}
{selectedIds.size > 0 && ( )}
{/* Tabs */}
{(['annotations', 'editor', 'distribution'] as Tab[]).map(tb => ( ))}
{/* Content */} {tab === 'annotations' && (
{items.map(item => (
{ if (e.ctrlKey) { setSelectedIds(prev => { const n = new Set(prev) n.has(item.annotationId) ? n.delete(item.annotationId) : n.add(item.annotationId) return n }) } else { setSelectedIds(new Set([item.annotationId])) } }} onDoubleClick={() => handleDoubleClick(item)} className={`bg-az-panel border rounded overflow-hidden cursor-pointer ${ selectedIds.has(item.annotationId) ? 'border-az-orange' : 'border-az-border' } ${item.isSeed ? 'ring-2 ring-az-red' : ''}`} > {item.imageName}
{item.imageName}
{new Date(item.createdDate).toLocaleDateString()} {item.status === AnnotationStatus.Validated ? t('dataset.status.validated') : item.status === AnnotationStatus.Edited ? t('dataset.status.edited') : t('dataset.status.created')}
))}
{/* Pagination */} {totalPages > 1 && (
{page} / {totalPages}
)}
)} {tab === 'editor' && editorMedia && editorAnnotation && (
)} {tab === 'distribution' && (
{distribution.map(d => (
{d.label}
{d.count}
))}
)}
) }