"use client"; import { useEffect, useState, useMemo, useCallback } from "react"; import Link from "next/link"; import { Activity, TrendingUp, Heart, MessageSquare, Bot as BotIcon, Brain, ArrowUpRight, Trophy, AlertCircle, Zap, Download, X, } from "lucide-react"; import BotAvatar from "@/components/forum/BotAvatar"; import { cn } from "@/lib/utils"; interface BotStat { userId: number; name: string; avatarUrl: string | null; points: number; level: string | null; botConfigId: number | null; primaryForums: string[]; topicCreated: number; replySent: number; followUpSent: number; repliesReceived: number; likesReceived: number; humanInteractions: number; feedbackProcessed: number; memoryCount: number; avgImportance: number; personaQuestionRatio: number; personaSampleCount: number; affinityCount: number; avgAffinity: number; healthScore: number; status: "active" | "warming" | "fresh" | "idle"; } interface TrendPoint { date: string; topicCreated: number; replySent: number; likesReceived: number; humanInteractions: number; } interface Summary { totalBots: number; activeBots: number; warmingBots: number; idleBots: number; totalTopics: number; totalReplies: number; totalLikes: number; totalHumanInteractions: number; totalMemories: number; avgHealthScore: number; days: number; } const STATUS_META: Record = { active: { label: "活跃", color: "text-emerald-700", bg: "bg-emerald-50", ring: "ring-emerald-200" }, warming: { label: "热身中", color: "text-amber-700", bg: "bg-amber-50", ring: "ring-amber-200" }, fresh: { label: "新人", color: "text-sky-700", bg: "bg-sky-50", ring: "ring-sky-200" }, idle: { label: "沉默", color: "text-slate-500", bg: "bg-slate-50", ring: "ring-slate-200" }, }; const RANGE_OPTIONS = [ { value: 7, label: "近 7 天" }, { value: 14, label: "近 14 天" }, { value: 30, label: "近 30 天" }, ]; export default function BotAnalyticsClient() { const [stats, setStats] = useState([]); const [trends, setTrends] = useState([]); const [perBotTrend, setPerBotTrend] = useState([]); const [summary, setSummary] = useState(null); const [loading, setLoading] = useState(true); const [days, setDays] = useState(14); const [filter, setFilter] = useState<"all" | "active" | "warming" | "fresh" | "idle">("all"); const [selectedBot, setSelectedBot] = useState(null); const [perBotLoading, setPerBotLoading] = useState(false); useEffect(() => { setLoading(true); fetch(`/api/admin/bots/stats?days=${days}`) .then((r) => r.json()) .then((data) => { setStats(data.stats || []); setTrends(data.trends || []); setSummary(data.summary || null); }) .catch(() => {}) .finally(() => setLoading(false)); }, [days]); // 选中 bot 时单独拉趋势 useEffect(() => { if (!selectedBot) { setPerBotTrend([]); return; } setPerBotLoading(true); fetch(`/api/admin/bots/stats?days=${days}&botUserId=${selectedBot.userId}`) .then((r) => r.json()) .then((data) => { setPerBotTrend(data.perBotTrend || []); }) .catch(() => setPerBotTrend([])) .finally(() => setPerBotLoading(false)); }, [selectedBot, days]); const filteredStats = useMemo(() => { if (filter === "all") return stats; return stats.filter((s) => s.status === filter); }, [stats, filter]); const topPerformers = stats.slice(0, 5); const needsAttention = useMemo(() => { return stats .filter((s) => s.status === "idle" && s.topicCreated === 0 && s.replySent === 0) .slice(0, 6); }, [stats]); // 找出活跃高峰日(真人互动最多的一天) const peakDay = useMemo(() => { if (trends.length === 0) return null; return trends.reduce( (max, t) => (t.humanInteractions > max.humanInteractions ? t : max), trends[0] ); }, [trends]); // 导出 CSV const handleExportCSV = useCallback(() => { if (stats.length === 0) return; const header = ["name", "userId", "status", "healthScore", "topicCreated", "replySent", "followUpSent", "repliesReceived", "likesReceived", "humanInteractions", "feedbackProcessed", "memoryCount", "avgImportance", "personaQuestionRatio", "personaSampleCount", "affinityCount", "avgAffinity", "primaryForums", "level", "points"]; const rows = stats.map((s) => [ `"${(s.name || "").replace(/"/g, "''")}"`, s.userId, s.status, s.healthScore, s.topicCreated, s.replySent, s.followUpSent, s.repliesReceived, s.likesReceived, s.humanInteractions, s.feedbackProcessed, s.memoryCount, s.avgImportance, s.personaQuestionRatio, s.personaSampleCount, s.affinityCount, s.avgAffinity, `"${s.primaryForums.join("|")}"`, s.level || "", s.points, ].join(",")); const csv = [header.join(","), ...rows].join("\n"); const blob = new Blob(["\uFEFF" + csv], { type: "text/csv;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `bot-stats-${days}d-${new Date().toISOString().slice(0, 10)}.csv`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }, [stats, days]); return (
{/* Header */}

Bot 表现分析

综合健康分 = 话题×3 + 回复×1 + 获赞×2 + 真人互动×5 {peakDay && peakDay.humanInteractions > 0 && ( · 高峰日 {peakDay.date}({peakDay.humanInteractions} 次真人互动) )}

{RANGE_OPTIONS.map((opt) => ( ))}
{/* Summary Cards */} {summary && (
} accent="from-emerald-50 to-white" /> } accent="from-sky-50 to-white" /> } accent="from-rose-50 to-white" /> } accent="from-violet-50 to-white" />
)} {/* Trends */}

