Переглянути джерело

1、课程中的小智接入智能小智api

liyanbo 9 місяців тому
батько
коміт
fbe9bb455f
1 змінених файлів з 370 додано та 22 видалено
  1. 370 22
      src/views/AIDevelop.vue

+ 370 - 22
src/views/AIDevelop.vue

@@ -139,6 +139,8 @@
           </div>
         </div>
       </div>
+
+
       <!-- 添加试题弹框 -->
       <transition name="fade-scale">
         <div
@@ -221,7 +223,7 @@
           <div class="ai-dialog-content">
             <div class="ai-message-history">
               <div
-                v-for="(message, index) in messageHistory"
+                v-for="(message, index) in messageList"
                 :key="index"
                 :class="['message', message.type]"
               >
@@ -231,7 +233,10 @@
                   class="avatar user"
                 />
                 <img v-else src="@/assets/images/xiaozhi.png" class="avatar" />
-                <div class="message-content" v-html="message.content"></div>
+                <div class="message-content" v-if="message.type === 'user'" v-html="message.content"></div>
+                <div class="message-content" v-else>
+                  <MarkdownView class="left-text" :content="message.content" />
+                </div>
               </div>
             </div>
             <!-- 弹框默认消息 -->
@@ -242,14 +247,13 @@
               @select-message="handleSelectMessage"
             />
             <el-input
-              v-model="userMessage"
+              v-model="prompt"
               placeholder="输入问题..."
               class="user-input"
-              @keyup.enter="sendMessage"
+              @keyup.enter="handleSendByKeydown"
             >
               <template #append class="flex flex-wrap items-center mb-4">
-                <!-- <el-button @click="sendMessage" class="child-button confirm">发送</el-button>-->
-                <el-button @click="sendMessage" size="large" round
+                <el-button @click="handleSendByButton" size="large" round
                   >发送</el-button
                 >
               </template>
@@ -263,7 +267,7 @@
 
 <script setup>
 import { ref, onMounted, onUnmounted, onBeforeUnmount,computed} from 'vue'
-import { useRouter } from 'vue-router'
+import {useRoute, useRouter} from 'vue-router'
 import videojs from 'video.js';
 import 'video.js/dist/video-js.css';
 import '@videojs/http-streaming'; // 支持HLS分片
@@ -293,9 +297,12 @@ import { ClassType } from '@/api/class.js'
 import { Message } from '@/utils/message/Message.js'
 
 import DefaultMessage from '@/components/DefaultMessage/index.vue'
+import {CreateDialogue, sendChatMessageStream} from "@/api/questions.js";
+import {teacherList} from "@/api/teachers.js";
+import MarkdownView from "@/components/MarkdownView/index.vue";
 // 处理选择的默认消息
 const handleSelectMessage = (message) => {
-  userMessage.value = message;
+  prompt.value = message;
 }
  
 
@@ -474,8 +481,8 @@ const playNextVideo = () => {
 
   // 重置
   pausedIndices.value = [];
-  userMessage.value = ''
-  messageHistory.value = []
+  prompt.value = ''
+  messageList.value = []
 }
 
 // 切换视频
@@ -562,10 +569,8 @@ const selectedOption = ref(null)
 
 // AI对话弹出框显示状态
 let showAIDialog = ref(false)
-// 用户输入的消息
-let userMessage = ref('')
 // 消息历史记录
