diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c63038..4101be6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ ## Unreleased +- 对话两侧的软件作业与轮次导航改为更紧凑的一致样式,并增加常显的名称和数量;既可悬停速览,也可点击固定展开,状态和当前位置更清楚。 + - DeepSeek Flash 的思考过程恢复实时显示,不再等到正文开始后才集中出现。 - Windows 专业软件节点启用完整的新管理界面,节点状态、首次注册、软件环境、本机任务、ANSYS 验收和数据目录迁移均可直接管理;应用图标、导航、状态提示和窄窗口布局也更清晰,关闭窗口后仍会继续在托盘运行。 diff --git a/tests/test_static_vendor.py b/tests/test_static_vendor.py index 8c16fd0..020f8bd 100644 --- a/tests/test_static_vendor.py +++ b/tests/test_static_vendor.py @@ -241,6 +241,10 @@ class StaticVendorTests(unittest.TestCase): self.assertNotIn('
科研助手
', frontend) self.assertIn(".tool-call.activity > summary", css) self.assertIn("#task-progress-dock .task-progress", css) + self.assertIn(".conversation-rail-caption", html) + self.assertIn("#msg-outline-rail.pinned .ol-label", html) + self.assertIn("点击固定展开或收起对话导航", frontend) + self.assertIn("点击固定展开或收起软件作业", frontend) def test_frontend_surfaces_context_and_cache_stats(self) -> None: src = _frontend_source() diff --git a/web/static/dev.html b/web/static/dev.html index b4e45fc..8a54abe 100644 --- a/web/static/dev.html +++ b/web/static/dev.html @@ -843,56 +843,94 @@ 0%, 100% { box-shadow: none; } 18% { box-shadow: 0 0 0 2px var(--accent), 0 0 0 6px var(--accent-soft); } } - /* 消息目录:悬浮在对话右缘的圆点轨道。容器 pointer-events:none 让圆点间隙漏给 - 滚动条/正文(不挡滚动),只有圆点本身可点;hover 圆点整列展开标题(ChatGPT 式)。 */ + /* 对话两侧索引轨共用紧凑视觉:收起时只占一条窄边轨,hover / 键盘聚焦后 + 展开可读标签。轨道本身不拦截正文滚动,只有条目可交互。 */ #msg-outline-rail { - position: absolute; right: 4px; top: 50%; transform: translateY(-50%); + position: absolute; right: 6px; top: 50%; transform: translateY(-50%); max-height: 72%; display: flex; flex-direction: column; align-items: flex-end; - gap: 7px; padding: 8px 4px; overflow-y: auto; overflow-x: hidden; z-index: 5; - pointer-events: none; scrollbar-width: none; + gap: 3px; padding: 5px 3px; overflow-y: auto; overflow-x: hidden; z-index: 5; + border: 1px solid rgba(225, 227, 229, .82); border-radius: 10px; + background: rgba(255, 255, 255, .84); box-shadow: 0 2px 10px rgba(24, 32, 41, .06); + backdrop-filter: blur(8px); pointer-events: none; scrollbar-width: none; + transition: border-color .15s ease, box-shadow .15s ease; + } + #msg-outline-rail:hover, #msg-outline-rail:focus-within { + border-color: var(--border); box-shadow: 0 6px 20px rgba(24, 32, 41, .10); } #msg-outline-rail::-webkit-scrollbar { display: none; } - .ol-dot { - display: flex; align-items: center; justify-content: flex-end; gap: 8px; - background: transparent; border: none; padding: 0; cursor: pointer; - max-width: 22px; pointer-events: auto; transition: max-width .18s ease; + .conversation-rail-caption { + display: inline-flex; align-items: center; gap: 4px; min-height: 20px; + padding: 2px 6px; border: 0; border-radius: 6px; background: var(--panel-muted); + color: var(--muted); font-size: 10px; font-weight: 600; line-height: 1; + white-space: nowrap; pointer-events: auto; } - #msg-outline-rail:hover .ol-dot { max-width: 240px; } + .conversation-rail-caption::after { + content: ""; width: 4px; height: 4px; border-right: 1.5px solid currentColor; + border-bottom: 1.5px solid currentColor; transform: rotate(45deg) translateY(-1px); + transition: transform .15s ease; + } + .conversation-rail-caption:hover, .conversation-rail-caption:focus-visible, + .pinned > .conversation-rail-caption { + color: var(--accent); background: var(--accent-soft); + } + .pinned > .conversation-rail-caption::after { + transform: rotate(225deg) translate(-1px, -1px); + } + .ol-dot { + display: flex; align-items: center; justify-content: flex-end; gap: 6px; + min-height: 16px; max-width: 18px; padding: 0; border: 0; border-radius: 7px; + background: transparent; cursor: pointer; pointer-events: auto; + transition: max-width .18s ease, background .15s ease; + } + #msg-outline-rail:hover .ol-dot, #msg-outline-rail:focus-within .ol-dot, + #msg-outline-rail.pinned .ol-dot { max-width: 246px; } .ol-dot .ol-num { display: none; } .ol-dot .ol-label { font-size: 12px; line-height: 1.3; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 0; opacity: 0; - background: var(--panel); border: 1px solid var(--border); border-radius: 10px; - padding: 2px 8px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); - transition: max-width .18s ease, opacity .18s ease; + padding: 1px 0; transition: max-width .18s ease, opacity .18s ease, padding .18s ease; + } + #msg-outline-rail:hover .ol-label, #msg-outline-rail:focus-within .ol-label, + #msg-outline-rail.pinned .ol-label { + max-width: 210px; opacity: 1; padding-left: 7px; } - #msg-outline-rail:hover .ol-label { max-width: 210px; opacity: 1; } .ol-dot::after { - content: ""; flex: none; width: 16px; height: 4px; border-radius: 3px; + content: ""; flex: none; width: 12px; height: 3px; border-radius: 3px; background: var(--border); transition: background .15s ease, width .15s ease; } - .ol-dot:hover::after { background: var(--muted); } - .ol-dot.active::after { background: var(--accent); width: 20px; } + .ol-dot:hover, .ol-dot:focus-visible { background: var(--panel-muted); } + .ol-dot:hover::after { background: #9aa0a6; } + .ol-dot.active .ol-label { color: var(--text-strong); font-weight: 600; } + .ol-dot.active::after { background: var(--accent); width: 16px; } /* 专业软件结果轨:与右侧消息目录对称,按 Job 独立堆叠。折叠态只露状态点, hover/focus 展开标题;结果与运行状态来自 software_jobs,不复制进聊天消息。 */ #software-result-rail { - position: absolute; left: 4px; top: 50%; transform: translateY(-50%); + position: absolute; left: 6px; top: 50%; transform: translateY(-50%); max-height: 72%; display: flex; flex-direction: column; align-items: flex-start; - gap: 7px; padding: 8px 4px; overflow-y: auto; overflow-x: hidden; z-index: 6; - pointer-events: none; scrollbar-width: none; + gap: 3px; padding: 5px 3px; overflow-y: auto; overflow-x: hidden; z-index: 6; + border: 1px solid rgba(225, 227, 229, .82); border-radius: 10px; + background: rgba(255, 255, 255, .84); box-shadow: 0 2px 10px rgba(24, 32, 41, .06); + backdrop-filter: blur(8px); pointer-events: none; scrollbar-width: none; + transition: border-color .15s ease, box-shadow .15s ease; + } + #software-result-rail:hover, #software-result-rail:focus-within { + border-color: var(--border); box-shadow: 0 6px 20px rgba(24, 32, 41, .10); } #software-result-rail::-webkit-scrollbar { display: none; } #mobile-software-results { display: none; } .sr-item { - display: flex; align-items: center; gap: 8px; max-width: 24px; min-height: 24px; - padding: 0; border: 0; background: transparent; color: var(--text); cursor: pointer; - pointer-events: auto; transition: max-width .18s ease; + display: flex; align-items: center; gap: 6px; max-width: 18px; min-height: 16px; + padding: 0; border: 0; border-radius: 7px; background: transparent; + color: var(--text); cursor: pointer; pointer-events: auto; + transition: max-width .18s ease, background .15s ease; } #software-result-rail:hover .sr-item, - #software-result-rail:focus-within .sr-item { max-width: 260px; } + #software-result-rail:focus-within .sr-item, + #software-result-rail.pinned .sr-item { max-width: 278px; } + .sr-item:hover, .sr-item:focus-visible { background: var(--panel-muted); } .sr-item::before { - content: ""; flex: none; width: 10px; height: 10px; margin-left: 3px; - border: 2px solid var(--panel); border-radius: 50%; background: var(--border); + content: ""; flex: none; width: 8px; height: 8px; margin-left: 2px; + border: 1.5px solid var(--panel); border-radius: 50%; background: var(--border); box-shadow: 0 0 0 1px var(--border); } .sr-item.running::before, .sr-item.queued::before, .sr-item.dispatched::before, @@ -905,18 +943,18 @@ .sr-item.cancelled::before { background: #8a929b; box-shadow: 0 0 0 1px #8a929b; } @keyframes sr-pulse { 50% { opacity: .45; transform: scale(.82); } } .sr-label { - min-width: 0; max-width: 0; opacity: 0; overflow: hidden; white-space: nowrap; - border: 1px solid transparent; border-radius: 10px; padding: 4px 0; - background: var(--panel); box-shadow: 0 2px 8px rgba(0,0,0,.08); + display: flex; align-items: baseline; gap: 6px; min-width: 0; max-width: 0; + opacity: 0; overflow: hidden; white-space: nowrap; padding: 1px 0; text-align: left; transition: max-width .18s ease, opacity .18s ease, padding .18s ease; } #software-result-rail:hover .sr-label, - #software-result-rail:focus-within .sr-label { - max-width: 228px; opacity: 1; padding: 4px 8px; border-color: var(--border); + #software-result-rail:focus-within .sr-label, + #software-result-rail.pinned .sr-label { + max-width: 250px; opacity: 1; padding-right: 7px; } - .sr-label strong, .sr-label small { display: block; overflow: hidden; text-overflow: ellipsis; } - .sr-label strong { font-size: 12px; font-weight: 600; } - .sr-label small { margin-top: 1px; color: var(--muted); font-size: 10px; } + .sr-label strong, .sr-label small { overflow: hidden; text-overflow: ellipsis; } + .sr-label strong { max-width: 142px; font-size: 12px; font-weight: 600; } + .sr-label small { max-width: 100px; color: var(--muted); font-size: 10px; } /* 阅读宽度:assistant/system/tool 限到 ~48rem(约 60-80 字/行,长文不至于满屏铺开难回扫); user 气泡更窄(36rem)。宽屏下提升可读性,窄屏 92% 仍生效(min 取小者) */ .msg { border: 1px solid var(--border); border-radius: var(--r-md); padding: 8px 12px; max-width: min(92%, 48rem); animation: msg-in .22s cubic-bezier(.2,.7,.2,1); scroll-margin-top: 16px; } diff --git a/web/static/js/chat.js b/web/static/js/chat.js index e43702d..c470fc4 100644 --- a/web/static/js/chat.js +++ b/web/static/js/chat.js @@ -1257,12 +1257,17 @@ function renderOutlineRail() { if (items.length < 2) { // 0/1 轮没必要显示目录 rail.style.display = "none"; rail.innerHTML = ""; + rail.classList.remove("pinned"); return; } rail.style.display = ""; - rail.innerHTML = items.map((it, i) => { + const pinned = rail.classList.contains("pinned"); + const caption = ``; + rail.innerHTML = caption + items.map((it, i) => { const label = it.snippet || `第 ${i + 1} 轮`; - return ``; }).join(""); @@ -1273,7 +1278,10 @@ function setActiveOutlineIdx(idx) { const rail = $("msg-outline-rail"); if (!rail) return; rail.querySelectorAll(".ol-dot").forEach((d) => { - d.classList.toggle("active", Number(d.dataset.idx) === Number(idx)); + const active = Number(d.dataset.idx) === Number(idx); + d.classList.toggle("active", active); + if (active) d.setAttribute("aria-current", "step"); + else d.removeAttribute("aria-current"); }); } @@ -1335,6 +1343,12 @@ $("chat-stream").addEventListener("scroll", () => { const _outlineRailEl = $("msg-outline-rail"); if (_outlineRailEl) { _outlineRailEl.addEventListener("click", (e) => { + const caption = e.target.closest(".conversation-rail-caption"); + if (caption) { + const pinned = _outlineRailEl.classList.toggle("pinned"); + caption.setAttribute("aria-expanded", String(pinned)); + return; + } const dot = e.target.closest(".ol-dot"); if (dot) jumpToMessage(Number(dot.dataset.idx)); }); diff --git a/web/static/js/software_jobs.js b/web/static/js/software_jobs.js index d02c5a2..46074b3 100644 --- a/web/static/js/software_jobs.js +++ b/web/static/js/software_jobs.js @@ -153,13 +153,18 @@ function renderTaskResultRail() { if (!state.taskId || taskJobsTaskId !== state.taskId || !taskJobs.length) { rail.innerHTML = ""; rail.style.display = "none"; + rail.classList.remove("pinned"); if (mobileResults) mobileResults.hidden = true; return; } - rail.innerHTML = taskJobs.map((job) => ` + const pinned = rail.classList.contains("pinned"); + rail.innerHTML = `` + + taskJobs.map((job) => ` `).join(""); @@ -168,6 +173,13 @@ function renderTaskResultRail() { mobileResults.hidden = false; mobileResults.textContent = `软件结果 ${taskJobs.length}`; } + const caption = rail.querySelector(".conversation-rail-caption"); + if (caption) { + caption.onclick = () => { + const next = rail.classList.toggle("pinned"); + caption.setAttribute("aria-expanded", String(next)); + }; + } rail.querySelectorAll("[data-result-job-id]").forEach((button) => { button.onclick = () => void openTaskResult(button.dataset.resultJobId); });