From 1476d27514874e9c95002451a81878bd9bec8382 Mon Sep 17 00:00:00 2001 From: wujingjing <gersonwu@qq.com> Date: 星期六, 14 十二月 2024 15:36:53 +0800 Subject: [PATCH] 多轮对话 --- src/api/ai/chat.ts | 11 +++ vite.config.ts | 2 src/components/chat/Chat.vue | 60 ++++++++++++++++--- src/components/chat/chatComponents/multiChat/Select.vue | 37 ++++++++++++ src/components/chat/chatComponents/multiChat/index.ts | 6 ++ src/components/chat/hooks/useScrollLoad.ts | 25 ++++++- src/components/chat/model/types.ts | 28 +++++--- 7 files changed, 142 insertions(+), 27 deletions(-) diff --git a/src/api/ai/chat.ts b/src/api/ai/chat.ts index 12be70d..20bcaa5 100644 --- a/src/api/ai/chat.ts +++ b/src/api/ai/chat.ts @@ -502,3 +502,14 @@ 'Content-Type': 'application/x-www-form-urlencoded', }, }); + + + export const question_stream_reply = (params) => + request({ + url: `/chat/question_stream_reply`, + method: 'post', + params: params, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }); \ No newline at end of file diff --git a/src/components/chat/Chat.vue b/src/components/chat/Chat.vue index 1f8ac88..75d56af 100644 --- a/src/components/chat/Chat.vue +++ b/src/components/chat/Chat.vue @@ -61,6 +61,7 @@ <!-- #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" @@ -73,10 +74,26 @@ <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 + <span class=""> + {{ subItem.title }} + + <span v-if="subItem.ms" class="text-green-600">{{ `锛�${subItem.ms}锛塦 }}</span></span > + </template> + + <template #description v-if="subItem?.subStep?.length > 0"> + <div class="my-1 flex flex-col gap-1 text-[14px]"> + + + <component + :key="`${item.historyId}-${index + 1}-${multiChatIndex + 1}`" + v-for="(multiChatItem, multiChatIndex) in subItem.subStep" + :order="`${index + 1}-${multiChatIndex + 1}`" + :item="multiChatItem" + :is="multiChatTypeMapCom[multiChatItem.type]" + :disabled="!(index + 1 === item.stepList.length && isTalking && msgIndex === computedMessageList.length - 1)" + /> + </div> </template> </el-step> </el-steps> @@ -279,8 +296,11 @@ </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"> + <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> @@ -346,12 +366,15 @@ isSharePage, roomConfig, } from '/@/stores/chatRoom'; + +import { multiChatTypeMapCom } from '/@/components/chat/chatComponents/multiChat'; 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<ChatContent>({ type: AnswerType.Text, values: '', @@ -547,6 +570,20 @@ computedMessageList.value.at(-1).conclusion = chunkRes.value; chunkRes.value = '鍒嗘瀽缁撴潫'; } + + if (chunkRes.mode === 'question') { + const stepList = computedMessageList.value.at(-1).stepList; + const lastStepItem = stepList.at(-1); + if (!lastStepItem.subStep) { + lastStepItem.subStep = []; + } + lastStepItem.subStep.push({ + type: chunkRes.value.type, + data: chunkRes.value, + }); + scrollToBottom(); + return; + } const stepList = computedMessageList.value.at(-1).stepList; const currentTimeStamp = new Date().getTime(); const ms = toMyFixed(currentTimeStamp - lastTimestamp, 2) + ' ms'; @@ -560,10 +597,13 @@ stepList.at(-1).ms = ms; } lastTimestamp = currentTimeStamp; - const stepItem = convertProcessItem(chunkRes); + stepList.push(stepItem); - scrollToBottom(); + + if (chunkRes.mode !== 'result') { + scrollToBottom(); + } }, { cancelToken: currentSource.token, @@ -581,7 +621,7 @@ }); questionRes = await resultP; - isTalking.value = false; + // isTalking.value = false; const content = parseContent(res, true); return content; @@ -945,10 +985,10 @@ font-size: 16px !important; } :deep(.el-step__description) { - height: 20px; + min-height: 20px; } :deep(.el-step:last-of-type .el-step__description) { - display: none; + // display: none; } </style> diff --git a/src/components/chat/chatComponents/multiChat/Select.vue b/src/components/chat/chatComponents/multiChat/Select.vue new file mode 100644 index 0000000..39272bd --- /dev/null +++ b/src/components/chat/chatComponents/multiChat/Select.vue @@ -0,0 +1,37 @@ +<template> + <div class="flex flex-col gap-1"> + <span class="text-gray-600 font-normal">{{ `${order} ${item?.data?.title}` }}</span> + + <div v-if="item?.data?.options?.length > 0" class="flex-items-center gap-5"> + <span + @click="select(subItem)" + v-for="subItem in item?.data?.options" + class="flex w-fit items-center cursor-pointer border-solid border px-3 border-gray-300 hover:text-blue-400 rounded-lg" + :class="{ 'cursor-not-allowed': disabled, }" + >{{ subItem }}</span + > + </div> + </div> +</template> + +<script setup lang="ts"> +import { ref, watch } from 'vue'; +import { question_stream_reply } from '/@/api/ai/chat'; +const props = defineProps(['order', 'item', 'disabled']); +// :class="[...(subItem === activeOption ? ['bg-blue-400', 'text-white'] : []), disabled ? 'cursor-not-allowed' : '']" +// 'bg-blue-400': subItem === activeOption, 'text-white': subItem === activeOption.value +const activeOption = ref(); +const select = async (option) => { + if (props.disabled) return; + const res = await question_stream_reply({ + select: option, + reply_id: props.item?.data?.reply_id, + }); + if (res.json_ok) { + activeOption.value = option; + } +}; + + +</script> +<style scoped lang="scss"></style> diff --git a/src/components/chat/chatComponents/multiChat/index.ts b/src/components/chat/chatComponents/multiChat/index.ts new file mode 100644 index 0000000..329ea11 --- /dev/null +++ b/src/components/chat/chatComponents/multiChat/index.ts @@ -0,0 +1,6 @@ +import { MultiChatType } from '../../model/types'; +import Select from './Select.vue'; + +export const multiChatTypeMapCom = { + [MultiChatType.Select]: Select, +}; diff --git a/src/components/chat/hooks/useScrollLoad.ts b/src/components/chat/hooks/useScrollLoad.ts index 4a8b0ec..63be29d 100644 --- a/src/components/chat/hooks/useScrollLoad.ts +++ b/src/components/chat/hooks/useScrollLoad.ts @@ -1,7 +1,7 @@ import moment from 'moment'; import { Ref, ShallowRef, computed, nextTick, onBeforeUnmount, ref, unref } from 'vue'; import { LOAD_CHAT_LIMIT } from '../constants'; -import { AnswerType, ChatContent, ChatMessage, RoleEnum, StepEnum, StepItem } from '../model/types'; +import { AnswerType, ChatContent, ChatMessage, MultiChatType, RoleEnum, StepEnum, StepItem } from '../model/types'; import { GetHistoryAnswer, QueryHistoryDetail, getShareChatJsonByPost } from '/@/api/ai/chat'; import router from '/@/router'; import { isSharePage } from '/@/stores/chatRoom'; @@ -22,14 +22,29 @@ return { status: StepEnum.Success, title: processItem.value, - }; + } as StepItem; }; export const convertProcessToStep = (process: any[]) => { - const stepList = (process ?? []).map<StepItem>((item) => { - return convertProcessItem(item); - }); + const stepList = (process ?? []).reduce((preVal, curVal) => { + if (curVal.mode === 'question') { + const last = preVal.at(-1); + if (!last.subStep) { + last.subStep = []; + } + const sub = { + data: curVal.value, + type: MultiChatType.Select, + }; + last.subStep.push(sub); + } else { + const cur = convertProcessItem(curVal); + preVal.push(cur); + } + return preVal; + }, []); return stepList; }; + /** * 婊氬姩鍔犺浇鏁版嵁 * @returns diff --git a/src/components/chat/model/types.ts b/src/components/chat/model/types.ts index 3212f7e..90ffaa1 100644 --- a/src/components/chat/model/types.ts +++ b/src/components/chat/model/types.ts @@ -54,22 +54,20 @@ role: RoleEnum; content?: ChatContent; state?: null | '1' | '0'; - sectionAId?:string, - createTime?:string, - stepList?:StepItem[], - stepIsShow?:boolean, - isStopMsg?:boolean, + sectionAId?: string; + createTime?: string; + stepList?: StepItem[]; + stepIsShow?: boolean; + isStopMsg?: boolean; /** @description 鏄惁琚�夋嫨鍒嗕韩 */ - isChecked:boolean, - conclusion?:any[] + isChecked: boolean; + conclusion?: any[]; } export const roleImageMap = { [RoleEnum.user]: userPic, [RoleEnum.assistant]: assistantPic, }; - - export const enum StepEnum { Loading, @@ -81,10 +79,18 @@ [StepEnum.Success]: 'process', [StepEnum.Error]: 'process', }; +export const enum MultiChatType { + Select = 'select', +} +type SubStep = { + data:any; + type: MultiChatType.Select; +}; export type StepItem = { title: string; status: StepEnum; // 娑堣�楁椂闂� - ms?:string | number; -}; \ No newline at end of file + ms?: string | number; + subStep?: SubStep[]; +}; diff --git a/vite.config.ts b/vite.config.ts index c8bccb9..badea13 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -58,7 +58,7 @@ host: '0.0.0.0', port: env.VITE_PORT as unknown as number, open: JSON.parse(env.VITE_OPEN), - hmr: false, + hmr: true, }, build: { // outDir: 'dist/' + mode.mode, -- Gitblit v1.9.3