-let messageHistory = ref([])
+// let messageList = ref([])
 
 // 处理视频时间更新事件
 const handleTimeUpdate = () => {
@@ -623,35 +628,43 @@ const handleSubmitAnswer = () => {
 
 // 发送消息
 const sendMessage = async () => {
-  if (userMessage.value.trim()) {
+  if (prompt.value.trim()) {
+
     // 添加用户消息到历史记录
-    messageHistory.value.push({
+    messageList.value.push({
       type: 'user',
-      content: userMessage.value
+      content: prompt.value
     })
 
+    console.log("=================", messageList.value,prompt.value)
+
     // 模拟 AI 回复
-    const aiResponse = await simulateAIResponse(userMessage.value)
-    messageHistory.value.push({
+    const aiResponse = await simulateAIResponse(prompt.value)
+    messageList.value.push({
       type: 'ai',
       content: aiResponse
     })
 
+    activeMessageList.value = messageList.value
+
     // 清空输入框
-    userMessage.value = ''
+    prompt.value = ''
   }
 }
 
 // 处理 AI 助手点击事件
 const handleAIClick = () => {
   // 清空输入框
-  messageHistory = ref([])
+  messageList.value = []
   if (courseConfig.value.ccQuestContent) {
-    userMessage.value = courseConfig.value.ccQuestContent
+    prompt.value = courseConfig.value.ccQuestContent
     sendMessage()
-    userMessage.value = ''
+    prompt.value = ''
   }
   showAIDialog.value = true
+
+  //创建对话
+  createAiChart()
 }
 
 // 模拟 AI 回复
@@ -713,6 +726,341 @@ const getAllCourseSections = () => {
   traverse(menuItems.value);
   return sections;
 };
+
+
+// =========== 【聊天对话】小智ai ===========
+
+// 数字人接口
+const route = useRoute()
+const grade = ref('')
+const xZAiData = ref({})
+const getXzAi = async ()=> {
+  try {
+    grade.value = route.query.grade || localStorage.getItem('selectedGrade')
+
+    // 获取小学低年级AI数据
+    const juniorAIRes = await teacherList({ category: grade.value + 'AI' })
+    const aiPerson = juniorAIRes.data.list.find(
+        person => person.name === '小智'
+    )
+    if (aiPerson) {
+      xZAiData.value = {
+        id: aiPerson.id,
+        name: aiPerson.name,
+        image: aiPerson.model2dPath,
+        message: aiPerson.systemMessage,
+        default: aiPerson.questTip
+      }
+
+    } else {
+      console.warn('未找到名为小智的数据')
+    }
+  } catch (error) {
+    console.error('获取年级AI数据失败:', error)
+  }
+}
+
+
+// =========== 【聊天对话】对话相关 ===========
+
+// 聊天对话
+const activeConversationModelPath = ref(null) // 选中的对话编号
+const activeConversationId = ref(null) // 选中的对话编号
+const activeConversation = ref(null) // 选中的 Conversation
+const conversationInProgress = ref(false) // 对话是否正在进行中。目前只有【发送】消息时,会更新为 true,避免切换对话、删除对话等操作,导致 stream 中断
+
+// 消息列表
+const messageRef = ref()
+const activeMessageList = ref([]) // 选中对话的消息列表
+// 消息滚动
+const textSpeed = ref(50) // Typing speed in milliseconds
+const textRoleRunning = ref(false) // Typing speed in milliseconds
+
+// 发送消息输入框
+const isComposing = ref(false) // 判断用户是否在输入
+const conversationInAbortController = ref() // 对话进行中 abort 控制器(控制 stream 对话)
+const inputTimeout = ref() // 处理输入中回车的定时器
+const prompt = ref() // prompt
+const enableContext = ref(true) // 是否开启上下文
+// 接收 Stream 消息
+const receiveMessageFullText = ref('')
+const receiveMessageDisplayedText = ref('')
+
+
+//创建对话
+const createAiChart = async ()=> {
+
+  // 先获取数字人接口
+  await getXzAi()
+
+  // 智能问答
+  CreateDialogue({ roleId: xZAiData.value.id })
+      .then(res => {
+        console.log('创建会话:', res)
+        activeConversationId.value = res.data
+      })
+      .catch(error => {
+        console.error('请求出错:', error)
+      })
+
+  await getConversation(xZAiData.value.id)
+}
+
+
+// =========== 【聊天对话】相关 ===========
+
+/** 获取对话信息 */
+const getConversation = async id => {
+  if (!id) {
+    return
+  }
+  const conversation = ref({})
+  if (!conversation) {
+    return
+  }
+  conversation.systemMessage = xZAiData.value.message
+  activeConversation.value = conversation
+  activeConversationModelPath.value = xZAiData.value.image
+}
+
+// =========== 【发送消息】相关 ===========
+
+/** 处理来自 keydown 的发送消息 */
+const handleSendByKeydown = async event => {
+  // 判断用户是否在输入
+  if (isComposing.value) {
+    return
+  }
+  // 进行中不允许发送
+  if (conversationInProgress.value) {
+    return
+  }
+  const content = prompt.value?.trim()
+
+  if (event.key === 'Enter') {
+    if (event.shiftKey) {
+      // 插入换行
+      prompt.value += '\r\n'
+      event.preventDefault() // 防止默认的换行行为
+    } else {
+      // 发送消息
+      await doSendMessage(content)
+      event.preventDefault() // 防止默认的提交行为
+    }
+  }
+}
+
+/** 处理来自【发送】按钮的发送消息 */
+const handleSendByButton = () => {
+  doSendMessage(prompt.value?.trim())
+}
+
+/** 处理 prompt 输入变化 */
+const handlePromptInput = event => {
+  // 非输入法 输入设置为 true
+  if (!isComposing.value) {
+    // 回车 event data 是 null
+    if (event.data == null) {
+      return
+    }
+    isComposing.value = true
+  }
+  // 清理定时器
+  if (inputTimeout.value) {
+    clearTimeout(inputTimeout.value)
+  }
+  // 重置定时器
+  inputTimeout.value = setTimeout(() => {
+    isComposing.value = false
+  }, 400)
+}
+// TODO注:是不是可以通过 @keydown.enter、@keydown.shift.enter 来实现,回车发送、shift+回车换行;主要看看,是不是可以简化 isComposing 相关的逻辑
+const onCompositionstart = () => {
+  isComposing.value = true
+}
+const onCompositionend = () => {
+  setTimeout(() => {
+    isComposing.value = false
+  }, 200)
+}
+
+/** 真正执行【发送】消息操作 */
+const doSendMessage = async content => {
+  // 校验
+  if (content.length < 1) {
+    console.error('发送失败,原因:内容为空!')
+    return
+  }
+
+  if (activeConversationId.value == null) {
+    console.error('还没创建对话,不能发送!')
+    return
+  }
+  // 清空输入框
+  prompt.value = ''
+  // 执行发送
+  await doSendMessageStream({
+    conversationId: activeConversationId.value,
+    content: content
+  })
+}
+
+/** 真正执行【发送】消息操作 */
+const doSendMessageStream = async userMessage => {
+  // 创建 AbortController 实例,以便中止请求
+  conversationInAbortController.value = new AbortController()
+  // 标记对话进行中
+  conversationInProgress.value = true
+  // 设置为空
+  receiveMessageFullText.value = ''
+
+  try {
+    // 1.1 先添加两个假数据,等 stream 返回再替换
+    activeMessageList.value.push({
+      id: -1,
+      conversationId: activeConversationId.value,
+      type: 'user',
+      content: userMessage.content,
+      createTime: new Date()
+    })
+    activeMessageList.value.push({
+      id: -2,
+      conversationId: activeConversationId.value,
+      type: 'assistant',
+      content: '思考中...',
+      createTime: new Date()
+    })
+
+    // 1.3 开始滚动
+    textRoll()
+
+    // 2. 发送 event stream
+    let isFirstChunk = true // 是否是第一个 chunk 消息段
+
+    await sendChatMessageStream(
+        userMessage.conversationId,
+        userMessage.content,
+        conversationInAbortController.value,
+        enableContext.value,
+        async res => {
+          const { code, data, msg } = JSON.parse(res.data)
+          if (code !== 0) {
+            console.log(`对话异常! ${msg}`)
+            return
+          }
+          // 如果内容为空,就不处理。
+          // if (data.receive.content === '') {
+          //   return
+          // }
+          receiveMessageFullText.value =
+              receiveMessageFullText.value + data.receive.content
+          // 首次返回需要添加一个 message 到页面,后面的都是更新
+          if (isFirstChunk) {
+            isFirstChunk = false
+            // 弹出两个假数据
+            activeMessageList.value.pop()
+            activeMessageList.value.pop()
+            // 更新返回的数据
+            activeMessageList.value.push(data.send)
+            activeMessageList.value.push(data.receive)
+          }
+        },
+        error => {
+          console.log(`对话异常! ${error}`)
+          stopStream()
+          // 需要抛出异常,禁止重试
+          throw error
+        },
+        () => {
+          stopStream()
+        }
+    )
+  } catch {}
+}
+
+/** 停止 stream 流式调用 */
+const stopStream = async () => {
+  // tip:如果 stream 进行中的 message,就需要调用 controller 结束
+  if (conversationInAbortController.value) {
+    conversationInAbortController.value.abort()
+  }
+  // 设置为 false
+  conversationInProgress.value = false
+}
+
+/**
+ * 消息列表
+ *
+ * 和 {@link #getMessageList()} 的差异是,把 systemMessage 考虑进去
+ */
+const messageList = ref([])
+
+// ============== 【消息滚动】相关 =============
+
+/** 滚动到 message 底部 */
+const scrollToBottom = async isIgnore => {
+  // if (messageRef.value) {
+  // messageRef.value.scrollToBottom(isIgnore)
+  // }
+}
+
+/** 自提滚动效果 */
+const textRoll = async () => {
+  let index = 0
+  try {
+    // 只能执行一次
+    if (textRoleRunning.value) {
+      return
+    }
+    // 设置状态
+    textRoleRunning.value = true
+    receiveMessageDisplayedText.value = ''
+    const task = async () => {
+      // 调整速度
+      const diff =
+          (receiveMessageFullText.value.length -
+              receiveMessageDisplayedText.value.length) /
+          10
+      if (diff > 5) {
+        textSpeed.value = 10
+      } else if (diff > 2) {
+        textSpeed.value = 30
+      } else if (diff > 1.5) {
+        textSpeed.value = 50
+      } else {
+        textSpeed.value = 100
+      }
+      // 对话结束,就按 30 的速度
+      if (!conversationInProgress.value) {
+        textSpeed.value = 10
+      }
+
+      if (index < receiveMessageFullText.value.length) {
+        receiveMessageDisplayedText.value += receiveMessageFullText.value[index]
+        index++
+
+        // 更新 message
+        const lastMessage =
+            activeMessageList.value[activeMessageList.value.length - 1]
+        lastMessage.content = receiveMessageDisplayedText.value
+        // 滚动到住下面
+        await scrollToBottom()
+        // 重新设置任务
+        timer = setTimeout(task, textSpeed.value)
+      } else {
+        // 不是对话中可以结束
+        if (!conversationInProgress.value) {
+          textRoleRunning.value = false
+          clearTimeout(timer)
+        } else {
+          // 重新设置任务
+          timer = setTimeout(task, textSpeed.value)
+        }
+      }
+    }
+    let timer = setTimeout(task, textSpeed.value)
+  } catch {}
+}
 </script>
 
 <style scoped lang="scss">