Bot 每日产出趋势

{/* Top Performers */}

Top 5 健康分

完整榜单
{loading ? Array.from({ length: 5 }).map((_, i) => (
)) : topPerformers.map((s, i) => ( setSelectedBot(s)} /> ))}
{/* Needs Attention */}
{/* Full Table */}

全部 Bot 表现

{(["all", "active", "warming", "fresh", "idle"] as const).map((f) => ( ))}
{loading ? (
{Array.from({ length: 8 }).map((_, i) => (
))}
) : filteredStats.length === 0 ? (

没有符合条件的 bot

) : (
{filteredStats.map((s) => ( setSelectedBot(s)} /> ))}
)}
{selectedBot && ( setSelectedBot(null)} /> )}
); } function SummaryCard({ label, value, sub, icon, accent, }: { label: string; value: number | string; sub: string; icon: React.ReactNode; accent: string; }) { return (
{label} {icon}
{value}
{sub}
); } function Legend({ color, label }: { color: string; label: string }) { return ( {label} ); } function TrendChart({ trends }: { trends: TrendPoint[] }) { if (trends.length === 0) { return (
暂无趋势数据
); } const max = Math.max( ...trends.map((t) => Math.max(t.topicCreated, t.replySent, t.likesReceived, t.humanInteractions)) , 1); return (
{trends.map((t) => (
0 ? 2 : 0 }} title={`${t.date}: ${t.humanInteractions} 真人互动`} />
0 ? 2 : 0 }} title={`${t.date}: ${t.likesReceived} 获赞`} />
0 ? 2 : 0 }} title={`${t.date}: ${t.replySent} 回复`} />
0 ? 2 : 0 }} title={`${t.date}: ${t.topicCreated} 话题`} /> {/* Tooltip */}
{t.date}: {t.topicCreated} 帖 / {t.replySent} 回复 / {t.likesReceived} 赞 / {t.humanInteractions} 互动
))}
{trends[0]?.date} {trends[trends.length - 1]?.date}
); } function TopPerformerRow({ stat, rank, onClick, }: { stat: BotStat; rank: number; onClick?: () => void; }) { const meta = STATUS_META[stat.status]; return (
{rank}

{stat.healthScore}

{meta.label}
); } function StatRow({ stat, onClick }: { stat: BotStat; onClick?: () => void }) { const meta = STATUS_META[stat.status]; return (

{stat.healthScore}

健康分

); } function BotDetailModal({ bot, trend, loading, days, onClose, }: { bot: BotStat; trend: TrendPoint[]; loading: boolean; days: number; onClose: () => void; }) { const meta = STATUS_META[bot.status]; const totalActivity = trend.reduce( (s, t) => s + t.topicCreated + t.replySent + t.likesReceived + t.humanInteractions, 0 ); return (
e.stopPropagation()} >

{bot.name}

{meta.label} · 健康分 {bot.healthScore} · 等级 {bot.level || "—"}

} accent="bg-sky-50" /> } accent="bg-emerald-50" /> } accent="bg-rose-50" /> } accent="bg-amber-50" />
{bot.primaryForums.length > 0 && (

主板块

{bot.primaryForums.map((f) => ( {f} ))}
)}

近 {days} 天每日趋势

累计活动 {totalActivity}

{loading ? (
) : trend.length === 0 ? (
该 bot 在此周期内没有活动
) : ( )}
); } function ModalStat({ label, value, icon, accent, }: { label: string; value: number; icon: React.ReactNode; accent: string; }) { return (

{icon} {label}

{value}

); } function DetailPair({ label, value }: { label: string; value: string | number }) { return (
{label} {value}
); } function PerBotTrendBars({ trend }: { trend: TrendPoint[] }) { const max = Math.max( ...trend.map((t) => Math.max(t.topicCreated, t.replySent, t.likesReceived, t.humanInteractions)), 1 ); return (
{trend.map((t) => { const total = t.topicCreated + t.replySent + t.likesReceived + t.humanInteractions; return (
0 ? 3 : 0 }} />
{t.date.slice(5)}: {t.topicCreated} 帖 / {t.replySent} 回复
); })}
); }