From b117bb1dc225c6a64b11656cf7a4d10a8ca601ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=89=E9=91=AB=E5=8D=87?= Date: Fri, 5 Jun 2026 11:34:39 +0800 Subject: [PATCH] Revert skill list layout and tune leaderboard report - Revert skill settings to vertical list layout with pinned sorting - Default leaderboard range 30d -> 7d, hide stats while report generating - Reframe leaderboard report prompt as user-behavior analysis (cache v5) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../settings/skill-settings-page.tsx | 346 ++++-------------- .../src/pages/AdminUserLeaderboardPage.tsx | 8 +- .../app/gateway/routers/admin_users.py | 21 +- 3 files changed, 81 insertions(+), 294 deletions(-) diff --git a/frontend-web/src/components/workspace/settings/skill-settings-page.tsx b/frontend-web/src/components/workspace/settings/skill-settings-page.tsx index 6fdec3c5..1f224cd2 100644 --- a/frontend-web/src/components/workspace/settings/skill-settings-page.tsx +++ b/frontend-web/src/components/workspace/settings/skill-settings-page.tsx @@ -12,11 +12,10 @@ import { FileArchive, GlobeIcon, InfoIcon, - Loader2, - LockIcon, - PinIcon, - PinOffIcon, - RefreshCwIcon, + Loader2, + LockIcon, + PinIcon, + RefreshCwIcon, SparklesIcon, TagsIcon, Trash2Icon, @@ -27,15 +26,8 @@ import { useRouter } from "next/navigation"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { - Card, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, @@ -203,11 +195,22 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) { }; }, [skills, currentUserId]); - const filteredSkills = filter === "mine" ? activeMine : activeAll; - const staleCount = staleVisible.length; - const archivedCount = archivedVisible.length; - - const totalPages = Math.max(1, Math.ceil(filteredSkills.length / PAGE_SIZE)); + const filteredSkills = filter === "mine" ? activeMine : activeAll; + const orderedSkills = useMemo( + () => + filteredSkills + .map((skill, index) => ({ skill, index })) + .sort((a, b) => { + const pinnedDelta = Number(!!b.skill.pinned) - Number(!!a.skill.pinned); + return pinnedDelta || a.index - b.index; + }) + .map(({ skill }) => skill), + [filteredSkills], + ); + const staleCount = staleVisible.length; + const archivedCount = archivedVisible.length; + + const totalPages = Math.max(1, Math.ceil(orderedSkills.length / PAGE_SIZE)); // Filter / search / tag changes may shrink the result set — pull the page // back into range without forcing a reset on every keystroke. useEffect(() => { @@ -216,10 +219,10 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) { useEffect(() => { setPage(1); }, [search, tagIds, sort]); - const pagedSkills = useMemo( - () => filteredSkills.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE), - [filteredSkills, page], - ); + const pagedSkills = useMemo( + () => orderedSkills.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE), + [orderedSkills, page], + ); const handleRescan = async () => { try { @@ -356,32 +359,18 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) { {!isLoading && !error && filteredSkills.length === 0 && ( )} - - {/* —— 原纵向列表布局(保留备查)—— - {!isLoading && !error && - filteredSkills.length > 0 && - pagedSkills.map((skill) => ( - - ))} */} - {/* card */} - {!isLoading && !error && filteredSkills.length > 0 && ( -
- {pagedSkills.map((skill) => ( - - ))} -
- )} + {!isLoading && !error && filteredSkills.length > 0 && ( +
+ {pagedSkills.map((skill) => ( + + ))} +
+ )} {!isLoading && !error && totalPages > 1 && (
)} {canManage && ( - - )} + : t.settings.skills.pinAction + } + > + + + )} {canTogglePublish && ( - )} - {canManage && ( - - )} - {canTogglePublish && ( - - )} - - {canManage && ( - - )} -
- - - + + 复制技能 diff --git a/frontend-web/src/pages/AdminUserLeaderboardPage.tsx b/frontend-web/src/pages/AdminUserLeaderboardPage.tsx index 144f3f4c..cfd7a5d7 100644 --- a/frontend-web/src/pages/AdminUserLeaderboardPage.tsx +++ b/frontend-web/src/pages/AdminUserLeaderboardPage.tsx @@ -100,7 +100,7 @@ function toDateInput(date: Date): string { return `${year}-${month}-${day}`; } -function defaultRange(days = 30): RangeState { +function defaultRange(days = 7): RangeState { const end = new Date(); const start = new Date(); start.setDate(end.getDate() - days + 1); @@ -108,7 +108,7 @@ function defaultRange(days = 30): RangeState { } function rangeForApi(range: RangeState) { - const normalized = range.since && range.until ? range : defaultRange(30); + const normalized = range.since && range.until ? range : defaultRange(7); const start = new Date(`${normalized.since}T00:00:00`); const end = new Date(`${normalized.until}T23:59:59`); return { @@ -1035,7 +1035,7 @@ function UserDetailPanel({ export default function AdminUserLeaderboardPage() { const navigate = useNavigate(); - const [range, setRange] = useState(() => defaultRange(30)); + const [range, setRange] = useState(() => defaultRange(7)); const [tab, setTab] = useState("activity"); const [query, setQuery] = useState(""); const [groupRules, setGroupRules] = useState("zero,two"); @@ -1288,7 +1288,7 @@ export default function AdminUserLeaderboardPage() { {reportGenerating ? "生成中" : "生成报告"} - {data && ( + {data && reportStatus === "idle" && !reportText && (
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 b34597b6..c7ef2b53 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": 4, + "version": 5, "since": _iso(since), "until": _iso(until), "limit": limit, @@ -1091,15 +1091,16 @@ async def _build_leaderboard_report_prompt( "trends": [t.model_dump() for t in trends[-21:]], } return ( - "请基于以下管理员排行榜统计数据,生成一份中文运营分析报告。" + "请基于以下管理员排行榜统计数据,生成一份中文用户行为分析报告。" "报告需要有结论,不要只罗列数字;请使用 Markdown 标题、列表和表格,让前端可以直接渲染。" "重点要求:" - "1) 优先突出用户最近关注的实时问题、事件、主题和业务方向,必须引用 recent_questions 中的近期问题作为依据;" - "2) 单独分析 frequent_questions,归纳高频关注簇,说明这些高频问题反映出的需求强度;" - "3) 结合 top_users、top_skills、top_tools、top_models 和 trends,说明使用频率、使用增长、常用能力和重点用户画像;" - "4) 可以提及使用强度、关注变化和运营机会,但不要把报告重点放在系统本身的问题、故障、失败率或回答质量缺陷上;" - "5) 给管理员 3-5 条可执行建议,建议应围绕用户关注主题、资源配置、内容供给、培训引导和高频能力优化。" - "输出结构:摘要、最近关注、高频问题与使用频率、重点用户与常用能力、趋势观察、行动建议。" + "1) 你是专门负责分析用户行为的专家,不要自称企业 AI 应用运营分析师;" + "2) 优先突出用户最近常用问题、最近关注的实时问题、事件、主题和业务方向,必须引用 recent_questions 中的近期问题作为依据;" + "3) 单独分析 frequent_questions,归纳高频关注簇,说明这些高频问题反映出的需求强度和重复频率;" + "4) 结合 top_users、top_skills、top_tools、top_models 和 trends,说明使用频率、使用增长、常用能力和重点用户行为画像;" + "5) 可以提及使用强度、关注变化和行为机会,但不要把报告重点放在系统本身的问题、故障、失败率或回答质量缺陷上;" + "6) 给管理员 3-5 条可执行建议,建议应围绕用户关注主题、资源配置、内容供给、培训引导和高频能力优化。" + "输出结构:摘要、最近关注、高频问题与频率、重点用户行为、常用能力、趋势观察、行动建议。" "\n\n统计数据如下:\n" f"{json.dumps(context, ensure_ascii=False, default=str)}" ) @@ -1141,7 +1142,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="你是专门负责分析用户行为的专家,擅长从用户最近常用问题、关注主题、使用频率和调用指标中生成管理报告;报告聚焦用户行为,不讨论系统本身问题。"), HumanMessage(content=prompt), ], config={"run_name": "admin_user_leaderboard_report"}, @@ -1206,7 +1207,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="你是专门负责分析用户行为的专家,擅长从用户最近常用问题、关注主题、使用频率和调用指标中生成管理报告;报告聚焦用户行为,不讨论系统本身问题。"), HumanMessage(content=prompt), ], config={"run_name": "admin_user_leaderboard_report"},