From 1c5150cc5f949e527f4d108eac1930bd7decca5f Mon Sep 17 00:00:00 2001 From: movcat Date: Wed, 23 Sep 2026 19:30:42 +0800 Subject: [PATCH 1/6] =?UTF-8?q?fix=20=E8=8A=AF=E7=89=87=E5=9C=A8=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E5=86=85=E5=AE=B9=E5=90=8E=E4=BC=9A=E7=9B=B4=E6=8E=A5?= =?UTF-8?q?=E6=B6=88=E5=A4=B1=E5=AF=BC=E8=87=B4=E6=97=A0=E6=B3=95=E6=AD=A3?= =?UTF-8?q?=E5=B8=B8=E8=A7=82=E7=9C=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 宿主 layout mutation 误杀了正在观看的面板 --- client.js | 40 +++++++++++++++++++++++------------- test/layout-overlay.test.mjs | 32 +++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 14 deletions(-) diff --git a/client.js b/client.js index ab37786..c9971a8 100644 --- a/client.js +++ b/client.js @@ -1712,7 +1712,7 @@ window.__ModuleLoader__.load({ render() renderMarkers() // 面板正在显示时同步重建:否则删掉的条目还留在面板里,必须重新 hover 才消失。 - if (tipLayer.childNodes.length > 0) showChipTip() + if (tipOwner === chipLayer) showChipTip() } function closeToolbar() { @@ -1734,6 +1734,21 @@ window.__ModuleLoader__.load({ var tipLayer = document.createElement('div') tipLayer.setAttribute('data-annotation-tip-layer', '') document.body.appendChild(tipLayer) + // tipLayer 是三类悬浮面板(输入框胶囊 / 气泡标签 / 回复芯片)共享的单例容器。 + // 记录当前面板的归属元素:任何清空都必须指名归属,无归属清空会误杀用户 + // 正在观看的面板——宿主 layout mutation 会高频触发 updateChip,面板刚显示 + // 就被抹掉(表现为「芯片显示内容后直接消失」)。 + var tipOwner = null + function clearTip(owner) { + if (owner !== undefined && tipOwner !== owner) return + tipLayer.textContent = '' + tipOwner = null + } + function presentTip(owner, el) { + tipLayer.textContent = '' + tipLayer.appendChild(el) + tipOwner = owner + } var observedComposer = null var composerObserver = typeof ResizeObserver === 'function' ? new ResizeObserver(onLayoutChange) @@ -1742,7 +1757,7 @@ window.__ModuleLoader__.load({ function updateChip() { if (ui.quotes.length === 0) { chipLayer.style.display = 'none' - tipLayer.textContent = '' + clearTip(chipLayer) return } chipLayer.textContent = '' @@ -1778,7 +1793,7 @@ window.__ModuleLoader__.load({ if (hoverGrace !== null) clearTimeout(hoverGrace) hoverGrace = setTimeout(function () { hoverGrace = null - tipLayer.textContent = '' + clearTip(chipLayer) }, 250) } function cancelHide() { @@ -1791,7 +1806,6 @@ window.__ModuleLoader__.load({ function showChipTip() { if (ui.quotes.length === 0) return - tipLayer.textContent = '' var el = document.createElement('div') el.className = 'dsh-ann-tip' el.style.cssText = 'position:fixed;z-index:1160;width:300px;max-width:calc(100vw - 16px);padding:10px 12px;border-radius:12px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu,#2c2c2e);box-shadow:var(--dsw-shadow-lv3);font-family:var(--dsw-font-family,system-ui);font-size:12px;color:var(--dsw-alias-label-primary);' @@ -1830,7 +1844,7 @@ window.__ModuleLoader__.load({ item.appendChild(del) el.appendChild(item) } - tipLayer.appendChild(el) + presentTip(chipLayer, el) var r2 = chipLayer.getBoundingClientRect() var w2 = 300 var h2 = el.offsetHeight || 120 @@ -1869,7 +1883,7 @@ window.__ModuleLoader__.load({ ui.quotes = [] annotationAttached = false writeCurrentPendingQuotes() - tipLayer.textContent = '' + clearTip(chipLayer) updateChip() renderMarkers() pendingDeco.push({ items: sentItems }) @@ -2001,7 +2015,6 @@ window.__ModuleLoader__.load({ tag.style.cssText = 'display:inline-flex;align-items:center;height:18px;padding:0 8px;margin:4px 0 0 4px;border-radius:9px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu,#2c2c2e);color:var(--dsw-alias-label-secondary);font-family:var(--dsw-font-family,system-ui);font-size:10px;cursor:default;' ;(function (list) { tag.addEventListener('mouseenter', function () { - tipLayer.textContent = '' var el = document.createElement('div') el.className = 'dsh-ann-tip' el.style.cssText = 'position:fixed;z-index:1160;width:300px;max-width:calc(100vw - 16px);padding:10px 12px;border-radius:12px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu,#2c2c2e);box-shadow:var(--dsw-shadow-lv3);font-family:var(--dsw-font-family,system-ui);font-size:12px;color:var(--dsw-alias-label-primary);' @@ -2028,7 +2041,7 @@ window.__ModuleLoader__.load({ } el.appendChild(item) } - tipLayer.appendChild(el) + presentTip(tag, el) var r2 = tag.getBoundingClientRect() var w2 = 300 var h2 = el.offsetHeight || 120 @@ -2044,7 +2057,7 @@ window.__ModuleLoader__.load({ if (bubbleGrace !== null) clearTimeout(bubbleGrace) bubbleGrace = setTimeout(function () { bubbleGrace = null - tipLayer.textContent = '' + clearTip(tag) }, 250) } function bubbleKeep() { @@ -2175,13 +2188,12 @@ window.__ModuleLoader__.load({ var grace = null function hide() { if (grace !== null) clearTimeout(grace) - grace = setTimeout(function () { grace = null; tipLayer.textContent = '' }, 250) + grace = setTimeout(function () { grace = null; clearTip(chip) }, 250) } function keep() { if (grace !== null) { clearTimeout(grace); grace = null } } chip.addEventListener('mouseenter', function () { - tipLayer.textContent = '' var el = document.createElement('div') el.className = 'dsh-ann-tip' el.style.cssText = 'position:fixed;z-index:1160;width:320px;max-width:calc(100vw - 16px);padding:10px 12px;border-radius:12px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu,#2c2c2e);box-shadow:var(--dsw-shadow-lv3);font-family:var(--dsw-font-family,system-ui);font-size:12px;color:var(--dsw-alias-label-primary);' @@ -2206,7 +2218,7 @@ window.__ModuleLoader__.load({ none.textContent = t('reply.missing') el.appendChild(none) } - tipLayer.appendChild(el) + presentTip(chip, el) var r2 = chip.getBoundingClientRect() var w2 = 320 var h2 = el.offsetHeight || 100 @@ -2278,7 +2290,7 @@ window.__ModuleLoader__.load({ if (ui.mode !== 'closed') render() updateChip() // 打开的悬浮面板按新语言关闭(下次 hover 以新语言重建)。 - tipLayer.textContent = '' + clearTip() var tags = document.querySelectorAll('[data-annotation-bubble-tag]') for (var i = 0; i < tags.length; i++) { var items = tags[i].__annotationItems @@ -2306,7 +2318,7 @@ window.__ModuleLoader__.load({ // 旧会话那条消息回切后会由气泡反解析路径重新装饰。 pendingDeco.length = 0 ui.noteDraft = '' - tipLayer.textContent = '' + clearTip() updateChip() watchInputDraft() renderMarkers() diff --git a/test/layout-overlay.test.mjs b/test/layout-overlay.test.mjs index cf23e39..3385c8a 100644 --- a/test/layout-overlay.test.mjs +++ b/test/layout-overlay.test.mjs @@ -25,3 +25,35 @@ test('工具条优先选区下方,底部空间不足时放在上方(PR #48 assert.equal(place({ left: 200, width: 300, top: 750, bottom: 770 }, 40).top, 702) assert.ok(place({ left: -100, width: 300, top: 0, bottom: 800 }, 40).top >= 8) }) + +test('共享悬浮面板按归属清理:宿主 layout mutation 不误杀正在观看的面板', () => { + // 归属机制存在:清空必须指名归属元素。 + assert.match(source, /var tipOwner = null/) + assert.match(source, /function clearTip\(owner\) \{\r?\n\s+if \(owner !== undefined && tipOwner !== owner\) return/) + assert.match(source, /function presentTip\(owner, el\) \{[\s\S]*?tipOwner = owner/) + + // 唯一的裸清空只允许在 clearTip / presentTip 内部,其它调用点一律走归属 API。 + const raw = source.match(/tipLayer\.textContent = ''/g) ?? [] + assert.equal(raw.length, 2, 'tipLayer 只能被 clearTip/presentTip 独占清空') + + // updateChip 的「无待发送批注」分支:只能清胶囊自己的面板。该分支由 body 上 + // MutationObserver(attributes/characterData/childList)经 onLayoutChange 高频 + // 触发,无归属清空会把 hover 中的气泡标签/回复芯片面板抹掉。 + const start = source.indexOf('function updateChip()') + const end = source.indexOf('// 悬停宽限', start) + assert.ok(start >= 0 && end > start, 'client.js 应定义 updateChip 与悬停宽限段') + const updateChip = source.slice(start, end) + assert.match(updateChip, /clearTip\(chipLayer\)/) + assert.doesNotMatch(updateChip, /tipLayer\.textContent = ''/) + + // 三类面板(输入框胶囊 / 气泡标签 / 回复芯片)各自带归属挂载与清理。 + assert.match(source, /presentTip\(chipLayer, el\)/) + assert.match(source, /clearTip\(chipLayer\)/) + assert.match(source, /presentTip\(tag, el\)/) + assert.match(source, /clearTip\(tag\)/) + assert.match(source, /presentTip\(chip, el\)/) + assert.match(source, /clearTip\(chip\)/) + + // 删除待发送批注时只重建胶囊面板,不接管他人的面板。 + assert.match(source, /if \(tipOwner === chipLayer\) showChipTip\(\)/) +}) From c0e3ae00be7907ba8634db15581bdb778f35b276 Mon Sep 17 00:00:00 2001 From: mov Date: Sun, 27 Sep 2026 02:50:08 +0800 Subject: [PATCH 2/6] =?UTF-8?q?=E9=80=82=E9=85=8D=20dsh=200.1.7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit add 批注内容浮窗增加删除按钮 fix 批注无法发送 fix 进入按钮显示超出 --- .gitignore | 2 + CHANGELOG.md | 11 ++ client.js | 270 +++++++++++++++++++++++++++-------- test/layout-overlay.test.mjs | 51 ++++++- test/message-block.test.mjs | 2 + test/overlay-browser.mjs | 83 +++++++++-- test/pending-clear.test.mjs | 9 +- test/sidebar-browser.mjs | 2 +- 8 files changed, 353 insertions(+), 77 deletions(-) diff --git a/.gitignore b/.gitignore index 89bdf5e..9a3a626 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ node_modules/ *.log scripts/ test-data/ +.pnpm-store/ +.dsh-meow/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 904dd27..53e1f62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [Unreleased] + +### 修复 +- 适配 DSH 0.1.7:`sessions.list` 快照不再暴露 `current`,当前会话 id 改经 `uiSession.adapter.current`(`binding.key`)读取,旧内核保留原字段兜底,会话切换订阅同步跟随。修复 0.1.7+ 上「批注标签在、但回车/发送按钮都没反应」——拼稿前取不到会话 id 会直接返回,空草稿时宿主又禁用发送,只有手动输入正文才发得出去。 + +### 交互 +- 编号只出现在会话正文可见区内(层级仍独立成层,`z-index 1100`,不跟随宿主文档结构):遮挡区取整块输入区(composer 容器,含输入卡片下方的统计行)与会话头部,编号必须落在正文滚动容器里、且不与遮挡区相交才渲染;不满足就整条不显示(跟正文一样,滚出可视区即消失,不再贴边钉住)。修复编号"压在输入框上"或"显示在对话区下面"。 +- 标记层挖去会话头部区域:高亮滚到会话头部范围内时会被头部遮住,不再盖在头部之上,行为与会话内容一致。 +- 编号跨在会话头部边界上时不再被裁成半截(半截蓝条加 `box-shadow` 会看过去像一道阴影残留):完全被头部盖住的编号不渲染,只压住一部分的编号让到头部下沿,保证数字完整、可点。 +- 编辑批注窗口:标题右侧紧邻一个删除按钮,可直接删掉这条批注;窗口中的取消按钮移除(标题栏右侧的关闭图标保留)。 + ## [1.4.11-preview.1] - 2026-09-15 - 更新 preview 预发布验收宿主至 DSH 0.1.6-alpha.1;发布任务验证同一安装包后发布至 npm preview。 diff --git a/client.js b/client.js index c9971a8..6958b53 100644 --- a/client.js +++ b/client.js @@ -2,7 +2,9 @@ // // 手写 CJS + ModuleLoader 包装(同 omdsh-dev navbar/greeter 模式,零构建 // 步骤):纯 DOM 自渲染,无任何 @deepseek-ai 值导入(bundle purity gate 合规); -// cordis 服务经 exports.inject 的字符串名接入(sessions / conversation / locale)。 +// cordis 服务经 exports.inject 的字符串名接入(sessions / conversation / +// locale / uiSession——uiSession 用于读取「当前会话 id」:DSH 0.1.7 起 +// sessions.list 快照不再带 current,见 apply 内的 readCurrentSessionId)。 // // v1.4.x · 自包含批注流(取代 v0.9 chip 设计与 v1.0 发送面板): // 1. 选中助手文字 → 工具条「批注」→ 写批注(可留空 = 仅标记原文) @@ -97,6 +99,7 @@ window.__ModuleLoader__.load({ ' margin-bottom: 8px; cursor: move; touch-action: none; user-select: none; }', '.dsh-ann-card-title { font-size: 13px; font-weight: 600;', ' color: var(--dsw-alias-label-primary); }', + '.dsh-ann-card-headmain { display: flex; align-items: center; gap: 2px; min-width: 0; }', '.dsh-ann-quote { font-size: 12px; line-height: 1.55;', ' color: var(--dsw-alias-label-tertiary);', ' border-left: 2px solid var(--dsw-alias-border-inverted);', @@ -135,11 +138,6 @@ window.__ModuleLoader__.load({ '.dsh-ann-input:focus { border-color: var(--dsw-alias-text-accent, #4c9aff); }', '.dsh-ann-input::placeholder { color: var(--dsw-alias-label-dimmed); }', '.dsh-ann-row { display: flex; gap: 8px; margin-top: 10px; justify-content: flex-end; }', - '.dsh-ann-cancel { display: inline-flex; align-items: center; height: 28px; padding: 0 12px;', - ' border: 1px solid var(--dsw-alias-border-l2); border-radius: 14px;', - ' background: transparent; color: var(--dsw-alias-label-primary);', - ' font-family: inherit; font-size: 12px; cursor: pointer; }', - '.dsh-ann-cancel:hover { background: var(--dsw-alias-interactive-bg-hover); }', '.dsh-ann-error { color: var(--dsw-alias-state-error-primary, #ff7a7a);', ' font-size: 12px; margin-top: 8px; word-break: break-word; }', '.dsh-ann-hl { position: fixed; z-index: 900; background: rgba(255, 195, 0, .15);', @@ -152,7 +150,8 @@ window.__ModuleLoader__.load({ ' box-shadow: 0 1px 4px rgba(0,0,0,.35); pointer-events: auto; cursor: pointer;', ' transition: filter .12s ease; }', '.dsh-ann-num:hover { filter: brightness(1.15); }', - 'body:has([role="dialog"][aria-modal="true"]) [data-annotation-overlay] { display: none; }', + 'body:has([role="dialog"][aria-modal="true"]) [data-annotation-overlay],', + 'body:has([role="dialog"][aria-modal="true"]) [data-annotation-num-layer] { display: none; }', '.dsh-ann-tip { animation: dsh-ann-pop .12s var(--ds-ease-in-out, ease); }', '@keyframes dsh-ann-fadein { from { opacity: 0; } to { opacity: 1; } }', ].join('\n') @@ -176,6 +175,7 @@ window.__ModuleLoader__.load({ editTitle: '编辑批注', placeholder: '写下批注…(可留空,保存后仅标记原文)', save: '保存批注', + delete: '删除这条批注', }, common: { cancel: '取消' }, error: { noSelection: '没有选中的内容' }, @@ -213,6 +213,7 @@ window.__ModuleLoader__.load({ editTitle: 'Edit annotation', placeholder: 'Write a note… (optional; saving only marks the passage)', save: 'Save annotation', + delete: 'Delete this annotation', }, common: { cancel: 'Cancel' }, error: { noSelection: 'No text selected' }, @@ -904,6 +905,50 @@ window.__ModuleLoader__.load({ function apply(ctx) { var sessions = ctx.sessions + // ---------- 当前会话 id 的跨版本读取 ---------- + // DSH 0.1.7 起 `sessions.list` 快照不再暴露 `current`(selection 收进 + // ClientSessions 私有 selection),改由 ui-session 服务的 + // `adapter.current`(HostObservable,binding.key = sessionId) + // 发布。旧版两条路都在,故这里以 uiSession 优先、list.current 兜底: + // 若只认旧字段,0.1.7+ 上 current 恒为 undefined,attachAndSend 第一行 + // 就 return false —— 表现为「有批注、标签也在,但回车/发送按钮都没反应」。 + var uiSessionService = null + + /** ui-session 服务(惰性探测:apply 时机早于它注册时下次再取)。 */ + function uiSessionFace() { + if (uiSessionService === null) { + try { uiSessionService = ctx.get('uiSession') || null } catch (_) { uiSessionService = null } + } + return uiSessionService + } + + function currentSessionSource() { + try { + var face = uiSessionFace() + var adapter = face !== null ? face.adapter : undefined + var source = adapter !== undefined && adapter !== null ? adapter.current : undefined + if (source !== undefined && source !== null + && typeof source.getSnapshot === 'function' && typeof source.subscribe === 'function') return source + } catch (_) { /* 旧版内核:退回 list store */ } + return sessions.list + } + + function readCurrentSessionId() { + try { + var face = uiSessionFace() + var adapter = face !== null ? face.adapter : undefined + var source = adapter !== undefined && adapter !== null ? adapter.current : undefined + if (source !== undefined && source !== null && typeof source.getSnapshot === 'function') { + var binding = source.getSnapshot() + if (binding !== undefined && binding !== null && binding.key !== undefined) return binding.key + } + } catch (_) { /* 旧版内核:退回 list store */ } + try { + var snap = sessions.list.getSnapshot() + return snap !== undefined && snap !== null ? snap.current : undefined + } catch (_) { return undefined } + } + var host = document.createElement('div') host.setAttribute('data-annotation-for-dsh', '') document.body.appendChild(host) @@ -912,6 +957,16 @@ window.__ModuleLoader__.load({ overlay.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:900;' document.body.appendChild(overlay) + // 编号单独成层:overlay 是 z-index 900 的 stacking context,编号挂在里面 + // 只能跟着它走——既会被宿主里层级更高的内容压住,也会被输入框/头部的 + // 裁剪一起切掉(表现为「滚到输入框上就不显示」)。分层之后编号有自己 + // 统一的层级(1100,仍低于胶囊/菜单层),不随宿主文档结构变化,也不再 + // 参与标记层的裁剪。 + var numLayer = document.createElement('div') + numLayer.setAttribute('data-annotation-num-layer', '') + numLayer.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:1100;' + document.body.appendChild(numLayer) + var ui = { mode: 'closed', // closed | actions | editing | composing editingId: null, // 非空 = 正在编辑该 id 的已有批注(点角标进入) @@ -948,7 +1003,7 @@ window.__ModuleLoader__.load({ } function writeCurrentPendingQuotes() { - writePendingQuotes(sessions.list.getSnapshot().current) + writePendingQuotes(readCurrentSessionId()) } var ignoreUntil = 0 @@ -1036,12 +1091,13 @@ window.__ModuleLoader__.load({ var ancEl = anc instanceof Element ? anc : (anc && anc.parentElement) if (ancEl !== null && ancEl.closest) { if (ancEl.closest('[data-annotation-for-dsh]') || ancEl.closest('[data-annotation-overlay]') + || ancEl.closest('[data-annotation-num-layer]') || ancEl.closest('[data-composer-card]') || ancEl.closest('[data-input-scroll]')) { clearSettle() return } } - if (host.contains(anc) || overlay.contains(anc)) { + if (host.contains(anc) || overlay.contains(anc) || numLayer.contains(anc)) { clearSettle() return } @@ -1050,7 +1106,7 @@ window.__ModuleLoader__.load({ var key = selectionKey(sel) var rootEl = annotationRootOf(range.commonAncestorContainer) var source = documentSourceOf(range.commonAncestorContainer) - if (source !== null && source.sessionId !== sessions.list.getSnapshot().current) rootEl = null + if (source !== null && source.sessionId !== readCurrentSessionId()) rootEl = null if (rootEl === null) { clearSettle(); closeToolbar(); return } if (ui.mode === 'actions' && key === ui.lastKey && text === ui.quote && rootEl === ui.selectionRoot && (ui.source && ui.source.sourceUrl) === (source && source.sourceUrl)) { clearSettle(); return } @@ -1064,7 +1120,7 @@ window.__ModuleLoader__.load({ if (annotationRootOf(r.commonAncestorContainer) !== rootEl) return var currentSource = documentSourceOf(r.commonAncestorContainer) if ((currentSource && currentSource.sourceUrl) !== (source && source.sourceUrl) - || (source !== null && source.sessionId !== sessions.list.getSnapshot().current)) return + || (source !== null && source.sessionId !== readCurrentSessionId())) return var rect = r.getBoundingClientRect() if (rect.width === 0 || rect.height === 0) return var p = placeAbove(rect, 40) @@ -1157,6 +1213,7 @@ window.__ModuleLoader__.load({ if (el === null || !el.closest) return true if (el.closest('[data-composer-card]') || el.closest('[data-input-scroll]') || el.closest('[data-annotation-for-dsh]') || el.closest('[data-annotation-overlay]') + || el.closest('[data-annotation-num-layer]') || el.closest('[data-annotation-chip]') || el.closest('[data-annotation-tip-layer]')) { continue } @@ -1224,7 +1281,7 @@ window.__ModuleLoader__.load({ document.addEventListener('keydown', onKeyDown, true) function submitAttached() { - var current = sessions.list.getSnapshot().current + var current = readCurrentSessionId() if (current === undefined) return var scoped = sessions.scope(current) if (scoped === undefined) return @@ -1315,10 +1372,28 @@ window.__ModuleLoader__.load({ var head = document.createElement('div') head.className = 'dsh-ann-card-head' makeEditorDraggable(head, card) + var headMain = document.createElement('div') + headMain.className = 'dsh-ann-card-headmain' var title = document.createElement('div') title.className = 'dsh-ann-card-title' title.textContent = ui.editingId !== null ? t('edit.editTitle') : t('edit.addTitle') - head.appendChild(title) + headMain.appendChild(title) + // 编辑已有批注时,标题右侧紧邻一个删除按钮(新增批注没有可删对象)。 + if (ui.editingId !== null) { + var editingId = ui.editingId + var del = document.createElement('button') + del.type = 'button' + del.className = 'dsh-ann-qdel' + del.title = t('edit.delete') + del.setAttribute('aria-label', t('edit.delete')) + del.appendChild(ICONS.trash()) + del.addEventListener('click', function () { + closeToolbar() + removeQuote(editingId) + }) + headMain.appendChild(del) + } + head.appendChild(headMain) head.appendChild(iconButton('dsh-ann-icon', ICONS.close, t('common.cancel'), closeToolbar)) card.appendChild(head) var quote = document.createElement('div') @@ -1338,18 +1413,12 @@ window.__ModuleLoader__.load({ card.appendChild(ta) var row = document.createElement('div') row.className = 'dsh-ann-row' - var cancel = document.createElement('button') - cancel.className = 'dsh-ann-cancel' - cancel.type = 'button' - cancel.textContent = t('common.cancel') - cancel.addEventListener('click', closeToolbar) var save = document.createElement('button') save.type = 'button' save.className = 'dsh-ann-action' save.appendChild(ICONS.check()) save.appendChild(document.createTextNode(t('edit.save'))) save.addEventListener('click', saveAnnotation) - row.appendChild(cancel) row.appendChild(save) card.appendChild(row) if (ui.error !== null) { @@ -1394,7 +1463,7 @@ window.__ModuleLoader__.load({ // ---------- 批注标记 ---------- var markersSig = null - function markersSignature() { + function markersSignature(blockers) { var parts = [] for (var i = 0; i < ui.quotes.length; i++) { var q = ui.quotes[i] @@ -1408,23 +1477,76 @@ window.__ModuleLoader__.load({ } if (rects.length > 0) parts.push(q.id + ':chip') } + // 遮挡区(输入框 + 会话头部)也进签名:它们改变时编号必须重走 + // 让位/隐藏,否则旧位置会被裁剪成半截残影。 + for (var b = 0; b < blockers.length; b++) { + var br = blockers[b] + parts.push('b:' + Math.round(br.left) + ',' + Math.round(br.top) + ',' + Math.round(br.width) + ',' + Math.round(br.height)) + } return parts.join('|') } - function renderMarkers() { - // 对整个标记层挖去输入框区域;滚动、尺寸变化时即使原文没动也要刷新。 - var composer = document.querySelector('[data-composer-card]') - var r = composer !== null ? composer.getBoundingClientRect() : null - overlay.style.clipPath = r !== null && r.width > 0 && r.height > 0 - ? 'polygon(evenodd, 0 0, 100% 0, 100% 100%, 0 100%, 0 0, ' - + r.left + 'px ' + r.top + 'px, ' + r.right + 'px ' + r.top + 'px, ' + /** 按 CSS Modules local 名取宿主元素的可见矩形:类名形如 `wSkVaW_header` + * (哈希前缀随版本变),故认「以 _ 收尾的 class token」, + * headerActions / composerSeatInner 这类同前缀兄弟名不会被误认。 */ + function hostLocalRects(local) { + var out = [] + var found = document.querySelectorAll('[class*="_' + local + '"]') + var re = new RegExp('(^|\\s)[^\\s]*_' + local + '(\\s|$)') + for (var i = 0; i < found.length; i++) { + var cls = found[i].getAttribute('class') || '' + if (!re.test(cls)) continue + var r = found[i].getBoundingClientRect() + if (r.width <= 0 || r.height <= 0) continue + if (r.bottom <= 0 || r.top >= window.innerHeight) continue + out.push(r) + } + return out + } + + /** 会盖住会话内容的宿主头部:标记层是 fixed 定位,原生绘制顺序上恒在 + * 头部之上——只有把头部区域从标记层裁掉,编号与高亮才会像会话内容 + * 一样被头部遮住。 */ + function hostHeaderRects() { + return hostLocalRects('header') + } + + /** 输入区(整块 composer 容器,含输入卡片与下方统计行)——不是单个 + * `[data-composer-card]`:卡片下方那段同样被容器盖住,编号落在那里 + * 会像「显示在对话区下面」。找不到容器时退回输入卡片。 */ + function hostComposerRects() { + var rects = hostLocalRects('composerSeat') + if (rects.length > 0) return rects + var card = document.querySelector('[data-composer-card]') + if (card === null) return [] + var r = card.getBoundingClientRect() + return r.width > 0 && r.height > 0 ? [r] : [] + } + + /** 外框保留、逐个矩形挖空(evenodd):标记层的裁剪路径。 */ + function clipWithHoles(rects) { + var d = 'polygon(evenodd, 0 0, 100% 0, 100% 100%, 0 100%, 0 0' + for (var i = 0; i < rects.length; i++) { + var r = rects[i] + d += ', ' + r.left + 'px ' + r.top + 'px, ' + r.right + 'px ' + r.top + 'px, ' + r.right + 'px ' + r.bottom + 'px, ' + r.left + 'px ' + r.bottom + 'px, ' - + r.left + 'px ' + r.top + 'px)' - : 'none' - var sig = markersSignature() + + r.left + 'px ' + r.top + 'px' + } + return d + ')' + } + + function renderMarkers() { + // 对整个标记层挖去输入区与会话头部区域;滚动、尺寸变化时即使原文没动也要刷新。 + var holes = hostComposerRects().concat(hostHeaderRects()) + overlay.style.clipPath = holes.length > 0 ? clipWithHoles(holes) : 'none' + // 编号层用同一条裁剪路径兜底:编号只允许出现在会话内容可视区里, + // 绝不压在输入框或会话头部之上(定位逻辑已先让位/隐藏,这里是保险)。 + numLayer.style.clipPath = overlay.style.clipPath + var sig = markersSignature(holes) if (sig !== markersSig) { markersSig = sig overlay.textContent = '' + numLayer.textContent = '' buildMarkers() } } @@ -1456,38 +1578,59 @@ window.__ModuleLoader__.load({ } if (anchor === null && rects.length > 0 && rects[0].width > 0) anchor = rects[0] if (anchor !== null) { - var chip = document.createElement('div') - chip.className = 'dsh-ann-num' - chip.textContent = String(i + 1) var chipTop = anchor.top - 20 if (chipTop < 4) chipTop = Math.min(anchor.top + 2, window.innerHeight - 22) if (chipTop < 4) chipTop = 4 var chipLeft = Math.max(4, Math.min(anchor.left - 4, window.innerWidth - 24)) - var tries = 0 - while (tries < 12) { - var clash = false - for (var p = 0; p < placed.length; p++) { - var bp = placed[p] - if (Math.abs(bp.left - chipLeft) < 18 && Math.abs(bp.top - chipTop) < 18) { - clash = true - break + // 编号只允许出现在「会话正文实际看得见」的地方:跟正文一样,滚出 + // 内容可视区就消失,绝不贴边钉住(钉住会浮在输入框上/下面的空处)。 + // 两条判据:① 落在正文滚动容器里;② 不与宿主遮挡 UI(会话头部、 + // 整块输入区)相交。任一不满足就整条不渲染——不渲染也就不会出现 + // 被裁掉半截的"阴影残影"。 + var skipChip = false + var views = hostLocalRects('scrollBody') + for (var vc = 0; vc < views.length; vc++) { + var vr = views[vc] + if (chipLeft + 26 <= vr.left || chipLeft >= vr.right) continue + if (chipTop < vr.top || chipTop + 18 > vr.bottom) skipChip = true + break + } + var blockers = hostHeaderRects().concat(hostComposerRects()) + for (var bc = 0; bc < blockers.length && !skipChip; bc++) { + var bk = blockers[bc] + if (chipLeft + 26 <= bk.left || chipLeft >= bk.right) continue + if (chipTop + 18 > bk.top && chipTop < bk.bottom) skipChip = true + } + if (!skipChip) { + var chip = document.createElement('div') + chip.className = 'dsh-ann-num' + chip.textContent = String(i + 1) + var tries = 0 + while (tries < 12) { + var clash = false + for (var p = 0; p < placed.length; p++) { + var bp = placed[p] + if (Math.abs(bp.left - chipLeft) < 18 && Math.abs(bp.top - chipTop) < 18) { + clash = true + break + } } + if (!clash) break + chipLeft += 18 + if (chipLeft > window.innerWidth - 24) { chipLeft = 4; chipTop += 18 } + tries++ } - if (!clash) break - chipLeft += 18 - if (chipLeft > window.innerWidth - 24) { chipLeft = 4; chipTop += 18 } - tries++ + placed.push({ left: chipLeft, top: chipTop }) + chip.style.left = chipLeft + 'px' + chip.style.top = chipTop + 'px' + ;(function (q) { + chip.addEventListener('click', function (ev) { + ev.stopPropagation() + openEditorFor(q) + }) + })(q) + numLayer.appendChild(chip) } - placed.push({ left: chipLeft, top: chipTop }) - chip.style.left = chipLeft + 'px' - chip.style.top = chipTop + 'px' - ;(function (q) { - chip.addEventListener('click', function (ev) { - ev.stopPropagation() - openEditorFor(q) - }) - })(q) - overlay.appendChild(chip) } } } @@ -1604,7 +1747,7 @@ window.__ModuleLoader__.load({ /** 提交前把批注块拼进 composer 草稿(随回车一起发送)。 * 返回 true 表示批注块已在草稿中(本次刚拼入,或之前已拼入未发送)。 */ function attachAndSend(e) { - var current = sessions.list.getSnapshot().current + var current = readCurrentSessionId() if (current === undefined) return false try { var scoped = sessions.scope(current) @@ -1900,10 +2043,10 @@ window.__ModuleLoader__.load({ function watchInputDraft() { if (inputWatchTimer !== null) { clearInterval(inputWatchTimer); inputWatchTimer = null } if (typeof inputUnsub === 'function') { inputUnsub(); inputUnsub = null } - var id = sessions.list.getSnapshot().current + var id = readCurrentSessionId() if (id !== undefined && tryWatchInputDraft(id)) return inputWatchTimer = setInterval(function () { - var cur = sessions.list.getSnapshot().current + var cur = readCurrentSessionId() if (cur !== undefined && tryWatchInputDraft(cur)) { clearInterval(inputWatchTimer) inputWatchTimer = null @@ -2304,10 +2447,12 @@ window.__ModuleLoader__.load({ } // ---------- 待发送批注按会话恢复 ---------- - var lastSessionId = sessions.list.getSnapshot().current + var lastSessionId = readCurrentSessionId() ui.quotes = readPendingQuotes(lastSessionId) - var unsub = sessions.list.subscribe(function () { - var cur = sessions.list.getSnapshot().current + // 会话切换:0.1.7 起 selection 变化不再写进 list 快照,必须订阅 + // ui-session 的 current source,否则切会话时待发送批注不会跟着切换。 + var unsub = currentSessionSource().subscribe(function () { + var cur = readCurrentSessionId() if (cur === lastSessionId) return writePendingQuotes(lastSessionId) lastSessionId = cur @@ -2353,11 +2498,12 @@ window.__ModuleLoader__.load({ tipLayer.remove() host.remove() overlay.remove() + numLayer.remove() } } exports.name = '@changfenhuang/dsh-annotation' - exports.inject = ['sessions', 'conversation', 'locale'] + exports.inject = ['sessions', 'conversation', 'locale', 'uiSession'] exports.apply = apply return module.exports diff --git a/test/layout-overlay.test.mjs b/test/layout-overlay.test.mjs index 3385c8a..ec6fe6f 100644 --- a/test/layout-overlay.test.mjs +++ b/test/layout-overlay.test.mjs @@ -4,8 +4,24 @@ import test from 'node:test' const source = readFileSync(new URL('../client.js', import.meta.url), 'utf8') -test('设置弹窗打开时隐藏会话高亮层(issue #44)', () => { - assert.match(source, /body:has\(\[role="dialog"\]\[aria-modal="true"\]\) \[data-annotation-overlay\] \{ display: none; \}/) +test('设置弹窗打开时隐藏会话高亮层与编号层(issue #44)', () => { + assert.match(source, /body:has\(\[role="dialog"\]\[aria-modal="true"\]\) \[data-annotation-overlay\],/) + assert.match(source, /body:has\(\[role="dialog"\]\[aria-modal="true"\]\) \[data-annotation-num-layer\] \{ display: none; \}/) +}) + +test('编号独立成层:层级固定,且不压在输入框/会话头部上', () => { + assert.match(source, /data-annotation-num-layer/, '编号应有自己的容器') + const layer = source.slice(source.indexOf('var numLayer = document.createElement'), source.indexOf('var ui = {')) + assert.match(layer, /z-index:1100/, '编号层使用统一层级(不跟随标记层的 900)') + assert.match(layer, /pointer-events:none/, '编号层本体不吃事件') + const render = source.slice(source.indexOf('function renderMarkers()'), source.indexOf('function buildMarkers()')) + assert.match(render, /numLayer\.style\.clipPath = overlay\.style\.clipPath/, '编号层共用裁剪路径兜底,编号不会压在宿主 UI 上') + const build = source.slice(source.indexOf('function buildMarkers()'), source.indexOf('// ---------- 动作 ----------')) + assert.match(build, /numLayer\.appendChild\(chip\)/, '编号挂到编号层而不是被裁剪的高亮层') + assert.doesNotMatch(build, /overlay\.appendChild\(chip\)/, '编号不再挂进被裁剪的 overlay') + assert.match(build, /hostComposerRects\(\)/, '编号的可见性判定覆盖整块输入区') + const sig = source.slice(source.indexOf('function markersSignature('), source.indexOf('function hostLocalRects(')) + assert.match(sig, /blockers/, '遮挡区进入签名:输入框/头部变化时编号重走可见性判定') }) test('滚动和布局变化会刷新输入框批注胶囊(issue #44)', () => { @@ -57,3 +73,34 @@ test('共享悬浮面板按归属清理:宿主 layout mutation 不误杀正在 // 删除待发送批注时只重建胶囊面板,不接管他人的面板。 assert.match(source, /if \(tipOwner === chipLayer\) showChipTip\(\)/) }) + +test('标记层裁掉会话头部与输入区:编号与高亮像内容一样被遮住', () => { + assert.match(source, /function hostLocalRects\(local\)/, '按 CSS Modules local 名统一取宿主矩形') + assert.match(source, /function hostHeaderRects\(\)/, 'client.js 应定位宿主的会话头部') + assert.match(source, /function hostComposerRects\(\)/, 'client.js 应定位整块输入区(含卡片下方统计行)') + assert.match(source, /hostLocalRects\('composerSeat'\)/, '输入区认 composerSeat,而不是单个输入卡片') + const render = source.slice(source.indexOf('function renderMarkers()'), source.indexOf('function buildMarkers()')) + assert.ok(render.length > 0, 'client.js 应定义 renderMarkers') + assert.match(render, /hostComposerRects\(\)\.concat\(hostHeaderRects\(\)\)/, '输入区与头部共用挖空列表') + assert.match(render, /clipWithHoles\(holes\)/, '共用同一条 evenodd 裁剪路径') + + // 编号只出现在"正文实际可见"的地方:与遮挡区或滚动容器边界冲突就不渲染。 + const build = source.slice(source.indexOf('function buildMarkers()'), source.indexOf('// ---------- 动作 ----------')) + assert.ok(build.length > 0, 'client.js 应定义 buildMarkers') + assert.match(build, /skipChip/, '不在可见区内的编号不渲染') + assert.match(build, /hostLocalRects\('scrollBody'\)/, '编号必须落在正文滚动容器内') + assert.match(build, /hostHeaderRects\(\)\.concat\(hostComposerRects\(\)\)/, '编号不得与会话头部或输入区相交') + assert.doesNotMatch(build, /chipTop = hd\.bottom \+ 4/, '不再贴边钉住编号(滚出可视区就隐藏)') +}) + +test('编辑窗口:标题右侧可删除该批注,且不再有取消按钮', () => { + const start = source.indexOf("} else if (ui.mode === 'editing') {") + const end = source.indexOf('function positionEditor(card, left, top)', start) + assert.ok(start >= 0 && end > start, 'client.js 应定义编辑卡片分支') + const editor = source.slice(start, end) + assert.match(editor, /dsh-ann-card-headmain/, '标题与其右侧按钮成组') + assert.match(editor, /t\('edit\.delete'\)/, '删除按钮使用 i18n 文案') + assert.match(editor, /removeQuote\(editingId\)/, '删除复用待发送批注移除逻辑') + assert.doesNotMatch(editor, /dsh-ann-cancel/, '编辑窗口不再有取消按钮') + assert.doesNotMatch(source, /dsh-ann-cancel/, '取消按钮的样式一并移除') +}) diff --git a/test/message-block.test.mjs b/test/message-block.test.mjs index ca1deb0..cf0978c 100644 --- a/test/message-block.test.mjs +++ b/test/message-block.test.mjs @@ -23,7 +23,9 @@ function harness(lang, draft, sourcePath) { var annotationAttached = false var sessions = { list: { getSnapshot: () => ({ current: 'session' }) }, scope: () => ({}) } var ctx = { conversation: { input: { for: () => shell } } } + var uiSessionService = null function showToast() {} + ${['uiSessionFace', 'currentSessionSource', 'readCurrentSessionId'].map(fn).join('\n')} ${['buildBlock', 'shouldAttachForEnter', 'isCommandDraft', 'attachAndSend', 'hideAnnotationBlock', 'parseItemsFromBubble'].map(fn).join('\n')} return { setLang, attachAndSend, hideAnnotationBlock, parseItemsFromBubble } `)(shell, document, { SHOW_TEXT: 4 }, sourcePath) diff --git a/test/overlay-browser.mjs b/test/overlay-browser.mjs index 3aedb08..a525e8c 100644 --- a/test/overlay-browser.mjs +++ b/test/overlay-browser.mjs @@ -7,6 +7,7 @@ try { const errors = [] page.on('pageerror', error => errors.push(String(error))) await page.setContent(` +

这是用来验证批注拖动和高亮遮挡的原文。

输入内容
`) await page.evaluate(() => { @@ -27,6 +28,8 @@ try { assert.ok(bar.y >= quote.y + quote.height, '工具条位于选区下方') await page.locator('.dsh-ann-bar button').click() await page.locator('.dsh-ann-input').fill('拖动后不能丢失的批注') + assert.equal(await page.locator('.dsh-ann-card-head .dsh-ann-qdel').count(), 0, '新增批注时标题旁没有删除按钮') + assert.equal(await page.locator('.dsh-ann-cancel').count(), 0, '编辑窗口没有取消按钮') const before = await page.locator('.dsh-ann-card').boundingBox() const head = await page.locator('.dsh-ann-card-title').boundingBox() await page.mouse.move(head.x + 15, head.y + 5) @@ -43,22 +46,46 @@ try { assert.ok(resized.x + resized.width <= 693 && resized.y + resized.height <= 643, '缩小窗口仍可操作') await page.locator('.dsh-ann-action').click() await page.locator('.dsh-ann-hl').first().waitFor() - // Make the numbered marker straddle the composer: clipped pixels must not intercept input. + // 输入框覆盖到内容上(悬浮输入区):高亮被裁掉,编号也不与输入框相交。 await page.evaluate(() => { const composer = document.querySelector('[data-composer-card]') composer.style.top = '340px' composer.style.left = '240px' window.dispatchEvent(new Event('resize')) }) - await page.waitForTimeout(100) - assert.ok(await page.evaluate(() => { + await page.waitForTimeout(200) + const composerOverlap = await page.evaluate(() => { const layer = document.querySelector('[data-annotation-overlay]') - const marker = document.querySelector('.dsh-ann-num') - marker.style.left = '260px'; marker.style.top = '350px' - const hit = document.elementFromPoint(265, 355) - return CSS.supports('clip-path', layer.style.clipPath) - && hit.closest('[data-composer-card]') !== null - }), '高亮层和编号在输入区域被裁掉,不遮挡输入') + const numLayer = document.querySelector('[data-annotation-num-layer]') + const hl = document.querySelector('.dsh-ann-hl') + const keep = { left: hl.style.left, top: hl.style.top } + hl.style.left = '260px' + hl.style.top = '350px' + const highlightPainted = document.elementsFromPoint(265, 355) + .some(el => el.hasAttribute('data-annotation-overlay')) + hl.style.left = keep.left + hl.style.top = keep.top + const composer = document.querySelector('[data-composer-card]').getBoundingClientRect() + const marker = numLayer.querySelector('.dsh-ann-num') + let overlaps = false + if (marker !== null) { + const r = marker.getBoundingClientRect() + overlaps = r.left < composer.right && r.right > composer.left + && r.top < composer.bottom && r.bottom > composer.top + } + return { + highlightPainted, + clipPath: layer.style.clipPath, + numClipPath: numLayer.style.clipPath, + markerHidden: marker === null, + overlaps, + } + }) + assert.equal(composerOverlap.highlightPainted, false, '输入区域内的高亮被裁掉,不遮挡输入') + assert.match(composerOverlap.clipPath, /polygon/, '标记层使用裁剪路径') + assert.match(composerOverlap.numClipPath, /polygon/, '编号层同样带裁剪路径兜底') + assert.ok(composerOverlap.markerHidden || !composerOverlap.overlaps, + `编号不能压在输入框上:${JSON.stringify(composerOverlap)}`) await page.evaluate(() => { document.querySelector('[data-composer-card]').style.top = '550px' window.dispatchEvent(new Event('resize')) @@ -66,11 +93,45 @@ try { await page.waitForTimeout(100) await page.locator('.dsh-ann-num').click() assert.equal(await page.locator('.dsh-ann-input').inputValue(), '拖动后不能丢失的批注') - await page.locator('.dsh-ann-card-head button').click() + assert.equal(await page.locator('.dsh-ann-cancel').count(), 0, '编辑窗口不再有取消按钮') + assert.equal(await page.locator('.dsh-ann-card-head .dsh-ann-qdel').count(), 1, '标题右侧有删除按钮') + // 标记层把会话头部区域裁掉:高亮不盖在头部之上(编号另有让位/隐藏规则)。 + const headerClip = await page.evaluate(() => { + const hl = document.querySelector('.dsh-ann-hl') + const keep = { left: hl.style.left, top: hl.style.top } + hl.style.left = '260px' + hl.style.top = '20px' + const inHeader = document.elementsFromPoint(265, 25) + .some(el => el.hasAttribute('data-annotation-overlay')) + hl.style.left = keep.left + hl.style.top = keep.top + return inHeader + }) + assert.equal(headerClip, false, '会话头部区域内的高亮被裁掉,不盖在头部之上') + await page.locator('.dsh-ann-card-head .dsh-ann-icon').click() assert.equal(await page.locator('.dsh-ann-card').count(), 0, '关闭按钮不触发拖动') + // 编号跨在头部边界上时:要么完整让到头部下方,要么整条不画,绝不剩半截残影。 + await page.evaluate(() => { + document.querySelector('[data-chat-flow-kind="assistant-step"]').style.top = '70px' + window.dispatchEvent(new Event('resize')) + }) + await page.waitForTimeout(200) + const straddle = await page.evaluate(() => { + const header = document.querySelector('.wSkVaW_header').getBoundingClientRect() + const marker = document.querySelector('.dsh-ann-num') + if (marker === null) return { hidden: true } + const r = marker.getBoundingClientRect() + return { hidden: false, top: Math.round(r.top), headerBottom: Math.round(header.bottom), full: r.top >= header.bottom } + }) + assert.ok(straddle.hidden || straddle.full, `编号不能跨在头部边界上留下半截:${JSON.stringify(straddle)}`) + // 标题右侧的删除按钮:连同标记与待发送清单一起删掉。 + await page.locator('.dsh-ann-num').click() + await page.locator('.dsh-ann-card-head .dsh-ann-qdel').click() + assert.equal(await page.locator('.dsh-ann-num').count(), 0, '删除按钮移除该批注的标记') + assert.equal(await page.locator('.dsh-ann-card').count(), 0, '删除后编辑窗口关闭') await page.evaluate(() => window.dispose()) assert.deepEqual(errors, []) - console.log('PASS: 工具条下方定位、标题栏拖动、边界、缩放、保存、裁剪、重新编辑、取消、无页面错误') + console.log('PASS: 工具条下方定位、标题栏拖动、边界、缩放、保存、裁剪、头部遮挡、边界残影、重新编辑、删除、无页面错误') } finally { await browser.close() } diff --git a/test/pending-clear.test.mjs b/test/pending-clear.test.mjs index bd7135a..b8f37c7 100644 --- a/test/pending-clear.test.mjs +++ b/test/pending-clear.test.mjs @@ -26,12 +26,19 @@ test('watchInputDraft 订阅失败时每秒重试(初始化时序洞由重试 }) test('会话切换作废旧会话未消费的发送暂存数据', () => { - const sw = source.match(/var unsub = sessions\.list\.subscribe\(function \(\) \{[\s\S]*?\n \}\)/) + const sw = source.match(/var unsub = currentSessionSource\(\)\.subscribe\(function \(\) \{[\s\S]*?\n \}\)/) assert.ok(sw, 'client.js should define the session-switch handler') assert.match(sw[0], /pendingDeco\.length = 0/, 'stale send staging from the previous session must be dropped, not consumed by the new session history') }) +test('当前会话 id 兼容 DSH 0.1.7(list 快照不再暴露 current)', () => { + assert.match(source, /function readCurrentSessionId\(\)/, 'client.js should route current-session reads through one helper') + assert.match(source, /adapter\.current/, 'helper should prefer uiSession.adapter.current') + assert.doesNotMatch(source, /sessions\.list\.getSnapshot\(\)\.current/, + 'the removed list.current field must not be read directly — it is always undefined on 0.1.7+, which breaks send') +}) + test('发送暂存数据在隐藏手术成功后才消费(peek → shift,不提前丢失)', () => { assert.match(source, /pendingDeco\[0\]\.items/, 'peek pendingDeco instead of popping') assert.doesNotMatch(source, /pendingDeco\.pop\(\)/, diff --git a/test/sidebar-browser.mjs b/test/sidebar-browser.mjs index 8f50236..2a6df36 100644 --- a/test/sidebar-browser.mjs +++ b/test/sidebar-browser.mjs @@ -73,7 +73,7 @@ try { assert.ok(marker.x > 450, '不会误标聊天中的同文') await page.locator('.dsh-ann-num').click() assert.equal(await page.locator('.dsh-ann-input').inputValue(), '解释这个文件') - await page.locator('.dsh-ann-card-head button').click() + await page.locator('.dsh-ann-card-head .dsh-ann-icon').click() await page.locator('[data-composer-input]').press('Enter') const draft = await page.evaluate(() => window.draft) assert.ok(draft.includes('[docs/a.md]') && draft.includes('[docs/b.md]'), '两份文件路径都随原文拼稿') From 62b1cdc33dd8f59ac60ce1595394c0cb221c700f Mon Sep 17 00:00:00 2001 From: movcat Date: Sun, 27 Sep 2026 17:24:35 +0800 Subject: [PATCH 3/6] =?UTF-8?q?fix=20=E6=89=B9=E6=B3=A8=E7=9A=84=E6=80=BB?= =?UTF-8?q?=E9=A2=84=E8=A7=88=E6=98=BE=E7=A4=BA=E5=9C=A8=E5=B1=8F=E5=B9=95?= =?UTF-8?q?=E5=A4=96=EF=BC=8C=E5=90=B8=E5=85=A5=E8=BF=87=E5=A4=9A=E6=97=B6?= =?UTF-8?q?=E5=8F=AF=E5=A2=9E=E5=8A=A0=E5=86=85=E5=AE=B9=E6=BB=91=E5=8A=A8?= =?UTF-8?q?=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++-- client.js | 37 +++++++++++++++++----- test/overlay-browser.mjs | 66 +++++++++++++++++++++++++++++++++++++--- 3 files changed, 94 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53e1f62..57d6001 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,9 @@ - 适配 DSH 0.1.7:`sessions.list` 快照不再暴露 `current`,当前会话 id 改经 `uiSession.adapter.current`(`binding.key`)读取,旧内核保留原字段兜底,会话切换订阅同步跟随。修复 0.1.7+ 上「批注标签在、但回车/发送按钮都没反应」——拼稿前取不到会话 id 会直接返回,空草稿时宿主又禁用发送,只有手动输入正文才发得出去。 ### 交互 -- 编号只出现在会话正文可见区内(层级仍独立成层,`z-index 1100`,不跟随宿主文档结构):遮挡区取整块输入区(composer 容器,含输入卡片下方的统计行)与会话头部,编号必须落在正文滚动容器里、且不与遮挡区相交才渲染;不满足就整条不显示(跟正文一样,滚出可视区即消失,不再贴边钉住)。修复编号"压在输入框上"或"显示在对话区下面"。 -- 标记层挖去会话头部区域:高亮滚到会话头部范围内时会被头部遮住,不再盖在头部之上,行为与会话内容一致。 -- 编号跨在会话头部边界上时不再被裁成半截(半截蓝条加 `box-shadow` 会看过去像一道阴影残留):完全被头部盖住的编号不渲染,只压住一部分的编号让到头部下沿,保证数字完整、可点。 +- 编号只出现在会话正文可见区内(图层独立,`z-index 1100`,不跟随宿主文档结构):遮挡区取整块输入区(composer 容器,含输入卡片下方的统计行)与会话头部,编号必须落在正文滚动容器里、且不与遮挡区相交才渲染;不满足就整条不显示——跟正文一样,滚出可视区即消失,不贴边钉住,也不会被裁成半截残影。修复编号"压在输入框上"、"显示在对话区下面"或盖在会话头部上的问题。 +- 批注清单浮窗(悬停「批注 ×N」标签):宽度改为 `border-box`(此前 300px 内容宽再加内外边距会顶出屏幕右边缘),并把浮窗限制在正文可见区内(避开会话头部与整块输入区),批注条数多时在浮窗内部滚动——修掉「批注多时浮窗溢出屏幕、压住输入框」。 +- 标记层挖去会话头部与输入区:高亮滚到这些区域时会被遮住,不再盖在上面。 - 编辑批注窗口:标题右侧紧邻一个删除按钮,可直接删掉这条批注;窗口中的取消按钮移除(标题栏右侧的关闭图标保留)。 ## [1.4.11-preview.1] - 2026-09-15 diff --git a/client.js b/client.js index 6958b53..a548935 100644 --- a/client.js +++ b/client.js @@ -1951,7 +1951,7 @@ window.__ModuleLoader__.load({ if (ui.quotes.length === 0) return var el = document.createElement('div') el.className = 'dsh-ann-tip' - el.style.cssText = 'position:fixed;z-index:1160;width:300px;max-width:calc(100vw - 16px);padding:10px 12px;border-radius:12px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu,#2c2c2e);box-shadow:var(--dsw-shadow-lv3);font-family:var(--dsw-font-family,system-ui);font-size:12px;color:var(--dsw-alias-label-primary);' + el.style.cssText = 'position:fixed;z-index:1160;box-sizing:border-box;width:300px;max-width:calc(100vw - 16px);padding:10px 12px;border-radius:12px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu,#2c2c2e);box-shadow:var(--dsw-shadow-lv3);font-family:var(--dsw-font-family,system-ui);font-size:12px;color:var(--dsw-alias-label-primary);' var head = document.createElement('div') head.style.cssText = 'font-weight:600;margin-bottom:6px;' head.textContent = t('tip.title', { n: ui.quotes.length }) @@ -1990,12 +1990,33 @@ window.__ModuleLoader__.load({ presentTip(chipLayer, el) var r2 = chipLayer.getBoundingClientRect() var w2 = 300 - var h2 = el.offsetHeight || 120 - var left = Math.max(8, Math.min(r2.left, window.innerWidth - w2 - 8)) - var top = r2.top - h2 - 6 - if (top < 8) top = r2.bottom + 6 - el.style.left = left + 'px' - el.style.top = Math.max(8, top) + 'px' + var gap = 8 + // 浮窗只允许出现在正文可见区里(避开会话头部与整块输入区):批注条数多 + // 时先把它限制在这个高度内、内部滚动——否则十来个批注会把浮窗顶出屏幕 + // 或压到输入框上,看起来就是"浮窗显示错乱"。 + var topLimit = gap + var heads = hostHeaderRects() + for (var hi = 0; hi < heads.length; hi++) { + if (r2.left + w2 <= heads[hi].left || r2.left >= heads[hi].right) continue + if (heads[hi].bottom + gap > topLimit) topLimit = heads[hi].bottom + gap + } + var bottomLimit = window.innerHeight - gap + var seats = hostComposerRects() + for (var si = 0; si < seats.length; si++) { + if (r2.left + w2 <= seats[si].left || r2.left >= seats[si].right) continue + if (seats[si].top - gap < bottomLimit) bottomLimit = seats[si].top - gap + } + var room = Math.round(r2.top - gap - topLimit) + if (room < 120) room = Math.round(bottomLimit - r2.bottom - gap) + room = Math.max(80, room) + el.style.maxHeight = room + 'px' + el.style.overflowY = 'auto' + var h2 = Math.min(el.offsetHeight || 120, room) + var top = r2.top - h2 - gap + if (top < topLimit) top = r2.bottom + gap + if (top + h2 > bottomLimit) top = bottomLimit - h2 + el.style.left = Math.max(8, Math.min(r2.left, window.innerWidth - w2 - 8)) + 'px' + el.style.top = Math.round(Math.max(topLimit, top)) + 'px' el.style.width = w2 + 'px' } @@ -2160,7 +2181,7 @@ window.__ModuleLoader__.load({ tag.addEventListener('mouseenter', function () { var el = document.createElement('div') el.className = 'dsh-ann-tip' - el.style.cssText = 'position:fixed;z-index:1160;width:300px;max-width:calc(100vw - 16px);padding:10px 12px;border-radius:12px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu,#2c2c2e);box-shadow:var(--dsw-shadow-lv3);font-family:var(--dsw-font-family,system-ui);font-size:12px;color:var(--dsw-alias-label-primary);' + el.style.cssText = 'position:fixed;z-index:1160;box-sizing:border-box;width:300px;max-width:calc(100vw - 16px);padding:10px 12px;border-radius:12px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu,#2c2c2e);box-shadow:var(--dsw-shadow-lv3);font-family:var(--dsw-font-family,system-ui);font-size:12px;color:var(--dsw-alias-label-primary);' var head = document.createElement('div') head.style.cssText = 'font-weight:600;margin-bottom:6px;' head.textContent = t('bubble.title', { n: list.length }) diff --git a/test/overlay-browser.mjs b/test/overlay-browser.mjs index a525e8c..2922348 100644 --- a/test/overlay-browser.mjs +++ b/test/overlay-browser.mjs @@ -110,7 +110,7 @@ try { assert.equal(headerClip, false, '会话头部区域内的高亮被裁掉,不盖在头部之上') await page.locator('.dsh-ann-card-head .dsh-ann-icon').click() assert.equal(await page.locator('.dsh-ann-card').count(), 0, '关闭按钮不触发拖动') - // 编号跨在头部边界上时:要么完整让到头部下方,要么整条不画,绝不剩半截残影。 + // 编号跨在头部边界上时:整条不画(不贴边钉住),绝不剩半截残影。 await page.evaluate(() => { document.querySelector('[data-chat-flow-kind="assistant-step"]').style.top = '70px' window.dispatchEvent(new Event('resize')) @@ -124,14 +124,72 @@ try { return { hidden: false, top: Math.round(r.top), headerBottom: Math.round(header.bottom), full: r.top >= header.bottom } }) assert.ok(straddle.hidden || straddle.full, `编号不能跨在头部边界上留下半截:${JSON.stringify(straddle)}`) + + // 批注很多时,清单浮窗必须完整待在正文可见区里(不越出视口、不压到输入框上)。 + await page.setViewportSize({ width: 1000, height: 800 }) + await page.evaluate(() => { + const composer = document.querySelector('[data-composer-card]') + composer.style.left = '200px' + composer.style.top = '600px' + window.dispatchEvent(new Event('resize')) + }) + await page.waitForTimeout(150) + for (let n = 0; n < 8; n++) { + await page.evaluate((index) => { + const row = document.querySelector('[data-chat-flow-kind="assistant-step"]') + let p = document.querySelector('#extra-quote-' + index) + if (p === null) { + p = document.createElement('p') + p.id = 'extra-quote-' + index + p.textContent = '第 ' + (index + 1) + ' 段用于堆高批注清单的原文内容。' + row.appendChild(p) + } + const range = document.createRange() + range.selectNodeContents(p) + getSelection().removeAllRanges() + getSelection().addRange(range) + }, n) + await page.waitForTimeout(150) + await page.locator('.dsh-ann-bar button').click() + await page.locator('.dsh-ann-input').fill('批注内容 ' + (n + 1)) + await page.locator('.dsh-ann-action').click() + await page.waitForTimeout(80) + } + // 直接派发 mouseenter(悬停面板本身会挡住指针检查,用事件触发等价路径)。 + await page.evaluate(() => { + document.querySelector('[data-annotation-chip]').dispatchEvent(new MouseEvent('mouseenter')) + }) + await page.locator('.dsh-ann-tip').waitFor({ state: 'visible', timeout: 5000 }) + const tip = await page.evaluate(() => { + const el = document.querySelector('.dsh-ann-tip') + const r = el.getBoundingClientRect() + const composer = document.querySelector('[data-composer-card]').getBoundingClientRect() + return { + title: (el.firstChild === null ? '' : el.firstChild.textContent) || '', + rect: { t: Math.round(r.top), b: Math.round(r.bottom), l: Math.round(r.left), r: Math.round(r.right) }, + viewport: { w: window.innerWidth, h: window.innerHeight }, + inViewport: r.top >= 0 && r.bottom <= window.innerHeight + 1 && r.left >= 0 && r.right <= window.innerWidth + 1, + overlapsComposer: r.left < composer.right && r.right > composer.left && r.top < composer.bottom && r.bottom > composer.top, + scrollable: el.scrollHeight > el.clientHeight + 1, + } + }) + assert.ok(tip.inViewport, `清单浮窗必须完整在视口内:${JSON.stringify(tip)}`) + assert.equal(tip.overlapsComposer, false, `清单浮窗不能压到输入框上:${JSON.stringify(tip)}`) + // 标题右侧的删除按钮:连同标记与待发送清单一起删掉。 - await page.locator('.dsh-ann-num').click() + const chipCount = async () => { + const text = (await page.locator('[data-annotation-chip]').textContent()) || '' + const hit = text.match(/\d+/) + return hit === null ? 0 : Number(hit[0]) + } + const beforeCount = await chipCount() + await page.locator('.dsh-ann-num').first().click() await page.locator('.dsh-ann-card-head .dsh-ann-qdel').click() - assert.equal(await page.locator('.dsh-ann-num').count(), 0, '删除按钮移除该批注的标记') + assert.equal(await chipCount(), beforeCount - 1, '删除按钮把该批注从待发送清单里移除') assert.equal(await page.locator('.dsh-ann-card').count(), 0, '删除后编辑窗口关闭') await page.evaluate(() => window.dispose()) assert.deepEqual(errors, []) - console.log('PASS: 工具条下方定位、标题栏拖动、边界、缩放、保存、裁剪、头部遮挡、边界残影、重新编辑、删除、无页面错误') + console.log('PASS: 工具条下方定位、标题栏拖动、边界、缩放、保存、裁剪、头部遮挡、边界残影、清单浮窗、重新编辑、删除、无页面错误') } finally { await browser.close() } From 51b9aa066bc64cc4492f474e66931cd91f5da136 Mon Sep 17 00:00:00 2001 From: movcat Date: Sun, 27 Sep 2026 19:36:50 +0800 Subject: [PATCH 4/6] =?UTF-8?q?fix=20=E5=9B=9E=E5=A4=8D=E8=8A=AF=E7=89=87?= =?UTF-8?q?=E6=89=B9=E6=B3=A8=E8=A7=A3=E6=9E=90=E5=AF=BC=E8=87=B4=E7=9A=84?= =?UTF-8?q?=E5=8D=A1=E6=AD=BB=20fix=20=E5=9B=9E=E5=A4=8D=E8=8A=AF=E7=89=87?= =?UTF-8?q?=E6=AD=A3=E5=88=99=E6=9D=A1=E4=BB=B6=E6=94=BE=E5=AE=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 3 ++ client.js | 101 +++++++++++++++++++++++++++++---------------------- 2 files changed, 61 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57d6001..dc9bd4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,12 @@ ## [Unreleased] ### 修复 +- **修复「插件一运行客户端就卡死」**:装饰动作会改写宿主 DOM 并再次触发 `MutationObserver`,而越界编号在「不生成芯片、保留原文」的分支里仍然无条件 `replaceChild`——等于用一个内容相同的新节点替换原节点,mutation 永不收敛,装饰与观察者互相触发成自激,把主线程打满(表现为整个客户端失去响应)。现在一个可替换项都没有时**完全不碰 DOM**;另外给全量装饰加了 120ms 合并调度(首拍仍立即执行,保住「绘制前隐藏、零闪烁」),避免长会话里装饰自身滚成高频全量扫描。 - 适配 DSH 0.1.7:`sessions.list` 快照不再暴露 `current`,当前会话 id 改经 `uiSession.adapter.current`(`binding.key`)读取,旧内核保留原字段兜底,会话切换订阅同步跟随。修复 0.1.7+ 上「批注标签在、但回车/发送按钮都没反应」——拼稿前取不到会话 id 会直接返回,空草稿时宿主又禁用发送,只有手动输入正文才发得出去。 ### 交互 +- 回复芯片匹配放宽:不再强制编号后面跟冒号——模型写成「**Annotation 1** —— 说明」「Annotation 2. 说明」这类变体同样会被装饰成芯片(此前只认协议里的「Annotation N:」,模型换写法时整段回复停在纯文本)。编号仍必须能对应到批注条目,越界编号与只有 "Annotation" 词的句子原样保留。 +- 回复芯片只服务真正的逐条回应:编号找不到对应批注条目(整行拿不到条目数据,或模型自己写的越界编号如「Annotation 5:」而只有 3 条)时一律**不生成芯片**、保留原文;正文里出现 "Annotation" 但不构成「Annotation N:」时同样静默,不再输出「未匹配到 Annotation N 模式」与「未找到对应批注条目」这类诊断/占位文案(相关诊断函数与标记一并移除)。能对应上的芯片 hover 照常显示原文与批注。 - 编号只出现在会话正文可见区内(图层独立,`z-index 1100`,不跟随宿主文档结构):遮挡区取整块输入区(composer 容器,含输入卡片下方的统计行)与会话头部,编号必须落在正文滚动容器里、且不与遮挡区相交才渲染;不满足就整条不显示——跟正文一样,滚出可视区即消失,不贴边钉住,也不会被裁成半截残影。修复编号"压在输入框上"、"显示在对话区下面"或盖在会话头部上的问题。 - 批注清单浮窗(悬停「批注 ×N」标签):宽度改为 `border-box`(此前 300px 内容宽再加内外边距会顶出屏幕右边缘),并把浮窗限制在正文可见区内(避开会话头部与整块输入区),批注条数多时在浮窗内部滚动——修掉「批注多时浮窗溢出屏幕、压住输入框」。 - 标记层挖去会话头部与输入区:高亮滚到这些区域时会被遮住,不再盖在上面。 diff --git a/client.js b/client.js index a548935..fe1ca9d 100644 --- a/client.js +++ b/client.js @@ -184,9 +184,7 @@ window.__ModuleLoader__.load({ bubble: { tag: '批注 ×{n}', title: '本消息携带批注({n} 条)' }, reply: { headWithQuote: '批注 {n} 的原文', - headNoQuote: '批注 {n}', notePrefix: '你的批注:', - missing: '(未找到对应批注条目)', }, toast: { attachFail: '批注拼稿失败,消息将不带批注发送:', @@ -222,9 +220,7 @@ window.__ModuleLoader__.load({ bubble: { tag: 'Annotations ×{n}', title: 'This message carries {n} annotation(s)' }, reply: { headWithQuote: 'Source of annotation {n}', - headNoQuote: 'Annotation {n}', notePrefix: 'Your note: ', - missing: '(no matching annotation found)', }, toast: { attachFail: 'Failed to attach annotations; the message will be sent without them: ', @@ -1237,7 +1233,7 @@ window.__ModuleLoader__.load({ if (mutations[i].type === 'childList') { hasRowInsert = true; break } } if (hasRowInsert) { - decorateAll() + scheduleDecorateAll() return } // 流式批次: 只做助手回复芯片的限流装饰(行内 data-streaming 守卫已保证 @@ -2255,13 +2251,6 @@ window.__ModuleLoader__.load({ return null } - /** 一次性诊断(仅控制台,不打扰用户):标记行 + 原因。 */ - function markRowDiag(row, msg) { - if (row.hasAttribute('data-annotation-diag')) return - row.setAttribute('data-annotation-diag', '') - console.warn('[annotation] ' + msg, row) - } - /** 扫描所有已结束流式输出的助手行:把「Annotation N:」替换为可悬浮芯片。 */ /** 流式 mutation 批次的芯片装饰限流: 前导 + 500ms 拖尾, 每批次最多触发一次 * 增量扫描(只扫助手行), 代替逐批全文档 decorateAll。 */ @@ -2295,18 +2284,16 @@ window.__ModuleLoader__.load({ if (el.querySelector('[data-annotation-reply-chip]') !== null) continue if ((el.textContent || '').indexOf('Annotation') === -1) continue var items = findPrevAnnotationItems(el) - if (items === null || items.length === 0) { - // 拿不到条目数据也照样生成芯片(hover 显示占位),并一次性提示。 - items = [] - markRowDiag(el, '未找到批注条目数据,芯片将显示占位内容(可继续用)') - } + // 拿不到条目数据就干脆不生成芯片,保留原文「Annotation N:」—— + // 一个 hover 只显示「未找到对应批注条目」的占位芯片没有任何价值。 + if (items === null || items.length === 0) continue decorateAnnotationLabels(el, items) } } /** 在行内所有文本节点中找「Annotation N:」(不区分大小写),替换为芯片。 */ function decorateAnnotationLabels(row, items) { - var re = /Annotation[\s\u200b\u200c\u200d\u00ad]*(\d+)[\s\u200b\u200c\u200d\u00ad]*[::]/gi + var re = /Annotation[\s\u200b\u200c\u200d\u00ad]*(\d+)[\s\u200b\u200c\u200d\u00ad]*[::]?/gi // 先快照所有文本节点,再逐个处理:遍历中途修改树会让 TreeWalker // 指针失效(处理完第一个节点后遍历就断了)——这是「只有第一个 // Annotation 变成芯片」的根因。 @@ -2325,30 +2312,43 @@ window.__ModuleLoader__.load({ var last = 0 re.lastIndex = 0 var m + var replaced = 0 while ((m = re.exec(v)) !== null) { if (m.index > last) frag.appendChild(document.createTextNode(v.slice(last, m.index))) - frag.appendChild(makeReplyChip(parseInt(m[1], 10), items)) + var num = parseInt(m[1], 10) + var item = items[num - 1] + // 编号越界(比如模型自己在思考里写了「Annotation 5:」,实际只有 3 条) + // 不是真正的逐条回应:原文照旧,不生成芯片。 + if (item !== undefined) { + frag.appendChild(makeReplyChip(num, item)) + replaced++ + done++ + } else { + frag.appendChild(document.createTextNode(m[0])) + } last = m.index + m[0].length - done++ } + // 一个都没真正替换时绝不碰 DOM:否则 replaceChild 会用一个内容相同的 + // 新节点换上,产生 childList mutation → MutationObserver → 全量装饰 → + // 再替换…… 自激成死循环,把客户端主线程打满。 + if (replaced === 0) continue if (last < v.length) frag.appendChild(document.createTextNode(v.slice(last))) n.parentNode.replaceChild(frag, n) } + // 匹配不到完整「Annotation N:」时静默:正文里出现 annotation 这个词 + // (模型自己的措辞、英文注解说明等)本来就不该产生任何日志。 if (done > 0) { console.log('[annotation] 回复批注芯片 ×' + done, row.querySelectorAll('[data-annotation-reply-chip]').length + ' 个元素') - } else { - // 行内含 Annotation 但一个都没匹配上 → 文本节点里没有完整「Annotation N:」模式 - markRowDiag(row, '行内含 Annotation 但未匹配到「Annotation N:」模式') } } - /** 构造「Annotation N」芯片(hover 显示该批注的原文与批注内容)。 */ - function makeReplyChip(num, items) { + /** 构造「Annotation N」芯片(hover 显示该批注的原文与批注内容)。 + * `item` 必定存在:调用方只在编号能对应到批注条目时才生成芯片。 */ + function makeReplyChip(num, item) { var chip = document.createElement('span') chip.setAttribute('data-annotation-reply-chip', '') chip.style.cssText = 'display:inline-flex;align-items:center;height:18px;padding:0 6px;margin:0 2px;border-radius:9px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu,#2c2c2e);color:var(--dsw-alias-text-accent,#4c9aff);font-family:var(--dsw-font-family,system-ui);font-size:11px;font-weight:600;cursor:default;vertical-align:middle;' chip.textContent = 'Annotation ' + num - var item = items[num - 1] var grace = null function hide() { if (grace !== null) clearTimeout(grace) @@ -2363,24 +2363,17 @@ window.__ModuleLoader__.load({ el.style.cssText = 'position:fixed;z-index:1160;width:320px;max-width:calc(100vw - 16px);padding:10px 12px;border-radius:12px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu,#2c2c2e);box-shadow:var(--dsw-shadow-lv3);font-family:var(--dsw-font-family,system-ui);font-size:12px;color:var(--dsw-alias-label-primary);' var head = document.createElement('div') head.style.cssText = 'font-weight:600;margin-bottom:6px;' - head.textContent = item !== undefined ? t('reply.headWithQuote', { n: num }) : t('reply.headNoQuote', { n: num }) + head.textContent = t('reply.headWithQuote', { n: num }) el.appendChild(head) - if (item !== undefined) { - var quote = document.createElement('div') - quote.style.cssText = 'font-size:12px;line-height:1.5;color:var(--dsw-alias-label-secondary);word-break:break-word;padding:6px 8px;border-radius:8px;background:rgba(127,127,127,.12);' - quote.textContent = truncate(item.text, 140) - el.appendChild(quote) - if (item.note !== '') { - var note = document.createElement('div') - note.style.cssText = 'font-size:11px;color:var(--dsw-alias-text-accent,#4c9aff);margin-top:6px;word-break:break-word;' - note.textContent = t('reply.notePrefix') + truncate(item.note, 80) - el.appendChild(note) - } - } else { - var none = document.createElement('div') - none.style.cssText = 'font-size:11px;color:var(--dsw-alias-label-tertiary);' - none.textContent = t('reply.missing') - el.appendChild(none) + var quote = document.createElement('div') + quote.style.cssText = 'font-size:12px;line-height:1.5;color:var(--dsw-alias-label-secondary);word-break:break-word;padding:6px 8px;border-radius:8px;background:rgba(127,127,127,.12);' + quote.textContent = truncate(item.text, 140) + el.appendChild(quote) + if (typeof item.note === 'string' && item.note !== '') { + var note = document.createElement('div') + note.style.cssText = 'font-size:11px;color:var(--dsw-alias-text-accent,#4c9aff);margin-top:6px;word-break:break-word;' + note.textContent = t('reply.notePrefix') + truncate(item.note, 80) + el.appendChild(note) } presentTip(chip, el) var r2 = chip.getBoundingClientRect() @@ -2431,6 +2424,27 @@ window.__ModuleLoader__.load({ } var decoTimer = null + /** 全量装饰的合并调度:首个批次立即执行(气泡隐藏要赶在浏览器绘制前, + * 否则会闪一下批注块),紧随其后的批次合并进 120ms 窗口。 + * 原因:装饰本身会改宿主 DOM(贴标签、换芯片、切气泡文本)并再次触发 + * MutationObserver,一条回复上的多次替换会在长会话里滚成高频全量扫描, + * 把主线程烧满——表现就是客户端卡死、什么都点不动。 */ + var decorateAllPending = null + var lastDecorateAllAt = 0 + function scheduleDecorateAll() { + var now = performance.now() + if (now - lastDecorateAllAt >= 120) { + lastDecorateAllAt = now + decorateAll() + return + } + if (decorateAllPending !== null) return + decorateAllPending = setTimeout(function () { + decorateAllPending = null + lastDecorateAllAt = performance.now() + decorateAll() + }, 120) + } function kickDecorate() { decorateAll() if (decoTimer === null) decoTimer = setInterval(decorateAll, 1000) @@ -2514,6 +2528,7 @@ window.__ModuleLoader__.load({ if (typeof inputUnsub === 'function') inputUnsub() if (typeof localeUnsub === 'function') localeUnsub() if (decoTimer !== null) { clearInterval(decoTimer); decoTimer = null } + if (decorateAllPending !== null) { clearTimeout(decorateAllPending); decorateAllPending = null } if (composerObserver !== null) composerObserver.disconnect() chipLayer.remove() tipLayer.remove() From 75cdb7f7dd34b5b6419c07a3df4542508fcd847a Mon Sep 17 00:00:00 2001 From: movcat Date: Sun, 27 Sep 2026 19:37:53 +0800 Subject: [PATCH 5/6] =?UTF-8?q?hover=20=E8=8A=AF=E7=89=87=E9=80=89?= =?UTF-8?q?=E4=B8=AD=E5=88=99=E9=80=80=E5=87=BA=E9=80=8F=E6=98=8E=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + client.js | 33 ++++++++++++++++++++++++++------- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc9bd4c..18c3b6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - 适配 DSH 0.1.7:`sessions.list` 快照不再暴露 `current`,当前会话 id 改经 `uiSession.adapter.current`(`binding.key`)读取,旧内核保留原字段兜底,会话切换订阅同步跟随。修复 0.1.7+ 上「批注标签在、但回车/发送按钮都没反应」——拼稿前取不到会话 id 会直接返回,空草稿时宿主又禁用发送,只有手动输入正文才发得出去。 ### 交互 +- 浮层底色改成「默认半透明、鼠标移入变实」:默认沿用 DSH 的菜单色(`--dsw-specific-menu`,暗色 `#303136f0`、浅色 `#f8f9faf0`)——悬浮在正文上时能看穿下面的文字;鼠标移进去后**带 0.15s 过渡**换成不透明的主题底色(`--dsw-alias-bg-layer-2`,暗色 `#2c2c2e`、浅色白),方便专心读批注内容。覆盖回复芯片、批注 ×N 标签、各类 hover 浮窗(回复芯片面板 / 批注清单 / 气泡标签面板)、选区工具条、编辑卡片与 toast。 - 回复芯片匹配放宽:不再强制编号后面跟冒号——模型写成「**Annotation 1** —— 说明」「Annotation 2. 说明」这类变体同样会被装饰成芯片(此前只认协议里的「Annotation N:」,模型换写法时整段回复停在纯文本)。编号仍必须能对应到批注条目,越界编号与只有 "Annotation" 词的句子原样保留。 - 回复芯片只服务真正的逐条回应:编号找不到对应批注条目(整行拿不到条目数据,或模型自己写的越界编号如「Annotation 5:」而只有 3 条)时一律**不生成芯片**、保留原文;正文里出现 "Annotation" 但不构成「Annotation N:」时同样静默,不再输出「未匹配到 Annotation N 模式」与「未找到对应批注条目」这类诊断/占位文案(相关诊断函数与标记一并移除)。能对应上的芯片 hover 照常显示原文与批注。 - 编号只出现在会话正文可见区内(图层独立,`z-index 1100`,不跟随宿主文档结构):遮挡区取整块输入区(composer 容器,含输入卡片下方的统计行)与会话头部,编号必须落在正文滚动容器里、且不与遮挡区相交才渲染;不满足就整条不显示——跟正文一样,滚出可视区即消失,不贴边钉住,也不会被裁成半截残影。修复编号"压在输入框上"、"显示在对话区下面"或盖在会话头部上的问题。 diff --git a/client.js b/client.js index fe1ca9d..52f7935 100644 --- a/client.js +++ b/client.js @@ -153,6 +153,25 @@ window.__ModuleLoader__.load({ 'body:has([role="dialog"][aria-modal="true"]) [data-annotation-overlay],', 'body:has([role="dialog"][aria-modal="true"]) [data-annotation-num-layer] { display: none; }', '.dsh-ann-tip { animation: dsh-ann-pop .12s var(--ds-ease-in-out, ease); }', + // 默认沿用带 0.94 alpha 的菜单色:悬浮在正文上时能看穿下面的文字(双赢); + // 鼠标移进去才换成不透明的主题底色,方便专心读批注内容。内联样式优先级 + // 更高,所以这里必须 !important。 + '.dsh-ann-tip:hover,', + '[data-annotation-reply-chip]:hover,', + '[data-annotation-bubble-tag]:hover,', + '[data-annotation-chip]:hover,', + '.dsh-ann-bar:hover,', + '.dsh-ann-card:hover,', + '[data-annotation-toast]:hover {', + ' background: var(--dsw-alias-bg-layer-2, #2c2c2e) !important; }', + // 半透明 ↔ 不透明之间过渡一下,鼠标进出时不会「啪」地闪一下。 + '.dsh-ann-tip,', + '[data-annotation-reply-chip],', + '[data-annotation-bubble-tag],', + '[data-annotation-chip],', + '.dsh-ann-bar,', + '.dsh-ann-card,', + '[data-annotation-toast] { transition: background-color .15s ease; }', '@keyframes dsh-ann-fadein { from { opacity: 0; } to { opacity: 1; } }', ].join('\n') document.head.appendChild(style) @@ -1053,7 +1072,7 @@ window.__ModuleLoader__.load({ var el = document.createElement('div') el.setAttribute('data-annotation-toast', '') el.textContent = msg - el.style.cssText = 'position:fixed;z-index:1300;left:50%;bottom:88px;transform:translateX(-50%);max-width:min(420px,calc(100vw - 24px));padding:8px 14px;border-radius:10px;background:var(--dsw-specific-menu,#2c2c2e);border:1px solid var(--dsw-alias-border-inverted);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family,system-ui);font-size:12px;pointer-events:none;' + el.style.cssText = 'position:fixed;z-index:1300;left:50%;bottom:88px;transform:translateX(-50%);max-width:min(420px,calc(100vw - 24px));padding:8px 14px;border-radius:10px;background:var(--dsw-specific-menu, #2c2c2e);border:1px solid var(--dsw-alias-border-inverted);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family,system-ui);font-size:12px;pointer-events:none;' document.body.appendChild(el) if (toastTimer !== null) clearTimeout(toastTimer) toastTimer = setTimeout(function () { @@ -1868,7 +1887,7 @@ window.__ModuleLoader__.load({ // ---------- 输入框旁的批注标签(N 条批注 · 悬浮看全部内容) ---------- var chipLayer = document.createElement('div') chipLayer.setAttribute('data-annotation-chip', '') - chipLayer.style.cssText = 'position:fixed;z-index:1150;display:none;align-items:center;gap:4px;height:22px;padding:0 10px;border-radius:11px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu,#2c2c2e);box-shadow:var(--dsw-shadow-lv3);font-family:var(--dsw-font-family,system-ui);font-size:11px;color:var(--dsw-alias-label-primary);cursor:default;animation:dsh-ann-pop .12s var(--ds-ease-in-out, ease);' + chipLayer.style.cssText = 'position:fixed;z-index:1150;display:none;align-items:center;gap:4px;height:22px;padding:0 10px;border-radius:11px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu, #2c2c2e);box-shadow:var(--dsw-shadow-lv3);font-family:var(--dsw-font-family,system-ui);font-size:11px;color:var(--dsw-alias-label-primary);cursor:default;animation:dsh-ann-pop .12s var(--ds-ease-in-out, ease);' document.body.appendChild(chipLayer) var tipLayer = document.createElement('div') tipLayer.setAttribute('data-annotation-tip-layer', '') @@ -1947,7 +1966,7 @@ window.__ModuleLoader__.load({ if (ui.quotes.length === 0) return var el = document.createElement('div') el.className = 'dsh-ann-tip' - el.style.cssText = 'position:fixed;z-index:1160;box-sizing:border-box;width:300px;max-width:calc(100vw - 16px);padding:10px 12px;border-radius:12px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu,#2c2c2e);box-shadow:var(--dsw-shadow-lv3);font-family:var(--dsw-font-family,system-ui);font-size:12px;color:var(--dsw-alias-label-primary);' + el.style.cssText = 'position:fixed;z-index:1160;box-sizing:border-box;width:300px;max-width:calc(100vw - 16px);padding:10px 12px;border-radius:12px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu, #2c2c2e);box-shadow:var(--dsw-shadow-lv3);font-family:var(--dsw-font-family,system-ui);font-size:12px;color:var(--dsw-alias-label-primary);' var head = document.createElement('div') head.style.cssText = 'font-weight:600;margin-bottom:6px;' head.textContent = t('tip.title', { n: ui.quotes.length }) @@ -2172,12 +2191,12 @@ window.__ModuleLoader__.load({ var tag = document.createElement('span') tag.setAttribute('data-annotation-bubble-tag', '') tag.textContent = t('bubble.tag', { n: items.length }) - tag.style.cssText = 'display:inline-flex;align-items:center;height:18px;padding:0 8px;margin:4px 0 0 4px;border-radius:9px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu,#2c2c2e);color:var(--dsw-alias-label-secondary);font-family:var(--dsw-font-family,system-ui);font-size:10px;cursor:default;' + tag.style.cssText = 'display:inline-flex;align-items:center;height:18px;padding:0 8px;margin:4px 0 0 4px;border-radius:9px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu, #2c2c2e);color:var(--dsw-alias-label-secondary);font-family:var(--dsw-font-family,system-ui);font-size:10px;cursor:default;' ;(function (list) { tag.addEventListener('mouseenter', function () { var el = document.createElement('div') el.className = 'dsh-ann-tip' - el.style.cssText = 'position:fixed;z-index:1160;box-sizing:border-box;width:300px;max-width:calc(100vw - 16px);padding:10px 12px;border-radius:12px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu,#2c2c2e);box-shadow:var(--dsw-shadow-lv3);font-family:var(--dsw-font-family,system-ui);font-size:12px;color:var(--dsw-alias-label-primary);' + el.style.cssText = 'position:fixed;z-index:1160;box-sizing:border-box;width:300px;max-width:calc(100vw - 16px);padding:10px 12px;border-radius:12px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu, #2c2c2e);box-shadow:var(--dsw-shadow-lv3);font-family:var(--dsw-font-family,system-ui);font-size:12px;color:var(--dsw-alias-label-primary);' var head = document.createElement('div') head.style.cssText = 'font-weight:600;margin-bottom:6px;' head.textContent = t('bubble.title', { n: list.length }) @@ -2347,7 +2366,7 @@ window.__ModuleLoader__.load({ function makeReplyChip(num, item) { var chip = document.createElement('span') chip.setAttribute('data-annotation-reply-chip', '') - chip.style.cssText = 'display:inline-flex;align-items:center;height:18px;padding:0 6px;margin:0 2px;border-radius:9px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu,#2c2c2e);color:var(--dsw-alias-text-accent,#4c9aff);font-family:var(--dsw-font-family,system-ui);font-size:11px;font-weight:600;cursor:default;vertical-align:middle;' + chip.style.cssText = 'display:inline-flex;align-items:center;height:18px;padding:0 6px;margin:0 2px;border-radius:9px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu, #2c2c2e);color:var(--dsw-alias-text-accent,#4c9aff);font-family:var(--dsw-font-family,system-ui);font-size:11px;font-weight:600;cursor:default;vertical-align:middle;' chip.textContent = 'Annotation ' + num var grace = null function hide() { @@ -2360,7 +2379,7 @@ window.__ModuleLoader__.load({ chip.addEventListener('mouseenter', function () { var el = document.createElement('div') el.className = 'dsh-ann-tip' - el.style.cssText = 'position:fixed;z-index:1160;width:320px;max-width:calc(100vw - 16px);padding:10px 12px;border-radius:12px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu,#2c2c2e);box-shadow:var(--dsw-shadow-lv3);font-family:var(--dsw-font-family,system-ui);font-size:12px;color:var(--dsw-alias-label-primary);' + el.style.cssText = 'position:fixed;z-index:1160;width:320px;max-width:calc(100vw - 16px);padding:10px 12px;border-radius:12px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu, #2c2c2e);box-shadow:var(--dsw-shadow-lv3);font-family:var(--dsw-font-family,system-ui);font-size:12px;color:var(--dsw-alias-label-primary);' var head = document.createElement('div') head.style.cssText = 'font-weight:600;margin-bottom:6px;' head.textContent = t('reply.headWithQuote', { n: num }) From 83bb46ee48a6aefdcf4e24cd22557ba8a51cdce5 Mon Sep 17 00:00:00 2001 From: movcat Date: Tue, 29 Sep 2026 06:04:52 +0800 Subject: [PATCH 6/6] =?UTF-8?q?update=20=E9=80=8F=E6=98=8E=E8=89=B2?= =?UTF-8?q?=E5=85=BC=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 +- client.js | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18c3b6b..2fea571 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - 适配 DSH 0.1.7:`sessions.list` 快照不再暴露 `current`,当前会话 id 改经 `uiSession.adapter.current`(`binding.key`)读取,旧内核保留原字段兜底,会话切换订阅同步跟随。修复 0.1.7+ 上「批注标签在、但回车/发送按钮都没反应」——拼稿前取不到会话 id 会直接返回,空草稿时宿主又禁用发送,只有手动输入正文才发得出去。 ### 交互 -- 浮层底色改成「默认半透明、鼠标移入变实」:默认沿用 DSH 的菜单色(`--dsw-specific-menu`,暗色 `#303136f0`、浅色 `#f8f9faf0`)——悬浮在正文上时能看穿下面的文字;鼠标移进去后**带 0.15s 过渡**换成不透明的主题底色(`--dsw-alias-bg-layer-2`,暗色 `#2c2c2e`、浅色白),方便专心读批注内容。覆盖回复芯片、批注 ×N 标签、各类 hover 浮窗(回复芯片面板 / 批注清单 / 气泡标签面板)、选区工具条、编辑卡片与 toast。 +- 浮层底色改成「默认半透明、鼠标移入变实」:默认半透明,悬浮在正文上时能看穿下面的文字;鼠标移进去后**带 0.15s 过渡**换成不透明底色,方便专心读批注内容。半透明**不再依赖宿主变量自带 alpha**(Windows 上 `--dsw-specific-menu` 可能缺失或本身不透明),改用 `color-mix(in srgb, <主题色> 94%, transparent)` 自行生成半透明底,并做多级回退(`--dsw-alias-bg-layer-2` → `--dsw-alias-bg-base` → 深灰),保证明暗主题都能跟随。覆盖回复芯片、批注 ×N 标签、各类 hover 浮窗(回复芯片面板 / 批注清单 / 气泡标签面板)、选区工具条、编辑卡片与 toast。 - 回复芯片匹配放宽:不再强制编号后面跟冒号——模型写成「**Annotation 1** —— 说明」「Annotation 2. 说明」这类变体同样会被装饰成芯片(此前只认协议里的「Annotation N:」,模型换写法时整段回复停在纯文本)。编号仍必须能对应到批注条目,越界编号与只有 "Annotation" 词的句子原样保留。 - 回复芯片只服务真正的逐条回应:编号找不到对应批注条目(整行拿不到条目数据,或模型自己写的越界编号如「Annotation 5:」而只有 3 条)时一律**不生成芯片**、保留原文;正文里出现 "Annotation" 但不构成「Annotation N:」时同样静默,不再输出「未匹配到 Annotation N 模式」与「未找到对应批注条目」这类诊断/占位文案(相关诊断函数与标记一并移除)。能对应上的芯片 hover 照常显示原文与批注。 - 编号只出现在会话正文可见区内(图层独立,`z-index 1100`,不跟随宿主文档结构):遮挡区取整块输入区(composer 容器,含输入卡片下方的统计行)与会话头部,编号必须落在正文滚动容器里、且不与遮挡区相交才渲染;不满足就整条不显示——跟正文一样,滚出可视区即消失,不贴边钉住,也不会被裁成半截残影。修复编号"压在输入框上"、"显示在对话区下面"或盖在会话头部上的问题。 diff --git a/client.js b/client.js index 52f7935..371bd37 100644 --- a/client.js +++ b/client.js @@ -62,6 +62,7 @@ window.__ModuleLoader__.load({ ' gap: 2px; padding: 4px; border-radius: 12px;', ' border: 1px solid var(--dsw-alias-border-inverted);', ' background: var(--dsw-specific-menu, #2c2c2e);', + ' background: color-mix(in srgb, var(--dsw-specific-menu, #2c2c2e) 94%, transparent);', ' box-shadow: var(--dsw-shadow-lv3);', ' font-family: var(--dsw-font-family, system-ui);', ' animation: dsh-ann-pop .12s var(--ds-ease-in-out, ease); }', @@ -92,6 +93,7 @@ window.__ModuleLoader__.load({ ' max-width: calc(100vw - 16px); padding: 12px; border-radius: 12px;', ' border: 1px solid var(--dsw-alias-border-inverted);', ' background: var(--dsw-specific-menu, #2c2c2e);', + ' background: color-mix(in srgb, var(--dsw-specific-menu, #2c2c2e) 94%, transparent);', ' box-shadow: var(--dsw-shadow-lv3);', ' font-family: var(--dsw-font-family, system-ui);', ' animation: dsh-ann-pop .12s var(--ds-ease-in-out, ease); }', @@ -163,7 +165,7 @@ window.__ModuleLoader__.load({ '.dsh-ann-bar:hover,', '.dsh-ann-card:hover,', '[data-annotation-toast]:hover {', - ' background: var(--dsw-alias-bg-layer-2, #2c2c2e) !important; }', + ' background: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-base, #2c2c2e)) !important; }', // 半透明 ↔ 不透明之间过渡一下,鼠标进出时不会「啪」地闪一下。 '.dsh-ann-tip,', '[data-annotation-reply-chip],', @@ -1072,7 +1074,7 @@ window.__ModuleLoader__.load({ var el = document.createElement('div') el.setAttribute('data-annotation-toast', '') el.textContent = msg - el.style.cssText = 'position:fixed;z-index:1300;left:50%;bottom:88px;transform:translateX(-50%);max-width:min(420px,calc(100vw - 24px));padding:8px 14px;border-radius:10px;background:var(--dsw-specific-menu, #2c2c2e);border:1px solid var(--dsw-alias-border-inverted);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family,system-ui);font-size:12px;pointer-events:none;' + el.style.cssText = 'position:fixed;z-index:1300;left:50%;bottom:88px;transform:translateX(-50%);max-width:min(420px,calc(100vw - 24px));padding:8px 14px;border-radius:10px;background:var(--dsw-specific-menu, #2c2c2e);background:color-mix(in srgb, var(--dsw-specific-menu, #2c2c2e) 94%, transparent);border:1px solid var(--dsw-alias-border-inverted);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family,system-ui);font-size:12px;pointer-events:none;' document.body.appendChild(el) if (toastTimer !== null) clearTimeout(toastTimer) toastTimer = setTimeout(function () { @@ -1887,7 +1889,7 @@ window.__ModuleLoader__.load({ // ---------- 输入框旁的批注标签(N 条批注 · 悬浮看全部内容) ---------- var chipLayer = document.createElement('div') chipLayer.setAttribute('data-annotation-chip', '') - chipLayer.style.cssText = 'position:fixed;z-index:1150;display:none;align-items:center;gap:4px;height:22px;padding:0 10px;border-radius:11px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu, #2c2c2e);box-shadow:var(--dsw-shadow-lv3);font-family:var(--dsw-font-family,system-ui);font-size:11px;color:var(--dsw-alias-label-primary);cursor:default;animation:dsh-ann-pop .12s var(--ds-ease-in-out, ease);' + chipLayer.style.cssText = 'position:fixed;z-index:1150;display:none;align-items:center;gap:4px;height:22px;padding:0 10px;border-radius:11px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu, #2c2c2e);background:color-mix(in srgb, var(--dsw-specific-menu, #2c2c2e) 94%, transparent);box-shadow:var(--dsw-shadow-lv3);font-family:var(--dsw-font-family,system-ui);font-size:11px;color:var(--dsw-alias-label-primary);cursor:default;animation:dsh-ann-pop .12s var(--ds-ease-in-out, ease);' document.body.appendChild(chipLayer) var tipLayer = document.createElement('div') tipLayer.setAttribute('data-annotation-tip-layer', '') @@ -1966,7 +1968,7 @@ window.__ModuleLoader__.load({ if (ui.quotes.length === 0) return var el = document.createElement('div') el.className = 'dsh-ann-tip' - el.style.cssText = 'position:fixed;z-index:1160;box-sizing:border-box;width:300px;max-width:calc(100vw - 16px);padding:10px 12px;border-radius:12px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu, #2c2c2e);box-shadow:var(--dsw-shadow-lv3);font-family:var(--dsw-font-family,system-ui);font-size:12px;color:var(--dsw-alias-label-primary);' + el.style.cssText = 'position:fixed;z-index:1160;box-sizing:border-box;width:300px;max-width:calc(100vw - 16px);padding:10px 12px;border-radius:12px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu, #2c2c2e);background:color-mix(in srgb, var(--dsw-specific-menu, #2c2c2e) 94%, transparent);box-shadow:var(--dsw-shadow-lv3);font-family:var(--dsw-font-family,system-ui);font-size:12px;color:var(--dsw-alias-label-primary);' var head = document.createElement('div') head.style.cssText = 'font-weight:600;margin-bottom:6px;' head.textContent = t('tip.title', { n: ui.quotes.length }) @@ -2191,12 +2193,12 @@ window.__ModuleLoader__.load({ var tag = document.createElement('span') tag.setAttribute('data-annotation-bubble-tag', '') tag.textContent = t('bubble.tag', { n: items.length }) - tag.style.cssText = 'display:inline-flex;align-items:center;height:18px;padding:0 8px;margin:4px 0 0 4px;border-radius:9px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu, #2c2c2e);color:var(--dsw-alias-label-secondary);font-family:var(--dsw-font-family,system-ui);font-size:10px;cursor:default;' + tag.style.cssText = 'display:inline-flex;align-items:center;height:18px;padding:0 8px;margin:4px 0 0 4px;border-radius:9px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu, #2c2c2e);background:color-mix(in srgb, var(--dsw-specific-menu, #2c2c2e) 94%, transparent);color:var(--dsw-alias-label-secondary);font-family:var(--dsw-font-family,system-ui);font-size:10px;cursor:default;' ;(function (list) { tag.addEventListener('mouseenter', function () { var el = document.createElement('div') el.className = 'dsh-ann-tip' - el.style.cssText = 'position:fixed;z-index:1160;box-sizing:border-box;width:300px;max-width:calc(100vw - 16px);padding:10px 12px;border-radius:12px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu, #2c2c2e);box-shadow:var(--dsw-shadow-lv3);font-family:var(--dsw-font-family,system-ui);font-size:12px;color:var(--dsw-alias-label-primary);' + el.style.cssText = 'position:fixed;z-index:1160;box-sizing:border-box;width:300px;max-width:calc(100vw - 16px);padding:10px 12px;border-radius:12px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu, #2c2c2e);background:color-mix(in srgb, var(--dsw-specific-menu, #2c2c2e) 94%, transparent);box-shadow:var(--dsw-shadow-lv3);font-family:var(--dsw-font-family,system-ui);font-size:12px;color:var(--dsw-alias-label-primary);' var head = document.createElement('div') head.style.cssText = 'font-weight:600;margin-bottom:6px;' head.textContent = t('bubble.title', { n: list.length }) @@ -2366,7 +2368,7 @@ window.__ModuleLoader__.load({ function makeReplyChip(num, item) { var chip = document.createElement('span') chip.setAttribute('data-annotation-reply-chip', '') - chip.style.cssText = 'display:inline-flex;align-items:center;height:18px;padding:0 6px;margin:0 2px;border-radius:9px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu, #2c2c2e);color:var(--dsw-alias-text-accent,#4c9aff);font-family:var(--dsw-font-family,system-ui);font-size:11px;font-weight:600;cursor:default;vertical-align:middle;' + chip.style.cssText = 'display:inline-flex;align-items:center;height:18px;padding:0 6px;margin:0 2px;border-radius:9px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu, #2c2c2e);background:color-mix(in srgb, var(--dsw-specific-menu, #2c2c2e) 94%, transparent);color:var(--dsw-alias-text-accent,#4c9aff);font-family:var(--dsw-font-family,system-ui);font-size:11px;font-weight:600;cursor:default;vertical-align:middle;' chip.textContent = 'Annotation ' + num var grace = null function hide() { @@ -2379,7 +2381,7 @@ window.__ModuleLoader__.load({ chip.addEventListener('mouseenter', function () { var el = document.createElement('div') el.className = 'dsh-ann-tip' - el.style.cssText = 'position:fixed;z-index:1160;width:320px;max-width:calc(100vw - 16px);padding:10px 12px;border-radius:12px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu, #2c2c2e);box-shadow:var(--dsw-shadow-lv3);font-family:var(--dsw-font-family,system-ui);font-size:12px;color:var(--dsw-alias-label-primary);' + el.style.cssText = 'position:fixed;z-index:1160;width:320px;max-width:calc(100vw - 16px);padding:10px 12px;border-radius:12px;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu, #2c2c2e);background:color-mix(in srgb, var(--dsw-specific-menu, #2c2c2e) 94%, transparent);box-shadow:var(--dsw-shadow-lv3);font-family:var(--dsw-font-family,system-ui);font-size:12px;color:var(--dsw-alias-label-primary);' var head = document.createElement('div') head.style.cssText = 'font-weight:600;margin-bottom:6px;' head.textContent = t('reply.headWithQuote', { n: num })