Browse Source

1、更改md显示格式导入外部包
2、优化语音输入功能样式和逻辑
3、用户回复接入数字人对话和回答

liyanbo 2 months ago
parent
commit
bb820edf13
4 changed files with 100 additions and 38 deletions
  1. 13 0
      package-lock.json
  2. 1 0
      package.json
  3. 0 1
      src/views/AIPage/AIDevelop.vue
  4. 86 37
      src/views/AIPage/aiGenerate/DialogContent.vue

+ 13 - 0
package-lock.json

@@ -24,6 +24,7 @@
         "js-cookie": "^3.0.5",
         "jsencrypt": "^3.3.2",
         "markdown-it": "^14.1.0",
+        "marked": "^17.0.4",
         "router": "^2.2.0",
         "video.js": "^7.21.5",
         "vue": "^3.5.17",
@@ -5733,6 +5734,18 @@
         "markdown-it": "bin/markdown-it.mjs"
       }
     },
+    "node_modules/marked": {
+      "version": "17.0.4",
+      "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.4.tgz",
+      "integrity": "sha512-NOmVMM+KAokHMvjWmC5N/ZOvgmSWuqJB8FoYI019j4ogb/PeRMKoKIjReZ2w3376kkA8dSJIP8uD993Kxc0iRQ==",
+      "license": "MIT",
+      "bin": {
+        "marked": "bin/marked.js"
+      },
+      "engines": {
+        "node": ">= 20"
+      }
+    },
     "node_modules/math-intrinsics": {
       "version": "1.1.0",
       "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",

+ 1 - 0
package.json

@@ -25,6 +25,7 @@
     "js-cookie": "^3.0.5",
     "jsencrypt": "^3.3.2",
     "markdown-it": "^14.1.0",
+    "marked": "^17.0.4",
     "router": "^2.2.0",
     "video.js": "^7.21.5",
     "vue": "^3.5.17",

+ 0 - 1
src/views/AIPage/AIDevelop.vue

@@ -717,7 +717,6 @@ const getRoleList = async () => {
     // 获取小学低年级AI数据
     const listAiRes = await teacherList({ category: grade + 'AI' })
     const listRes = await teacherList({ category: grade })
-    debugger
     const listAi = listAiRes.data.list || []
     const list = listRes.data.list || []
     listAi.push(...list)

+ 86 - 37
src/views/AIPage/aiGenerate/DialogContent.vue

@@ -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()
   }
 }