mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-06-14 11:49:02 +08:00
feat: AI对话支持复制
This commit is contained in:
@@ -1,12 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useToast } from '@nuxt/ui/runtime/composables/useToast.js'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import MarkdownIt from 'markdown-it'
|
import MarkdownIt from 'markdown-it'
|
||||||
import type { ContentBlock, ToolBlockContent } from '@/composables/useAIChat'
|
import type { ContentBlock, ToolBlockContent } from '@/composables/useAIChat'
|
||||||
import CaptureButton from '@/components/common/CaptureButton.vue'
|
import CaptureButton from '@/components/common/CaptureButton.vue'
|
||||||
|
|
||||||
const { t, te, locale } = useI18n()
|
const { t, te, locale } = useI18n()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -83,6 +85,16 @@ const useBlocksRendering = computed(() => {
|
|||||||
return props.role === 'assistant' && visibleBlocks.value.length > 0
|
return props.role === 'assistant' && visibleBlocks.value.length > 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function getToolDisplayName(tool: ToolBlockContent): string {
|
||||||
|
return te(`ai.chat.message.tools.${tool.name}`) ? t(`ai.chat.message.tools.${tool.name}`) : tool.displayName
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatToolStatusForCopy(status: ToolBlockContent['status']): string {
|
||||||
|
if (status === 'running') return 'running'
|
||||||
|
if (status === 'done') return 'done'
|
||||||
|
return 'error'
|
||||||
|
}
|
||||||
|
|
||||||
// 格式化时间参数显示
|
// 格式化时间参数显示
|
||||||
function formatTimeParams(params: Record<string, unknown>): string {
|
function formatTimeParams(params: Record<string, unknown>): string {
|
||||||
// 优先使用 start_time/end_time
|
// 优先使用 start_time/end_time
|
||||||
@@ -229,6 +241,67 @@ function formatToolParams(tool: ToolBlockContent): string {
|
|||||||
|
|
||||||
return genericParts.join(' | ')
|
return genericParts.join(' | ')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const copyMarkdownText = computed(() => {
|
||||||
|
if (props.content.trim()) return props.content
|
||||||
|
if (!useBlocksRendering.value) return ''
|
||||||
|
|
||||||
|
const lines = visibleBlocks.value
|
||||||
|
.map((block) => {
|
||||||
|
if (block.type === 'text') {
|
||||||
|
return block.text
|
||||||
|
}
|
||||||
|
|
||||||
|
if (block.type === 'think') {
|
||||||
|
const thinkTitle = getThinkLabel(block.tag)
|
||||||
|
const thinkBody = block.text
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => `> ${line}`)
|
||||||
|
.join('\n')
|
||||||
|
return `> ${thinkTitle}\n>\n${thinkBody}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (block.type === 'skill') {
|
||||||
|
return `> ${t('ai.skill.active.label', { name: block.skillName })}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (block.type === 'tool') {
|
||||||
|
const toolName = getToolDisplayName(block.tool)
|
||||||
|
const toolParams = formatToolParams(block.tool)
|
||||||
|
const paramsSuffix = toolParams ? ` (${toolParams})` : ''
|
||||||
|
return `- [${formatToolStatusForCopy(block.tool.status)}] ${toolName}${paramsSuffix}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
.filter((line) => line.trim().length > 0)
|
||||||
|
|
||||||
|
return lines.join('\n\n')
|
||||||
|
})
|
||||||
|
|
||||||
|
const canCopyMarkdown = computed(() => !props.isStreaming && copyMarkdownText.value.trim().length > 0)
|
||||||
|
|
||||||
|
async function handleCopyMarkdown() {
|
||||||
|
if (!canCopyMarkdown.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(copyMarkdownText.value)
|
||||||
|
toast.add({
|
||||||
|
title: t('ai.chat.message.copy.success'),
|
||||||
|
color: 'primary',
|
||||||
|
icon: 'i-heroicons-clipboard-document-check',
|
||||||
|
duration: 2000,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({
|
||||||
|
title: t('ai.chat.message.copy.failed'),
|
||||||
|
description: String(error),
|
||||||
|
color: 'error',
|
||||||
|
icon: 'i-heroicons-x-circle',
|
||||||
|
duration: 3000,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -323,13 +396,7 @@ function formatToolParams(tool: ToolBlockContent): string {
|
|||||||
/>
|
/>
|
||||||
<!-- 工具信息 -->
|
<!-- 工具信息 -->
|
||||||
<div class="flex min-w-0 items-baseline gap-1.5 font-medium">
|
<div class="flex min-w-0 items-baseline gap-1.5 font-medium">
|
||||||
<span>
|
<span>{{ getToolDisplayName(block.tool) }}</span>
|
||||||
{{
|
|
||||||
te(`ai.chat.message.tools.${block.tool.name}`)
|
|
||||||
? t(`ai.chat.message.tools.${block.tool.name}`)
|
|
||||||
: block.tool.displayName
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
<span
|
<span
|
||||||
v-if="formatToolParams(block.tool)"
|
v-if="formatToolParams(block.tool)"
|
||||||
class="truncate font-normal text-[11px] opacity-75 max-w-[200px] sm:max-w-[300px]"
|
class="truncate font-normal text-[11px] opacity-75 max-w-[200px] sm:max-w-[300px]"
|
||||||
@@ -375,6 +442,16 @@ function formatToolParams(tool: ToolBlockContent): string {
|
|||||||
<!-- 时间戳 + 操作按钮 -->
|
<!-- 时间戳 + 操作按钮 -->
|
||||||
<div class="mt-1 flex items-center gap-2 px-1" :class="[isUser ? 'flex-row-reverse' : '']">
|
<div class="mt-1 flex items-center gap-2 px-1" :class="[isUser ? 'flex-row-reverse' : '']">
|
||||||
<span class="text-xs text-gray-400">{{ formattedTime }}</span>
|
<span class="text-xs text-gray-400">{{ formattedTime }}</span>
|
||||||
|
<UTooltip :text="t('ai.chat.message.copy.tooltip')" class="no-capture">
|
||||||
|
<UButton
|
||||||
|
icon="i-heroicons-document-duplicate"
|
||||||
|
variant="ghost"
|
||||||
|
color="primary"
|
||||||
|
size="xs"
|
||||||
|
:disabled="!canCopyMarkdown"
|
||||||
|
@click="handleCopyMarkdown"
|
||||||
|
/>
|
||||||
|
</UTooltip>
|
||||||
<!-- 截屏按钮(仅 AI 回复显示) -->
|
<!-- 截屏按钮(仅 AI 回复显示) -->
|
||||||
<CaptureButton
|
<CaptureButton
|
||||||
v-if="showCaptureButton && !isUser && !isStreaming"
|
v-if="showCaptureButton && !isUser && !isStreaming"
|
||||||
|
|||||||
@@ -46,6 +46,11 @@
|
|||||||
"message": {
|
"message": {
|
||||||
"userAvatar": "User Avatar",
|
"userAvatar": "User Avatar",
|
||||||
"calling": "Calling",
|
"calling": "Calling",
|
||||||
|
"copy": {
|
||||||
|
"tooltip": "Copy Markdown",
|
||||||
|
"success": "Markdown copied to clipboard",
|
||||||
|
"failed": "Failed to copy Markdown"
|
||||||
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"get_chat_overview": "Get Chat Overview",
|
"get_chat_overview": "Get Chat Overview",
|
||||||
"search_messages": "Search Messages",
|
"search_messages": "Search Messages",
|
||||||
|
|||||||
@@ -46,6 +46,11 @@
|
|||||||
"message": {
|
"message": {
|
||||||
"userAvatar": "ユーザーアバター",
|
"userAvatar": "ユーザーアバター",
|
||||||
"calling": "実行中",
|
"calling": "実行中",
|
||||||
|
"copy": {
|
||||||
|
"tooltip": "Markdown をコピー",
|
||||||
|
"success": "Markdown をクリップボードにコピーしました",
|
||||||
|
"failed": "Markdown のコピーに失敗しました"
|
||||||
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"get_chat_overview": "チャット概要を取得",
|
"get_chat_overview": "チャット概要を取得",
|
||||||
"search_messages": "チャット履歴を検索",
|
"search_messages": "チャット履歴を検索",
|
||||||
|
|||||||
@@ -46,6 +46,11 @@
|
|||||||
"message": {
|
"message": {
|
||||||
"userAvatar": "用户头像",
|
"userAvatar": "用户头像",
|
||||||
"calling": "调用",
|
"calling": "调用",
|
||||||
|
"copy": {
|
||||||
|
"tooltip": "复制 Markdown",
|
||||||
|
"success": "Markdown 已复制到剪贴板",
|
||||||
|
"failed": "复制 Markdown 失败"
|
||||||
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"get_chat_overview": "获取聊天概览",
|
"get_chat_overview": "获取聊天概览",
|
||||||
"search_messages": "搜索聊天记录",
|
"search_messages": "搜索聊天记录",
|
||||||
|
|||||||
@@ -46,6 +46,11 @@
|
|||||||
"message": {
|
"message": {
|
||||||
"userAvatar": "使用者頭像",
|
"userAvatar": "使用者頭像",
|
||||||
"calling": "執行",
|
"calling": "執行",
|
||||||
|
"copy": {
|
||||||
|
"tooltip": "複製 Markdown",
|
||||||
|
"success": "Markdown 已複製到剪貼簿",
|
||||||
|
"failed": "複製 Markdown 失敗"
|
||||||
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"get_chat_overview": "取得聊天概覽",
|
"get_chat_overview": "取得聊天概覽",
|
||||||
"search_messages": "搜尋聊天紀錄",
|
"search_messages": "搜尋聊天紀錄",
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export interface ToolBlockContent {
|
|||||||
displayName: string
|
displayName: string
|
||||||
status: 'running' | 'done' | 'error'
|
status: 'running' | 'done' | 'error'
|
||||||
params?: Record<string, unknown>
|
params?: Record<string, unknown>
|
||||||
|
durationMs?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MentionedMemberContext {
|
export interface MentionedMemberContext {
|
||||||
|
|||||||
Reference in New Issue
Block a user