WIP
This commit is contained in:
@@ -1,113 +1,482 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Check, AlertCircle } from 'lucide-react';
|
||||
import { Button } from './Button';
|
||||
import { DocumentStatus } from '../types';
|
||||
import React, { useState, useMemo } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Database, Plus, Trash2, Eye, Play, Check, Loader2, AlertCircle } from 'lucide-react'
|
||||
import { Button } from './Button'
|
||||
import { AugmentationConfig } from './AugmentationConfig'
|
||||
import { useDatasets } from '../hooks/useDatasets'
|
||||
import { useTrainingDocuments } from '../hooks/useTraining'
|
||||
import { trainingApi } from '../api/endpoints'
|
||||
import type { DatasetListItem } from '../api/types'
|
||||
import type { AugmentationConfig as AugmentationConfigType } from '../api/endpoints/augmentation'
|
||||
|
||||
export const Training: React.FC = () => {
|
||||
const [split, setSplit] = useState(80);
|
||||
type Tab = 'datasets' | 'create'
|
||||
|
||||
const docs = [
|
||||
{ id: '1', name: 'Document Document 1', date: '12/28/2024', status: DocumentStatus.VERIFIED },
|
||||
{ id: '2', name: 'Document Document 2', date: '12/29/2024', status: DocumentStatus.VERIFIED },
|
||||
{ id: '3', name: 'Document Document 3', date: '12/29/2024', status: DocumentStatus.VERIFIED },
|
||||
{ id: '4', name: 'Document Document 4', date: '12/29/2024', status: DocumentStatus.PARTIAL },
|
||||
{ id: '5', name: 'Document Document 5', date: '12/29/2024', status: DocumentStatus.PARTIAL },
|
||||
{ id: '6', name: 'Document Document 6', date: '12/29/2024', status: DocumentStatus.PARTIAL },
|
||||
{ id: '8', name: 'Document Document 8', date: '12/29/2024', status: DocumentStatus.VERIFIED },
|
||||
];
|
||||
interface TrainingProps {
|
||||
onNavigate?: (view: string, id?: string) => void
|
||||
}
|
||||
|
||||
const STATUS_STYLES: Record<string, string> = {
|
||||
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 (
|
||||
<div className="p-8 max-w-7xl mx-auto h-[calc(100vh-56px)] flex gap-8">
|
||||
{/* Document Selection List */}
|
||||
<div className="flex-1 flex flex-col">
|
||||
<h2 className="text-2xl font-bold text-warm-text-primary mb-6">Document Selection</h2>
|
||||
|
||||
<div className="flex-1 bg-warm-card border border-warm-border rounded-lg overflow-hidden flex flex-col shadow-sm">
|
||||
<div className="overflow-auto flex-1">
|
||||
<table className="w-full text-left">
|
||||
<thead className="sticky top-0 bg-white border-b border-warm-border z-10">
|
||||
<tr>
|
||||
<th className="py-3 pl-6 pr-4 w-12"><input type="checkbox" className="rounded border-warm-divider"/></th>
|
||||
<th className="py-3 px-4 text-xs font-semibold text-warm-text-muted uppercase">Document name</th>
|
||||
<th className="py-3 px-4 text-xs font-semibold text-warm-text-muted uppercase">Date</th>
|
||||
<th className="py-3 px-4 text-xs font-semibold text-warm-text-muted uppercase">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{docs.map(doc => (
|
||||
<tr key={doc.id} className="border-b border-warm-border hover:bg-warm-hover transition-colors">
|
||||
<td className="py-3 pl-6 pr-4"><input type="checkbox" defaultChecked className="rounded border-warm-divider accent-warm-state-info"/></td>
|
||||
<td className="py-3 px-4 text-sm font-medium text-warm-text-secondary">{doc.name}</td>
|
||||
<td className="py-3 px-4 text-sm text-warm-text-muted font-mono">{doc.date}</td>
|
||||
<td className="py-3 px-4">
|
||||
{doc.status === DocumentStatus.VERIFIED ? (
|
||||
<div className="flex items-center text-warm-state-success text-sm font-medium">
|
||||
<div className="w-5 h-5 rounded-full bg-warm-state-success flex items-center justify-center text-white mr-2">
|
||||
<Check size={12} strokeWidth={3}/>
|
||||
</div>
|
||||
Verified
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center text-warm-text-muted text-sm">
|
||||
<div className="w-5 h-5 rounded-full bg-[#BDBBB5] flex items-center justify-center text-white mr-2">
|
||||
<span className="font-bold text-[10px]">!</span>
|
||||
</div>
|
||||
Partial
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className={`inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium ${STATUS_STYLES[displayStatus] ?? 'bg-warm-border text-warm-text-muted'}`}>
|
||||
{(displayStatus === 'building' || displayStatus === 'training') && <Loader2 size={12} className="mr-1 animate-spin" />}
|
||||
{displayStatus === 'ready' && <Check size={12} className="mr-1" />}
|
||||
{displayStatus === 'failed' && <AlertCircle size={12} className="mr-1" />}
|
||||
{displayStatus}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
{/* Configuration Panel */}
|
||||
<div className="w-96">
|
||||
<div className="bg-warm-card rounded-lg border border-warm-border shadow-card p-6 sticky top-8">
|
||||
<h3 className="text-lg font-semibold text-warm-text-primary mb-6">Training Configuration</h3>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-warm-text-secondary mb-2">Model Name</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g. Invoices Q4"
|
||||
// --- 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<TrainDialogProps> = ({ 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<string | null>(null)
|
||||
const [augmentationEnabled, setAugmentationEnabled] = useState(false)
|
||||
const [augmentationConfig, setAugmentationConfig] = useState<Partial<AugmentationConfigType>>({})
|
||||
const [augmentationMultiplier, setAugmentationMultiplier] = useState(2)
|
||||
|
||||
// Fetch available trained models
|
||||
const { data: modelsData } = useQuery({
|
||||
queryKey: ['training', 'models', 'completed'],
|
||||
queryFn: () => trainingApi.getModels({ status: 'completed' }),
|
||||
})
|
||||
const completedModels = modelsData?.models ?? []
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSubmit({
|
||||
name,
|
||||
config: {
|
||||
model_name: baseModelType === 'pretrained' ? 'yolo11n.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 (
|
||||
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50" onClick={onClose}>
|
||||
<div className="bg-white rounded-lg border border-warm-border shadow-lg w-[480px] max-h-[90vh] overflow-y-auto p-6" onClick={e => e.stopPropagation()}>
|
||||
<h3 className="text-lg font-semibold text-warm-text-primary mb-4">Start Training</h3>
|
||||
<p className="text-sm text-warm-text-muted mb-4">
|
||||
Dataset: <span className="font-medium text-warm-text-secondary">{dataset.name}</span>
|
||||
{' '}({dataset.total_images} images, {dataset.total_annotations} annotations)
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-warm-text-secondary mb-1">Task Name</label>
|
||||
<input type="text" value={name} onChange={e => 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" />
|
||||
</div>
|
||||
|
||||
{/* Base Model Selection */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-warm-text-secondary mb-1">Base Model</label>
|
||||
<select
|
||||
value={baseModelType === 'pretrained' ? 'pretrained' : baseModelVersionId ?? ''}
|
||||
onChange={e => {
|
||||
if (e.target.value === 'pretrained') {
|
||||
setBaseModelType('pretrained')
|
||||
setBaseModelVersionId(null)
|
||||
} else {
|
||||
setBaseModelType('existing')
|
||||
setBaseModelVersionId(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"
|
||||
>
|
||||
<option value="pretrained">yolo11n.pt (Pretrained)</option>
|
||||
{completedModels.map(m => (
|
||||
<option key={m.task_id} value={m.task_id}>
|
||||
{m.name} ({m.metrics_mAP ? `${(m.metrics_mAP * 100).toFixed(1)}% mAP` : 'No metrics'})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs text-warm-text-muted mt-1">
|
||||
{baseModelType === 'pretrained'
|
||||
? 'Start from pretrained YOLO model'
|
||||
: 'Continue training from an existing model (incremental training)'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-1">
|
||||
<label htmlFor="train-epochs" className="block text-sm font-medium text-warm-text-secondary mb-1">Epochs</label>
|
||||
<input
|
||||
id="train-epochs"
|
||||
type="number"
|
||||
min={1}
|
||||
max={1000}
|
||||
value={epochs}
|
||||
onChange={e => 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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-warm-text-secondary mb-2">Base Model</label>
|
||||
<select 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 appearance-none">
|
||||
<option>LayoutLMv3 (Standard)</option>
|
||||
<option>Donut (Beta)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex justify-between mb-2">
|
||||
<label className="block text-sm font-medium text-warm-text-secondary">Train/Test Split</label>
|
||||
<span className="text-xs font-mono text-warm-text-muted">{split}% / {100-split}%</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="50"
|
||||
max="95"
|
||||
value={split}
|
||||
onChange={(e) => setSplit(parseInt(e.target.value))}
|
||||
className="w-full h-1.5 bg-warm-border rounded-lg appearance-none cursor-pointer accent-warm-state-info"
|
||||
<div className="flex-1">
|
||||
<label htmlFor="train-batch-size" className="block text-sm font-medium text-warm-text-secondary mb-1">Batch Size</label>
|
||||
<input
|
||||
id="train-batch-size"
|
||||
type="number"
|
||||
min={1}
|
||||
max={128}
|
||||
value={batchSize}
|
||||
onChange={e => 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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Augmentation Configuration */}
|
||||
<AugmentationConfig
|
||||
enabled={augmentationEnabled}
|
||||
onEnabledChange={setAugmentationEnabled}
|
||||
config={augmentationConfig}
|
||||
onConfigChange={setAugmentationConfig}
|
||||
/>
|
||||
|
||||
{/* Augmentation Multiplier - only shown when augmentation is enabled */}
|
||||
{augmentationEnabled && (
|
||||
<div>
|
||||
<label htmlFor="aug-multiplier" className="block text-sm font-medium text-warm-text-secondary mb-1">
|
||||
Augmentation Multiplier
|
||||
</label>
|
||||
<input
|
||||
id="aug-multiplier"
|
||||
type="number"
|
||||
min={1}
|
||||
max={10}
|
||||
value={augmentationMultiplier}
|
||||
onChange={e => 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"
|
||||
/>
|
||||
<p className="text-xs text-warm-text-muted mt-1">
|
||||
Number of augmented copies per original image (1-10)
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 mt-6">
|
||||
<Button variant="secondary" onClick={onClose} disabled={isPending}>Cancel</Button>
|
||||
<Button onClick={handleSubmit} disabled={isPending || !name.trim()}>
|
||||
{isPending ? <><Loader2 size={14} className="mr-1 animate-spin" />Training...</> : 'Start Training'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// --- 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<DatasetListItem | null>(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 <div className="flex items-center justify-center py-20 text-warm-text-muted"><Loader2 size={24} className="animate-spin mr-2" />Loading datasets...</div>
|
||||
}
|
||||
|
||||
if (datasets.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-warm-text-muted">
|
||||
<Database size={48} className="mb-4 opacity-40" />
|
||||
<p className="text-lg mb-2">No datasets yet</p>
|
||||
<p className="text-sm mb-4">Create a dataset to start training</p>
|
||||
<Button onClick={() => onSwitchTab('create')}><Plus size={14} className="mr-1" />Create Dataset</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-warm-card border border-warm-border rounded-lg overflow-hidden shadow-sm">
|
||||
<table className="w-full text-left">
|
||||
<thead className="bg-white border-b border-warm-border">
|
||||
<tr>
|
||||
<th className="py-3 px-4 text-xs font-semibold text-warm-text-muted uppercase">Name</th>
|
||||
<th className="py-3 px-4 text-xs font-semibold text-warm-text-muted uppercase">Status</th>
|
||||
<th className="py-3 px-4 text-xs font-semibold text-warm-text-muted uppercase">Docs</th>
|
||||
<th className="py-3 px-4 text-xs font-semibold text-warm-text-muted uppercase">Images</th>
|
||||
<th className="py-3 px-4 text-xs font-semibold text-warm-text-muted uppercase">Annotations</th>
|
||||
<th className="py-3 px-4 text-xs font-semibold text-warm-text-muted uppercase">Created</th>
|
||||
<th className="py-3 px-4 text-xs font-semibold text-warm-text-muted uppercase">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{datasets.map(ds => (
|
||||
<tr key={ds.dataset_id} className="border-b border-warm-border hover:bg-warm-hover transition-colors">
|
||||
<td className="py-3 px-4 text-sm font-medium text-warm-text-secondary">{ds.name}</td>
|
||||
<td className="py-3 px-4"><StatusBadge status={ds.status} trainingStatus={ds.training_status} /></td>
|
||||
<td className="py-3 px-4 text-sm text-warm-text-muted font-mono">{ds.total_documents}</td>
|
||||
<td className="py-3 px-4 text-sm text-warm-text-muted font-mono">{ds.total_images}</td>
|
||||
<td className="py-3 px-4 text-sm text-warm-text-muted font-mono">{ds.total_annotations}</td>
|
||||
<td className="py-3 px-4 text-sm text-warm-text-muted">{new Date(ds.created_at).toLocaleDateString()}</td>
|
||||
<td className="py-3 px-4">
|
||||
<div className="flex gap-1">
|
||||
<button title="View" onClick={() => onNavigate?.('dataset-detail', ds.dataset_id)}
|
||||
className="p-1.5 rounded hover:bg-warm-selected text-warm-text-muted hover:text-warm-state-info transition-colors">
|
||||
<Eye size={14} />
|
||||
</button>
|
||||
{ds.status === 'ready' && (
|
||||
<button title="Train" onClick={() => setTrainTarget(ds)}
|
||||
className="p-1.5 rounded hover:bg-warm-selected text-warm-text-muted hover:text-warm-state-success transition-colors">
|
||||
<Play size={14} />
|
||||
</button>
|
||||
)}
|
||||
<button title="Delete" onClick={() => deleteDataset(ds.dataset_id)}
|
||||
disabled={isDeleting}
|
||||
className="p-1.5 rounded hover:bg-warm-selected text-warm-text-muted hover:text-warm-state-error transition-colors">
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{trainTarget && (
|
||||
<TrainDialog dataset={trainTarget} onClose={() => 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<Set<string>>(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 (
|
||||
<div className="flex gap-8">
|
||||
{/* Document selection */}
|
||||
<div className="flex-1 flex flex-col">
|
||||
<h3 className="text-lg font-semibold text-warm-text-primary mb-4">Select Documents</h3>
|
||||
{isLoadingDocs ? (
|
||||
<div className="flex items-center justify-center py-12 text-warm-text-muted"><Loader2 size={20} className="animate-spin mr-2" />Loading...</div>
|
||||
) : (
|
||||
<div className="bg-warm-card border border-warm-border rounded-lg overflow-hidden shadow-sm flex-1">
|
||||
<div className="overflow-auto max-h-[calc(100vh-240px)]">
|
||||
<table className="w-full text-left">
|
||||
<thead className="sticky top-0 bg-white border-b border-warm-border z-10">
|
||||
<tr>
|
||||
<th className="py-3 pl-6 pr-4 w-12">
|
||||
<input type="checkbox" checked={selectedIds.size === documents.length && documents.length > 0}
|
||||
onChange={toggleAll} className="rounded border-warm-divider accent-warm-state-info" />
|
||||
</th>
|
||||
<th className="py-3 px-4 text-xs font-semibold text-warm-text-muted uppercase">Document ID</th>
|
||||
<th className="py-3 px-4 text-xs font-semibold text-warm-text-muted uppercase">Pages</th>
|
||||
<th className="py-3 px-4 text-xs font-semibold text-warm-text-muted uppercase">Annotations</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{documents.map((doc) => (
|
||||
<tr key={doc.document_id} className="border-b border-warm-border hover:bg-warm-hover transition-colors cursor-pointer"
|
||||
onClick={() => toggleDoc(doc.document_id)}>
|
||||
<td className="py-3 pl-6 pr-4">
|
||||
<input type="checkbox" checked={selectedIds.has(doc.document_id)} readOnly
|
||||
className="rounded border-warm-divider accent-warm-state-info pointer-events-none" />
|
||||
</td>
|
||||
<td className="py-3 px-4 text-sm font-mono text-warm-text-secondary">{doc.document_id.slice(0, 8)}...</td>
|
||||
<td className="py-3 px-4 text-sm text-warm-text-muted font-mono">{doc.page_count}</td>
|
||||
<td className="py-3 px-4 text-sm text-warm-text-muted font-mono">{doc.annotation_count ?? 0}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<p className="text-sm text-warm-text-muted mt-2">{selectedIds.size} of {documents.length} documents selected</p>
|
||||
</div>
|
||||
|
||||
{/* Config panel */}
|
||||
<div className="w-80">
|
||||
<div className="bg-warm-card rounded-lg border border-warm-border shadow-card p-6 sticky top-8">
|
||||
<h3 className="text-lg font-semibold text-warm-text-primary mb-4">Dataset Configuration</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-warm-text-secondary mb-1">Name</label>
|
||||
<input type="text" value={name} onChange={e => 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" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-warm-text-secondary mb-1">Description</label>
|
||||
<textarea value={description} onChange={e => setDescription(e.target.value)} rows={2} placeholder="Optional"
|
||||
className="w-full px-3 py-2 rounded-md border border-warm-divider bg-white text-warm-text-primary focus:outline-none focus:ring-1 focus:ring-warm-state-info resize-none" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-warm-text-secondary mb-1">Train / Val / Test Split</label>
|
||||
<div className="flex gap-2 text-sm">
|
||||
<div className="flex-1">
|
||||
<span className="text-xs text-warm-text-muted">Train</span>
|
||||
<input type="number" step={0.05} min={0.1} max={0.9} value={trainRatio} onChange={e => setTrainRatio(Number(e.target.value))}
|
||||
className="w-full h-9 px-2 rounded-md border border-warm-divider bg-white text-warm-text-primary text-center font-mono focus:outline-none focus:ring-1 focus:ring-warm-state-info" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<span className="text-xs text-warm-text-muted">Val</span>
|
||||
<input type="number" step={0.05} min={0} max={0.5} value={valRatio} onChange={e => setValRatio(Number(e.target.value))}
|
||||
className="w-full h-9 px-2 rounded-md border border-warm-divider bg-white text-warm-text-primary text-center font-mono focus:outline-none focus:ring-1 focus:ring-warm-state-info" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<span className="text-xs text-warm-text-muted">Test</span>
|
||||
<input type="number" value={testRatio} readOnly
|
||||
className="w-full h-9 px-2 rounded-md border border-warm-divider bg-warm-hover text-warm-text-muted text-center font-mono" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-warm-border">
|
||||
<Button className="w-full h-12">Start Training</Button>
|
||||
{selectedIds.size > 0 && selectedIds.size < 10 && (
|
||||
<p className="text-xs text-warm-state-warning mb-2">
|
||||
Minimum 10 documents required for training ({selectedIds.size}/10 selected)
|
||||
</p>
|
||||
)}
|
||||
<Button className="w-full h-11" onClick={handleCreate}
|
||||
disabled={isCreating || selectedIds.size < 10 || !name.trim()}>
|
||||
{isCreating ? <><Loader2 size={14} className="mr-1 animate-spin" />Creating...</> : <><Plus size={14} className="mr-1" />Create Dataset</>}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
// --- Main Training Component ---
|
||||
|
||||
export const Training: React.FC<TrainingProps> = ({ onNavigate }) => {
|
||||
const [activeTab, setActiveTab] = useState<Tab>('datasets')
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-7xl mx-auto">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-2xl font-bold text-warm-text-primary">Training</h2>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-1 mb-6 border-b border-warm-border">
|
||||
{([['datasets', 'Datasets'], ['create', 'Create Dataset']] as const).map(([key, label]) => (
|
||||
<button key={key} onClick={() => setActiveTab(key)}
|
||||
className={`px-4 py-2.5 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === key
|
||||
? 'border-warm-state-info text-warm-state-info'
|
||||
: 'border-transparent text-warm-text-muted hover:text-warm-text-secondary'
|
||||
}`}>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{activeTab === 'datasets' && <DatasetList onNavigate={onNavigate} onSwitchTab={setActiveTab} />}
|
||||
{activeTab === 'create' && <CreateDataset onSwitchTab={setActiveTab} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user