Reorganize navigation and module structure based on new requirements. Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
682 lines
26 KiB
TypeScript
682 lines
26 KiB
TypeScript
"use client"
|
|
|
|
import { useState } from "react"
|
|
import { Database, Plus, Settings, Play, Pause, RotateCcw, Brain, Zap, CheckCircle, AlertCircle } from "lucide-react"
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Progress } from "@/components/ui/progress"
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Label } from "@/components/ui/label"
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
|
import { Textarea } from "@/components/ui/textarea"
|
|
import { Switch } from "@/components/ui/switch"
|
|
|
|
interface DataSource {
|
|
id: string
|
|
name: string
|
|
type: string
|
|
status: "connected" | "disconnected" | "syncing"
|
|
lastSync: string
|
|
recordCount: number
|
|
description: string
|
|
}
|
|
|
|
interface AIModel {
|
|
id: string
|
|
name: string
|
|
type: string
|
|
status: "training" | "ready" | "error"
|
|
accuracy: number
|
|
lastTrained: string
|
|
parameters: Record<string, any>
|
|
}
|
|
|
|
export default function DataPlatformPage() {
|
|
const [dataSources, setDataSources] = useState<DataSource[]>([
|
|
{
|
|
id: "ds_001",
|
|
name: "微信用户数据库",
|
|
type: "MySQL",
|
|
status: "connected",
|
|
lastSync: "2024-01-15T10:30:00Z",
|
|
recordCount: 2500000000,
|
|
description: "存储微信用户基础信息和行为数据",
|
|
},
|
|
{
|
|
id: "ds_002",
|
|
name: "流量关键词库",
|
|
type: "PostgreSQL",
|
|
status: "connected",
|
|
lastSync: "2024-01-15T09:45:00Z",
|
|
recordCount: 150000,
|
|
description: "搜索引擎关键词和流量数据",
|
|
},
|
|
{
|
|
id: "ds_003",
|
|
name: "用户行为日志",
|
|
type: "MongoDB",
|
|
status: "syncing",
|
|
lastSync: "2024-01-15T11:00:00Z",
|
|
recordCount: 1500000000,
|
|
description: "用户操作行为和交互记录",
|
|
},
|
|
])
|
|
|
|
const [aiModels, setAiModels] = useState<AIModel[]>([
|
|
{
|
|
id: "model_001",
|
|
name: "用户价值预测模型",
|
|
type: "Classification",
|
|
status: "ready",
|
|
accuracy: 0.92,
|
|
lastTrained: "2024-01-14T15:30:00Z",
|
|
parameters: {
|
|
algorithm: "RandomForest",
|
|
features: 25,
|
|
epochs: 100,
|
|
learningRate: 0.01,
|
|
},
|
|
},
|
|
{
|
|
id: "model_002",
|
|
name: "流量趋势分析模型",
|
|
type: "Regression",
|
|
status: "training",
|
|
accuracy: 0.87,
|
|
lastTrained: "2024-01-15T08:00:00Z",
|
|
parameters: {
|
|
algorithm: "LSTM",
|
|
features: 15,
|
|
epochs: 200,
|
|
learningRate: 0.001,
|
|
},
|
|
},
|
|
{
|
|
id: "model_003",
|
|
name: "用户聚类模型",
|
|
type: "Clustering",
|
|
status: "ready",
|
|
accuracy: 0.89,
|
|
lastTrained: "2024-01-13T12:00:00Z",
|
|
parameters: {
|
|
algorithm: "KMeans",
|
|
clusters: 8,
|
|
features: 20,
|
|
iterations: 300,
|
|
},
|
|
},
|
|
])
|
|
|
|
const [isAddingDataSource, setIsAddingDataSource] = useState(false)
|
|
const [isTrainingModel, setIsTrainingModel] = useState(false)
|
|
const [newDataSource, setNewDataSource] = useState({
|
|
name: "",
|
|
type: "MySQL",
|
|
host: "",
|
|
port: "",
|
|
database: "",
|
|
username: "",
|
|
password: "",
|
|
description: "",
|
|
})
|
|
|
|
const [modelTrainingConfig, setModelTrainingConfig] = useState({
|
|
modelId: "",
|
|
algorithm: "RandomForest",
|
|
features: 25,
|
|
epochs: 100,
|
|
learningRate: 0.01,
|
|
validationSplit: 0.2,
|
|
autoTune: true,
|
|
})
|
|
|
|
// 添加数据源
|
|
const handleAddDataSource = async () => {
|
|
try {
|
|
const newSource: DataSource = {
|
|
id: `ds_${Date.now()}`,
|
|
name: newDataSource.name,
|
|
type: newDataSource.type,
|
|
status: "connected",
|
|
lastSync: new Date().toISOString(),
|
|
recordCount: 0,
|
|
description: newDataSource.description,
|
|
}
|
|
|
|
setDataSources((prev) => [...prev, newSource])
|
|
setIsAddingDataSource(false)
|
|
setNewDataSource({
|
|
name: "",
|
|
type: "MySQL",
|
|
host: "",
|
|
port: "",
|
|
database: "",
|
|
username: "",
|
|
password: "",
|
|
description: "",
|
|
})
|
|
|
|
// 模拟数据导入
|
|
setTimeout(() => {
|
|
setDataSources((prev) =>
|
|
prev.map((ds) =>
|
|
ds.id === newSource.id
|
|
? { ...ds, recordCount: Math.floor(Math.random() * 1000000) + 10000, status: "connected" as const }
|
|
: ds,
|
|
),
|
|
)
|
|
}, 2000)
|
|
} catch (error) {
|
|
console.error("添加数据源失败:", error)
|
|
}
|
|
}
|
|
|
|
// 同步数据源
|
|
const handleSyncDataSource = (id: string) => {
|
|
setDataSources((prev) =>
|
|
prev.map((ds) => (ds.id === id ? { ...ds, status: "syncing" as const, lastSync: new Date().toISOString() } : ds)),
|
|
)
|
|
|
|
// 模拟同步完成
|
|
setTimeout(() => {
|
|
setDataSources((prev) =>
|
|
prev.map((ds) =>
|
|
ds.id === id
|
|
? {
|
|
...ds,
|
|
status: "connected" as const,
|
|
recordCount: ds.recordCount + Math.floor(Math.random() * 10000),
|
|
lastSync: new Date().toISOString(),
|
|
}
|
|
: ds,
|
|
),
|
|
)
|
|
}, 3000)
|
|
}
|
|
|
|
// 训练AI模型
|
|
const handleTrainModel = async () => {
|
|
if (!modelTrainingConfig.modelId) return
|
|
|
|
setIsTrainingModel(true)
|
|
|
|
// 更新模型状态为训练中
|
|
setAiModels((prev) =>
|
|
prev.map((model) =>
|
|
model.id === modelTrainingConfig.modelId ? { ...model, status: "training" as const } : model,
|
|
),
|
|
)
|
|
|
|
// 模拟训练过程
|
|
setTimeout(() => {
|
|
setAiModels((prev) =>
|
|
prev.map((model) =>
|
|
model.id === modelTrainingConfig.modelId
|
|
? {
|
|
...model,
|
|
status: "ready" as const,
|
|
accuracy: Math.random() * 0.1 + 0.85,
|
|
lastTrained: new Date().toISOString(),
|
|
parameters: {
|
|
algorithm: modelTrainingConfig.algorithm,
|
|
features: modelTrainingConfig.features,
|
|
epochs: modelTrainingConfig.epochs,
|
|
learningRate: modelTrainingConfig.learningRate,
|
|
},
|
|
}
|
|
: model,
|
|
),
|
|
)
|
|
setIsTrainingModel(false)
|
|
}, 5000)
|
|
}
|
|
|
|
// 格式化数字
|
|
const formatNumber = (num: number): string => {
|
|
if (num >= 1000000000) {
|
|
return `${(num / 1000000000).toFixed(1)}B`
|
|
}
|
|
if (num >= 1000000) {
|
|
return `${(num / 1000000).toFixed(1)}M`
|
|
}
|
|
if (num >= 1000) {
|
|
return `${(num / 1000).toFixed(1)}K`
|
|
}
|
|
return num.toString()
|
|
}
|
|
|
|
// 获取状态颜色
|
|
const getStatusColor = (status: string) => {
|
|
switch (status) {
|
|
case "connected":
|
|
case "ready":
|
|
return "text-green-600 bg-green-50 border-green-200"
|
|
case "syncing":
|
|
case "training":
|
|
return "text-yellow-600 bg-yellow-50 border-yellow-200"
|
|
case "disconnected":
|
|
case "error":
|
|
return "text-red-600 bg-red-50 border-red-200"
|
|
default:
|
|
return "text-gray-600 bg-gray-50 border-gray-200"
|
|
}
|
|
}
|
|
|
|
// 获取状态图标
|
|
const getStatusIcon = (status: string) => {
|
|
switch (status) {
|
|
case "connected":
|
|
case "ready":
|
|
return <CheckCircle className="w-4 h-4" />
|
|
case "syncing":
|
|
case "training":
|
|
return <Zap className="w-4 h-4 animate-pulse" />
|
|
case "disconnected":
|
|
case "error":
|
|
return <AlertCircle className="w-4 h-4" />
|
|
default:
|
|
return <Database className="w-4 h-4" />
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50">
|
|
<div className="container mx-auto px-4 py-8">
|
|
{/* 页面标题 */}
|
|
<div className="mb-8">
|
|
<h1 className="text-4xl font-bold text-gray-900 mb-2">数据中台</h1>
|
|
<p className="text-gray-600">数据源管理与AI模型训练平台</p>
|
|
</div>
|
|
|
|
<Tabs defaultValue="datasources" className="space-y-6">
|
|
<TabsList className="grid w-full grid-cols-2">
|
|
<TabsTrigger value="datasources" className="flex items-center gap-2">
|
|
<Database className="w-4 h-4" />
|
|
数据源管理
|
|
</TabsTrigger>
|
|
<TabsTrigger value="aimodels" className="flex items-center gap-2">
|
|
<Brain className="w-4 h-4" />
|
|
AI模型
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
{/* 数据源管理 */}
|
|
<TabsContent value="datasources" className="space-y-6">
|
|
<div className="flex justify-between items-center">
|
|
<h2 className="text-2xl font-semibold">数据源管理</h2>
|
|
<Dialog open={isAddingDataSource} onOpenChange={setIsAddingDataSource}>
|
|
<DialogTrigger asChild>
|
|
<Button className="flex items-center gap-2">
|
|
<Plus className="w-4 h-4" />
|
|
添加数据源
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent className="max-w-2xl">
|
|
<DialogHeader>
|
|
<DialogTitle>添加新数据源</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="grid grid-cols-2 gap-4 py-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="name">数据源名称</Label>
|
|
<Input
|
|
id="name"
|
|
value={newDataSource.name}
|
|
onChange={(e) => setNewDataSource((prev) => ({ ...prev, name: e.target.value }))}
|
|
placeholder="输入数据源名称"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="type">数据库类型</Label>
|
|
<Select
|
|
value={newDataSource.type}
|
|
onValueChange={(value) => setNewDataSource((prev) => ({ ...prev, type: value }))}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="MySQL">MySQL</SelectItem>
|
|
<SelectItem value="PostgreSQL">PostgreSQL</SelectItem>
|
|
<SelectItem value="MongoDB">MongoDB</SelectItem>
|
|
<SelectItem value="Redis">Redis</SelectItem>
|
|
<SelectItem value="ClickHouse">ClickHouse</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="host">主机地址</Label>
|
|
<Input
|
|
id="host"
|
|
value={newDataSource.host}
|
|
onChange={(e) => setNewDataSource((prev) => ({ ...prev, host: e.target.value }))}
|
|
placeholder="localhost"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="port">端口</Label>
|
|
<Input
|
|
id="port"
|
|
value={newDataSource.port}
|
|
onChange={(e) => setNewDataSource((prev) => ({ ...prev, port: e.target.value }))}
|
|
placeholder="3306"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="database">数据库名</Label>
|
|
<Input
|
|
id="database"
|
|
value={newDataSource.database}
|
|
onChange={(e) => setNewDataSource((prev) => ({ ...prev, database: e.target.value }))}
|
|
placeholder="database_name"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="username">用户名</Label>
|
|
<Input
|
|
id="username"
|
|
value={newDataSource.username}
|
|
onChange={(e) => setNewDataSource((prev) => ({ ...prev, username: e.target.value }))}
|
|
placeholder="username"
|
|
/>
|
|
</div>
|
|
<div className="col-span-2 space-y-2">
|
|
<Label htmlFor="password">密码</Label>
|
|
<Input
|
|
id="password"
|
|
type="password"
|
|
value={newDataSource.password}
|
|
onChange={(e) => setNewDataSource((prev) => ({ ...prev, password: e.target.value }))}
|
|
placeholder="password"
|
|
/>
|
|
</div>
|
|
<div className="col-span-2 space-y-2">
|
|
<Label htmlFor="description">描述</Label>
|
|
<Textarea
|
|
id="description"
|
|
value={newDataSource.description}
|
|
onChange={(e) => setNewDataSource((prev) => ({ ...prev, description: e.target.value }))}
|
|
placeholder="数据源描述信息"
|
|
rows={3}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="flex justify-end gap-2">
|
|
<Button variant="outline" onClick={() => setIsAddingDataSource(false)}>
|
|
取消
|
|
</Button>
|
|
<Button onClick={handleAddDataSource}>连接并导入数据</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
|
|
{dataSources.map((source) => (
|
|
<Card key={source.id} className="border-2 hover:shadow-lg transition-all duration-200">
|
|
<CardHeader className="pb-3">
|
|
<div className="flex items-center justify-between">
|
|
<CardTitle className="text-lg">{source.name}</CardTitle>
|
|
<Badge className={`${getStatusColor(source.status)} border`}>
|
|
{getStatusIcon(source.status)}
|
|
<span className="ml-1 capitalize">{source.status}</span>
|
|
</Badge>
|
|
</div>
|
|
<p className="text-sm text-gray-600">{source.description}</p>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
|
<div>
|
|
<span className="text-gray-500">类型:</span>
|
|
<span className="ml-2 font-medium">{source.type}</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-500">记录数:</span>
|
|
<span className="ml-2 font-medium">{formatNumber(source.recordCount)}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="text-sm">
|
|
<span className="text-gray-500">最后同步:</span>
|
|
<span className="ml-2">{new Date(source.lastSync).toLocaleString()}</span>
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => handleSyncDataSource(source.id)}
|
|
disabled={source.status === "syncing"}
|
|
className="flex-1"
|
|
>
|
|
{source.status === "syncing" ? (
|
|
<>
|
|
<RotateCcw className="w-3 h-3 mr-1 animate-spin" />
|
|
同步中
|
|
</>
|
|
) : (
|
|
<>
|
|
<RotateCcw className="w-3 h-3 mr-1" />
|
|
同步数据
|
|
</>
|
|
)}
|
|
</Button>
|
|
<Button size="sm" variant="outline">
|
|
<Settings className="w-3 h-3" />
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</TabsContent>
|
|
|
|
{/* AI模型管理 */}
|
|
<TabsContent value="aimodels" className="space-y-6">
|
|
<div className="flex justify-between items-center">
|
|
<h2 className="text-2xl font-semibold">AI模型管理</h2>
|
|
<Dialog open={isTrainingModel} onOpenChange={setIsTrainingModel}>
|
|
<DialogTrigger asChild>
|
|
<Button className="flex items-center gap-2">
|
|
<Brain className="w-4 h-4" />
|
|
重新训练模型
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent className="max-w-2xl">
|
|
<DialogHeader>
|
|
<DialogTitle>模型训练配置</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="grid grid-cols-2 gap-4 py-4">
|
|
<div className="col-span-2 space-y-2">
|
|
<Label htmlFor="model">选择模型</Label>
|
|
<Select
|
|
value={modelTrainingConfig.modelId}
|
|
onValueChange={(value) => setModelTrainingConfig((prev) => ({ ...prev, modelId: value }))}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="选择要训练的模型" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{aiModels.map((model) => (
|
|
<SelectItem key={model.id} value={model.id}>
|
|
{model.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="algorithm">算法</Label>
|
|
<Select
|
|
value={modelTrainingConfig.algorithm}
|
|
onValueChange={(value) => setModelTrainingConfig((prev) => ({ ...prev, algorithm: value }))}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="RandomForest">Random Forest</SelectItem>
|
|
<SelectItem value="XGBoost">XGBoost</SelectItem>
|
|
<SelectItem value="LSTM">LSTM</SelectItem>
|
|
<SelectItem value="KMeans">K-Means</SelectItem>
|
|
<SelectItem value="SVM">SVM</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="features">特征数量</Label>
|
|
<Input
|
|
id="features"
|
|
type="number"
|
|
value={modelTrainingConfig.features}
|
|
onChange={(e) =>
|
|
setModelTrainingConfig((prev) => ({ ...prev, features: Number.parseInt(e.target.value) }))
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="epochs">训练轮数</Label>
|
|
<Input
|
|
id="epochs"
|
|
type="number"
|
|
value={modelTrainingConfig.epochs}
|
|
onChange={(e) =>
|
|
setModelTrainingConfig((prev) => ({ ...prev, epochs: Number.parseInt(e.target.value) }))
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="learningRate">学习率</Label>
|
|
<Input
|
|
id="learningRate"
|
|
type="number"
|
|
step="0.001"
|
|
value={modelTrainingConfig.learningRate}
|
|
onChange={(e) =>
|
|
setModelTrainingConfig((prev) => ({
|
|
...prev,
|
|
learningRate: Number.parseFloat(e.target.value),
|
|
}))
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="validationSplit">验证集比例</Label>
|
|
<Input
|
|
id="validationSplit"
|
|
type="number"
|
|
step="0.1"
|
|
min="0.1"
|
|
max="0.5"
|
|
value={modelTrainingConfig.validationSplit}
|
|
onChange={(e) =>
|
|
setModelTrainingConfig((prev) => ({
|
|
...prev,
|
|
validationSplit: Number.parseFloat(e.target.value),
|
|
}))
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
<div className="col-span-2 flex items-center space-x-2">
|
|
<Switch
|
|
id="autoTune"
|
|
checked={modelTrainingConfig.autoTune}
|
|
onCheckedChange={(checked) =>
|
|
setModelTrainingConfig((prev) => ({ ...prev, autoTune: checked }))
|
|
}
|
|
/>
|
|
<Label htmlFor="autoTune">自动调参</Label>
|
|
</div>
|
|
</div>
|
|
<div className="flex justify-end gap-2">
|
|
<Button variant="outline" onClick={() => setIsTrainingModel(false)}>
|
|
取消
|
|
</Button>
|
|
<Button onClick={handleTrainModel} disabled={!modelTrainingConfig.modelId}>
|
|
开始训练
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
|
|
{aiModels.map((model) => (
|
|
<Card key={model.id} className="border-2 hover:shadow-lg transition-all duration-200">
|
|
<CardHeader className="pb-3">
|
|
<div className="flex items-center justify-between">
|
|
<CardTitle className="text-lg">{model.name}</CardTitle>
|
|
<Badge className={`${getStatusColor(model.status)} border`}>
|
|
{getStatusIcon(model.status)}
|
|
<span className="ml-1 capitalize">{model.status}</span>
|
|
</Badge>
|
|
</div>
|
|
<p className="text-sm text-gray-600">{model.type} 模型</p>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="space-y-2">
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-gray-500">准确率</span>
|
|
<span className="font-medium">{(model.accuracy * 100).toFixed(1)}%</span>
|
|
</div>
|
|
<Progress value={model.accuracy * 100} className="h-2" />
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
|
<div>
|
|
<span className="text-gray-500">算法:</span>
|
|
<span className="ml-2 font-medium">{model.parameters.algorithm}</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-500">特征数:</span>
|
|
<span className="ml-2 font-medium">{model.parameters.features}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="text-sm">
|
|
<span className="text-gray-500">最后训练:</span>
|
|
<span className="ml-2">{new Date(model.lastTrained).toLocaleString()}</span>
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
disabled={model.status === "training"}
|
|
className="flex-1 bg-transparent"
|
|
>
|
|
{model.status === "training" ? (
|
|
<>
|
|
<Pause className="w-3 h-3 mr-1" />
|
|
训练中
|
|
</>
|
|
) : (
|
|
<>
|
|
<Play className="w-3 h-3 mr-1" />
|
|
预测
|
|
</>
|
|
)}
|
|
</Button>
|
|
<Button size="sm" variant="outline">
|
|
<Settings className="w-3 h-3" />
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|