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>
This commit is contained in:
v0
2026-01-31 04:32:36 +00:00
parent 22e725887a
commit b17b488f8e
105 changed files with 20530 additions and 3622 deletions

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

View File

@@ -0,0 +1,881 @@
"use client"
import { useState } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Textarea } from "@/components/ui/textarea"
import { Slider } from "@/components/ui/slider"
import {
ArrowLeft,
Sparkles,
CheckCircle,
AlertTriangle,
Play,
Settings,
Eye,
RefreshCw,
Database,
Zap,
Plus,
Brain,
MessageSquare,
Shield,
History,
Check,
X,
Pause,
FileText,
} from "lucide-react"
import Link from "next/link"
const availableDataSources = [
{ id: "1", name: "存客宝-用户主库", type: "mysql", records: 128956342, tables: ["users", "orders", "profiles"] },
{ id: "2", name: "触客宝-行为数据", type: "kafka", records: 89234567, tables: ["events", "sessions"] },
{ id: "3", name: "数智员工-账号API", type: "restapi", records: 45678901, tables: ["accounts", "activities"] },
{ id: "7", name: "微信消息-Webhook", type: "webhook", records: 34567890, tables: ["messages", "contacts"] },
]
const aiCleaningRules = [
{
id: 1,
name: "用户数据智能清洗",
description: "自动识别并修正用户数据中的格式错误、缺失值和异常值",
dataSource: "存客宝-用户主库",
targetTable: "users",
prompt:
"分析用户数据表识别以下问题并提供修正建议1) 手机号格式不规范 2) 邮箱格式错误 3) 姓名包含特殊字符 4) 地址信息不完整 5) 日期格式不统一。对于每个问题,给出具体的修正规则。",
model: "GPT-4",
status: "running",
confidenceThreshold: 0.8,
autoApproveThreshold: 0.95,
processed: 125800,
pending: 3400,
approved: 120000,
rejected: 2400,
lastRun: "10分钟前",
accuracy: 96.2,
},
{
id: 2,
name: "交易流水异常检测",
description: "识别交易数据中的异常记录和潜在错误",
dataSource: "存客宝-用户主库",
targetTable: "orders",
prompt:
"检查交易流水数据识别以下异常1) 金额为负或超出合理范围 2) 时间戳异常 3) 重复交易记录 4) 用户ID与订单不匹配 5) 状态字段值异常。标记问题记录并建议处理方式。",
model: "GPT-4",
status: "running",
confidenceThreshold: 0.7,
autoApproveThreshold: 0.9,
processed: 89000,
pending: 1200,
approved: 85000,
rejected: 2800,
lastRun: "30分钟前",
accuracy: 94.5,
},
{
id: 3,
name: "聊天记录语义清洗",
description: "清洗聊天记录中的敏感信息和无效内容",
dataSource: "微信消息-Webhook",
targetTable: "messages",
prompt:
"处理聊天记录数据1) 识别并脱敏个人敏感信息身份证、银行卡号等2) 过滤垃圾信息和广告 3) 标准化时间格式 4) 提取并分类用户意图 5) 标记需要人工处理的复杂内容。",
model: "Claude-3",
status: "paused",
confidenceThreshold: 0.6,
autoApproveThreshold: 0.85,
processed: 456000,
pending: 15000,
approved: 420000,
rejected: 21000,
lastRun: "2小时前",
accuracy: 91.8,
},
]
const pendingCleaningSuggestions = [
{
id: 1,
ruleId: 1,
ruleName: "用户数据智能清洗",
field: "phone",
originalValue: "86-138-1234-5678",
suggestedValue: "13812345678",
confidence: 0.95,
reason: "去除国际区号前缀和分隔符,保留纯数字手机号",
affectedRows: 2350,
createdAt: "2025-12-12 14:30",
},
{
id: 2,
ruleId: 1,
ruleName: "用户数据智能清洗",
field: "email",
originalValue: "user@example,com",
suggestedValue: "user@example.com",
confidence: 0.88,
reason: "修正邮箱格式,将逗号替换为点号",
affectedRows: 156,
createdAt: "2025-12-12 14:28",
},
{
id: 3,
ruleId: 2,
ruleName: "交易流水异常检测",
field: "amount",
originalValue: "-500.00",
suggestedValue: "500.00",
confidence: 0.72,
reason: "检测到负数金额,建议修正为正数(可能是退款记录录入错误)",
affectedRows: 45,
createdAt: "2025-12-12 14:25",
},
{
id: 4,
ruleId: 3,
ruleName: "聊天记录语义清洗",
field: "content",
originalValue: "我的身份证号是310101199001011234",
suggestedValue: "我的身份证号是310***********1234",
confidence: 0.92,
reason: "检测到身份证号,进行脱敏处理保护用户隐私",
affectedRows: 890,
createdAt: "2025-12-12 14:22",
},
]
// 清洗历史
const cleaningHistory = [
{ id: 1, rule: "用户数据智能清洗", action: "approved", operator: "系统自动", count: 5000, time: "2025-12-12 14:00" },
{ id: 2, rule: "交易流水异常检测", action: "approved", operator: "张管理员", count: 120, time: "2025-12-12 13:45" },
{
id: 3,
rule: "聊天记录语义清洗",
action: "rejected",
operator: "李管理员",
count: 35,
time: "2025-12-12 13:30",
reason: "置信度过低",
},
]
export default function AIDataCleaningPage() {
const [activeTab, setActiveTab] = useState("rules")
const [createRuleDialogOpen, setCreateRuleDialogOpen] = useState(false)
const [settingsDialogOpen, setSettingsDialogOpen] = useState(false)
const [previewDialogOpen, setPreviewDialogOpen] = useState(false)
const [selectedRule, setSelectedRule] = useState<(typeof aiCleaningRules)[0] | null>(null)
const [selectedSuggestion, setSelectedSuggestion] = useState<(typeof pendingCleaningSuggestions)[0] | null>(null)
const [selectedSuggestions, setSelectedSuggestions] = useState<number[]>([])
const [confidenceFilter, setConfidenceFilter] = useState([0.5])
const [newRule, setNewRule] = useState({
name: "",
dataSource: "",
targetTable: "",
prompt: "",
model: "gpt-4",
confidenceThreshold: 0.7,
autoApproveThreshold: 0.9,
})
const totalPending = aiCleaningRules.reduce((sum, r) => sum + r.pending, 0)
const totalApproved = aiCleaningRules.reduce((sum, r) => sum + r.approved, 0)
const totalProcessed = aiCleaningRules.reduce((sum, r) => sum + r.processed, 0)
const filteredSuggestions = pendingCleaningSuggestions.filter((s) => s.confidence >= confidenceFilter[0])
// 获取选中数据源的表列表
const getTablesForDataSource = (dsName: string) => {
const ds = availableDataSources.find((d) => d.name === dsName)
return ds?.tables || []
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-violet-50/30 p-6">
<div className="max-w-7xl mx-auto space-y-6">
{/* 页面标题 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Link href="/ai-insight">
<Button variant="ghost" size="icon" className="rounded-full">
<ArrowLeft className="w-5 h-5" />
</Button>
</Link>
<div>
<h1 className="text-2xl font-bold text-slate-800">AI数据清洗</h1>
<p className="text-slate-500 mt-1"></p>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline">
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
<Button
className="bg-gradient-to-r from-violet-500 to-purple-600 hover:from-violet-600 hover:to-purple-700"
onClick={() => setCreateRuleDialogOpen(true)}
>
<Plus className="w-4 h-4 mr-2" />
AI规则
</Button>
</div>
</div>
{/* 统计概览 */}
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-slate-500"></p>
<p className="text-2xl font-bold text-slate-800">{(totalProcessed / 1000).toFixed(0)}K</p>
</div>
<Database className="w-8 h-8 text-blue-500" />
</div>
</CardContent>
</Card>
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-slate-500"></p>
<p className="text-2xl font-bold text-emerald-600">
{((totalApproved / totalProcessed) * 100).toFixed(1)}%
</p>
</div>
<CheckCircle className="w-8 h-8 text-emerald-500" />
</div>
</CardContent>
</Card>
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-slate-500"></p>
<p className="text-2xl font-bold text-amber-600">{totalPending.toLocaleString()}</p>
</div>
<AlertTriangle className="w-8 h-8 text-amber-500" />
</div>
</CardContent>
</Card>
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-slate-500"></p>
<p className="text-2xl font-bold text-slate-800">
{aiCleaningRules.filter((r) => r.status === "running").length} / {aiCleaningRules.length}
</p>
</div>
<RefreshCw className="w-8 h-8 text-blue-500" />
</div>
</CardContent>
</Card>
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-slate-500"></p>
<p className="text-2xl font-bold text-violet-600">0.89</p>
</div>
<Brain className="w-8 h-8 text-violet-500" />
</div>
</CardContent>
</Card>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="bg-white/60 backdrop-blur">
<TabsTrigger value="rules" className="gap-2">
<Sparkles className="w-4 h-4" />
AI清洗规则
</TabsTrigger>
<TabsTrigger value="review" className="gap-2">
<Shield className="w-4 h-4" />
{totalPending > 0 && <Badge className="ml-1 bg-amber-100 text-amber-700">{totalPending}</Badge>}
</TabsTrigger>
<TabsTrigger value="history" className="gap-2">
<History className="w-4 h-4" />
</TabsTrigger>
</TabsList>
{/* AI清洗规则列表 */}
<TabsContent value="rules" className="mt-4">
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-base">AI清洗规则配置</CardTitle>
<p className="text-sm text-slate-500"></p>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
{aiCleaningRules.map((rule) => (
<div key={rule.id} className="p-4 bg-slate-50/80 rounded-lg border border-slate-200/60">
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-blue-500 to-cyan-600 flex items-center justify-center">
<Zap className="w-5 h-5 text-white" />
</div>
<div>
<div className="flex items-center gap-2">
<h4 className="font-medium text-slate-800">{rule.name}</h4>
<Badge variant="outline" className="text-xs">
{rule.model}
</Badge>
{rule.status === "running" ? (
<Badge className="bg-emerald-100 text-emerald-700"></Badge>
) : (
<Badge variant="secondary"></Badge>
)}
</div>
<p className="text-sm text-slate-500 mt-0.5">{rule.description}</p>
</div>
</div>
<div className="flex items-center gap-2">
{rule.status === "running" ? (
<Button variant="outline" size="sm">
<Pause className="w-4 h-4" />
</Button>
) : (
<Button variant="outline" size="sm">
<Play className="w-4 h-4" />
</Button>
)}
<Button
variant="ghost"
size="sm"
onClick={() => {
setSelectedRule(rule)
setSettingsDialogOpen(true)
}}
>
<Settings className="w-4 h-4" />
</Button>
<Button variant="ghost" size="sm">
<FileText className="w-4 h-4" />
</Button>
</div>
</div>
{/* 数据源和目标表 */}
<div className="grid grid-cols-2 gap-4 mb-3">
<div className="p-3 bg-white rounded-lg">
<div className="flex items-center gap-2 text-xs text-slate-500 mb-1">
<Database className="w-3 h-3" />
</div>
<p className="text-sm font-medium text-slate-700">{rule.dataSource}</p>
<p className="text-xs text-slate-500 mt-1">: {rule.targetTable}</p>
</div>
<div className="p-3 bg-white rounded-lg">
<div className="flex items-center gap-2 text-xs text-slate-500 mb-1">
<Zap className="w-3 h-3" />
</div>
<p className="text-sm font-medium text-slate-700">
: {">="}
{rule.confidenceThreshold} | : {">="}
{rule.autoApproveThreshold}
</p>
</div>
</div>
{/* 提示词 */}
<div className="p-3 bg-white rounded-lg mb-3">
<div className="flex items-center gap-2 text-xs text-slate-500 mb-1">
<MessageSquare className="w-3 h-3" />
AI提示词
</div>
<p className="text-sm text-slate-600 line-clamp-2">{rule.prompt}</p>
</div>
{/* 统计数据 */}
<div className="grid grid-cols-5 gap-4 text-sm">
<div className="text-center">
<p className="text-lg font-bold text-slate-800">{rule.accuracy}%</p>
<p className="text-xs text-slate-500"></p>
</div>
<div className="text-center">
<p className="text-lg font-bold text-slate-800">{(rule.processed / 1000).toFixed(0)}K</p>
<p className="text-xs text-slate-500"></p>
</div>
<div className="text-center">
<p className="text-lg font-bold text-amber-600">{rule.pending.toLocaleString()}</p>
<p className="text-xs text-slate-500"></p>
</div>
<div className="text-center">
<p className="text-lg font-bold text-emerald-600">{(rule.approved / 1000).toFixed(0)}K</p>
<p className="text-xs text-slate-500"></p>
</div>
<div className="text-center">
<p className="text-lg font-bold text-red-600">{rule.rejected.toLocaleString()}</p>
<p className="text-xs text-slate-500"></p>
</div>
</div>
<div className="mt-3 pt-3 border-t border-slate-200/60 flex items-center justify-between text-xs text-slate-500">
<span>: {rule.lastRun}</span>
<Button variant="link" size="sm" className="h-auto p-0 text-violet-600">
</Button>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</TabsContent>
{/* 审核中心 */}
<TabsContent value="review" className="mt-4">
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-base"></CardTitle>
<p className="text-sm text-slate-500 mt-1">AI生成的数据修正建议</p>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<span className="text-sm text-slate-500">:</span>
<div className="w-32">
<Slider
value={confidenceFilter}
onValueChange={setConfidenceFilter}
min={0}
max={1}
step={0.1}
/>
</div>
<span className="text-sm font-medium">
{">="}
{confidenceFilter[0]}
</span>
</div>
{selectedSuggestions.length > 0 && (
<div className="flex items-center gap-2">
<span className="text-sm text-slate-500"> {selectedSuggestions.length} </span>
<Button size="sm" variant="outline" className="text-emerald-600 bg-transparent">
<Check className="w-4 h-4 mr-1" />
</Button>
<Button size="sm" variant="outline" className="text-red-600 bg-transparent">
<X className="w-4 h-4 mr-1" />
</Button>
</div>
)}
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-3">
{filteredSuggestions.map((suggestion) => (
<div key={suggestion.id} className="p-4 bg-slate-50/80 rounded-lg border border-slate-200/60">
<div className="flex items-start justify-between">
<div className="flex items-start gap-3">
<input
type="checkbox"
className="mt-1 rounded"
checked={selectedSuggestions.includes(suggestion.id)}
onChange={(e) => {
if (e.target.checked) {
setSelectedSuggestions([...selectedSuggestions, suggestion.id])
} else {
setSelectedSuggestions(selectedSuggestions.filter((id) => id !== suggestion.id))
}
}}
/>
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<Badge variant="outline">{suggestion.ruleName}</Badge>
<Badge className="bg-blue-100 text-blue-700">: {suggestion.field}</Badge>
<Badge
className={`${
suggestion.confidence >= 0.9
? "bg-emerald-100 text-emerald-700"
: suggestion.confidence >= 0.7
? "bg-amber-100 text-amber-700"
: "bg-red-100 text-red-700"
}`}
>
: {(suggestion.confidence * 100).toFixed(0)}%
</Badge>
<span className="text-xs text-slate-500"> {suggestion.affectedRows} </span>
</div>
<div className="grid grid-cols-2 gap-4 p-3 bg-white rounded-lg mb-2">
<div>
<p className="text-xs text-slate-500 mb-1"></p>
<code className="text-sm text-red-600 bg-red-50 px-2 py-1 rounded">
{suggestion.originalValue}
</code>
</div>
<div>
<p className="text-xs text-slate-500 mb-1"></p>
<code className="text-sm text-emerald-600 bg-emerald-50 px-2 py-1 rounded">
{suggestion.suggestedValue}
</code>
</div>
</div>
<div className="p-3 bg-white rounded-lg">
<div className="flex items-center gap-2 text-xs text-slate-500 mb-1">
<Brain className="w-3 h-3" />
AI分析理由
</div>
<p className="text-sm text-slate-600">{suggestion.reason}</p>
</div>
<p className="text-xs text-slate-500 mt-2">: {suggestion.createdAt}</p>
</div>
</div>
<div className="flex items-center gap-2">
<Button size="sm" variant="outline" className="text-emerald-600 bg-transparent">
<Check className="w-4 h-4" />
</Button>
<Button size="sm" variant="outline" className="text-red-600 bg-transparent">
<X className="w-4 h-4" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => {
setSelectedSuggestion(suggestion)
setPreviewDialogOpen(true)
}}
>
<Eye className="w-4 h-4" />
</Button>
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</TabsContent>
{/* 操作历史 */}
<TabsContent value="history" className="mt-4">
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardHeader className="pb-3">
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{cleaningHistory.map((item) => (
<div key={item.id} className="flex items-center justify-between p-3 bg-slate-50/80 rounded-lg">
<div className="flex items-center gap-3">
<Badge variant="outline">{item.rule}</Badge>
<Badge
className={`${
item.action === "approved" ? "bg-emerald-100 text-emerald-700" : "bg-red-100 text-red-700"
}`}
>
{item.action === "approved" ? "通过" : "拒绝"}
</Badge>
<span className="text-sm text-slate-600">{item.count} </span>
{item.reason && <span className="text-sm text-slate-500">: {item.reason}</span>}
</div>
<div className="flex items-center gap-4 text-sm text-slate-500">
<span>{item.operator}</span>
<span>{item.time}</span>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
{/* 创建AI规则弹窗 */}
<Dialog open={createRuleDialogOpen} onOpenChange={setCreateRuleDialogOpen}>
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle>AI清洗规则</DialogTitle>
<DialogDescription>AI模型</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label></Label>
<Input
placeholder="例如:用户数据智能清洗"
value={newRule.name}
onChange={(e) => setNewRule({ ...newRule, name: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label>AI模型</Label>
<Select value={newRule.model} onValueChange={(v) => setNewRule({ ...newRule, model: v })}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="gpt-4">GPT-4</SelectItem>
<SelectItem value="gpt-4-turbo">GPT-4 Turbo</SelectItem>
<SelectItem value="claude-3">Claude 3</SelectItem>
<SelectItem value="qwen-max"> Max</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label></Label>
<Select
value={newRule.dataSource}
onValueChange={(v) => setNewRule({ ...newRule, dataSource: v, targetTable: "" })}
>
<SelectTrigger>
<SelectValue placeholder="从数据治理模块选择" />
</SelectTrigger>
<SelectContent>
{availableDataSources.map((ds) => (
<SelectItem key={ds.id} value={ds.name}>
<div className="flex items-center gap-2">
<Database className="w-4 h-4" />
<span>{ds.name}</span>
<Badge variant="secondary" className="text-xs">
{ds.type}
</Badge>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label></Label>
<Select
value={newRule.targetTable}
onValueChange={(v) => setNewRule({ ...newRule, targetTable: v })}
disabled={!newRule.dataSource}
>
<SelectTrigger>
<SelectValue placeholder="选择要清洗的表" />
</SelectTrigger>
<SelectContent>
{getTablesForDataSource(newRule.dataSource).map((table) => (
<SelectItem key={table} value={table}>
{table}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label>AI提示词</Label>
<Textarea
placeholder="描述你希望AI如何清洗数据。例如分析用户数据表识别以下问题并提供修正建议..."
value={newRule.prompt}
onChange={(e) => setNewRule({ ...newRule, prompt: e.target.value })}
rows={4}
/>
<p className="text-xs text-slate-500"></p>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label> ()</Label>
<div className="flex items-center gap-4">
<Slider
value={[newRule.confidenceThreshold]}
onValueChange={(v) => setNewRule({ ...newRule, confidenceThreshold: v[0] })}
min={0.1}
max={1}
step={0.1}
className="flex-1"
/>
<span className="w-12 text-sm font-medium">{newRule.confidenceThreshold}</span>
</div>
</div>
<div className="space-y-2">
<Label></Label>
<div className="flex items-center gap-4">
<Slider
value={[newRule.autoApproveThreshold]}
onValueChange={(v) => setNewRule({ ...newRule, autoApproveThreshold: v[0] })}
min={0.1}
max={1}
step={0.1}
className="flex-1"
/>
<span className="w-12 text-sm font-medium">{newRule.autoApproveThreshold}</span>
</div>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setCreateRuleDialogOpen(false)}>
</Button>
<Button onClick={() => setCreateRuleDialogOpen(false)}>
<Sparkles className="w-4 h-4 mr-2" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 规则设置弹窗 */}
<Dialog open={settingsDialogOpen} onOpenChange={setSettingsDialogOpen}>
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle> - {selectedRule?.name}</DialogTitle>
</DialogHeader>
{selectedRule && (
<div className="space-y-4 py-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label></Label>
<Input defaultValue={selectedRule.name} />
</div>
<div className="space-y-2">
<Label>AI模型</Label>
<Select defaultValue={selectedRule.model.toLowerCase().replace(" ", "-")}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="gpt-4">GPT-4</SelectItem>
<SelectItem value="claude-3">Claude 3</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label></Label>
<Input defaultValue={selectedRule.dataSource} disabled />
</div>
<div className="space-y-2">
<Label>AI提示词</Label>
<Textarea defaultValue={selectedRule.prompt} rows={4} />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label></Label>
<div className="flex items-center gap-4">
<Slider
defaultValue={[selectedRule.confidenceThreshold]}
min={0.1}
max={1}
step={0.1}
className="flex-1"
/>
<span className="w-12 text-sm">{selectedRule.confidenceThreshold}</span>
</div>
</div>
<div className="space-y-2">
<Label></Label>
<div className="flex items-center gap-4">
<Slider
defaultValue={[selectedRule.autoApproveThreshold]}
min={0.1}
max={1}
step={0.1}
className="flex-1"
/>
<span className="w-12 text-sm">{selectedRule.autoApproveThreshold}</span>
</div>
</div>
</div>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => setSettingsDialogOpen(false)}>
</Button>
<Button onClick={() => setSettingsDialogOpen(false)}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 预览弹窗 */}
<Dialog open={previewDialogOpen} onOpenChange={setPreviewDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
{selectedSuggestion && (
<div className="space-y-4 py-4">
<div className="grid grid-cols-2 gap-4">
<div className="p-3 bg-slate-50 rounded-lg">
<p className="text-xs text-slate-500"></p>
<p className="font-medium">{selectedSuggestion.ruleName}</p>
</div>
<div className="p-3 bg-slate-50 rounded-lg">
<p className="text-xs text-slate-500"></p>
<p className="font-medium">{selectedSuggestion.field}</p>
</div>
</div>
<div className="p-4 bg-slate-50 rounded-lg">
<p className="text-xs text-slate-500 mb-2"></p>
<div className="flex items-center gap-4">
<code className="flex-1 p-2 bg-red-50 text-red-600 rounded text-sm">
{selectedSuggestion.originalValue}
</code>
<span className="text-slate-400"></span>
<code className="flex-1 p-2 bg-emerald-50 text-emerald-600 rounded text-sm">
{selectedSuggestion.suggestedValue}
</code>
</div>
</div>
<div className="p-4 bg-slate-50 rounded-lg">
<div className="flex items-center gap-2 text-xs text-slate-500 mb-1">
<Brain className="w-3 h-3" />
AI分析理由
</div>
<p className="text-sm">{selectedSuggestion.reason}</p>
</div>
<div className="flex items-center justify-between text-sm text-slate-500">
<span>: {selectedSuggestion.affectedRows}</span>
<span>: {(selectedSuggestion.confidence * 100).toFixed(0)}%</span>
</div>
</div>
)}
<DialogFooter>
<Button
variant="outline"
className="text-red-600 bg-transparent"
onClick={() => setPreviewDialogOpen(false)}
>
<X className="w-4 h-4 mr-2" />
</Button>
<Button className="bg-emerald-600 hover:bg-emerald-700" onClick={() => setPreviewDialogOpen(false)}>
<Check className="w-4 h-4 mr-2" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}

View File

@@ -0,0 +1,21 @@
import { Skeleton } from "@/components/ui/skeleton"
export default function Loading() {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50/30 p-6">
<div className="max-w-7xl mx-auto space-y-6">
<Skeleton className="h-10 w-64" />
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{[...Array(4)].map((_, i) => (
<Skeleton key={i} className="h-28 rounded-xl" />
))}
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{[...Array(4)].map((_, i) => (
<Skeleton key={i} className="h-40 rounded-xl" />
))}
</div>
</div>
</div>
)
}

251
app/ai-insight/nlq/page.tsx Normal file
View File

@@ -0,0 +1,251 @@
"use client"
import { useState } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import { MessageSquare, ArrowLeft, Sparkles, Code, Copy, Play, Clock, CheckCircle2, Table } from "lucide-react"
import Link from "next/link"
const exampleQueries = [
"查询最近7天新增的高价值用户",
"统计各项目的用户流失率",
"找出消费金额前100的用户",
"分析用户活跃时段分布",
]
const queryHistory = [
{
id: 1,
query: "查询最近30天流失风险大于80的用户数量",
sql: "SELECT COUNT(*) FROM users WHERE churn_risk > 80 AND last_active >= DATE_SUB(NOW(), INTERVAL 30 DAY)",
result: "共 2,340 位用户",
time: "10分钟前",
},
{
id: 2,
query: "统计各省份的用户分布",
sql: "SELECT province, COUNT(*) as count FROM users GROUP BY province ORDER BY count DESC",
result: "返回 31 条记录",
time: "1小时前",
},
{
id: 3,
query: "找出RFM评分S级的用户",
sql: "SELECT * FROM users WHERE rfm_level = 'S' ORDER BY total_value DESC LIMIT 100",
result: "返回 100 条记录",
time: "2小时前",
},
]
export default function NLQPage() {
const [query, setQuery] = useState("")
const [isProcessing, setIsProcessing] = useState(false)
const [result, setResult] = useState<{
sql: string
data: { columns: string[]; rows: string[][] }
summary: string
} | null>(null)
const handleSubmit = () => {
if (!query.trim()) return
setIsProcessing(true)
// 模拟AI处理
setTimeout(() => {
setResult({
sql: `SELECT user_id, name, phone, rfm_score, total_value
FROM users
WHERE rfm_level = 'S'
AND last_active >= DATE_SUB(NOW(), INTERVAL 7 DAY)
ORDER BY total_value DESC
LIMIT 10`,
data: {
columns: ["用户ID", "姓名", "手机号", "RFM评分", "总价值"],
rows: [
["U001", "张伟", "138****8888", "95", "¥125,800"],
["U002", "李娜", "139****6666", "92", "¥98,500"],
["U003", "王芳", "137****5555", "90", "¥87,200"],
["U004", "刘洋", "136****4444", "88", "¥76,800"],
["U005", "陈静", "135****3333", "86", "¥65,400"],
],
},
summary: "找到 5 位符合条件的S级高价值用户总价值 ¥453,700",
})
setIsProcessing(false)
}, 1500)
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-emerald-50/30 p-6">
<div className="max-w-7xl mx-auto space-y-6">
{/* 页面标题 */}
<div className="flex items-center gap-4">
<Link href="/ai-insight">
<Button variant="ghost" size="icon" className="rounded-full">
<ArrowLeft className="w-5 h-5" />
</Button>
</Link>
<div>
<h1 className="text-2xl font-bold text-slate-800"></h1>
<p className="text-slate-500 mt-1">AI自动生成SQL并执行</p>
</div>
</div>
{/* 查询输入区 */}
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardContent className="p-6">
<div className="flex items-center gap-3">
<div className="flex-1 relative">
<MessageSquare className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-400" />
<Input
placeholder="用自然语言描述你想查询的数据例如查询最近7天新增的高价值用户..."
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSubmit()}
className="pl-12 pr-4 py-6 text-base"
/>
</div>
<Button
onClick={handleSubmit}
disabled={isProcessing || !query.trim()}
className="bg-gradient-to-r from-emerald-500 to-teal-600 hover:from-emerald-600 hover:to-teal-700 px-6 py-6"
>
{isProcessing ? (
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
) : (
<>
<Sparkles className="w-5 h-5 mr-2" />
AI查询
</>
)}
</Button>
</div>
{/* 示例查询 */}
<div className="flex items-center gap-2 mt-4 flex-wrap">
<span className="text-sm text-slate-500"></span>
{exampleQueries.map((eq, i) => (
<Button
key={i}
variant="outline"
size="sm"
className="text-xs bg-transparent"
onClick={() => setQuery(eq)}
>
{eq}
</Button>
))}
</div>
</CardContent>
</Card>
{/* 查询结果 */}
{result && (
<div className="space-y-4">
{/* 生成的SQL */}
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-base flex items-center gap-2">
<Code className="w-4 h-4 text-slate-500" />
SQL
</CardTitle>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm">
<Copy className="w-4 h-4 mr-1" />
</Button>
<Button variant="outline" size="sm">
<Play className="w-4 h-4 mr-1" />
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<pre className="bg-slate-900 text-slate-100 p-4 rounded-lg text-sm overflow-x-auto">
<code>{result.sql}</code>
</pre>
</CardContent>
</Card>
{/* 查询结果表格 */}
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-base flex items-center gap-2">
<Table className="w-4 h-4 text-slate-500" />
</CardTitle>
<Badge className="bg-emerald-100 text-emerald-700">
<CheckCircle2 className="w-3 h-3 mr-1" />
{result.summary}
</Badge>
</div>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-slate-200">
{result.data.columns.map((col, i) => (
<th key={i} className="text-left py-3 px-4 text-xs font-medium text-slate-500">
{col}
</th>
))}
</tr>
</thead>
<tbody>
{result.data.rows.map((row, i) => (
<tr key={i} className="border-b border-slate-100 hover:bg-slate-50/50">
{row.map((cell, j) => (
<td key={j} className="py-3 px-4 text-sm text-slate-700">
{cell}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
</div>
)}
{/* 查询历史 */}
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<Clock className="w-4 h-4 text-slate-500" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{queryHistory.map((item) => (
<div key={item.id} className="p-4 bg-slate-50/80 rounded-lg border border-slate-200/60">
<div className="flex items-start justify-between">
<div className="flex-1">
<p className="text-sm font-medium text-slate-800">{item.query}</p>
<pre className="mt-2 text-xs text-slate-500 bg-slate-100 p-2 rounded overflow-x-auto">
{item.sql}
</pre>
</div>
<div className="text-right ml-4">
<Badge variant="outline" className="text-xs">
{item.result}
</Badge>
<p className="text-xs text-slate-500 mt-1">{item.time}</p>
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
</div>
)
}

292
app/ai-insight/page.tsx Normal file
View File

@@ -0,0 +1,292 @@
"use client"
import { useState, useEffect } from "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 {
Brain,
Sparkles,
TrendingUp,
MessageSquare,
FileText,
Tag,
Target,
Zap,
ArrowRight,
Activity,
Clock,
CheckCircle2,
AlertTriangle,
} from "lucide-react"
import Link from "next/link"
// 模拟数据
const aiModules = [
{
id: "smart-tag",
name: "智能打标",
description: "基于NLP和行为分析自动为用户生成标签",
icon: Tag,
color: "from-violet-500 to-purple-600",
status: "running",
stats: { today: 12580, accuracy: 94.2 },
href: "/ai-insight/smart-tag",
},
{
id: "prediction",
name: "智能预测",
description: "预测用户流失、转化、价值变化趋势",
icon: TrendingUp,
color: "from-blue-500 to-cyan-600",
status: "running",
stats: { today: 8420, accuracy: 91.8 },
href: "/ai-insight/prediction",
},
{
id: "nlq",
name: "自然语言查询",
description: "用自然语言查询数据AI自动生成SQL",
icon: MessageSquare,
color: "from-emerald-500 to-teal-600",
status: "running",
stats: { today: 256, accuracy: 88.5 },
href: "/ai-insight/nlq",
},
{
id: "report",
name: "智能报告生成",
description: "自动生成数据分析报告和洞察摘要",
icon: FileText,
color: "from-orange-500 to-amber-600",
status: "running",
stats: { today: 48, accuracy: 92.1 },
href: "/ai-insight/report",
},
]
const recentTasks = [
{ id: 1, type: "智能打标", target: "新增用户批次", status: "completed", count: 2580, time: "2分钟前" },
{ id: 2, type: "流失预测", target: "全量用户扫描", status: "running", progress: 67, time: "进行中" },
{ id: 3, type: "NLQ查询", target: "高价值用户分布", status: "completed", count: 1, time: "5分钟前" },
{ id: 4, type: "报告生成", target: "周度运营报告", status: "completed", count: 1, time: "1小时前" },
{ id: 5, type: "智能打标", target: "行为标签更新", status: "warning", count: 156, time: "2小时前" },
]
const aiInsights = [
{ id: 1, type: "预警", content: "检测到 2,340 位用户有流失风险,建议启动挽留计划", priority: "high" },
{ id: 2, type: "机会", content: "发现 890 位高潜力用户,转化概率 > 75%", priority: "medium" },
{ id: 3, type: "趋势", content: "近7天用户活跃度环比上升 12.5%", priority: "low" },
]
export default function AIInsightPage() {
const [gpuUsage, setGpuUsage] = useState(68)
const [apiCalls, setApiCalls] = useState(125800)
useEffect(() => {
const interval = setInterval(() => {
setGpuUsage((prev) => Math.min(95, Math.max(50, prev + (Math.random() - 0.5) * 10)))
setApiCalls((prev) => prev + Math.floor(Math.random() * 100))
}, 3000)
return () => clearInterval(interval)
}, [])
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50/30 p-6">
<div className="max-w-7xl mx-auto space-y-6">
{/* 页面标题 */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-800">AI赋能与洞察中心</h1>
<p className="text-slate-500 mt-1"></p>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2 px-3 py-1.5 bg-emerald-50 rounded-full">
<span className="w-2 h-2 bg-emerald-500 rounded-full animate-pulse" />
<span className="text-sm text-emerald-700">AI引擎运行中</span>
</div>
</div>
</div>
{/* AI 资源状态 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-slate-500">GPU使用率</p>
<p className="text-2xl font-bold text-slate-800">{gpuUsage.toFixed(1)}%</p>
</div>
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-violet-500 to-purple-600 flex items-center justify-center">
<Brain className="w-6 h-6 text-white" />
</div>
</div>
<Progress value={gpuUsage} className="mt-3 h-1.5" />
</CardContent>
</Card>
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-slate-500">API调用</p>
<p className="text-2xl font-bold text-slate-800">{apiCalls.toLocaleString()}</p>
</div>
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-blue-500 to-cyan-600 flex items-center justify-center">
<Zap className="w-6 h-6 text-white" />
</div>
</div>
<p className="text-xs text-emerald-600 mt-3"> +18.2%</p>
</CardContent>
</Card>
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-slate-500"></p>
<p className="text-2xl font-bold text-slate-800">91.6%</p>
</div>
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-emerald-500 to-teal-600 flex items-center justify-center">
<Target className="w-6 h-6 text-white" />
</div>
</div>
<p className="text-xs text-emerald-600 mt-3"> +3.2%</p>
</CardContent>
</Card>
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-slate-500"></p>
<p className="text-2xl font-bold text-slate-800">12</p>
</div>
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-orange-500 to-amber-600 flex items-center justify-center">
<Activity className="w-6 h-6 text-white" />
</div>
</div>
<p className="text-xs text-slate-500 mt-3"></p>
</CardContent>
</Card>
</div>
{/* AI 功能模块入口 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{aiModules.map((module) => (
<Link key={module.id} href={module.href}>
<Card className="bg-white/70 backdrop-blur border-slate-200/60 hover:shadow-lg hover:border-slate-300 transition-all cursor-pointer group h-full">
<CardContent className="p-5">
<div
className={`w-12 h-12 rounded-xl bg-gradient-to-br ${module.color} flex items-center justify-center mb-4`}
>
<module.icon className="w-6 h-6 text-white" />
</div>
<h3 className="font-semibold text-slate-800 mb-1">{module.name}</h3>
<p className="text-sm text-slate-500 mb-4">{module.description}</p>
<div className="flex items-center justify-between">
<div className="text-xs text-slate-500">
:{" "}
<span className="font-medium text-slate-700">{module.stats.today.toLocaleString()}</span>
</div>
<ArrowRight className="w-4 h-4 text-slate-400 group-hover:text-slate-600 group-hover:translate-x-1 transition-all" />
</div>
</CardContent>
</Card>
</Link>
))}
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* AI 洞察 */}
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<Sparkles className="w-4 h-4 text-amber-500" />
AI
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{aiInsights.map((insight) => (
<div
key={insight.id}
className={`p-3 rounded-lg border ${
insight.priority === "high"
? "bg-red-50 border-red-200"
: insight.priority === "medium"
? "bg-amber-50 border-amber-200"
: "bg-blue-50 border-blue-200"
}`}
>
<div className="flex items-start gap-2">
<Badge
variant="outline"
className={
insight.priority === "high"
? "bg-red-100 text-red-700 border-red-300"
: insight.priority === "medium"
? "bg-amber-100 text-amber-700 border-amber-300"
: "bg-blue-100 text-blue-700 border-blue-300"
}
>
{insight.type}
</Badge>
</div>
<p className="text-sm text-slate-700 mt-2">{insight.content}</p>
</div>
))}
<Button variant="outline" className="w-full mt-2 bg-transparent" size="sm">
</Button>
</CardContent>
</Card>
{/* 最近任务 */}
<Card className="lg:col-span-2 bg-white/70 backdrop-blur border-slate-200/60">
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<Clock className="w-4 h-4 text-slate-500" />
AI任务
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{recentTasks.map((task) => (
<div key={task.id} className="flex items-center justify-between p-3 bg-slate-50/80 rounded-lg">
<div className="flex items-center gap-3">
{task.status === "completed" ? (
<CheckCircle2 className="w-5 h-5 text-emerald-500" />
) : task.status === "running" ? (
<div className="w-5 h-5 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
) : (
<AlertTriangle className="w-5 h-5 text-amber-500" />
)}
<div>
<p className="text-sm font-medium text-slate-800">{task.type}</p>
<p className="text-xs text-slate-500">{task.target}</p>
</div>
</div>
<div className="text-right">
{task.status === "running" ? (
<div className="flex items-center gap-2">
<Progress value={task.progress} className="w-20 h-1.5" />
<span className="text-xs text-slate-500">{task.progress}%</span>
</div>
) : (
<>
<p className="text-sm font-medium text-slate-700">{task.count?.toLocaleString()}</p>
<p className="text-xs text-slate-500">{task.time}</p>
</>
)}
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,331 @@
"use client"
import { useState } from "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 {
TrendingUp,
TrendingDown,
Users,
AlertTriangle,
Target,
ArrowLeft,
Play,
Calendar,
BarChart3,
PieChart,
} from "lucide-react"
import Link from "next/link"
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area } from "recharts"
// 模拟预测数据
const churnPrediction = [
{ date: "12-06", actual: 120, predicted: 125 },
{ date: "12-07", actual: 135, predicted: 130 },
{ date: "12-08", actual: 128, predicted: 132 },
{ date: "12-09", actual: 142, predicted: 138 },
{ date: "12-10", actual: 138, predicted: 145 },
{ date: "12-11", actual: 155, predicted: 150 },
{ date: "12-12", actual: null, predicted: 162 },
{ date: "12-13", actual: null, predicted: 158 },
{ date: "12-14", actual: null, predicted: 165 },
]
const conversionPrediction = [
{ date: "12-06", rate: 3.2 },
{ date: "12-07", rate: 3.5 },
{ date: "12-08", rate: 3.3 },
{ date: "12-09", rate: 3.8 },
{ date: "12-10", rate: 4.1 },
{ date: "12-11", rate: 4.0 },
{ date: "12-12", rate: 4.3 },
{ date: "12-13", rate: 4.5 },
{ date: "12-14", rate: 4.8 },
]
const riskUsers = [
{ id: 1, name: "用户A****8", risk: 92, lastActive: "7天前", value: "高", reason: "活跃度骤降" },
{ id: 2, name: "用户B****2", risk: 88, lastActive: "5天前", value: "高", reason: "消费频次下降" },
{ id: 3, name: "用户C****6", risk: 85, lastActive: "6天前", value: "中", reason: "互动减少" },
{ id: 4, name: "用户D****1", risk: 82, lastActive: "4天前", value: "高", reason: "投诉增加" },
{ id: 5, name: "用户E****9", risk: 78, lastActive: "8天前", value: "中", reason: "登录减少" },
]
export default function PredictionPage() {
const [timeRange, setTimeRange] = useState("7d")
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50/30 p-6">
<div className="max-w-7xl mx-auto space-y-6">
{/* 页面标题 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Link href="/ai-insight">
<Button variant="ghost" size="icon" className="rounded-full">
<ArrowLeft className="w-5 h-5" />
</Button>
</Link>
<div>
<h1 className="text-2xl font-bold text-slate-800"></h1>
<p className="text-slate-500 mt-1"></p>
</div>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setTimeRange("7d")}
className={timeRange === "7d" ? "bg-slate-100" : ""}
>
<Calendar className="w-4 h-4 mr-1" />
7
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setTimeRange("30d")}
className={timeRange === "30d" ? "bg-slate-100" : ""}
>
30
</Button>
<Button className="bg-gradient-to-r from-blue-500 to-cyan-600 hover:from-blue-600 hover:to-cyan-700">
<Play className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
{/* 预测概览 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-slate-500"></p>
<p className="text-2xl font-bold text-red-600">2,340</p>
</div>
<div className="w-10 h-10 rounded-lg bg-red-100 flex items-center justify-center">
<TrendingDown className="w-5 h-5 text-red-600" />
</div>
</div>
<p className="text-xs text-red-600 mt-2">7</p>
</CardContent>
</Card>
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-slate-500"></p>
<p className="text-2xl font-bold text-emerald-600">890</p>
</div>
<div className="w-10 h-10 rounded-lg bg-emerald-100 flex items-center justify-center">
<TrendingUp className="w-5 h-5 text-emerald-600" />
</div>
</div>
<p className="text-xs text-emerald-600 mt-2"> {"> "}75%</p>
</CardContent>
</Card>
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-slate-500"></p>
<p className="text-2xl font-bold text-slate-800">91.8%</p>
</div>
<div className="w-10 h-10 rounded-lg bg-blue-100 flex items-center justify-center">
<Target className="w-5 h-5 text-blue-600" />
</div>
</div>
<p className="text-xs text-emerald-600 mt-2"> +1.2%</p>
</CardContent>
</Card>
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-slate-500"></p>
<p className="text-2xl font-bold text-amber-600">156</p>
</div>
<div className="w-10 h-10 rounded-lg bg-amber-100 flex items-center justify-center">
<AlertTriangle className="w-5 h-5 text-amber-600" />
</div>
</div>
<p className="text-xs text-amber-600 mt-2"> {"> "}80</p>
</CardContent>
</Card>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* 流失预测趋势 */}
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardHeader className="pb-2">
<CardTitle className="text-base flex items-center gap-2">
<BarChart3 className="w-4 h-4 text-slate-500" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={churnPrediction}>
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
<XAxis dataKey="date" tick={{ fontSize: 12 }} stroke="#94a3b8" />
<YAxis tick={{ fontSize: 12 }} stroke="#94a3b8" />
<Tooltip
contentStyle={{
backgroundColor: "white",
border: "1px solid #e2e8f0",
borderRadius: "8px",
boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1)",
}}
/>
<Line
type="monotone"
dataKey="actual"
stroke="#3b82f6"
strokeWidth={2}
dot={{ r: 4 }}
name="实际值"
/>
<Line
type="monotone"
dataKey="predicted"
stroke="#f97316"
strokeWidth={2}
strokeDasharray="5 5"
dot={{ r: 4 }}
name="预测值"
/>
</LineChart>
</ResponsiveContainer>
</div>
<div className="flex items-center justify-center gap-6 mt-4">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-blue-500" />
<span className="text-xs text-slate-600"></span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-0.5 bg-orange-500" style={{ borderStyle: "dashed" }} />
<span className="text-xs text-slate-600"></span>
</div>
</div>
</CardContent>
</Card>
{/* 转化率预测 */}
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardHeader className="pb-2">
<CardTitle className="text-base flex items-center gap-2">
<PieChart className="w-4 h-4 text-slate-500" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={conversionPrediction}>
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
<XAxis dataKey="date" tick={{ fontSize: 12 }} stroke="#94a3b8" />
<YAxis tick={{ fontSize: 12 }} stroke="#94a3b8" tickFormatter={(v) => `${v}%`} />
<Tooltip
contentStyle={{
backgroundColor: "white",
border: "1px solid #e2e8f0",
borderRadius: "8px",
}}
formatter={(value: number) => [`${value}%`, "转化率"]}
/>
<defs>
<linearGradient id="colorRate" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#10b981" stopOpacity={0.3} />
<stop offset="95%" stopColor="#10b981" stopOpacity={0} />
</linearGradient>
</defs>
<Area type="monotone" dataKey="rate" stroke="#10b981" strokeWidth={2} fill="url(#colorRate)" />
</AreaChart>
</ResponsiveContainer>
</div>
<div className="text-center mt-4">
<p className="text-sm text-slate-600">
7 <span className="font-bold text-emerald-600">4.8%</span>
</p>
</div>
</CardContent>
</Card>
</div>
{/* 高风险用户列表 */}
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-base flex items-center gap-2">
<AlertTriangle className="w-4 h-4 text-amber-500" />
</CardTitle>
<Button variant="outline" size="sm">
</Button>
</div>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-slate-200">
<th className="text-left py-3 px-4 text-xs font-medium text-slate-500"></th>
<th className="text-left py-3 px-4 text-xs font-medium text-slate-500"></th>
<th className="text-left py-3 px-4 text-xs font-medium text-slate-500"></th>
<th className="text-left py-3 px-4 text-xs font-medium text-slate-500"></th>
<th className="text-left py-3 px-4 text-xs font-medium text-slate-500"></th>
<th className="text-left py-3 px-4 text-xs font-medium text-slate-500"></th>
</tr>
</thead>
<tbody>
{riskUsers.map((user) => (
<tr key={user.id} className="border-b border-slate-100 hover:bg-slate-50/50">
<td className="py-3 px-4">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-slate-200 to-slate-300 flex items-center justify-center">
<Users className="w-4 h-4 text-slate-600" />
</div>
<span className="text-sm font-medium text-slate-800">{user.name}</span>
</div>
</td>
<td className="py-3 px-4">
<div className="flex items-center gap-2">
<Progress value={user.risk} className="w-16 h-2" />
<span className="text-sm font-medium text-red-600">{user.risk}</span>
</div>
</td>
<td className="py-3 px-4">
<Badge
className={
user.value === "高" ? "bg-amber-100 text-amber-700" : "bg-slate-100 text-slate-700"
}
>
{user.value}
</Badge>
</td>
<td className="py-3 px-4 text-sm text-slate-600">{user.lastActive}</td>
<td className="py-3 px-4 text-sm text-slate-600">{user.reason}</td>
<td className="py-3 px-4">
<Button variant="outline" size="sm">
</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
</div>
</div>
)
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

View File

@@ -0,0 +1,228 @@
"use client"
import { useState } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import {
FileText,
ArrowLeft,
Plus,
Download,
Eye,
Calendar,
Clock,
CheckCircle2,
RefreshCw,
Search,
Filter,
BarChart3,
PieChart,
TrendingUp,
} from "lucide-react"
import Link from "next/link"
const reportTemplates = [
{
id: 1,
name: "日度运营报告",
description: "每日用户活跃、增长、转化数据汇总",
schedule: "每日 09:00",
icon: BarChart3,
},
{
id: 2,
name: "周度分析报告",
description: "每周关键指标趋势分析和洞察",
schedule: "每周一 10:00",
icon: TrendingUp,
},
{ id: 3, name: "月度价值报告", description: "月度用户价值评估和排名变化", schedule: "每月1日 09:00", icon: PieChart },
{ id: 4, name: "自定义报告", description: "根据需求自定义报告内容和维度", schedule: "手动触发", icon: FileText },
]
const generatedReports = [
{
id: 1,
name: "2024年12月第2周运营分析报告",
type: "周度分析报告",
status: "completed",
pages: 12,
generatedAt: "2024-12-12 10:00",
summary: "本周新增用户 12,580环比增长 15.3%;活跃用户 89,200DAU/MAU 提升至 32.5%",
},
{
id: 2,
name: "2024年12月11日运营日报",
type: "日度运营报告",
status: "completed",
pages: 6,
generatedAt: "2024-12-11 09:00",
summary: "日活 28,500新增 1,890转化率 3.8%,各项指标均达预期",
},
{
id: 3,
name: "2024年12月12日运营日报",
type: "日度运营报告",
status: "generating",
pages: 0,
generatedAt: "生成中...",
summary: "",
},
{
id: 4,
name: "2024年11月用户价值评估报告",
type: "月度价值报告",
status: "completed",
pages: 28,
generatedAt: "2024-12-01 09:00",
summary: "S级用户占比提升至 8.2%,高价值用户总价值突破 125 亿元",
},
]
export default function ReportPage() {
const [searchTerm, setSearchTerm] = useState("")
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-orange-50/30 p-6">
<div className="max-w-7xl mx-auto space-y-6">
{/* 页面标题 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Link href="/ai-insight">
<Button variant="ghost" size="icon" className="rounded-full">
<ArrowLeft className="w-5 h-5" />
</Button>
</Link>
<div>
<h1 className="text-2xl font-bold text-slate-800"></h1>
<p className="text-slate-500 mt-1"></p>
</div>
</div>
<Button className="bg-gradient-to-r from-orange-500 to-amber-600 hover:from-orange-600 hover:to-amber-700">
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
{/* 报告模板 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{reportTemplates.map((template) => (
<Card
key={template.id}
className="bg-white/70 backdrop-blur border-slate-200/60 hover:shadow-lg hover:border-slate-300 transition-all cursor-pointer"
>
<CardContent className="p-5">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-orange-500 to-amber-600 flex items-center justify-center mb-3">
<template.icon className="w-5 h-5 text-white" />
</div>
<h3 className="font-semibold text-slate-800 mb-1">{template.name}</h3>
<p className="text-xs text-slate-500 mb-3">{template.description}</p>
<div className="flex items-center gap-1 text-xs text-slate-400">
<Calendar className="w-3 h-3" />
{template.schedule}
</div>
</CardContent>
</Card>
))}
</div>
{/* 已生成报告列表 */}
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-base"></CardTitle>
<div className="flex items-center gap-2">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<Input
placeholder="搜索报告..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-9 w-64"
/>
</div>
<Button variant="outline" size="sm">
<Filter className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-3">
{generatedReports.map((report) => (
<div
key={report.id}
className="flex items-center justify-between p-4 bg-slate-50/80 rounded-lg border border-slate-200/60"
>
<div className="flex items-center gap-4 flex-1">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-orange-100 to-amber-100 flex items-center justify-center">
<FileText className="w-5 h-5 text-orange-600" />
</div>
<div className="flex-1">
<div className="flex items-center gap-2">
<h4 className="font-medium text-slate-800">{report.name}</h4>
<Badge variant="outline" className="text-xs">
{report.type}
</Badge>
{report.status === "completed" ? (
<Badge className="bg-emerald-100 text-emerald-700 border-emerald-300">
<CheckCircle2 className="w-3 h-3 mr-1" />
</Badge>
) : (
<Badge className="bg-blue-100 text-blue-700 border-blue-300">
<RefreshCw className="w-3 h-3 mr-1 animate-spin" />
</Badge>
)}
</div>
{report.summary && <p className="text-sm text-slate-500 mt-1 line-clamp-1">{report.summary}</p>}
</div>
</div>
<div className="flex items-center gap-6">
<div className="text-right">
{report.status === "completed" && (
<p className="text-sm font-medium text-slate-700">{report.pages} </p>
)}
<p className="text-xs text-slate-500 flex items-center gap-1">
<Clock className="w-3 h-3" />
{report.generatedAt}
</p>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" disabled={report.status !== "completed"}>
<Eye className="w-4 h-4 mr-1" />
</Button>
<Button variant="outline" size="sm" disabled={report.status !== "completed"}>
<Download className="w-4 h-4 mr-1" />
</Button>
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
{/* 报告预览区域 */}
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardHeader className="pb-3">
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent>
<div className="bg-slate-100 rounded-lg p-8 text-center">
<FileText className="w-16 h-16 text-slate-300 mx-auto mb-4" />
<p className="text-slate-500"></p>
<p className="text-sm text-slate-400 mt-1">线 PDF </p>
</div>
</CardContent>
</Card>
</div>
</div>
)
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

View File

@@ -0,0 +1,127 @@
"use client"
import { useState } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { ArrowLeft, Search, Download, RefreshCw, CheckCircle, XCircle, AlertTriangle, Clock } from "lucide-react"
import Link from "next/link"
const logs = [
{ id: 1, time: "2025-12-12 14:35:12", level: "info", message: "开始执行打标任务 BATCH-20241212-003", users: 3200 },
{ id: 2, time: "2025-12-12 14:35:15", level: "info", message: "加载用户数据完成,共 3200 条", users: null },
{ id: 3, time: "2025-12-12 14:35:20", level: "info", message: "调用AI模型 XGBoost 进行预测...", users: null },
{ id: 4, time: "2025-12-12 14:36:45", level: "success", message: "生成候选标签 2856 个,置信度 >= 0.7", users: 2856 },
{ id: 5, time: "2025-12-12 14:36:48", level: "warning", message: "低置信度标签 344 个被过滤 (< 0.4)", users: 344 },
{ id: 6, time: "2025-12-12 14:36:50", level: "info", message: "写入候选标签库完成", users: null },
{ id: 7, time: "2025-12-12 14:36:52", level: "success", message: "任务完成,耗时 1m 40s", users: null },
{ id: 8, time: "2025-12-12 14:30:00", level: "info", message: "开始执行打标任务 BATCH-20241212-002", users: 1890 },
{ id: 9, time: "2025-12-12 14:32:56", level: "success", message: "任务完成,生成标签 1654 个", users: 1654 },
{ id: 10, time: "2025-12-12 14:00:00", level: "error", message: "AI模型调用超时任务中断", users: null },
{ id: 11, time: "2025-12-12 14:00:30", level: "info", message: "自动重试任务...", users: null },
{ id: 12, time: "2025-12-12 14:03:24", level: "success", message: "重试成功,任务完成", users: null },
]
const levelConfig = {
info: { color: "bg-blue-100 text-blue-700", icon: Clock },
success: { color: "bg-emerald-100 text-emerald-700", icon: CheckCircle },
warning: { color: "bg-amber-100 text-amber-700", icon: AlertTriangle },
error: { color: "bg-red-100 text-red-700", icon: XCircle },
}
export default function TagLogsPage() {
const [searchTerm, setSearchTerm] = useState("")
const [levelFilter, setLevelFilter] = useState("all")
const filteredLogs = logs.filter((log) => {
const matchesSearch = log.message.toLowerCase().includes(searchTerm.toLowerCase())
const matchesLevel = levelFilter === "all" || log.level === levelFilter
return matchesSearch && matchesLevel
})
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-violet-50/30 p-6">
<div className="max-w-5xl mx-auto space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Link href="/ai-insight/smart-tag">
<Button variant="ghost" size="icon" className="rounded-full">
<ArrowLeft className="w-5 h-5" />
</Button>
</Link>
<div>
<h1 className="text-2xl font-bold text-slate-800"></h1>
<p className="text-slate-500 mt-1"> - </p>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
</Button>
<Button variant="outline">
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-base"></CardTitle>
<div className="flex items-center gap-2">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<Input
placeholder="搜索日志..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-9 w-64"
/>
</div>
<Select value={levelFilter} onValueChange={setLevelFilter}>
<SelectTrigger className="w-32">
<SelectValue placeholder="日志级别" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="info"></SelectItem>
<SelectItem value="success"></SelectItem>
<SelectItem value="warning"></SelectItem>
<SelectItem value="error"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-2">
{filteredLogs.map((log) => {
const config = levelConfig[log.level as keyof typeof levelConfig]
const Icon = config.icon
return (
<div key={log.id} className="flex items-start gap-3 p-3 bg-slate-50/80 rounded-lg font-mono text-sm">
<Badge className={`${config.color} text-xs shrink-0`}>
<Icon className="w-3 h-3 mr-1" />
{log.level.toUpperCase()}
</Badge>
<span className="text-slate-500 shrink-0">{log.time}</span>
<span className="text-slate-800 flex-1">{log.message}</span>
{log.users && (
<Badge variant="outline" className="shrink-0">
{log.users.toLocaleString()}
</Badge>
)}
</div>
)
})}
</div>
</CardContent>
</Card>
</div>
</div>
)
}

View File

@@ -0,0 +1,980 @@
"use client"
import { useState } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Textarea } from "@/components/ui/textarea"
import { Slider } from "@/components/ui/slider"
import {
Tag,
Play,
Pause,
Settings,
RefreshCw,
CheckCircle2,
Users,
ArrowLeft,
Search,
Eye,
Check,
X,
AlertTriangle,
FileText,
Brain,
History,
Shield,
Sparkles,
Database,
MessageSquare,
Zap,
Plus,
} from "lucide-react"
import Link from "next/link"
const availableDataSources = [
{ id: "1", name: "存客宝-用户主库", type: "mysql", records: 128956342 },
{ id: "2", name: "触客宝-行为数据", type: "kafka", records: 89234567 },
{ id: "3", name: "数智员工-账号API", type: "restapi", records: 45678901 },
{ id: "7", name: "微信消息-Webhook", type: "webhook", records: 34567890 },
]
const tagRules = [
{
id: 1,
name: "高消费潜力识别",
description: "基于消费行为和浏览历史识别高消费潜力用户",
dataSource: "存客宝-用户主库",
prompt:
"分析用户的消费记录和浏览行为,识别具有高消费潜力的用户。考虑因素包括:消费频次、客单价、浏览高端商品频率、收藏夹内容等。",
model: "GPT-4",
status: "running",
confidenceThreshold: 0.7,
autoApproveThreshold: 0.9,
tagged: 125800,
pending: 2340,
approved: 120000,
rejected: 3460,
lastRun: "10分钟前",
accuracy: 94.2,
},
{
id: 2,
name: "流失风险预警",
description: "基于活跃度衰减和行为变化预测流失风险",
dataSource: "触客宝-行为数据",
prompt:
"分析用户近期的活跃度变化趋势,识别可能流失的用户。重点关注:登录频次下降、互动减少、浏览时长缩短、取消关注等负面信号。",
model: "GPT-4",
status: "running",
confidenceThreshold: 0.6,
autoApproveThreshold: 0.85,
tagged: 23400,
pending: 890,
approved: 21000,
rejected: 1510,
lastRun: "30分钟前",
accuracy: 91.5,
},
{
id: 3,
name: "兴趣偏好分析",
description: "基于聊天内容和互动行为分析用户兴趣",
dataSource: "微信消息-Webhook",
prompt:
"分析用户的聊天记录和互动内容,提取用户的兴趣偏好标签。包括但不限于:产品类目偏好、品牌偏好、价格敏感度、购买意向等。",
model: "Claude-3",
status: "paused",
confidenceThreshold: 0.5,
autoApproveThreshold: 0.8,
tagged: 456000,
pending: 12500,
approved: 420000,
rejected: 23500,
lastRun: "2小时前",
accuracy: 88.7,
},
]
const candidateTags = [
{
id: 1,
userId: "U10086",
userName: "张三",
tag: "高消费潜力",
confidence: 0.92,
source: "消费行为分析",
createdAt: "2025-12-12 14:30",
reason: "近30天消费3次客单价680元",
aiAnalysis:
"该用户近30天内有3次消费记录平均客单价680元高于平台均值45%。同时多次浏览高端商品页面收藏夹中有5件单价超过1000元的商品。综合判断具有高消费潜力。",
},
{
id: 2,
userId: "U10087",
userName: "李四",
tag: "流失风险-高",
confidence: 0.85,
source: "活跃度衰减",
createdAt: "2025-12-12 14:28",
reason: "连续7天未登录上月活跃度下降60%",
aiAnalysis:
"该用户从高活跃状态日均登录2次下降到连续7天未登录。上月互动量较前月下降60%且最后一次登录时取消了3个商品收藏。行为模式符合典型流失前兆。",
},
{
id: 3,
userId: "U10088",
userName: "王五",
tag: "兴趣偏好-美妆",
confidence: 0.78,
source: "NLP语义分析",
createdAt: "2025-12-12 14:25",
reason: "聊天记录多次提及护肤、化妆品关键词",
aiAnalysis:
"在最近20条聊天记录中用户10次提及护肤相关词汇补水、抗皱、精华5次询问化妆品推荐。结合其浏览记录中70%为美妆类目,判断其主要兴趣为美妆护肤。",
},
{
id: 4,
userId: "U10089",
userName: "赵六",
tag: "社交影响力-KOC",
confidence: 0.71,
source: "社交网络分析",
createdAt: "2025-12-12 14:22",
reason: "好友数500+朋友圈互动率12%",
aiAnalysis:
"该用户好友数量达到523人朋友圈发布频率为每周3次平均互动率12%(点赞+评论/好友数高于平台平均3.5%。具备一定的社交影响力可作为KOC培养对象。",
},
{
id: 5,
userId: "U10090",
userName: "钱七",
tag: "价格敏感型",
confidence: 0.68,
source: "购买行为分析",
createdAt: "2025-12-12 14:20",
reason: "多次使用优惠券,偏好促销商品",
aiAnalysis:
'该用户过去6个月的12次购买中11次使用了优惠券平均折扣率达到28%。浏览记录显示经常筛选"促销"标签,且多次在大促期间集中购买。符合价格敏感型用户特征。',
},
{
id: 6,
userId: "U10091",
userName: "孙八",
tag: "高消费潜力",
confidence: 0.45,
source: "消费行为分析",
createdAt: "2025-12-12 14:18",
reason: "单次消费较高但频次低",
aiAnalysis:
"该用户仅有2次消费记录但单次消费金额较高1200元、980元。由于样本量不足对其消费潜力的判断置信度较低建议人工审核确认。",
},
]
const tagHistory = [
{ id: 1, tag: "高消费潜力", action: "approved", operator: "张管理员", count: 150, time: "2025-12-12 14:00" },
{
id: 2,
tag: "流失风险",
action: "rejected",
operator: "李管理员",
count: 23,
time: "2025-12-12 13:45",
reason: "置信度过低",
},
{ id: 3, tag: "兴趣偏好", action: "approved", operator: "系统自动", count: 500, time: "2025-12-12 13:30" },
{
id: 4,
tag: "社交影响力",
action: "modified",
operator: "王管理员",
count: 12,
time: "2025-12-12 13:15",
reason: "调整为KOL级别",
},
]
export default function SmartTagPage() {
const [searchTerm, setSearchTerm] = useState("")
const [activeTab, setActiveTab] = useState("rules")
const [selectedTags, setSelectedTags] = useState<number[]>([])
const [settingsDialogOpen, setSettingsDialogOpen] = useState(false)
const [reviewDialogOpen, setReviewDialogOpen] = useState(false)
const [selectedRule, setSelectedRule] = useState<(typeof tagRules)[0] | null>(null)
const [selectedCandidate, setSelectedCandidate] = useState<(typeof candidateTags)[0] | null>(null)
const [confidenceFilter, setConfidenceFilter] = useState([0.4])
const [batchRunDialogOpen, setBatchRunDialogOpen] = useState(false)
const [createRuleDialogOpen, setCreateRuleDialogOpen] = useState(false)
const [newRule, setNewRule] = useState({
name: "",
dataSource: "",
prompt: "",
model: "gpt-4",
confidenceThreshold: 0.7,
autoApproveThreshold: 0.9,
})
const totalPending = tagRules.reduce((sum, r) => sum + r.pending, 0)
const totalApproved = tagRules.reduce((sum, r) => sum + r.approved, 0)
const totalRejected = tagRules.reduce((sum, r) => sum + r.rejected, 0)
const handleApprove = (id: number) => {
setSelectedTags((prev) => prev.filter((t) => t !== id))
}
const handleReject = (id: number) => {
setSelectedTags((prev) => prev.filter((t) => t !== id))
}
const handleBatchApprove = () => {
setSelectedTags([])
}
const filteredCandidates = candidateTags.filter((c) => c.confidence >= confidenceFilter[0])
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-violet-50/30 p-6">
<div className="max-w-7xl mx-auto space-y-6">
{/* 页面标题 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Link href="/ai-insight">
<Button variant="ghost" size="icon" className="rounded-full">
<ArrowLeft className="w-5 h-5" />
</Button>
</Link>
<div>
<h1 className="text-2xl font-bold text-slate-800">AI智能打标</h1>
<p className="text-slate-500 mt-1"></p>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={() => setBatchRunDialogOpen(true)}>
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
<Button
className="bg-gradient-to-r from-violet-500 to-purple-600 hover:from-violet-600 hover:to-purple-700"
onClick={() => setCreateRuleDialogOpen(true)}
>
<Plus className="w-4 h-4 mr-2" />
AI规则
</Button>
</div>
</div>
{/* 统计卡片 */}
<div className="grid grid-cols-1 md:grid-cols-6 gap-4">
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-slate-500"></p>
<p className="text-2xl font-bold text-slate-800">12,580</p>
</div>
<Users className="w-8 h-8 text-violet-500" />
</div>
<p className="text-xs text-emerald-600 mt-2"> +15.3%</p>
</CardContent>
</Card>
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-slate-500"></p>
<p className="text-2xl font-bold text-slate-800">45,200</p>
</div>
<Tag className="w-8 h-8 text-blue-500" />
</div>
<p className="text-xs text-emerald-600 mt-2"> +22.1%</p>
</CardContent>
</Card>
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-slate-500"></p>
<p className="text-2xl font-bold text-amber-600">{totalPending.toLocaleString()}</p>
</div>
<AlertTriangle className="w-8 h-8 text-amber-500" />
</div>
<p className="text-xs text-slate-500 mt-2"></p>
</CardContent>
</Card>
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-slate-500"></p>
<p className="text-2xl font-bold text-emerald-600">{totalApproved.toLocaleString()}</p>
</div>
<CheckCircle2 className="w-8 h-8 text-emerald-500" />
</div>
<p className="text-xs text-emerald-600 mt-2">
{((totalApproved / (totalApproved + totalRejected)) * 100).toFixed(1)}%
</p>
</CardContent>
</Card>
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-slate-500"></p>
<p className="text-2xl font-bold text-slate-800">0.82</p>
</div>
<Brain className="w-8 h-8 text-purple-500" />
</div>
<p className="text-xs text-emerald-600 mt-2"> +5.2%</p>
</CardContent>
</Card>
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-slate-500"></p>
<p className="text-2xl font-bold text-slate-800">2 / 3</p>
</div>
<RefreshCw className="w-8 h-8 text-emerald-500" />
</div>
<p className="text-xs text-slate-500 mt-2">1</p>
</CardContent>
</Card>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="bg-white/60 backdrop-blur">
<TabsTrigger value="rules" className="gap-2">
<Sparkles className="w-4 h-4" />
AI打标规则
</TabsTrigger>
<TabsTrigger value="review" className="gap-2">
<Shield className="w-4 h-4" />
{totalPending > 0 && <Badge className="ml-1 bg-amber-100 text-amber-700">{totalPending}</Badge>}
</TabsTrigger>
<TabsTrigger value="history" className="gap-2">
<History className="w-4 h-4" />
</TabsTrigger>
</TabsList>
{/* AI打标规则Tab - 重构展示提示词和数据源 */}
<TabsContent value="rules" className="mt-4">
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-base">AI打标规则配置</CardTitle>
<div className="flex items-center gap-2">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<Input
placeholder="搜索规则..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-9 w-64"
/>
</div>
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
{tagRules.map((rule) => (
<div key={rule.id} className="p-4 bg-slate-50/80 rounded-lg border border-slate-200/60">
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-violet-500 to-purple-600 flex items-center justify-center">
<Brain className="w-5 h-5 text-white" />
</div>
<div>
<div className="flex items-center gap-2">
<h4 className="font-medium text-slate-800">{rule.name}</h4>
<Badge variant="outline" className="text-xs">
{rule.model}
</Badge>
{rule.status === "running" ? (
<Badge className="bg-emerald-100 text-emerald-700 border-emerald-300"></Badge>
) : (
<Badge variant="secondary"></Badge>
)}
</div>
<p className="text-sm text-slate-500 mt-0.5">{rule.description}</p>
</div>
</div>
<div className="flex items-center gap-2">
{rule.status === "running" ? (
<Button variant="outline" size="sm">
<Pause className="w-4 h-4" />
</Button>
) : (
<Button variant="outline" size="sm">
<Play className="w-4 h-4" />
</Button>
)}
<Button
variant="ghost"
size="sm"
onClick={() => {
setSelectedRule(rule)
setSettingsDialogOpen(true)
}}
>
<Settings className="w-4 h-4" />
</Button>
<Button variant="ghost" size="sm" asChild>
<Link href={`/ai-insight/smart-tag/logs/${rule.id}`}>
<FileText className="w-4 h-4" />
</Link>
</Button>
</div>
</div>
<div className="grid grid-cols-2 gap-4 mb-3">
<div className="p-3 bg-white rounded-lg">
<div className="flex items-center gap-2 text-xs text-slate-500 mb-1">
<Database className="w-3 h-3" />
</div>
<p className="text-sm font-medium text-slate-700">{rule.dataSource}</p>
</div>
<div className="p-3 bg-white rounded-lg">
<div className="flex items-center gap-2 text-xs text-slate-500 mb-1">
<Zap className="w-3 h-3" />
</div>
<p className="text-sm font-medium text-slate-700">
: {">="}
{rule.confidenceThreshold} | : {">="}
{rule.autoApproveThreshold}
</p>
</div>
</div>
<div className="p-3 bg-white rounded-lg mb-3">
<div className="flex items-center gap-2 text-xs text-slate-500 mb-1">
<MessageSquare className="w-3 h-3" />
AI提示词
</div>
<p className="text-sm text-slate-600 line-clamp-2">{rule.prompt}</p>
</div>
{/* 统计数据 */}
<div className="grid grid-cols-5 gap-4 text-sm">
<div className="text-center">
<p className="text-lg font-bold text-slate-800">{rule.accuracy}%</p>
<p className="text-xs text-slate-500"></p>
</div>
<div className="text-center">
<p className="text-lg font-bold text-slate-800">{rule.tagged.toLocaleString()}</p>
<p className="text-xs text-slate-500"></p>
</div>
<div className="text-center">
<p className="text-lg font-bold text-amber-600">{rule.pending.toLocaleString()}</p>
<p className="text-xs text-slate-500"></p>
</div>
<div className="text-center">
<p className="text-lg font-bold text-emerald-600">{rule.approved.toLocaleString()}</p>
<p className="text-xs text-slate-500"></p>
</div>
<div className="text-center">
<p className="text-lg font-bold text-red-600">{rule.rejected.toLocaleString()}</p>
<p className="text-xs text-slate-500"></p>
</div>
</div>
<div className="mt-3 pt-3 border-t border-slate-200/60 flex items-center justify-between text-xs text-slate-500">
<span>: {rule.lastRun}</span>
<Button variant="link" size="sm" className="h-auto p-0 text-violet-600">
</Button>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</TabsContent>
{/* 审核中心Tab - 增强审核详情展示AI分析依据 */}
<TabsContent value="review" className="mt-4">
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-base"></CardTitle>
<p className="text-sm text-slate-500 mt-1">
AI生成的标签需要人工确认后才能入库
</p>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<span className="text-sm text-slate-500">:</span>
<div className="w-32">
<Slider
value={confidenceFilter}
onValueChange={setConfidenceFilter}
min={0}
max={1}
step={0.1}
/>
</div>
<span className="text-sm font-medium">
{">="}
{confidenceFilter[0]}
</span>
</div>
{selectedTags.length > 0 && (
<div className="flex items-center gap-2">
<span className="text-sm text-slate-500"> {selectedTags.length} </span>
<Button
size="sm"
variant="outline"
className="text-emerald-600 bg-transparent"
onClick={handleBatchApprove}
>
<Check className="w-4 h-4 mr-1" />
</Button>
<Button size="sm" variant="outline" className="text-red-600 bg-transparent">
<X className="w-4 h-4 mr-1" />
</Button>
</div>
)}
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-3">
{filteredCandidates.map((candidate) => (
<div key={candidate.id} className="p-4 bg-slate-50/80 rounded-lg border border-slate-200/60">
<div className="flex items-start justify-between">
<div className="flex items-start gap-3">
<input
type="checkbox"
className="mt-1 rounded"
checked={selectedTags.includes(candidate.id)}
onChange={(e) => {
if (e.target.checked) {
setSelectedTags((prev) => [...prev, candidate.id])
} else {
setSelectedTags((prev) => prev.filter((t) => t !== candidate.id))
}
}}
/>
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<span className="font-medium text-slate-800">{candidate.userName}</span>
<span className="text-xs text-slate-400">{candidate.userId}</span>
<Badge className="bg-violet-100 text-violet-700">{candidate.tag}</Badge>
<Badge
className={`${
candidate.confidence >= 0.8
? "bg-emerald-100 text-emerald-700"
: candidate.confidence >= 0.6
? "bg-amber-100 text-amber-700"
: "bg-red-100 text-red-700"
}`}
>
: {(candidate.confidence * 100).toFixed(0)}%
</Badge>
</div>
<p className="text-sm text-slate-600 mb-2">{candidate.reason}</p>
<div className="p-3 bg-white rounded-lg border border-slate-200">
<div className="flex items-center gap-2 text-xs text-slate-500 mb-1">
<Brain className="w-3 h-3" />
AI分析依据
</div>
<p className="text-sm text-slate-600">{candidate.aiAnalysis}</p>
</div>
<div className="flex items-center gap-4 mt-2 text-xs text-slate-500">
<span>: {candidate.source}</span>
<span>: {candidate.createdAt}</span>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Button
size="sm"
variant="outline"
className="text-emerald-600 bg-transparent"
onClick={() => handleApprove(candidate.id)}
>
<Check className="w-4 h-4" />
</Button>
<Button
size="sm"
variant="outline"
className="text-red-600 bg-transparent"
onClick={() => handleReject(candidate.id)}
>
<X className="w-4 h-4" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => {
setSelectedCandidate(candidate)
setReviewDialogOpen(true)
}}
>
<Eye className="w-4 h-4" />
</Button>
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</TabsContent>
{/* 操作历史Tab */}
<TabsContent value="history" className="mt-4">
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
<CardHeader className="pb-3">
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{tagHistory.map((item) => (
<div key={item.id} className="flex items-center justify-between p-3 bg-slate-50/80 rounded-lg">
<div className="flex items-center gap-3">
<Badge className="bg-violet-100 text-violet-700">{item.tag}</Badge>
<Badge
className={`${
item.action === "approved"
? "bg-emerald-100 text-emerald-700"
: item.action === "rejected"
? "bg-red-100 text-red-700"
: "bg-blue-100 text-blue-700"
}`}
>
{item.action === "approved" ? "通过" : item.action === "rejected" ? "拒绝" : "修改"}
</Badge>
<span className="text-sm text-slate-600">{item.count} </span>
{item.reason && <span className="text-sm text-slate-500">: {item.reason}</span>}
</div>
<div className="flex items-center gap-4 text-sm text-slate-500">
<span>{item.operator}</span>
<span>{item.time}</span>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
<Dialog open={createRuleDialogOpen} onOpenChange={setCreateRuleDialogOpen}>
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle>AI打标规则</DialogTitle>
<DialogDescription>AI模型</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label></Label>
<Input
placeholder="例如:高消费潜力识别"
value={newRule.name}
onChange={(e) => setNewRule({ ...newRule, name: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label>AI模型</Label>
<Select value={newRule.model} onValueChange={(v) => setNewRule({ ...newRule, model: v })}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="gpt-4">GPT-4</SelectItem>
<SelectItem value="gpt-4-turbo">GPT-4 Turbo</SelectItem>
<SelectItem value="claude-3">Claude 3</SelectItem>
<SelectItem value="qwen-max"> Max</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label> ()</Label>
<Select value={newRule.dataSource} onValueChange={(v) => setNewRule({ ...newRule, dataSource: v })}>
<SelectTrigger>
<SelectValue placeholder="选择要分析的数据源" />
</SelectTrigger>
<SelectContent>
{availableDataSources.map((ds) => (
<SelectItem key={ds.id} value={ds.name}>
<div className="flex items-center gap-2">
<Database className="w-4 h-4" />
<span>{ds.name}</span>
<Badge variant="secondary" className="text-xs">
{ds.type}
</Badge>
<span className="text-xs text-slate-500">{(ds.records / 1000000).toFixed(1)}M </span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>AI提示词</Label>
<Textarea
placeholder="描述你希望AI如何分析用户数据并生成标签。例如分析用户的消费记录和浏览行为识别具有高消费潜力的用户..."
value={newRule.prompt}
onChange={(e) => setNewRule({ ...newRule, prompt: e.target.value })}
rows={4}
/>
<p className="text-xs text-slate-500"></p>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label> ()</Label>
<div className="flex items-center gap-4">
<Slider
value={[newRule.confidenceThreshold]}
onValueChange={(v) => setNewRule({ ...newRule, confidenceThreshold: v[0] })}
min={0.1}
max={1}
step={0.1}
className="flex-1"
/>
<span className="w-12 text-sm font-medium">{newRule.confidenceThreshold}</span>
</div>
<p className="text-xs text-slate-500"></p>
</div>
<div className="space-y-2">
<Label></Label>
<div className="flex items-center gap-4">
<Slider
value={[newRule.autoApproveThreshold]}
onValueChange={(v) => setNewRule({ ...newRule, autoApproveThreshold: v[0] })}
min={0.1}
max={1}
step={0.1}
className="flex-1"
/>
<span className="w-12 text-sm font-medium">{newRule.autoApproveThreshold}</span>
</div>
<p className="text-xs text-slate-500"></p>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setCreateRuleDialogOpen(false)}>
</Button>
<Button onClick={() => setCreateRuleDialogOpen(false)}>
<Sparkles className="w-4 h-4 mr-2" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 规则设置弹窗 */}
<Dialog open={settingsDialogOpen} onOpenChange={setSettingsDialogOpen}>
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle> - {selectedRule?.name}</DialogTitle>
<DialogDescription>AI打标规则参数</DialogDescription>
</DialogHeader>
{selectedRule && (
<div className="space-y-4 py-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label></Label>
<Input defaultValue={selectedRule.name} />
</div>
<div className="space-y-2">
<Label>AI模型</Label>
<Select defaultValue={selectedRule.model.toLowerCase().replace(" ", "-")}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="gpt-4">GPT-4</SelectItem>
<SelectItem value="claude-3">Claude 3</SelectItem>
<SelectItem value="qwen-max"> Max</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label></Label>
<Select defaultValue={selectedRule.dataSource}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{availableDataSources.map((ds) => (
<SelectItem key={ds.id} value={ds.name}>
{ds.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>AI提示词</Label>
<Textarea defaultValue={selectedRule.prompt} rows={4} />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label></Label>
<div className="flex items-center gap-4">
<Slider
defaultValue={[selectedRule.confidenceThreshold]}
min={0.1}
max={1}
step={0.1}
className="flex-1"
/>
<span className="w-12 text-sm">{selectedRule.confidenceThreshold}</span>
</div>
</div>
<div className="space-y-2">
<Label></Label>
<div className="flex items-center gap-4">
<Slider
defaultValue={[selectedRule.autoApproveThreshold]}
min={0.1}
max={1}
step={0.1}
className="flex-1"
/>
<span className="w-12 text-sm">{selectedRule.autoApproveThreshold}</span>
</div>
</div>
</div>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => setSettingsDialogOpen(false)}>
</Button>
<Button onClick={() => setSettingsDialogOpen(false)}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 审核详情弹窗 */}
<Dialog open={reviewDialogOpen} onOpenChange={setReviewDialogOpen}>
<DialogContent className="sm:max-w-xl">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
{selectedCandidate && (
<div className="space-y-4 py-4">
<div className="flex items-center gap-4 p-4 bg-slate-50 rounded-lg">
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-violet-500 to-purple-600 flex items-center justify-center text-white font-bold">
{selectedCandidate.userName[0]}
</div>
<div>
<h4 className="font-medium">{selectedCandidate.userName}</h4>
<p className="text-sm text-slate-500">{selectedCandidate.userId}</p>
</div>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-slate-500"></span>
<Badge className="bg-violet-100 text-violet-700">{selectedCandidate.tag}</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-slate-500"></span>
<span
className={`font-medium ${selectedCandidate.confidence >= 0.7 ? "text-emerald-600" : "text-amber-600"}`}
>
{(selectedCandidate.confidence * 100).toFixed(1)}%
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-slate-500"></span>
<span>{selectedCandidate.source}</span>
</div>
</div>
<div className="p-4 bg-slate-50 rounded-lg">
<h5 className="font-medium mb-2 flex items-center gap-2">
<Brain className="w-4 h-4 text-violet-500" />
AI分析依据
</h5>
<p className="text-sm text-slate-600">{selectedCandidate.aiAnalysis}</p>
</div>
</div>
)}
<DialogFooter>
<Button
variant="outline"
className="text-red-600 bg-transparent"
onClick={() => setReviewDialogOpen(false)}
>
<X className="w-4 h-4 mr-2" />
</Button>
<Button className="bg-emerald-600 hover:bg-emerald-700" onClick={() => setReviewDialogOpen(false)}>
<Check className="w-4 h-4 mr-2" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 批量重算弹窗 */}
<Dialog open={batchRunDialogOpen} onOpenChange={setBatchRunDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>AI打标规则</DialogDescription>
</DialogHeader>
<div className="space-y-3 py-4">
{tagRules.map((rule) => (
<label
key={rule.id}
className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg cursor-pointer hover:bg-slate-100"
>
<input type="checkbox" className="rounded" defaultChecked={rule.status === "running"} />
<div>
<p className="font-medium">{rule.name}</p>
<p className="text-sm text-slate-500">: {rule.dataSource}</p>
</div>
</label>
))}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setBatchRunDialogOpen(false)}>
</Button>
<Button onClick={() => setBatchRunDialogOpen(false)}>
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}