Files
users/app/value-model/models/page.tsx
v0 b17b488f8e refactor: restructure navigation and module layout
Reorganize navigation and module structure based on new requirements.

Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
2026-01-31 04:32:36 +00:00

359 lines
13 KiB
TypeScript

"use client"
import { useState } from "react"
import {
Boxes,
Plus,
Search,
Play,
Settings,
CheckCircle,
Clock,
AlertTriangle,
GitBranch,
BarChart3,
Cpu,
TrendingUp,
} from "lucide-react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Badge } from "@/components/ui/badge"
import { Progress } from "@/components/ui/progress"
import { UploadModelDialog } from "@/components/dialogs/upload-model-dialog"
interface Model {
id: string
name: string
type: "preset" | "custom"
category: "clv" | "rfm" | "churn" | "fraud" | "segment"
version: string
status: "online" | "training" | "testing" | "offline"
accuracy: number
precision: number
recall: number
f1Score: number
lastTrained: string
inputFeatures: string[]
outputFormat: string
updateFrequency: string
description: string
}
const MOCK_MODELS: Model[] = [
{
id: "1",
name: "CLV预测模型",
type: "preset",
category: "clv",
version: "v2.3.1",
status: "online",
accuracy: 87.5,
precision: 85.2,
recall: 89.1,
f1Score: 87.1,
lastTrained: "2025-12-10",
inputFeatures: ["历史消费金额", "消费频次", "注册时长", "活跃度"],
outputFormat: "CLV分数 (0-100)",
updateFrequency: "每周",
description: "基于用户历史行为预测客户终身价值",
},
{
id: "2",
name: "RFM评分模型",
type: "preset",
category: "rfm",
version: "v1.5.0",
status: "online",
accuracy: 92.3,
precision: 91.5,
recall: 93.2,
f1Score: 92.3,
lastTrained: "2025-12-11",
inputFeatures: ["最近消费时间", "消费频率", "消费金额"],
outputFormat: "R/F/M各维度评分 (0-100)",
updateFrequency: "每天",
description: "基于RFM模型的用户价值分层",
},
{
id: "3",
name: "流失预警模型",
type: "preset",
category: "churn",
version: "v3.1.2",
status: "online",
accuracy: 85.8,
precision: 87.3,
recall: 84.2,
f1Score: 85.7,
lastTrained: "2025-12-09",
inputFeatures: ["活跃度变化", "消费趋势", "投诉记录", "登录频次"],
outputFormat: "流失概率 (0-1)",
updateFrequency: "每天",
description: "预测用户未来30天流失概率",
},
{
id: "4",
name: "欺诈检测模型",
type: "preset",
category: "fraud",
version: "v2.0.0",
status: "online",
accuracy: 96.2,
precision: 94.8,
recall: 97.5,
f1Score: 96.1,
lastTrained: "2025-12-08",
inputFeatures: ["交易金额", "交易频率", "设备信息", "地理位置"],
outputFormat: "欺诈风险等级 (低/中/高)",
updateFrequency: "实时",
description: "实时检测可疑交易行为",
},
{
id: "5",
name: "用户分群模型",
type: "custom",
category: "segment",
version: "v1.0.0",
status: "training",
accuracy: 78.5,
precision: 76.2,
recall: 80.1,
f1Score: 78.1,
lastTrained: "2025-12-12",
inputFeatures: ["消费行为", "浏览偏好", "互动记录", "人口属性"],
outputFormat: "用户群体标签",
updateFrequency: "每周",
description: "基于多维特征的用户自动分群",
},
]
const CATEGORY_CONFIG = {
clv: { label: "CLV模型", color: "bg-blue-100 text-blue-700" },
rfm: { label: "RFM模型", color: "bg-green-100 text-green-700" },
churn: { label: "流失预警", color: "bg-orange-100 text-orange-700" },
fraud: { label: "欺诈检测", color: "bg-red-100 text-red-700" },
segment: { label: "用户分群", color: "bg-purple-100 text-purple-700" },
}
const STATUS_CONFIG = {
online: { label: "已上线", color: "bg-green-100 text-green-700", icon: CheckCircle },
training: { label: "训练中", color: "bg-blue-100 text-blue-700", icon: Cpu },
testing: { label: "测试中", color: "bg-yellow-100 text-yellow-700", icon: Clock },
offline: { label: "已下线", color: "bg-gray-100 text-gray-700", icon: AlertTriangle },
}
export default function ModelManagementPage() {
const [models, setModels] = useState<Model[]>(MOCK_MODELS)
const [searchQuery, setSearchQuery] = useState("")
const [selectedModel, setSelectedModel] = useState<Model | null>(null)
const [showUploadDialog, setShowUploadDialog] = useState(false)
const stats = {
total: models.length,
online: models.filter((m) => m.status === "online").length,
avgAccuracy: (models.reduce((sum, m) => sum + m.accuracy, 0) / models.length).toFixed(1),
presetModels: models.filter((m) => m.type === "preset").length,
}
return (
<div className="space-y-6 p-6">
{/* Header */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-gray-900"></h1>
<p className="text-gray-500 mt-1"></p>
</div>
<Button
className="bg-gradient-to-r from-blue-500 to-purple-500 text-white"
onClick={() => setShowUploadDialog(true)}
>
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
{/* Stats */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
<CardContent className="p-4">
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
<Boxes className="w-4 h-4" />
</div>
<div className="text-2xl font-bold text-gray-900">{stats.total}</div>
</CardContent>
</Card>
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
<CardContent className="p-4">
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
<CheckCircle className="w-4 h-4 text-green-500" />
线
</div>
<div className="text-2xl font-bold text-green-600">{stats.online}</div>
</CardContent>
</Card>
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
<CardContent className="p-4">
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
<TrendingUp className="w-4 h-4" />
</div>
<div className="text-2xl font-bold text-gray-900">{stats.avgAccuracy}%</div>
</CardContent>
</Card>
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
<CardContent className="p-4">
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
<GitBranch className="w-4 h-4" />
</div>
<div className="text-2xl font-bold text-gray-900">{stats.presetModels}</div>
</CardContent>
</Card>
</div>
{/* Search */}
<div className="relative max-w-md">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
placeholder="搜索模型..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 bg-white/60"
/>
</div>
{/* Model List */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{models
.filter((m) => m.name.toLowerCase().includes(searchQuery.toLowerCase()))
.map((model) => {
const StatusIcon = STATUS_CONFIG[model.status].icon
return (
<Card
key={model.id}
className={`border-none shadow-sm bg-white/60 backdrop-blur-sm hover:shadow-md transition-shadow cursor-pointer ${selectedModel?.id === model.id ? "ring-2 ring-blue-500" : ""}`}
onClick={() => setSelectedModel(model)}
>
<CardContent className="p-5">
<div className="flex items-start justify-between mb-4">
<div>
<div className="flex items-center gap-2 mb-1">
<h3 className="font-semibold text-gray-900">{model.name}</h3>
<Badge className={CATEGORY_CONFIG[model.category].color}>
{CATEGORY_CONFIG[model.category].label}
</Badge>
</div>
<p className="text-sm text-gray-500">{model.description}</p>
</div>
<Badge className={STATUS_CONFIG[model.status].color}>
<StatusIcon className={`w-3 h-3 mr-1 ${model.status === "training" ? "animate-spin" : ""}`} />
{STATUS_CONFIG[model.status].label}
</Badge>
</div>
<div className="grid grid-cols-4 gap-4 mb-4">
<div className="text-center p-2 rounded-lg bg-gray-50">
<div className="text-lg font-bold text-gray-900">{model.accuracy}%</div>
<div className="text-xs text-gray-500"></div>
</div>
<div className="text-center p-2 rounded-lg bg-gray-50">
<div className="text-lg font-bold text-gray-900">{model.precision}%</div>
<div className="text-xs text-gray-500"></div>
</div>
<div className="text-center p-2 rounded-lg bg-gray-50">
<div className="text-lg font-bold text-gray-900">{model.recall}%</div>
<div className="text-xs text-gray-500"></div>
</div>
<div className="text-center p-2 rounded-lg bg-gray-50">
<div className="text-lg font-bold text-gray-900">{model.f1Score}%</div>
<div className="text-xs text-gray-500">F1分数</div>
</div>
</div>
<div className="flex items-center justify-between text-sm text-gray-500 pt-4 border-t border-gray-100">
<div className="flex items-center gap-4">
<span>: {model.version}</span>
<span>: {model.updateFrequency}</span>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm">
<Play className="w-4 h-4 mr-1" />
</Button>
<Button variant="ghost" size="sm">
<Settings className="w-4 h-4" />
</Button>
</div>
</div>
</CardContent>
</Card>
)
})}
</div>
{/* Model Detail Panel */}
{selectedModel && (
<Card className="border-none shadow-md bg-white/80 backdrop-blur-sm">
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
<BarChart3 className="w-5 h-5" />
: {selectedModel.name}
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h4 className="font-medium text-gray-900 mb-3"></h4>
<div className="space-y-2">
{selectedModel.inputFeatures.map((feature, i) => (
<div key={i} className="flex items-center gap-2 p-2 rounded-lg bg-gray-50">
<div className="w-2 h-2 rounded-full bg-blue-500" />
<span className="text-sm text-gray-700">{feature}</span>
</div>
))}
</div>
</div>
<div>
<h4 className="font-medium text-gray-900 mb-3"></h4>
<div className="p-3 rounded-lg bg-gray-50">
<code className="text-sm text-gray-700">{selectedModel.outputFormat}</code>
</div>
<h4 className="font-medium text-gray-900 mb-3 mt-6"></h4>
<div className="space-y-3">
<div>
<div className="flex justify-between text-sm mb-1">
<span></span>
<span>{selectedModel.accuracy}%</span>
</div>
<Progress value={selectedModel.accuracy} className="h-2" />
</div>
<div>
<div className="flex justify-between text-sm mb-1">
<span></span>
<span>{selectedModel.precision}%</span>
</div>
<Progress value={selectedModel.precision} className="h-2" />
</div>
<div>
<div className="flex justify-between text-sm mb-1">
<span></span>
<span>{selectedModel.recall}%</span>
</div>
<Progress value={selectedModel.recall} className="h-2" />
</div>
</div>
</div>
</div>
</CardContent>
</Card>
)}
<UploadModelDialog open={showUploadDialog} onOpenChange={setShowUploadDialog} />
</div>
)
}