diff --git a/tests/frontend_preview.test.mjs b/tests/frontend_preview.test.mjs index 4054833..152ef38 100644 --- a/tests/frontend_preview.test.mjs +++ b/tests/frontend_preview.test.mjs @@ -78,3 +78,14 @@ test("artifact chips expose a compact file type and preview affordance", () => { assert.match(pageHtml, /\.art-chip-icon/); assert.match(pageHtml, /\.art-chip:focus-visible/); }); + +test("message actions copy raw content and revise by appending a new message", () => { + assert.match(chatJs, /class="msg-action msg-copy"[^>]+aria-label="复制消息"/); + assert.match(chatJs, /class="msg-action msg-revise"[^>]+aria-label="引用并修改消息"/); + assert.match(chatJs, /state\.loadedMessages\.find/); + assert.match(chatJs, /navigator\.clipboard\.writeText\(text\)/); + assert.match(chatJs, /function loadMessageIntoComposer\(text\)/); + assert.match(chatJs, /修改后发送将作为一条新消息追加/); + assert.match(pageHtml, /\.msg:hover \.msg-actions, \.msg:focus-within \.msg-actions/); + assert.match(pageHtml, /\.msg-action:focus-visible/); +}); diff --git a/web/static/dev.html b/web/static/dev.html index 680ed6e..3b6eb90 100644 --- a/web/static/dev.html +++ b/web/static/dev.html @@ -886,18 +886,26 @@ .msg .body a, .md-render a { color: var(--accent); } .msg .body img, .md-render img { max-width: 100%; } .msg .body hr, .md-render hr { border: none; border-top: 1px solid var(--border); margin: 0.8em 0; } - /* 卡片底部操作栏(仿 GPT/DeepSeek):默认隐,悬停/聚焦现;触屏无 hover → 常显淡态 */ - .msg-actions { display: flex; gap: 6px; margin-top: 6px; opacity: 0; transition: opacity .12s; } + /* 消息快捷操作:桌面悬停/键盘聚焦时显示;触屏无 hover 时保留淡态可发现性。 */ + .msg-actions { + display: flex; align-items: center; gap: 2px; min-height: 26px; margin-top: 4px; + opacity: 0; transition: opacity .12s; + } .msg:hover .msg-actions, .msg:focus-within .msg-actions { opacity: 1; } .msg.user .msg-actions { justify-content: flex-end; } - .msg-actions button { - font: inherit; font-size: 11px; line-height: 1; color: var(--muted); - background: transparent; border: 1px solid var(--border); border-radius: var(--r-sm); - padding: 3px 8px; cursor: pointer; display: inline-flex; align-items: center; gap: 4px; - transition: background .12s, color .12s, border-color .12s; + .msg-action { + display: inline-flex; align-items: center; justify-content: center; + width: 26px; height: 26px; padding: 4px; border: 0; border-radius: var(--r-sm); + color: var(--muted); background: transparent; cursor: pointer; + transition: color .12s, background .12s; + } + .msg-action:hover { color: var(--text); background: var(--code-bg); } + .msg-action:focus-visible { opacity: 1; outline: 2px solid var(--accent); outline-offset: 1px; } + .msg-action.copied { color: #16a34a; } + .msg-action svg { + width: 16px; height: 16px; fill: none; stroke: currentColor; + stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; } - .msg-actions button:hover { color: var(--text); background: var(--code-bg); border-color: var(--muted); } - .msg-actions button.copied { color: #16a34a; border-color: #16a34a; background: transparent; } @media (hover: none) { .msg-actions { opacity: .55; } } .tool-call { margin-top: 6px; font-family: var(--mono); font-size: 12px; } .tool-call summary { diff --git a/web/static/js/chat.js b/web/static/js/chat.js index 77d038b..18d4259 100644 --- a/web/static/js/chat.js +++ b/web/static/js/chat.js @@ -1411,15 +1411,21 @@ function setTaskProgress(taskId, steps) { if (state.taskId === taskId) renderTaskProgressDock(normalized); } -// 卡片底部「复制」按钮 HTML。复制的是正文原文(Markdown 源),点击时经事件委托据 -// 卡片 data-idx 回 state.loadedMessages 取 payload.content —— 不从渲染后的 .body HTML 反推。 -// 直播卡不带此栏:流结束后 loadMessages 整屏重渲成持久卡,自然带上。 -function msgActionsHtml() { - return `
`; +const COPY_ICON = ``; +const REVISE_ICON = ``; +const CHECK_ICON = ``; + +// 消息操作保持轻量:复制取 DB payload 中的 Markdown 原文;用户的“修改”只把旧消息 +// 载入 composer,后续发送仍是 append-only 新消息,不改写历史、不重跑旧工具链。 +function msgActionsHtml(role) { + const copy = ``; + const canRevise = role === "user" && !channelCfg(state.taskMeta && state.taskMeta.channel); + const revise = canRevise + ? `` + : ""; + return `