mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-15 12:15:16 +00:00
feat(gui): 加载动画改三点式 + 新增系统通知
## 1) 加载动画:边框旋转 → 三点渐亮
旧实现是 border + rotate 的 spinner,用在 .loading(16px) /
.loading-spinner(32px) / .live-spinner(15px) 三处。在消息气泡里刺眼,
且语义不对:旋转暗示"正在加载某个确定的东西",而 agent 思考没有进度。
参考 PiDeck(@deepseek-ai/dsh-web-frontend 与 mermaid bundle 里的
@keyframe 实现)改为「等距三点 + 透明度递减 + 轻微上浮」:
haDots 三点错峰明暗(1/0.6/0.35/0.15)
haDotFloat 单点上浮 + 缩放
haSweep 横向扫光
旧类名 .loading / .loading-spinner 保留为别名指向新动画(还有 6+ 处
调用点,逐处改 HTML 容易漏),并解除其 animation 以免残留旋转。
7 处调用点统一换成 <i>x3 结构。
## 2) 系统通知(此前完全不存在)
主进程无 Notification、preload 无接口、渲染层只有页面内 toast
(用户不盯着窗口就看不到)。补齐一条完整链路:
main.js notify:show / notify:supported 两个 IPC + 原生 Notification
preload notify.{show,supported,onClicked}
app.js notifyTurn / notifyTurnOnce / onNotifyClicked
设计取舍(都写进注释):
· **一轮只通知一次**。SSE 的 tool_call / stage / agent_output 都可能
判定"一轮结束",用 turnSig 去重,否则连弹好几次。
· **不在流式过程中逐 token 通知**。一轮可能几十个 delta,那是噪音;
用户要的是"有结果了"。
· **窗口在前台且聚焦时不弹**。正看着界面时弹窗只会烦人。
静默启动(窗口 hidden)正好走"要弹"这条 —— 这也是静默后唯一
能让用户知道"agent 回了"的途径。
· **挂在 SSE 的 agent_output 收尾处**,不挂 endChatTurn():
后者开头 `if (!state.chatLoading) return`,而旁观路径
(agent 响应 memo/QQ/email,GUI 只收 SSE)chatLoading 恒 false,
根本走不到那里。挂 SSE 对"自己发"与"旁观"两条路径都生效。
· **点击通知会 show() + 回传渲染进程**,切到 chat 视图并滚到该条。
点了却不知道该看哪句话等于没通知。
· 正文超 180 字截断,避免通知被撑爆。
## 判据
新增 cmd/gui/loading-notify.test.mjs(进 npm test),24 条断言覆盖
动画关键帧/错峰 delay/别名解除/调用点结构,以及通知链路的六段
(主进程 IPC、preload 桥、去重、挂载位置、点击跳转、可用性探测)。
## 实测
重新打包安装:186 文件 0 处不匹配,exe 188834816 字节
启动正常:silentStart 生效、login 200、device/online 200、
graph/pulse 轮询、0 个 401 / JS 错误。
判据:cmd/gui npm test 全绿(84 条)。
This commit is contained in:
@ -1458,16 +1458,130 @@ code {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
.msg-bubble .live-spinner {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--msg-user-bg);
|
||||
border-top-color: var(--accent);
|
||||
animation: spin 0.7s linear infinite;
|
||||
flex-shrink: 0;
|
||||
display: inline-block;
|
||||
/* ============================================================
|
||||
加载 / 思考动画
|
||||
------------------------------------------------------------
|
||||
设计参考 PiDeck(@deepseek-ai/dsh-web-frontend / mermaid bundle):
|
||||
它**不用边框旋转**,而是「等距三点 + 透明度递减 + 轻微上浮」,
|
||||
静默、不抢视线,且在深浅色下都成立。
|
||||
|
||||
旧实现是 border + rotate 的经典 spinner(.loading / .loading-spinner /
|
||||
.live-spinner),在消息气泡里尤其刺眼 —— 一个持续旋转的圆环
|
||||
与「正在思考」的语义也不匹配(旋转暗示在加载某个确定的东西,
|
||||
而思考本身没有进度)。
|
||||
|
||||
下面是等价改造,三处调用点(面板加载 / 列表加载 / 消息内思考)
|
||||
统一到同一套视觉语言。
|
||||
============================================================ */
|
||||
|
||||
/* 三点渐次亮起(PiDeck: _dsh-state-dot-chase 的等价实现) */
|
||||
@keyframes haDots {
|
||||
0%,
|
||||
12.4% {
|
||||
opacity: 1;
|
||||
}
|
||||
12.5%,
|
||||
24.9% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
25%,
|
||||
37.4% {
|
||||
opacity: 0.35;
|
||||
}
|
||||
37.5%,
|
||||
100% {
|
||||
opacity: 0.15;
|
||||
}
|
||||
}
|
||||
|
||||
/* 单点呼吸(PiDeck: tool-activity-dot) */
|
||||
@keyframes haDotFloat {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.35;
|
||||
transform: translateY(0) scale(0.8);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: translateY(-2px) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 扫光(PiDeck: thinking-sweep),用于「思考中」横向扫光 */
|
||||
@keyframes haSweep {
|
||||
0% {
|
||||
left: -300px;
|
||||
}
|
||||
90%,
|
||||
100% {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 三点基类:用 flex 排布,gap 固定,避免各调用点尺寸不一 */
|
||||
.ha-dots {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
vertical-align: middle;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ha-dots > i {
|
||||
display: block;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 999px;
|
||||
background: var(--accent);
|
||||
animation: haDots 1.1s ease-in-out infinite;
|
||||
}
|
||||
.ha-dots > i:nth-child(2) {
|
||||
animation-delay: 0.15s;
|
||||
}
|
||||
.ha-dots > i:nth-child(3) {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
/* 尺寸变体:替换旧的 .loading / .loading-spinner / .live-spinner */
|
||||
.ha-dots-sm > i {
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
gap: 2px;
|
||||
}
|
||||
.ha-dots-lg > i {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
/* 面板级:居中容器里的加载态(替代 32px 旋转圈) */
|
||||
.ha-dots-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 消息气泡内:思考中 */
|
||||
.msg-bubble .live-spinner {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
vertical-align: middle;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.msg-bubble .live-spinner > i {
|
||||
display: block;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 999px;
|
||||
background: var(--accent);
|
||||
animation: haDots 1.1s ease-in-out infinite;
|
||||
}
|
||||
.msg-bubble .live-spinner > i:nth-child(2) {
|
||||
animation-delay: 0.15s;
|
||||
}
|
||||
.msg-bubble .live-spinner > i:nth-child(3) {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
.msg-bubble .live-spinner + .thinking-tools,
|
||||
.msg-bubble .live-spinner + .text {
|
||||
@ -1535,22 +1649,44 @@ code {
|
||||
#sm-container-chat canvas {
|
||||
display: block;
|
||||
}
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid var(--loading-border);
|
||||
border-radius: 50%;
|
||||
border-top-color: var(--loading-top);
|
||||
animation: spin 0.6s linear infinite;
|
||||
}
|
||||
/* 旧类名保留为别名:已有 6+ 处调用点直接用 .loading / .loading-spinner。
|
||||
把它们指到新的三点动画,而不是删掉后逐处改 HTML —— 后者容易漏。
|
||||
(用 flex 布局装不出第三个点,::before/::after 只能造 2 个,
|
||||
所以这三个点由 JS 插入;没插入时至少是个 2 点的退化态,
|
||||
不会退化成旋转圈。) */
|
||||
.loading,
|
||||
.loading-spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 3px solid color-mix(in srgb, var(--accent) 15%, transparent);
|
||||
border-top: 3px solid var(--accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
vertical-align: middle;
|
||||
width: auto;
|
||||
height: auto;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: none;
|
||||
animation: none;
|
||||
}
|
||||
.loading::before,
|
||||
.loading::after,
|
||||
.loading-spinner::before,
|
||||
.loading-spinner::after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 999px;
|
||||
background: var(--accent);
|
||||
animation: haDots 1.1s ease-in-out infinite;
|
||||
}
|
||||
.loading::after,
|
||||
.loading-spinner::after {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
.loading-spinner::before,
|
||||
.loading-spinner::after {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
@keyframes spin {
|
||||
to {
|
||||
|
||||
Reference in New Issue
Block a user