import { useState, useRef, useEffect, useCallback } from 'react' import { getAnnualReport, type AnnualReportData } from '@/lib/annual-report-api' import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card' import { Skeleton } from '@/components/ui/skeleton' import { Badge } from '@/components/ui/badge' import { ScrollArea } from '@/components/ui/scroll-area' import { Button } from '@/components/ui/button' import { useToast } from '@/hooks/use-toast' import { toPng } from 'html-to-image' import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, } from 'recharts' import { Clock, Users, Brain, Smile, Trophy, Calendar, MessageSquare, Zap, Moon, Sun, AtSign, Heart, Image as ImageIcon, Bot, Download, Loader2, } from 'lucide-react' import { cn } from '@/lib/utils' // 颜色常量 const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8', '#82ca9d'] // 动态比喻生成函数 function getOnlineHoursMetaphor(hours: number): string { if (hours >= 8760) return "相当于全年无休,7x24小时在线!" if (hours >= 5000) return "相当于一位全职员工的年工作时长" if (hours >= 2000) return "相当于看完了 1000 部电影" if (hours >= 1000) return "相当于环球飞行 80 次" if (hours >= 500) return "相当于读完了 100 本书" if (hours >= 100) return "相当于马拉松跑了 25 次" return "虽然不多,但每一刻都很珍贵" } function getMidnightMetaphor(count: number): string { if (count >= 1000) return "夜深人静时的知心好友" if (count >= 500) return "午夜场的常客" if (count >= 100) return "偶尔熬夜的小伙伴" if (count >= 50) return "深夜有时也会陪你聊聊" return "早睡早起,健康作息" } function getTokenMetaphor(tokens: number): string { const millions = tokens / 1000000 if (millions >= 100) return "思考量堪比一座图书馆" if (millions >= 50) return "相当于写了一部百科全书" if (millions >= 10) return "脑细胞估计消耗了不少" if (millions >= 1) return "也算是费了一番脑筋" return "轻轻松松,游刃有余" } function getCostMetaphor(cost: number): string { if (cost >= 1000) return "这钱够吃一年的泡面了" if (cost >= 500) return "相当于买了一台游戏机" if (cost >= 100) return "够请大家喝几杯奶茶" if (cost >= 50) return "一顿火锅的钱" if (cost >= 10) return "几杯咖啡的价格" return "省钱小能手" } function getSilenceMetaphor(rate: number): string { if (rate >= 80) return "沉默是金,惜字如金" if (rate >= 60) return "话不多但句句到位" if (rate >= 40) return "该说的时候才开口" if (rate >= 20) return "能聊的都聊了" return "话痨本痨,有问必答" } function getImageMetaphor(count: number): string { if (count >= 10000) return "眼睛都快看花了" if (count >= 5000) return "堪比专业摄影师的阅片量" if (count >= 1000) return "看图小达人" if (count >= 500) return "图片鉴赏家" if (count >= 100) return "偶尔欣赏一下美图" return "图片?有空再看" } function getRejectedMetaphor(count: number): string { if (count >= 500) return "在不断的纠正中成长" if (count >= 200) return "学习永无止境" if (count >= 100) return "虚心接受,积极改正" if (count >= 50) return "偶尔也会犯错" if (count >= 10) return "表现还算不错" return "完美表达,无需纠正" } function getExpensiveThinkingMetaphor(cost: number): string { if (cost >= 1) return "这次思考的价值堪比一顿大餐!" if (cost >= 0.5) return "为了这个问题,我可是认真思考了!" if (cost >= 0.1) return "下了点功夫,值得的!" if (cost >= 0.01) return "花了点小钱,但很值得" return "小小思考,不足挂齿" } function getFavoriteReplyMetaphor(count: number, botName: string): string { if (count >= 100) return "这句话简直是万能钥匙!" if (count >= 50) return "百试不爽的经典回复" if (count >= 20) return `${botName}的口头禅` if (count >= 10) return "常用语录之一" return "偶尔用用的小确幸" } function getNightOwlMetaphor(isNightOwl: boolean, midnightCount: number): string { if (isNightOwl) { if (midnightCount >= 1000) return "深夜的守护者,黑暗中的光芒" if (midnightCount >= 500) return "月亮是我的好朋友" if (midnightCount >= 100) return "越夜越精神,夜晚才是主场" return "偶尔熬夜,享受宁静时光" } else { if (midnightCount <= 10) return "作息规律,健康生活的典范" if (midnightCount <= 50) return "早睡早起,偶尔也会熬个夜" return "虽然是早起鸟,但也会守候深夜" } } function getBusiestDayMetaphor(count: number): string { if (count >= 1000) return "忙到飞起,键盘都要冒烟了" if (count >= 500) return "这天简直是话痨附体" if (count >= 200) return "社交达人上线" if (count >= 100) return "比平时活跃不少" if (count >= 50) return "小忙一下" return "还算轻松的一天" } export function AnnualReportPage() { const [year] = useState(2025) const [data, setData] = useState(null) const [isLoading, setIsLoading] = useState(true) const [isExporting, setIsExporting] = useState(false) const [error, setError] = useState(null) const reportRef = useRef(null) const { toast } = useToast() const loadReport = useCallback(async () => { try { setIsLoading(true) setError(null) const result = await getAnnualReport(year) setData(result) } catch (err) { setError(err instanceof Error ? err : new Error('获取年度报告失败')) } finally { setIsLoading(false) } }, [year]) // 导出为图片 const handleExport = useCallback(async () => { if (!reportRef.current || !data) return setIsExporting(true) toast({ title: '正在生成图片', description: '请稍候...', }) try { const element = reportRef.current // 获取当前主题的背景色 const computedStyle = getComputedStyle(document.documentElement) const backgroundColor = computedStyle.getPropertyValue('--background').trim() ? `hsl(${computedStyle.getPropertyValue('--background').trim()})` : (document.documentElement.classList.contains('dark') ? '#0a0a0a' : '#ffffff') // 保存原始样式 const originalWidth = element.style.width const originalMaxWidth = element.style.maxWidth // 临时设置固定宽度以去除左右空白 element.style.width = '1024px' element.style.maxWidth = '1024px' const dataUrl = await toPng(element, { quality: 1, pixelRatio: 2, backgroundColor, cacheBust: true, filter: (node) => { // 过滤掉导出按钮 if (node instanceof HTMLElement && node.hasAttribute('data-export-btn')) { return false } return true }, }) // 恢复原始样式 element.style.width = originalWidth element.style.maxWidth = originalMaxWidth // 创建下载链接 const link = document.createElement('a') link.download = `${data.bot_name}_${data.year}_年度总结.png` link.href = dataUrl link.click() toast({ title: '导出成功', description: '年度报告已保存为图片', }) } catch (err) { console.error('导出图片失败:', err) toast({ title: '导出失败', description: '请重试', variant: 'destructive', }) } finally { setIsExporting(false) } }, [data, toast]) useEffect(() => { loadReport() }, [loadReport]) if (isLoading) { return } if (error) { return (
获取年度报告失败: {error.message}
) } if (!data) return null return (
{/* 头部 Hero */}
{/* 导出按钮 */}

{data.bot_name} {data.year} 年度总结

连接与成长 · Connection & Growth

生成时间: {data.generated_at}
{/* 背景装饰 */}
{/* 维度一:时光足迹 */}

时光足迹

} /> } /> } /> : } />
24小时活跃时钟 {data.bot_name}在一天中各个时段的活跃程度 ({ hour: `${hour}点`, count }))}> {data.time_footprint.first_message_time && (

2025年的故事开始于

{data.time_footprint.first_message_time}

{data.time_footprint.first_message_user} 说: "{data.time_footprint.first_message_content}"

)}
{/* 维度二:社交网络 */}

