This commit is contained in:
Yaojia Wang
2026-01-30 00:44:21 +01:00
parent d2489a97d4
commit 33ada0350d
79 changed files with 9737 additions and 297 deletions

View File

@@ -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>
)
}