Files
users/app/ai-agent/report/page.tsx

710 lines
26 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, 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 {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Label } from "@/components/ui/label"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import {
FileText,
Plus,
Download,
RefreshCw,
Eye,
Calendar,
Users,
TrendingUp,
PieChart,
BarChart3,
Clock,
CheckCircle2,
Loader2,
Send,
Mail,
MessageSquare,
Play,
Sparkles,
} from "lucide-react"
// 报告规则接口
interface ReportRule {
id: string
name: string
templateId: string
schedule: string
enabled: boolean
isDefault: boolean
config: {
dateRange: string
includeCharts: boolean
sendTo: string[]
format: 'pdf' | 'html' | 'markdown'
}
promptTemplate: string
}
// 报告模板
interface ReportTemplate {
id: string
name: string
description: string
type: 'daily' | 'weekly' | 'monthly' | 'custom'
sections: string[]
icon: string
}
// 默认报告规则
const DEFAULT_RULES: ReportRule[] = [
{
id: 'rule_daily',
name: '每日运营日报规则',
templateId: 'tpl_daily',
schedule: '每日 23:59',
enabled: true,
isDefault: true,
config: {
dateRange: 'today',
includeCharts: true,
sendTo: ['admin@company.com'],
format: 'pdf',
},
promptTemplate: `生成每日运营日报,包含以下内容:
1. 核心指标
- 总用户数及日增量
- 活跃用户数及活跃率
- 新增用户数
2. 用户活跃
- 各等级用户活跃情况
- 活跃时段分布
3. 渠道消息
- 各渠道消息量统计
- 响应时间分析
4. AI查询统计
- 查询次数
- 平均响应时间
- 热门查询类型
输出格式:结构化报告,包含数据表格和趋势描述`,
},
{
id: 'rule_weekly',
name: '周度数据质量规则',
templateId: 'tpl_weekly',
schedule: '每周日 00:00',
enabled: true,
isDefault: true,
config: {
dateRange: 'week',
includeCharts: true,
sendTo: ['data@company.com'],
format: 'pdf',
},
promptTemplate: `生成周度数据质量报告:
1. 质量概览
- 数据完整度评分
- 数据准确度评分
- 本周质量趋势
2. 清洗统计
- 清洗记录数
- 格式化字段数
- 异常数据处理
3. 去重效果
- 去重前后对比
- 重复率统计
4. 数据完整度
- 各字段填充率
- 缺失字段分析`,
},
{
id: 'rule_monthly',
name: '月度资产报告规则',
templateId: 'tpl_monthly',
schedule: '每月1日 08:00',
enabled: true,
isDefault: true,
config: {
dateRange: 'month',
includeCharts: true,
sendTo: ['admin@company.com', 'ceo@company.com'],
format: 'pdf',
},
promptTemplate: `生成月度用户资产报告:
1. 资产总览
- 用户总资产价值
- 月度变化趋势
- 核心指标汇总
2. 用户增长
- 新增用户分析
- 流失用户分析
- 净增长情况
3. RFM分析
- RFM评分分布
- 各等级用户占比
- 价值迁移分析
4. 流量池分布
- 各流量池规模
- 月度变化
- 转化分析
5. 标签覆盖
- 标签覆盖率
- 热门标签TOP10`,
},
]
// 预定义报告模板
const REPORT_TEMPLATES: ReportTemplate[] = [
{
id: 'tpl_daily',
name: '每日运营日报',
description: '包含当日用户活跃、渠道消息、AI查询等实时数据汇总',
type: 'daily',
sections: ['核心指标', '用户活跃', '渠道消息', 'AI查询统计'],
icon: '📊',
},
{
id: 'tpl_weekly',
name: '周度数据质量报告',
description: '数据清洗、去重、补全等质量指标周度汇总',
type: 'weekly',
sections: ['质量概览', '清洗统计', '去重效果', '数据完整度'],
icon: '📈',
},
{
id: 'tpl_monthly',
name: '月度用户资产报告',
description: '用户增长、RFM分布、流量池变化等核心资产指标',
type: 'monthly',
sections: ['资产总览', '用户增长', 'RFM分析', '流量池分布', '标签覆盖'],
icon: '📑',
},
{
id: 'tpl_rfm',
name: 'RFM分析报告',
description: '详细的RFM评分分布和用户价值分层分析',
type: 'custom',
sections: ['RFM概览', '评分分布', '等级分层', '价值趋势'],
icon: '💎',
},
{
id: 'tpl_portrait',
name: '用户画像报告',
description: '用户画像特征分析,包含地域、年龄、偏好等维度',
type: 'custom',
sections: ['画像概览', '地域分布', '行为特征', '偏好分析'],
icon: '👤',
},
{
id: 'tpl_traffic',
name: '流量池分析报告',
description: '各流量池用户规模、转化率、价值贡献分析',
type: 'custom',
sections: ['池规模', '转化漏斗', '价值贡献', '增长趋势'],
icon: '🏊',
},
]
// 生成的报告
interface GeneratedReport {
id: string
templateId: string
title: string
generatedAt: string
status: 'ready' | 'generating'
content?: string
metrics: {
totalUsers: number
activeUsers: number
newUsers: number
rfmAvg: number
}
}
const GENERATED_REPORTS: GeneratedReport[] = [
{
id: 'rpt_1',
templateId: 'tpl_monthly',
title: '2024年1月用户资产月报',
generatedAt: '2024-02-01 08:00',
status: 'ready',
metrics: { totalUsers: 2013000000, activeUsers: 450000000, newUsers: 15000000, rfmAvg: 52.3 },
},
{
id: 'rpt_2',
templateId: 'tpl_weekly',
title: '本周数据质量报告',
generatedAt: '2024-01-28 00:00',
status: 'ready',
metrics: { totalUsers: 2013000000, activeUsers: 320000000, newUsers: 2500000, rfmAvg: 51.8 },
},
{
id: 'rpt_3',
templateId: 'tpl_daily',
title: '今日运营日报',
generatedAt: '2024-01-30 23:59',
status: 'ready',
metrics: { totalUsers: 2013000000, activeUsers: 45000000, newUsers: 350000, rfmAvg: 52.5 },
},
]
export default function ReportPage() {
const [reports, setReports] = useState<GeneratedReport[]>(GENERATED_REPORTS)
const [rules, setRules] = useState<ReportRule[]>(DEFAULT_RULES)
const [activeTab, setActiveTab] = useState("templates")
const [generating, setGenerating] = useState<string | null>(null)
const [showGenerateDialog, setShowGenerateDialog] = useState(false)
const [selectedTemplate, setSelectedTemplate] = useState<ReportTemplate | null>(null)
const [showPreviewDialog, setShowPreviewDialog] = useState(false)
const [previewReport, setPreviewReport] = useState<GeneratedReport | null>(null)
const [showRuleDialog, setShowRuleDialog] = useState(false)
const [selectedRule, setSelectedRule] = useState<ReportRule | null>(null)
const [editingPrompt, setEditingPrompt] = useState('')
const formatNumber = (num: number): string => {
if (num >= 1000000000) return `${(num / 1000000000).toFixed(2)}B`
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`
if (num >= 1000) return `${(num / 1000).toFixed(0)}K`
return num.toLocaleString()
}
// 通过Skill生成报告
const generateReport = async (template: ReportTemplate) => {
setGenerating(template.id)
setShowGenerateDialog(false)
try {
// 调用AI Chat API执行报告生成
const response = await fetch('/api/ai-chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: `生成${template.name},包含以下章节:${template.sections.join('、')}`
})
})
const data = await response.json()
// 添加新报告到列表
const newReport: GeneratedReport = {
id: `rpt_${Date.now()}`,
templateId: template.id,
title: `${template.name} - ${new Date().toLocaleDateString()}`,
generatedAt: new Date().toLocaleString(),
status: 'ready',
content: data.success ? data.response?.content : '报告生成失败',
metrics: {
totalUsers: 2013000000,
activeUsers: 450000000,
newUsers: 15000000,
rfmAvg: 52.3,
},
}
setReports([newReport, ...reports])
} catch (error) {
console.error('报告生成失败:', error)
} finally {
setGenerating(null)
}
}
const openPreview = (report: GeneratedReport) => {
setPreviewReport(report)
setShowPreviewDialog(true)
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-purple-50/20">
<div className="p-6 space-y-6">
{/* 顶部标题 */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900"></h1>
<p className="text-sm text-gray-500 mt-1">AI自动生成数据分析报告</p>
</div>
<Button onClick={() => setShowGenerateDialog(true)}>
<Plus className="h-4 w-4 mr-2" />
</Button>
</div>
{/* 标签页 */}
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="bg-white/80 backdrop-blur">
<TabsTrigger value="templates"></TabsTrigger>
<TabsTrigger value="rules"></TabsTrigger>
<TabsTrigger value="history"></TabsTrigger>
</TabsList>
{/* 报告模板 */}
<TabsContent value="templates" className="mt-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{REPORT_TEMPLATES.map(template => (
<Card
key={template.id}
className="border-0 shadow-sm bg-white/80 hover:shadow-lg transition-all cursor-pointer group"
>
<CardContent className="p-5">
<div className="flex items-start justify-between mb-3">
<span className="text-3xl">{template.icon}</span>
<Badge variant="outline">{
template.type === 'daily' ? '日报' :
template.type === 'weekly' ? '周报' :
template.type === 'monthly' ? '月报' : '自定义'
}</Badge>
</div>
<h3 className="font-bold text-gray-900 text-lg mb-1">{template.name}</h3>
<p className="text-sm text-gray-500 mb-3">{template.description}</p>
<div className="flex flex-wrap gap-1 mb-4">
{template.sections.map((section, i) => (
<Badge key={i} variant="secondary" className="text-xs">{section}</Badge>
))}
</div>
<Button
className="w-full"
onClick={() => generateReport(template)}
disabled={generating === template.id}
>
{generating === template.id ? (
<><Loader2 className="h-4 w-4 animate-spin mr-2" />...</>
) : (
<><Play className="h-4 w-4 mr-2" /></>
)}
</Button>
</CardContent>
</Card>
))}
</div>
</TabsContent>
{/* 规则配置 */}
<TabsContent value="rules" className="mt-4 space-y-3">
{rules.map(rule => {
const template = REPORT_TEMPLATES.find(t => t.id === rule.templateId)
return (
<Card key={rule.id} className="border-0 shadow-sm bg-white/80 backdrop-blur">
<CardContent className="p-4">
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<div className="p-2.5 rounded-xl bg-gradient-to-r from-purple-500 to-blue-500 text-2xl">
{template?.icon || '📄'}
</div>
<div>
<div className="flex items-center gap-2">
<h3 className="font-semibold text-gray-900">{rule.name}</h3>
{rule.isDefault && <Badge variant="outline" className="text-xs"></Badge>}
{rule.enabled ? (
<Badge className="bg-green-100 text-green-700"></Badge>
) : (
<Badge className="bg-gray-100 text-gray-700"></Badge>
)}
</div>
<p className="text-sm text-gray-500">{template?.name}</p>
<div className="flex items-center gap-4 text-xs text-gray-400 mt-1">
<span className="flex items-center gap-1">
<Clock className="h-3 w-3" />
{rule.schedule}
</span>
<span>: {rule.config.format.toUpperCase()}</span>
<span>: {rule.config.sendTo.join(', ')}</span>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => {
setSelectedRule(rule)
setEditingPrompt(rule.promptTemplate)
setShowRuleDialog(true)
}}
>
</Button>
<Button
variant={rule.enabled ? "outline" : "default"}
size="sm"
onClick={() => setRules(rules.map(r => r.id === rule.id ? { ...r, enabled: !r.enabled } : r))}
>
{rule.enabled ? '禁用' : '启用'}
</Button>
</div>
</div>
</CardContent>
</Card>
)
})}
</TabsContent>
{/* 历史报告 */}
<TabsContent value="history" className="mt-4">
<Card className="border-0 shadow-sm bg-white/80">
<CardContent className="p-4">
<div className="space-y-3">
{reports.map(report => {
const template = REPORT_TEMPLATES.find(t => t.id === report.templateId)
return (
<div key={report.id} className="flex items-center justify-between p-4 rounded-xl bg-gray-50 hover:bg-gray-100 transition-colors">
<div className="flex items-center gap-4">
<div className="p-2.5 rounded-xl bg-white text-2xl">
{template?.icon || '📄'}
</div>
<div>
<h3 className="font-semibold text-gray-900">{report.title}</h3>
<div className="flex items-center gap-4 mt-1 text-xs text-gray-500">
<span className="flex items-center gap-1">
<Clock className="h-3 w-3" />
{report.generatedAt}
</span>
<span className="flex items-center gap-1">
<Users className="h-3 w-3" />
{formatNumber(report.metrics.totalUsers)}
</span>
<span>RFM均值: {report.metrics.rfmAvg}</span>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={() => openPreview(report)}>
<Eye className="h-4 w-4" />
</Button>
<Button variant="outline" size="sm">
<Download className="h-4 w-4" />
</Button>
<Button variant="outline" size="sm">
<Send className="h-4 w-4" />
</Button>
</div>
</div>
)
})}
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
{/* 生成报告弹窗 */}
<Dialog open={showGenerateDialog} onOpenChange={setShowGenerateDialog}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>AI将自动生成分析报告</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label></Label>
<Select onValueChange={(v) => setSelectedTemplate(REPORT_TEMPLATES.find(t => t.id === v) || null)}>
<SelectTrigger>
<SelectValue placeholder="选择报告模板" />
</SelectTrigger>
<SelectContent>
{REPORT_TEMPLATES.map(template => (
<SelectItem key={template.id} value={template.id}>
{template.icon} {template.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{selectedTemplate && (
<div className="p-3 rounded-lg bg-purple-50">
<p className="text-sm text-gray-600 mb-2">{selectedTemplate.description}</p>
<div className="flex flex-wrap gap-1">
{selectedTemplate.sections.map((section, i) => (
<Badge key={i} variant="secondary" className="text-xs">{section}</Badge>
))}
</div>
</div>
)}
<div className="space-y-2">
<Label></Label>
<Select defaultValue="all">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="today"></SelectItem>
<SelectItem value="week"></SelectItem>
<SelectItem value="month"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowGenerateDialog(false)}></Button>
<Button
onClick={() => selectedTemplate && generateReport(selectedTemplate)}
disabled={!selectedTemplate}
>
<Sparkles className="h-4 w-4 mr-2" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 报告预览弹窗 */}
<Dialog open={showPreviewDialog} onOpenChange={setShowPreviewDialog}>
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{previewReport?.title}</DialogTitle>
<DialogDescription>: {previewReport?.generatedAt}</DialogDescription>
</DialogHeader>
{previewReport && (
<div className="space-y-4 py-4">
{/* 核心指标 */}
<div className="grid grid-cols-4 gap-4">
<Card className="border bg-blue-50">
<CardContent className="p-3 text-center">
<div className="text-2xl font-bold text-blue-600">{formatNumber(previewReport.metrics.totalUsers)}</div>
<div className="text-xs text-gray-500"></div>
</CardContent>
</Card>
<Card className="border bg-green-50">
<CardContent className="p-3 text-center">
<div className="text-2xl font-bold text-green-600">{formatNumber(previewReport.metrics.activeUsers)}</div>
<div className="text-xs text-gray-500"></div>
</CardContent>
</Card>
<Card className="border bg-orange-50">
<CardContent className="p-3 text-center">
<div className="text-2xl font-bold text-orange-600">{formatNumber(previewReport.metrics.newUsers)}</div>
<div className="text-xs text-gray-500"></div>
</CardContent>
</Card>
<Card className="border bg-purple-50">
<CardContent className="p-3 text-center">
<div className="text-2xl font-bold text-purple-600">{previewReport.metrics.rfmAvg}</div>
<div className="text-xs text-gray-500">RFM均值</div>
</CardContent>
</Card>
</div>
{/* 报告内容 */}
<Card className="border">
<CardContent className="p-4">
<h4 className="font-medium text-gray-900 mb-3"></h4>
<div className="prose prose-sm text-gray-600">
{previewReport.content || (
<div className="space-y-2">
<p></p>
<p><strong>:</strong> {formatNumber(previewReport.metrics.totalUsers)}{((previewReport.metrics.activeUsers / previewReport.metrics.totalUsers) * 100).toFixed(1)}%</p>
<p><strong>:</strong> RFM评分{previewReport.metrics.rfmAvg}0.5</p>
<p><strong>:</strong> {formatNumber(previewReport.metrics.newUsers)}8.2%</p>
</div>
)}
</div>
</CardContent>
</Card>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => setShowPreviewDialog(false)}></Button>
<Button variant="outline">
<Download className="h-4 w-4 mr-2" />
PDF
</Button>
<Button>
<Send className="h-4 w-4 mr-2" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 规则编辑弹窗 */}
<Dialog open={showRuleDialog} onOpenChange={setShowRuleDialog}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>{selectedRule?.name}</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 value={selectedRule.schedule} readOnly className="bg-gray-50" />
</div>
<div className="space-y-2">
<Label></Label>
<Select defaultValue={selectedRule.config.format}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="pdf">PDF</SelectItem>
<SelectItem value="html">HTML</SelectItem>
<SelectItem value="markdown">Markdown</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label></Label>
<Input defaultValue={selectedRule.config.sendTo.join(', ')} placeholder="多个邮箱用逗号分隔" />
</div>
<div className="space-y-2">
<Label> <span className="text-gray-400 text-xs">(AI根据此提示词生成报告内容)</span></Label>
<Textarea
value={editingPrompt}
onChange={(e) => setEditingPrompt(e.target.value)}
rows={12}
className="font-mono text-sm"
/>
</div>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => setShowRuleDialog(false)}></Button>
<Button onClick={() => {
if (selectedRule) {
setRules(rules.map(r => r.id === selectedRule.id ? { ...r, promptTemplate: editingPrompt } : r))
}
setShowRuleDialog(false)
}}>
<CheckCircle2 className="h-4 w-4 mr-2" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</div>
)
}