Files
SQLBot/frontend/src/views/chat/index.vue
T

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>