原来首屏对每个人都长一个样:网格顺序写死,指标卡全是「能力 91 项 / 可提交 39 项」 这类覆盖数字。用过一百次请假的人,请假入口照样排在第 4 位。 新增 src/utils/usage.ts(本机使用画像) - 用 localStorage 记「模块 / 能力的调用次数 + 最近时间」与「写操作成功件数」。 后端 GET /api/agent/tools 只下发"你能用什么",不提供"你常用什么", 所以第一步先落在本机:零后端改动、零数据出网;换设备重新累积。 - 归属按 userId + roleId 隔离(一个账号多重身份能力范围不同,混在一起会串推荐); 目录未回来时退回平台写入的 currentRole,取不到才退化匿名。 - 模块按「轮」计数(一轮查 3 个接口只 +1,符合用户心里"我问过几次"), 能力按「调用次数」计数;done 只统计 write && success —— 查询再多次也不算成果。 - rankFrequent / summarize 有意吃 state 而不是吃 scope:调用方手里那份快照带版本依赖, 记录或清除后能立刻重算,避免"点了清除,页面上常用区块还在"的幽灵状态。 首屏(QuickActions.vue)改成双形态 - 无使用记录 → 完全保持原有引导形态(自我介绍 + 覆盖面指标 + 各模块一句成品请求)。 - 有使用记录 → 个性化问候「欢迎回来,X」,正文直接列出「你最近常办:…」; 指标卡换成「本周已办成 / 常用模块 / 可操作能力」; 新增「常用」区块(用过最多的模块,点一下就发); 快速开始网格按频次重排,用过 2 次以上的卡片描边并打「常用 N 次」角标。 - 能力清单弹层叠加使用次数(模块级「你用过 N 次」+ 能力级次数), 底部提供「清除本机使用记录」(两段式确认),并说明数据不出本机。 顺带把「常用」延伸到导航与顶栏 - AppSidebar:模块按频次置顶 + 常用项左缘一道色条;排序只在进页面时算一次, 不在会话内实时跳动(否则"我刚点的那行跑哪去了"会打断操作)。 - App.vue:顶栏胶囊补「已帮你办成 N 件」——原来只回答"系统有多大", 现在同时回答"它为我做过多少事"。 埋点(Chat.vue) - 挂在每轮响应的 finally,只认回包里的 toolCalls(含浏览器代执行续跑后的最终结果), 不认"用户点了哪张卡":用户完全可以自己在输入框里打出「我要请假」, 只有后端的执行轨迹才是事实;失败时 traces 为空,自动不记。 铁律不变:所有入口仍由 /api/agent/tools 目录驱动,前端不自行判权。 「常用」只做排序不做增删;无权使用的能力即使局部画像里有记录,也不进清单。 验证:真机 46/46(.scratch/verify-usage-personalization.mjs,覆盖首次访问形态、 埋点计数口径、写操作才计件、常用前置与角标、侧栏顺序、清除记录、多账号隔离); 请假登记回归 17/17。
This commit is contained in:
@@ -4,7 +4,8 @@
|
||||
*
|
||||
* 承担三件事,让主区可以专心做对话:
|
||||
* 1. 品牌与身份 —— 一眼知道这是什么、当前是谁在用
|
||||
* 2. 能力导航 —— 7 个业务模块全部列出,点一下就等于发出一句完整请求
|
||||
* 2. 能力导航 —— 当前身份可用的模块全部列出(**常用的排在前面并带一道色条**),
|
||||
* 点一下就等于发出一句完整请求
|
||||
* 3. 使用提示 —— 常驻说明"信息不全也没关系",降低首次使用门槛
|
||||
*/
|
||||
import { computed, ref } from "vue";
|
||||
@@ -13,6 +14,7 @@ import { baseApi } from "@/api/module/agent";
|
||||
import conf from "@/assets/config/host.config";
|
||||
import { MODULE_LABEL, moduleMeta, orderedModuleKeys } from "@/assets/config/modules";
|
||||
import { beginLogoutGuard } from "@/utils/logout-guard";
|
||||
import { rankModuleKeys, usageScope, usageSnapshot } from "@/utils/usage";
|
||||
import type { AgentToolCatalog, UserInfoVo } from "@/types/agent";
|
||||
|
||||
const props = defineProps<{ catalog: AgentToolCatalog | null; user?: UserInfoVo | null }>();
|
||||
@@ -74,16 +76,26 @@ async function logout() {
|
||||
const active = ref<string>("");
|
||||
|
||||
/**
|
||||
* 侧栏只列出「当前身份真正可用」的模块。
|
||||
* 侧栏只列出「当前身份真正可用」的模块,并**按使用频次把常用项置顶**。
|
||||
*
|
||||
* 能力目录由后端按「角色 + 已授权模块」过滤后下发,学生 / 教师 / 管理员拿到的集合本就不同,
|
||||
* 因此这里直接以目录为准,而不是任何前端写死的名单 —— 既不会出现"点了就被拒"的假入口,
|
||||
* 也不会漏掉后端新上线、前端还没配元数据的模块。
|
||||
*
|
||||
* ★ 排序只在**目录到达时算一次**(`catalog` 变化才重算),不订阅画像变更:
|
||||
* 侧栏是导航而非展示位,如果用户点完一个模块它就当场跳到第一位,
|
||||
* "我刚点的那一行跑哪去了"会打断操作。下次进页面时它自然已经在前面。
|
||||
*/
|
||||
const modules = computed(() => {
|
||||
const mods = props.catalog?.modules || {};
|
||||
return orderedModuleKeys(mods).map(key => {
|
||||
const keyList = orderedModuleKeys(mods);
|
||||
const scope = usageScope(props.catalog);
|
||||
const keys = rankModuleKeys(scope, keyList);
|
||||
const heat = usageSnapshot(scope).modules;
|
||||
|
||||
return keys.map(key => {
|
||||
const meta = moduleMeta(key);
|
||||
const used = heat[key]?.count ?? 0;
|
||||
return {
|
||||
key,
|
||||
icon: meta.icon,
|
||||
@@ -91,6 +103,9 @@ const modules = computed(() => {
|
||||
sample: meta.sample,
|
||||
label: MODULE_LABEL[key] || key,
|
||||
count: mods[key].length,
|
||||
/** 用过 2 次以上算常用:左侧给一道色条,方便扫视时定位 */
|
||||
hot: used >= 2,
|
||||
used,
|
||||
};
|
||||
});
|
||||
});
|
||||
@@ -156,8 +171,12 @@ function pick(m: { key: string; sample: string }) {
|
||||
v-for="m in modules"
|
||||
:key="m.key"
|
||||
class="sb__item"
|
||||
:class="[`tone-${m.tone}`, { 'sb__item--on': active === m.key }]"
|
||||
:title="`对「${m.label}」说:${m.sample}`"
|
||||
:class="[`tone-${m.tone}`, { 'sb__item--on': active === m.key, 'sb__item--hot': m.hot }]"
|
||||
:title="
|
||||
m.hot
|
||||
? `你常办这个(用过 ${m.used} 次)|对「${m.label}」说:${m.sample}`
|
||||
: `对「${m.label}」说:${m.sample}`
|
||||
"
|
||||
@click="pick(m)"
|
||||
>
|
||||
<span class="sb__item-icon">{{ m.icon }}</span>
|
||||
@@ -383,6 +402,15 @@ function pick(m: { key: string; sample: string }) {
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
/* 常用模块:左缘一道亮色条(深色侧栏上比文字标记更安静,也不会挤掉模块名) */
|
||||
.sb__item--hot {
|
||||
box-shadow: inset 2px 0 0 var(--tone-light, #93c5fd);
|
||||
}
|
||||
|
||||
.sb__item--hot.sb__item--on {
|
||||
box-shadow: inset 2px 0 0 var(--tone-light, #93c5fd), inset 0 0 0 1px rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.sb__item-icon {
|
||||
flex: none;
|
||||
width: 25px;
|
||||
|
||||
Reference in New Issue
Block a user