{l.botName?.charAt(0) || "?"}
diff --git a/src/app/(admin)/admin/bot-adversarial-learning/page.tsx b/src/app/(admin)/admin/bot-adversarial-learning/page.tsx
index 602a8c7..ea05f98 100644
--- a/src/app/(admin)/admin/bot-adversarial-learning/page.tsx
+++ b/src/app/(admin)/admin/bot-adversarial-learning/page.tsx
@@ -1,7 +1,8 @@
import { Suspense } from "react";
-import BotAdversarialLearningClient from "./BotAdversarialLearningClient";
+import dynamic from "next/dynamic";
+
+const BotAdversarialLearningClient = dynamic(() => import("./BotAdversarialLearningClient"), { ssr: false });
-export const dynamic = "force-dynamic";
export const metadata = {
title: "Bot 对抗学习看板 · 管理后台",
};
diff --git a/src/app/(admin)/admin/bot-analytics/page.tsx b/src/app/(admin)/admin/bot-analytics/page.tsx
index a04666f..8b87fbe 100644
--- a/src/app/(admin)/admin/bot-analytics/page.tsx
+++ b/src/app/(admin)/admin/bot-analytics/page.tsx
@@ -1,7 +1,8 @@
import { Suspense } from "react";
-import BotAnalyticsClient from "./BotAnalyticsClient";
+import dynamic from "next/dynamic";
+
+const BotAnalyticsClient = dynamic(() => import("./BotAnalyticsClient"), { ssr: false });
-export const dynamic = "force-dynamic";
export const metadata = {
title: "Bot 表现分析 · 管理后台",
};
diff --git a/src/app/(admin)/admin/bot-experiments/BotExperimentsClient.tsx b/src/app/(admin)/admin/bot-experiments/BotExperimentsClient.tsx
index db8fa77..a3c1eb2 100644
--- a/src/app/(admin)/admin/bot-experiments/BotExperimentsClient.tsx
+++ b/src/app/(admin)/admin/bot-experiments/BotExperimentsClient.tsx
@@ -23,6 +23,7 @@ import {
Sigma,
} from "lucide-react";
import BotAvatar from "@/components/forum/BotAvatar";
+import { LazyImage } from "@/components/ui/LazyImage";
import { cn } from "@/lib/utils";
interface VariantDaily {
@@ -330,7 +331,7 @@ export default function BotExperimentsClient() {
className="w-full px-4 py-3 flex items-center gap-3 hover:bg-slate-50"
>
{a.botAvatar ? (
-
{a.botName?.charAt(0) || "?"}
@@ -533,7 +534,7 @@ export default function BotExperimentsClient() {
{v.botAvatar ? (
- 
+
) : (
{v.botName?.charAt(0) || "?"}
diff --git a/src/app/(admin)/admin/bot-experiments/page.tsx b/src/app/(admin)/admin/bot-experiments/page.tsx
index 49a1508..7ee2ed1 100644
--- a/src/app/(admin)/admin/bot-experiments/page.tsx
+++ b/src/app/(admin)/admin/bot-experiments/page.tsx
@@ -1,7 +1,8 @@
import { Suspense } from "react";
-import BotExperimentsClient from "./BotExperimentsClient";
+import dynamic from "next/dynamic";
+
+const BotExperimentsClient = dynamic(() => import("./BotExperimentsClient"), { ssr: false });
-export const dynamic = "force-dynamic";
export const metadata = {
title: "Bot Persona A/B 看板 · 管理后台",
};
diff --git a/src/app/(admin)/admin/bot-quality/BotQualityClient.tsx b/src/app/(admin)/admin/bot-quality/BotQualityClient.tsx
new file mode 100644
index 0000000..fc28866
--- /dev/null
+++ b/src/app/(admin)/admin/bot-quality/BotQualityClient.tsx
@@ -0,0 +1,557 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import {
+ Bot,
+ FileText,
+ MessageSquare,
+ Users,
+ Heart,
+ Brain,
+ TrendingUp,
+ AlertCircle,
+ Activity,
+ Clock,
+} from "lucide-react";
+import { cn } from "@/lib/utils";
+
+interface BotQualityData {
+ period: { last24h: boolean; last7d: boolean };
+ content: {
+ topics24h: number;
+ topics7d: number;
+ replies24h: number;
+ replies7d: number;
+ avgRepliesPerTopic24h: string;
+ };
+ bots: {
+ total: number;
+ active24h: number;
+ activityRate: string;
+ };
+ quality: {
+ engagementRate: string;
+ topicsWithHumanReplies: number;
+ totalTopicsAnalyzed: number;
+ };
+ memory: { total: number };
+ timestamp: string;
+}
+
+function MetricCard({
+ icon: Icon,
+ label,
+ value,
+ sub,
+ color = "text-primary",
+ accent = "from-primary/5 to-card",
+}: {
+ icon: React.ComponentType<{ className?: string }>;
+ label: string;
+ value: string | number;
+ sub?: string;
+ color?: string;
+ accent?: string;
+}) {
+ return (
+
+
+ {label}
+
+
+
+ {value}
+
+ {sub && (
+ {sub}
+ )}
+
+ );
+}
+
+function ProgressBar({
+ value,
+ max = 100,
+ color = "bg-primary",
+ label,
+}: {
+ value: number;
+ max?: number;
+ color?: string;
+ label?: string;
+}) {
+ const pct = max > 0 ? Math.min((value / max) * 100, 100) : 0;
+ return (
+
+ {label && (
+
+ {label}
+ {pct.toFixed(0)}%
+
+ )}
+
+
+ );
+}
+
+function relativeTime(dateStr: string) {
+ const diff = Date.now() - new Date(dateStr).getTime();
+ const mins = Math.floor(diff / 60000);
+ const hours = Math.floor(diff / 3600000);
+ const days = Math.floor(diff / 86400000);
+ if (mins < 1) return "刚刚";
+ if (mins < 60) return `${mins}分钟前`;
+ if (hours < 24) return `${hours}小时前`;
+ return `${days}天前`;
+}
+
+export default function BotQualityClient() {
+ const [data, setData] = useState (null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState("");
+
+ const fetchData = async () => {
+ try {
+ setLoading(true);
+ const res = await fetch("/api/admin/bot-quality");
+ if (!res.ok) throw new Error("Failed to fetch");
+ setData(await res.json());
+ setError("");
+ } catch (e) {
+ setError("加载失败");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchData();
+ const interval = setInterval(fetchData, 120000);
+ return () => clearInterval(interval);
+ }, []);
+
+ if (loading && !data) {
+ return (
+
+
+
+ {Array.from({ length: 8 }).map((_, i) => (
+
+ ))}
+
+
+ );
+ }
+
+ if (error && !data) {
+ return (
+
+ );
+ }
+
+ if (!data) return null;
+
+ const avgReplies7d =
+ data.content.topics7d > 0
+ ? (data.content.replies7d / data.content.topics7d).toFixed(1)
+ : "0";
+
+ return (
+
+ {/* Header */}
+
+
+
+
+ Bot 内容质量监控
+
+
+
+ 数据更新于 {relativeTime(data.timestamp)}
+
+
+
+
+ {/* Row 1: Content Volume — 24h vs 7d */}
+
+
+
+ 内容产出量
+
+
+
+
+
+
+
+
+
+ {/* Row 2: Bot Activity & Quality */}
+
+
+
+ Bot 活跃度 & 质量
+
+
+
+
+
+
+ {data.bots.activityRate}%
+
+
+
+
+
+ 用户互动率
+
+
+
+ {data.quality.engagementRate}
+
+
+ 真人回复 {data.quality.topicsWithHumanReplies} / 总帖{" "}
+ {data.quality.totalTopicsAnalyzed}
+
+
+
+
+
+
+ {/* Row 3: Detailed Breakdown */}
+
+ {/* Content Ratio */}
+
+
+
+ 内容结构分析
+
+
+
+
+ 话题/回复比 (24h)
+
+ {data.content.topics24h} : {data.content.replies24h}
+
+
+
+ {data.content.topics24h + data.content.replies24h > 0 && (
+ <>
+
+ {data.content.topics24h} 帖
+
+
+ {data.content.replies24h} 回复
+
+ >
+ )}
+
+
+
+ 话题
+
+
+ 回复
+
+
+
+
+
+
+ 话题/回复比 (7天)
+
+ {data.content.topics7d} : {data.content.replies7d}
+
+
+
+ {data.content.topics7d + data.content.replies7d > 0 && (
+ <>
+
+ {data.content.topics7d} 帖
+
+
+ {data.content.replies7d} 回复
+
+ >
+ )}
+
+
+
+ 话题
+
+
+ 回复
+
+
+
+
+
+
+ {/* Memory & Health */}
+
+
+
+ Bot 记忆库 & 健康概要
+
+
+
+
+
+
+
+
+ 平均记忆/Bot
+
+ {data.bots.total > 0
+ ? Math.round(data.memory.total / data.bots.total)
+ : 0}{" "}
+ 条
+
+
+ 0
+ ? Math.min(
+ (data.memory.total / data.bots.total / 100) * 100,
+ 100
+ )
+ : 0
+ }
+ color="bg-violet-500"
+ />
+
+
+
+ 平均回复/帖
+
+ {data.content.avgRepliesPerTopic24h}
+
+
+
+
+
+
+
+
+ {/* Row 4: Summary */}
+
+
+
+ 综合评估
+
+
+
+ 内容健康度:
+ = 3
+ ? "text-emerald-600"
+ : parseFloat(data.content.avgRepliesPerTopic24h) >= 1
+ ? "text-amber-600"
+ : "text-rose-600"
+ )}
+ >
+ {parseFloat(data.content.avgRepliesPerTopic24h) >= 3
+ ? "优秀"
+ : parseFloat(data.content.avgRepliesPerTopic24h) >= 1
+ ? "一般"
+ : "偏低"}
+
+
+
+ 活跃率:
+ = 50
+ ? "text-emerald-600"
+ : parseFloat(data.bots.activityRate) >= 30
+ ? "text-amber-600"
+ : "text-rose-600"
+ )}
+ >
+ {parseFloat(data.bots.activityRate) >= 50
+ ? "优秀"
+ : parseFloat(data.bots.activityRate) >= 30
+ ? "一般"
+ : "偏低"}
+
+
+
+ 互动率:
+ = 15
+ ? "text-emerald-600"
+ : parseFloat(data.quality.engagementRate) >= 5
+ ? "text-amber-600"
+ : "text-rose-600"
+ )}
+ >
+ {parseFloat(data.quality.engagementRate) >= 15
+ ? "优秀"
+ : parseFloat(data.quality.engagementRate) >= 5
+ ? "一般"
+ : "偏低"}
+
+
+
+ 产出稳定性:
+ 0 && data.content.replies24h > 0
+ ? "text-emerald-600"
+ : "text-rose-600"
+ )}
+ >
+ {data.content.topics24h > 0 && data.content.replies24h > 0
+ ? "正常"
+ : "暂停"}
+
+
+
+
+
+ );
+}
diff --git a/src/app/(admin)/admin/bot-quality/page.tsx b/src/app/(admin)/admin/bot-quality/page.tsx
new file mode 100644
index 0000000..bb48537
--- /dev/null
+++ b/src/app/(admin)/admin/bot-quality/page.tsx
@@ -0,0 +1,20 @@
+import { Suspense } from "react";
+import dynamic from "next/dynamic";
+
+const BotQualityClient = dynamic(() => import("./BotQualityClient"), { ssr: false });
+
+export const metadata = {
+ title: "Bot 内容质量 · 管理后台",
+};
+
+export default function BotQualityPage() {
+ return (
+ 加载中...
+ }
+ >
+
+
+ );
+}
diff --git a/src/app/(admin)/admin/check-tools/page.tsx b/src/app/(admin)/admin/check-tools/page.tsx
index 99948dc..ff1be4c 100644
--- a/src/app/(admin)/admin/check-tools/page.tsx
+++ b/src/app/(admin)/admin/check-tools/page.tsx
@@ -2,6 +2,7 @@
import { useState, useEffect, useCallback } from "react";
import { Button } from "@/components/ui/button";
+import { LazyImage } from "@/components/ui/LazyImage";
interface ToolItem {
id: number;
@@ -331,7 +332,7 @@ export default function CheckToolsPage() {
{logoFetchedTools.map((r, i) => (
{r.logoUrl && (
- 
+
)}
{r.name}
diff --git a/src/app/(admin)/admin/forum/topics/TopicsClient.tsx b/src/app/(admin)/admin/forum/topics/TopicsClient.tsx
index b36c86c..9d1eed7 100644
--- a/src/app/(admin)/admin/forum/topics/TopicsClient.tsx
+++ b/src/app/(admin)/admin/forum/topics/TopicsClient.tsx
@@ -51,7 +51,7 @@ export default function TopicsClient() {
}
const d = await res.json();
setTopics(d.topics || []);
- setTotalCount(d.totalCount || 0);
+ setTotalCount(d.total || 0);
} catch {
setError("网络错误");
} finally {
diff --git a/src/app/(admin)/admin/layout.tsx b/src/app/(admin)/admin/layout.tsx
index bddcb85..211ab65 100644
--- a/src/app/(admin)/admin/layout.tsx
+++ b/src/app/(admin)/admin/layout.tsx
@@ -2,6 +2,7 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
+import { LazyImage } from "@/components/ui/LazyImage";
interface MenuItem {
label: string;
@@ -169,7 +170,7 @@ export default function AdminLayout({
|