feat(web): dev SPA 加 9 处克制入场微动效(纯 CSS + 一处极小 JS)
保留现有红主色 / VS Code 三栏审美不改风格,只补低风险微动效: - 消息气泡 .msg 淡入+上滑(批量加载退化为柔和集体淡入) - 4 个 .modal 卡片 scale 弹入 + 遮罩淡入 - 全局 button:active 下压 1px - 进度 dock / 上传 toast 顶部滑下淡入 - 下拉操作菜单 #floating-menu 从右上锚点弹出 - 拖拽 overlay #file-droparea 快淡入 - 拖拽文件放下 → 落点 pane-right 一次 drop-pulse 轻回弹(files.js 加 .drop-pulse + animationend once 自摘 + reflow 保证可重放) - 全部纳入 prefers-reduced-motion 守卫(spinner/blink 等功能动画保留) 刻意未做:进度块「打勾」逐步动画(dock.innerHTML 每 tick 全量重渲染, keyframe 会逐 tick 重放);复制 ✓ 闪(当前 SPA 无剪贴板复制功能,无触发点)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8b6e66b006
commit
815aeb81a9
|
|
@ -23,6 +23,7 @@
|
|||
|
||||
### 2026-06-10
|
||||
|
||||
- **dev SPA 加克制入场微动效(纯 CSS、单文件、可一键回退)**:`web/static/dev.html` 的 `<style>` 补 5 处低风险微动效,保留现有红主色/VS Code 三栏审美不改风格——① 消息气泡 `.msg` 入场淡入+上滑 6px(`msg-in .22s`,历史批量加载退化为一次柔和集体淡入,不抖);② 通用 4 个 modal 卡片 `modal-pop` scale .96→1 弹入 + 遮罩 `modal-fade` 淡入(此前 admin/chpw/src-picker/new-task/file-preview 直接 `display:flex` 无过渡);③ 全局 `button:active` 下压 1px(原仅登录主按钮有);④ `#task-progress-dock.show` 顶部 `dock-in` 滑下淡入;⑤ `@media (prefers-reduced-motion: reduce)` 守卫禁用上述入场动画(spinner/blink 等功能性动画保留)。**二轮补全剩余弹框**:⑥ 下拉操作菜单 `#floating-menu`(任务行 ⋯)`menu-in` 从右上锚点 scale+下落弹出(.14s,菜单要快);⑦ 拖拽上传 overlay `#file-droparea` 复用 `modal-fade` 快淡入;⑧ 上传进度 toast `.upload-status` 复用 `dock-in` 顶部滑下;⑨ 拖拽文件**放下时落点 `#pane-right` 一次 `drop-pulse` 轻回弹脉冲**(`files.js` drop 处理器加 `.drop-pulse` + `animationend` once 自摘 + reflow 保证可重放);上述均纳入 reduced-motion 守卫。**刻意未做**:进度块「打勾」逐步动画——`chat.js` 每 tick `dock.innerHTML` 全量重渲染,keyframe 会逐 tick 重放,故不加。**未做(已定)**:「复制成功 ✓ 闪」——查实当前 SPA **无剪贴板复制功能**(`navigator.clipboard`/`writeText` 零处,"复制"按钮全是文件复制到目录且成功即关弹框),无现成触发点;加复制按钮属"加功能"非"微动效",用户决定先不加,故未保留 `.copied` 死 CSS,以后真加复制入口再接。前端测试 `frontend_task_progress.test.mjs` 仍过。
|
||||
- **上下文压缩加"压力门槛":体量未逼近上限前不压缩(护缓存 + 不丢旧细节)**:此前 `loop` 每轮无条件压缩,短任务也把旧 tool 结果砍掉——既白丢信息(context 预算还很空),又因压缩边界逐轮滑动破 DeepSeek 前缀缓存。落地 `prepare_messages_with_stats(compact_threshold_chars=...)`:总 chars 未超阈值则**完全跳过压缩、原样发**(短任务前缀逐轮字节一致、缓存全程命中);超阈值才走原压缩逻辑。loop 按 `caps.reliable_context × 0.5 × 2.5(char/token 粗折算)` 算阈值(flash ≈ 33 万 chars),`_COMPACT_CONTEXT_RATIO/_CHARS_PER_TOKEN` 两常量可调。`compaction_skipped` 进 stats/`llm_start` 事件可观测。默认阈值 0 = 永远压缩(向后兼容)。背景:实测 task `b27466a0` DeepSeek 缓存命中已 92-94%、滑动边界损失有限(压缩函数确定性、旧消息压缩态稳定),故只补门槛、不改边界为阶梯式(收益仅再抬几个点不值复杂度)。新增 2 测试(below/above 门槛),全量 105 过。
|
||||
- **单轮停机判据从"步数"解耦为"是否在推进":`max_iterations` 升为纯 backstop + 新增全局「无进展」熔断 + 撞顶明确提示**:DB 诊断 task `b27466a0`(智能体介绍 PPT)所谓"中途断了"——查实=该 run 跑满 `max_iterations`(flash 旧值 50)后 `return "[reached max iterations]"` 干净停下、留一条悬空 tool 结果,用户离开 25min 回来打"继续"才续完(`run_status=idle/run_error=None`,非崩溃);"步骤太长"=少数轮 DeepSeek API 延迟 126-185s,工具本身全 <13s;顺带实测该 task DeepSeek 前缀缓存命中 92-94%,**上下文压缩对缓存几乎无害**(压缩函数确定性→旧消息压缩态稳定,只滑动边界这一处断,每轮 miss 几十~几百 token)。**洞察**:`max_iterations` 把"用户感知的轮(来回对话)"和"一轮内自主工作步数"混在一个旋钮上——自主 tool 链概念上是 1 轮,该松;真正要掐的是"空转"。落地:① yaml `max_iterations` flash 50→120 / pro 100→150,dataclass 默认 50→120,定位为安全兜底非"轮"预算;② `_RepeatGuard.record` 多返一个 `productive`(净产出=非 `[Error]` 且非一字不差重复);③ `_execute_tool_call` 三个返回点都带 `productive`(invalid-JSON/被拦=False);④ run loop 累计 `self._stall`——整步所有 tool 都无净产出则 +1、任一净产出清零,连续 `_STALL_LIMIT=8` 步空转主动停(`[stopped: no progress]`),比撞 120 早得多掐死循环,配 `_RepeatGuard` 逐指纹 HARD=4 双保险;⑤ 撞 backstop / 熔断都 emit 明确"回复『继续』可接着跑"提示,不再静默停。`tests/test_loop_repeat_guard.py` 更新 record 解包调用 + 加 `productive` 信号用例(17 例过,全量 103 过)。
|
||||
- **`systemctl restart` 优雅 drain in-flight run(单实例止血,不再误标 error)**:此前 restart 硬杀 BG run 线程,下次启动 reaper 把所有 `running/cancelling` 标 `error: server restarted before run finished` —— 用户一多就不能随便重启。落地纯进程内、**零 DB 改动**:① lifespan 加 `app.state.draining`(asyncio.Event)+ `app.state.inflight`(`{asyncio.Task: task_id}`,顺手修 `create_task` 不留引用可能被 GC 的旧坑);② POST `/messages` 起 run 时登记+done 回调自摘除,draining 置位时返 503+`Retry-After`;③ lifespan `finally` 先置 draining 拒新 run,`asyncio.wait(inflight, drain_timeout)` 等收尾,超时的 `broker.request_cancel` 转协作式 cancel(下个 chunk 间隙退、标 idle 不报 error),再过 `cancel_grace` 仍没退的留给 SIGKILL(最坏退化=改前)。④ `main.py` uvicorn 加 `timeout_graceful_shutdown=5`(否则长连 SSE 无限挡在 drain 前);⑤ `config/agent.yaml` 加 `shutdown` 段(drain_timeout 30s / cancel_grace 15s,超时转 cancel = 用户按停止可重发,故偏短);⑥ dev SPA `chat.js` 发送包退避重试(503 背压 + 交接拒连 TypeError 都重试 ~26s,显"服务更新中",耗尽贴友好提示)。**部署强耦合**:unit `TimeoutStopSec` 从 10 提到 90(必须 > drain+grace+sandbox 清扫余量,否则 SIGKILL 砍掉 drain),已写进 RUN.md unit + 故障兜底。B 蓝绿(零 503 窗口)留作触发信号后再做,前置是 instance-aware reaper(§7.8)。
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@
|
|||
transition: var(--t);
|
||||
}
|
||||
button:hover:not(:disabled) { background: var(--hover); }
|
||||
button:active:not(:disabled) { transform: translateY(1px); }
|
||||
button:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
button.primary { background: var(--accent); color: #fff; border-color: var(--accent); }
|
||||
button.primary:hover { background: var(--accent); filter: brightness(1.08); }
|
||||
|
|
@ -74,7 +75,8 @@
|
|||
position: fixed; inset: 0; background: rgba(0,0,0,0.4);
|
||||
display: none; align-items: center; justify-content: center;
|
||||
}
|
||||
.modal.show { display: flex; }
|
||||
.modal.show { display: flex; animation: modal-fade .18s ease-out; }
|
||||
.modal.show > .card { animation: modal-pop .22s cubic-bezier(.2,.7,.2,1); }
|
||||
.modal > .card {
|
||||
background: var(--panel); border-radius: var(--r-lg);
|
||||
box-shadow: var(--shadow-card);
|
||||
|
|
@ -311,7 +313,7 @@
|
|||
box-shadow: 0 4px 14px rgba(0,0,0,0.12);
|
||||
z-index: 60; padding: 4px 0;
|
||||
}
|
||||
#floating-menu.show { display: block; }
|
||||
#floating-menu.show { display: block; transform-origin: top right; animation: menu-in .14s cubic-bezier(.2,.7,.2,1); }
|
||||
.dd-item {
|
||||
display: block; width: 100%; text-align: left;
|
||||
padding: 6px 14px; font-size: 13px; line-height: 1.4;
|
||||
|
|
@ -370,7 +372,7 @@
|
|||
display: flex; flex-direction: column; gap: 8px;
|
||||
min-height: 0; /* 允许在 flex 容器里收缩 + 触发自身滚动 */
|
||||
}
|
||||
.msg { border: 1px solid var(--border); border-radius: var(--r-md); padding: 8px 12px; max-width: 92%; }
|
||||
.msg { border: 1px solid var(--border); border-radius: var(--r-md); padding: 8px 12px; max-width: 92%; animation: msg-in .22s cubic-bezier(.2,.7,.2,1); }
|
||||
.msg.user { background: var(--user-bg); align-self: flex-end; }
|
||||
.msg.assistant, .msg.system, .msg.tool, .msg.error { background: var(--asst-bg); align-self: flex-start; }
|
||||
.msg.error { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); }
|
||||
|
|
@ -480,7 +482,7 @@
|
|||
padding: 8px 12px; border-bottom: 1px solid var(--border);
|
||||
background: #fff;
|
||||
}
|
||||
#task-progress-dock.show { display: block; }
|
||||
#task-progress-dock.show { display: block; animation: dock-in .2s ease-out; }
|
||||
#task-progress-dock .task-progress {
|
||||
margin-top: 0; border-color: rgba(192,57,43,0.22);
|
||||
background: linear-gradient(180deg, #fff, #fffafa);
|
||||
|
|
@ -573,12 +575,12 @@
|
|||
color: var(--accent); font-size: 14px; font-weight: 500;
|
||||
z-index: 10;
|
||||
}
|
||||
#file-droparea.show { display: flex; }
|
||||
#file-droparea.show { display: flex; animation: modal-fade .14s ease-out; }
|
||||
.upload-status {
|
||||
display: none; padding: 6px 12px; border-bottom: 1px solid var(--border-soft);
|
||||
background: #fff; color: var(--muted); font-size: 12px;
|
||||
}
|
||||
.upload-status.show { display: block; }
|
||||
.upload-status.show { display: block; animation: dock-in .18s ease-out; }
|
||||
.upload-status .bar {
|
||||
height: 4px; margin-top: 4px; background: var(--border-soft);
|
||||
border-radius: 999px; overflow: hidden;
|
||||
|
|
@ -872,6 +874,23 @@
|
|||
border: 2px solid var(--border); border-top-color: var(--accent);
|
||||
animation: embed-spin .8s linear infinite;
|
||||
}
|
||||
/* ───── 入场微动效(克制:仅淡入/轻位移,不影响交互速度)───── */
|
||||
@keyframes msg-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
|
||||
@keyframes modal-fade{ from { opacity: 0; } to { opacity: 1; } }
|
||||
@keyframes modal-pop { from { opacity: 0; transform: scale(.96); } to { opacity: 1; transform: none; } }
|
||||
@keyframes dock-in { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }
|
||||
@keyframes menu-in { from { opacity: 0; transform: translateY(-4px) scale(.97); } to { opacity: 1; transform: none; } }
|
||||
/* 拖拽放下:落点区域一次轻回弹脉冲(JS 加 .drop-pulse,动画结束自摘) */
|
||||
@keyframes drop-pulse { 0% { box-shadow: inset 0 0 0 2px var(--accent); } 60% { box-shadow: inset 0 0 0 2px transparent; } 100% { box-shadow: inset 0 0 0 0 transparent; } }
|
||||
.drop-pulse { animation: drop-pulse .4s ease-out; }
|
||||
/* 系统开启"减弱动态效果"时,禁用入场动画(spinner/blink 等功能性动画保留) */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.msg, .modal.show, .modal.show > .card, #task-progress-dock.show,
|
||||
#login .card, #login .tab-body.active,
|
||||
#floating-menu.show, #file-droparea.show, .upload-status.show,
|
||||
.drop-pulse { animation: none !important; }
|
||||
button:active:not(:disabled) { transform: none; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -172,6 +172,12 @@ $("pane-right").addEventListener("drop", async (e) => {
|
|||
$("file-droparea").classList.remove("show");
|
||||
const files = Array.from(e.dataTransfer.files || []);
|
||||
if (!files.length) return;
|
||||
// 落点轻回弹脉冲:一次性,动画结束自摘(避免再次拖入不触发)
|
||||
const pane = $("pane-right");
|
||||
pane.classList.remove("drop-pulse");
|
||||
void pane.offsetWidth; // 强制 reflow 让动画可重放
|
||||
pane.classList.add("drop-pulse");
|
||||
pane.addEventListener("animationend", () => pane.classList.remove("drop-pulse"), { once: true });
|
||||
await uploadFilesWithPaneStatus(files);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue