From fa6de28a6fe7e79891f3b0f74f2501e010e460b1 Mon Sep 17 00:00:00 2001 From: gerson <1405270578@qq.com> Date: 星期日, 15 十二月 2024 16:33:03 +0800 Subject: [PATCH] 时间排序 --- src/components/chat/Chat.vue | 1056 +++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 files changed, 898 insertions(+), 158 deletions(-) diff --git a/src/components/chat/Chat.vue b/src/components/chat/Chat.vue index b0d66bc..1f8ac88 100644 --- a/src/components/chat/Chat.vue +++ b/src/components/chat/Chat.vue @@ -1,214 +1,954 @@ <template> - <div class="flex flex-col h-full"> - <div class="h-full flex flex-col items-center overflow-y-auto"> - <div ref="chatListDom" class="h-full w-[100ch]"> - <div - class="group flex px-4 py-4 hover:bg-slate-100 rounded-lg" - v-for="(item, index) of messageList.filter((v) => v.role !== 'system')" - :key="index" - > - <img class="rounded-full size-12 mr-4" :src="roleImageMap[item.role]" alt="" srcset="" /> + <div class="flex h-full"> + <div class="flex flex-col h-full flex-auto relative"> + <div ref="chatListDom" class="relative h-full flex flex-col items-center overflow-y-auto" style="height: calc(100% - 50px)"> + <span + class="more-loading absolute text-blue-400 left-[50%] translate-x-[-50%] cursor-pointer w-10" + v-loading="moreIsLoading" + ></span> + <div class="h-full relative" v-loading="chatListLoading" :style="{ width: chatWidth }"> + <template v-if="computedMessageList?.length > 0"> + <div + class="flex px-4 py-6 rounded-lg relative" + :class="{ 'flex-row-reverse': item.role === RoleEnum.user, 'px-10': isShareCheck }" + v-for="(item, msgIndex) of computedMessageList" + :key="`${item.historyId}_${item.role}`" + > + <div class="absolute top-0 left-[72px] text-[#8d8e99]">{{ item?.createTime }}</div> + <!-- :class="{ 'top-[30px]': item.role === RoleEnum.user, 'top-[30px]': item.role === RoleEnum.assistant }" --> - <div class="flex"> - <div class="relative" v-if="item.content"> - <div - :class="{ 'bg-[#d8d8ff]': item.role === RoleEnum.assistant, 'bg-white': item.role === RoleEnum.user }" - class="prose text-sm rounded-[6px] p-4 leading-relaxed max-w-[100ch]" - v-html="md.render(item.content)" - ></div> - <div v-if="item.role === RoleEnum.assistant" class="absolute flex items-center right-0 space-x-2 mr-2 mt-2"> - <SvgIcon class="cursor-pointer" name="ele-CopyDocument" @click="copyClick(item.content)" /> - <SvgIcon class="cursor-pointer" name="ywicon icon-dianzan" /> - <SvgIcon class="cursor-pointer" :size="12" name="ywicon icon-buzan" /> + <el-checkbox + v-if="isShareCheck" + class="absolute left-0 top-[28px]" + size="large" + v-model="item.isChecked" + @change="(isChecked) => shareCheckChange(isChecked as boolean, item)" + ></el-checkbox> + <img + class="rounded-full size-12 flex-0" + :class="{ 'mr-4': item.role === RoleEnum.assistant, 'ml-4': item.role === RoleEnum.user }" + :src="roleImageMap[item.role]" + alt="" + srcset="" + /> + <div class="flex-auto flex" :class="{ 'justify-end': item.role === RoleEnum.user }"> + <div class="inline-flex flex-col" :class="{ 'w-full': item.role === RoleEnum.assistant }"> + <div class="w-full"> + <div + class="rounded-[6px] p-4 leading-relaxed" + :style="{ backgroundColor: item.role === RoleEnum.user ? 'rgb(197 224 255)' : 'white' }" + :class="{ group: item.role === RoleEnum.user }" + > + <!-- #region ====================== 鎰忓浘鍒嗘瀽 ======================--> + <div class="flex flex-col" v-if="item?.stepList?.length > 0"> + <!-- #region ====================== 鎰忓浘鍒嗘瀽 ======================--> + <div class="flex items-center"> + <span class="mr-2">鎰忓浘鍒嗘瀽锛�</span> + <div + @click="toggleStepList(item)" + class="cursor-pointer border border-gray-300 border-solid w-fit px-2 flex items-center space-x-2 rounded-lg hover:bg-gray-100 active:bg-gray-200" + > + <span> + {{ toggleStepLabel(item) }} + </span> + <span + class="ywifont" + :class="{ 'ywicon-unfold': !item.stepIsShow, 'ywicon-fold': item.stepIsShow }" + ></span> + </div> + </div> + <!-- #endregion --> + + <!-- #region ====================== 杩囩▼杈撳嚭 ======================--> + <el-steps v-show="item.stepIsShow" class="mt-3" direction="vertical" :active="activeStep"> + <el-step + v-for="(subItem, index) in item.stepList" + :title="subItem.title" + :status="stepEnumMap[subItem.status]" + > + <template + #icon + v-if="index + 1 === item.stepList.length && isTalking && msgIndex === computedMessageList.length - 1" + > + <span class="ywifont ywicon-loading1 animate-spin !text-[24px]"></span> + </template> + <template #title> + <span class="" + >{{ subItem.title + }}<span v-if="subItem.ms" class="text-green-600">{{ `锛�${subItem.ms}锛塦 }}</span></span + > + </template> + </el-step> + </el-steps> + <!-- #endregion --> + </div> + <!-- #endregion --> + + <!-- #region ====================== 鐢ㄦ埛鎿嶄綔鎸夐挳 ======================--> + <div + v-if="item.role === RoleEnum.user && item.content?.values && !isSharePage && !isShareCheck" + class="absolute flex items-center bottom-0 group invisible" + > + <div + class="bg-[#fff] flex items-center relative mr-4 space-x-2 flex-nowrap rounded-[6px] py-2 px-2 group-hover:visible" + > + <el-tooltip effect="dark" content="澶嶅埗" placement="top"> + <div class="flex items-center justify-center size-[20px]"> + <i + class="p-2 ywifont ywicon-copy cursor-pointer hover:text-[#0284ff] font-medium !text-[15px] hover:!text-[18px]" + @click="copyUserClick(item)" + /> + </div> + </el-tooltip> + <el-tooltip effect="dark" content="璁句负甯哥敤璇�" placement="top"> + <div class="flex items-center justify-center size-[20px]"> + <i + class="p-2 ywifont ywicon-cubelifangti cursor-pointer hover:text-[#0284ff] text-[#000] font-[590] !text-[15px] hover:!text-[18px]" + @click="setCommonQuestionClick(item)" + /> + </div> + </el-tooltip> + <el-tooltip effect="dark" content="鍒嗕韩" placement="top"> + <div class="flex items-center justify-center size-[15px]"> + <i + :class="{ 'text-[#0284ff]': item.state === AnswerState.Unlike }" + class="p-2 ywifont ywicon-fenxiang cursor-pointer hover:text-[#0284ff] !text-[15px] hover:!text-[18px]" + @click="shareClick(item)" + /> + </div> + </el-tooltip> + </div> + </div> + + <!-- #endregion --> + <!-- #region ====================== 娑堟伅鍐呭 ======================--> + <template v-if="item.content?.values"> + <!-- #region ====================== 鎶ラ敊淇℃伅 ======================--> + <div v-if="item.content.errCode === ErrorCode.Message" class="flex-column w-full"> + <p class="text-red-500"> + {{ item.content.errMsg }} + </p> + <div class="mt-3 flex" v-if="showFixQuestion(item)"> + <div class="text-gray-600 flex-0 mb-auto py-3"> + {{ '鐚滀綘鎯抽棶锛�' }} + </div> + <div class="flex-auto space-x-2 space-y-1 inline-flex flex-wrap items-center"> + <div + v-for="fixItem in item.content.origin?.sample_question" + :key="fixItem" + class="bg-gray-200 p-3 hover:bg-[#c5e0ff] hover:text-[#1c86ff] cursor-pointer rounded-lg first-of-type:ml-2 first-of-type:mt-1" + @click="fixQuestionClick(fixItem, item.content.origin)" + > + {{ fixItem }} + </div> + </div> + </div> + </div> + <!-- #endregion --> + <!-- #region ====================== 鍥炵瓟缁勪欢 ======================--> + <template v-else> + <component + :conclusion="item.conclusion" + :is="answerTypeMapCom[item.content.type]" + :data="item.content.values" + :originData="item" + :isTalking="isTalking && msgIndex === computedMessageList.length - 1" + /> + <div + v-if="item.role === RoleEnum.assistant && item.content.origin?.ext_call_list" + class="flex font-bold items-center mt-6" + > + <div class="flex-0 mb-auto -mr-4">鍏宠仈鍔熻兘锛�</div> + <div class="space-x-5 flex flex-wrap"> + <div + v-for="callItem in item.content.origin?.ext_call_list" + :key="callItem.call_ext_id" + @click="relativeQueryClick(callItem)" + class="cursor-pointer hover:underline first-of-type:ml-5" + > + {{ callItem.question }} + </div> + </div> + </div> + </template> + <!-- #endregion --> + </template> + + <!-- #endregion --> + <!-- #region ====================== 闄勫姞鍐呭 ======================--> + <!-- #region ====================== 鍋滄 ======================--> + <span v-if="item.isStopMsg && item?.role === RoleEnum.assistant" class="text-gray-400 text-[12px]" + >锛堝凡鍋滄锛�</span + > + <!-- parseContent 杩斿洖涓� null --> + <p v-if="!item.content && !isTalking && !item.isStopMsg" class="text-red-500">鏆傛棤鏁版嵁</p> + <!-- #endregion --> + <!-- #endregion --> + </div> + <!-- #region ====================== ai 娑堟伅鎿嶄綔 ======================--> + <div + v-if="item.role === RoleEnum.assistant && item.content?.values && !isSharePage && !isShareCheck" + class="absolute flex items-center right-0 mr-4 mt-2 space-x-2" + > + <div + class="flex items-center justify-center size-[15px]" + v-if="item.content?.type === AnswerType.Text || item.content?.type === AnswerType.Knowledge" + > + <i + class="p-2 ywifont ywicon-copy cursor-pointer hover:text-[#0284ff] hover:!text-[18px]" + @click="copyClick(item)" + /> + </div> + <template v-if="item.content.errCode !== ErrorCode.Message"> + <el-tooltip effect="dark" content="鐐硅禐" placement="top"> + <div class="flex items-center justify-center size-[15px]"> + <i + :class="{ 'text-[#0284ff]': item.state === AnswerState.Like }" + class="p-2 ywifont ywicon-dianzan cursor-pointer hover:text-[#0284ff] font-medium hover:!text-[18px]" + @click="likeClick(item)" + /> + </div> + </el-tooltip> + <el-tooltip effect="dark" content="鐐硅俯" placement="top"> + <div class="flex items-center justify-center size-[15px]"> + <i + :class="{ 'text-[#0284ff]': item.state === AnswerState.Unlike }" + class="p-2 ywifont ywicon-buzan cursor-pointer hover:text-[#0284ff] !text-[13px] hover:!text-[15px]" + @click="unLikeClick(item)" + /> + </div> + </el-tooltip> + </template> + <el-tooltip effect="dark" content="鍒嗕韩" placement="top"> + <div class="flex items-center justify-center size-[15px]"> + <i + class="p-2 ywifont ywicon-fenxiang cursor-pointer hover:text-[#0284ff] !text-[15px] hover:!text-[18px]" + @click="shareClick(item)" + /> + </div> + </el-tooltip> + <el-tooltip effect="dark" content="鍙嶉" placement="top"> + <div class="flex items-center justify-center size-[15px] relative"> + <i + class="p-2 ywifont ywicon-wentifankui cursor-pointer hover:text-[#0284ff] !text-[13px] hover:!text-[15px]" + @click=" + ($event) => + feedbackClick( + $event, + item, + computedMessageList + .filter((v) => v.role === RoleEnum.assistant) + .findIndex((v) => v.historyId === item.historyId) + ) + " + /> + <FeedbackPanel + v-show="feedbackIsShow && currentFeedbackMapItem === item" + ref="feedbackPanelRef" + v-model:isShow="feedbackIsShow" + v-model:content="feedbackContent" + :chatItem="currentFeedbackMapItem" + :position="feedbackPosition" + /> + </div> + </el-tooltip> + </div> + <!-- #endregion --> + </div> + </div> </div> </div> - - <Loding v-else /> + </template> + <el-empty v-else-if="isSharePage && !chatListLoading" :image-size="200"> + <template #description> + <span class="text-[15px]"> 鍒嗕韩鐨勫璇濅笉瀛樺湪鎴栧凡澶辨晥 </span> + </template> + </el-empty> + <div v-if="showAskMore" class="ml-4 mt-5 pb-10"> + <div class="text-gray-600 mb-5">浣犲彲浠ョ户缁棶鎴戯細</div> + <div class="space-y-2 inline-flex flex-col"> + <div + v-for="item in computedMessageList.at(-1).content.askMoreList" + :key="item.history_id" + class="bg-white p-3 hover:bg-[#c5e0ff] hover:text-[#1c86ff] cursor-pointer rounded-lg" + @click="askMoreClick(item)" + > + {{ item.question }} + </div> + </div> </div> </div> </div> - </div> + <div class="absolute right-28 bottom-40 " v-if="!isBottom"> + <div class="flex items-center justify-center size-[38px] cursor-pointer hover:text-[#0284ff] border rounded-full hover:bg-[#f6f7f9] shadow bg-white" @click="scrollToBottom"> + <i class="ywifont ywicon-xiangxiajiantou !text-[20px]" /> + </div> + </div> - <div class="sticky bottom-0 w-full p-6 pb-8 bg-gray-100 flex justify-center"> - <PlayBar :isTalking="isTalking" v-model="messageContent" @sendClick="sendOrSave" /> + <div + class="sticky bottom-0 w-full px-6 pt-12 pb-6 bg-[rgb(247,248,250)] flex justify-center" + v-if="!isSharePage && !isShareCheck" + > + <PlayBar + v-model:voicePageIsShow="voicePageIsShow" + :isTalking="isTalking" + :isHome="false" + v-model="messageContent.values" + @sendClick="sendClick" + @showUpChatClick="showUpChatClick" + @stopGenClick="stopGenClick" + @showDownChatClick="showDownChatClick" + :style="{ width: chatWidth }" + :setCommonQuestionInfo="setCommonQuestionInfo" + ></PlayBar> + </div> + <div class="sticky bottom-0 w-full p-6 bg-[rgb(247,248,250)] flex justify-center" v-if="isShareCheck"></div> </div> + <CustomDrawer v-model:isShow="drawerIsShow" @updateChatInput="updateChatInput" /> + <el-dialog title="鍒嗕韩閾炬帴" v-model="shareCodeIsShow" width="12%" modal-append-to-body lock-scroll :before-close="closeShareClick"> + <div class="w100 h100 flex justify-center items-center flex-col text-center"> + <div class="qrcode h100" ref="qrcodeRef"></div> + </div> + </el-dialog> </div> </template> <script setup lang="ts"> -import cryptoJS from 'crypto-js'; +import type { CancelTokenSource } from 'axios'; +import axios from 'axios'; import { ElMessage } from 'element-plus'; -import { nextTick, onMounted, ref, watch } from 'vue'; +import { findLast, orderBy } from 'lodash-es'; +import moment from 'moment'; +import QRCode from 'qrcodejs2-fixes'; +import { computed, nextTick, onActivated, onMounted, ref } from 'vue'; import useClipboard from 'vue-clipboard3'; -import Loding from './components/Loding.vue'; -import { md } from './libs/markdown'; -import { RoleEnum, roleImageMap, type ChatMessage } from './types'; +import { loadAmisSource } from '../amis/load'; +import FeedbackPanel from './components/FeedbackPanel.vue'; +import { useAssistantContentOpt } from './hooks/useAssistantContentOpt'; +import { convertProcessItem, useScrollLoad } from './hooks/useScrollLoad'; +import { useScrollToBottom } from './hooks/useScrollToBottom'; +import type { ChatContent, StepItem } from './model/types'; +import { AnswerState, AnswerType, RoleEnum, answerTypeMapCom, roleImageMap, stepEnumMap, type ChatMessage } from './model/types'; +import { extCallQuery, questionStreamByPost, shareChatHistoryByPost } from '/@/api/ai/chat'; import PlayBar from '/@/components/chat/components/playBar/PlayBar.vue'; - -let apiKey = ''; -let isConfig = ref(false); +import CustomDrawer from '/@/components/drawer/CustomDrawer.vue'; +import { SHARE_URL } from '/@/constants'; +import { Logger } from '/@/model/logger/Logger'; +import router from '/@/router'; +import { + activeChatRoom, + activeGroupType, + activeLLMId, + activeRoomId, + activeSampleId, + activeSectionAId, + getRoomConfig, + isSharePage, + roomConfig, +} from '/@/stores/chatRoom'; +import emitter from '/@/utils/mitt'; +import { ErrorCode } from '/@/utils/request'; +import { toMyFixed } from '/@/utils/util'; +const chatWidth = '75%'; +const voicePageIsShow = ref(false); let isTalking = ref(false); -let messageContent = ref(''); +let messageContent = ref<ChatContent>({ + type: AnswerType.Text, + values: '', +}); +const currentRoute = router.currentRoute; +const currentRouteId = currentRoute.value.query.id as string; +activeRoomId.value = currentRouteId; const chatListDom = ref<HTMLDivElement>(); -const decoder = new TextDecoder('utf-8'); -const roleAlias = { user: 'ME', assistant: 'ChatGPT', system: 'System' }; -const messageList = ref<ChatMessage[]>([ - // { - // role: RoleEnum.assistant, - // content: `浣犲ソ锛屾垜鏄疉I璇█妯″瀷锛屾垜鍙互鎻愪緵涓�浜涘父鐢ㄦ湇鍔″拰淇℃伅锛屼緥濡傦細 - // 1. 缈昏瘧锛氭垜鍙互鎶婁腑鏂囩炕璇戞垚鑻辨枃锛岃嫳鏂囩炕璇戞垚涓枃锛岃繕鏈夊叾浠栦竴浜涜瑷�缈昏瘧锛屾瘮濡傛硶璇�佹棩璇�佽タ鐝墮璇瓑銆� - // 2. 鍜ㄨ鏈嶅姟锛氬鏋滀綘鏈変换浣曢棶棰橀渶瑕佸挩璇紝渚嬪鍋ュ悍銆佹硶寰嬨�佹姇璧勭瓑鏂归潰锛屾垜鍙互灏藉彲鑳戒负浣犳彁渚涘府鍔┿�� - // 3. 闂茶亰锛氬鏋滀綘鎰熷埌瀵傚癁鎴栨棤鑱婏紝鎴戜滑鍙互鑱婁竴浜涙湁瓒g殑璇濋锛屼互鍑忚交浣犵殑鍘嬪姏銆� - // 璇峰憡璇夋垜浣犻渶瑕佸摢鏂归潰鐨勫府鍔╋紝鎴戜細鏍规嵁浣犵殑闇�姹傜粰浣犳彁渚涚浉搴旂殑淇℃伅鍜屽缓璁�俙, - // }, - // { - // role: RoleEnum.user, - // content: `浣犲ソ`, - // }, -]); - -onMounted(() => { - if (getAPIKey()) { - switchConfigStatus(); - } - const inputValue = history.state.inputValue; - messageContent.value = inputValue; - sendOrSave(); +const messageList = ref<ChatMessage[]>([]); +const computedMessageList = computed(() => { + return messageList.value.filter((v) => !!v); }); -const sendChatMessage = async (content: string = messageContent.value) => { +const parseContent = (res, reportIsShow = false) => { + if (!res) return null; + let content: ChatContent = { + type: AnswerType.Text, + values: '瑙f瀽澶辫触锛�', + }; + + switch (res.answer_type) { + case AnswerType.RecordSet: + content = { + type: AnswerType.RecordSet, + values: res.values, + }; + break; + case AnswerType.Text: + content = { + type: AnswerType.Text, + values: res.values ?? res.answer, + }; + break; + + case AnswerType.Knowledge: + content = { + type: AnswerType.Knowledge, + values: res.knowledge, + }; + + break; + case AnswerType.Summary: + content = { + type: AnswerType.Summary, + values: res.summary?.map((item) => { + item.reportIsShow = reportIsShow; + return item; + }), + }; + break; + case AnswerType.Url: + content = { + type: AnswerType.Url, + values: res.url, + }; + break; + case AnswerType.Map: + content = { + type: AnswerType.Map, + values: res.values, + }; + break; + default: + content = { + type: AnswerType.Text, + values: '瑙f瀽澶辫触锛�', + }; + break; + } + content.askMoreList = orderBy(res.context_history, [(item) => Number(item.radio)], ['desc']); + content.errCode = res?.err_code; + content.errMsg = res?.json_msg; + content.origin = res; + return content; +}; + +//#region ====================== 姝ラ step ====================== +const activeStep = ref(-1); +const stepList = ref<StepItem[]>([ + { + title: '鎰忓浘鍒嗘瀽涓�...', + status: 0, + }, + { + title: '鎰忓浘鍒嗘瀽瀹屾垚', + status: 1, + }, + { + title: '鎬濊�冨浣曟墽琛�:鎸囨爣鏄庣粏鏌ヨ', + status: 1, + }, + { + title: '鎸囨爣鏄庣粏鏌ヨ瀹屾垚', + status: 1, + }, +]); + +const resetStep = () => { + activeStep.value = -1; + stepList.value = []; +}; + +const toggleStepLabel = (item: ChatMessage) => (item.stepIsShow ? '鏀惰捣' : '灞曞紑'); +const toggleStepList = (item: ChatMessage) => { + item.stepIsShow = !item.stepIsShow; +}; + +//#endregion + +const DEFAULT_SECTION_A_ID = 'knowledge_base'; +let questionRes = null; +let position = null; +let finalCalcSectionAId = null; + +let lastAxiosSource: CancelTokenSource = null; +const questionAi = async (text) => { + let judgeParams = null; + if (!preQuestion.value) { + // const aiContent = computedMessageList.value.filter((item) => item.role === RoleEnum.assistant); + // const lastQuestion = aiContent[aiContent.length - 2]?.content?.origin?.question; + // judgeParams = lastQuestion + // ? { + // prev_question: lastQuestion, + // } + // : {}; + // 姝e父鍥炵瓟鏆傛椂涓嶉噰鐢� + judgeParams = {}; + } else { + judgeParams = { + prev_question: preQuestion.value, + }; + } + let currentSectionAId = ''; + if (activeSectionAId.value) { + currentSectionAId = activeSectionAId.value; + activeSectionAId.value = ''; + } else { + const lastSectionAItem = findLast( + computedMessageList.value as any, + (item) => item.role === RoleEnum.assistant && !!item.sectionAId + ); + currentSectionAId = lastSectionAItem?.sectionAId ?? DEFAULT_SECTION_A_ID; + } + finalCalcSectionAId = currentSectionAId; + + const params = { + // process_id: processId.value, + question: text, + // FIXME: 鏆傛椂杩欐牱 + // section_a_id: currentSectionAId, + history_group_id: currentRouteId, + raw_mode: roomConfig.value?.[currentRouteId]?.isAnswerByLLM ?? false, + ...judgeParams, + } as any; + + if (position) { + const longitude = position.coords.longitude; + const latitude = position.coords.latitude; + params.cur_pos = [longitude, latitude].join(','); + } + + if (activeGroupType.value) { + params.group_type = activeGroupType.value; + } + + if (currentSampleId) { + params.sample_id = currentSampleId; + currentSampleId = ''; + } + + // if (currentLLMId) { + // params.llm_id = currentLLMId; + // } + // clearQueryProcess(); + // queryProcess(); + resetStep(); + let res = null; + let lastTimestamp = new Date().getTime(); + const resultP = new Promise(async (resolve, reject) => { + const currentSource = axios.CancelToken.source(); + lastAxiosSource = currentSource; + await questionStreamByPost( + params, + (chunkRes) => { + Logger.info('chunk response锛歕n\n' + JSON.stringify(chunkRes)); + if (chunkRes.mode === 'result') { + res = chunkRes.value; + resolve(res); + chunkRes.value = '鍑嗗鏁版嵁鍒嗘瀽'; + } + + if (chunkRes.mode === 'conclusion') { + computedMessageList.value.at(-1).conclusion = chunkRes.value; + chunkRes.value = '鍒嗘瀽缁撴潫'; + } + const stepList = computedMessageList.value.at(-1).stepList; + const currentTimeStamp = new Date().getTime(); + const ms = toMyFixed(currentTimeStamp - lastTimestamp, 2) + ' ms'; + if (chunkRes.mode === 'finish') { + stepList.at(-1).ms = ms; + + return; + } + + if (stepList?.length >= 1) { + stepList.at(-1).ms = ms; + } + lastTimestamp = currentTimeStamp; + + const stepItem = convertProcessItem(chunkRes); + stepList.push(stepItem); + scrollToBottom(); + }, + { + cancelToken: currentSource.token, + } + ) + .catch((err) => { + throw err; + }) + .finally(() => { + isTalking.value = false; + computedMessageList.value.at(-1).stepIsShow = false; + + resetStep(); + }); + }); + + questionRes = await resultP; + isTalking.value = false; + + const content = parseContent(res, true); + return content; +}; + +const clearMessageContent = () => + (messageContent.value = { + type: AnswerType.Text, + values: '', + }); + +let currentSampleId = ''; + +let currentLLMId = null; + +const stopGenClick = () => { + lastAxiosSource?.cancel(); + isTalking.value = false; + chatListLoading.value = false; + resetStep(); + computedMessageList.value.at(-1).isStopMsg = true; +}; + +const sendChatMessage = async (content: ChatContent = messageContent.value, cb?: any, isCallExtParams?: any) => { + if (!content?.values) { + return; + } + if (isTalking.value || chatListLoading.value) { + ElMessage.warning('ai 姝e湪鍥炲涓紝璇风◢鍚庡皾璇曟彁闂�'); + return; + } + // position = position ?? (await getCurrentPosition()); + + const isNewChat = messageList.value.length === 0; + if (isNewChat) { + if (activeSampleId.value) { + currentSampleId = activeSampleId.value; + } + + if (activeLLMId.value) { + currentLLMId = activeLLMId.value; + } + } + let resMsgContent: ChatContent = null; + try { isTalking.value = true; - // if (messageList.value.length === 0) { - // messageList.value.pop(); - // } - messageList.value.push({ role: RoleEnum.user, content }); + const userItem: ChatMessage = { role: RoleEnum.user, content, isChecked: false } as any; + const assistantItem: ChatMessage = { + role: RoleEnum.assistant, + content: null, + state: AnswerState.Null, + stepList: [], + stepIsShow: true, + isStopMsg: false, + isChecked: false, + } as any; + // 鍙戦�佸綋鍓� + messageList.value.push(userItem); + // 娓呯┖杈撳叆妗� clearMessageContent(); - messageList.value.push({ role: RoleEnum.assistant, content: '' }); - // const { body, status } = await chat(messageList.value, getAPIKey()); - // if (body) { - // const reader = body.getReader(); - // await readStream(reader, status); - // } - const a = new Promise<string>((resolve) => { - setTimeout(() => { - resolve('浣犲ソ锛屾垜鏄疉I璇█妯″瀷 '); - }, 500); - }); + // 鍑虹幇鍥炲锛岀疆绌哄嚭鐜扮瓑寰呭姩鐢� + messageList.value.push(assistantItem); + // 婊氬姩鑷冲綋鍓嶅彂閫佹秷鎭� + scrollToBottom(); - const msg = await a; - appendLastMessageContent(msg); + if (isCallExtParams) { + const extRes = await extCallQuery(isCallExtParams); + questionRes = extRes; + resMsgContent = parseContent(extRes, true); + } else { + resMsgContent = await questionAi(content.values); + } + nextUserMsgEndIndex.value++; + if (isNewChat) { + const firstResCb = getRoomConfig(currentRouteId, 'firstResCb'); + firstResCb?.(resMsgContent); + } else { + cb?.(resMsgContent); + } + userItem.historyId = questionRes.history_id; + userItem.content.values = questionRes?.question ?? userItem.content.values; + assistantItem.historyId = questionRes.history_id; + assistantItem.sectionAId = finalCalcSectionAId; + appendLastMessageContent(resMsgContent); + setTimeout(() => { + // 鏀跺埌鍥炲锛岀户缁粴 + scrollToBottom(); + }, 300); } catch (error: any) { - appendLastMessageContent(error); - } finally { - isTalking.value = false; + // appendLastMessageContent({ + // type: AnswerType.Text, + // values: '鍙戠敓閿欒锛�', + // }); } }; -const readStream = async (reader: ReadableStreamDefaultReader<Uint8Array>, status: number) => { - let partialLine = ''; - - while (true) { - // eslint-disable-next-line no-await-in-loop - const { value, done } = await reader.read(); - if (done) break; - - const decodedText = decoder.decode(value, { stream: true }); - - if (status !== 200) { - const json = JSON.parse(decodedText); // start with "data: " - const content = json.error.message ?? decodedText; - appendLastMessageContent(content); - return; - } - - const chunk = partialLine + decodedText; - const newLines = chunk.split(/\r?\n/); - - partialLine = newLines.pop() ?? ''; - - for (const line of newLines) { - if (line.length === 0) continue; // ignore empty message - if (line.startsWith(':')) continue; // ignore sse comment message - if (line === 'data: [DONE]') return; // - - const json = JSON.parse(line.substring(6)); // start with "data: " - const content = status === 200 ? json.choices[0].delta.content ?? '' : json.error.message; - appendLastMessageContent(content); - } +const sendClick = (cb) => { + sendChatMessage(messageContent.value, cb); +}; +const appendLastMessageContent = (content: ChatContent) => { + const currentTime = moment().format('MM鏈圖D鏃� HH:mm:ss'); + if (messageList.value.at(-1)) { + messageList.value.at(-1).content = content; + messageList.value.at(-1).createTime = currentTime; } }; -const appendLastMessageContent = (content: string) => (messageList.value[messageList.value.length - 1].content += content); +const { loadRangeData, onChatListScroll, moreIsLoading, nextUserMsgEndIndex } = useScrollLoad({ + container: chatListDom, + historyGroupId: currentRouteId, + messageList, + parseAnswerContent: parseContent, +}); -const sendOrSave = () => { - if (!messageContent.value.length) return; - if (isConfig.value) { - if (saveAPIKey(messageContent.value.trim())) { - switchConfigStatus(); - } - clearMessageContent(); - } else { +const chatListLoading = ref(true); + +const { scrollToBottom, scrollToTop, isBottom } = useScrollToBottom({ + chatListDom: chatListDom, +}); + +onActivated(() => { + emitter.emit('updateHeaderTitle', activeChatRoom.value?.title ?? ''); +}); + +onMounted(async () => { + messageList.value = []; + // 鍔犺浇鍒濆鏁版嵁 + chatListLoading.value = true; + await loadRangeData().finally(() => { + chatListLoading.value = false; + }); + setTimeout(() => { + emitter.emit('updateHeaderTitle', activeChatRoom.value?.title ?? ''); + }, 300); + + if (messageList.value.length === 0) { + messageContent.value = { + type: AnswerType.Text, + values: activeChatRoom.value?.title, + }; + sendChatMessage(); - } -}; - -const clickConfig = () => { - if (!isConfig.value) { - messageContent.value = getAPIKey(); } else { - clearMessageContent(); + if (isSharePage.value) { + // setTimeout(() => { + // // 婊氬姩鍒伴《閮� + // scrollToTop(); + // }, 300); + } else { + setTimeout(() => { + // 鍒濆鐘舵�佹粴涓�涓� + scrollToBottom(); + + setTimeout(() => { + chatListDom.value.addEventListener('scroll', onChatListScroll); + }, 300); + }, 300); + } } - switchConfigStatus(); + loadAmisSource(); +}); +//#region ====================== 鍏宠仈鏌ヨ ====================== +const relativeQueryClick = async (val) => { + sendChatMessage( + { + type: AnswerType.Text, + values: val.question, + }, + undefined, + { + history_group_id: currentRouteId, + question: val.question, + call_ext_id: val.call_ext_id, + call_ext_args: val.agrs ? JSON.stringify(val.agrs) : null, + } + ); }; - -const getSecretKey = () => 'lianginx'; - -const saveAPIKey = (apiKey: string) => { - if (apiKey.slice(0, 3) !== 'sk-' || apiKey.length !== 51) { - alert('API Key 閿欒锛岃妫�鏌ュ悗閲嶆柊杈撳叆锛�'); - return false; +//#endregion +//#region ====================== 鍏夋爣杈撳叆涓婁笅绠ご鏄剧ず鍘嗗彶娑堟伅 ====================== +const currentIndex = ref(null); +const history_data = computed(() => { + return computedMessageList.value.filter((item) => item.role === RoleEnum.user); +}); +//鏄剧ず涓婁竴鏉℃秷鎭� +const showUpChatClick = () => { + if (computedMessageList.value.length === 0) return; + if (currentIndex.value == 0) { + messageContent.value.values = history_data.value[currentIndex.value].content.values; + return; + } else { + currentIndex.value = (currentIndex.value + history_data.value.length - 1) % history_data.value.length; } - const aesAPIKey = cryptoJS.AES.encrypt(apiKey, getSecretKey()).toString(); - localStorage.setItem('apiKey', aesAPIKey); - return true; + messageContent.value.values = history_data.value[currentIndex.value].content.values; }; - -const getAPIKey = () => { - if (apiKey) return apiKey; - const aesAPIKey = localStorage.getItem('apiKey') ?? ''; - apiKey = cryptoJS.AES.decrypt(aesAPIKey, getSecretKey()).toString(cryptoJS.enc.Utf8); - return apiKey; +//鏄剧ず涓嬩竴鏉℃秷鎭� +const showDownChatClick = () => { + if (computedMessageList.value.length === 0) return; + if (currentIndex.value == history_data.value.length - 1) { + messageContent.value.values = history_data.value[currentIndex.value].content.values; + return; + } + if (currentIndex.value === null) { + currentIndex.value = 0; + } else { + currentIndex.value = (currentIndex.value + 1) % history_data.value.length; + } + messageContent.value.values = history_data.value[currentIndex.value].content.values; }; +//#endregion +const { + copyClick, + likeClick, + unLikeClick, + feedbackPosition, + feedbackIsShow, + feedbackContent, + feedbackPanelRef, + currentFeedbackMapItem, + feedbackClick, + askMoreClick, + fixQuestionClick, + preQuestion, + showFixQuestion, + showAskMore, +} = useAssistantContentOpt({ + sendChatMessage, + displayMessageList: computedMessageList, +}); -const switchConfigStatus = () => (isConfig.value = !isConfig.value); +//#region ====================== 渚ц竟鏍廳rawer ====================== +const drawerIsShow = ref(false); -const clearMessageContent = () => (messageContent.value = ''); - -const scrollToBottom = () => { - if (!chatListDom.value) return; - chatListDom.value.lastElementChild.scrollIntoView(); - // scrollTo(0, chatListDom.value.scrollHeight); +const updateChatInput = (content) => { + messageContent.value.values = content; }; - -watch(messageList.value, () => nextTick(() => scrollToBottom())); - -//#region ====================== 鑱婂ぉ鍐呭鎿嶄綔 ====================== - +//#endregion +//#region ====================== 鐢ㄦ埛璇㈤棶鐨勯棶棰樿缃负甯哥敤璇� ====================== +const setCommonQuestionInfo = ref({}); const { toClipboard } = useClipboard(); -const copyClick = (content) => { +//鐢ㄦ埛澶嶅埗闂 +const copyUserClick = (item) => { + const text = item.content.values; ElMessage.success('澶嶅埗鎴愬姛'); - toClipboard(content); + toClipboard(text); }; +//鐢ㄦ埛闂璁剧疆涓哄父鐢ㄨ +const setCommonQuestionClick = (item) => { + setCommonQuestionInfo.value = item; +}; +//#endregion + +//#region ====================== 鍒嗕韩 ====================== + +const resetShare = () => { + computedMessageList.value.forEach((item) => { + item.isChecked = false; + }); + isShareCheck.value = false; +}; + +const isShareCheck = ref(false); +const qrcodeRef = ref<HTMLElement | null>(null); +const shareCodeIsShow = ref(false); //鏄惁寮圭獥鏄剧ず鍒嗕韩浜岀淮鐮� +const shareCoderUrl = ref(''); +const shareClick = async (item: ChatMessage) => { + item.isChecked = true; + shareCheckChange(true, item); + // 鐩墠鍙垎浜竴涓紝涓嶈繘鍏ュ閫夋ā寮忥紝鍒嗕韩澶氫釜 + // isShareCheck.value = true; + shareCodeIsShow.value = true; + const url = await generateShareUrl(); + shareCoderUrl.value = url; + const qrCodeElement = document.querySelector('.qrcode'); + // ElMessage.success('宸插鍒跺垎浜摼鎺�'); + nextTick(() => { + (<HTMLElement>qrCodeElement && qrCodeElement).innerHTML = ''; + new QRCode(qrCodeElement, { + text: url, + width: 100, + height: 100, + colorDark: '#000000', + colorLight: '#ffffff', + }); + }); + copyShareCodeClick(); +}; +//閾炬帴澶嶅埗 +const copyShareCodeClick = () => { + const url = shareCoderUrl.value; + if (!url) return; + toClipboard(url); + ElMessage.success('宸插鍒跺垎浜摼鎺�'); +}; +//鍏抽棴鍒嗕韩寮圭獥 +const closeShareClick = () => { + shareCodeIsShow.value = false; + resetShare(); +}; +const shareCheckChange = (isChecked: boolean, item: ChatMessage) => { + const toFindRole = item.role === RoleEnum.user ? RoleEnum.assistant : RoleEnum.user; + + const foundMapItem = computedMessageList.value.find( + (msgItem) => msgItem.historyId === item.historyId && msgItem.role === toFindRole + ); + if (!foundMapItem) return; + foundMapItem.isChecked = isChecked; +}; + +const generateShareUrl = async () => { + const shareList = computedMessageList.value.filter((item) => item.isChecked && item.role === RoleEnum.user && !!item.historyId); + if (shareList.length === 0) { + // ElMessage.warning('璇烽�夋嫨瑕佸垎浜殑鍐呭'); + return; + } + const historyIdStr = shareList.map((item) => item.historyId).join(','); + const res = await shareChatHistoryByPost({ + history_ids: historyIdStr, + share_days: 1, + }).finally(() => { + resetShare(); + }); + + if (!res.values) return; + const shareId = Object.values(res.values)[0]; + if (!shareId) return; + const shareLink = `${SHARE_URL}?id=${shareId}`; + return shareLink; +}; + //#endregion </script> -<style scoped> +<style scoped lang="scss"> pre { font-family: -apple-system, 'Noto Sans', 'Helvetica Neue', Helvetica, 'Nimbus Sans L', Arial, 'Liberation Sans', 'PingFang SC', 'Hiragino Sans GB', 'Noto Sans CJK SC', 'Source Han Sans SC', 'Source Han Sans CN', 'Microsoft YaHei', 'Wenquanyi Micro Hei', 'WenQuanYi Zen Hei', 'ST Heiti', SimHei, 'WenQuanYi Zen Hei Sharp', sans-serif; } + +.more-loading { + :deep(.el-loading-spinner) { + --loading-size: 35px; + margin-top: 0; + .circular { + width: var(--loading-size); + height: var(--loading-size); + } + } +} + +:deep(.el-step__icon.is-text) { + --radius-size: 24px; + width: var(--radius-size); + height: var((--radius-size)); +} + +:deep(.el-step__icon-inner) { + font-size: 16px !important; +} +:deep(.el-step__description) { + height: 20px; +} + +:deep(.el-step:last-of-type .el-step__description) { + display: none; +} </style> -- Gitblit v1.9.3