import React, { useState, useMemo } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { Database, Plus, Trash2, Eye, Play, Check, Loader2, AlertCircle, Shield, CheckCircle, XCircle } from 'lucide-react' import { Button } from './Button' import { AugmentationConfig } from './AugmentationConfig' import { useDatasets } from '../hooks/useDatasets' import { useTrainingDocuments } from '../hooks/useTraining' import { trainingApi, poolApi } from '../api/endpoints' import type { DatasetListItem, PoolEntryItem } from '../api/types' import type { AugmentationConfig as AugmentationConfigType } from '../api/endpoints/augmentation' type Tab = 'datasets' | 'create' | 'pool' interface TrainingProps { onNavigate?: (view: string, id?: string) => void } const STATUS_STYLES: Record = { ready: 'bg-warm-state-success/10 text-warm-state-success', building: 'bg-warm-state-info/10 text-warm-state-info', training: 'bg-warm-state-info/10 text-warm-state-info', failed: 'bg-warm-state-error/10 text-warm-state-error', pending: 'bg-warm-state-warning/10 text-warm-state-warning', scheduled: 'bg-warm-state-warning/10 text-warm-state-warning', running: 'bg-warm-state-info/10 text-warm-state-info', } const StatusBadge: React.FC<{ status: string; trainingStatus?: string | null }> = ({ status, trainingStatus }) => { // If there's an active training task, show training status const displayStatus = trainingStatus === 'running' ? 'training' : trainingStatus === 'pending' || trainingStatus === 'scheduled' ? 'pending' : status return ( {(displayStatus === 'building' || displayStatus === 'training') && } {displayStatus === 'ready' && } {displayStatus === 'failed' && } {displayStatus} ) } // --- Train Dialog --- interface TrainDialogProps { dataset: DatasetListItem onClose: () => void onSubmit: (config: { name: string config: { model_name?: string base_model_version_id?: string | null epochs: number batch_size: number augmentation?: AugmentationConfigType augmentation_multiplier?: number } }) => void isPending: boolean } const TrainDialog: React.FC = ({ dataset, onClose, onSubmit, isPending }) => { const [name, setName] = useState(`train-${dataset.name}`) const [epochs, setEpochs] = useState(100) const [batchSize, setBatchSize] = useState(16) const [baseModelType, setBaseModelType] = useState<'pretrained' | 'existing'>('pretrained') const [baseModelVersionId, setBaseModelVersionId] = useState(null) const [augmentationEnabled, setAugmentationEnabled] = useState(false) const [augmentationConfig, setAugmentationConfig] = useState>({}) const [augmentationMultiplier, setAugmentationMultiplier] = useState(2) const isFineTune = baseModelType === 'existing' // Fetch available trained models (active or inactive, not archived) const { data: modelsData } = useQuery({ queryKey: ['training', 'models', 'available'], queryFn: () => trainingApi.getModels(), }) // Only show base models (not fine-tuned) for selection - prevents chaining fine-tunes const availableModels = (modelsData?.models ?? []).filter( m => m.status !== 'archived' && (m.model_type ?? 'base') === 'base' ) const handleSubmit = () => { onSubmit({ name, config: { model_name: baseModelType === 'pretrained' ? 'yolo26s.pt' : undefined, base_model_version_id: baseModelType === 'existing' ? baseModelVersionId : null, epochs, batch_size: batchSize, augmentation: augmentationEnabled ? (augmentationConfig as AugmentationConfigType) : undefined, augmentation_multiplier: augmentationEnabled ? augmentationMultiplier : undefined, }, }) } return (
e.stopPropagation()}>

Start Training

Dataset: {dataset.name} {' '}({dataset.total_images} images, {dataset.total_annotations} annotations)

setName(e.target.value)} className="w-full h-10 px-3 rounded-md border border-warm-divider bg-white text-warm-text-primary focus:outline-none focus:ring-1 focus:ring-warm-state-info" />
{/* Base Model Selection */}

{baseModelType === 'pretrained' ? 'Start from pretrained YOLO model' : 'Fine-tune from base model (freeze=10, cos_lr, data mixing)'}

