UnisKB/ui/src/components/ai-chat/component/operation-button/ChatOperationButton.vue

287 lines
9.2 KiB
Vue
Raw Normal View History

2023-12-01 03:36:04 +00:00
<template>
2024-05-21 10:32:48 +00:00
<div>
<el-text type="info">
<span class="ml-4">{{ datetimeFormat(data.create_time) }}</span>
</el-text>
</div>
2023-12-01 03:36:04 +00:00
<div>
2024-09-13 07:47:05 +00:00
<!-- 语音播放 -->
<span v-if="tts">
2025-01-21 07:25:21 +00:00
<el-tooltip effect="dark" :content="$t('chat.operation.play')" placement="top" v-if="!audioPlayerStatus">
2024-09-24 08:56:53 +00:00
<el-button text :disabled="!data?.write_ed" @click="playAnswerText(data?.answer_text)">
2024-09-20 09:45:49 +00:00
<AppIcon iconName="app-video-play"></AppIcon>
</el-button>
2024-09-24 08:56:53 +00:00
</el-tooltip>
2025-01-21 07:25:21 +00:00
<el-tooltip v-else effect="dark" :content="$t('chat.operation.pause')" placement="top">
2024-09-24 08:56:53 +00:00
<el-button type="primary" text :disabled="!data?.write_ed" @click="pausePlayAnswerText()">
<AppIcon iconName="app-video-pause"></AppIcon>
2024-09-13 07:47:05 +00:00
</el-button>
</el-tooltip>
<el-divider direction="vertical" />
</span>
2024-11-28 07:56:19 +00:00
<span v-if="type == 'ai-chat' || type == 'log'">
2025-01-21 07:25:21 +00:00
<el-tooltip effect="dark" :content="$t('chat.operation.regeneration')" placement="top">
2024-09-19 06:56:55 +00:00
<el-button :disabled="chat_loading" text @click="regeneration">
<el-icon><RefreshRight /></el-icon>
</el-button>
</el-tooltip>
<el-divider direction="vertical" />
2025-01-21 07:25:21 +00:00
<el-tooltip effect="dark" :content="$t('common.copy')" placement="top">
2025-01-22 07:06:30 +00:00
<el-button text @click="copyClick(data?.answer_text.trim())">
2024-09-19 06:56:55 +00:00
<AppIcon iconName="app-copy"></AppIcon>
</el-button>
</el-tooltip>
<el-divider direction="vertical" />
<el-tooltip
effect="dark"
2025-01-21 07:25:21 +00:00
:content="$t('chat.operation.like')"
2024-09-19 06:56:55 +00:00
placement="top"
v-if="buttonData?.vote_status === '-1'"
>
<el-button text @click="voteHandle('0')" :disabled="loading">
<AppIcon iconName="app-like"></AppIcon>
</el-button>
</el-tooltip>
<el-tooltip
effect="dark"
2025-01-21 07:25:21 +00:00
:content="$t('chat.operation.cancelLike')"
2024-09-19 06:56:55 +00:00
placement="top"
v-if="buttonData?.vote_status === '0'"
>
<el-button text @click="voteHandle('-1')" :disabled="loading">
<AppIcon iconName="app-like-color"></AppIcon>
</el-button>
</el-tooltip>
<el-divider direction="vertical" v-if="buttonData?.vote_status === '-1'" />
<el-tooltip
effect="dark"
2025-01-21 07:25:21 +00:00
:content="$t('chat.operation.oppose')"
2024-09-19 06:56:55 +00:00
placement="top"
v-if="buttonData?.vote_status === '-1'"
>
<el-button text @click="voteHandle('1')" :disabled="loading">
<AppIcon iconName="app-oppose"></AppIcon>
</el-button>
</el-tooltip>
<el-tooltip
effect="dark"
2025-01-21 07:25:21 +00:00
:content="$t('chat.operation.cancelOppose')"
2024-09-19 06:56:55 +00:00
placement="top"
v-if="buttonData?.vote_status === '1'"
>
<el-button text @click="voteHandle('-1')" :disabled="loading">
<AppIcon iconName="app-oppose-color"></AppIcon>
</el-button>
</el-tooltip>
</span>
2023-12-01 03:36:04 +00:00
</div>
2024-09-13 07:47:05 +00:00
<!-- 先渲染不然不能播放 -->
<audio ref="audioPlayer" v-for="item in audioList" :key="item" controls hidden="hidden"></audio>
2023-12-01 03:36:04 +00:00
</template>
<script setup lang="ts">
import { nextTick, onMounted, ref } from 'vue'
2024-09-20 09:45:49 +00:00
import { useRoute } from 'vue-router'
2023-12-01 03:36:04 +00:00
import { copyClick } from '@/utils/clipboard'
2023-12-01 10:21:49 +00:00
import applicationApi from '@/api/application'
2024-05-21 10:32:48 +00:00
import { datetimeFormat } from '@/utils/time'
import { MsgError } from '@/utils/message'
2025-01-21 07:25:21 +00:00
import { t } from '@/locales'
const route = useRoute()
const {
2024-09-20 09:45:49 +00:00
params: { id }
} = route as any
2024-11-13 02:37:16 +00:00
const props = withDefaults(
defineProps<{
data: any
type: 'log' | 'ai-chat' | 'debug-ai-chat'
chatId: string
chat_loading: boolean
applicationId: string
tts: boolean
tts_type: string
tts_autoplay: boolean
2024-11-13 02:37:16 +00:00
}>(),
{
data: () => ({}),
type: 'ai-chat'
}
)
2023-12-01 03:36:04 +00:00
2023-12-01 11:17:58 +00:00
const emit = defineEmits(['update:data', 'regeneration'])
2023-12-01 03:36:04 +00:00
const audioPlayer = ref<HTMLAudioElement[] | null>([])
const audioPlayerStatus = ref(false)
2023-12-01 10:21:49 +00:00
const buttonData = ref(props.data)
const loading = ref(false)
const utterance = ref<SpeechSynthesisUtterance | null>(null)
const audioList = ref<string[]>([])
const currentAudioIndex = ref(0)
2023-12-01 10:21:49 +00:00
2023-12-07 11:28:38 +00:00
function regeneration() {
2023-12-01 11:17:58 +00:00
emit('regeneration')
}
2023-12-01 10:21:49 +00:00
function voteHandle(val: string) {
applicationApi
2024-05-20 09:50:14 +00:00
.putChatVote(props.applicationId, props.chatId, props.data.record_id, val, loading)
.then(() => {
2023-12-01 10:21:49 +00:00
buttonData.value['vote_status'] = val
emit('update:data', buttonData.value)
})
}
2024-09-13 07:47:05 +00:00
function markdownToPlainText(md: string) {
2024-09-20 09:45:49 +00:00
return (
md
// 移除图片 ![alt](url)
.replace(/!\[.*?\]\(.*?\)/g, '')
// 移除链接 [text](url)
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
// 移除 Markdown 标题符号 (#, ##, ###)
.replace(/^#{1,6}\s+/gm, '')
// 移除加粗 **text** 或 __text__
.replace(/\*\*(.*?)\*\*/g, '$1')
.replace(/__(.*?)__/g, '$1')
// 移除斜体 *text* 或 _text_
.replace(/\*(.*?)\*/g, '$1')
.replace(/_(.*?)_/g, '$1')
// 移除行内代码 `code`
.replace(/`(.*?)`/g, '$1')
// 移除代码块 ```code```
.replace(/```[\s\S]*?```/g, '')
// 移除多余的换行符
.replace(/\n{2,}/g, '\n')
.trim()
)
}
function removeFormRander(text: string) {
return text
.replace(/<form_rander>[\s\S]*?<\/form_rander>/g, '')
.trim()
}
2024-09-13 07:47:05 +00:00
const playAnswerText = (text: string) => {
if (!text) {
2025-01-21 07:25:21 +00:00
text = t('chat.tip.answerMessage')
}
// 移除表单渲染器
text = removeFormRander(text)
// text 处理成纯文本
text = markdownToPlainText(text)
// console.log(text)
audioPlayerStatus.value = true
// 分割成多份
audioList.value = text.split(/(<audio[^>]*><\/audio>)/).filter((item) => item.trim().length > 0)
nextTick(()=>{
// console.log(audioList.value, audioPlayer.value)
playAnswerTextPart()
})
}
const playAnswerTextPart = () => {
// console.log(audioList.value, currentAudioIndex.value)
if (currentAudioIndex.value === audioList.value.length) {
audioPlayerStatus.value = false
currentAudioIndex.value = 0
return
}
if (audioList.value[currentAudioIndex.value].includes('<audio')) {
if (audioPlayer.value) {
audioPlayer.value[currentAudioIndex.value].src = audioList.value[currentAudioIndex.value].match(/src="([^"]*)"/)?.[1] || ''
audioPlayer.value[currentAudioIndex.value].play() // 自动播放音频
audioPlayer.value[currentAudioIndex.value].onended = () => {
currentAudioIndex.value += 1
playAnswerTextPart()
}
}
} else if (props.tts_type === 'BROWSER') {
if (audioList.value[currentAudioIndex.value] !== utterance.value?.text) {
window.speechSynthesis.cancel()
}
if (window.speechSynthesis.paused) {
window.speechSynthesis.resume()
return
}
2024-09-13 07:47:05 +00:00
// 创建一个新的 SpeechSynthesisUtterance 实例
utterance.value = new SpeechSynthesisUtterance(audioList.value[currentAudioIndex.value])
utterance.value.onend = () => {
utterance.value = null
currentAudioIndex.value += 1
playAnswerTextPart()
}
utterance.value.onerror = () => {
audioPlayerStatus.value = false
utterance.value = null
}
2024-09-13 07:47:05 +00:00
// 调用浏览器的朗读功能
window.speechSynthesis.speak(utterance.value)
} else if (props.tts_type === 'TTS') {
// 恢复上次暂停的播放
if (audioPlayer.value && audioPlayer.value[currentAudioIndex.value]?.src) {
audioPlayer.value[currentAudioIndex.value].play()
return
}
2024-09-13 07:47:05 +00:00
applicationApi
.postTextToSpeech((props.applicationId as string) || (id as string), { text: audioList.value[currentAudioIndex.value] }, loading)
.then(async (res: any) => {
if (res.type === 'application/json') {
const text = await res.text()
MsgError(text)
return
}
2024-09-13 07:47:05 +00:00
// 假设我们有一个 MP3 文件的字节数组
// 创建 Blob 对象
const blob = new Blob([res], { type: 'audio/mp3' })
// 创建对象 URL
const url = URL.createObjectURL(blob)
// 测试blob是否能正常播放
// const link = document.createElement('a')
// link.href = window.URL.createObjectURL(blob)
// link.download = "abc.mp3"
// link.click()
// 检查 audioPlayer 是否已经引用了 DOM 元素
if (audioPlayer.value) {
audioPlayer.value[currentAudioIndex.value].src = url
audioPlayer.value[currentAudioIndex.value].play() // 自动播放音频
audioPlayer.value[currentAudioIndex.value].onended = () => {
currentAudioIndex.value += 1
playAnswerTextPart()
}
2024-09-13 07:47:05 +00:00
} else {
console.error('audioPlayer.value is not an instance of HTMLAudioElement')
}
})
.catch((err) => {
console.log('err: ', err)
})
}
}
const pausePlayAnswerText = () => {
audioPlayerStatus.value = false
if (props.tts_type === 'TTS') {
if (audioPlayer.value) {
audioPlayer.value?.forEach((item) => {
item.pause()
})
}
}
if (props.tts_type === 'BROWSER') {
window.speechSynthesis.pause()
}
}
onMounted(() => {
// 第一次回答后自动播放, 打开历史记录不自动播放
if (props.tts_autoplay && buttonData.value.write_ed && !buttonData.value.update_time) {
playAnswerText(buttonData.value.answer_text)
}
})
2023-12-01 03:36:04 +00:00
</script>
<style lang="scss" scoped></style>