|
|
@@ -101,7 +101,7 @@ import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
|
|
import { useRouter } from 'vue-router'
|
|
|
import { ArrowLeftBold, CaretLeft, CaretRight, Grid } from '@element-plus/icons-vue'
|
|
|
import VoiceInput from '@/components/ai/voice/VoiceInput.vue'
|
|
|
-import MarkdownIt from 'markdown-it'
|
|
|
+import { marked } from 'marked'
|
|
|
import {CreateDialogue, sendChatMessageStream} from "@/api/questions.js";
|
|
|
import {useAudioPlayer} from "@/api/tts/useAudioPlayer.js";
|
|
|
|
|
|
@@ -139,6 +139,10 @@ const userInput = ref('')
|
|
|
const isVoiceRecording = ref(false)
|
|
|
// 实时语音识别结果
|
|
|
const voiceRecognizedText = ref("")
|
|
|
+// 录音开始时的光标位置
|
|
|
+const recordingStartCursorPos = ref(0)
|
|
|
+// 录音开始时的原始文本
|
|
|
+const recordingStartText = ref("")
|
|
|
|
|
|
// 音频对象
|
|
|
// 背景音频
|
|
|
@@ -164,28 +168,32 @@ const currentBackgroundImage = computed(() => {
|
|
|
return currentSection.value.backgroundImage.url
|
|
|
})
|
|
|
|
|
|
-// 创建 markdown-it 实例
|
|
|
-const md = new MarkdownIt({
|
|
|
- html: true,
|
|
|
- linkify: true,
|
|
|
- typographer: true
|
|
|
-})
|
|
|
+// 当前对话缓存
|
|
|
+const currentDialogueCache = ref(null)
|
|
|
|
|
|
// 方法
|
|
|
const handleVoiceRecognized = (text) => {
|
|
|
console.log('语音识别结果:', text)
|
|
|
if (isVoiceRecording.value) {
|
|
|
- // 在同一次录音过程中,只更新临时变量,不修改userInput.value
|
|
|
+ // 在同一次录音过程中,实时更新文本框内容
|
|
|
voiceRecognizedText.value = text
|
|
|
+ const textarea = document.querySelector('.user-input-textarea')
|
|
|
+ if (textarea) {
|
|
|
+ // 使用录音开始时的原始文本和光标位置
|
|
|
+ const startPos = recordingStartCursorPos.value
|
|
|
+ const originalText = recordingStartText.value
|
|
|
+ // 在光标位置插入实时识别结果
|
|
|
+ userInput.value = originalText.substring(0, startPos) + text + originalText.substring(startPos)
|
|
|
+ }
|
|
|
} else {
|
|
|
// 在录音结束时,将最终的语音内容追加到userInput.value
|
|
|
const textarea = document.querySelector('.user-input-textarea')
|
|
|
if (textarea) {
|
|
|
- // 获取光标位置
|
|
|
- const startPos = textarea.selectionStart
|
|
|
- const endPos = textarea.selectionEnd
|
|
|
+ // 使用录音开始时的光标位置和原始文本
|
|
|
+ const startPos = recordingStartCursorPos.value
|
|
|
+ const originalText = recordingStartText.value
|
|
|
// 在光标位置插入文本
|
|
|
- userInput.value = userInput.value.substring(0, startPos) + text + userInput.value.substring(endPos)
|
|
|
+ userInput.value = originalText.substring(0, startPos) + text + originalText.substring(startPos)
|
|
|
// 重新设置光标位置到插入文本的末尾
|
|
|
setTimeout(() => {
|
|
|
textarea.selectionStart = textarea.selectionEnd = startPos + text.length
|
|
|
@@ -205,28 +213,23 @@ const handleRecordingStatusChanged = (isRecording) => {
|
|
|
const wasRecording = isVoiceRecording.value
|
|
|
isVoiceRecording.value = isRecording
|
|
|
|
|
|
- // 如果是从录音状态切换到非录音状态,需要将临时的语音识别结果追加到userInput.value
|
|
|
- if (wasRecording && !isRecording) {
|
|
|
- if (voiceRecognizedText.value) {
|
|
|
- const textarea = document.querySelector('.user-input-textarea')
|
|
|
- if (textarea) {
|
|
|
- // 获取光标位置
|
|
|
- const startPos = textarea.selectionStart
|
|
|
- const endPos = textarea.selectionEnd
|
|
|
- // 在光标位置插入文本
|
|
|
- userInput.value = userInput.value.substring(0, startPos) + voiceRecognizedText.value + userInput.value.substring(endPos)
|
|
|
- // 重新设置光标位置到插入文本的末尾
|
|
|
- setTimeout(() => {
|
|
|
- textarea.selectionStart = textarea.selectionEnd = startPos + voiceRecognizedText.value.length
|
|
|
- }, 0)
|
|
|
- } else {
|
|
|
- // 如果没有找到输入框,直接替换整个内容
|
|
|
- userInput.value = voiceRecognizedText.value
|
|
|
- }
|
|
|
- // 清空临时变量
|
|
|
- voiceRecognizedText.value = ""
|
|
|
+ // 如果是从未录音状态切换到录音状态,记录当前光标位置和文本内容
|
|
|
+ if (!wasRecording && isRecording) {
|
|
|
+ const textarea = document.querySelector('.user-input-textarea')
|
|
|
+ if (textarea) {
|
|
|
+ recordingStartCursorPos.value = textarea.selectionStart
|
|
|
+ recordingStartText.value = userInput.value
|
|
|
+ } else {
|
|
|
+ recordingStartCursorPos.value = 0
|
|
|
+ recordingStartText.value = userInput.value
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ // 如果是从录音状态切换到非录音状态,只需要清空临时变量
|
|
|
+ if (wasRecording && !isRecording) {
|
|
|
+ // 清空临时变量
|
|
|
+ voiceRecognizedText.value = ""
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// 提交用户输入
|
|
|
@@ -247,7 +250,8 @@ const cancelUserInput = () => {
|
|
|
|
|
|
// 解析 Markdown 内容
|
|
|
const parseMarkdown = (content) => {
|
|
|
- return md.render(content)
|
|
|
+ if (!content) return ''
|
|
|
+ return marked(content)
|
|
|
}
|
|
|
|
|
|
const getCharacterSide = (roleName) => {
|
|
|
@@ -377,6 +381,13 @@ const playPrevious = () => {
|
|
|
// 停止当前音频
|
|
|
stopAllAudio()
|
|
|
|
|
|
+ // 如果正在进行数字人对话,调用stopStream清理
|
|
|
+ recoverQuestDialogue()
|
|
|
+ stopPlayback()
|
|
|
+ if (conversationInProgress.value) {
|
|
|
+ stopStream()
|
|
|
+ }
|
|
|
+
|
|
|
if (currentDialogueIndex.value > 0) {
|
|
|
currentDialogueIndex.value--
|
|
|
} else if (currentSectionIndex.value > 0) {
|
|
|
@@ -397,6 +408,13 @@ const playNext = () => {
|
|
|
dialogueAudio.value.pause()
|
|
|
dialogueAudio.value.currentTime = 0
|
|
|
}
|
|
|
+
|
|
|
+ // 如果正在进行数字人对话,调用stopStream清理
|
|
|
+ recoverQuestDialogue()
|
|
|
+ stopPlayback()
|
|
|
+ if (conversationInProgress.value) {
|
|
|
+ stopStream()
|
|
|
+ }
|
|
|
|
|
|
if (currentSection.value && currentDialogueIndex.value < currentSection.value.dialogues.length - 1) {
|
|
|
currentDialogueIndex.value++
|
|
|
@@ -452,9 +470,9 @@ const receiveMessageFullText = ref('')
|
|
|
|
|
|
//创建对话
|
|
|
const createAiChart = async () => {
|
|
|
- // let role = props.scriptRoles.find(r => r.id === "roleName")
|
|
|
+ let role = props.scriptRoles.find(r => r.name === currentDialogue.value.roleName)
|
|
|
// 智能问答
|
|
|
- await CreateDialogue({ roleId: 54 })
|
|
|
+ await CreateDialogue({ roleId: role.id })
|
|
|
.then(res => {
|
|
|
console.log("创建会话:", res.data);
|
|
|
activeConversationId.value = res.data
|
|
|
@@ -477,16 +495,37 @@ const doSendMessage = async () => {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
+ let userInputTemp = userInput.value;
|
|
|
+ let currentDialogueTemp = currentSection.value.dialogues[currentDialogueIndex.value-1]
|
|
|
+ userInputTemp += "(此内容是帮我解答的问题,问题是:" + currentDialogueTemp.content + ",回复要求:根据问题处理我回答的内容是否正确并给予鼓励或夸赞;注意请使用精简回答,尽量控制字体数量在50个字内)"
|
|
|
// 执行发送
|
|
|
await doSendMessageStream({
|
|
|
conversationId: activeConversationId.value,
|
|
|
- content: userInput.value,
|
|
|
+ content: userInputTemp,
|
|
|
contentAnswer: null,
|
|
|
})
|
|
|
-
|
|
|
// 清空输入框
|
|
|
userInput.value = ''
|
|
|
isInputCardVisible.value = false
|
|
|
+ recoverQuestDialogue()
|
|
|
+}
|
|
|
+
|
|
|
+/** 显示问题回答对话 */
|
|
|
+const showQuestAnswerDialogue = () => {
|
|
|
+ // 缓存当前对话
|
|
|
+ currentDialogueCache.value = currentDialogue.value
|
|
|
+
|
|
|
+ // 将当前对话类型设置为数字人对话
|
|
|
+ currentDialogue.value.type = "digital"
|
|
|
+ // 设置默认内容为"让我思考一下..."
|
|
|
+ currentDialogue.value.content = "让我思考一下..."
|
|
|
+
|
|
|
+}
|
|
|
+/** 回复对话 */
|
|
|
+const recoverQuestDialogue = () => {
|
|
|
+ // 缓存当前对话
|
|
|
+ currentDialogue.value = currentDialogueCache.value
|
|
|
+ currentDialogueCache.value = null
|
|
|
}
|
|
|
|
|
|
/** 真正执行【发送】消息操作 */
|
|
|
@@ -498,6 +537,8 @@ const doSendMessageStream = async userMessage => {
|
|
|
// 设置为空
|
|
|
receiveMessageFullText.value = ''
|
|
|
|
|
|
+ showQuestAnswerDialogue()
|
|
|
+
|
|
|
try {
|
|
|
|
|
|
// 发送 event stream
|
|
|
@@ -507,10 +548,13 @@ const doSendMessageStream = async userMessage => {
|
|
|
userMessage.content,
|
|
|
userMessage.contentAnswer,
|
|
|
conversationInAbortController.value,
|
|
|
+ true, // enableContext 参数
|
|
|
async res => {
|
|
|
const { code, data, msg } = JSON.parse(res.data)
|
|
|
if (code !== 0) {
|
|
|
console.log(`对话异常! ${msg}`)
|
|
|
+ recoverQuestDialogue();
|
|
|
+ stopStream();
|
|
|
return
|
|
|
}
|
|
|
|
|
|
@@ -521,6 +565,8 @@ const doSendMessageStream = async userMessage => {
|
|
|
return
|
|
|
}
|
|
|
receiveMessageFullText.value += data.receive.content
|
|
|
+ // 更新数字人对话框内容
|
|
|
+ currentDialogue.value.content = receiveMessageFullText.value
|
|
|
// 首次返回需要添加一个 message 到页面,后面的都是更新
|
|
|
if (isFirstChunk) {
|
|
|
isFirstChunk = false
|
|
|
@@ -536,17 +582,20 @@ const doSendMessageStream = async userMessage => {
|
|
|
},
|
|
|
error => {
|
|
|
console.log(`对话异常! ${error}`)
|
|
|
+ recoverQuestDialogue();
|
|
|
stopStream()
|
|
|
// 需要抛出异常,禁止重试
|
|
|
throw error
|
|
|
},
|
|
|
() => {
|
|
|
console.log(`结束对话! `)
|
|
|
+ recoverQuestDialogue();
|
|
|
stopStream()
|
|
|
}
|
|
|
)
|
|
|
} catch (error) {
|
|
|
console.error('发送消息失败:', error)
|
|
|
+ recoverQuestDialogue();
|
|
|
stopStream()
|
|
|
}
|
|
|
}
|