Files
shensheshou/app/ai-agent/data-cleaning/page.tsx
v0 1219166526 refactor: overhaul system navigation based on 5 HTML docs
Reorganize modules, add AI Agent smart interaction, include prompt settings, enhance data output with subscriptions, and separate system monitoring.

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

541 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useState } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Switch } from "@/components/ui/switch"
import { Progress } from "@/components/ui/progress"
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import {
Sparkles, Play, Pause, Settings, FileText, Database,
CheckCircle2, XCircle, Clock, AlertTriangle, Eye, Trash2,
RefreshCw, Download, ChevronRight, Zap, Bot
} from "lucide-react"
// AI清洗规则数据
const aiCleaningRules = [
{
id: "1",
name: "手机号格式标准化",
prompt: "识别并标准化所有手机号格式去除空格、括号等特殊字符统一为11位纯数字格式",
dataSource: "用户主表",
targetField: "phone",
status: "running",
confidence: 0.95,
processedCount: 12580,
totalCount: 15000,
errorCount: 23,
lastRun: "2024-01-15 10:30:00",
needsReview: false,
},
{
id: "2",
name: "地址信息补全",
prompt: "根据已有地址信息,智能补全省市区三级地址,并标准化地址格式",
dataSource: "用户主表",
targetField: "address",
status: "pending_review",
confidence: 0.78,
processedCount: 8900,
totalCount: 8900,
errorCount: 156,
lastRun: "2024-01-15 09:15:00",
needsReview: true,
},
{
id: "3",
name: "姓名脱敏处理",
prompt: "对姓名字段进行脱敏处理,保留姓氏,名字用*替代",
dataSource: "交易记录表",
targetField: "customer_name",
status: "completed",
confidence: 0.99,
processedCount: 50000,
totalCount: 50000,
errorCount: 5,
lastRun: "2024-01-14 18:00:00",
needsReview: false,
},
]
// 待审核数据
const pendingReviewData = [
{
id: "r1",
ruleId: "2",
ruleName: "地址信息补全",
originalValue: "北京朝阳区",
aiSuggestedValue: "北京市朝阳区建国路88号",
confidence: 0.72,
reason: "根据用户历史订单地址推断",
},
{
id: "r2",
ruleId: "2",
ruleName: "地址信息补全",
originalValue: "上海浦东",
aiSuggestedValue: "上海市浦东新区陆家嘴环路1000号",
confidence: 0.65,
reason: "根据IP定位和常用地址推断",
},
]
export default function AIDataCleaningPage() {
const [activeTab, setActiveTab] = useState("rules")
const [showCreateDialog, setShowCreateDialog] = useState(false)
const [showReviewDialog, setShowReviewDialog] = useState(false)
const [selectedReview, setSelectedReview] = useState<typeof pendingReviewData[0] | null>(null)
const [newRule, setNewRule] = useState({
name: "",
prompt: "",
dataSource: "",
targetField: "",
autoApprove: false,
confidenceThreshold: 0.9,
})
const getStatusBadge = (status: string) => {
switch (status) {
case "running":
return <Badge className="bg-blue-100 text-blue-700"></Badge>
case "completed":
return <Badge className="bg-green-100 text-green-700"></Badge>
case "pending_review":
return <Badge className="bg-yellow-100 text-yellow-700"></Badge>
case "failed":
return <Badge className="bg-red-100 text-red-700"></Badge>
default:
return <Badge variant="secondary"></Badge>
}
}
const handleApprove = (id: string) => {
console.log("Approved:", id)
setShowReviewDialog(false)
}
const handleReject = (id: string) => {
console.log("Rejected:", id)
setShowReviewDialog(false)
}
return (
<div className="space-y-6">
{/* 页面标题 */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-foreground">AI数据清洗</h1>
<p className="text-muted-foreground">AI提示词自动识别和清洗数据</p>
</div>
<Button onClick={() => setShowCreateDialog(true)}>
<Sparkles className="mr-2 h-4 w-4" />
AI清洗规则
</Button>
</div>
{/* 统计卡片 */}
<div className="grid gap-4 md:grid-cols-4">
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="text-2xl font-bold">12</p>
</div>
<Bot className="h-8 w-8 text-blue-500" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="text-2xl font-bold">71,480</p>
</div>
<Zap className="h-8 w-8 text-green-500" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="text-2xl font-bold text-yellow-600">156</p>
</div>
<AlertTriangle className="h-8 w-8 text-yellow-500" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="text-2xl font-bold">92.3%</p>
</div>
<CheckCircle2 className="h-8 w-8 text-purple-500" />
</div>
</CardContent>
</Card>
</div>
{/* 主内容区 */}
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList>
<TabsTrigger value="rules"></TabsTrigger>
<TabsTrigger value="review">
<Badge className="ml-2 bg-yellow-100 text-yellow-700">156</Badge>
</TabsTrigger>
<TabsTrigger value="history"></TabsTrigger>
</TabsList>
<TabsContent value="rules" className="space-y-4">
{aiCleaningRules.map((rule) => (
<Card key={rule.id}>
<CardContent className="pt-6">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h3 className="font-semibold text-lg">{rule.name}</h3>
{getStatusBadge(rule.status)}
{rule.needsReview && (
<Badge variant="outline" className="text-yellow-600 border-yellow-300">
</Badge>
)}
</div>
<div className="bg-muted/50 rounded-lg p-3 mb-3">
<p className="text-sm text-muted-foreground">
<span className="font-medium text-foreground">AI提示词</span>
{rule.prompt}
</p>
</div>
<div className="flex items-center gap-6 text-sm text-muted-foreground">
<span className="flex items-center gap-1">
<Database className="h-4 w-4" />
{rule.dataSource} / {rule.targetField}
</span>
<span>: {(rule.confidence * 100).toFixed(0)}%</span>
<span>: {rule.errorCount}</span>
<span>: {rule.lastRun}</span>
</div>
{rule.status === "running" && (
<div className="mt-3">
<div className="flex items-center justify-between text-sm mb-1">
<span></span>
<span>{rule.processedCount.toLocaleString()} / {rule.totalCount.toLocaleString()}</span>
</div>
<Progress value={(rule.processedCount / rule.totalCount) * 100} className="h-2" />
</div>
)}
</div>
<div className="flex items-center gap-2 ml-4">
{rule.status === "running" ? (
<Button variant="outline" size="sm">
<Pause className="h-4 w-4 mr-1" />
</Button>
) : (
<Button variant="outline" size="sm">
<Play className="h-4 w-4 mr-1" />
</Button>
)}
<Button variant="outline" size="sm">
<Eye className="h-4 w-4 mr-1" />
</Button>
<Button variant="outline" size="sm">
<Settings className="h-4 w-4" />
</Button>
</div>
</div>
</CardContent>
</Card>
))}
</TabsContent>
<TabsContent value="review" className="space-y-4">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription>AI清洗结果中置信度较低或需要人工确认的数据</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead>AI建议值</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{pendingReviewData.map((item) => (
<TableRow key={item.id}>
<TableCell className="font-medium">{item.ruleName}</TableCell>
<TableCell className="text-muted-foreground">{item.originalValue}</TableCell>
<TableCell className="text-blue-600">{item.aiSuggestedValue}</TableCell>
<TableCell>
<Badge
className={item.confidence >= 0.7 ? "bg-yellow-100 text-yellow-700" : "bg-red-100 text-red-700"}
>
{(item.confidence * 100).toFixed(0)}%
</Badge>
</TableCell>
<TableCell className="max-w-[200px] truncate text-muted-foreground">
{item.reason}
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-2">
<Button
size="sm"
variant="outline"
className="text-green-600 hover:text-green-700 bg-transparent"
onClick={() => handleApprove(item.id)}
>
<CheckCircle2 className="h-4 w-4 mr-1" />
</Button>
<Button
size="sm"
variant="outline"
className="text-red-600 hover:text-red-700 bg-transparent"
onClick={() => handleReject(item.id)}
>
<XCircle className="h-4 w-4 mr-1" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => {
setSelectedReview(item)
setShowReviewDialog(true)
}}
>
<Eye className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="history" className="space-y-4">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription>AI清洗规则的历史执行记录</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-medium"></TableCell>
<TableCell>2024-01-15 10:30:00</TableCell>
<TableCell>15,000</TableCell>
<TableCell>99.8%</TableCell>
<TableCell>235</TableCell>
<TableCell><Badge className="bg-green-100 text-green-700"></Badge></TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm">
<FileText className="h-4 w-4 mr-1" />
</Button>
</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium"></TableCell>
<TableCell>2024-01-15 09:15:00</TableCell>
<TableCell>8,900</TableCell>
<TableCell>98.2%</TableCell>
<TableCell>512</TableCell>
<TableCell><Badge className="bg-yellow-100 text-yellow-700"></Badge></TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm">
<FileText className="h-4 w-4 mr-1" />
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
</Tabs>
{/* 创建AI清洗规则弹窗 */}
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>AI清洗规则</DialogTitle>
<DialogDescription>AI将自动识别和处理数据</DialogDescription>
</DialogHeader>
<div className="space-y-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></Label>
<Select
value={newRule.dataSource}
onValueChange={(v) => setNewRule({...newRule, dataSource: v})}
>
<SelectTrigger>
<SelectValue placeholder="选择数据源" />
</SelectTrigger>
<SelectContent>
<SelectItem value="user_main"></SelectItem>
<SelectItem value="transaction"></SelectItem>
<SelectItem value="behavior"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label></Label>
<Input
placeholder="输入要清洗的字段名"
value={newRule.targetField}
onChange={(e) => setNewRule({...newRule, targetField: e.target.value})}
/>
</div>
<div className="space-y-2">
<Label>AI清洗提示词</Label>
<Textarea
placeholder="用自然语言描述清洗需求例如识别并标准化所有手机号格式去除空格和特殊字符统一为11位纯数字格式"
className="min-h-[120px]"
value={newRule.prompt}
onChange={(e) => setNewRule({...newRule, prompt: e.target.value})}
/>
<p className="text-xs text-muted-foreground">
AI清洗效果越好
</p>
</div>
<div className="flex items-center justify-between p-4 bg-muted/50 rounded-lg">
<div>
<Label></Label>
<p className="text-sm text-muted-foreground"></p>
</div>
<div className="flex items-center gap-4">
<Switch
checked={newRule.autoApprove}
onCheckedChange={(v) => setNewRule({...newRule, autoApprove: v})}
/>
{newRule.autoApprove && (
<div className="flex items-center gap-2">
<Label className="text-sm">:</Label>
<Select
value={newRule.confidenceThreshold.toString()}
onValueChange={(v) => setNewRule({...newRule, confidenceThreshold: parseFloat(v)})}
>
<SelectTrigger className="w-24">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="0.95">95%</SelectItem>
<SelectItem value="0.9">90%</SelectItem>
<SelectItem value="0.85">85%</SelectItem>
<SelectItem value="0.8">80%</SelectItem>
</SelectContent>
</Select>
</div>
)}
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowCreateDialog(false)}></Button>
<Button>
<Sparkles className="mr-2 h-4 w-4" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 审核详情弹窗 */}
<Dialog open={showReviewDialog} onOpenChange={setShowReviewDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>AI清洗建议的详细信息</DialogDescription>
</DialogHeader>
{selectedReview && (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-muted-foreground"></Label>
<p className="font-medium">{selectedReview.ruleName}</p>
</div>
<div>
<Label className="text-muted-foreground"></Label>
<p className="font-medium">{(selectedReview.confidence * 100).toFixed(0)}%</p>
</div>
</div>
<div>
<Label className="text-muted-foreground"></Label>
<p className="p-2 bg-red-50 rounded border border-red-200 text-red-700">{selectedReview.originalValue}</p>
</div>
<div>
<Label className="text-muted-foreground">AI建议值</Label>
<p className="p-2 bg-green-50 rounded border border-green-200 text-green-700">{selectedReview.aiSuggestedValue}</p>
</div>
<div>
<Label className="text-muted-foreground"></Label>
<p className="p-2 bg-muted rounded">{selectedReview.reason}</p>
</div>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => handleReject(selectedReview?.id || "")}>
<XCircle className="mr-2 h-4 w-4" />
</Button>
<Button onClick={() => handleApprove(selectedReview?.id || "")}>
<CheckCircle2 className="mr-2 h-4 w-4" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}