From 5193dbc49e7585009b6cbacb8dbba4aa22ea417b Mon Sep 17 00:00:00 2001 From: 356252190-star <356252190@163.com> Date: Sun, 26 Apr 2026 22:59:43 +0800 Subject: [PATCH] feat: add copy bubble button to copy entire message content (#245) - Hover over any message to reveal a copy icon button - Click to copy the full message text to clipboard - Shows success/error toast notification - Skips tool messages (no copy button shown) - i18n support for all 8 languages (EN/ZH/DE/ES/FR/JA/KO/PT) - Dark mode compatible styling Co-authored-by: 356252190-star <356252190-star@users.noreply.github.com> --- .../components/hermes/chat/MessageItem.vue | 80 ++++++++++++++++++- packages/client/src/i18n/locales/de.ts | 3 + packages/client/src/i18n/locales/en.ts | 3 + packages/client/src/i18n/locales/es.ts | 3 + packages/client/src/i18n/locales/fr.ts | 3 + packages/client/src/i18n/locales/ja.ts | 3 + packages/client/src/i18n/locales/ko.ts | 3 + packages/client/src/i18n/locales/pt.ts | 3 + packages/client/src/i18n/locales/zh.ts | 3 + 9 files changed, 101 insertions(+), 3 deletions(-) diff --git a/packages/client/src/components/hermes/chat/MessageItem.vue b/packages/client/src/components/hermes/chat/MessageItem.vue index cfa5e88..4ca1c6e 100644 --- a/packages/client/src/components/hermes/chat/MessageItem.vue +++ b/packages/client/src/components/hermes/chat/MessageItem.vue @@ -27,6 +27,25 @@ const previewUrl = ref(null); const chatStore = useChatStore(); const settingsStore = useSettingsStore(); +// Copy entire bubble content +const copyableContent = computed(() => { + if (props.message.role === 'tool') return null + const content = props.message.content || '' + if (!content.trim()) return null + return content +}) + +async function copyBubbleContent() { + const text = copyableContent.value + if (!text) return + try { + await navigator.clipboard.writeText(text) + toast.success(t('chat.copiedBubble')) + } catch { + toast.error(t('chat.copyFailed')) + } +} + const parsedThinking = computed(() => parseThinking(props.message.content || "", { streaming: !!props.message.isStreaming }), ); @@ -414,7 +433,20 @@ const renderedToolResult = computed(() => { -
{{ timeStr }}
+
+ + {{ timeStr }} +
@@ -620,11 +652,53 @@ const renderedToolResult = computed(() => { } } +.message-meta { + display: flex; + align-items: center; + gap: 6px; + margin-top: 4px; + padding: 0 4px; + opacity: 0; + transition: opacity 0.15s ease; + + .message:hover & { + opacity: 1; + } +} + +.copy-bubble-btn { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border: none; + background: transparent; + color: $text-muted; + cursor: pointer; + border-radius: $radius-sm; + padding: 0; + transition: color 0.15s ease, background 0.15s ease; + + &:hover { + color: $text-secondary; + background: rgba(0, 0, 0, 0.06); + } + + .dark & { + color: #999999; + + &:hover { + color: #cccccc; + background: rgba(255, 255, 255, 0.08); + } + } +} + .message-time { font-size: 11px; color: $text-muted; - margin-top: 4px; - padding: 0 4px; + user-select: none; .dark & { color: #999999; diff --git a/packages/client/src/i18n/locales/de.ts b/packages/client/src/i18n/locales/de.ts index b5f239d..2bd880d 100644 --- a/packages/client/src/i18n/locales/de.ts +++ b/packages/client/src/i18n/locales/de.ts @@ -137,6 +137,9 @@ export default { thinkingHide: 'Denkprozess ausblenden', thinkingDuration: 'Beobachtet {duration}', thinkingChars: '{count} Zeichen', + copyBubble: 'Nachricht kopieren', + copiedBubble: 'Nachricht kopiert', + copyFailed: 'Kopieren fehlgeschlagen', }, // Jobs diff --git a/packages/client/src/i18n/locales/en.ts b/packages/client/src/i18n/locales/en.ts index 1f73e2f..992ea4a 100644 --- a/packages/client/src/i18n/locales/en.ts +++ b/packages/client/src/i18n/locales/en.ts @@ -160,6 +160,9 @@ export default { thinkingHide: 'Hide thinking', thinkingDuration: 'Observed {duration}', thinkingChars: '{count} chars', + copyBubble: 'Copy message', + copiedBubble: 'Message copied', + copyFailed: 'Copy failed', }, // Jobs diff --git a/packages/client/src/i18n/locales/es.ts b/packages/client/src/i18n/locales/es.ts index 87258c2..df18059 100644 --- a/packages/client/src/i18n/locales/es.ts +++ b/packages/client/src/i18n/locales/es.ts @@ -137,6 +137,9 @@ export default { thinkingHide: 'Ocultar pensamiento', thinkingDuration: 'Observado {duration}', thinkingChars: '{count} caracteres', + copyBubble: 'Copiar mensaje', + copiedBubble: 'Mensaje copiado', + copyFailed: 'Error al copiar', }, // Jobs diff --git a/packages/client/src/i18n/locales/fr.ts b/packages/client/src/i18n/locales/fr.ts index 0634c0c..acc2bdc 100644 --- a/packages/client/src/i18n/locales/fr.ts +++ b/packages/client/src/i18n/locales/fr.ts @@ -137,6 +137,9 @@ export default { thinkingHide: 'Masquer le raisonnement', thinkingDuration: 'Observé {duration}', thinkingChars: '{count} caractères', + copyBubble: 'Copier le message', + copiedBubble: 'Message copié', + copyFailed: 'Échec de la copie', }, // Jobs diff --git a/packages/client/src/i18n/locales/ja.ts b/packages/client/src/i18n/locales/ja.ts index 89f9d48..c55e1ae 100644 --- a/packages/client/src/i18n/locales/ja.ts +++ b/packages/client/src/i18n/locales/ja.ts @@ -137,6 +137,9 @@ export default { thinkingHide: '思考過程を隠す', thinkingDuration: '観測 {duration}', thinkingChars: '{count} 文字', + copyBubble: 'メッセージをコピー', + copiedBubble: 'コピーしました', + copyFailed: 'コピーに失敗しました', }, // スケジュールジョブ diff --git a/packages/client/src/i18n/locales/ko.ts b/packages/client/src/i18n/locales/ko.ts index 7a18288..aa09ccd 100644 --- a/packages/client/src/i18n/locales/ko.ts +++ b/packages/client/src/i18n/locales/ko.ts @@ -137,6 +137,9 @@ export default { thinkingHide: '사고 과정 접기', thinkingDuration: '관측 {duration}', thinkingChars: '{count}자', + copyBubble: '메시지 복사', + copiedBubble: '복사됨', + copyFailed: '복사 실패', }, // 예약 작업 diff --git a/packages/client/src/i18n/locales/pt.ts b/packages/client/src/i18n/locales/pt.ts index bd63daf..02183f3 100644 --- a/packages/client/src/i18n/locales/pt.ts +++ b/packages/client/src/i18n/locales/pt.ts @@ -137,6 +137,9 @@ export default { thinkingHide: 'Ocultar raciocínio', thinkingDuration: 'Observado {duration}', thinkingChars: '{count} caracteres', + copyBubble: 'Copiar mensagem', + copiedBubble: 'Mensagem copiada', + copyFailed: 'Falha ao copiar', }, // Jobs diff --git a/packages/client/src/i18n/locales/zh.ts b/packages/client/src/i18n/locales/zh.ts index ddc55cc..a274fb2 100644 --- a/packages/client/src/i18n/locales/zh.ts +++ b/packages/client/src/i18n/locales/zh.ts @@ -160,6 +160,9 @@ export default { thinkingHide: '收起思考过程', thinkingDuration: '已观察 {duration}', thinkingChars: '{count} 字', + copyBubble: '复制消息', + copiedBubble: '已复制', + copyFailed: '复制失败', }, // 定时任务