diff --git a/frontend-web/src/components/page-sidebar.tsx b/frontend-web/src/components/page-sidebar.tsx index 9dce22c4..5cebcc9f 100644 --- a/frontend-web/src/components/page-sidebar.tsx +++ b/frontend-web/src/components/page-sidebar.tsx @@ -269,14 +269,6 @@ export function PageSidebar({ 用户排行榜 )} - {isAdmin && ( - navigate("/page/strategy/admin/user-leaderboard/settings")} - > - - 排行榜设置 - - )} openSettings("account")}> 个人信息 diff --git a/frontend-web/src/components/workspace/workspace-nav-menu.tsx b/frontend-web/src/components/workspace/workspace-nav-menu.tsx index bc84f4a0..2c7c085f 100644 --- a/frontend-web/src/components/workspace/workspace-nav-menu.tsx +++ b/frontend-web/src/components/workspace/workspace-nav-menu.tsx @@ -198,16 +198,6 @@ export function WorkspaceNavMenu() { 用户排行榜 )} - {isAdmin && ( - - navigate("/page/strategy/admin/user-leaderboard/settings") - } - > - - 排行榜设置 - - )} {isAdmin && ( navigate("/page/workspace/admin")} diff --git a/frontend-web/src/core/page-layout/sidebar-menu.ts b/frontend-web/src/core/page-layout/sidebar-menu.ts index c3b700e3..48cd5afc 100644 --- a/frontend-web/src/core/page-layout/sidebar-menu.ts +++ b/frontend-web/src/core/page-layout/sidebar-menu.ts @@ -253,13 +253,6 @@ export const sidebarMenuItems: MenuItem[] = [ path: "/page/strategy/admin/user-leaderboard", adminOnly: true, }, - { - id: "mgmt-user-leaderboard-settings", - label: "排行榜设置", - icon: Settings, - path: "/page/strategy/admin/user-leaderboard/settings", - adminOnly: true, - }, { id: "mgmt-users", label: "用户管理", diff --git a/frontend-web/src/pages/AdminUserLeaderboardPage.tsx b/frontend-web/src/pages/AdminUserLeaderboardPage.tsx index 08545894..08d47aa9 100644 --- a/frontend-web/src/pages/AdminUserLeaderboardPage.tsx +++ b/frontend-web/src/pages/AdminUserLeaderboardPage.tsx @@ -42,10 +42,12 @@ import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { MarkdownContent } from "@/components/workspace/messages/markdown-content"; import type { AdminLeaderboardDimension, AdminLeaderboardOverview, AdminLeaderboardQuestion, + AdminLeaderboardResponse, AdminLeaderboardSkill, AdminLeaderboardTrendPoint, AdminLeaderboardUser, @@ -53,7 +55,9 @@ import type { import { streamUserLeaderboardReport } from "@/core/admin/api"; import { useUserAnalytics, useUserLeaderboard } from "@/core/admin/hooks"; import { useModels } from "@/core/models/hooks"; +import { downloadMarkdownAsDocx } from "@/core/scheduled-tasks/markdown-docx"; import { cn } from "@/lib/utils"; +import { toast } from "sonner"; type UserRankTab = "activity" | "questions" | "tokens" | "skills" | "efficiency"; type RangeState = { since: string; until: string; days?: number }; @@ -193,6 +197,39 @@ function exportUsersCsv(users: AdminLeaderboardUser[], tab: UserRankTab) { ); } +function markdownList(items: string[]): string { + return items.length ? items.map((item) => `- ${item}`).join("\n") : "- 暂无数据"; +} + +function buildReportFocusMarkdown(data: AdminLeaderboardResponse | undefined, range: RangeState): string { + if (!data) return ""; + const recentQuestions = data.recent_questions + .slice(0, 8) + .map((item) => `${clampText(item.text, 120)}(${fmtTime(item.asked_at)})`); + const frequentQuestions = data.frequent_questions + .slice(0, 8) + .map((item) => clampText(item.text, 120)); + const frequentUsage = [ + ...data.skills.slice(0, 5).map((item) => `技能:${item.name},${fmtNumber(item.call_count)} 次调用`), + ...data.tools.slice(0, 5).map((item) => `工具:${item.name},${fmtNumber(item.call_count)} 次调用`), + ]; + + return [ + "# AI 榜单分析报告", + "", + `统计周期:${range.since} 至 ${range.until}`, + "", + "## 最近关注", + markdownList(recentQuestions), + "", + "## 高频问题", + markdownList(frequentQuestions), + "", + "## 使用频率重点", + markdownList(frequentUsage), + ].join("\n"); +} + function parseGroupRules(value: string): string[] { return value .split(/[\n,,;;\s]+/) @@ -1020,8 +1057,14 @@ export default function AdminUserLeaderboardPage() { const [reportStatus, setReportStatus] = useState<"idle" | "streaming" | "done" | "cached" | "error">("idle"); const [reportGeneratedAt, setReportGeneratedAt] = useState(null); const [reportResolvedModel, setReportResolvedModel] = useState(null); + const [reportExporting, setReportExporting] = useState(false); const reportAbortRef = useRef(null); const reportGenerating = reportStatus === "streaming"; + const reportFocusMarkdown = useMemo(() => buildReportFocusMarkdown(data, range), [data, range]); + const reportWordMarkdown = useMemo( + () => [reportFocusMarkdown, reportText.trim()].filter(Boolean).join("\n\n---\n\n"), + [reportFocusMarkdown, reportText], + ); const rankedUsers = useMemo(() => { const base = data?.users_by_activity ?? []; @@ -1066,6 +1109,26 @@ export default function AdminUserLeaderboardPage() { } }; + const exportReportAsWord = async () => { + if (!reportWordMarkdown.trim()) { + toast.error("暂无可导出的报告内容"); + return; + } + setReportExporting(true); + try { + await downloadMarkdownAsDocx( + reportWordMarkdown, + `user-leaderboard-ai-report-${new Date().toISOString().slice(0, 10)}`, + "AI 榜单分析报告", + ); + toast.success("Word 文件已生成"); + } catch (err) { + toast.error(err instanceof Error ? err.message : "导出 Word 失败"); + } finally { + setReportExporting(false); + } + }; + const startReportGeneration = async () => { reportAbortRef.current?.abort(); const controller = new AbortController(); @@ -1228,6 +1291,54 @@ export default function AdminUserLeaderboardPage() { {reportGenerating ? "生成中" : "生成报告"} + {data && ( + + + + + 最近关注 + + + {data.recent_questions.slice(0, 3).map((item, index) => ( + + {fmtTime(item.asked_at)} + {clampText(item.text, 58)} + + ))} + {data.recent_questions.length === 0 && 暂无数据} + + + + + + 高频问题 + + + {data.frequent_questions.slice(0, 3).map((item, index) => ( + + {index + 1}. {clampText(item.text, 72)} + + ))} + {data.frequent_questions.length === 0 && 暂无数据} + + + + + + 使用频率 + + + {[...data.skills.slice(0, 2), ...data.tools.slice(0, 1)].map((item, index) => ( + + {item.name} + {fmtNumber(item.call_count)} 次 + + ))} + {data.skills.length + data.tools.length === 0 && 暂无数据} + + + + )} {reportStatus === "idle" && !reportText && ( @@ -1246,25 +1357,20 @@ export default function AdminUserLeaderboardPage() { )} {reportText && ( - - {reportText} - + + + )} {reportText && ( - downloadText( - `user-leaderboard-ai-report-${new Date().toISOString().slice(0, 10)}.md`, - reportText, - "text/markdown;charset=utf-8", - ) - } + onClick={exportReportAsWord} + disabled={reportExporting} > - - 下载报告 + {reportExporting ? : } + 导出 Word )} diff --git a/offline-backend-20260512/backend/app/gateway/routers/admin_users.py b/offline-backend-20260512/backend/app/gateway/routers/admin_users.py index 64265701..b34597b6 100644 --- a/offline-backend-20260512/backend/app/gateway/routers/admin_users.py +++ b/offline-backend-20260512/backend/app/gateway/routers/admin_users.py @@ -318,7 +318,7 @@ def _report_cache_key( settings, ) -> str: payload = { - "version": 3, + "version": 4, "since": _iso(since), "until": _iso(until), "limit": limit, @@ -1092,14 +1092,14 @@ async def _build_leaderboard_report_prompt( } return ( "请基于以下管理员排行榜统计数据,生成一份中文运营分析报告。" - "报告需要有结论,不要只罗列数字。必须分析:" - "1) 谁使用次数最多、问答最多、Token 消耗最高;" - "2) 用户通常在问什么、相似问题聚类反映了哪些关注方向;" - "3) 当前所有用户关注的事件/主题/业务方向;" - "4) 从问题重复、失败率、追问密度等信号推断用户是否满意系统回答,注明这是推断;" - "5) 哪些技能/工具/模型最常用,可能说明什么;" - "6) 给管理员 3-5 条可执行建议。" - "输出结构:摘要、关键发现、用户观察、问题主题、满意度推断、建议。" + "报告需要有结论,不要只罗列数字;请使用 Markdown 标题、列表和表格,让前端可以直接渲染。" + "重点要求:" + "1) 优先突出用户最近关注的实时问题、事件、主题和业务方向,必须引用 recent_questions 中的近期问题作为依据;" + "2) 单独分析 frequent_questions,归纳高频关注簇,说明这些高频问题反映出的需求强度;" + "3) 结合 top_users、top_skills、top_tools、top_models 和 trends,说明使用频率、使用增长、常用能力和重点用户画像;" + "4) 可以提及使用强度、关注变化和运营机会,但不要把报告重点放在系统本身的问题、故障、失败率或回答质量缺陷上;" + "5) 给管理员 3-5 条可执行建议,建议应围绕用户关注主题、资源配置、内容供给、培训引导和高频能力优化。" + "输出结构:摘要、最近关注、高频问题与使用频率、重点用户与常用能力、趋势观察、行动建议。" "\n\n统计数据如下:\n" f"{json.dumps(context, ensure_ascii=False, default=str)}" ) @@ -1141,7 +1141,7 @@ async def generate_user_leaderboard_report( model = create_chat_model(name=resolved_model_name, thinking_enabled=thinking_enabled, app_config=config) response = await model.ainvoke( [ - SystemMessage(content="你是企业 AI 系统的运营分析师,擅长从用户行为、问题文本和调用指标中生成管理报告。"), + SystemMessage(content="你是企业 AI 应用的运营分析师,擅长从用户行为、问题文本和调用指标中生成管理报告;报告聚焦用户关注主题和使用频率,不讨论系统本身问题。"), HumanMessage(content=prompt), ], config={"run_name": "admin_user_leaderboard_report"}, @@ -1206,7 +1206,7 @@ async def stream_user_leaderboard_report( model = create_chat_model(name=resolved_model_name, thinking_enabled=thinking_enabled, app_config=config) async for chunk in model.astream( [ - SystemMessage(content="你是企业 AI 系统的运营分析师,擅长从用户行为、问题文本和调用指标中生成管理报告。"), + SystemMessage(content="你是企业 AI 应用的运营分析师,擅长从用户行为、问题文本和调用指标中生成管理报告;报告聚焦用户关注主题和使用频率,不讨论系统本身问题。"), HumanMessage(content=prompt), ], config={"run_name": "admin_user_leaderboard_report"},
- {reportText} -