|
@@ -13,40 +13,53 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="main-content">
|
|
|
- <div class="chat-container">
|
|
|
+ <div v-loading="loadingHistoryRecord" class="chat-container" ref="chatContainerRef">
|
|
|
<div class="message-list">
|
|
|
<div v-for="(item, index) in chatRecordList" :key="item.id">
|
|
|
- <!-- 会话图标 -->
|
|
|
- <!--<div :class="item.isUser == 1? 'user-image' : 'system-image' "></div>-->
|
|
|
<div v-if="item.isUser == 1" class="user-message">
|
|
|
- <img src="@/assets/images/common/ai/user.png" alt="icon"/>
|
|
|
+ <img src="@/assets/images/common/ai/user.png" alt="icon" style="margin-left: 4px"/>
|
|
|
<span class="user-message-content">{{item.content}}</span>
|
|
|
</div>
|
|
|
- <div v-else class="message bot-message">
|
|
|
- <img src="@/assets/images/common/ai/system.png" alt="icon"/>
|
|
|
- <div v-show="!expandIndexList.includes(index)" class="bot-message-content" v-html="item.content.substring(0, 80)"></div>
|
|
|
- <div v-show="expandIndexList.includes(index)" class="bot-message-content" v-html="item.content"></div>
|
|
|
- <el-button v-if="item.content.length>80" @click="foldOrExpandMessage(index, expandIndexList.includes(index))">{{expandIndexList.includes(index)?'折叠':'展开'}}</el-button>
|
|
|
+ <div v-else class="bot-message">
|
|
|
+ <img src="@/assets/images/common/ai/system.png" alt="icon" style="margin-left: 4px"/>
|
|
|
+ <div style="margin-left: 4px;">
|
|
|
+ <div class="bot-message-content">
|
|
|
+ <div v-if="index === chatRecordList.length-1">
|
|
|
+ <div v-if="item.isTyping">
|
|
|
+ <div v-html="item.content"></div>
|
|
|
+ <span class="loading-dots" style="display: flex;justify-content: center">
|
|
|
+ <span class="dot"></span>
|
|
|
+ <span class="dot"></span>
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <div v-else>
|
|
|
+ <div v-html="item.content"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-else>
|
|
|
+ <div v-show="!expandIndexList.includes(index)" v-html="item.content.substring(0, 80)"></div>
|
|
|
+ <div v-show="expandIndexList.includes(index)" v-html="item.content"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div style="margin: 4px">
|
|
|
+ <el-text>{{item.createTime}}</el-text>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-if="index !== chatRecordList.length-1">
|
|
|
+ <el-button v-if="item.content.length>80" style="margin-left: 4px"
|
|
|
+ @click="foldOrExpandMessage(index, expandIndexList.includes(index))">
|
|
|
+ {{expandIndexList.includes(index)?'折叠':'展开'}}
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
<el-button>播放</el-button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div class="message-box">
|
|
|
- <div class="typing-message">
|
|
|
- <span>
|
|
|
- <span v-html="streamHtmlData"></span>
|
|
|
- <span class="loading-dots">
|
|
|
- <span class="dot"></span>
|
|
|
- <span class="dot"></span>
|
|
|
- </span>
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <el-button @click="stopMessage">停止回答</el-button>
|
|
|
</div>
|
|
|
<div class="input-container">
|
|
|
<el-input
|
|
|
v-model="inputMessage"
|
|
|
+ type="textarea"
|
|
|
placeholder="请输入消息"
|
|
|
@keyup.enter="sendMessage"
|
|
|
></el-input>
|
|
@@ -59,10 +72,13 @@
|
|
|
<el-button style="width:140px">退出</el-button>
|
|
|
</div>
|
|
|
<div class="control-button">
|
|
|
- <el-button style="width:140px" @click="startVoice">静音</el-button>
|
|
|
+ <el-button style="width:140px" @click="startVoice">开始</el-button>
|
|
|
+ </div>
|
|
|
+ <div class="control-button">
|
|
|
+ <el-button style="width:140px" @click="closeVoiceOpen">结束</el-button>
|
|
|
</div>
|
|
|
<div class="control-button">
|
|
|
- <el-button style="width:140px">新会话</el-button>
|
|
|
+ <el-button style="width:140px" @click="stopMessage">停止回答</el-button>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="session-list-box">
|
|
@@ -73,15 +89,18 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
- import {nextTick, onMounted} from 'vue'
|
|
|
- import {aiChatRecordList} from '@/api/xjc-integratedmachine/common/aiChat.js'
|
|
|
+ import {nextTick, onMounted, useTemplateRef} from 'vue'
|
|
|
+ import {aiChatRecordList, aiChatRecordAdd} from '@/api/xjc-integratedmachine/common/aiChat.js'
|
|
|
import { getToken } from '@/utils/auth'
|
|
|
+ import { setScroll } from '@/utils/scroll.js'
|
|
|
// md转换为html
|
|
|
import { marked } from 'marked'
|
|
|
// 语音识别
|
|
|
import CryptoJS from 'crypto-js';
|
|
|
import * as RecorderManager from "/public/ai/iat/dist/index.umd.js"
|
|
|
+ import * as AudioPlayer from "/public/ai/tts/dist/index.umd.js"
|
|
|
|
|
|
+ const loadingHistoryRecord = ref(false)
|
|
|
// 聊天记录
|
|
|
let chatRecordList = ref([])
|
|
|
// md流式数据
|
|
@@ -106,15 +125,27 @@
|
|
|
// 查看所有聊天记录
|
|
|
list();
|
|
|
function list() {
|
|
|
+ loadingHistoryRecord.value = true
|
|
|
let queryForm = {
|
|
|
-
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 10000
|
|
|
}
|
|
|
aiChatRecordList(queryForm).then(resp =>{
|
|
|
- console.log(resp)
|
|
|
chatRecordList.value = resp.rows;
|
|
|
+ loadingHistoryRecord.value = false
|
|
|
})
|
|
|
}
|
|
|
|
|
|
+ function addRecord(content) {
|
|
|
+ let queryForm = {
|
|
|
+ content: content,
|
|
|
+ isUser: 0
|
|
|
+ }
|
|
|
+ aiChatRecordAdd(queryForm).then(resp =>{
|
|
|
+ console.log(resp)
|
|
|
+
|
|
|
+ })
|
|
|
+ }
|
|
|
const sendMessage = () => {
|
|
|
if (inputMessage.value.trim()) {
|
|
|
sendRequest(inputMessage.value.trim())
|
|
@@ -130,7 +161,16 @@
|
|
|
isTyping: false
|
|
|
}
|
|
|
// 消息加入聊天记录
|
|
|
- // chatRecordList.value.push(userMessage)
|
|
|
+ chatRecordList.value.push(userMessage)
|
|
|
+ const botMsg = {
|
|
|
+ isUser: false,
|
|
|
+ content: '', // 增量填充
|
|
|
+ isTyping: true, // 显示加载动画
|
|
|
+ }
|
|
|
+ chatRecordList.value.push(botMsg)
|
|
|
+ const lastMsg = chatRecordList.value[chatRecordList.value.length - 1]
|
|
|
+
|
|
|
+
|
|
|
try{
|
|
|
isLoading.value = true;
|
|
|
streamMarkdownData.value = ''; // 清空之前的数据
|
|
@@ -166,16 +206,22 @@
|
|
|
chunk = chunk.replace(/\n\n/g, '').replace(/data:/g, '')
|
|
|
streamMarkdownData.value += chunk;
|
|
|
streamHtmlData.value = marked(streamMarkdownData.value)
|
|
|
+ lastMsg.content = marked(streamMarkdownData.value)
|
|
|
}
|
|
|
}catch (error) {
|
|
|
// 如果是手动中止,不显示错误
|
|
|
if (error.name !== 'AbortError') {
|
|
|
console.error('流式读取失败:', error);
|
|
|
- streamMarkdownData.value = 'Error: ' + error.message;
|
|
|
+ streamHtmlData.value = 'Error: ' + error.message;
|
|
|
}
|
|
|
} finally {
|
|
|
- streamHtmlData.value = marked(streamMarkdownData.value)
|
|
|
isLoading.value = false;
|
|
|
+ let htmlData = marked(streamMarkdownData.value);
|
|
|
+ addRecord(htmlData);
|
|
|
+ streamHtmlData.value = htmlData;
|
|
|
+ lastMsg.content = marked(streamMarkdownData.value)
|
|
|
+ lastMsg.isTyping = false
|
|
|
+ // list()
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -211,6 +257,7 @@
|
|
|
let startVoiceStatus = false
|
|
|
// 识别中状态
|
|
|
let identifyStatus = false
|
|
|
+ // 录音对象
|
|
|
let recorder = null
|
|
|
let transcription = ''
|
|
|
let btnStatus = ''
|
|
@@ -227,8 +274,10 @@
|
|
|
APIKey: '8b1a53486bec887eb817b4410aa743ed',
|
|
|
}
|
|
|
// 初始化语音识别
|
|
|
- function initRecognize(){
|
|
|
+ function initSpeechRecognition(){
|
|
|
+ // 初始化录音
|
|
|
recorder = new window.RecorderManager('/ai/iat/dist');
|
|
|
+ // 开始录音
|
|
|
recorder.onStart = () => {
|
|
|
changeBtnStatus('OPEN');
|
|
|
};
|
|
@@ -323,8 +372,8 @@
|
|
|
} else {
|
|
|
resultText += str;
|
|
|
}
|
|
|
- transcription = resultTextTemp || resultText || '';
|
|
|
- console.log("识别:"+transcription);
|
|
|
+ inputMessage.value = resultTextTemp || resultText || '';
|
|
|
+ console.log("识别:"+inputMessage.value);
|
|
|
}
|
|
|
if (jsonData.code === 0 && jsonData.data.status === 2) {
|
|
|
iatWS.close();
|
|
@@ -413,7 +462,7 @@
|
|
|
}
|
|
|
|
|
|
function countdown() {
|
|
|
- let seconds = 10;
|
|
|
+ let seconds = 60;
|
|
|
console.log(`录音中(${seconds}s)`);
|
|
|
countdownInterval = setInterval(() => {
|
|
|
seconds -= 1;
|
|
@@ -460,10 +509,31 @@
|
|
|
}
|
|
|
/* 语音识别 ↑*/
|
|
|
|
|
|
+ /*语音合成 ↓*/
|
|
|
+
|
|
|
+
|
|
|
+ function initSpeechsynthesis(){
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /*语音合成 ↑*/
|
|
|
+
|
|
|
+ let chatContainerRef = ref(null)
|
|
|
+ let chatContainerRefObj = useTemplateRef(chatContainerRef)
|
|
|
+
|
|
|
+
|
|
|
+ const scrollToBottom = () => {
|
|
|
+ nextTick(() => {
|
|
|
+ chatContainerRef.value.scrollTop = chatContainerRef.value.scrollHeight;
|
|
|
+ });
|
|
|
+ };
|
|
|
|
|
|
onMounted(()=>{
|
|
|
nextTick(()=>{
|
|
|
- initRecognize();
|
|
|
+ // 组件挂载后自动滚动到底部
|
|
|
+ scrollToBottom();
|
|
|
+ initSpeechRecognition();
|
|
|
+ initSpeechsynthesis();
|
|
|
sendMessage()
|
|
|
})
|
|
|
|
|
@@ -556,6 +626,11 @@
|
|
|
/* align-items: center; */
|
|
|
}
|
|
|
|
|
|
+ .typing {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+
|
|
|
.user-message {
|
|
|
display: flex;
|
|
|
align-self: flex-end;
|
|
@@ -643,6 +718,7 @@
|
|
|
/*输入框*/
|
|
|
.input-container {
|
|
|
display: flex;
|
|
|
+ align-items: center;
|
|
|
}
|
|
|
|
|
|
.input-container .el-input {
|