Add more tests

This commit is contained in:
Yaojia Wang
2026-02-01 22:40:41 +01:00
parent a564ac9d70
commit 400b12a967
55 changed files with 9306 additions and 267 deletions

View File

@@ -0,0 +1,25 @@
import apiClient from '../client'
import type {
DashboardStatsResponse,
DashboardActiveModelResponse,
RecentActivityResponse,
} from '../types'
export const dashboardApi = {
getStats: async (): Promise<DashboardStatsResponse> => {
const response = await apiClient.get('/api/v1/admin/dashboard/stats')
return response.data
},
getActiveModel: async (): Promise<DashboardActiveModelResponse> => {
const response = await apiClient.get('/api/v1/admin/dashboard/active-model')
return response.data
},
getRecentActivity: async (limit: number = 10): Promise<RecentActivityResponse> => {
const response = await apiClient.get('/api/v1/admin/dashboard/activity', {
params: { limit },
})
return response.data
},
}

View File

@@ -5,3 +5,4 @@ export { inferenceApi } from './inference'
export { datasetsApi } from './datasets'
export { augmentationApi } from './augmentation'
export { modelsApi } from './models'
export { dashboardApi } from './dashboard'

View File

@@ -362,3 +362,48 @@ export interface ActiveModelResponse {
has_active_model: boolean
model: ModelVersionItem | null
}
// Dashboard types
export interface DashboardStatsResponse {
total_documents: number
annotation_complete: number
annotation_incomplete: number
pending: number
completeness_rate: number
}
export interface DashboardActiveModelInfo {
version_id: string
version: string
name: string
metrics_mAP: number | null
metrics_precision: number | null
metrics_recall: number | null
document_count: number
activated_at: string | null
}
export interface DashboardRunningTrainingInfo {
task_id: string
name: string
status: string
started_at: string | null
progress: number
}
export interface DashboardActiveModelResponse {
model: DashboardActiveModelInfo | null
running_training: DashboardRunningTrainingInfo | null
}
export interface ActivityItem {
type: 'document_uploaded' | 'annotation_modified' | 'training_completed' | 'training_failed' | 'model_activated'
description: string
timestamp: string
metadata: Record<string, unknown>
}
export interface RecentActivityResponse {
activities: ActivityItem[]
}

View File

