// frontend/src/components/ui/TrainingTab.tsx 'use client' import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from 'react' import Button from './Button' import LoadingSpinner from './LoadingSpinner' import { formatBytes, formatDuration } from './formatters' import { TrashIcon } from '@heroicons/react/20/solid' import { getSegmentLabelItem } from './Icons' import Modal from './Modal' type ScoredLabel = { label: string score: number } type TrainingJobStatus = { running: boolean progress: number step: string message?: string error?: string startedAt?: string finishedAt?: string } type TrainingStatus = { feedbackCount: number requiredCount: number canTrain: boolean training?: TrainingJobStatus } type TrainingPrediction = { modelAvailable: boolean source?: string peopleCount: number maleCount: number femaleCount: number unknownCount: number sexPosition: string sexPositionScore: number bodyPartsPresent: ScoredLabel[] objectsPresent: ScoredLabel[] clothingPresent: ScoredLabel[] boxes?: TrainingBox[] } type TrainingSample = { sampleId: string frameUrl: string sourceFile: string sourcePath?: string sourceSizeBytes?: number second: number createdAt: string prediction: TrainingPrediction } type TrainingLabels = { people: string[] sexPositions: string[] bodyParts: string[] objects: string[] clothing: string[] } type CorrectionState = { peopleCount: number maleCount: number femaleCount: number unknownCount: number sexPosition: string bodyPartsPresent: string[] objectsPresent: string[] clothingPresent: string[] boxes: TrainingBox[] } type TrainingBox = { label: string score?: number x: number y: number w: number h: number } type BoxInteraction = | { type: 'move' index: number startX: number startY: number original: TrainingBox } | { type: 'resize' index: number handle: 'nw' | 'ne' | 'sw' | 'se' startX: number startY: number original: TrainingBox } type MagnifierState = { visible: boolean clientX: number clientY: number imageX: number imageY: number } type TrainingConfidence = { score: number level: 'none' | 'low' | 'mid' | 'high' label: string } type TrainingLabelStat = { label: string count: number confidence?: TrainingConfidence } type TrainingStats = { feedbackCount: number acceptedCount: number correctedCount: number sampleCount: number boxCount: number modelAvailable: boolean confidence?: TrainingConfidence labels: { people: TrainingLabelStat[] sexPositions: TrainingLabelStat[] bodyParts: TrainingLabelStat[] objects: TrainingLabelStat[] clothing: TrainingLabelStat[] } } type TrainingNoticeKind = 'success' | 'error' | 'info' | 'warning' type TrainingNotice = { kind: TrainingNoticeKind title: string message: string detail?: string } function trainingNoticeClass(kind: TrainingNoticeKind) { switch (kind) { case 'success': return { wrap: 'border-emerald-200 bg-emerald-50 text-emerald-900 dark:border-emerald-400/30 dark:bg-emerald-500/10 dark:text-emerald-100', icon: 'bg-emerald-500 text-white', detail: 'text-emerald-800/80 dark:text-emerald-100/70', } case 'error': return { wrap: 'border-red-200 bg-red-50 text-red-900 dark:border-red-400/30 dark:bg-red-500/10 dark:text-red-100', icon: 'bg-red-500 text-white', detail: 'text-red-800/80 dark:text-red-100/70', } case 'warning': return { wrap: 'border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-400/30 dark:bg-amber-500/10 dark:text-amber-100', icon: 'bg-amber-500 text-white', detail: 'text-amber-800/80 dark:text-amber-100/70', } default: return { wrap: 'border-indigo-200 bg-indigo-50 text-indigo-900 dark:border-indigo-400/30 dark:bg-indigo-500/10 dark:text-indigo-100', icon: 'bg-indigo-500 text-white', detail: 'text-indigo-800/80 dark:text-indigo-100/70', } } } function TrainingNoticeCard(props: { notice: TrainingNotice onClose?: () => void }) { const cls = trainingNoticeClass(props.notice.kind) const icon = props.notice.kind === 'success' ? '✓' : props.notice.kind === 'error' ? '!' : props.notice.kind === 'warning' ? '⚠' : 'i' return (
{props.notice.detail}