社交网络

} /> } /> } />
话痨群组 TOP5
{data.social_network.top_groups.length > 0 ? ( data.social_network.top_groups.map((group: { group_id: string; group_name: string; message_count: number; is_webui?: boolean }, index: number) => (
{index + 1} {group.group_name} {group.is_webui && ( WebUI )}
{group.message_count} 条消息
)) ) : (
暂无数据
)}
年度最佳损友 TOP5
{data.social_network.top_users.length > 0 ? ( data.social_network.top_users.map((user: { user_id: string; user_nickname: string; message_count: number; is_webui?: boolean }, index: number) => (
{index + 1} {user.user_nickname} {user.is_webui && ( WebUI )}
{user.message_count} 次互动
)) ) : (
暂无数据
)}
{/* 维度三:最强大脑 */}

最强大脑

} /> $} /> } /> } />
模型偏好分布
{data.brain_power.model_distribution.slice(0, 5).map((item: { model: string; count: number }, index: number) => { const maxCount = data.brain_power.model_distribution[0]?.count || 1 const percentage = Math.round((item.count / maxCount) * 100) return (
{item.model} {item.count.toLocaleString()} 次
) })}
{/* 最喜欢的回复模型 TOP5 */} {data.brain_power.top_reply_models && data.brain_power.top_reply_models.length > 0 && ( 最喜欢的回复模型 TOP5 {data.bot_name}用来回复消息的模型偏好
{data.brain_power.top_reply_models.map((item: { model: string; count: number }, index: number) => { const maxCount = data.brain_power.top_reply_models[0]?.count || 1 const percentage = Math.round((item.count / maxCount) * 100) return (
{item.model} {item.count.toLocaleString()} 次
) })}
)} {/* 烧钱大户 - 只有有有效用户数据时才显示 */} {data.brain_power.top_token_consumers && data.brain_power.top_token_consumers.length > 0 && ( 烧钱大户 TOP3 谁消耗了最多的 API 额度
{data.brain_power.top_token_consumers.map((consumer: { user_id: string; cost: number; tokens: number }) => (
用户 {consumer.user_id} ${consumer.cost.toFixed(2)}
))}
)}
{/* 最昂贵的思考 & 思考深度 */}
💰 最昂贵的一次思考
${data.brain_power.most_expensive_cost.toFixed(4)}
{data.brain_power.most_expensive_time && (

发生在 {data.brain_power.most_expensive_time}

)}

