|
@@ -76,7 +76,7 @@
|
|
|
>
|
|
>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="ai-dialog-content">
|
|
<div class="ai-dialog-content">
|
|
|
- <div class="ai-message-history">
|
|
|
|
|
|
|
+ <div class="ai-message-history" ref="messageContainer" @scroll="handleScroll">
|
|
|
<div
|
|
<div
|
|
|
v-for="(message, index) in messageList"
|
|
v-for="(message, index) in messageList"
|
|
|
:key="index"
|
|
:key="index"
|
|
@@ -124,7 +124,7 @@
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup>
|
|
<script setup>
|
|
|
-import { ref, defineProps, defineEmits, onMounted,watch } from 'vue'
|
|
|
|
|
|
|
+import {ref, defineProps, defineEmits, onMounted, watch, nextTick} from 'vue'
|
|
|
import { ElMessage } from 'element-plus'
|
|
import { ElMessage } from 'element-plus'
|
|
|
import { CreateDialogue, sendChatMessageStream } from '@/api/questions.js'
|
|
import { CreateDialogue, sendChatMessageStream } from '@/api/questions.js'
|
|
|
import { teacherList } from '@/api/teachers.js'
|
|
import { teacherList } from '@/api/teachers.js'
|
|
@@ -152,7 +152,9 @@ const showAIDialog = ref(false)
|
|
|
const selectedOption = ref(null)
|
|
const selectedOption = ref(null)
|
|
|
const messageList = ref([])
|
|
const messageList = ref([])
|
|
|
const prompt = ref('')
|
|
const prompt = ref('')
|
|
|
|
|
+const messageContainer = ref(null)
|
|
|
const aiQuestionCount = ref(0)
|
|
const aiQuestionCount = ref(0)
|
|
|
|
|
+const userScrolled = ref(false)//是否用户手动滚动
|
|
|
const xZAiData = ref({})
|
|
const xZAiData = ref({})
|
|
|
const activeConversationId = ref(null)
|
|
const activeConversationId = ref(null)
|
|
|
const conversationInProgress = ref(false)
|
|
const conversationInProgress = ref(false)
|
|
@@ -162,6 +164,10 @@ const isComposing = ref(false)
|
|
|
const inputTimeout = ref()
|
|
const inputTimeout = ref()
|
|
|
const enableContext = ref(true)
|
|
const enableContext = ref(true)
|
|
|
|
|
|
|
|
|
|
+//tts
|
|
|
|
|
+import { useAudioPlayer } from '@/api/tts/useAudioPlayer';
|
|
|
|
|
+const { playAudioChunk } = useAudioPlayer();
|
|
|
|
|
+
|
|
|
// 处理选择的默认消息
|
|
// 处理选择的默认消息
|
|
|
const handleSelectMessage = message => {
|
|
const handleSelectMessage = message => {
|
|
|
prompt.value = message
|
|
prompt.value = message
|
|
@@ -184,18 +190,27 @@ const handleSubmitAnswer = () => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 处理 AI 助手点击事件
|
|
// 处理 AI 助手点击事件
|
|
|
-const handleAIClick = () => {
|
|
|
|
|
|
|
+const handleAIClick = async () => {
|
|
|
// 清空输入框
|
|
// 清空输入框
|
|
|
messageList.value = []
|
|
messageList.value = []
|
|
|
- if (props.currentQuestion.ccQuestContent) {
|
|
|
|
|
- prompt.value = props.currentQuestion.ccQuestContent
|
|
|
|
|
- sendMessage()
|
|
|
|
|
- prompt.value = ''
|
|
|
|
|
- }
|
|
|
|
|
showAIDialog.value = true
|
|
showAIDialog.value = true
|
|
|
|
|
|
|
|
//创建对话
|
|
//创建对话
|
|
|
- createAiChart()
|
|
|
|
|
|
|
+ await createAiChart()
|
|
|
|
|
+
|
|
|
|
|
+ if (props.currentQuestion.ccQuestContent) {
|
|
|
|
|
+ // prompt.value = props.currentQuestion.ccQuestContent
|
|
|
|
|
+ // sendMessage()
|
|
|
|
|
+
|
|
|
|
|
+ prompt.value = ''
|
|
|
|
|
+ // 执行发送
|
|
|
|
|
+ await doSendMessageStream({
|
|
|
|
|
+ conversationId: activeConversationId.value,
|
|
|
|
|
+ content: props.currentQuestion.ccQuestContent,
|
|
|
|
|
+ contentAnswer: props.currentQuestion.ccAiAnswer,
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 数字人接口
|
|
// 数字人接口
|
|
@@ -230,8 +245,9 @@ const createAiChart = async () => {
|
|
|
await getXzAi()
|
|
await getXzAi()
|
|
|
|
|
|
|
|
// 智能问答
|
|
// 智能问答
|
|
|
- CreateDialogue({ roleId: xZAiData.value.id })
|
|
|
|
|
|
|
+ await CreateDialogue({ roleId: xZAiData.value.id })
|
|
|
.then(res => {
|
|
.then(res => {
|
|
|
|
|
+ console.log("创建会话:", res);
|
|
|
activeConversationId.value = res.data
|
|
activeConversationId.value = res.data
|
|
|
})
|
|
})
|
|
|
.catch(error => {
|
|
.catch(error => {
|
|
@@ -262,12 +278,12 @@ const sendMessage = async () => {
|
|
|
console.error('保存AI问答次数失败:', error)
|
|
console.error('保存AI问答次数失败:', error)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 模拟 AI 回复
|
|
|
|
|
- const aiResponse = await simulateAIResponse(prompt.value)
|
|
|
|
|
- messageList.value.push({
|
|
|
|
|
- type: 'ai',
|
|
|
|
|
- content: aiResponse
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ // // 模拟 AI 回复
|
|
|
|
|
+ // const aiResponse = await simulateAIResponse(prompt.value)
|
|
|
|
|
+ // messageList.value.push({
|
|
|
|
|
+ // type: 'ai',
|
|
|
|
|
+ // content: aiResponse
|
|
|
|
|
+ // })
|
|
|
|
|
|
|
|
// 清空输入框
|
|
// 清空输入框
|
|
|
prompt.value = ''
|
|
prompt.value = ''
|
|
@@ -336,7 +352,8 @@ const doSendMessage = async content => {
|
|
|
// 执行发送
|
|
// 执行发送
|
|
|
await doSendMessageStream({
|
|
await doSendMessageStream({
|
|
|
conversationId: activeConversationId.value,
|
|
conversationId: activeConversationId.value,
|
|
|
- content: content
|
|
|
|
|
|
|
+ content: content,
|
|
|
|
|
+ contentAnswer: null,
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -368,10 +385,11 @@ const doSendMessageStream = async userMessage => {
|
|
|
|
|
|
|
|
// 2. 发送 event stream
|
|
// 2. 发送 event stream
|
|
|
let isFirstChunk = true // 是否是第一个 chunk 消息段
|
|
let isFirstChunk = true // 是否是第一个 chunk 消息段
|
|
|
-
|
|
|
|
|
|
|
+ console.log("userMessage", userMessage)
|
|
|
await sendChatMessageStream(
|
|
await sendChatMessageStream(
|
|
|
userMessage.conversationId,
|
|
userMessage.conversationId,
|
|
|
userMessage.content,
|
|
userMessage.content,
|
|
|
|
|
+ userMessage.contentAnswer,
|
|
|
conversationInAbortController.value,
|
|
conversationInAbortController.value,
|
|
|
enableContext.value,
|
|
enableContext.value,
|
|
|
async res => {
|
|
async res => {
|
|
@@ -380,25 +398,39 @@ const doSendMessageStream = async userMessage => {
|
|
|
console.log(`对话异常! ${msg}`)
|
|
console.log(`对话异常! ${msg}`)
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
- receiveMessageFullText.value = receiveMessageFullText.value + data.receive.content
|
|
|
|
|
- // 首次返回需要添加一个 message 到页面,后面的都是更新
|
|
|
|
|
- if (isFirstChunk) {
|
|
|
|
|
- isFirstChunk = false
|
|
|
|
|
- // 弹出两个假数据
|
|
|
|
|
- messageList.value.pop()
|
|
|
|
|
- messageList.value.pop()
|
|
|
|
|
- // 更新返回的数据
|
|
|
|
|
- messageList.value.push(data.send)
|
|
|
|
|
- messageList.value.push(data.receive)
|
|
|
|
|
- } else {
|
|
|
|
|
- // 更新最后一条消息
|
|
|
|
|
- if (messageList.value.length > 0) {
|
|
|
|
|
- const lastMessage = messageList.value[messageList.value.length - 1]
|
|
|
|
|
- if (lastMessage.id === data.receive.id) {
|
|
|
|
|
- lastMessage.content = receiveMessageFullText.value
|
|
|
|
|
|
|
+
|
|
|
|
|
+ if (data.eventType === 'TEXT') {
|
|
|
|
|
+
|
|
|
|
|
+ // 如果内容为空,就不处理。
|
|
|
|
|
+ if (data.receive?.content === '') {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ receiveMessageFullText.value += data.receive.content
|
|
|
|
|
+ // 首次返回需要添加一个 message 到页面,后面的都是更新
|
|
|
|
|
+ if (isFirstChunk) {
|
|
|
|
|
+ isFirstChunk = false
|
|
|
|
|
+ // 弹出两个假数据
|
|
|
|
|
+ messageList.value.pop()
|
|
|
|
|
+ messageList.value.pop()
|
|
|
|
|
+ // 更新返回的数据
|
|
|
|
|
+ messageList.value.push(data.send)
|
|
|
|
|
+ messageList.value.push(data.receive)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ //更新最后一条消息
|
|
|
|
|
+ if (messageList.value.length > 0) {
|
|
|
|
|
+ const lastMessage = messageList.value[messageList.value.length - 1]
|
|
|
|
|
+ if (lastMessage.id === data.receive.id) {
|
|
|
|
|
+ lastMessage.content = receiveMessageFullText.value
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ } else if (data.eventType === 'AUDIO') {
|
|
|
|
|
+ // 处理音频消息
|
|
|
|
|
+ await playAudioChunk(data.audioData);
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ // 添加此行确保触发滚动
|
|
|
|
|
+ scrollToBottom()
|
|
|
},
|
|
},
|
|
|
error => {
|
|
error => {
|
|
|
console.log(`对话异常! ${error}`)
|
|
console.log(`对话异常! ${error}`)
|
|
@@ -463,7 +495,36 @@ watch(() => props.questionDialogVisible, (newVal) => {
|
|
|
selectedOption.value = null
|
|
selectedOption.value = null
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
-
|
|
|
|
|
|
|
+// 监听消息列表变化,自动滚动到底部
|
|
|
|
|
+watch(messageList, () => {
|
|
|
|
|
+ scrollToBottom()
|
|
|
|
|
+}, { deep: true })
|
|
|
|
|
+
|
|
|
|
|
+//处理滚动事件,判断用户是否手动滚动
|
|
|
|
|
+const handleScroll = () => {
|
|
|
|
|
+ if (messageContainer.value) {
|
|
|
|
|
+ const { scrollTop, scrollHeight, clientHeight } = messageContainer.value
|
|
|
|
|
+ // 当用户滚动距离底部超过50px时,认为是手动滚动
|
|
|
|
|
+ userScrolled.value = scrollTop + clientHeight < scrollHeight - 50
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+// 单独的滚动到底部函数
|
|
|
|
|
+const scrollToBottom = () => {
|
|
|
|
|
+
|
|
|
|
|
+ // 如果用户手动滚动过,不自动滚动
|
|
|
|
|
+ if (userScrolled.value) return
|
|
|
|
|
+
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ if (messageContainer.value) {
|
|
|
|
|
+ // 强制重排以确保获取最新高度
|
|
|
|
|
+ messageContainer.value.scrollTop = messageContainer.value.scrollHeight
|
|
|
|
|
+ // 双重保险:使用requestAnimationFrame确保在浏览器重绘后执行
|
|
|
|
|
+ requestAnimationFrame(() => {
|
|
|
|
|
+ messageContainer.value.scrollTop = messageContainer.value.scrollHeight
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
onMounted(() => {
|
|
onMounted(() => {
|
|
|
// 初始化
|
|
// 初始化
|
|
|
})
|
|
})
|