@@ -1,47 +1,58 @@
import React from 'react'
import { FileText, CheckCircle, Clock, TrendingUp, Activity } from 'lucide-react'
import { Button } from './Button'
import { useDocuments } from '../hooks/useDocuments'
import { useTraining } from '../hooks/useTraining'
import { FileText, CheckCircle, AlertCircle, Clock, RefreshCw } from 'lucide-react'
import {
StatsCard,
DataQualityPanel,
ActiveModelPanel,
RecentActivityPanel,
SystemStatusBar,
} from './dashboard/index'
import { useDashboard } from '../hooks/useDashboard'
interface DashboardOverviewProps {
onNavigate: (view: string) => void
}
export const DashboardOverview: React.FC<DashboardOverviewProps> = ({ onNavigate }) => {
const { total: totalDocs, isLoading: docsLoading } = useDocuments({ limit: 1 })
const { models, isLoadingModels } = useTraining()
const {
stats,
model,
runningTraining,
activities,
isLoading,
error,
} = useDashboard()
const stats = [
{
label: 'Total Documents',
value: docsLoading ? '...' : totalDocs.toString(),
icon: FileText,
color: 'text-warm-text-primary',
bgColor: 'bg-warm-bg',
},
{
label: 'Labeled',
value: '0',
icon: CheckCircle,
color: 'text-warm-state-success',
bgColor: 'bg-green-50',
},
{
label: 'Pending',
value: '0',
icon: Clock,
color: 'text-warm-state-warning',
bgColor: 'bg-yellow-50',
},
{
label: 'Training Models',
value: isLoadingModels ? '...' : models.length.toString(),
icon: TrendingUp,
color: 'text-warm-state-info',
bgColor: 'bg-blue-50',
},
]
const handleStatsClick = (filter?: string) => {
if (filter) {
onNavigate(`documents?status=${filter}`)
} else {
onNavigate('documents')
}
}
if (error) {
return (
<div className="p-8 max-w-7xl mx-auto">
<div className="bg-red-50 border border-red-200 rounded-lg p-6 text-center">
<AlertCircle className="w-12 h-12 text-red-500 mx-auto mb-4" />
<h2 className="text-lg font-semibold text-red-800 mb-2">
Failed to load dashboard
</h2>
<p className="text-sm text-red-600 mb-4">
{error instanceof Error ? error.message : 'An unexpected error occurred'}
</p>
<button
onClick={() => window.location.reload()}
className="inline-flex items-center gap-2 px-4 py-2 bg-red-100 hover:bg-red-200 text-red-800 rounded-md text-sm font-medium transition-colors"
>
<RefreshCw className="w-4 h-4" />
Retry
</button>
</div>
</div>
)
}
return (
<div className="p-8 max-w-7xl mx-auto animate-fade-in">
@@ -55,94 +66,74 @@ export const DashboardOverview: React.FC<DashboardOverviewProps> = ({ onNavigate
</p>
</div>
{/* Stats Grid */}
{/* Stats Cards Row */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
{stats.map((stat) => (
<div
key={stat.label}
className="bg-warm-card border border-warm-border rounded-lg p-6 shadow-sm hover:shadow-md transition-shadow"
>
<div className="flex items-center justify-between mb-4">
<div className={`p-3 rounded-lg ${stat.bgColor}`}>
<stat.icon className={stat.color} size={24} />
</div>
</div>
<p className="text-2xl font-bold text-warm-text-primary mb-1">
{stat.value}
</p>
<p className="text-sm text-warm-text-muted">{stat.label}</p>
</div>
))}
<StatsCard
label="Total Documents"
value={stats?.total_documents ?? 0}
icon={FileText}
iconColor="text-warm-text-primary"
iconBgColor="bg-warm-bg"
isLoading={isLoading}
onClick={() => handleStatsClick()}
/>
<StatsCard
label="Complete"
value={stats?.annotation_complete ?? 0}
icon={CheckCircle}
iconColor="text-warm-state-success"
iconBgColor="bg-green-50"
isLoading={isLoading}
onClick={() => handleStatsClick('labeled')}
/>
<StatsCard
label="Incomplete"
value={stats?.annotation_incomplete ?? 0}
icon={AlertCircle}
iconColor="text-orange-600"
iconBgColor="bg-orange-50"
isLoading={isLoading}
onClick={() => handleStatsClick('labeled')}
/>
<StatsCard
label="Pending"
value={stats?.pending ?? 0}
icon={Clock}
iconColor="text-blue-600"
iconBgColor="bg-blue-50"
isLoading={isLoading}
onClick={() => handleStatsClick('pending')}
/>
</div>
{/* Quick Actions */}
<div className="bg-warm-card border border-warm-border rounded-lg p-6 shadow-sm mb-8">
<h2 className="text-lg font-semibold text-warm-text-primary mb-4">
Quick Actions
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Button onClick={() => onNavigate('documents')} className="justify-start">
<FileText size={18} className="mr-2" />
Manage Documents
</Button>
<Button onClick={() => onNavigate('training')} variant="secondary" className="justify-start">
<Activity size={18} className="mr-2" />
Start Training
</Button>
<Button onClick={() => onNavigate('models')} variant="secondary" className="justify-start">
<TrendingUp size={18} className="mr-2" />
View Models
</Button>
</div>
{/* Two-column layout: Data Quality + Active Model */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<DataQualityPanel
completenessRate={stats?.completeness_rate ?? 0}
completeCount={stats?.annotation_complete ?? 0}
incompleteCount={stats?.annotation_incomplete ?? 0}
pendingCount={stats?.pending ?? 0}
isLoading={isLoading}
onViewIncomplete={() => handleStatsClick('labeled')}
/>
<ActiveModelPanel
model={model}
runningTraining={runningTraining}
isLoading={isLoading}
onGoToTraining={() => onNavigate('training')}
/>
</div>
{/* Recent Activity */}
<div className="bg-warm-card border border-warm-border rounded-lg shadow-sm overflow-hidden">
<div className="p-6 border-b border-warm-border">
<h2 className="text-lg font-semibold text-warm-text-primary">
Recent Activity
</h2>
</div>
<div className="p-6">
<div className="text-center py-8 text-warm-text-muted">
<Activity size={48} className="mx-auto mb-3 opacity-20" />
<p className="text-sm">No recent activity</p>
<p className="text-xs mt-1">
Start by uploading documents or creating training jobs
</p>
</div>
</div>
<div className="mb-8">
<RecentActivityPanel
activities={activities}
isLoading={isLoading}
/>
</div>
{/* System Status */}
<div className="mt-8 bg-warm-card border border-warm-border rounded-lg p-6 shadow-sm">
<h2 className="text-lg font-semibold text-warm-text-primary mb-4">
System Status
</h2>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-warm-text-secondary">Backend API</span>
<span className="flex items-center text-sm text-warm-state-success">
<span className="w-2 h-2 bg-green-500 rounded-full mr-2"></span>
Online
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-warm-text-secondary">Database</span>
<span className="flex items-center text-sm text-warm-state-success">
<span className="w-2 h-2 bg-green-500 rounded-full mr-2"></span>
Connected
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-warm-text-secondary">GPU</span>
<span className="flex items-center text-sm text-warm-state-success">
<span className="w-2 h-2 bg-green-500 rounded-full mr-2"></span>
Available
</span>
</div>
</div>
</div>
<SystemStatusBar />
</div>
)
}

View File

@@ -0,0 +1,143 @@
import React from 'react'
import { TrendingUp } from 'lucide-react'
import { Button } from '../Button'
import type { DashboardActiveModelInfo, DashboardRunningTrainingInfo } from '../../api/types'
interface ActiveModelPanelProps {
model: DashboardActiveModelInfo | null
runningTraining: DashboardRunningTrainingInfo | null
isLoading?: boolean
onGoToTraining?: () => void
}
const formatDate = (dateStr: string | null): string => {
if (!dateStr) return 'N/A'
const date = new Date(dateStr)
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
})
}
const formatMetric = (value: number | null): string => {
if (value === null) return 'N/A'
return `${(value * 100).toFixed(1)}%`
}
const getMetricColor = (value: number | null): string => {
if (value === null) return 'text-warm-text-muted'
if (value >= 0.9) return 'text-green-600'
if (value >= 0.8) return 'text-yellow-600'
return 'text-red-600'
}
export const ActiveModelPanel: React.FC<ActiveModelPanelProps> = ({
model,
runningTraining,
isLoading = false,
onGoToTraining,
}) => {
if (isLoading) {
return (
<div className="bg-warm-card border border-warm-border rounded-lg p-6 shadow-sm">
<h2 className="text-sm font-semibold text-warm-text-muted uppercase tracking-wide mb-4">
Active Model
</h2>
<div className="flex items-center justify-center py-8">
<div className="animate-pulse text-warm-text-muted">Loading...</div>
</div>
</div>
)
}
if (!model) {
return (
<div className="bg-warm-card border border-warm-border rounded-lg p-6 shadow-sm">
<h2 className="text-sm font-semibold text-warm-text-muted uppercase tracking-wide mb-4">
Active Model
</h2>
<div className="flex flex-col items-center justify-center py-8 text-center">
<TrendingUp className="w-12 h-12 text-warm-text-disabled mb-3 opacity-20" />
<p className="text-warm-text-primary font-medium mb-1">No Active Model</p>
<p className="text-sm text-warm-text-muted mb-4">
Train and activate a model to see stats here
</p>
{onGoToTraining && (
<Button onClick={onGoToTraining} variant="primary" size="sm">
Go to Training
</Button>
)}
</div>
</div>
)
}
return (
<div className="bg-warm-card border border-warm-border rounded-lg p-6 shadow-sm">
<h2 className="text-sm font-semibold text-warm-text-muted uppercase tracking-wide mb-4">
Active Model
</h2>
<div className="mb-4">
<span className="text-lg font-bold text-warm-text-primary">{model.version}</span>
<span className="text-warm-text-secondary ml-2">- {model.name}</span>
</div>
<div className="border-t border-warm-border pt-4 mb-4">
<div className="grid grid-cols-3 gap-4">
<div className="text-center">
<p className={`text-2xl font-bold ${getMetricColor(model.metrics_mAP)}`}>
{formatMetric(model.metrics_mAP)}
</p>
<p className="text-xs text-warm-text-muted uppercase">mAP</p>
</div>
<div className="text-center">
<p className={`text-2xl font-bold ${getMetricColor(model.metrics_precision)}`}>
{formatMetric(model.metrics_precision)}
</p>
<p className="text-xs text-warm-text-muted uppercase">Precision</p>
</div>
<div className="text-center">
<p className={`text-2xl font-bold ${getMetricColor(model.metrics_recall)}`}>
{formatMetric(model.metrics_recall)}
</p>
<p className="text-xs text-warm-text-muted uppercase">Recall</p>
</div>
</div>
</div>
<div className="space-y-1 text-sm text-warm-text-secondary">
<p>
<span className="text-warm-text-muted">Activated:</span>{' '}
{formatDate(model.activated_at)}
</p>
<p>
<span className="text-warm-text-muted">Documents:</span>{' '}
{model.document_count.toLocaleString()}
</p>
</div>
{runningTraining && (
<div className="mt-4 p-3 bg-blue-50 rounded-lg border border-blue-100">
<div className="flex items-center gap-2 mb-2">
<span className="w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
<span className="text-sm font-medium text-warm-text-primary">
Training in Progress
</span>
</div>
<p className="text-sm text-warm-text-secondary mb-2">{runningTraining.name}</p>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-500 h-2 rounded-full transition-all duration-500"
style={{ width: `${runningTraining.progress}%` }}
/>
</div>
<p className="text-xs text-warm-text-muted mt-1">
{runningTraining.progress}% complete
</p>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,105 @@
import React from 'react'
import { Button } from '../Button'
interface DataQualityPanelProps {
completenessRate: number
completeCount: number
incompleteCount: number
pendingCount: number
isLoading?: boolean
onViewIncomplete?: () => void
}
export const DataQualityPanel: React.FC<DataQualityPanelProps> = ({
completenessRate,
completeCount,
incompleteCount,
pendingCount,
isLoading = false,
onViewIncomplete,
}) => {
const radius = 54
const circumference = 2 * Math.PI * radius
const strokeDashoffset = circumference - (completenessRate / 100) * circumference
return (
<div className="bg-warm-card border border-warm-border rounded-lg p-6 shadow-sm">
<h2 className="text-sm font-semibold text-warm-text-muted uppercase tracking-wide mb-4">
Data Quality
</h2>
<div className="flex items-center gap-6">
<div className="relative">
<svg width="120" height="120" className="transform -rotate-90">
<circle
cx="60"
cy="60"
r={radius}
stroke="#E5E7EB"
strokeWidth="12"
fill="none"
/>
<circle
cx="60"
cy="60"
r={radius}
stroke="#22C55E"
strokeWidth="12"
fill="none"
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={isLoading ? circumference : strokeDashoffset}
className="transition-all duration-500"
/>
</svg>
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-3xl font-bold text-warm-text-primary">
{isLoading ? '...' : `${Math.round(completenessRate)}%`}
</span>
</div>
</div>
<div className="flex-1">
<p className="text-sm text-warm-text-secondary mb-4">
Annotation Complete
</p>
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="flex items-center gap-2">
<span className="w-2 h-2 bg-green-500 rounded-full" />
Complete
</span>
<span className="font-medium">{isLoading ? '...' : completeCount}</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="flex items-center gap-2">
<span className="w-2 h-2 bg-orange-500 rounded-full" />
Incomplete
</span>
<span className="font-medium">{isLoading ? '...' : incompleteCount}</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="flex items-center gap-2">
<span className="w-2 h-2 bg-blue-500 rounded-full" />
Pending
</span>
<span className="font-medium">{isLoading ? '...' : pendingCount}</span>
</div>
</div>
</div>
</div>
{onViewIncomplete && incompleteCount > 0 && (
<div className="mt-4 pt-4 border-t border-warm-border">
<button
onClick={onViewIncomplete}
className="text-sm text-blue-600 hover:text-blue-800 font-medium"
>
View Incomplete Docs
</button>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,134 @@
import React from 'react'
import {
FileText,
Edit,
CheckCircle,
XCircle,
Rocket,
Activity,
} from 'lucide-react'
import type { ActivityItem } from '../../api/types'
interface RecentActivityPanelProps {
activities: ActivityItem[]
isLoading?: boolean
onSeeAll?: () => void
}
const getActivityIcon = (type: ActivityItem['type']) => {
switch (type) {
case 'document_uploaded':
return { Icon: FileText, color: 'text-blue-500', bg: 'bg-blue-50' }
case 'annotation_modified':
return { Icon: Edit, color: 'text-orange-500', bg: 'bg-orange-50' }
case 'training_completed':
return { Icon: CheckCircle, color: 'text-green-500', bg: 'bg-green-50' }
case 'training_failed':
return { Icon: XCircle, color: 'text-red-500', bg: 'bg-red-50' }
case 'model_activated':
return { Icon: Rocket, color: 'text-purple-500', bg: 'bg-purple-50' }
default:
return { Icon: Activity, color: 'text-gray-500', bg: 'bg-gray-50' }
}
}
const formatTimestamp = (timestamp: string): string => {
const date = new Date(timestamp)
const now = new Date()
const diffMs = now.getTime() - date.getTime()
const diffMinutes = Math.floor(diffMs / 60000)
const diffHours = Math.floor(diffMs / 3600000)
const diffDays = Math.floor(diffMs / 86400000)
if (diffMinutes < 1) return 'just now'
if (diffMinutes < 60) return `${diffMinutes} minutes ago`
if (diffHours < 24) return `${diffHours} hours ago`
if (diffDays === 1) return 'yesterday'
if (diffDays < 7) return `${diffDays} days ago`
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
}
export const RecentActivityPanel: React.FC<RecentActivityPanelProps> = ({
activities,
isLoading = false,
onSeeAll,
}) => {
if (isLoading) {
return (
<div className="bg-warm-card border border-warm-border rounded-lg shadow-sm overflow-hidden">
<div className="p-6 border-b border-warm-border flex items-center justify-between">
<h2 className="text-sm font-semibold text-warm-text-muted uppercase tracking-wide">
Recent Activity
</h2>
</div>
<div className="p-6">
<div className="flex items-center justify-center py-8">
<div className="animate-pulse text-warm-text-muted">Loading...</div>
</div>
</div>
</div>
)
}
if (activities.length === 0) {
return (
<div className="bg-warm-card border border-warm-border rounded-lg shadow-sm overflow-hidden">
<div className="p-6 border-b border-warm-border">
<h2 className="text-sm font-semibold text-warm-text-muted uppercase tracking-wide">
Recent Activity
</h2>
</div>
<div className="p-6">
<div className="flex flex-col items-center justify-center py-8 text-center">
<Activity className="w-12 h-12 text-warm-text-disabled mb-3 opacity-20" />
<p className="text-warm-text-primary font-medium mb-1">No recent activity</p>
<p className="text-sm text-warm-text-muted">
Start by uploading documents or creating training jobs
</p>
</div>
</div>
</div>
)
}
return (
<div className="bg-warm-card border border-warm-border rounded-lg shadow-sm overflow-hidden">
<div className="p-6 border-b border-warm-border flex items-center justify-between">
<h2 className="text-sm font-semibold text-warm-text-muted uppercase tracking-wide">
Recent Activity
</h2>
{onSeeAll && (
<button
onClick={onSeeAll}
className="text-sm text-blue-600 hover:text-blue-800 font-medium"
>
See All
</button>
)}
</div>
<div className="divide-y divide-warm-border">
{activities.map((activity, index) => {
const { Icon, color, bg } = getActivityIcon(activity.type)
return (
<div
key={`${activity.type}-${activity.timestamp}-${index}`}
className="px-6 py-3 flex items-center gap-4 hover:bg-warm-hover transition-colors"
>
<div className={`p-2 rounded-lg ${bg}`}>
<Icon className={color} size={16} />
</div>
<p className="flex-1 text-sm text-warm-text-primary truncate">
{activity.description}
</p>
<span className="text-xs text-warm-text-muted whitespace-nowrap">
{formatTimestamp(activity.timestamp)}
</span>
</div>
)
})}
</div>
</div>
)
}

View File

@@ -0,0 +1,44 @@
import React from 'react'
import { LucideIcon } from 'lucide-react'
interface StatsCardProps {
label: string
value: string | number
icon: LucideIcon
iconColor: string
iconBgColor: string
onClick?: () => void
isLoading?: boolean
}
export const StatsCard: React.FC<StatsCardProps> = ({
label,
value,
icon: Icon,
iconColor,
iconBgColor,
onClick,
isLoading = false,
}) => {
return (
<div
className={`bg-warm-card border border-warm-border rounded-lg p-6 shadow-sm hover:shadow-md transition-shadow ${
onClick ? 'cursor-pointer' : ''
}`}
onClick={onClick}
role={onClick ? 'button' : undefined}
tabIndex={onClick ? 0 : undefined}
onKeyDown={onClick ? (e) => e.key === 'Enter' && onClick() : undefined}
>
<div className="flex items-center justify-between mb-4">
<div className={`p-3 rounded-lg ${iconBgColor}`}>
<Icon className={iconColor} size={24} />
</div>
</div>
<p className="text-2xl font-bold text-warm-text-primary mb-1">
{isLoading ? '...' : value}
</p>
<p className="text-sm text-warm-text-muted">{label}</p>
</div>
)
}

View File

@@ -0,0 +1,62 @@
import React from 'react'
interface StatusItem {
label: string
status: 'online' | 'degraded' | 'offline'
statusText: string
}
interface SystemStatusBarProps {
items?: StatusItem[]
}
const getStatusColor = (status: StatusItem['status']) => {
switch (status) {
case 'online':
return 'bg-green-500'
case 'degraded':
return 'bg-yellow-500'
case 'offline':
return 'bg-red-500'
}
}
const getStatusTextColor = (status: StatusItem['status']) => {
switch (status) {
case 'online':
return 'text-warm-state-success'
case 'degraded':
return 'text-yellow-600'
case 'offline':
return 'text-red-600'
}
}
const defaultItems: StatusItem[] = [
{ label: 'Backend API', status: 'online', statusText: 'Online' },
{ label: 'Database', status: 'online', statusText: 'Connected' },
{ label: 'GPU', status: 'online', statusText: 'Available' },
]
export const SystemStatusBar: React.FC<SystemStatusBarProps> = ({
items = defaultItems,
}) => {
return (
<div className="bg-warm-card border border-warm-border rounded-lg p-6 shadow-sm">
<h2 className="text-sm font-semibold text-warm-text-muted uppercase tracking-wide mb-4">
System Status
</h2>
<div className="space-y-3">
{items.map((item) => (
<div key={item.label} className="flex items-center justify-between">
<span className="text-sm text-warm-text-secondary">{item.label}</span>
<span className={`flex items-center text-sm ${getStatusTextColor(item.status)}`}>
<span className={`w-2 h-2 ${getStatusColor(item.status)} rounded-full mr-2`} />
{item.statusText}
</span>
</div>
))}
</div>
</div>
)
}

View File

@@ -0,0 +1,5 @@
export { StatsCard } from './StatsCard'
export { DataQualityPanel } from './DataQualityPanel'
export { ActiveModelPanel } from './ActiveModelPanel'
export { RecentActivityPanel } from './RecentActivityPanel'
export { SystemStatusBar } from './SystemStatusBar'

View File

@@ -5,3 +5,4 @@ export { useTraining, useTrainingDocuments } from './useTraining'
export { useDatasets, useDatasetDetail } from './useDatasets'
export { useAugmentation } from './useAugmentation'
export { useModels, useModelDetail, useActiveModel } from './useModels'
export { useDashboard, useDashboardStats, useActiveModel as useDashboardActiveModel, useRecentActivity } from './useDashboard'

View File

@@ -0,0 +1,76 @@
import { useQuery } from '@tanstack/react-query'
import { dashboardApi } from '../api/endpoints'
import type {
DashboardStatsResponse,
DashboardActiveModelResponse,
RecentActivityResponse,
} from '../api/types'
export const useDashboardStats = () => {
const { data, isLoading, error, refetch } = useQuery<DashboardStatsResponse>({
queryKey: ['dashboard', 'stats'],
queryFn: () => dashboardApi.getStats(),
staleTime: 30000,
refetchInterval: 60000,
})
return {
stats: data,
isLoading,
error,
refetch,
}
}
export const useActiveModel = () => {
const { data, isLoading, error, refetch } = useQuery<DashboardActiveModelResponse>({
queryKey: ['dashboard', 'active-model'],
queryFn: () => dashboardApi.getActiveModel(),
staleTime: 30000,
refetchInterval: 60000,
})
return {
model: data?.model ?? null,
runningTraining: data?.running_training ?? null,
isLoading,
error,
refetch,
}
}
export const useRecentActivity = (limit: number = 10) => {
const { data, isLoading, error, refetch } = useQuery<RecentActivityResponse>({
queryKey: ['dashboard', 'activity', limit],
queryFn: () => dashboardApi.getRecentActivity(limit),
staleTime: 30000,
refetchInterval: 60000,
})
return {
activities: data?.activities ?? [],
isLoading,
error,
refetch,
}
}
export const useDashboard = () => {
const stats = useDashboardStats()
const activeModel = useActiveModel()
const activity = useRecentActivity()
return {
stats: stats.stats,
model: activeModel.model,
runningTraining: activeModel.runningTraining,
activities: activity.activities,
isLoading: stats.isLoading || activeModel.isLoading || activity.isLoading,
error: stats.error || activeModel.error || activity.error,
refetch: () => {
stats.refetch()
activeModel.refetch()
activity.refetch()
},
}
}