{/* Fine-tune info panel */} {isFineTune && (

Fine-Tune Mode

  • Epochs: 10 (auto-set), Backbone frozen (10 layers)
  • Cosine LR scheduler, Pool data mixed with old data
  • Requires 50+ verified pool entries
  • Deployment gating runs automatically after training
)}
setEpochs(Math.max(1, Math.min(1000, Number(e.target.value) || 1)))} className="w-full h-10 px-3 rounded-md border border-warm-divider bg-white text-warm-text-primary focus:outline-none focus:ring-1 focus:ring-warm-state-info" />
setBatchSize(Math.max(1, Math.min(128, Number(e.target.value) || 1)))} className="w-full h-10 px-3 rounded-md border border-warm-divider bg-white text-warm-text-primary focus:outline-none focus:ring-1 focus:ring-warm-state-info" />
{/* Augmentation Configuration */} {/* Augmentation Multiplier - only shown when augmentation is enabled */} {augmentationEnabled && (
setAugmentationMultiplier(Math.max(1, Math.min(10, Number(e.target.value) || 1)))} className="w-full h-10 px-3 rounded-md border border-warm-divider bg-white text-warm-text-primary focus:outline-none focus:ring-1 focus:ring-warm-state-info" />

Number of augmented copies per original image (1-10)

)}
) } // --- Dataset List --- const DatasetList: React.FC<{ onNavigate?: (view: string, id?: string) => void onSwitchTab: (tab: Tab) => void }> = ({ onNavigate, onSwitchTab }) => { const { datasets, isLoading, deleteDataset, isDeleting, trainFromDataset, isTraining } = useDatasets() const [trainTarget, setTrainTarget] = useState(null) const handleTrain = (config: { name: string config: { model_name?: string base_model_version_id?: string | null epochs: number batch_size: number augmentation?: AugmentationConfigType augmentation_multiplier?: number } }) => { if (!trainTarget) return // Pass config to the training API const trainRequest = { name: config.name, config: config.config, } trainFromDataset( { datasetId: trainTarget.dataset_id, req: trainRequest }, { onSuccess: () => setTrainTarget(null) }, ) } if (isLoading) { return
Loading datasets...
} if (datasets.length === 0) { return (

No datasets yet

Create a dataset to start training

) } return ( <>
{datasets.map(ds => ( ))}
Name Status Docs Images Annotations Created Actions
{ds.name} {ds.total_documents} {ds.total_images} {ds.total_annotations} {new Date(ds.created_at).toLocaleDateString()}
{ds.status === 'ready' && ( )}
{trainTarget && ( setTrainTarget(null)} onSubmit={handleTrain} isPending={isTraining} /> )} ) } // --- Create Dataset --- const CreateDataset: React.FC<{ onSwitchTab: (tab: Tab) => void }> = ({ onSwitchTab }) => { const { documents, isLoading: isLoadingDocs } = useTrainingDocuments({ has_annotations: true }) const { createDatasetAsync, isCreating } = useDatasets() const [selectedIds, setSelectedIds] = useState>(new Set()) const [name, setName] = useState('') const [description, setDescription] = useState('') const [trainRatio, setTrainRatio] = useState(0.7) const [valRatio, setValRatio] = useState(0.2) const testRatio = useMemo(() => Math.max(0, +(1 - trainRatio - valRatio).toFixed(2)), [trainRatio, valRatio]) const toggleDoc = (id: string) => { setSelectedIds(prev => { const next = new Set(prev) if (next.has(id)) { next.delete(id) } else { next.add(id) } return next }) } const toggleAll = () => { if (selectedIds.size === documents.length) { setSelectedIds(new Set()) } else { setSelectedIds(new Set(documents.map((d) => d.document_id))) } } const handleCreate = async () => { await createDatasetAsync({ name, description: description || undefined, document_ids: [...selectedIds], train_ratio: trainRatio, val_ratio: valRatio, }) onSwitchTab('datasets') } return (
{/* Document selection */}

Select Documents

{isLoadingDocs ? (
Loading...
) : (
{documents.map((doc) => ( toggleDoc(doc.document_id)}> ))}
0} onChange={toggleAll} className="rounded border-warm-divider accent-warm-state-info" /> Document ID Pages Annotations
{doc.document_id.slice(0, 8)}... {doc.page_count} {doc.annotation_count ?? 0}
)}

{selectedIds.size} of {documents.length} documents selected

{/* Config panel */}

Dataset Configuration

setName(e.target.value)} placeholder="e.g. invoice-dataset-v1" className="w-full h-10 px-3 rounded-md border border-warm-divider bg-white text-warm-text-primary focus:outline-none focus:ring-1 focus:ring-warm-state-info" />