13 changed files with 2383 additions and 295 deletions
@ -0,0 +1,105 @@ |
|||
<template> |
|||
<el-dialog width="600px" append-to-body :title="title" v-model="open"> |
|||
<el-form label-width="100px" ref="formRef" :model="form" :rules="rules"> |
|||
<el-form-item label="当前资产"> |
|||
<el-input |
|||
placeholder="自动带入" |
|||
:disabled="true" |
|||
v-model="currentRow.dataAstCnName" |
|||
/> |
|||
</el-form-item> |
|||
<el-form-item label="收藏目录" prop="contentOnum"> |
|||
<el-select |
|||
placeholder="请选择收藏目录" |
|||
:disabled="disabled" |
|||
v-model="form.contentOnum" |
|||
> |
|||
<el-option |
|||
v-for="item in bookmarkFolder" |
|||
:key="item.content_onum" |
|||
:label="item.content_name" |
|||
:value="item.content_onum" |
|||
/> |
|||
</el-select> |
|||
</el-form-item> |
|||
</el-form> |
|||
<template #footer> |
|||
<div class="dialog-footer"> |
|||
<el-button @click="cancel">取消</el-button> |
|||
<el-button type="primary" :disabled="disabled" @click="submitForm" |
|||
>确定</el-button |
|||
> |
|||
</div> |
|||
</template> |
|||
</el-dialog> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import useUserStore from '@/store/modules/user' |
|||
import { computed, nextTick } from 'vue' |
|||
import { |
|||
getBookmarkFolder, |
|||
addDirectoryCollection, |
|||
} from '@/api/dataAsset/directory' |
|||
|
|||
const userStore = useUserStore() |
|||
const title = ref('') |
|||
const open = ref(false) |
|||
const disabled = ref(false) |
|||
const { proxy } = getCurrentInstance() |
|||
const form = ref({}) |
|||
const rules = ref({ |
|||
contentOnum: [ |
|||
{ required: true, message: '收藏目录不能为空', trigger: 'blur' }, |
|||
], |
|||
}) |
|||
|
|||
const bookmarkFolder = ref([]) |
|||
const setBookmarkFolder = () => { |
|||
getBookmarkFolder().then(({ data }) => { |
|||
bookmarkFolder.value = data |
|||
}) |
|||
} |
|||
|
|||
const formRef = ref(null) |
|||
const currentRow = ref({}) |
|||
const openDialog = (row) => { |
|||
open.value = true |
|||
form.value = { |
|||
dataAstNo: undefined, |
|||
userId: undefined, |
|||
contentOnum: undefined, |
|||
} |
|||
setBookmarkFolder() |
|||
if (row.dataAstNo) { |
|||
currentRow.value = row |
|||
form.value = { |
|||
...form.value, |
|||
dataAstNo: String(row.dataAstNo), |
|||
userId: String(userStore.id), |
|||
} |
|||
} |
|||
nextTick(() => { |
|||
formRef.value.clearValidate() |
|||
}) |
|||
} |
|||
|
|||
const emit = defineEmits(['onSuccess']) |
|||
const submitForm = () => { |
|||
formRef.value.validate((valid) => { |
|||
if (valid) { |
|||
addDirectoryCollection(form.value).then((response) => { |
|||
proxy.$modal.msgSuccess('收藏成功') |
|||
open.value = false |
|||
emit('onSuccess') |
|||
}) |
|||
} |
|||
}) |
|||
} |
|||
|
|||
const cancel = () => { |
|||
open.value = false |
|||
} |
|||
|
|||
defineExpose({ title, disabled, openDialog }) |
|||
</script> |
@ -0,0 +1,82 @@ |
|||
<template> |
|||
<el-dialog width="600px" append-to-body :title="title" v-model="open"> |
|||
<el-form label-width="100px" ref="formRef" :model="form" :rules="rules"> |
|||
<el-form-item label="目录名称" prop="contentName"> |
|||
<el-input |
|||
placeholder="请输入目录名称" |
|||
:disabled="disabled" |
|||
v-model="form.contentName" |
|||
/> |
|||
</el-form-item> |
|||
</el-form> |
|||
<template #footer> |
|||
<div class="dialog-footer"> |
|||
<el-button @click="cancel">取消</el-button> |
|||
<el-button type="primary" :disabled="disabled" @click="submitForm" |
|||
>确定</el-button |
|||
> |
|||
</div> |
|||
</template> |
|||
</el-dialog> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { computed, nextTick } from 'vue' |
|||
import { |
|||
addBookmarkFolder, |
|||
updateBookmarkFolder, |
|||
} from '@/api/dataAsset/directory' |
|||
|
|||
const title = ref('') |
|||
const open = ref(false) |
|||
const disabled = ref(false) |
|||
const { proxy } = getCurrentInstance() |
|||
const form = ref({}) |
|||
const rules = ref({ |
|||
contentName: [ |
|||
{ required: true, message: '目录名称不能为空', trigger: 'blur' }, |
|||
], |
|||
}) |
|||
|
|||
const formRef = ref(null) |
|||
const openDialog = (row) => { |
|||
open.value = true |
|||
form.value = { |
|||
contentName: undefined, |
|||
contentStat: '1', |
|||
} |
|||
if (row.contentOnum) { |
|||
form.value = { |
|||
...form.value, |
|||
...row, |
|||
} |
|||
} |
|||
nextTick(() => { |
|||
formRef.value.clearValidate() |
|||
}) |
|||
} |
|||
|
|||
const emit = defineEmits(['onSuccess']) |
|||
const submitForm = () => { |
|||
formRef.value.validate((valid) => { |
|||
if (valid) { |
|||
const request = form.value.contentOnum |
|||
? updateBookmarkFolder |
|||
: addBookmarkFolder |
|||
request(form.value).then((response) => { |
|||
proxy.$modal.msgSuccess( |
|||
form.value.contentOnum ? '修改成功' : '新增成功' |
|||
) |
|||
open.value = false |
|||
emit('onSuccess') |
|||
}) |
|||
} |
|||
}) |
|||
} |
|||
|
|||
const cancel = () => { |
|||
open.value = false |
|||
} |
|||
|
|||
defineExpose({ title, disabled, openDialog }) |
|||
</script> |
@ -0,0 +1,117 @@ |
|||
<template> |
|||
<el-dialog width="800px" append-to-body :title="title" v-model="open"> |
|||
<el-form label-width="100px" ref="formRef" :model="form" :rules="rules"> |
|||
<el-row :gutter="16"> |
|||
<el-col :span="11"> |
|||
<el-form-item label="当前资产" prop="dataAstCnName"> |
|||
<el-input :disabled="true" v-model="form.dataAstCnName" /> |
|||
</el-form-item> |
|||
</el-col> |
|||
<el-col :span="2"> |
|||
<div class="arrow"> |
|||
<span>········</span> |
|||
<el-icon><Right /></el-icon> |
|||
</div> |
|||
</el-col> |
|||
<el-col :span="11"> |
|||
<el-form-item label="目标目录" prop="contentOnumAfter"> |
|||
<el-select |
|||
placeholder="请选择收藏目录" |
|||
:disabled="disabled" |
|||
v-model="form.contentOnumAfter" |
|||
> |
|||
<el-option |
|||
v-for="item in bookmarkFolder" |
|||
:key="item.content_onum" |
|||
:label="item.content_name" |
|||
:value="item.content_onum" |
|||
/> |
|||
</el-select> |
|||
</el-form-item> |
|||
</el-col> |
|||
</el-row> |
|||
</el-form> |
|||
<template #footer> |
|||
<div class="dialog-footer"> |
|||
<el-button @click="cancel">取消</el-button> |
|||
<el-button type="primary" :disabled="disabled" @click="submitForm" |
|||
>确定</el-button |
|||
> |
|||
</div> |
|||
</template> |
|||
</el-dialog> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { nextTick } from 'vue' |
|||
import { getBookmarkFolder, moveBookmarkAsset } from '@/api/dataAsset/directory' |
|||
|
|||
const title = ref('') |
|||
const open = ref(false) |
|||
const disabled = ref(false) |
|||
const { proxy } = getCurrentInstance() |
|||
const form = ref({}) |
|||
const rules = ref({ |
|||
contentOnumAfter: [ |
|||
{ required: true, message: '目标目录不能为空', trigger: 'blur' }, |
|||
], |
|||
}) |
|||
|
|||
const bookmarkFolder = ref([]) |
|||
const setBookmarkFolder = () => { |
|||
getBookmarkFolder().then(({ data }) => { |
|||
bookmarkFolder.value = data |
|||
}) |
|||
} |
|||
|
|||
const formRef = ref(null) |
|||
const openDialog = (row) => { |
|||
open.value = true |
|||
form.value = { |
|||
relaOnum: undefined, |
|||
contentOnum: undefined, |
|||
contentOnumAfter: undefined, |
|||
} |
|||
setBookmarkFolder() |
|||
if (row.relaOnum) { |
|||
form.value = { |
|||
...form.value, |
|||
...row, |
|||
} |
|||
} |
|||
nextTick(() => { |
|||
formRef.value.clearValidate() |
|||
}) |
|||
} |
|||
|
|||
const emit = defineEmits(['onSuccess']) |
|||
const submitForm = () => { |
|||
formRef.value.validate((valid) => { |
|||
if (valid) { |
|||
moveBookmarkAsset(form.value).then((response) => { |
|||
proxy.$modal.msgSuccess('移动成功') |
|||
open.value = false |
|||
emit('onSuccess') |
|||
}) |
|||
} |
|||
}) |
|||
} |
|||
|
|||
const cancel = () => { |
|||
open.value = false |
|||
} |
|||
|
|||
defineExpose({ title, disabled, openDialog }) |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.arrow { |
|||
display: flex; |
|||
font-size: 18px; |
|||
text-align: center; |
|||
margin: 8px auto; |
|||
span { |
|||
line-height: 18px; |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,633 @@ |
|||
<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> |
|||
</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> |
|||
</div> |
|||
</template> |
|||
</div> |
|||
</el-scrollbar> |
|||
<div class="ai-chat__operate p-24" style="padding: 24px"> |
|||
<!-- <slot name="operateBefore" />--> |
|||
<div class="operate-textarea flex chat-width"> |
|||
<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)" |
|||
/> |
|||
<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> |
|||
</div> |
|||
</template> |
|||
<script setup> |
|||
import { ref, nextTick, computed, watch, reactive, onMounted } from 'vue' |
|||
import { useRoute } from 'vue-router' |
|||
import MdRenderer from '@/views/aichat/MdRenderer.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("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) => { |
|||
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({ |
|||
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> |
@ -1,9 +1,206 @@ |
|||
<template> |
|||
<div>数据问答</div> |
|||
<div class="chat-embed layout-bg"> |
|||
<div class="chat-embed__header"> |
|||
<div class="chat-width flex align-center" style="height: 56px;align-items: center;line-height: 56px"> |
|||
<div class="mr-12 ml-24 flex"> |
|||
<el-avatar |
|||
shape="square" |
|||
:size="32" |
|||
style="background: none" |
|||
> |
|||
<img src="@/assets/logo/deepseek.png" alt="" /> |
|||
</el-avatar> |
|||
</div> |
|||
<h4 style="color: #1f2329;font-size: 16px; font-style: normal; font-weight: bold;margin: 0; -webkit-font-smoothing: antialiased">果知小助手</h4> |
|||
</div> |
|||
</div> |
|||
<div class="chat-embed__main"> |
|||
<dataquerychat |
|||
ref="AiChatRef" |
|||
:record="currentRecordList" |
|||
:is_large="is_large" |
|||
:cookieSessionId = "sessionId" |
|||
class="AiChat-embed" |
|||
@scroll="handleScroll" |
|||
></dataquerychat> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { ref, onMounted, reactive, nextTick, computed } from 'vue' |
|||
import { v4 as uuidv4 } from 'uuid'; |
|||
import Cookies from 'js-cookie' |
|||
import { useRoute } from 'vue-router' |
|||
import { listChatHistory, getChatList, DeleteChatSession } from "@/api/aichat/aichat"; |
|||
import Dataquerychat from "./dataquerychat.vue"; |
|||
const route = useRoute() |
|||
const { proxy } = getCurrentInstance(); |
|||
const { |
|||
params: { accessToken } |
|||
} = route |
|||
const props = defineProps({ |
|||
is_large: Boolean, |
|||
chatDataList: Array, |
|||
}) |
|||
const AiChatRef = ref() |
|||
const chatLogeData = ref([]) |
|||
const show = ref(false) |
|||
const currentRecordList = ref([]) |
|||
const sessionId = ref(Cookies.get("chatSessionId")) // 当前历史记录Id 默认为'new' |
|||
const mouseId = ref('') |
|||
|
|||
|
|||
|
|||
|
|||
|
|||
|
|||
function handleScroll(event) { |
|||
if (event.scrollTop === 0 && currentRecordList.value.length>0) { |
|||
const history_height = event.dialogScrollbar.offsetHeight |
|||
event.scrollDiv.setScrollTop(event.dialogScrollbar.offsetHeight - history_height) |
|||
} |
|||
} |
|||
|
|||
function clickListHandle(item){ |
|||
getChatList(item.sessionId).then(res=>{ |
|||
currentRecordList.value = [] |
|||
let array = res.data |
|||
for (let i = 0; i < array.length; i++) { |
|||
if (array[i].type === 'answer'){ |
|||
array[i].content = JSON.parse(array[i].content) |
|||
} |
|||
if (array[i].type === 'question'){ |
|||
array[i].file = JSON.parse(array[i].file) |
|||
} |
|||
currentRecordList.value.push(array[i]) |
|||
AiChatRef.value.setScrollBottom() |
|||
} |
|||
show.value = false |
|||
sessionId.value = item.sessionId |
|||
Cookies.set("chatSessionId",sessionId.value) |
|||
}) |
|||
} |
|||
watch(() => props.chatDataList, value => currentRecordList.value = JSON.parse(JSON.stringify(value))) |
|||
onMounted( |
|||
()=>{ |
|||
if (Cookies.get("chatSessionId")){ |
|||
//调用子页面的赋值方法,给子页面赋值 |
|||
getChatList(Cookies.get("chatSessionId")).then(res=>{ |
|||
let array = res.data |
|||
if (array && array.length >0){ |
|||
for (let i = 0; i < array.length; i++) { |
|||
if (array[i].type === 'answer'){ |
|||
array[i].content = JSON.parse(array[i].content) |
|||
} |
|||
if (array[i].type === 'question'){ |
|||
array[i].file = JSON.parse(array[i].file) |
|||
} |
|||
} |
|||
} |
|||
currentRecordList.value = array |
|||
}).then(()=>{ |
|||
AiChatRef.value.setScrollBottom() |
|||
}) |
|||
}else { |
|||
Cookies.set("chatSessionId",uuidv4()) |
|||
} |
|||
} |
|||
) |
|||
</script> |
|||
<style scoped lang="scss"> |
|||
|
|||
</style> |
|||
<style lang="scss"> |
|||
.chat-embed { |
|||
overflow: hidden; |
|||
height: 100%; |
|||
&__header { |
|||
background: linear-gradient(90deg, #ebf1ff 24.34%, #e5fbf8 56.18%, #f2ebfe 90.18%);; |
|||
position: absolute; |
|||
width: 100%; |
|||
left: 0; |
|||
top: 0; |
|||
z-index: 100; |
|||
height: 56px; |
|||
line-height: 56px; |
|||
box-sizing: border-box; |
|||
border-bottom: 1px solid #dee0e3; |
|||
} |
|||
&__main { |
|||
padding-top: 80px; |
|||
height: 100%; |
|||
overflow: hidden; |
|||
} |
|||
.new-chat-button { |
|||
z-index: 11; |
|||
} |
|||
// 历史对话弹出层 |
|||
.chat-popover { |
|||
position: absolute; |
|||
top: 56px; |
|||
background: #ffffff; |
|||
padding-bottom: 24px; |
|||
z-index: 2009; |
|||
} |
|||
.chat-popover-button { |
|||
z-index: 100; |
|||
position: absolute; |
|||
top: 18px; |
|||
right: 85px; |
|||
font-size: 20px; |
|||
} |
|||
.chat-popover-mask { |
|||
background-color: var(--el-overlay-color-lighter); |
|||
bottom: 0; |
|||
height: 100%; |
|||
left: 0; |
|||
overflow: auto; |
|||
position: absolute; |
|||
right: 0; |
|||
top: 56px; |
|||
z-index: 2008; |
|||
} |
|||
.gradient-divider { |
|||
position: relative; |
|||
text-align: center; |
|||
color: var(--el-color-info); |
|||
::before { |
|||
content: ''; |
|||
width: 17%; |
|||
height: 1px; |
|||
background: linear-gradient(90deg, rgba(222, 224, 227, 0) 0%, #dee0e3 100%); |
|||
position: absolute; |
|||
left: 16px; |
|||
top: 50%; |
|||
} |
|||
::after { |
|||
content: ''; |
|||
width: 17%; |
|||
height: 1px; |
|||
background: linear-gradient(90deg, #dee0e3 0%, rgba(222, 224, 227, 0) 100%); |
|||
position: absolute; |
|||
right: 16px; |
|||
top: 50%; |
|||
} |
|||
} |
|||
.AiChat-embed { |
|||
.ai-chat__operate { |
|||
padding-top: 12px; |
|||
} |
|||
} |
|||
.chat-width { |
|||
max-width: 860px; |
|||
margin: 0 auto; |
|||
} |
|||
} |
|||
.flex { |
|||
display: flex; |
|||
} |
|||
|
|||
.mr-12 { |
|||
margin-right: 12px; |
|||
|
|||
} |
|||
.ml-24 { |
|||
margin-left: 24px; |
|||
} |
|||
</style> |
|||
|
Loading…
Reference in new issue