mirror of
https://gh-proxy.org/https://github.com/klinsmaya/SQLBot.git
synced 2026-11-11 02:36:54 +08:00
1336 lines
37 KiB
Python
1336 lines
37 KiB
Python
<template>
|
|
<el-popover
|
|
v-if="assistantStore.assistant && !assistantStore.pageEmbedded && assistantStore.type != 4"
|
|
:width="280"
|
|
placement="bottom-start"
|
|
popper-class="popover-chat_history popover-chat_history_small"
|
|
>
|
|
<template #reference>
|
|
<el-icon class="show-history_icon" style="" size="20" @click="showFloatPopover">
|
|
<icon_sidebar_outlined></icon_sidebar_outlined>
|
|
</el-icon>
|
|
</template>
|
|
<ChatListContainer
|
|
ref="floatPopoverRef"
|
|
v-model:chat-list="chatList"
|
|
v-model:current-chat-id="currentChatId"
|
|
v-model:current-chat="currentChat"
|
|
v-model:loading="loading"
|
|
:in-popover="!chatListSideBarShow"
|
|
@go-empty="goEmpty"
|
|
@on-chat-created="onChatCreated"
|
|
@on-click-history="onClickHistory"
|
|
@on-chat-deleted="onChatDeleted"
|
|
@on-chat-renamed="onChatRenamed"
|
|
@on-click-side-bar-btn="hideSideBar"
|
|
/>
|
|
</el-popover>
|
|
<el-container class="chat-container no-padding">
|
|
<el-aside
|
|
v-if="(isCompletePage || pageEmbedded) && chatListSideBarShow"
|
|
class="chat-container-left"
|
|
>
|
|
<ChatListContainer
|
|
v-model:chat-list="chatList"
|
|
v-model:current-chat-id="currentChatId"
|
|
v-model:current-chat="currentChat"
|
|
v-model:loading="loading"
|
|
:in-popover="!chatListSideBarShow"
|
|
@go-empty="goEmpty"
|
|
@on-chat-created="onChatCreated"
|
|
@on-click-history="onClickHistory"
|
|
@on-chat-deleted="onChatDeleted"
|
|
@on-chat-renamed="onChatRenamed"
|
|
@on-click-side-bar-btn="hideSideBar"
|
|
/>
|
|
</el-aside>
|
|
|
|
<div
|
|
v-if="(!isCompletePage && !pageEmbedded) || !chatListSideBarShow"
|
|
class="hidden-sidebar-btn"
|
|
:class="{ 'assistant-popover-sidebar': !isCompletePage && !pageEmbedded }"
|
|
>
|
|
<el-popover
|
|
:width="280"
|
|
placement="bottom-start"
|
|
popper-class="popover-chat_history"
|
|
:popper-style="{ ...defaultFloatPopoverStyle }"
|
|
>
|
|
<template #reference>
|
|
<el-button link type="primary" class="icon-btn" @click="showSideBar">
|
|
<el-icon>
|
|
<icon_sidebar_outlined />
|
|
</el-icon>
|
|
</el-button>
|
|
</template>
|
|
<ChatListContainer
|
|
ref="floatPopoverRef"
|
|
v-model:chat-list="chatList"
|
|
v-model:current-chat-id="currentChatId"
|
|
v-model:current-chat="currentChat"
|
|
v-model:loading="loading"
|
|
:in-popover="!chatListSideBarShow"
|
|
@go-empty="goEmpty"
|
|
@on-chat-created="onChatCreated"
|
|
@on-click-history="onClickHistory"
|
|
@on-chat-deleted="onChatDeleted"
|
|
@on-chat-renamed="onChatRenamed"
|
|
@on-click-side-bar-btn="hideSideBar"
|
|
/>
|
|
</el-popover>
|
|
|
|
<el-drawer
|
|
v-model="floatPopoverVisible"
|
|
:with-header="false"
|
|
:modal="false"
|
|
direction="ltr"
|
|
size="278"
|
|
modal-class="assistant-popover_sidebar"
|
|
:before-close="hideSideBar"
|
|
>
|
|
<ChatListContainer
|
|
ref="floatPopoverRef"
|
|
v-model:chat-list="chatList"
|
|
v-model:current-chat-id="currentChatId"
|
|
v-model:current-chat="currentChat"
|
|
v-model:loading="loading"
|
|
:in-popover="false"
|
|
@go-empty="goEmpty"
|
|
@on-chat-created="onChatCreated"
|
|
@on-click-history="onClickHistory"
|
|
@on-chat-deleted="onChatDeleted"
|
|
@on-chat-renamed="onChatRenamed"
|
|
@on-click-side-bar-btn="hideSideBar"
|
|
/>
|
|
</el-drawer>
|
|
|
|
<el-tooltip effect="dark" :offset="8" :content="t('qa.new_chat')" placement="bottom">
|
|
<el-button link type="primary" class="icon-btn" @click="createNewChatSimple">
|
|
<el-icon>
|
|
<icon_new_chat_outlined />
|
|
</el-icon>
|
|
</el-button>
|
|
</el-tooltip>
|
|
</div>
|
|
<el-container :loading="loading">
|
|
<el-main
|
|
class="chat-record-list"
|
|
:class="{
|
|
'hide-sidebar': (isCompletePage || pageEmbedded) && !chatListSideBarShow,
|
|
'assistant-chat-main': !isCompletePage && !pageEmbedded,
|
|
}"
|
|
>
|
|
<div v-if="computedMessages.length == 0 && !loading" class="welcome-content-block">
|
|
<div class="welcome-content">
|
|
<template v-if="isCompletePage">
|
|
<div class="greeting">
|
|
<el-icon size="32">
|
|
<logo_fold />
|
|
</el-icon>
|
|
{{ t('qa.greeting') }}
|
|
</div>
|
|
<div class="sub">
|
|
{{ t('qa.hint_description') }}
|
|
</div>
|
|
</template>
|
|
|
|
<div v-else class="assistant-desc">
|
|
<img
|
|
v-if="logoAssistant"
|
|
:src="logoAssistant"
|
|
class="logo"
|
|
width="30px"
|
|
height="30px"
|
|
alt=""
|
|
/>
|
|
<el-icon v-else size="32">
|
|
<logo_fold />
|
|
</el-icon>
|
|
<div class="i-am">{{ welcome }}</div>
|
|
<div class="i-can">{{ welcomeDesc }}</div>
|
|
</div>
|
|
|
|
<el-button
|
|
v-if="isCompletePage && currentChatId === undefined"
|
|
size="large"
|
|
type="primary"
|
|
class="greeting-btn"
|
|
@click="createNewChatSimple"
|
|
>
|
|
<span class="inner-icon">
|
|
<el-icon>
|
|
<icon_new_chat_outlined />
|
|
</el-icon>
|
|
</span>
|
|
{{ t('qa.start_sqlbot') }}
|
|
</el-button>
|
|
</div>
|
|
</div>
|
|
<div v-else-if="computedMessages.length == 0 && loading" class="welcome-content-block">
|
|
<logo />
|
|
</div>
|
|
<el-scrollbar
|
|
v-if="computedMessages.length > 0"
|
|
ref="chatListRef"
|
|
class="no-horizontal"
|
|
@scroll="handleScroll"
|
|
>
|
|
<div
|
|
ref="innerRef"
|
|
class="chat-scroll"
|
|
:class="{
|
|
'no-sidebar': isCompletePage && !chatListSideBarShow,
|
|
pad16: !isCompletePage,
|
|
}"
|
|
>
|
|
<template v-for="(message, _index) in computedMessages" :key="_index">
|
|
<ChatRow :current-chat="currentChat" :msg="message" :hide-avatar="message.first_chat">
|
|
<RecommendQuestion
|
|
v-if="message.role === 'assistant' && message.first_chat"
|
|
ref="recommendQuestionRef"
|
|
:current-chat="currentChat"
|
|
:record-id="message.record?.id"
|
|
:questions="message.recommended_question"
|
|
:disabled="isTyping"
|
|
:first-chat="message.first_chat"
|
|
@click-question="quickAsk"
|
|
@stop="onChatStop"
|
|
@loadingOver="loadingOver"
|
|
/>
|
|
<UserChat v-if="message.role === 'user'" :message="message" />
|
|
<template v-if="message.role === 'assistant' && !message.first_chat">
|
|
<ChartAnswer
|
|
v-if="
|
|
(message?.record?.analysis_record_id === undefined ||
|
|
message?.record?.analysis_record_id === null) &&
|
|
(message?.record?.predict_record_id === undefined ||
|
|
message?.record?.predict_record_id === null)
|
|
"
|
|
ref="chartAnswerRef"
|
|
:chat-list="chatList"
|
|
:current-chat="currentChat"
|
|
:current-chat-id="currentChatId"
|
|
:loading="isTyping"
|
|
:message="message"
|
|
:reasoning-name="['sql_answer', 'chart_answer']"
|
|
@finish="onChartAnswerFinish"
|
|
@error="onChartAnswerError"
|
|
@stop="onChatStop"
|
|
>
|
|
<ErrorInfo :error="message.record?.error" class="error-container" />
|
|
<template #tool>
|
|
<ChatToolBar v-if="!message.isTyping" :message="message">
|
|
<div class="tool-btns">
|
|
<el-tooltip
|
|
effect="dark"
|
|
:offset="8"
|
|
:content="t('qa.ask_again')"
|
|
placement="top"
|
|
>
|
|
<el-button
|
|
class="tool-btn"
|
|
text
|
|
:disabled="isTyping"
|
|
@click="askAgain(message)"
|
|
>
|
|
<el-icon size="18">
|
|
<icon_replace_outlined />
|
|
</el-icon>
|
|
</el-button>
|
|
</el-tooltip>
|
|
<template v-if="message.record?.chart">
|
|
<div class="divider"></div>
|
|
<div>
|
|
<el-button
|
|
class="tool-btn"
|
|
text
|
|
:disabled="isTyping"
|
|
@click="clickAnalysis(message.record?.id)"
|
|
>
|
|
<span class="tool-btn-inner">
|
|
<el-icon size="18">
|
|
<icon_screen_outlined />
|
|
</el-icon>
|
|
<span class="btn-text">
|
|
{{ t('chat.data_analysis') }}
|
|
</span>
|
|
</span>
|
|
</el-button>
|
|
</div>
|
|
<div>
|
|
<el-button
|
|
class="tool-btn"
|
|
text
|
|
:disabled="isTyping"
|
|
@click="clickPredict(message.record?.id)"
|
|
>
|
|
<span class="tool-btn-inner">
|
|
<el-icon size="18">
|
|
<icon_start_outlined />
|
|
</el-icon>
|
|
<span class="btn-text">
|
|
{{ t('chat.data_predict') }}
|
|
</span>
|
|
</span>
|
|
</el-button>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</ChatToolBar>
|
|
</template>
|
|
<template #footer>
|
|
<RecommendQuestion
|
|
ref="recommendQuestionRef"
|
|
:current-chat="currentChat"
|
|
:record-id="message.record?.id"
|
|
:questions="message.recommended_question"
|
|
:first-chat="message.first_chat"
|
|
:disabled="isTyping"
|
|
@click-question="quickAsk"
|
|
@loadingOver="loadingOver"
|
|
@stop="onChatStop"
|
|
/>
|
|
</template>
|
|
</ChartAnswer>
|
|
<AnalysisAnswer
|
|
v-if="
|
|
message?.record?.analysis_record_id !== undefined &&
|
|
message?.record?.analysis_record_id !== null
|
|
"
|
|
ref="analysisAnswerRef"
|
|
:chat-list="chatList"
|
|
:current-chat="currentChat"
|
|
:current-chat-id="currentChatId"
|
|
:loading="isTyping"
|
|
:message="message"
|
|
@finish="onAnalysisAnswerFinish"
|
|
@error="onAnalysisAnswerError"
|
|
@stop="onChatStop"
|
|
>
|
|
<ErrorInfo :error="message.record?.error" class="error-container" />
|
|
<template #tool>
|
|
<ChatToolBar v-if="!message.isTyping" :message="message" />
|
|
</template>
|
|
</AnalysisAnswer>
|
|
<PredictAnswer
|
|
v-if="
|
|
message?.record?.predict_record_id !== undefined &&
|
|
message?.record?.predict_record_id !== null
|
|
"
|
|
ref="predictAnswerRef"
|
|
:chat-list="chatList"
|
|
:current-chat="currentChat"
|
|
:current-chat-id="currentChatId"
|
|
:loading="isTyping"
|
|
:message="message"
|
|
@finish="onPredictAnswerFinish"
|
|
@error="onPredictAnswerError"
|
|
@stop="onChatStop"
|
|
>
|
|
<ErrorInfo :error="message.record?.error" class="error-container" />
|
|
<template #tool>
|
|
<ChatToolBar v-if="!message.isTyping" :message="message" />
|
|
</template>
|
|
</PredictAnswer>
|
|
</template>
|
|
</ChatRow>
|
|
</template>
|
|
</div>
|
|
</el-scrollbar>
|
|
</el-main>
|
|
<el-footer v-if="computedMessages.length > 0 || !isCompletePage" class="chat-footer">
|
|
<div class="input-wrapper" @click="clickInput">
|
|
<div v-if="isCompletePage" class="datasource">
|
|
<template v-if="currentChat.datasource && currentChat.datasource_name">
|
|
{{ t('qa.selected_datasource') }}:
|
|
<img
|
|
v-if="currentChatEngineType"
|
|
style="margin-left: 4px; margin-right: 4px"
|
|
:src="currentChatEngineType"
|
|
width="16px"
|
|
height="16px"
|
|
alt=""
|
|
/>
|
|
<span class="name">
|
|
{{ currentChat.datasource_name }}
|
|
</span>
|
|
</template>
|
|
</div>
|
|
<el-input
|
|
ref="inputRef"
|
|
v-model="inputMessage"
|
|
:disabled="isTyping"
|
|
clearable
|
|
class="input-area"
|
|
:class="!isCompletePage && 'is-assistant'"
|
|
type="textarea"
|
|
:autosize="{ minRows: 1, maxRows: 8.583 }"
|
|
:placeholder="t('qa.question_placeholder')"
|
|
@keydown.enter.exact.prevent="($event: any) => sendMessage($event)"
|
|
@keydown.ctrl.enter.exact.prevent="handleCtrlEnter"
|
|
/>
|
|
|
|
<el-button
|
|
circle
|
|
type="primary"
|
|
class="input-icon"
|
|
:disabled="isTyping"
|
|
@click.stop="sendMessage"
|
|
>
|
|
<el-icon size="16">
|
|
<icon_send_filled />
|
|
</el-icon>
|
|
</el-button>
|
|
</div>
|
|
</el-footer>
|
|
</el-container>
|
|
|
|
<ChatCreator v-if="isCompletePage" ref="chatCreatorRef" @on-chat-created="onChatCreatedQuick" />
|
|
<ChatCreator ref="hiddenChatCreatorRef" hidden @on-chat-created="onChatCreatedQuick" />
|
|
</el-container>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, nextTick, onMounted, ref } from 'vue'
|
|
import { Chat, chatApi, ChatInfo, type ChatMessage, ChatRecord } from '@/api/chat'
|
|
import ChatRow from './ChatRow.vue'
|
|
import ChartAnswer from './answer/ChartAnswer.vue'
|
|
import AnalysisAnswer from './answer/AnalysisAnswer.vue'
|
|
import PredictAnswer from './answer/PredictAnswer.vue'
|
|
import UserChat from './chat-block/UserChat.vue'
|
|
import RecommendQuestion from './RecommendQuestion.vue'
|
|
import ChatListContainer from './ChatListContainer.vue'
|
|
import ChatCreator from '@/views/chat/ChatCreator.vue'
|
|
import ErrorInfo from './ErrorInfo.vue'
|
|
import ChatToolBar from './ChatToolBar.vue'
|
|
import { dsTypeWithImg } from '@/views/ds/js/ds-type'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { find, forEach } from 'lodash-es'
|
|
import icon_new_chat_outlined from '@/assets/svg/icon_new_chat_outlined.svg'
|
|
import icon_sidebar_outlined from '@/assets/svg/icon_sidebar_outlined.svg'
|
|
import icon_replace_outlined from '@/assets/svg/icon_replace_outlined.svg'
|
|
import icon_screen_outlined from '@/assets/svg/icon_screen_outlined.svg'
|
|
import icon_start_outlined from '@/assets/svg/icon_start_outlined.svg'
|
|
import logo_fold from '@/assets/svg/logo-custom_small.svg'
|
|
import logo from '@/assets/LOGO.svg'
|
|
import icon_send_filled from '@/assets/svg/icon_send_filled.svg'
|
|
import { useAssistantStore } from '@/stores/assistant'
|
|
import { onClickOutside } from '@vueuse/core'
|
|
import { useUserStore } from '@/stores/user'
|
|
import router from '@/router'
|
|
const userStore = useUserStore()
|
|
const props = defineProps<{
|
|
startChatDsId?: number
|
|
welcomeDesc?: string
|
|
logoAssistant?: string
|
|
welcome?: string
|
|
pageEmbedded?: boolean
|
|
}>()
|
|
const floatPopoverRef = ref()
|
|
const floatPopoverVisible = ref(false)
|
|
const assistantStore = useAssistantStore()
|
|
const defaultFloatPopoverStyle = ref({
|
|
padding: '0',
|
|
height: '654px',
|
|
border: '1px solid rgba(222, 224, 227, 1)',
|
|
borderRadius: '6px',
|
|
})
|
|
|
|
const isCompletePage = computed(() => !assistantStore.getAssistant || assistantStore.getEmbedded)
|
|
|
|
const { t } = useI18n()
|
|
|
|
const inputMessage = ref('')
|
|
|
|
const chatListRef = ref()
|
|
const innerRef = ref()
|
|
const chatCreatorRef = ref()
|
|
|
|
function scrollToBottom() {
|
|
nextTick(() => {
|
|
chatListRef.value?.scrollTo({
|
|
top: chatListRef.value.wrapRef.scrollHeight,
|
|
behavior: 'smooth',
|
|
})
|
|
})
|
|
}
|
|
|
|
const loading = ref<boolean>(false)
|
|
const chatList = ref<Array<ChatInfo>>([])
|
|
|
|
const currentChatId = ref<number | undefined>()
|
|
const currentChat = ref<ChatInfo>(new ChatInfo())
|
|
const isTyping = ref<boolean>(false)
|
|
|
|
const computedMessages = computed<Array<ChatMessage>>(() => {
|
|
const messages: Array<ChatMessage> = []
|
|
if (currentChatId.value === undefined) {
|
|
return messages
|
|
}
|
|
for (let i = 0; i < currentChat.value.records.length; i++) {
|
|
const record = currentChat.value.records[i]
|
|
if (record.question !== undefined && !record.first_chat) {
|
|
messages.push({
|
|
role: 'user',
|
|
create_time: record.create_time,
|
|
record: record,
|
|
content: record.question,
|
|
index: i,
|
|
})
|
|
}
|
|
messages.push({
|
|
role: 'assistant',
|
|
create_time: record.create_time,
|
|
record: record,
|
|
isTyping: i === currentChat.value.records.length - 1 && isTyping.value,
|
|
first_chat: record.first_chat,
|
|
recommended_question: record.recommended_question,
|
|
index: i,
|
|
})
|
|
}
|
|
|
|
return messages
|
|
})
|
|
|
|
const goEmpty = (func?: (...p: any[]) => void, ...param: any[]) => {
|
|
inputMessage.value = ''
|
|
stop(func, ...param)
|
|
}
|
|
|
|
let scrollTime: any
|
|
let scrollingTime: any
|
|
let scrollTopVal = 0
|
|
let scrolling = false
|
|
const scrollBottom = () => {
|
|
if (scrolling) return
|
|
if (!isTyping.value && !getRecommendQuestionsLoading.value) {
|
|
clearInterval(scrollTime)
|
|
}
|
|
chatListRef.value!.setScrollTop(innerRef.value!.clientHeight)
|
|
}
|
|
|
|
const handleScroll = (val: any) => {
|
|
if (!isCompletePage.value) return
|
|
scrollTopVal = val.scrollTop
|
|
scrolling = true
|
|
clearTimeout(scrollingTime)
|
|
scrollingTime = setTimeout(() => {
|
|
scrolling = false
|
|
}, 400)
|
|
if (
|
|
scrollTopVal + 200 <
|
|
innerRef.value!.clientHeight - (document.querySelector('.chat-record-list')!.clientHeight - 20)
|
|
) {
|
|
clearInterval(scrollTime)
|
|
scrollTime = null
|
|
return
|
|
}
|
|
|
|
if (
|
|
!scrollTime &&
|
|
isTyping.value &&
|
|
scrollTopVal + 30 <
|
|
innerRef.value!.clientHeight -
|
|
(document.querySelector('.chat-record-list')!.clientHeight - 20)
|
|
) {
|
|
scrollTime = setInterval(() => {
|
|
scrollBottom()
|
|
}, 300)
|
|
}
|
|
}
|
|
|
|
const createNewChatSimple = async () => {
|
|
currentChat.value = new ChatInfo()
|
|
currentChatId.value = undefined
|
|
await createNewChat()
|
|
}
|
|
|
|
const createNewChat = async () => {
|
|
try {
|
|
await chatApi.checkLLMModel()
|
|
} catch (error: any) {
|
|
console.error(error)
|
|
let errorMsg = t('model.default_miss')
|
|
let confirm_text = t('datasource.got_it')
|
|
if (userStore.isAdmin) {
|
|
errorMsg = t('model.default_miss_admin')
|
|
confirm_text = t('model.to_config')
|
|
}
|
|
ElMessageBox.confirm(t('qa.ask_failed'), {
|
|
confirmButtonType: 'primary',
|
|
tip: errorMsg,
|
|
showCancelButton: userStore.isAdmin,
|
|
confirmButtonText: confirm_text,
|
|
cancelButtonText: t('common.cancel'),
|
|
customClass: 'confirm-no_icon',
|
|
autofocus: false,
|
|
showClose: false,
|
|
callback: (val: string) => {
|
|
if (userStore.isAdmin && val === 'confirm') {
|
|
router.push('/system/model')
|
|
}
|
|
},
|
|
})
|
|
return
|
|
}
|
|
goEmpty()
|
|
if (!isCompletePage.value) {
|
|
currentChat.value = new ChatInfo()
|
|
currentChatId.value = undefined
|
|
return
|
|
}
|
|
chatCreatorRef.value?.showDs()
|
|
}
|
|
|
|
function getChatList(callback?: () => void) {
|
|
loading.value = true
|
|
chatApi
|
|
.list()
|
|
.then((res) => {
|
|
chatList.value = chatApi.toChatInfoList(res)
|
|
})
|
|
.finally(() => {
|
|
loading.value = false
|
|
if (callback && typeof callback === 'function') {
|
|
callback()
|
|
}
|
|
})
|
|
}
|
|
|
|
function onClickHistory(chat: ChatInfo) {
|
|
scrollToBottom()
|
|
forEach(chat?.records, (record: ChatRecord) => {
|
|
// getChatData(record.id)
|
|
if (record.predict_record_id) {
|
|
// getChatPredictData(record.id)
|
|
}
|
|
})
|
|
}
|
|
|
|
const currentChatEngineType = computed(() => {
|
|
return (dsTypeWithImg.find((ele) => currentChat.value.ds_type === ele.type) || {}).img
|
|
})
|
|
|
|
function onChatDeleted(id: number) {
|
|
console.info('deleted', id)
|
|
}
|
|
|
|
function onChatRenamed(chat: Chat) {
|
|
console.info('renamed', chat)
|
|
}
|
|
|
|
const chatListSideBarShow = ref<boolean>(true)
|
|
function hideSideBar() {
|
|
if (!isCompletePage.value && !props.pageEmbedded) {
|
|
floatPopoverVisible.value = false
|
|
return
|
|
}
|
|
chatListSideBarShow.value = false
|
|
}
|
|
|
|
function showSideBar() {
|
|
chatListSideBarShow.value = true
|
|
}
|
|
|
|
function onChatCreatedQuick(chat: ChatInfo) {
|
|
chatList.value.unshift(chat)
|
|
currentChatId.value = chat.id
|
|
currentChat.value = chat
|
|
onChatCreated(chat)
|
|
}
|
|
|
|
function onChatCreated(chat: ChatInfo) {
|
|
if (chat.records.length === 1) {
|
|
getRecommendQuestions(chat.records[0].id)
|
|
}
|
|
}
|
|
|
|
const recommendQuestionRef = ref()
|
|
|
|
function getRecommendQuestions(id?: number) {
|
|
nextTick(() => {
|
|
if (recommendQuestionRef.value) {
|
|
if (recommendQuestionRef.value instanceof Array) {
|
|
for (let i = 0; i < recommendQuestionRef.value.length; i++) {
|
|
const _id = recommendQuestionRef.value[i].id()
|
|
if (_id === id) {
|
|
recommendQuestionRef.value[i].getRecommendQuestions()
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
recommendQuestionRef.value.getRecommendQuestions()
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
function quickAsk(question: string) {
|
|
inputMessage.value = question
|
|
nextTick(() => {
|
|
sendMessage()
|
|
})
|
|
}
|
|
|
|
const chartAnswerRef = ref()
|
|
const getRecommendQuestionsLoading = ref(false)
|
|
async function onChartAnswerFinish(id: number) {
|
|
getRecommendQuestionsLoading.value = true
|
|
loading.value = false
|
|
isTyping.value = false
|
|
getRecommendQuestions(id)
|
|
}
|
|
|
|
const loadingOver = () => {
|
|
getRecommendQuestionsLoading.value = false
|
|
}
|
|
|
|
function onChartAnswerError() {
|
|
loading.value = false
|
|
isTyping.value = false
|
|
console.debug('onChartAnswerError')
|
|
}
|
|
|
|
function onChatStop() {
|
|
loading.value = false
|
|
isTyping.value = false
|
|
console.debug('onChatStop')
|
|
}
|
|
const assistantPrepareSend = async () => {
|
|
if (
|
|
!isCompletePage.value &&
|
|
(currentChatId.value == null || typeof currentChatId.value == 'undefined')
|
|
) {
|
|
const assistantChat = await assistantStore.setChat()
|
|
if (assistantChat) {
|
|
onChatCreatedQuick(assistantChat as any)
|
|
}
|
|
}
|
|
}
|
|
const sendMessage = async ($event: any = {}) => {
|
|
if ($event?.isComposing) {
|
|
return
|
|
}
|
|
if (!inputMessage.value.trim()) return
|
|
|
|
loading.value = true
|
|
isTyping.value = true
|
|
if (isCompletePage.value) {
|
|
scrollTopVal = innerRef.value!.clientHeight
|
|
scrollTime = setInterval(() => {
|
|
scrollBottom()
|
|
}, 300)
|
|
}
|
|
await assistantPrepareSend()
|
|
const currentRecord = new ChatRecord()
|
|
currentRecord.create_time = new Date()
|
|
currentRecord.chat_id = currentChatId.value
|
|
currentRecord.question = inputMessage.value
|
|
currentRecord.sql_answer = ''
|
|
currentRecord.sql = ''
|
|
currentRecord.chart_answer = ''
|
|
currentRecord.chart = ''
|
|
|
|
currentChat.value.records.push(currentRecord)
|
|
inputMessage.value = ''
|
|
|
|
nextTick(async () => {
|
|
const index = currentChat.value.records.length - 1
|
|
if (chartAnswerRef.value) {
|
|
if (chartAnswerRef.value instanceof Array) {
|
|
for (let i = 0; i < chartAnswerRef.value.length; i++) {
|
|
const _index = chartAnswerRef.value[i].index()
|
|
if (index === _index) {
|
|
await chartAnswerRef.value[i].sendMessage()
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
await chartAnswerRef.value.sendMessage()
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
const analysisAnswerRef = ref()
|
|
|
|
async function onAnalysisAnswerFinish(id: number) {
|
|
loading.value = false
|
|
isTyping.value = false
|
|
console.debug(id)
|
|
//await getRecommendQuestions(id)
|
|
}
|
|
function onAnalysisAnswerError() {
|
|
loading.value = false
|
|
isTyping.value = false
|
|
}
|
|
|
|
function askAgain(message: ChatMessage) {
|
|
inputMessage.value = message.record?.question ?? ''
|
|
nextTick(() => {
|
|
sendMessage()
|
|
})
|
|
}
|
|
|
|
async function clickAnalysis(id?: number) {
|
|
const baseRecord = find(currentChat.value.records, (value) => id === value.id)
|
|
if (baseRecord == undefined) {
|
|
return
|
|
}
|
|
|
|
loading.value = true
|
|
isTyping.value = true
|
|
|
|
const currentRecord = new ChatRecord()
|
|
currentRecord.create_time = new Date()
|
|
currentRecord.chat_id = baseRecord.chat_id
|
|
currentRecord.question = baseRecord.question
|
|
currentRecord.chart = baseRecord.chart
|
|
currentRecord.data = baseRecord.data
|
|
currentRecord.analysis_record_id = id
|
|
currentRecord.analysis = ''
|
|
|
|
currentChat.value.records.push(currentRecord)
|
|
|
|
nextTick(async () => {
|
|
const index = currentChat.value.records.length - 1
|
|
if (analysisAnswerRef.value) {
|
|
if (analysisAnswerRef.value instanceof Array) {
|
|
for (let i = 0; i < analysisAnswerRef.value.length; i++) {
|
|
const _index = analysisAnswerRef.value[i].index()
|
|
if (index === _index) {
|
|
await analysisAnswerRef.value[i].sendMessage()
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
await analysisAnswerRef.value.sendMessage()
|
|
}
|
|
}
|
|
})
|
|
|
|
return
|
|
}
|
|
|
|
const predictAnswerRef = ref()
|
|
|
|
async function onPredictAnswerFinish(id: number) {
|
|
loading.value = false
|
|
isTyping.value = false
|
|
console.debug('onPredictAnswerFinish: ', id)
|
|
//await getRecommendQuestions(id)
|
|
}
|
|
function onPredictAnswerError() {
|
|
loading.value = false
|
|
isTyping.value = false
|
|
}
|
|
|
|
async function clickPredict(id?: number) {
|
|
const baseRecord = find(currentChat.value.records, (value) => id === value.id)
|
|
if (baseRecord == undefined) {
|
|
return
|
|
}
|
|
|
|
loading.value = true
|
|
isTyping.value = true
|
|
|
|
const currentRecord = new ChatRecord()
|
|
currentRecord.create_time = new Date()
|
|
currentRecord.chat_id = baseRecord.chat_id
|
|
currentRecord.question = baseRecord.question
|
|
currentRecord.chart = baseRecord.chart
|
|
currentRecord.data = baseRecord.data
|
|
currentRecord.predict_record_id = id
|
|
currentRecord.predict = ''
|
|
currentRecord.predict_data = ''
|
|
|
|
currentChat.value.records.push(currentRecord)
|
|
|
|
nextTick(async () => {
|
|
const index = currentChat.value.records.length - 1
|
|
if (predictAnswerRef.value) {
|
|
if (predictAnswerRef.value instanceof Array) {
|
|
for (let i = 0; i < predictAnswerRef.value.length; i++) {
|
|
const _index = predictAnswerRef.value[i].index()
|
|
if (index === _index) {
|
|
await predictAnswerRef.value[i].sendMessage()
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
await predictAnswerRef.value.sendMessage()
|
|
}
|
|
}
|
|
})
|
|
|
|
return
|
|
}
|
|
|
|
const handleCtrlEnter = (e: KeyboardEvent) => {
|
|
const textarea = e.target as HTMLTextAreaElement
|
|
const start = textarea.selectionStart
|
|
const end = textarea.selectionEnd
|
|
const value = textarea.value
|
|
|
|
inputMessage.value = value.substring(0, start) + '\n' + value.substring(end)
|
|
|
|
nextTick(() => {
|
|
textarea.selectionStart = textarea.selectionEnd = start + 1
|
|
})
|
|
}
|
|
|
|
const inputRef = ref()
|
|
|
|
function clickInput() {
|
|
inputRef.value?.focus()
|
|
}
|
|
|
|
function stop(func?: (...p: any[]) => void, ...param: any[]) {
|
|
if (recommendQuestionRef.value) {
|
|
if (recommendQuestionRef.value instanceof Array) {
|
|
for (let i = 0; i < recommendQuestionRef.value.length; i++) {
|
|
recommendQuestionRef.value[i].stop()
|
|
}
|
|
} else {
|
|
recommendQuestionRef.value.stop()
|
|
}
|
|
}
|
|
if (chartAnswerRef.value) {
|
|
if (chartAnswerRef.value instanceof Array) {
|
|
for (let i = 0; i < chartAnswerRef.value.length; i++) {
|
|
chartAnswerRef.value[i].stop()
|
|
}
|
|
} else {
|
|
chartAnswerRef.value.stop()
|
|
}
|
|
}
|
|
if (analysisAnswerRef.value) {
|
|
if (analysisAnswerRef.value instanceof Array) {
|
|
for (let i = 0; i < analysisAnswerRef.value.length; i++) {
|
|
analysisAnswerRef.value[i].stop()
|
|
}
|
|
} else {
|
|
analysisAnswerRef.value.stop()
|
|
}
|
|
}
|
|
if (predictAnswerRef.value) {
|
|
if (predictAnswerRef.value instanceof Array) {
|
|
for (let i = 0; i < predictAnswerRef.value.length; i++) {
|
|
predictAnswerRef.value[i].stop()
|
|
}
|
|
} else {
|
|
predictAnswerRef.value.stop()
|
|
}
|
|
}
|
|
if (func && typeof func === 'function') {
|
|
func(...param)
|
|
}
|
|
}
|
|
const showFloatPopover = () => {
|
|
if (!isCompletePage.value && !floatPopoverVisible.value) {
|
|
floatPopoverVisible.value = true
|
|
}
|
|
}
|
|
const assistantPrepareInit = () => {
|
|
if (isCompletePage.value || props.pageEmbedded) {
|
|
return
|
|
}
|
|
Object.assign(defaultFloatPopoverStyle.value, {
|
|
height: '100% !important',
|
|
inset: '0px auto auto 0px',
|
|
})
|
|
goEmpty()
|
|
onClickOutside(floatPopoverRef, (event: any) => {
|
|
if (floatPopoverVisible.value) {
|
|
let parentElement: any = event.target
|
|
let isEdOverlay = false
|
|
while (parentElement) {
|
|
if (parentElement.className.includes('ed-overlay')) {
|
|
isEdOverlay = true
|
|
break
|
|
} else {
|
|
parentElement = parentElement.parentElement
|
|
}
|
|
}
|
|
if (isEdOverlay) return
|
|
floatPopoverVisible.value = false
|
|
}
|
|
})
|
|
}
|
|
defineExpose({
|
|
createNewChat,
|
|
showFloatPopover,
|
|
})
|
|
|
|
const hiddenChatCreatorRef = ref()
|
|
|
|
function jumpCreatChat() {
|
|
if (props.startChatDsId) {
|
|
const _id = props.startChatDsId
|
|
nextTick(() => {
|
|
hiddenChatCreatorRef.value?.createChat(_id)
|
|
})
|
|
const newUrl = window.location.hash.replace(/\?.*$/, '')
|
|
history.replaceState({}, '', newUrl)
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
getChatList(jumpCreatChat)
|
|
assistantPrepareInit()
|
|
})
|
|
</script>
|
|
|
|
<style lang="less" scoped>
|
|
.chat-container {
|
|
height: 100%;
|
|
position: relative;
|
|
|
|
border-radius: 12px;
|
|
.assistant-popover-sidebar {
|
|
button {
|
|
display: none;
|
|
}
|
|
}
|
|
.hidden-sidebar-btn {
|
|
z-index: 1;
|
|
position: absolute;
|
|
padding: 16px;
|
|
top: 0;
|
|
left: 0;
|
|
}
|
|
|
|
.icon-btn {
|
|
min-width: unset;
|
|
width: 26px;
|
|
height: 26px;
|
|
font-size: 18px;
|
|
|
|
--ed-button-text-color: rgba(31, 35, 41, 1);
|
|
--ed-button-hover-text-color: var(--ed-button-text-color);
|
|
--ed-button-active-text-color: var(--ed-button-text-color);
|
|
--ed-button-hover-link-text-color: var(--ed-button-text-color);
|
|
&:hover {
|
|
background: rgba(31, 35, 41, 0.1);
|
|
}
|
|
}
|
|
|
|
.chat-container-left {
|
|
--ed-aside-width: 280px;
|
|
border-radius: 12px 0 0 12px;
|
|
|
|
background: rgba(245, 246, 247, 1);
|
|
}
|
|
|
|
:deep(.chat-record-list) {
|
|
padding: 0 0 20px 0;
|
|
border-radius: 0 12px 12px 0;
|
|
|
|
.no-horizontal.ed-scrollbar {
|
|
.ed-scrollbar__bar.is-horizontal {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
&.hide-sidebar {
|
|
border-radius: 12px;
|
|
}
|
|
}
|
|
.assistant-chat-main {
|
|
padding: 0 0 20px 0;
|
|
}
|
|
|
|
.chat-scroll {
|
|
width: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding-left: 56px;
|
|
padding-right: 56px;
|
|
|
|
&.no-sidebar {
|
|
padding-left: 96px;
|
|
}
|
|
|
|
&.pad16 {
|
|
padding-left: 16px;
|
|
padding-right: 16px;
|
|
}
|
|
}
|
|
|
|
.chat-footer {
|
|
min-height: calc(120px + 16px);
|
|
max-height: calc(300px + 16px);
|
|
height: fit-content;
|
|
|
|
padding-bottom: 16px;
|
|
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
|
|
.input-wrapper {
|
|
width: 100%;
|
|
position: relative;
|
|
max-width: 800px;
|
|
|
|
.datasource {
|
|
width: calc(100% - 2px);
|
|
position: absolute;
|
|
margin-left: 1px;
|
|
margin-top: 1px;
|
|
left: 0;
|
|
top: 0;
|
|
padding-top: 12px;
|
|
padding-left: 12px;
|
|
z-index: 10;
|
|
background: transparent;
|
|
line-height: 22px;
|
|
font-size: 14px;
|
|
font-weight: 400;
|
|
border-top-right-radius: 16px;
|
|
border-top-left-radius: 16px;
|
|
color: rgba(100, 106, 115, 1);
|
|
display: flex;
|
|
align-items: center;
|
|
|
|
.name {
|
|
color: rgba(31, 35, 41, 1);
|
|
}
|
|
}
|
|
|
|
.input-area {
|
|
border-color: #d9dcdf;
|
|
|
|
:deep(.ed-textarea__inner) {
|
|
padding: 42px 12px 52px 12px;
|
|
background: #f8f9fa;
|
|
border-radius: 16px;
|
|
line-height: 24px;
|
|
}
|
|
|
|
&.is-assistant {
|
|
:deep(.ed-textarea__inner) {
|
|
padding: 12px 12px 52px 12px;
|
|
font-weight: 400;
|
|
font-size: 16px;
|
|
line-height: 24px;
|
|
border-radius: 16px;
|
|
|
|
&::placeholder {
|
|
color: #8f959e;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.input-icon {
|
|
min-width: unset;
|
|
position: absolute;
|
|
bottom: 12px;
|
|
right: 12px;
|
|
|
|
border-color: unset;
|
|
|
|
&.is-disabled {
|
|
background: rgba(187, 191, 196, 1);
|
|
border-color: unset;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.send-btn {
|
|
min-width: 0;
|
|
}
|
|
}
|
|
|
|
.error-container {
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.tool-btns {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
|
|
column-gap: 16px;
|
|
row-gap: 8px;
|
|
|
|
.tool-btn {
|
|
font-size: 14px;
|
|
font-weight: 400;
|
|
line-height: 22px;
|
|
color: rgba(100, 106, 115, 1);
|
|
|
|
.tool-btn-inner {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
}
|
|
|
|
&:hover {
|
|
background: rgba(31, 35, 41, 0.1);
|
|
}
|
|
&:active {
|
|
background: rgba(31, 35, 41, 0.1);
|
|
}
|
|
}
|
|
|
|
.btn-text {
|
|
margin-left: 4px;
|
|
}
|
|
|
|
.divider {
|
|
width: 1px;
|
|
height: 16px;
|
|
border-left: 1px solid rgba(31, 35, 41, 0.15);
|
|
}
|
|
}
|
|
|
|
.welcome-content-block {
|
|
height: 100%;
|
|
width: 100%;
|
|
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
|
|
.welcome-content {
|
|
width: 100%;
|
|
max-width: 800px;
|
|
display: flex;
|
|
gap: 16px;
|
|
align-items: center;
|
|
flex-direction: column;
|
|
|
|
.assistant-desc {
|
|
width: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
flex-direction: column;
|
|
|
|
.i-am {
|
|
font-weight: 600;
|
|
font-size: 24px;
|
|
line-height: 32px;
|
|
margin: 16px 0;
|
|
max-width: 100%;
|
|
word-break: break-all;
|
|
padding: 0 20px;
|
|
}
|
|
|
|
.i-can {
|
|
margin-bottom: 4px;
|
|
max-width: 350px;
|
|
text-align: center;
|
|
font-weight: 400;
|
|
font-size: 14px;
|
|
line-height: 24px;
|
|
color: #646a73;
|
|
max-width: 88%;
|
|
word-break: break-all;
|
|
padding: 0 20px;
|
|
}
|
|
}
|
|
|
|
.greeting {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
line-height: 32px;
|
|
font-size: 24px;
|
|
font-weight: 600;
|
|
color: rgba(31, 35, 41, 1);
|
|
}
|
|
|
|
.sub {
|
|
color: grey;
|
|
font-size: 16px;
|
|
line-height: 24px;
|
|
}
|
|
|
|
.greeting-btn {
|
|
width: 100%;
|
|
height: 88px;
|
|
border-radius: 16px;
|
|
border-style: dashed;
|
|
|
|
.inner-icon {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
|
|
margin-right: 6px;
|
|
}
|
|
|
|
font-size: 16px;
|
|
line-height: 24px;
|
|
font-weight: 500;
|
|
|
|
--ed-button-text-color: var(--ed-color-primary, rgba(28, 186, 144, 1));
|
|
--ed-button-hover-text-color: var(--ed-color-primary, rgba(28, 186, 144, 1));
|
|
--ed-button-active-text-color: var(--ed-color-primary, rgba(28, 186, 144, 1));
|
|
--ed-button-bg-color: rgba(248, 249, 250, 1);
|
|
--ed-button-hover-bg-color: var(--ed-color-primary-1a, #1cba901a);
|
|
--ed-button-border-color: rgba(217, 220, 223, 1);
|
|
--ed-button-hover-border-color: var(--ed-color-primary, rgba(28, 186, 144, 1));
|
|
--ed-button-active-bg-color: var(--ed-color-primary-33, #1cba9033);
|
|
--ed-button-active-border-color: var(--ed-color-primary, rgba(28, 186, 144, 1));
|
|
}
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<style lang="less">
|
|
.assistant-popover_sidebar {
|
|
.ed-drawer {
|
|
height: 100% !important;
|
|
margin-top: 0 !important;
|
|
}
|
|
.ed-drawer__body {
|
|
padding: 0;
|
|
}
|
|
}
|
|
|
|
.popover-chat_history {
|
|
box-shadow: 0px 4px 8px 0px #1f23291a !important;
|
|
border-radius: 12px !important;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.popover-chat_history_small {
|
|
height: calc(100% - 54px);
|
|
padding: 0 !important;
|
|
border: 1px solid rgba(222, 224, 227, 1);
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.show-history_icon {
|
|
cursor: pointer;
|
|
position: absolute;
|
|
top: 18px;
|
|
left: 16px;
|
|
z-index: 199;
|
|
&::after {
|
|
content: '';
|
|
background-color: #1f23291a;
|
|
position: absolute;
|
|
border-radius: 6px;
|
|
width: 28px;
|
|
height: 28px;
|
|
transform: translate(-50%, -50%);
|
|
top: 50%;
|
|
left: 50%;
|
|
display: none;
|
|
}
|
|
|
|
&:hover {
|
|
&::after {
|
|
display: block;
|
|
}
|
|
}
|
|
}
|
|
</style>
|