|
@ -15,18 +15,26 @@ |
|
|
|
|
|
|
|
|
<template v-for="(item, index) in chatList" :key="index"> |
|
|
<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 v-if="item.type === 'question'"> |
|
|
<div class="avatar"> |
|
|
<div class="item-content mb-16 lighter" style="display:flex;justify-content:flex-end;gap:10px;margin-bottom: 16px;font-weight: 400"> |
|
|
<el-avatar style="width:30px;height: 30px;background: #3370FF"> |
|
|
<div> |
|
|
<img src="@/assets/aichat/user-icon.svg" style="width: 30px" alt="" /> |
|
|
<div class="text break-all pre-wrap" style="word-break: break-all;white-space: pre-wrap;"> |
|
|
</el-avatar> |
|
|
{{ item.content }} |
|
|
</div> |
|
|
</div> |
|
|
<div class="content"> |
|
|
|
|
|
<div class="text break-all pre-wrap" style="word-break: break-all;white-space: pre-wrap;"> |
|
|
|
|
|
{{ item.content }} |
|
|
|
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<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> |
|
|
|
|
|
<div style="display:flex;justify-content:flex-end;gap:10px;margin-bottom: 16px;font-weight: 400"> |
|
|
|
|
|
<el-text type="info"> |
|
|
|
|
|
<span class="ml-4" style="margin-left: 4px">{{ datetimeFormat(item.time) }}</span> |
|
|
|
|
|
</el-text> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<!-- 回答 --> |
|
|
<!-- 回答 --> |
|
|
<div v-if="item.type === 'answer'" class="item-content mb-16 lighter" style="margin-bottom: 16px;font-weight: 400"> |
|
|
<div v-if="item.type === 'answer'" class="item-content mb-16 lighter" style="margin-bottom: 16px;font-weight: 400"> |
|
|
<div class="avatar"> |
|
|
<div class="avatar"> |
|
@ -34,24 +42,109 @@ |
|
|
</div> |
|
|
</div> |
|
|
<div class="content"> |
|
|
<div class="content"> |
|
|
<el-card shadow="always" class="dialog-card"> |
|
|
<el-card shadow="always" class="dialog-card"> |
|
|
<MdRenderer :is_large="is_large" :chatIndex="index" :source="item.content" @fullscreenG6="fullscreen"></MdRenderer> |
|
|
<el-form |
|
|
|
|
|
:model="item" |
|
|
|
|
|
ref="formRef" |
|
|
|
|
|
label-width="125px"> |
|
|
|
|
|
<el-form-item label="原始问题:"> |
|
|
|
|
|
<el-input v-model="item.content.question" placeholder="请补充其它条件"></el-input> |
|
|
|
|
|
</el-form-item> |
|
|
|
|
|
<el-form-item v-if="item.content.query.length > 0" label="查询条件:"> |
|
|
|
|
|
<template v-for="filter in item.content.query"> |
|
|
|
|
|
<div v-if="filter.ct_type === 'mselect'" style="display:flex;width: 100%;margin-bottom: 10px"> |
|
|
|
|
|
<span style="width: 150px">{{filter.name + ": "}}</span> |
|
|
|
|
|
<el-select v-model="filter.default_value" multiple style="width: 100%;" clearable> |
|
|
|
|
|
<el-option v-for="opt in filter.options" :key="opt" :value="opt" :label="opt"></el-option> |
|
|
|
|
|
</el-select> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div v-if="filter.ct_type === 'datePicker'" style="display:flex;width: 100%;margin-bottom: 10px"> |
|
|
|
|
|
<span style="width: 150px">{{filter.name + ": "}}</span> |
|
|
|
|
|
<el-date-picker |
|
|
|
|
|
v-model="filter.default_value" |
|
|
|
|
|
type="date" |
|
|
|
|
|
placeholder="Pick a day" |
|
|
|
|
|
/> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div v-if="filter.ct_type === 'dateRangePicker'" style="display:flex;width: 100%;margin-bottom: 10px"> |
|
|
|
|
|
<span style="width: 150px">{{filter.name + ": "}}</span> |
|
|
|
|
|
<el-date-picker |
|
|
|
|
|
v-model="filter.dateRangeValue" |
|
|
|
|
|
type="daterange" |
|
|
|
|
|
range-separator="-" |
|
|
|
|
|
start-placeholder="Start date" |
|
|
|
|
|
end-placeholder="End date" |
|
|
|
|
|
/> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div v-if="filter.ct_type === 'radioGroup'" style="display:flex;width: 100%;margin-bottom: 10px"> |
|
|
|
|
|
<span style="width: 150px">{{filter.name + ": "}}</span> |
|
|
|
|
|
<el-radio-group v-model="filter.default_value"> |
|
|
|
|
|
<el-radio :key="'radio'+opt" v-for="opt in filter.options" :value="opt">{{ opt }}</el-radio> |
|
|
|
|
|
</el-radio-group> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div v-if="filter.ct_type === 'input'" style="display:flex;width: 100%;margin-bottom: 10px"> |
|
|
|
|
|
<span style="width: 150px">{{filter.name + ": "}}</span> |
|
|
|
|
|
<el-input style="width: calc(100% - 150px)" v-model="filter.default_value"/> |
|
|
|
|
|
</div> |
|
|
|
|
|
</template> |
|
|
|
|
|
|
|
|
|
|
|
</el-form-item> |
|
|
|
|
|
<el-form-item v-if="item.content.result.length > 0" label="输出结果:"> |
|
|
|
|
|
<span v-for="result in item.content.result">{{result.name}}</span> |
|
|
|
|
|
</el-form-item> |
|
|
|
|
|
<el-form-item label="数据结果:"> |
|
|
|
|
|
<template v-if="item.content.data"> |
|
|
|
|
|
<el-tabs type="border-card" v-model="item.content.tab" style="width: 100%" @tabChange="changeTab(item.content)"> |
|
|
|
|
|
<el-tab-pane label="数据结果" name="result"> |
|
|
|
|
|
<div style="max-height: calc(100vh - 400px);overflow: auto"> |
|
|
|
|
|
<el-table :data="item.content.data" style="height: 100%"> |
|
|
|
|
|
<el-table-column v-for="(val,key) in item.content.data[0]" :prop="key" :label="key"/> |
|
|
|
|
|
</el-table> |
|
|
|
|
|
</div> |
|
|
|
|
|
</el-tab-pane> |
|
|
|
|
|
<el-tab-pane label="分析结果" name="analysis" v-if="item.content.html_content && item.content.html_content !== ''"> |
|
|
|
|
|
<iframe :srcdoc="item.content.html_content" |
|
|
|
|
|
style="width: 100%;height: 300px;border: none; |
|
|
|
|
|
overflow-y:hidden; overflow-x: auto"></iframe> |
|
|
|
|
|
</el-tab-pane> |
|
|
|
|
|
<el-tab-pane label="sql" name="sql"> |
|
|
|
|
|
<SQLCodeMirror ref="codemirror" v-if="item.content.tab === 'sql'" :data="item.content.sql.replace('\n',' ')"></SQLCodeMirror> |
|
|
|
|
|
</el-tab-pane> |
|
|
|
|
|
</el-tabs> |
|
|
|
|
|
</template> |
|
|
|
|
|
<template v-else> |
|
|
|
|
|
<span>查询结果为空,请重新提问,或者点击编辑按钮辅助提问^_^!</span> |
|
|
|
|
|
</template> |
|
|
|
|
|
</el-form-item> |
|
|
|
|
|
</el-form> |
|
|
</el-card> |
|
|
</el-card> |
|
|
<div class="flex-between mt-8" style="display: flex; justify-content: space-between; align-items: center; margin-top: 8px"> |
|
|
<div class="flex-between" style="display: flex; justify-content: space-between; align-items: center;margin-top: 16px"> |
|
|
<div> |
|
|
<DataQueryButton :data="item" @edit="edit(item)" @download="download(item)"></DataQueryButton> |
|
|
<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> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div v-if="item.type === 'answer_err'" 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 content="content" style="padding-left:40px"> |
|
|
|
|
|
<el-card shadow="always" class="dialog-card"> |
|
|
|
|
|
<span style="font-size: 14px">{{item.content}}</span> |
|
|
|
|
|
</el-card> |
|
|
|
|
|
<div class="flex-between" style="display: flex; justify-content: space-between; align-items: center;margin-top: 16px"> |
|
|
|
|
|
<DataQueryButton :data="item"></DataQueryButton> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div v-if="item.type === 'loading'" 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 content="content" style="padding-left:40px"> |
|
|
|
|
|
<el-card shadow="always" class="dialog-card"> |
|
|
|
|
|
<span style="font-size: 14px">思考中...</span> |
|
|
|
|
|
</el-card> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
</template> |
|
|
</template> |
|
|
</div> |
|
|
</div> |
|
|
</el-scrollbar> |
|
|
</el-scrollbar> |
|
@ -61,22 +154,16 @@ |
|
|
<el-input |
|
|
<el-input |
|
|
ref="quickInputRef" |
|
|
ref="quickInputRef" |
|
|
v-model="inputValue" |
|
|
v-model="inputValue" |
|
|
:placeholder="'@选择机器人,Ctrl+Enter 换行,Enter发送'" |
|
|
:placeholder="'Ctrl+Enter 换行,Enter发送'" |
|
|
:autosize="{ minRows: 1, maxRows: 4 }" |
|
|
:autosize="{ minRows: 1, maxRows: 4 }" |
|
|
type="textarea" |
|
|
type="textarea" |
|
|
:maxlength="100000" |
|
|
:maxlength="100000" |
|
|
@input="handleInput" |
|
|
|
|
|
@keydown.enter="sendChatHandle($event)" |
|
|
@keydown.enter="sendChatHandle($event)" |
|
|
/> |
|
|
/> |
|
|
<div class="operate flex align-center"> |
|
|
<div class="operate flex align-center"> |
|
|
<el-button |
|
|
<el-button text class="sent-button" :disabled="chatList.length>0 && chatList[chatList.length-1].type === 'loading'" @click="sendChatHandle"> |
|
|
text |
|
|
<img v-show="chatList.length>0 && chatList[chatList.length-1].type === 'loading'" src="@/assets/aichat/icon_send.svg" alt="" /> |
|
|
class="sent-button" |
|
|
<img v-show="chatList.length === 0 || chatList[chatList.length-1].type !== 'loading'" src="@/assets/aichat/icon_send_colorful.svg" alt="" /> |
|
|
: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-button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
@ -85,24 +172,16 @@ |
|
|
</template> |
|
|
</template> |
|
|
<script setup> |
|
|
<script setup> |
|
|
import { ref, nextTick, computed, watch, reactive, onMounted } from 'vue' |
|
|
import { ref, nextTick, computed, watch, reactive, onMounted } from 'vue' |
|
|
import { useRoute } from 'vue-router' |
|
|
import {postDataQuery} from "@/api/aichat/aichat" |
|
|
import MdRenderer from '@/views/aichat/MdRenderer.vue' |
|
|
import { datetimeFormat } from '@/utils/time' |
|
|
import {getToken} from "@/utils/auth.js"; |
|
|
import {v4 as uuidv4} from "uuid"; |
|
|
import {postChatMessage} from "@/api/aichat/aichat.js" |
|
|
|
|
|
import cache from "@/plugins/cache.js"; |
|
|
|
|
|
import Cookies from "js-cookie"; |
|
|
import Cookies from "js-cookie"; |
|
|
import {addChat} from "@/api/aichat/aichat"; |
|
|
import SQLCodeMirror from "@/components/codemirror/SQLCodeMirror.vue"; |
|
|
|
|
|
import DataQueryButton from "@/views/dataint/dataquery/DataQueryButton.vue"; |
|
|
|
|
|
|
|
|
defineOptions({ name: '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({ |
|
|
const props = defineProps({ |
|
|
is_large: Boolean, |
|
|
|
|
|
cookieSessionId: String, |
|
|
cookieSessionId: String, |
|
|
data: { |
|
|
data: { |
|
|
type: Object, |
|
|
type: Object, |
|
@ -132,36 +211,27 @@ const loading = ref(false) |
|
|
const inputValue = ref('') |
|
|
const inputValue = ref('') |
|
|
const chartOpenId = ref('') |
|
|
const chartOpenId = ref('') |
|
|
const chatList = ref([]) |
|
|
const chatList = ref([]) |
|
|
const answerList = ref([]) |
|
|
|
|
|
const controller = ref(null) |
|
|
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( |
|
|
const isDisabledChart = computed( |
|
|
() => !(inputValue.value.trim()) |
|
|
() => !(inputValue.value.trim()) |
|
|
) |
|
|
) |
|
|
|
|
|
|
|
|
const emit = defineEmits(['scroll']) |
|
|
const emit = defineEmits(['scroll','showEdit','download']) |
|
|
|
|
|
function changeTab(content){ |
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
function setScrollBottom() { |
|
|
function setScrollBottom() { |
|
|
// 将滚动条滚动到最下面 |
|
|
// 将滚动条滚动到最下面 |
|
|
scrollDiv.value.setScrollTop(getMaxHeight()) |
|
|
scrollDiv.value.setScrollTop(getMaxHeight()) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function edit(data){ |
|
|
|
|
|
emit('showEdit',data) |
|
|
|
|
|
} |
|
|
|
|
|
function download(data){ |
|
|
|
|
|
emit('download',data) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 滚动条距离最上面的高度 |
|
|
* 滚动条距离最上面的高度 |
|
@ -188,25 +258,6 @@ const handleScroll = () => { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**文件上传中处理 */ |
|
|
|
|
|
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( |
|
|
watch( |
|
|
() => props.chatId, |
|
|
() => props.chatId, |
|
|
(val) => { |
|
|
(val) => { |
|
@ -231,69 +282,12 @@ watch( |
|
|
|
|
|
|
|
|
watch(() => props.cookieSessionId, value => upload.data = {sessionId:value}) |
|
|
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){ |
|
|
function changeThumb(index,chat){ |
|
|
chatList.value[index].operate = chat.operate |
|
|
chatList.value[index].operate = chat.operate |
|
|
chatList.value[index].thumbDownReason = chat.thumbDownReason |
|
|
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) { |
|
|
function padZero(num) { |
|
|
return num < 10 ? '0' + num : num; |
|
|
return num < 10 ? '0' + num : num; |
|
@ -310,70 +304,144 @@ function formatDate(date) { |
|
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; |
|
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function downloadFile(file,bucket,sessionId){ |
|
|
|
|
|
let data = {file,bucket,sessionId} |
|
|
|
|
|
proxy.download("aichat/file/download", { |
|
|
|
|
|
...data, |
|
|
|
|
|
}, file); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
async function sendChatHandle(event) { |
|
|
async function sendChatHandle(event) { |
|
|
if (!event.ctrlKey) { |
|
|
if (!event.ctrlKey) { |
|
|
// 如果没有按下组合键ctrl,则会阻止默认事件 |
|
|
// 如果没有按下组合键ctrl,则会阻止默认事件 |
|
|
event.preventDefault() |
|
|
event.preventDefault() |
|
|
if (inputValue.value.trim() && (chatList.value.length === 0|| |
|
|
if (inputValue.value.trim().length>0) { |
|
|
(chatList.value[chatList.value.length - 1].isStop || |
|
|
let query = { |
|
|
chatList.value[chatList.value.length - 1].isEnd))) { |
|
|
|
|
|
chatList.value.push({ |
|
|
|
|
|
"type": "question", |
|
|
"type": "question", |
|
|
"content": inputValue.value.trim(), |
|
|
"content": inputValue.value.trim(), |
|
|
"time": formatDate(new Date()), |
|
|
"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), |
|
|
handleSend(query,inputValue.value.trim()) |
|
|
"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 = '' |
|
|
inputValue.value = '' |
|
|
sendChatMessage(data) |
|
|
|
|
|
} |
|
|
} |
|
|
} else { |
|
|
} else { |
|
|
// 如果同时按下ctrl+回车键,则会换行 |
|
|
// 如果同时按下ctrl+回车键,则会换行 |
|
|
inputValue.value += '\n' |
|
|
inputValue.value += '\n' |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
function handleSend(queryObj,inputVal){ |
|
|
|
|
|
queryObj.sessionId = Cookies.get("chatSessionId") |
|
|
|
|
|
queryObj.sessionName = chatList.value.length > 0 ? chatList.value[0].content.substring(0, 20) : inputValue.value.trim().substring(0, 20), |
|
|
|
|
|
chatList.value.push(queryObj) |
|
|
|
|
|
chatList.value.push({type:'loading'}) |
|
|
|
|
|
// let question = JSON.parse(JSON.stringify(chatList.value[chatList.value.length - 1])) |
|
|
|
|
|
// await addChat(question) |
|
|
|
|
|
nextTick(() => { |
|
|
|
|
|
// 将滚动条滚动到最下面 |
|
|
|
|
|
scrollDiv.value.setScrollTop(getMaxHeight()) |
|
|
|
|
|
}) |
|
|
|
|
|
let data = { |
|
|
|
|
|
query: inputVal, |
|
|
|
|
|
} |
|
|
|
|
|
sendChatMessage(data) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function generate_existing_names(existing_names, paramName){ |
|
|
|
|
|
if (existing_names.length > 0){ |
|
|
|
|
|
if (paramName in existing_names){ |
|
|
|
|
|
let lastPara = paramName.charAt(paramName.length -1) |
|
|
|
|
|
let num = parseInt(lastPara) |
|
|
|
|
|
if (isNaN(num)){ |
|
|
|
|
|
paramName += "1" |
|
|
|
|
|
}else{ |
|
|
|
|
|
paramName = paramName.replace(lastPara,num+1) |
|
|
|
|
|
} |
|
|
|
|
|
generate_existing_names(existing_names, paramName) |
|
|
|
|
|
}else { |
|
|
|
|
|
existing_names.push(paramName) |
|
|
|
|
|
} |
|
|
|
|
|
}else { |
|
|
|
|
|
existing_names.push(paramName) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
function sendChatMessage(data){ |
|
|
function sendChatMessage(data){ |
|
|
controller.value = new AbortController() |
|
|
postDataQuery(data).then(res=>{ |
|
|
postChatMessage(data,{signal:controller.value.signal}).then(res=>{ |
|
|
let control_params = res.filters |
|
|
if (res.status !== 200){ |
|
|
let data = res.data //表格结果 |
|
|
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":''}) |
|
|
let sql = res.sql // sql结果 |
|
|
|
|
|
let datas = res.datas // tree |
|
|
|
|
|
let html_content = res.html_content //echarts图表 |
|
|
|
|
|
let existing_names = [] |
|
|
|
|
|
let query = [] // 查询条件 |
|
|
|
|
|
let question = '' //原始问题 |
|
|
|
|
|
let result = [] //结果 |
|
|
|
|
|
for (let i = 0; i < control_params.length; i++) { |
|
|
|
|
|
let param = control_params[i] |
|
|
|
|
|
generate_existing_names(existing_names,param.name) |
|
|
|
|
|
if (param.ct_type === 'mselect' |
|
|
|
|
|
|| param.ct_type === 'datePicker' |
|
|
|
|
|
|| param.ct_type === 'dateRangePicker' |
|
|
|
|
|
|| param.ct_type === 'input' |
|
|
|
|
|
){ |
|
|
|
|
|
if ( ['维度筛选', '时间筛选', '分组条件', '查询其它条件'].includes(param.cd_type)){ |
|
|
|
|
|
if (param.ct_type === 'dateRangePicker'){ |
|
|
|
|
|
param.dateRangeValue = [param.default_value.start_date,param.default_value.end_date] |
|
|
|
|
|
} |
|
|
|
|
|
query.push(param) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
if (param.ct_type === 'output' && param.cd_type === '输出结果'){ |
|
|
|
|
|
result.push(param) |
|
|
|
|
|
} |
|
|
|
|
|
if (param.name === '原始问题'){ |
|
|
|
|
|
question = param.default_value |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
if (chatList.value[chatList.value.length - 1].type === 'loading'){ |
|
|
|
|
|
chatList.value[chatList.value.length - 1] = { |
|
|
|
|
|
type:'answer', |
|
|
|
|
|
"time": formatDate(new Date()), |
|
|
|
|
|
content:{ |
|
|
|
|
|
query:query, |
|
|
|
|
|
question: question === ''? chatList.value[chatList.value.length - 2].content:question, |
|
|
|
|
|
result: result, |
|
|
|
|
|
data: data, |
|
|
|
|
|
sql: sql, |
|
|
|
|
|
datas: datas, |
|
|
|
|
|
html_content: html_content, |
|
|
|
|
|
tab:'result' |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
}else { |
|
|
}else { |
|
|
currentFiles.value = [] |
|
|
chatList.value.push( |
|
|
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() |
|
|
type:'answer', |
|
|
const write = getWrite(reader) |
|
|
"time": formatDate(new Date()), |
|
|
reader.read().then(write).then(()=> { |
|
|
content:{ |
|
|
let answer = JSON.parse(JSON.stringify(chatList.value[chatList.value.length - 1])) |
|
|
query:query, |
|
|
answer.content = JSON.stringify(answer.content) |
|
|
question: question === ''? chatList.value[chatList.value.length - 1].content:question, |
|
|
addChat(answer) |
|
|
result: result, |
|
|
}) |
|
|
data: data, |
|
|
|
|
|
sql: sql, |
|
|
|
|
|
datas: datas, |
|
|
|
|
|
html_content: html_content, |
|
|
|
|
|
tab:'result' |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
) |
|
|
} |
|
|
} |
|
|
}).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":""}) |
|
|
}).catch(err=>{ |
|
|
|
|
|
if (chatList.value[chatList.value.length - 1].type === 'loading'){ |
|
|
|
|
|
chatList.value[chatList.value.length - 1] = { |
|
|
|
|
|
type:'answer_err', |
|
|
|
|
|
content: "服务器繁忙,请稍后再试", |
|
|
|
|
|
"time": formatDate(new Date()), |
|
|
|
|
|
} |
|
|
|
|
|
}else { |
|
|
|
|
|
chatList.value.push( |
|
|
|
|
|
{ |
|
|
|
|
|
type:'answer_err', |
|
|
|
|
|
"time": formatDate(new Date()), |
|
|
|
|
|
content: "服务器繁忙,请稍后再试" |
|
|
|
|
|
} |
|
|
|
|
|
) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
}) |
|
|
}) |
|
|
} |
|
|
} |
|
|
watch( |
|
|
watch( |
|
@ -381,103 +449,17 @@ watch( |
|
|
() => { |
|
|
() => { |
|
|
nextTick(() => { |
|
|
nextTick(() => { |
|
|
// 将滚动条滚动到最下面 |
|
|
// 将滚动条滚动到最下面 |
|
|
scrollDiv.value.setScrollTop(getMaxHeight()) |
|
|
if (chatList.value.length >0){ |
|
|
|
|
|
scrollDiv.value.setScrollTop(getMaxHeight()) |
|
|
|
|
|
} |
|
|
}) |
|
|
}) |
|
|
},{ |
|
|
},{ |
|
|
deep:true, immediate:true |
|
|
deep:true, immediate:true |
|
|
} |
|
|
} |
|
|
) |
|
|
) |
|
|
const getWrite = (reader) => { |
|
|
|
|
|
let tempResult = ''; |
|
|
|
|
|
/** |
|
|
|
|
|
* 处理流式数据 |
|
|
|
|
|
* @param {done, value} obj - 包含 done 和 value 属性的对象 |
|
|
|
|
|
*/ |
|
|
|
|
|
const write_stream = ({ done,value }) => { |
|
|
|
|
|
try { |
|
|
|
|
|
if (done) { |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
const decoder = new TextDecoder('utf-8'); |
|
|
|
|
|
let str = decoder.decode(value, { stream: true }); |
|
|
|
|
|
// 解释:数据流返回的 chunk 可能不完整,因此我们需要累积并拆分它们。 |
|
|
|
|
|
tempResult += str; |
|
|
|
|
|
console.log(tempResult) |
|
|
|
|
|
const split = tempResult.match(/data:.*}\r\n/g); |
|
|
|
|
|
if (split) { |
|
|
|
|
|
str = split.join(''); |
|
|
|
|
|
tempResult = tempResult.replace(str, ''); |
|
|
|
|
|
} else { |
|
|
|
|
|
return reader.read().then(write_stream); |
|
|
|
|
|
} |
|
|
|
|
|
// 如果 str 存在且以 "data:" 开头,则处理数据块。 |
|
|
|
|
|
if (str && str.startsWith('data:')) { |
|
|
|
|
|
split.forEach((chunkStr) => { |
|
|
|
|
|
const chunk = JSON.parse(chunkStr.replace('data:', '').trim()); |
|
|
|
|
|
if (chunk.docs && chunk.docs[0].length > 0){ |
|
|
|
|
|
//超链接 |
|
|
|
|
|
chatList.value[chatList.value.length - 1].content.push({"content":chunk.docs[0],"type":"docs"}) |
|
|
|
|
|
}else if (chunk.G6_ER && chunk.G6_ER.length > 0){ |
|
|
|
|
|
//说明是ER图 |
|
|
|
|
|
chatList.value[chatList.value.length - 1].content.push({"content":chunk.G6_ER,"type":"G6_ER"}) |
|
|
|
|
|
}else if (chunk.html_image && chunk.html_image.length > 0){ |
|
|
|
|
|
//说明是html的echarts图 |
|
|
|
|
|
chatList.value[chatList.value.length - 1].content.push({"content":chunk.html_image,"type":"html_image"}) |
|
|
|
|
|
}else if (chunk.table && chunk.table.length > 0){ |
|
|
|
|
|
//说明是 表格 |
|
|
|
|
|
chatList.value[chatList.value.length - 1].content.push({"content":chunk.table,"type":"table"}) |
|
|
|
|
|
}else { |
|
|
|
|
|
// 纯文本 |
|
|
|
|
|
let last_answer = chatList.value[chatList.value.length - 1].content |
|
|
|
|
|
chunk.choices[0].delta.content = chunk.choices[0].delta.content.replace("\n","\n\n") |
|
|
|
|
|
if (last_answer.length > 0) { |
|
|
|
|
|
if(last_answer[last_answer.length - 1].type === 'text'){ |
|
|
|
|
|
chatList.value[chatList.value.length - 1].content[last_answer.length - 1].content += chunk.choices[0].delta.content |
|
|
|
|
|
}else{ |
|
|
|
|
|
chatList.value[chatList.value.length - 1].content.push({"content":chunk.choices[0].delta.content,"type":"text"}) |
|
|
|
|
|
} |
|
|
|
|
|
}else{ |
|
|
|
|
|
chatList.value[chatList.value.length - 1].content.push({"content":chunk.choices[0].delta.content,"type":"text"}) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
nextTick(() => { |
|
|
|
|
|
// 将滚动条滚动到最下面 |
|
|
|
|
|
scrollDiv.value.setScrollTop(getMaxHeight()) |
|
|
|
|
|
}) |
|
|
|
|
|
if (chunk.isEnd || chunk.is_end){ |
|
|
|
|
|
chatList.value[chatList.value.length - 1].isEnd = true |
|
|
|
|
|
chatList.value[chatList.value.length - 1].time = formatDate(new Date()) |
|
|
|
|
|
nextTick(() => { |
|
|
|
|
|
// 将滚动条滚动到最下面 |
|
|
|
|
|
scrollDiv.value.setScrollTop(getMaxHeight()) |
|
|
|
|
|
}) |
|
|
|
|
|
return Promise.resolve(); |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
return Promise.reject(e); |
|
|
|
|
|
} |
|
|
|
|
|
return reader.read().then(write_stream); |
|
|
|
|
|
}; |
|
|
|
|
|
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({ |
|
|
defineExpose({ |
|
|
setScrollBottom |
|
|
setScrollBottom,handleSend |
|
|
}) |
|
|
}) |
|
|
|
|
|
|
|
|
</script> |
|
|
</script> |
|
|