mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
feat(web-chat): add hover copy button for user/assistant messages (#948)
* ci(staging): use default branch instead of hardcoded main * feat(web-chat): add hover copy button for message bubbles * fix(web-chat): address Gemini review for copy state and streaming safety * chore(pr): drop unrelated staging workflow change from #948
This commit is contained in:
@@ -702,16 +702,25 @@ function copyCodeBlock(btn) {
|
||||
});
|
||||
}
|
||||
|
||||
function copyMessage(btn) {
|
||||
const message = btn.closest('.message');
|
||||
if (!message) return;
|
||||
const text = message.getAttribute('data-copy-text')
|
||||
|| message.getAttribute('data-raw')
|
||||
|| message.textContent
|
||||
|| '';
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
btn.textContent = 'Copied';
|
||||
setTimeout(() => { btn.textContent = 'Copy'; }, 1200);
|
||||
}).catch(() => {
|
||||
btn.textContent = 'Failed';
|
||||
setTimeout(() => { btn.textContent = 'Copy'; }, 1200);
|
||||
});
|
||||
}
|
||||
|
||||
function addMessage(role, content) {
|
||||
const container = document.getElementById('chat-messages');
|
||||
const div = document.createElement('div');
|
||||
div.className = 'message ' + role;
|
||||
if (role === 'user') {
|
||||
div.textContent = content;
|
||||
} else {
|
||||
div.setAttribute('data-raw', content);
|
||||
div.innerHTML = renderMarkdown(content);
|
||||
}
|
||||
const div = createMessageElement(role, content);
|
||||
container.appendChild(div);
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
@@ -723,7 +732,11 @@ function appendToLastAssistant(chunk) {
|
||||
const last = messages[messages.length - 1];
|
||||
const raw = (last.getAttribute('data-raw') || '') + chunk;
|
||||
last.setAttribute('data-raw', raw);
|
||||
last.innerHTML = renderMarkdown(raw);
|
||||
last.setAttribute('data-copy-text', raw);
|
||||
const content = last.querySelector('.message-content');
|
||||
if (content) {
|
||||
content.innerHTML = renderMarkdown(raw);
|
||||
}
|
||||
container.scrollTop = container.scrollHeight;
|
||||
} else {
|
||||
addMessage('assistant', chunk);
|
||||
@@ -1310,12 +1323,31 @@ function loadHistory(before) {
|
||||
function createMessageElement(role, content) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'message ' + role;
|
||||
if (role === 'user') {
|
||||
div.textContent = content;
|
||||
|
||||
if (role === 'assistant' || role === 'user') {
|
||||
div.classList.add('has-copy');
|
||||
div.setAttribute('data-copy-text', content);
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.className = 'message-copy-btn';
|
||||
copyBtn.type = 'button';
|
||||
copyBtn.setAttribute('aria-label', 'Copy message');
|
||||
copyBtn.textContent = 'Copy';
|
||||
copyBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
copyMessage(copyBtn);
|
||||
});
|
||||
div.appendChild(copyBtn);
|
||||
}
|
||||
|
||||
const body = document.createElement('div');
|
||||
body.className = 'message-content';
|
||||
if (role === 'user' || role === 'system') {
|
||||
body.textContent = content;
|
||||
} else {
|
||||
div.setAttribute('data-raw', content);
|
||||
div.innerHTML = renderMarkdown(content);
|
||||
body.innerHTML = renderMarkdown(content);
|
||||
}
|
||||
div.appendChild(body);
|
||||
return div;
|
||||
}
|
||||
|
||||
|
||||
@@ -666,6 +666,7 @@ body {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
word-wrap: break-word;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.message.user {
|
||||
@@ -686,6 +687,58 @@ body {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.message.has-copy {
|
||||
padding-right: 52px;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.message-copy-btn {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
z-index: 2;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-secondary);
|
||||
border-radius: 8px;
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.message.user:hover .message-copy-btn,
|
||||
.message.assistant:hover .message-copy-btn,
|
||||
.message.user:focus-within .message-copy-btn,
|
||||
.message.assistant:focus-within .message-copy-btn {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.message-copy-btn:focus-visible {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.message-copy-btn:hover {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
@media (hover: none) {
|
||||
.message.user .message-copy-btn,
|
||||
.message.assistant .message-copy-btn {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.message.system {
|
||||
align-self: center;
|
||||
background: var(--bg-tertiary);
|
||||
|
||||
Reference in New Issue
Block a user