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:
3
app/ai-insight/data-cleaning/loading.tsx
Normal file
3
app/ai-insight/data-cleaning/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
881
app/ai-insight/data-cleaning/page.tsx
Normal file
881
app/ai-insight/data-cleaning/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
21
app/ai-insight/loading.tsx
Normal file
21
app/ai-insight/loading.tsx
Normal 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
251
app/ai-insight/nlq/page.tsx
Normal 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
292
app/ai-insight/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
331
app/ai-insight/prediction/page.tsx
Normal file
331
app/ai-insight/prediction/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
3
app/ai-insight/report/loading.tsx
Normal file
3
app/ai-insight/report/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
228
app/ai-insight/report/page.tsx
Normal file
228
app/ai-insight/report/page.tsx
Normal 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,200,DAU/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>
|
||||
)
|
||||
}
|
||||
3
app/ai-insight/smart-tag/loading.tsx
Normal file
3
app/ai-insight/smart-tag/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
3
app/ai-insight/smart-tag/logs/[id]/loading.tsx
Normal file
3
app/ai-insight/smart-tag/logs/[id]/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
127
app/ai-insight/smart-tag/logs/[id]/page.tsx
Normal file
127
app/ai-insight/smart-tag/logs/[id]/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
980
app/ai-insight/smart-tag/page.tsx
Normal file
980
app/ai-insight/smart-tag/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user