{getExpensiveThinkingMetaphor(data.brain_power.most_expensive_cost)}

🧠 思考深度
{data.brain_power.avg_reasoning_length?.toFixed(0) || 0}
平均思考字数
{data.brain_power.max_reasoning_length?.toLocaleString() || 0}
最长思考字数
{data.brain_power.max_reasoning_time && (

最深沉的思考发生在 {data.brain_power.max_reasoning_time}

)}
{/* 维度四:个性与表达 */}

个性与表达

{/* 深夜回复 & 最喜欢的回复 */} {(data.expression_vibe.late_night_reply || data.expression_vibe.favorite_reply) && (
{data.expression_vibe.late_night_reply && ( 🌙 深夜还在回复 凌晨 {data.expression_vibe.late_night_reply.time},{data.bot_name}还在回复...

"{data.expression_vibe.late_night_reply.content}"

是有什么心事吗?

)} {data.expression_vibe.favorite_reply && ( 💬 最喜欢的回复 使用了 {data.expression_vibe.favorite_reply.count} 次

"{data.expression_vibe.favorite_reply.content}"

{getFavoriteReplyMetaphor(data.expression_vibe.favorite_reply.count, data.bot_name)}

)}
)}
{/* 使用最多的表情包 TOP3 */} 使用最多的表情包 TOP3 年度最爱的表情包们 {data.expression_vibe.top_emojis && data.expression_vibe.top_emojis.length > 0 ? (
{data.expression_vibe.top_emojis.slice(0, 3).map((emoji: { id: number; usage_count: number }, index: number) => (
{`TOP {index + 1}

{emoji.usage_count} 次

))}
) : (
暂无数据
)}
印象最深刻的表达风格 {data.bot_name}最常使用的表达方式
{data.expression_vibe.top_expressions.map((exp: { style: string; count: number }, index: number) => ( {exp.style} ({exp.count}) ))}
} /> } />
{/* 行动派 */} {data.expression_vibe.action_types.length > 0 && ( 行动派 除了聊天,我还帮大家做了这些事
{data.expression_vibe.action_types.map((action: { action: string; count: number }) => (
{action.action} {action.count} 次
))}
)}
{/* 维度五:趣味成就 */}

趣味成就

新学到的"黑话" 今年我学会了 {data.achievements.new_jargon_count} 个新词
{data.achievements.sample_jargons.map((jargon: { content: string; meaning: string; count: number }) => (
{jargon.content}
{jargon.meaning || '暂无解释'}
))}
{data.achievements.total_messages.toLocaleString()}
年度总消息数
其中回复了 {data.achievements.total_replies.toLocaleString()} 次
{/* 底部 */}

MaiBot 2025 Annual Report

Generated with ❤️ by MaiBot Team

) } function StatCard({ title, value, description, icon, }: { title: string value: string | number description: string icon: React.ReactNode }) { return ( {title}
{icon}
{value}

{description}

) } function LoadingSkeleton() { return (
{[...Array(4)].map((_, i) => ( ))}
) }