You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
734 lines
24 KiB
734 lines
24 KiB
<template>
|
|
<div ref="aiChatRef" class="ai-chat">
|
|
<el-scrollbar ref="scrollDiv" @scroll="handleScrollTop">
|
|
<div ref="dialogScrollbar" class="ai-chat__content p-24 chat-width" style="padding: 24px;">
|
|
<div class="item-content mb-16" style="margin-bottom: 16px">
|
|
<div class="avatar">
|
|
<img src="@/assets/logo/logo2.png" height="30px" />
|
|
</div>
|
|
<div class="content">
|
|
<el-card shadow="always" class="dialog-card">
|
|
<span style="font-size: 14px">您好,我是 果知小助手,您可以向我提出关于 果知的相关问题。</span>
|
|
</el-card>
|
|
</div>
|
|
</div>
|
|
|
|
<template v-for="(item, index) in chatList" :key="index">
|
|
<!-- 问题 -->
|
|
<div v-if="item.type === 'question'" class="item-content mb-16 lighter" style="margin-bottom: 16px;font-weight: 400">
|
|
<div class="avatar">
|
|
<el-avatar style="width:30px;height: 30px;background: #3370FF">
|
|
<img src="@/assets/aichat/user-icon.svg" style="width: 30px" alt="" />
|
|
</el-avatar>
|
|
</div>
|
|
<div class="content">
|
|
<div class="text break-all pre-wrap" style="word-break: break-all;white-space: pre-wrap;">
|
|
{{ item.content }}
|
|
</div>
|
|
<el-tag type="success" style="margin-bottom: 5px;cursor: pointer" v-for="(fileName,index) in item.file" :disable-transitions="false" @click="downloadFile(fileName.file,fileName.bucket,item.sessionId)">{{fileName.file}}</el-tag>
|
|
</div>
|
|
</div>
|
|
<!-- 回答 -->
|
|
<div v-if="item.type === 'answer'" class="item-content mb-16 lighter" style="margin-bottom: 16px;font-weight: 400">
|
|
<div class="avatar">
|
|
<img src="@/assets/logo/logo2.png" height="30px" />
|
|
</div>
|
|
<div class="content">
|
|
<el-card shadow="always" class="dialog-card">
|
|
<MdRenderer :is_large="is_large" :chatIndex="index" :source="item.content" @fullscreenG6="fullscreen"></MdRenderer>
|
|
</el-card>
|
|
<div class="flex-between mt-8" style="display: flex; justify-content: space-between; align-items: center; margin-top: 8px">
|
|
<div>
|
|
<el-button
|
|
type="primary"
|
|
v-if="item.isStop && !item.isEnd"
|
|
@click="startChat(index)"
|
|
link
|
|
>重新生成
|
|
</el-button>
|
|
<el-button type="primary" v-else-if="!item.isEnd" @click="stopChat(index)" link
|
|
>停止回答
|
|
</el-button>
|
|
</div>
|
|
</div>
|
|
<div v-if="item.isEnd" class="flex-between" style="display: flex; justify-content: space-between; align-items: center;">
|
|
<OperationButton :data="item" :index="index" @regeneration="regenerationChart(index)" @changeThumb="changeThumb" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</el-scrollbar>
|
|
<div class="ai-chat__operate p-24" style="padding: 24px">
|
|
<!-- <slot name="operateBefore" />-->
|
|
<div class="chat-width">
|
|
<el-button type="primary" link class="new-chat-button mb-8" style="margin-bottom: 8px" @click="openUpload">
|
|
<el-icon><Upload /></el-icon><span class="ml-4">导入文件</span>
|
|
</el-button>
|
|
</div>
|
|
<div>
|
|
<el-tag type="success" style="margin-bottom: 5px" v-for="(tag,index) in currentFiles" closable :disable-transitions="false" @close="removeFile(index)">{{tag.file}}</el-tag>
|
|
</div>
|
|
<div>
|
|
<span v-if="currentMachine.length > 0">与 </span>
|
|
<el-tag style="margin-bottom: 5px" size="large" v-for="tag in currentMachine" closable :disable-transitions="false" @close="removeMachine">
|
|
<span style="font-size: 16px;margin-left: 10px">{{tag}}</span>
|
|
</el-tag>
|
|
<span v-if="currentMachine.length > 0"> 对话</span>
|
|
</div>
|
|
<div class="operate-textarea flex chat-width">
|
|
<el-popover
|
|
:visible="popoverVisible"
|
|
placement="top"
|
|
:width="is_large?'400px':'860px'"
|
|
popper-style="max-width:860px;z-index:99999;margin-left:35px"
|
|
:show-arrow=false
|
|
:offset=1
|
|
>
|
|
<template #reference>
|
|
<el-input
|
|
ref="quickInputRef"
|
|
v-model="inputValue"
|
|
:placeholder="'@选择机器人,Ctrl+Enter 换行,Enter发送'"
|
|
:autosize="{ minRows: 1, maxRows: 4 }"
|
|
type="textarea"
|
|
:maxlength="100000"
|
|
@input="handleInput"
|
|
@keydown.enter="sendChatHandle($event)"
|
|
/>
|
|
</template>
|
|
<template #default>
|
|
<div style="width: 100%" v-click-outside="clickoutside">
|
|
<div style="align-items: center;display: flex">
|
|
<span style="font-size: 25px;font-weight: bold">选择智能体</span>
|
|
<el-link :underline="false" style="font-size: 20px;margin-left: auto" @click="closeMachinePop"><i class="ri-close-large-line"></i></el-link>
|
|
</div>
|
|
<el-scrollbar max-height="200px">
|
|
<div class="machineDiv" @click="chooseMachine('元数据专家','@/assets/aichat/智能体logo.jpg')" style="align-items: center;width:100%;display: flex">
|
|
<el-avatar style="width: 30px;height: 30px"><img src="@/assets/aichat/智能体logo.jpg"></el-avatar>
|
|
<span style="font-size: 16px;margin-left: 10px">元数据专家</span>
|
|
</div>
|
|
<div class="machineDiv" @click="chooseMachine('数据标准专家','@/assets/aichat/智能体logo.jpg')" style="align-items: center;width:100%;display: flex">
|
|
<el-avatar style="width: 30px;height: 30px"><img src="@/assets/aichat/智能体logo.jpg"></el-avatar>
|
|
<span style="font-size: 16px;margin-left: 10px">数据标准专家</span>
|
|
</div>
|
|
<div class="machineDiv" @click="chooseMachine('数据质量专家','@/assets/aichat/智能体logo.jpg')" style="align-items: center;width:100%;display: flex">
|
|
<el-avatar style="width: 30px;height: 30px"><img src="@/assets/aichat/智能体logo.jpg"></el-avatar>
|
|
<span style="font-size: 16px;margin-left: 10px">数据质量专家</span>
|
|
</div>
|
|
<div class="machineDiv" @click="chooseMachine('数据模型专家','@/assets/aichat/智能体logo.jpg')" style="align-items: center;width:100%;display: flex">
|
|
<el-avatar style="width: 30px;height: 30px"><img src="@/assets/aichat/智能体logo.jpg"></el-avatar>
|
|
<span style="font-size: 16px;margin-left: 10px">数据模型专家</span>
|
|
</div>
|
|
<div class="machineDiv" @click="chooseMachine('数据安全专家','@/assets/aichat/智能体logo.jpg')" style="align-items: center;width:100%;display: flex">
|
|
<el-avatar style="width: 30px;height: 30px"><img src="@/assets/aichat/智能体logo.jpg"></el-avatar>
|
|
<span style="font-size: 16px;margin-left: 10px">数据安全专家</span>
|
|
</div>
|
|
<div class="machineDiv" @click="chooseMachine('数据分析专家','@/assets/aichat/智能体logo.jpg')" style="align-items: center;width:100%;display: flex">
|
|
<el-avatar style="width: 30px;height: 30px"><img src="@/assets/aichat/智能体logo.jpg"></el-avatar>
|
|
<span style="font-size: 16px;margin-left: 10px">数据分析专家</span>
|
|
</div>
|
|
<div class="machineDiv" @click="chooseMachine('数据治理管理专家','@/assets/aichat/智能体logo.jpg')" style="align-items: center;width:100%;display: flex">
|
|
<el-avatar style="width: 30px;height: 30px"><img src="@/assets/aichat/智能体logo.jpg"></el-avatar>
|
|
<span style="font-size: 16px;margin-left: 10px">数据治理管理专家</span>
|
|
</div>
|
|
</el-scrollbar>
|
|
</div>
|
|
</template>
|
|
</el-popover>
|
|
<div class="operate flex align-center">
|
|
<el-button
|
|
text
|
|
class="sent-button"
|
|
:disabled="isDisabledChart || loading"
|
|
@click="sendChatHandle"
|
|
>
|
|
<img v-show="isDisabledChart || loading" src="@/assets/aichat/icon_send.svg" alt="" />
|
|
<img v-show="!isDisabledChart && !loading" src="@/assets/aichat/icon_send_colorful.svg" alt="" />
|
|
</el-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- 文件导入对话框 -->
|
|
<el-dialog :z-index="99999" title="文件导入" v-model="upload.open" width="400px" append-to-body>
|
|
<el-upload v-if="upload.open"
|
|
ref="uploadRef"
|
|
:headers="upload.headers"
|
|
:action="upload.url"
|
|
:data = "upload.data"
|
|
:disabled="upload.isUploading"
|
|
:on-progress="handleFileUploadProgress"
|
|
:on-success="handleFileSuccess"
|
|
:auto-upload="false"
|
|
drag
|
|
>
|
|
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
|
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
|
</el-upload>
|
|
<template #footer>
|
|
<div class="dialog-footer">
|
|
<el-button type="primary" @click="submitFileForm">确 定</el-button>
|
|
<el-button @click="upload.open = false">取 消</el-button>
|
|
</div>
|
|
</template>
|
|
</el-dialog>
|
|
<el-dialog z-index="99999999999999999999" title="ER图全览" v-if="showFullscreenG6Data" :fullscreen="true" v-model="showFullscreenG6Data" append-to-body>
|
|
<fullscreenG6 :g6Data="fullscreenG6Data"></fullscreenG6>
|
|
</el-dialog>
|
|
</div>
|
|
</template>
|
|
<script setup>
|
|
import { ref, nextTick, computed, watch, reactive, onMounted } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
import antvg6 from './antv-g6.vue'
|
|
import OperationButton from './OperationButton.vue'
|
|
import MdRenderer from '@/views/aichat/MdRenderer.vue'
|
|
import fullscreenG6 from '@/views/aichat/fullscreenG6.vue'
|
|
import {getToken} from "@/utils/auth.js";
|
|
import {postChatMessage} from "@/api/aichat/aichat.js"
|
|
import cache from "@/plugins/cache.js";
|
|
import Cookies from "js-cookie";
|
|
import {addChat} from "@/api/aichat/aichat";
|
|
|
|
defineOptions({ name: 'AiChat' })
|
|
const route = useRoute()
|
|
const fullscreenG6Data = ref(null)
|
|
const showFullscreenG6Data = ref(false)
|
|
const {
|
|
params: { accessToken, id },
|
|
query: { mode }
|
|
} = route
|
|
const props = defineProps({
|
|
is_large: Boolean,
|
|
cookieSessionId: String,
|
|
data: {
|
|
type: Object,
|
|
default: () => {}
|
|
},
|
|
record: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
chatId: {
|
|
type: String,
|
|
default: ''
|
|
}, // 历史记录Id
|
|
debug: {
|
|
type: Boolean,
|
|
default: false
|
|
}
|
|
})
|
|
|
|
const { proxy } = getCurrentInstance();
|
|
|
|
const aiChatRef = ref()
|
|
const quickInputRef = ref()
|
|
const scrollDiv = ref()
|
|
const dialogScrollbar = ref()
|
|
const loading = ref(false)
|
|
const inputValue = ref('')
|
|
const chartOpenId = ref('')
|
|
const chatList = ref([])
|
|
const answerList = ref([])
|
|
const controller = ref(null)
|
|
|
|
const popoverVisible = ref(false)
|
|
const currentMachine = ref([])
|
|
const currentFiles = ref([])
|
|
const upload = reactive({
|
|
// 是否显示弹出层(用户导入)
|
|
open: false,
|
|
// 弹出层标题(用户导入)
|
|
title: "",
|
|
// 是否禁用上传
|
|
isUploading: false,
|
|
// 设置上传的请求头部
|
|
headers: { Authorization: "Bearer " + getToken() },
|
|
// 上传的地址
|
|
url: import.meta.env.VITE_APP_BASE_API + "/aichat/upload",
|
|
data: {"sessionId":Cookies.get("chatSessionId")}
|
|
});
|
|
const isDisabledChart = computed(
|
|
() => !(inputValue.value.trim())
|
|
)
|
|
|
|
const emit = defineEmits(['scroll'])
|
|
|
|
function setScrollBottom() {
|
|
// 将滚动条滚动到最下面
|
|
scrollDiv.value.setScrollTop(getMaxHeight())
|
|
}
|
|
|
|
|
|
/**
|
|
* 滚动条距离最上面的高度
|
|
*/
|
|
const scrollTop = ref(0)
|
|
|
|
|
|
const getMaxHeight = () => {
|
|
return dialogScrollbar.value.scrollHeight
|
|
}
|
|
const handleScrollTop = ($event) => {
|
|
scrollTop.value = $event.scrollTop
|
|
emit('scroll', { ...$event, dialogScrollbar: dialogScrollbar.value, scrollDiv: scrollDiv.value })
|
|
}
|
|
|
|
const handleScroll = () => {
|
|
if (scrollDiv.value) {
|
|
// 内部高度小于外部高度 就需要出滚动条
|
|
if (scrollDiv.value.wrapRef.offsetHeight < dialogScrollbar.value.scrollHeight) {
|
|
// 如果当前滚动条距离最下面的距离在 规定距离 滚动条就跟随
|
|
scrollDiv.value.setScrollTop(getMaxHeight())
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**文件上传中处理 */
|
|
const handleFileUploadProgress = (event, file, fileList) => {
|
|
upload.isUploading = true;
|
|
};
|
|
|
|
const handleFileSuccess = (response, file, fileList) => {
|
|
currentFiles.value.push({file:response.data.file,bucket:response.data.bucket})
|
|
upload.open = false;
|
|
upload.isUploading = false;
|
|
proxy.$modal.msgSuccess(response.msg);
|
|
// proxy.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", "导入结果", { dangerouslyUseHTMLString: true });
|
|
};
|
|
|
|
/** 提交上传文件 */
|
|
function submitFileForm() {
|
|
proxy.$refs["uploadRef"].submit();
|
|
}
|
|
|
|
watch(
|
|
() => props.chatId,
|
|
(val) => {
|
|
if (val && val !== 'new') {
|
|
chartOpenId.value = val
|
|
} else {
|
|
chartOpenId.value = ''
|
|
}
|
|
},
|
|
{ deep: true }
|
|
)
|
|
|
|
watch(
|
|
() => props.record,
|
|
(value) => {
|
|
chatList.value = value
|
|
},
|
|
{
|
|
immediate: true
|
|
}
|
|
)
|
|
|
|
watch(() => props.cookieSessionId, value => upload.data = {sessionId:value})
|
|
|
|
function regenerationChart(index){
|
|
let question = chatList.value[index - 1]
|
|
let chat = {
|
|
"type":"question",
|
|
"content":question.content,
|
|
"time": formatDate(new Date()),
|
|
"file": question.file}
|
|
chatList.value.push(chat)
|
|
let data = {
|
|
"query": chat.content,
|
|
"user_id": cache.local.get("username"),
|
|
"robot": currentMachine.value.length>0?currentMachine.value[0]:"",
|
|
"session_id": Cookies.get("chatSessionId"),
|
|
"doc": chat.file,
|
|
"history": []
|
|
}
|
|
sendChatMessage(data)
|
|
}
|
|
|
|
function changeThumb(index,chat){
|
|
chatList.value[index].operate = chat.operate
|
|
chatList.value[index].thumbDownReason = chat.thumbDownReason
|
|
}
|
|
|
|
function openUpload(){
|
|
upload.open = true
|
|
}
|
|
function chooseMachine(val){
|
|
inputValue.value = inputValue.value.slice(0,-1)
|
|
currentMachine.value = [val]
|
|
popoverVisible.value = false
|
|
}
|
|
|
|
function closeMachinePop(){
|
|
if (inputValue.value.endsWith('@')){
|
|
inputValue.value = inputValue.value.slice(0,-1)
|
|
}
|
|
popoverVisible.value = false
|
|
}
|
|
|
|
function removeMachine(){
|
|
currentMachine.value = []
|
|
}
|
|
|
|
function fullscreen(data){
|
|
fullscreenG6Data.value = data
|
|
showFullscreenG6Data.value = true
|
|
}
|
|
|
|
function clickoutside() {
|
|
if (inputValue.value.endsWith('@')){
|
|
inputValue.value = inputValue.value.slice(0,-1)
|
|
}
|
|
popoverVisible.value = false
|
|
}
|
|
|
|
function removeFile(index){
|
|
currentFiles.value = currentFiles.value.slice(index,1)
|
|
}
|
|
|
|
function handleInput() {
|
|
popoverVisible.value = inputValue.value.endsWith("@");
|
|
}
|
|
|
|
function padZero(num) {
|
|
return num < 10 ? '0' + num : num;
|
|
}
|
|
|
|
function formatDate(date) {
|
|
const year = date.getFullYear();
|
|
const month = padZero(date.getMonth() + 1); // 月份从0开始,所以需要+1
|
|
const day = padZero(date.getDate());
|
|
const hours = padZero(date.getHours());
|
|
const minutes = padZero(date.getMinutes());
|
|
const seconds = padZero(date.getSeconds());
|
|
|
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
}
|
|
|
|
function downloadFile(file,bucket,sessionId){
|
|
let data = {file,bucket,sessionId}
|
|
proxy.download("/default-api/aichat/file/download", {
|
|
...data,
|
|
}, file);
|
|
}
|
|
|
|
async function sendChatHandle(event) {
|
|
if (!event.ctrlKey) {
|
|
// 如果没有按下组合键ctrl,则会阻止默认事件
|
|
event.preventDefault()
|
|
if (inputValue.value.trim() && (chatList.value.length === 0||
|
|
(chatList.value[chatList.value.length - 1].isStop ||
|
|
chatList.value[chatList.value.length - 1].isEnd))) {
|
|
chatList.value.push({
|
|
"type": "question",
|
|
"content": inputValue.value.trim(),
|
|
"time": formatDate(new Date()),
|
|
"sessionId": Cookies.get("chatSessionId"),
|
|
"sessionName": chatList.value.length > 0 ? chatList.value[0].content.substring(0, 20) : inputValue.value.trim().substring(0, 20),
|
|
"file": currentFiles.value
|
|
})
|
|
let question = JSON.parse(JSON.stringify(chatList.value[chatList.value.length - 1]))
|
|
question.file = JSON.stringify(question.file)
|
|
await addChat(question)
|
|
nextTick(() => {
|
|
// 将滚动条滚动到最下面
|
|
scrollDiv.value.setScrollTop(getMaxHeight())
|
|
})
|
|
let data = {
|
|
"query": inputValue.value.trim(),
|
|
"user_id": cache.local.get("username"),
|
|
"robot": currentMachine.value.length > 0 ? currentMachine.value[0] : "",
|
|
"session_id": Cookies.get("chatSessionId"),
|
|
"doc": currentFiles.value,
|
|
"history": []
|
|
}
|
|
inputValue.value = ''
|
|
sendChatMessage(data)
|
|
}
|
|
} else {
|
|
// 如果同时按下ctrl+回车键,则会换行
|
|
inputValue.value += '\n'
|
|
}
|
|
}
|
|
|
|
function sendChatMessage(data){
|
|
controller.value = new AbortController()
|
|
postChatMessage(data,{signal:controller.value.signal}).then(res=>{
|
|
if (res.status !== 200){
|
|
chatList.value.push({"type":"answer","content":[{"type":"text","content":"服务异常,错误码:"+res.status}],"isEnd":true,"isStop":false,"sessionId":chatList.value[0].sessionId,"sessionName":chatList.value[0].sessionName,"operate":'',"thumbDownReason":''})
|
|
}else {
|
|
currentFiles.value = []
|
|
chatList.value.push({"type":"answer","content":[],"isEnd":false,"isStop":false,"sessionId":chatList.value[0].sessionId,"sessionName":chatList.value[0].sessionName, "operate":'',"thumbDownReason":''})
|
|
const reader = res.body.getReader()
|
|
const write = getWrite(reader)
|
|
reader.read().then(write).then(()=> {
|
|
let answer = JSON.parse(JSON.stringify(chatList.value[chatList.value.length - 1]))
|
|
answer.content = JSON.stringify(answer.content)
|
|
addChat(answer)
|
|
})
|
|
}
|
|
}).catch((e) => {
|
|
chatList.value.push({"type":"answer","content":[{"type":"text","content":"服务异常"}],"isEnd":true,"isStop":false,"sessionId":chatList.value[0].sessionId,"sessionName":chatList.value[0].sessionName,"operate":"","thumbDownReason":""})
|
|
})
|
|
}
|
|
watch(
|
|
chatList,
|
|
() => {
|
|
nextTick(() => {
|
|
// 将滚动条滚动到最下面
|
|
scrollDiv.value.setScrollTop(getMaxHeight())
|
|
})
|
|
},{
|
|
deep:true, immediate:true
|
|
}
|
|
)
|
|
const getWrite = (reader) => {
|
|
// 修复点1:将tempResult改为局部变量,避免递归状态污染
|
|
let tempResult = '';
|
|
|
|
const write_stream = ({ done, value }) => {
|
|
if (done) return;
|
|
const decoder = new TextDecoder('utf-8');
|
|
let str = decoder.decode(value, { stream: true });
|
|
console.log(str)
|
|
tempResult += str;
|
|
|
|
// 修复点2:使用非贪婪匹配确保精确分块
|
|
const split = tempResult.match(/data:.*?}\r\n/g);
|
|
|
|
if (split) {
|
|
// 修复点3:按顺序处理每个匹配块,避免遗漏
|
|
for (let i = 0; i < split.length; i++) {
|
|
const chunkStr = split[i];
|
|
tempResult = tempResult.replace(chunkStr, '', 1); // 修复点4:单次替换避免残留
|
|
|
|
try {
|
|
const chunk = JSON.parse(chunkStr.replace('data:', '').trim());
|
|
processChunk(chunk);
|
|
} catch (e) {
|
|
console.error('解析错误:', e, chunkStr);
|
|
}
|
|
}
|
|
|
|
// 递归处理剩余数据
|
|
return reader.read().then(write_stream);
|
|
} else {
|
|
// 无匹配块时继续累积数据
|
|
return reader.read().then(write_stream);
|
|
}
|
|
};
|
|
|
|
const processChunk = (chunk) => {
|
|
const lastMsg = chatList.value[chatList.value.length - 1];
|
|
|
|
// 修复点5:统一处理所有类型的数据块
|
|
if (chunk.docs?.length) {
|
|
lastMsg.content.push({ content: chunk.docs[0], type: "docs" });
|
|
} else if (chunk.G6_ER?.length) {
|
|
lastMsg.content.push({ content: chunk.G6_ER, type: "G6_ER" });
|
|
} else if (chunk.html_image?.length) {
|
|
lastMsg.content.push({ content: chunk.html_image, type: "html_image" });
|
|
} else if (chunk.table?.length) {
|
|
lastMsg.content.push({ content: chunk.table, type: "table" });
|
|
} else if (chunk.choices?.length) {
|
|
// 修复点6:纯文本处理增加防重复校验
|
|
// const text = chunk.choices[0].delta.content.replace(/\n/g, "\n\n");
|
|
// 智能换行处理
|
|
const content = chunk.choices[0].delta.content;
|
|
const isNewParagraph = content.startsWith('\n\n');
|
|
// 仅当需要时添加换行
|
|
const text = isNewParagraph
|
|
? content.replace(/\n{2,}/g, '\n\n')
|
|
: content.replace(/\n/g, ' ');
|
|
const lastContent = lastMsg.content[lastMsg.content.length - 1];
|
|
console.log(lastContent)
|
|
if (lastContent?.type === "text") {
|
|
lastContent.content += text;
|
|
} else {
|
|
lastMsg.content.push({ content: text, type: "text" });
|
|
}
|
|
}
|
|
|
|
// 修复点7:统一处理结束标志
|
|
if (chunk.isEnd || chunk.is_end) {
|
|
lastMsg.isEnd = true;
|
|
console.log(lastMsg)
|
|
lastMsg.time = formatDate(new Date());
|
|
}
|
|
|
|
nextTick(() => scrollDiv.value.setScrollTop(getMaxHeight()));
|
|
};
|
|
|
|
return write_stream;
|
|
};
|
|
|
|
const stopChat = (index) => {
|
|
chatList.value[index].isStop = true
|
|
if (controller.value !== null){
|
|
controller.value.abort()
|
|
}
|
|
let answer = JSON.parse(JSON.stringify(chatList.value[index]))
|
|
answer.content = JSON.stringify(answer.content)
|
|
addChat(answer)
|
|
}
|
|
const startChat = (index) => {
|
|
regenerationChart(index)
|
|
}
|
|
|
|
defineExpose({
|
|
setScrollBottom
|
|
})
|
|
|
|
</script>
|
|
<style lang="scss" scoped>
|
|
.ai-chat {
|
|
--padding-left: 40px;
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
box-sizing: border-box;
|
|
position: relative;
|
|
color: #1f2329;
|
|
|
|
&__content {
|
|
height: auto;
|
|
padding-top: 0;
|
|
box-sizing: border-box;
|
|
|
|
.avatar {
|
|
float: left;
|
|
}
|
|
|
|
.content {
|
|
padding-left: var(--padding-left);
|
|
|
|
:deep(ol) {
|
|
margin-left: 16px !important;
|
|
}
|
|
}
|
|
|
|
.text {
|
|
padding: 6px 0;
|
|
}
|
|
|
|
.problem-button {
|
|
width: 100%;
|
|
border: none;
|
|
border-radius: 8px;
|
|
background: #f5f6f7;
|
|
height: 46px;
|
|
padding: 0 12px;
|
|
line-height: 46px;
|
|
box-sizing: border-box;
|
|
color: #1f2329;
|
|
-webkit-line-clamp: 1;
|
|
word-break: break-all;
|
|
|
|
&:hover {
|
|
background: var(--el-color-primary-light-9);
|
|
}
|
|
|
|
&.disabled {
|
|
&:hover {
|
|
background: #f5f6f7;
|
|
}
|
|
}
|
|
|
|
:deep(.el-icon) {
|
|
color: var(--el-color-primary);
|
|
}
|
|
}
|
|
}
|
|
|
|
&__operate {
|
|
background: #f3f7f9;
|
|
position: relative;
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
z-index: 10;
|
|
|
|
&:before {
|
|
background: linear-gradient(0deg, #f3f7f9 0%, rgba(243, 247, 249, 0) 100%);
|
|
content: '';
|
|
position: absolute;
|
|
width: 100%;
|
|
top: -16px;
|
|
left: 0;
|
|
height: 16px;
|
|
}
|
|
|
|
.operate-textarea {
|
|
box-shadow: 0px 6px 24px 0px rgba(31, 35, 41, 0.08);
|
|
background-color: #ffffff;
|
|
border-radius: 8px;
|
|
border: 1px solid #ffffff;
|
|
box-sizing: border-box;
|
|
|
|
&:has(.el-textarea__inner:focus) {
|
|
border: 1px solid var(--el-color-primary);
|
|
scrollbar-width: none;
|
|
}
|
|
|
|
:deep(.el-textarea__inner) {
|
|
border-radius: 8px !important;
|
|
box-shadow: none;
|
|
resize: none;
|
|
padding: 12px 16px;
|
|
box-sizing: border-box;
|
|
scrollbar-width: none;
|
|
}
|
|
|
|
.operate {
|
|
padding: 6px 10px;
|
|
.el-icon {
|
|
font-size: 20px;
|
|
}
|
|
|
|
.sent-button {
|
|
max-height: none;
|
|
.el-icon {
|
|
font-size: 24px;
|
|
}
|
|
}
|
|
|
|
:deep(.el-loading-spinner) {
|
|
margin-top: -15px;
|
|
|
|
.circular {
|
|
width: 31px;
|
|
height: 31px;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.dialog-card {
|
|
border: none;
|
|
border-radius: 8px;
|
|
box-sizing: border-box;
|
|
}
|
|
}
|
|
|
|
.chat-width {
|
|
max-width: 80%;
|
|
margin: 0 auto;
|
|
}
|
|
.machineDiv {
|
|
cursor: pointer;
|
|
padding-top: 3px;
|
|
padding-bottom: 3px;
|
|
}
|
|
.machineDiv:hover {
|
|
background-color: #ebf1ff;
|
|
font-weight: bold;
|
|
}
|
|
@media only screen and (max-width: 1000px) {
|
|
.chat-width {
|
|
max-width: 100% !important;
|
|
margin: 0 auto;
|
|
}
|
|
}
|
|
</style>
|
|
|