<script setup lang="ts"> import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue' import { onBeforeRouteLeave } from 'vue-router' import { Button, Spin, message } from 'ant-design-vue' import { AppContainerBox } from '@/components/AppContainerBox' import { AppSubMenuTitle } from '@/components/AppSubMenuTitle' import { AppSubMenuList } from '@/components/AppSubMenuList' import { AppRoleDefault } from '@/components/AppRoleDefault' import { AppUserInfo } from '@/components/AppUserInfo' import { SubMenuActionEnum } from '@/components/AppSubMenuList/index.d' import type { SubMenuItem } from '@/components/AppSubMenuList/index.d' import { AppTextarea } from '@/components/AppTextarea' import { AppMessage } from '@/components/AppMessage' import { AppModelSelect } from '@/components/AppModelSelect' import type { ModelSelect } from '@/components/AppModelSelect/index.d' import type { RoleData, RoleInfoAppModel } from '@/components/AppRoleDefault/index.d' import type { MessageItem } from '@/components/AppMessage/index.d' import { useMessageStore } from '@/store/moules/messageStore/index' import { MenuTypeEnum } from '@/enums/menuEnum' import { MessageStatusEnum, MessageTypeEnum, ModelTypeEnum } from '@/enums/messageEnum' import { addMessage, conversationList, conversationToTop, historyMessage, removeMessage, sendTextToText, updateMessage } from '@/api/base/message' import { getRoleDecs } from '@/api/base/role' import { useMqtt } from '@/hooks/useMqtt' import { useMessage } from '@/hooks/useMessage' const { createConfirm } = useMessage() const messageStore = useMessageStore() const sendBtnLoading = ref(false) const subMenuActiveIndex = ref(-1) // 当前会话索引 const subMenuActionIndex = ref(-1) // 会话操作索引 const subMenuList = ref<SubMenuItem[]>([]) const subMenuInputValue = ref<string>('') const appMessageRef = ref() const modelOptions: ModelSelect[] = [ { label: '同聪3.5', value: ModelTypeEnum.GPT3, }, { label: '同聪4.0', value: ModelTypeEnum.GPT4, }, ] const modelIndex = ref(0) const messageList = computed(() => messageStore.getMessageList) const messageStatus = computed(() => messageStore.getMessageStatus) const conversationData = computed(() => messageStore.getConversationData) const historyMessageParams = ref({ conversationId: '', current: 1, size: 10, total: 0, }) const conversationDefaultShow = ref(false) const appMessageShow = ref(true) const spinning = ref(true) watch( () => messageStatus.value, (val) => { if (val === MessageStatusEnum.END) { sendBtnLoading.value = false } }, ) /** * @description: 点击会话操作项 */ function handlesubMenuActionIndex(type: SubMenuActionEnum, item: SubMenuItem, index: number) { subMenuActionIndex.value = index const subMenuActionData = subMenuList.value[index] if (type === SubMenuActionEnum.EDIT) { subMenuList.value.forEach((item) => { item.actionType = SubMenuActionEnum.NOT }) subMenuList.value[index].actionType = SubMenuActionEnum.EDIT subMenuInputValue.value = item.title } if (type === SubMenuActionEnum.DELETE) { createConfirm({ title: '删除', content: `确定要删除${item.title}会话吗?`, iconType: 'warning', onOk: () => { removeMessage(item.id).then(async () => { message.success('删除成功') subMenuActiveIndex.value = -1 conversationDefaultShow.value = true await getConversationList() await handleSubMenuChange(subMenuActiveIndex.value) }) }, }) } if (type === SubMenuActionEnum.TO_TOP) { conversationToTop(item.id).then(async () => { message.success('置顶成功') await getConversationList() const index = subMenuList.value.findIndex(item => item.id === subMenuActionData.id) await handleSubMenuChange(index) getHistoryMessage() }) } } /** * @description: 切换会话 */ async function handleSubMenuChange(index: number, item?: SubMenuItem) { if (messageStatus.value !== MessageStatusEnum.END) { message.warn('请先结束对话') return } if (!subMenuList.value.length) { return } subMenuActiveIndex.value = index historyMessageParams.value.current = 1 historyMessageParams.value.total = 0 messageStore.setConversationData(subMenuList.value[subMenuActiveIndex.value]) messageStore.setMessageClear() const modelIdx = modelOptions.findIndex(v => v.value === Number(subMenuList.value[subMenuActiveIndex.value].modelType)) if (modelIdx === -1) { message.warn('未找到该对话下的模型信息') return } modelIndex.value = modelIdx useMqtt().end() useMqtt().connect() // item有的话说明是点击切换,需要重新获取历史消息,否则是不是点击切换调用,不需要获取历史记录 if (item) { getHistoryMessage() } } /** * @description: 发送消息 */ async function handleSend(value: string) { sendBtnLoading.value = true conversationDefaultShow.value = false if (!conversationData.value) { return } sendMessage(conversationData.value.id, value) } /** * @description: 获取会话列表 */ async function getConversationList() { const res = await conversationList(MenuTypeEnum.ROLE) res.forEach((item: SubMenuItem) => { item.actionType = SubMenuActionEnum.NOT }) subMenuList.value = res } /** * @description: 获取历史对话记录 */ async function getHistoryMessage() { if (!conversationData.value) { spinning.value = false conversationDefaultShow.value = true return } spinning.value = true historyMessageParams.value.conversationId = conversationData.value.id const res = await historyMessage(historyMessageParams.value) messageStore.setMessageClear() spinning.value = false if (!res || !res.records || !res.records.length) { getRoleDecs(String(conversationData.value.roleId)).then((res) => { conversationDefaultShow.value = false messageStore.setMessagePushItem({ messageType: MessageTypeEnum.DESCRIBE, content: res, time: String(new Date().getTime()), avatar: '', messageStatus: MessageStatusEnum.END, }) }) return } res.records.forEach((item: any) => { const itemData: MessageItem = { messageType: item.roleType === MessageTypeEnum.USER ? MessageTypeEnum.USER : MessageTypeEnum.AI, content: item.messageContent, time: item.messageTime, avatar: '', messageStatus: MessageStatusEnum.END, } historyMessageParams.value.total = res.total messageStore.setMessageUnshiftItem(itemData) }) conversationDefaultShow.value = false // 因为会话切换时滚动底部有问题,所以重新加载下 appMessageShow.value = false setTimeout(() => { appMessageShow.value = true }, 0) } /** * @description: 滚动监听 */ async function onScrollTop(scrollTop: number) { if (scrollTop !== 0) { return } if (historyMessageParams.value.current * historyMessageParams.value.size > historyMessageParams.value.total) { return } if (historyMessageParams.value.current < historyMessageParams.value.total) { historyMessageParams.value.current++ await getHistoryMessage() // 无缝滚动 appMessageRef.value.seamlessScrollToTop() } } /** * @description: 发送消息hook */ async function sendMessage(conversationId: string, question: string): Promise<void> { if (!messageStore.getConversationData) { return } conversationDefaultShow.value = false messageStore.setMessageStatus(MessageStatusEnum.LOADING) messageStore.setMessagePushItem({ messageType: MessageTypeEnum.USER, content: question, time: String(new Date().getTime()), avatar: '', }) messageStore.setMessagePushItem({ messageType: MessageTypeEnum.AI, content: '正在思考中...', time: String(new Date().getTime()), avatar: '', messageStatus: MessageStatusEnum.LOADING, }) sendTextToText({ conversationId, question, modelType: modelOptions[modelIndex.value].value, }).catch(() => { messageStore.getMessageList.splice(-2) messageStore.setMessageStatus(MessageStatusEnum.END) }) } /** * @description: 重新回答 */ function reloadMessage() { if (!conversationData.value) { return } const question = messageList.value[messageList.value.length - 2]?.content sendMessage(conversationData.value.id, question) } /** * @description: 点击新建会话按钮 */ function handleAddMessage() { if (messageStatus.value !== MessageStatusEnum.END) { message.warn('请先结束对话') return } conversationDefaultShow.value = true subMenuActiveIndex.value = -1 } /** * @description: 选择角色 */ async function handleRole(item: RoleData) { const obj = subMenuList.value.find(v => String(v.roleId) === item.id) if (obj) { const index = subMenuList.value.findIndex(v => String(v.roleId) === item.id) await handleSubMenuChange(index) getHistoryMessage() } else { await addMessage({ type: MenuTypeEnum.ROLE, title: item.roleName, roleId: item.id, sort: subMenuList.value.length + 1, modelType: modelOptions[modelIndex.value].value }) await getConversationList() await nextTick() const index = subMenuList.value.findIndex(v => String(v.roleId) === item.id) subMenuActiveIndex.value = index await handleSubMenuChange(index) getHistoryMessage() } } /** * @description: 选择应用 */ async function handleApply(item: RoleInfoAppModel) { const obj = subMenuList.value.find(v => v.roleId === item.id) if (obj) { const index = subMenuList.value.findIndex(v => v.roleId === item.id) await handleSubMenuChange(index) getHistoryMessage() } else { await addMessage({ type: MenuTypeEnum.ROLE, title: item.roleName, roleId: String(item.id), sort: subMenuList.value.length + 1, modelType: modelOptions[modelIndex.value].value }) await getConversationList() await nextTick() const index = subMenuList.value.findIndex(v => v.roleId === item.id) subMenuActiveIndex.value = index await handleSubMenuChange(index) getHistoryMessage() } } /** * @description: gpt模型切换 */ function handleModel(index: number) { modelIndex.value = index } // 下面是子菜单操作项 function handleSubMenuInputAffirm(index: number, item: SubMenuItem, inputValue: string) { updateMessage({ ...item, title: inputValue }).then(() => { message.success('修改成功') subMenuList.value[index].title = inputValue subMenuList.value[index].actionType = SubMenuActionEnum.NOT }) } function handleSubMenuInputClose(index: number) { subMenuList.value[index].actionType = SubMenuActionEnum.NOT } function handleSubMenuInputBlur(index: number, item: SubMenuItem, inputValue: string) { subMenuActionIndex.value = -1 handleSubMenuInputAffirm(index, item, inputValue) } onMounted(async () => { await getConversationList() if (subMenuList.value.length) { subMenuActiveIndex.value = 0 } await handleSubMenuChange(subMenuActiveIndex.value) getHistoryMessage() }) onUnmounted(() => { messageStore.setMessageClear() useMqtt().end() }) // 路由离开时的操作 onBeforeRouteLeave(() => { if (messageStatus.value !== MessageStatusEnum.END) { message.warn('请先结束对话') return false } }) </script> <template> <AppContainerBox> <template #subMenu> <!-- 标题 --> <AppSubMenuTitle title="角色会话"></AppSubMenuTitle> <!-- 按钮 --> <div class="px-5 mb-5"> <Button type="primary" class="w-full" @click="handleAddMessage"> 新建会话 </Button> </div> <!-- 会话列表 --> <AppSubMenuList v-model:input-value="subMenuInputValue" :list="subMenuList" :active-index="subMenuActiveIndex" :action-index="subMenuActionIndex" @change="handleSubMenuChange" @handle-action="handlesubMenuActionIndex" @input-affirm="handleSubMenuInputAffirm" @input-close="handleSubMenuInputClose" @input-blur="handleSubMenuInputBlur" > </AppSubMenuList> <AppUserInfo /> </template> <template #content> <Spin :spinning="spinning" wrapper-class-name="app-content-spin"> <AppModelSelect v-if="conversationDefaultShow" :active-index="modelIndex" :options="modelOptions" @change="handleModel" > </AppModelSelect> <!-- 默认导语 --> <AppRoleDefault v-if="conversationDefaultShow" height="calc(100% - 120px)" @handle-role="handleRole" @handle-apply="handleApply" > </AppRoleDefault> <!-- 消息列表 --> <AppMessage v-if="!conversationDefaultShow && appMessageShow" ref="appMessageRef" class="pl-27 pr-5" :list="messageList" @on-scroll-top="onScrollTop" @reload-message="reloadMessage" > </AppMessage> </Spin> <!-- 发送框 --> <AppTextarea v-if="!conversationDefaultShow" class="pl-52 pr-32 mt-10" :btn-loading="sendBtnLoading" @send="handleSend" ></AppTextarea> </template> </AppContainerBox> </template> <style lang="scss" scoped> </style>