go-proxy-bingai/frontend/src/views/chat/components/Chat/Chat.vue

315 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<script setup lang="ts">
import { onMounted, ref, computed } from 'vue';
import { NEmpty, NButton, useMessage, NResult, NInput } from 'naive-ui';
import conversationCssText from '@/assets/css/conversation.css?raw';
import { usePromptStore, type IPrompt } from '@/stores/modules/prompt';
import { storeToRefs } from 'pinia';
import VirtualList from 'vue3-virtual-scroll-list';
import ChatPromptItem from './ChatPromptItem.vue';
import { isMobile } from '@/utils/utils';
import LoadingSpinner from '@/components/LoadingSpinner/LoadingSpinner.vue';
import { ApiResultCode } from '@/api/model/ApiResult';
import type { SysConfig } from '@/api/model/sysconf/SysConfig';
import { useChatStore } from '@/stores/modules/chat';
import ChatServiceSelect from '@/components/ChatServiceSelect/ChatServiceSelect.vue';
import { useUserStore } from '@/stores/modules/user';
const message = useMessage();
const isShowLoading = ref(true);
const promptStore = usePromptStore();
const { isShowPromptSotre, isShowChatPrompt, keyword, promptList, searchPromptList, selectedPromptIndex } = storeToRefs(promptStore);
const chatStore = useChatStore();
const { isShowChatServiceSelectModal, sydneyConfigs, selectedSydneyBaseUrl } = storeToRefs(chatStore);
const userStore = useUserStore();
const scrollbarRef = ref<{
scrollToIndex: (index: number) => {};
getOffset: () => number;
getClientSize: () => number;
getScrollSize: () => number;
}>();
const isInput = ref(false);
const isPromptScrolling = ref(false);
const promptItemHeight = 130;
const isShowUnauthorizedModal = ref(false);
const authKey = ref('');
const isAuthBtnLoading = ref(false);
const isShowHistory = computed(() => {
return (CIB.vm.isMobile && CIB.vm.sidePanel.isVisibleMobile) || (!CIB.vm.isMobile && CIB.vm.sidePanel.isVisibleDesktop);
});
onMounted(async () => {
await initChat();
// CIB.vm.isMobile = isMobile();
initSysConfig();
// show conversion
SydneyFullScreenConv.initWithWaitlistUpdate({ cookLoc: {} }, 10);
initChatService();
isShowLoading.value = false;
hackStyle();
initChatPrompt();
});
const initChatService = () => {
if (selectedSydneyBaseUrl.value) {
CIB.config.sydney.baseUrl = selectedSydneyBaseUrl.value;
isShowChatServiceSelectModal.value = false;
} else {
isShowChatServiceSelectModal.value = true;
selectedSydneyBaseUrl.value = CIB.config.sydney.baseUrl;
const isCus = sydneyConfigs.value.filter((x) => !x.isCus).every((x) => x.baseUrl !== selectedSydneyBaseUrl.value);
if (isCus) {
const cusSydneyConfig = sydneyConfigs.value.find((x) => x.isCus);
if (cusSydneyConfig) {
cusSydneyConfig.baseUrl = selectedSydneyBaseUrl.value;
}
}
chatStore.checkAllSydneyConfig();
}
};
const initSysConfig = async () => {
const res = await userStore.getSysConfig();
switch (res.code) {
case ApiResultCode.OK:
{
if (!res.data.isAuth) {
isShowUnauthorizedModal.value = true;
return;
}
afterAuth(res.data);
}
break;
default:
message.error(`[${res.code}] ${res.message}`);
break;
}
};
const afterAuth = (data: SysConfig) => {
if (!data.isSysCK) {
userStore.checkUserToken();
}
};
const initChat = async () => {
return new Promise((resolve, reject) => {
sj_evt.bind('sydFSC.init', resolve, true);
sj_evt.fire('showSydFSC');
});
};
const hackStyle = () => {
if (location.hostname === 'localhost') {
CIB.config.sydney.hostnamesToBypassSecureConnection = CIB.config.sydney.hostnamesToBypassSecureConnection.filter((x) => x !== location.hostname);
}
const serpEle = document.querySelector('cib-serp');
// 居中
serpEle?.setAttribute('alignment', 'center');
const conversationEle = serpEle?.shadowRoot?.querySelector('cib-conversation') as HTMLElement;
// todo 反馈暂时无法使用,先移除
const welcomeEle = conversationEle?.shadowRoot?.querySelector('cib-welcome-container');
welcomeEle?.shadowRoot?.querySelector('.learn-tog-item')?.remove();
serpEle?.shadowRoot?.querySelector('cib-serp-feedback')?.remove();
if (isMobile()) {
welcomeEle?.shadowRoot?.querySelector('.container-item')?.remove();
CIB.vm.actionBar.input.placeholder = '有问题尽管问我..."/" 触发提示词)';
}
// 加入css
const conversationStyleEle = document.createElement('style');
conversationStyleEle.innerText = conversationCssText;
conversationEle.shadowRoot?.append(conversationStyleEle);
};
interface IActionBarElement extends HTMLElement {
handleInputTextKey: (ev: KeyboardEvent) => void;
}
const initChatPrompt = () => {
const actionBarEle = document.querySelector('#b_sydConvCont > cib-serp')?.shadowRoot?.querySelector('#cib-action-bar-main') as IActionBarElement;
const oldHandleInputTextKey = actionBarEle.handleInputTextKey;
actionBarEle.handleInputTextKey = function (ev: KeyboardEvent) {
// 有提示词时,优先选择提示词
if (ev.key === 'Enter' && isShowChatPrompt.value) {
return;
}
return oldHandleInputTextKey.apply(this, [ev]);
};
CIB.vm.actionBar.input.addEventListener('compositionstart', handleInputStart);
CIB.vm.actionBar.input.addEventListener('compositionend', handleInputEnd);
CIB.vm.actionBar.input.addEventListener('change', handleInputTextChanged);
CIB.vm.actionBar.input.addEventListener('input', handleInputTextChanged);
// CIB.vm.actionBar.input.addEventListener('keyup', handleInputTextKey);
CIB.vm.actionBar.input.addEventListener('keydown', handleInputTextKey);
CIB.vm.actionBar.input.addEventListener('focus', handleInputFocus);
CIB.vm.actionBar.input.addEventListener('blur', handleInputBlur);
};
const handleInputStart = (ev: Event) => {
// console.log('compositionstart : ', ev);
isInput.value = true;
};
const handleInputEnd = (ev: Event) => {
// console.log('compositionend : ', ev);
isInput.value = false;
handleInputTextChanged(ev);
};
const handleInputTextChanged = (ev: Event) => {
// console.log('ev : ', ev);
if (isInput.value) {
return;
}
if ((ev instanceof InputEvent || ev instanceof CompositionEvent) && ev.target instanceof HTMLTextAreaElement) {
if (ev.target.value?.startsWith('/')) {
isShowChatPrompt.value = true;
keyword.value = ev.target.value.slice(1);
selectedPromptIndex.value = 0;
} else {
keyword.value = '';
isShowChatPrompt.value = false;
}
}
};
const handleInputFocus = (ev: FocusEvent) => {
// console.log('获取焦点:', ev);
};
const handleInputBlur = (ev: FocusEvent) => {
// 简单解决失焦与点击冲突
setTimeout(() => {
isShowChatPrompt.value = false;
}, 200);
};
const handleInputTextKey = (ev: KeyboardEvent) => {
switch (ev.key) {
case 'ArrowUp':
{
// ev.preventDefault();
if (selectedPromptIndex.value > 0) {
selectedPromptIndex.value--;
if (scrollbarRef.value) {
scrollbarRef.value.scrollToIndex(selectedPromptIndex.value);
}
}
}
break;
case 'ArrowDown':
{
// ev.preventDefault();
if (selectedPromptIndex.value < searchPromptList.value.length - 1) {
selectedPromptIndex.value++;
if (scrollbarRef.value) {
scrollbarRef.value.scrollToIndex(selectedPromptIndex.value);
}
}
}
break;
case 'Tab':
case 'Enter':
{
// ev.preventDefault();
if (!CIB.vm.actionBar.inputText || !CIB.vm.actionBar.inputText.startsWith('/')) {
return;
}
selectPrompt(searchPromptList.value[selectedPromptIndex.value]);
}
break;
}
};
const selectPrompt = (item: IPrompt) => {
// console.log('select prompt : ', item);
if (!item) {
return;
}
keyword.value = '';
CIB.vm.actionBar.inputText = item.prompt;
isShowChatPrompt.value = false;
};
const handlePromptListScroll = () => {
isPromptScrolling.value = true;
setTimeout(() => {
if (isPromptScrolling.value === true) {
isPromptScrolling.value = false;
// 滚动结束设置选中
const offset = scrollbarRef.value?.getOffset() || 0;
selectedPromptIndex.value = Math.round(offset / promptItemHeight);
}
}, 100);
};
const auth = async () => {
if (!authKey.value) {
message.error('请先输入授权码');
return;
}
isAuthBtnLoading.value = true;
userStore.setAuthKey(authKey.value);
const res = await userStore.getSysConfig();
if (res.data.isAuth) {
message.success('授权成功');
isShowUnauthorizedModal.value = false;
afterAuth(res.data);
} else {
message.error('授权码有误');
}
isAuthBtnLoading.value = false;
};
</script>
<template>
<LoadingSpinner :is-show="isShowLoading" />
<main>
<div
v-if="isShowChatPrompt"
class="box-border fixed bottom-[110px] w-full flex justify-center px-[14px] md:px-[34px] z-999"
:class="{
'md:px-[170px]': isShowHistory,
'xl:px-[220px]': isShowHistory,
}"
>
<div class="w-0 md:w-[60px]"></div>
<VirtualList
ref="scrollbarRef"
v-if="promptList.length > 0"
class="bg-white w-full max-w-[1060px] max-h-[390px] rounded-xl overflow-y-auto"
:data-key="'prompt'"
:data-sources="searchPromptList"
:data-component="ChatPromptItem"
:keeps="10"
@scroll="handlePromptListScroll"
/>
<NEmpty v-else class="bg-white w-full max-w-[1060px] max-h-[390px] rounded-xl py-6" description="暂未设置提示词数据">
<template #extra>
<NButton secondary type="info" @click="isShowPromptSotre = true">去提示词库添加</NButton>
</template>
</NEmpty>
</div>
</main>
<footer>
<!-- 服务器选择 -->
<ChatServiceSelect />
<!-- 授权 -->
<div v-if="isShowUnauthorizedModal" class="fixed top-0 left-0 w-screen h-screen flex justify-center items-center bg-black/40 z-50">
<NResult class="bg-white px-28 py-4 rounded-md" status="403" title="401 未授权">
<template #footer>
<NInput v-model:value="authKey" type="password" placeholder="请输入授权码" maxlength="60"></NInput>
<n-button class="mt-4" secondary type="info" :loading="isAuthBtnLoading" @click="auth"></n-button>
</template>
</NResult>
</div>
</footer>
</template>