mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-22 18:08:04 +00:00
feat(webui): 阶段管道的工具格改为滚动展示最新一条调用
「工具」是循环格:一轮里可能调几十次工具/输出通道。此前每一次都追加成 chip,这一格被撑成一长条,反而看不出「现在在调什么」。改为固定一行的 滚动视口——只留最新一条,右侧给出本轮累计次数;新调用到来时旧条向上 滚出、新条滑入(morph 就地改文本不会重放 CSS 动画,故摘类 + 强制 reflow + 重加类)。并给 chip 名称加 .rt-chip-t 承接省略号,窄框不再硬切半截。 配套 TestStagePipelineToolCellShowsLatestOnly 钉住该口径。
This commit is contained in:
@ -2767,6 +2767,44 @@
|
||||
gap: 4px;
|
||||
min-height: 23px;
|
||||
}
|
||||
/* 「工具」格的滚动视口:固定一行高,只露最新一条。
|
||||
工具调用一轮内可能几十次,全部追加会把这一格撑成长条,反而看不出
|
||||
「现在在调什么」;这里让旧条向上滚出视口,留下最新那条。 */
|
||||
.rt-pipe-scroll {
|
||||
flex-wrap: nowrap;
|
||||
height: 23px;
|
||||
min-height: 23px;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
}
|
||||
/* 名称可缩、计数不可缩:窄框里先截断工具名,累计次数始终看得见。 */
|
||||
.rt-pipe-scroll .rt-chip {
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
}
|
||||
/* rtFlashLatestTool 每来一条新调用就重加一次,重置动画时间轴。 */
|
||||
.rt-chip-enter {
|
||||
animation: rt-chip-scroll-in 0.3s var(--ease-out);
|
||||
}
|
||||
@keyframes rt-chip-scroll-in {
|
||||
from {
|
||||
transform: translateY(115%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.rt-scroll-count {
|
||||
flex: 0 0 auto;
|
||||
margin-left: auto;
|
||||
font-style: normal;
|
||||
font-size: 10.5px;
|
||||
opacity: 0.6;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
/* 拓扑光点:沿 animateMotion 给的路径跑;不吃鼠标事件,跑完由 JS 摘掉。 */
|
||||
.rt-spark {
|
||||
pointer-events: none;
|
||||
@ -2923,6 +2961,14 @@
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.rt-chip-t {
|
||||
/* font-weight 继承,不因为换成 <b> 而变粗;省略号靠它生效 */
|
||||
font-weight: inherit;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.rt-chip-tool {
|
||||
border-color: rgba(136, 192, 208, 0.42);
|
||||
color: var(--frost-300);
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
pipelinePhase: "", // 当前阶段(SSE stage 事件驱动总览页)
|
||||
pipelineTimer: null,
|
||||
stageTrail: [], // 本轮已发生的事件轨迹(工具/输出调用,供阶段管道展示循环)
|
||||
toolFlash: false, // 本轮新到一条工具调用:本轮渲染后给「工具」格放一次滑入动画
|
||||
healthResult: null,
|
||||
starmapInit: false,
|
||||
starmapLoading: false,
|
||||
@ -841,6 +842,19 @@
|
||||
if (d > n) out += '<b class="rt-slots-more">+' + (d - n) + "</b>";
|
||||
return out + "</span>";
|
||||
}
|
||||
// rtChipEl 画一枚事件 chip。工具与输出通道调用各有配色;名称包进
|
||||
// .rt-chip-t,让窄框里由它做省略号截断(.rt-chip 本身是 flex 容器,
|
||||
// text-overflow 对直接子文本不生效,会被硬切掉半截)。
|
||||
function rtChipEl(t) {
|
||||
var kind = t.kind || "stage";
|
||||
var ico = kind === "output" ? RT_ICO.out : kind === "tool" ? RT_ICO.tool : "";
|
||||
return (
|
||||
'<span class="rt-chip rt-chip-' + kind + '" title="' + escHtml(t.label) + '">' + ico +
|
||||
'<b class="rt-chip-t">' + escHtml(t.short || t.label) + "</b>" +
|
||||
(t.n > 1 ? '<i class="rt-chip-n">x' + t.n + "</i>" : "") +
|
||||
"</span>"
|
||||
);
|
||||
}
|
||||
// rtPipelineHtml 画阶段管道:**五个等大的表框**,每框里是本阶段本轮发生的事。
|
||||
//
|
||||
// 为什么不是「小圆点 + 连接线 + 9px 小字」:那种画法在总览里几乎读不出
|
||||
@ -853,22 +867,25 @@
|
||||
var items = (trail || []).filter(function (t) {
|
||||
return (t.g | 0) === i;
|
||||
});
|
||||
var body = items.length
|
||||
? items
|
||||
.map(function (t) {
|
||||
var kind = t.kind || "stage";
|
||||
var ico =
|
||||
kind === "output" ? RT_ICO.out : kind === "tool" ? RT_ICO.tool : "";
|
||||
return (
|
||||
'<span class="rt-chip rt-chip-' + kind + '" title="' +
|
||||
escHtml(t.label) + '">' + ico +
|
||||
escHtml(t.short || t.label) +
|
||||
(t.n > 1 ? '<i class="rt-chip-n">x' + t.n + "</i>" : "") +
|
||||
"</span>"
|
||||
);
|
||||
})
|
||||
.join("")
|
||||
: '<span class="rt-chip rt-chip-none">' + __("无", "none") + "</span>";
|
||||
// 「工具」格是**循环**格:一轮里可能调几十次工具/输出通道。把每一次都
|
||||
// 追加成 chip,这格会被撑成一长条,读者反而看不出「现在正在调什么」。
|
||||
// 所以它只保留**最新一条**,旧条在同一行视口里向上滚走
|
||||
// (rtFlashLatestTool 触发滑入),右侧另给本轮累计次数。
|
||||
var cls = "rt-pipe-events";
|
||||
var body;
|
||||
if (!items.length) {
|
||||
body = '<span class="rt-chip rt-chip-none">' + __("无", "none") + "</span>";
|
||||
} else if (s.loop) {
|
||||
cls += " rt-pipe-scroll";
|
||||
var total = 0;
|
||||
for (var k = 0; k < items.length; k++) total += items[k].n || 1;
|
||||
body =
|
||||
rtChipEl(items[items.length - 1]) +
|
||||
'<i class="rt-scroll-count" title="' +
|
||||
__("本轮工具调用累计次数", "tool calls this turn") + '">x' + total + "</i>";
|
||||
} else {
|
||||
body = items.map(rtChipEl).join("");
|
||||
}
|
||||
return (
|
||||
'<div class="rt-pipe-cell' + (i === g ? " active" : "") + '">' +
|
||||
'<div class="rt-pipe-head">' + RT_ICO[s.ico] +
|
||||
@ -879,7 +896,7 @@
|
||||
'">' + RT_ICO.loop + "</em>"
|
||||
: "") +
|
||||
"</div>" +
|
||||
'<div class="rt-pipe-events">' + body + "</div>" +
|
||||
'<div class="' + cls + '">' + body + "</div>" +
|
||||
"</div>"
|
||||
);
|
||||
}).join("");
|
||||
@ -906,6 +923,21 @@
|
||||
if (arr.length > 24) arr.shift();
|
||||
}
|
||||
|
||||
// rtFlashLatestTool 让「工具」格里最新那条做一次「向上滚入」。
|
||||
//
|
||||
// morph 是就地改文本:新工具到来时 chip 节点不会被替换,纯 CSS 的
|
||||
// animation 因此不会自动重放。这里摘类 → 强制 reflow → 重加类,重置动画
|
||||
// 时间轴。prefers-reduced-motion 由样式表统一压到 0.01ms,无需在此判断。
|
||||
function rtFlashLatestTool() {
|
||||
var box = document.querySelector(".rt-pipe-events.rt-pipe-scroll");
|
||||
if (!box) return;
|
||||
var chip = box.querySelector(".rt-chip");
|
||||
if (!chip) return;
|
||||
chip.classList.remove("rt-chip-enter");
|
||||
void chip.offsetWidth; // 强制 reflow:少了这句,重加类不会重启动画
|
||||
chip.classList.add("rt-chip-enter");
|
||||
}
|
||||
|
||||
// ---- per-agent 负载 ----
|
||||
//
|
||||
// 负载 = 该 agent **自己的**调度器积压折算成的百分比。
|
||||
@ -3686,6 +3718,7 @@
|
||||
} else if (phase === "before_toolcall" && tool) {
|
||||
var isOut = tool.indexOf("output_") === 0;
|
||||
rtTrailPush(2, isOut ? "output" : "tool", tool, rtShortTool(tool));
|
||||
state.toolFlash = true;
|
||||
} else if (phase === "before_output") {
|
||||
rtTrailPush(3, "stage", __("生成回复", "generate reply"), __("生成", "gen"));
|
||||
} else if (phase === "after_output") {
|
||||
@ -3698,6 +3731,11 @@
|
||||
if (document.getElementById("rt-sec-pipe")) renderRuntime();
|
||||
}, 2500);
|
||||
if (document.getElementById("rt-sec-pipe")) renderRuntime();
|
||||
// 渲染后再放动画:morph 已经把 chip 文本更新成最新那条。
|
||||
if (state.toolFlash) {
|
||||
state.toolFlash = false;
|
||||
rtFlashLatestTool();
|
||||
}
|
||||
if (phase === "pre_action") {
|
||||
state.chatStage = __("AI 思考中...", "AI thinking...");
|
||||
} else if (phase === "before_toolcall") {
|
||||
|
||||
@ -1339,6 +1339,40 @@ func TestDashboardAssetsSplit(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestStagePipelineToolCellShowsLatestOnly 钉住总览「阶段管道」里工具格的展示口径:
|
||||
// 只显示**最新一条**工具/输出调用(单行视口 + 滑入动画),而不是把本轮每一次
|
||||
// 都追加成 chip —— 后者在几十次工具调用的一轮里会把这一格撑成长条,
|
||||
// 反而看不出「现在正在调什么」。
|
||||
//
|
||||
// 前端没有 JS 测试运行器,这里按仓库既有做法(见 TestDashboardAssetsSplit)
|
||||
// 直接对产物做字符串钉桩:若日后有人把这条口径改回「不断追加」,用例会红。
|
||||
func TestStagePipelineToolCellShowsLatestOnly(t *testing.T) {
|
||||
js, err := dashboardFS.ReadFile("dashboard.js")
|
||||
if err != nil {
|
||||
t.Fatalf("读 dashboard.js: %v", err)
|
||||
}
|
||||
css, err := dashboardFS.ReadFile("dashboard.css")
|
||||
if err != nil {
|
||||
t.Fatalf("读 dashboard.css: %v", err)
|
||||
}
|
||||
sjs, scss := string(js), string(css)
|
||||
for _, want := range []string{
|
||||
"items[items.length - 1]", // 工具格只取末条(最新)
|
||||
"rt-pipe-scroll", // 单行滚动视口
|
||||
"rtFlashLatestTool", // 新调用到来时重放滑入动画
|
||||
"rt-scroll-count", // 右侧本轮累计次数
|
||||
} {
|
||||
if !strings.Contains(sjs, want) {
|
||||
t.Errorf("dashboard.js 缺 %q:工具格应只滚动展示最新一条调用", want)
|
||||
}
|
||||
}
|
||||
for _, want := range []string{".rt-pipe-scroll", "@keyframes rt-chip-scroll-in"} {
|
||||
if !strings.Contains(scss, want) {
|
||||
t.Errorf("dashboard.css 缺 %q:工具格的滚动视口/滑入动画样式", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestChatHistoryDefaultIsPaged 钉住 /chat/history 的默认页大小。
|
||||
//
|
||||
// 原先缺省 limit=0 表示"不限制",于是任何不带 limit 的调用每次都拿到完整聊天记录
|
||||
|
||||
Reference in New Issue
Block a user