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.
 
 
 
 
 

1023 lines
36 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">
<span style="font-size: 14px">您好我是 果知小助手您可以向我提出关于 果知的相关问题</span>
</div>
</div>
<template v-for="(item, index) in chatList" :key="index">
<!-- 问题 -->
<div v-if="item.type === 'question'" class="item-content mb-16 lighter" style="font-weight: 400;display: flex;align-items: flex-start;max-width: 100%;flex-direction: row-reverse;">
<!-- <div class="avatar" style="margin-left: 10px">-->
<!-- <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" style="max-width: 100%;word-break: break-all;white-space: pre-wrap;">
<el-card class="chat_question_card" shadow="never" style="border-radius: 26px;background-color: #f5f5f5">
<div class="text break-all pre-wrap" style="word-break: break-all;white-space: pre-wrap;font-size: 14px">
{{ item.content }}
</div>
</el-card>
<el-tag type="success" style="margin-bottom: 5px;cursor: pointer;display: inline-block" 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="font-weight: 400">
<el-popconfirm popper-style="z-index:2000" title="确定要回到这一步吗?" @confirm="confirmReturn(item,index)">
<template #reference>
<div class="returnToHere" style="margin-top: 5px;margin-bottom: 5px">
<i class="ri-bookmark-2-fill returnToHereIcon"></i>
<el-divider class="returnToHereDivider">
<span class="divider-text">回到这里</span>
</el-divider>
<i class="ri-arrow-go-back-line returnToHereIcon"></i>
</div>
</template>
</el-popconfirm>
<!-- <div class="avatar">-->
<!-- <img src="@/assets/logo/logo2.png" height="30px" />-->
<!-- </div>-->
<div class="content">
<MdRenderer :is_large="is_large" :chatIndex="index" :source="item.content" @fullscreenG6="fullscreen"></MdRenderer>
<Interrupt :isLastChat="index === (chatList.length - 1)" :chatId="item.chatId" :action="item.action" :source="item.interrupt" :checkpointer="item.checkpointer" @processAuth="processAuth"></Interrupt>
<div v-if="item.isEnd" class="flex-between" style="display: flex; justify-content: space-between; align-items: center;">
<OperationButton :data="item" :index="index" @changeThumb="changeThumb" />
<!-- <OperationButton :data="item" :index="index" @regeneration="regenerationChart(index)" @changeThumb="changeThumb" />-->
</div>
</div>
</div>
</template>
<div class="flex-between mt-8" style="display: flex; justify-content: space-between; align-items: center; margin-top: 8px">
<div>
<div v-if="currentJobId !== ''">
<i class="ri-loader-2-line loading-icon"></i>
<span style="font-size: 14px;color: #409EFF">回复中...</span>
</div>
<!-- <el-button-->
<!-- type="primary"-->
<!-- v-if="chatList.length>0 && chatList[chatList.length-1].isStop && !chatList[chatList.length-1].isEnd"-->
<!-- @click="startChat(chatList.length-1)"-->
<!-- link-->
<!-- >重新生成-->
<!-- </el-button>-->
<!-- <el-button type="primary" v-else-if="chatList.length>0 && !chatList[chatList.length-1].isEnd" @click="stopChat(chatList.length-1)" link-->
<!-- >停止回答-->
<!-- </el-button>-->
<el-button type="primary" v-if="currentJobId !== ''" link @click="stopChat(chatList.length-1)"
>停止回答
</el-button>
</div>
</div>
<div v-if="currentError !== ''">
<div class="returnToHere" style="margin-top: 5px;margin-bottom: 5px">
<i class="ri-bookmark-2-fill returnToHereIcon"></i>
<el-divider class="returnToHereDivider">
</el-divider>
</div>
<span style="font-size: 14px">{{currentError}}</span>
<el-link :underline="false" icon="Refresh" type="primary" @click="refreshModal">重试</el-link>
</div>
</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:2000;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>
<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>
<el-popover
popper-style="z-index:2000;width:300px"
title="自动审批配置"
placement="left-end"
trigger="click"
>
<template #reference>
<el-button
text
style="padding: 0;margin: 0"
>
<el-icon><Setting /></el-icon>
</el-button>
</template>
<template #default>
<div class="autoProcessClass" @click.prevent="handleCheckAllRobot" style="cursor: pointer">
<el-checkbox
v-model="autoProcess.checkAll"
:indeterminate="autoProcess.isIndeterminate"
>
全部自动审批
</el-checkbox>
</div>
<el-divider style="margin-top: 5px;margin-bottom: 5px" />
<el-checkbox-group
v-model="autoProcess.autoArray"
style="display: flex; flex-direction: column"
>
<div v-for="robot in autoProcess.robots" @click.prevent="toggleRobot(robot)" class="autoProcessClass" style="cursor: pointer">
<el-checkbox :key="robot" :label="robot" :value="robot"></el-checkbox>
</div>
</el-checkbox-group>
</template>
</el-popover>
</div>
</div>
</div>
<!-- 文件导入对话框 -->
<el-dialog :z-index="2000" 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 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 {addChat, cancelJob, DeleteChatList, postChatMessage, updateChatProcessData} from "@/api/aichat/aichat.js"
import cache from "@/plugins/cache.js";
import Cookies from "js-cookie";
import Interrupt from "@/views/aichat/Interrupt.vue";
import { v4 as uuidv4 } from 'uuid';
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 currentQuestion = ref({})
const chartOpenId = ref('')
const chatList = ref([])
const controller = ref(null)
const currentChatData = ref({})
const autoProcess = ref({
checkAll: false,
isIndeterminate: false,
autoArray:[],
robots:['数据治理管理专家自动批准','元数据专家自动批准','数据标准专家自动批准','数据安全专家自动批准','数据分析专家自动批准','数据模型专家自动批准','数据质量专家自动批准','智能导航专家自动审批'],
})
const popoverVisible = ref(false)
const currentMachine = ref([])
const currentFiles = ref([])
const currentError = ref('')
const currentJobId = ref('')
const lastQuestion = 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())
}
function confirmReturn(item,index){
DeleteChatList(item.chatId).then(res=>{
chatList.value.splice(index, chatList.value.length - index);
let reqData = {
"user_id": cache.local.get("username"),
"session_id": Cookies.get("chatSessionId"),
"checkpointer": item.checkpointer
}
sendChatMessage(reqData)
})
}
function handleCheckAllRobot(){
autoProcess.value.checkAll = !autoProcess.value.checkAll
autoProcess.value.autoArray = autoProcess.value.checkAll ? autoProcess.value.robots : []
autoProcess.value.isIndeterminate = false
}
function toggleRobot(robot){
const index = autoProcess.value.autoArray.indexOf(robot)
if (index !== -1) {
// 已选中则取消
autoProcess.value.autoArray = autoProcess.value.autoArray.filter(item => item !== robot)
} else {
// 未选中则添加
autoProcess.value.autoArray = [...autoProcess.value.autoArray, robot]
}
const checkedCount = autoProcess.value.autoArray.length
autoProcess.value.checkAll = checkedCount === autoProcess.value.robots.length
autoProcess.value.isIndeterminate = checkedCount > 0 && checkedCount < autoProcess.value.robots.length
}
/**
* 滚动条距离最上面的高度
*/
const scrollTop = ref(0)
const getMaxHeight = () => {
return dialogScrollbar.value?.scrollHeight ?? 0;
}
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) {
// 如果当前滚动条距离最下面的距离在 规定距离 滚动条就跟随
// if (scorll.value) {
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){
//
// if (currentQuestion.value.query){
// sendChatMessage(currentQuestion.value)
// }else{
// // let question = chatList.value[index - 1]
// // let chat = {
// // "chatId":uuidv4(),
// // "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,
// // }
// 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);
}
function processAuth(data){
for (let i = 0; i < data.interrupt.block.length; i++) {
for (let key in data.formData){
if (key && key === data.interrupt.block[i].name){
data.interrupt.block[i].default_value = data.formData[key]
}
}
}
let updateData = {
chatId: data.chatId,
action: data.action,
interrupt: JSON.stringify(data.interrupt)
}
updateChatProcessData(updateData).then(res=>{
let reqData = {
"user_id": cache.local.get("username"),
"session_id": Cookies.get("chatSessionId"),
"checkpointer": data.checkpointer,
"action": data.action,
"resume": true,
"block": data.interrupt.block
}
currentChatData.value = reqData
sendChatMessage(reqData)
})
}
function refreshModal(){
sendChatMessage(currentChatData.value)
}
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({
"chatId": uuidv4(),
"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
}
if (chatList.value.length > 1){
// 判断为1 因为上方已将问题加入 chatList了 , >1 则视为已存在提问交互
//取最新的checkpointer值
data.checkpointer = chatList.value[chatList.value.length - 2].content.checkpointer
}
currentQuestion.value = data
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({"chatId":uuidv4(),"type":"answer","content":[{"type":"text","content":"服务异常,错误码:"+res.status}],"isEnd":true,"isStop":false,"sessionId":chatList.value[0].sessionId,"sessionName":chatList.value[0].sessionName,"operate":'',"thumbDownReason":''})
currentError.value = "服务异常,错误码:"+res.status +",请联系管理员!"
}else {
currentError.value = ''
currentChatData.value = {}
currentFiles.value = []
chatList.value.push({"chatId":uuidv4(),"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)
answer.interrupt = answer.interrupt ? JSON.stringify(answer.interrupt): null
answer.checkpointer = JSON.stringify(answer.checkpointer)
addChat(answer)
}).then(()=>{
let answer = JSON.parse(JSON.stringify(chatList.value[chatList.value.length - 1]))
if(answer.interrupt){
let robot = answer.interrupt.robot
let block = answer.interrupt.block
let action = answer.interrupt.action
let autoRequest = false
if (autoProcess.value.autoArray.length > 0 && autoProcess.value.autoArray.indexOf(robot) !== -1){
//自动审批信号有效,需判断 block内容的必填项,有无默认值,有的话,可以自动发送,没有需填写
autoRequest = true
if (block && block.length>0){
for (let i = 0; i < block.length; i++) {
if (block[i].required){
if (block[i].ct_type === 'dateRangePicker' || block[i].ct_type === 'checkboxGroup' || block[i].ct_type === 'multiselect'){
//default_value 是数组
if (!block[i].default_value || block[i].default_value === []){
autoRequest = false
}
}
if (block[i].ct_type === 'datePicker' || block[i].ct_type === 'input' || block[i].ct_type === 'radioGroup' ||block[i].ct_type === 'select'){
//default_value 是文字
if (!block[i].default_value || block[i].default_value.trim() === ''){
autoRequest = false
}
}
}
}
}
if (autoRequest){
let reqData = {
"user_id": cache.local.get("username"),
"session_id": Cookies.get("chatSessionId"),
"checkpointer": answer.checkpointer,
"action":"",
"resume": true,
"block": block
}
for (let i = 0; i < action.length; i++) {
if (action[i].style === 'primary'){
reqData.action = action[i].action
}
}
sendChatMessage(reqData)
}
}
}
})
}
}).catch((e) => {
currentError.value = "服务异常,请联系系统管理员!"
})
}
watch(
chatList,
() => {
nextTick(() => {
// 将滚动条滚动到最下面
// scrollDiv.value.setScrollTop(getMaxHeight())
handleScroll()
})
},{
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 });
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) => {
currentJobId.value = chunk.job_id;
const lastMsg = chatList.value[chatList.value.length - 1];
// 修复点5:统一处理所有类型的数据块
if (chunk.docs?.length) {
lastMsg.content.push({ content: chunk.docs, 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.router) {
lastMsg.content.push({ content: chunk.router, type: "router" });
} else if (chunk.choices?.length) {
// 修复点6:纯文本处理增加防重复校验
// const text = chunk.choices[0].delta.content.replace(/\n/g, "\n\n");
// 智能换行处理
const text = chunk.choices[0].delta.content;
// const isNewParagraph = content.startsWith('\n\n');
// 仅当需要时添加换行
// const text = content;
// const text = isNewParagraph
// ? content.replace(/\n{2,}/g, '\n\n')
// : content.replace(/\n/g, '\n\n');
const lastContent = lastMsg.content[lastMsg.content.length - 1];
if (lastContent?.type === "text") {
lastContent.content += text;
} else {
lastMsg.content.push({ content: text, type: "text" });
}
}
if (chunk.interrupt){
lastMsg.interrupt = chunk.interrupt
}
// 修复点7:统一处理结束标志
if (chunk.checkpointer) {
lastMsg.checkpointer = chunk.checkpointer
currentJobId.value = ''
lastMsg.isEnd = true;
lastMsg.time = formatDate(new Date());
}
nextTick(() => scrollDiv.value.setScrollTop(getMaxHeight()));
};
return write_stream;
};
const stopChat = (index) => {
if (currentJobId.value !== ''){
cancelJob({job_id:currentJobId.value})
}
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)
currentJobId.value = ''
}
// const startChat = (index) => {
// regenerationChart(index)
// }
defineExpose({
setScrollBottom
})
</script>
<style lang="scss" scoped>
.ai-chat {
--padding-left: 0;
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;
}
}
.autoProcessClass:hover {
background-color: var(--el-color-primary-light-9);
}
:deep(.chat_question_card .el-card__body) {
padding: 10px 20px !important;
}
.returnToHere {
display: flex;
align-items: center;
cursor: pointer;
/* 文字默认隐藏 */
--text-color: transparent;
--divider-border: rgba(0,0,0,0);
}
.returnToHereIcon {
color: var(--icon-color, #000);
transition: color 0.3s;
}
.returnToHereDivider {
flex: 1;
margin: 0 10px;
--divider-border-color: var(--divider-border);
}
/* 文字默认透明 */
.divider-text {
color: var(--text-color);
transition: color 0.3s;
}
/* 鼠标移入时整体变化 */
.returnToHere:hover {
--icon-color: #409EFF; /* 蓝色 */
--text-color: #409EFF; /* 文字变蓝 */
--divider-border: #409EFF; /* 分隔线变蓝 */
}
/* 处理Element Plus分隔线边框颜色 */
:deep(.el-divider__text) {
background: transparent !important;
padding: 0 !important;
}
:deep(.el-divider__line) {
border-top-color: var(--divider-border-color) !important;
}
/* 定义旋转动画 */
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* 应用动画到图标 */
//.ri-loader-2-line {
// display: inline-block; /* 确保 transform 生效 */
// animation: rotate 1s linear infinite; /* 1秒一圈,匀速旋转,无限循环 */
// transform-origin: 50% 50%;
//}
/* 可选:添加渐隐效果 */
//.ri-loader-2-line.fade {
// opacity: 0.7;
// transition: opacity 0.3s;
//}
.loading-icon {
display: inline-block; /* 确保 transform 生效 */
animation: rotate 1s linear infinite; /* 1秒一圈,匀速旋转,无限循环 */
transform-origin: 50% 50%;
color: #409EFF; /* 修改颜色 */
//animation: rotate 1s cubic-bezier(0.68, -0.55, 0.27, 1.55) infinite;
/* 使用非匀速旋转增加动感 */
}
</style>