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>
This commit is contained in:
356252190-star
2026-04-26 22:59:43 +08:00
committed by GitHub
parent 610f3eb9d0
commit 5193dbc49e
9 changed files with 101 additions and 3 deletions
@@ -27,6 +27,25 @@ const previewUrl = ref<string | null>(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(() => {
<span></span><span></span><span></span>
</span>
</div>
<div class="message-time">{{ timeStr }}</div>
<div class="message-meta">
<button
v-if="copyableContent"
class="copy-bubble-btn"
@click="copyBubbleContent"
:title="t('chat.copyBubble')"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg>
</button>
<span class="message-time">{{ timeStr }}</span>
</div>
</div>
</div>
</template>
@@ -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;
+3
View File
@@ -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
+3
View File
@@ -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
+3
View File
@@ -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
+3
View File
@@ -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
+3
View File
@@ -137,6 +137,9 @@ export default {
thinkingHide: '思考過程を隠す',
thinkingDuration: '観測 {duration}',
thinkingChars: '{count} 文字',
copyBubble: 'メッセージをコピー',
copiedBubble: 'コピーしました',
copyFailed: 'コピーに失敗しました',
},
// スケジュールジョブ
+3
View File
@@ -137,6 +137,9 @@ export default {
thinkingHide: '사고 과정 접기',
thinkingDuration: '관측 {duration}',
thinkingChars: '{count}자',
copyBubble: '메시지 복사',
copiedBubble: '복사됨',
copyFailed: '복사 실패',
},
// 예약 작업
+3
View File
@@ -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
+3
View File
@@ -160,6 +160,9 @@ export default {
thinkingHide: '收起思考过程',
thinkingDuration: '已观察 {duration}',
thinkingChars: '{count} 字',
copyBubble: '复制消息',
copiedBubble: '已复制',
copyFailed: '复制失败',
},
// 定时任务