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> |
<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> |
</template> |
||||
|
|
||||
<script setup> |
<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> |
</script> |
||||
<style scoped lang="scss"> |
|
||||
|
|
||||
|
<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> |
</style> |
||||
Loading…
Reference in new issue