| | |
| | | ref="sidebarRef"> |
| | | <ConversationList |
| | | :active-id="activeConversationId" |
| | | :quick-access="quickAccessFlag" |
| | | :model-name="modelName" |
| | | :default-message="defaultMessage" |
| | | ref="conversationListRef" |
| | | @on-conversation-create="handleConversationCreateSuccess" |
| | | @on-conversation-click="handleConversationClick" |
| | |
| | | <!-- 情况一:消息加载中 --> |
| | | <MessageLoading v-if="activeMessageListLoading" /> |
| | | <!-- 情况二:无聊天对话时 --> |
| | | <MessageNewConversation |
| | | <MessageListEmpty |
| | | v-if="!activeConversation" |
| | | @on-new-conversation="handleConversationCreate" |
| | | /> |
| | | <!-- 情况三:消息列表为空 --> |
| | | <MessageListEmpty |
| | |
| | | @input="handlePromptInput" |
| | | @compositionstart="onCompositionstart" |
| | | @compositionend="onCompositionend" |
| | | placeholder="问我任何问题...(Shift+Enter 换行,按下 Enter 发送)" |
| | | placeholder="请问我问题...(Shift+Enter 换行,按下 Enter 发送)" |
| | | ></textarea> |
| | | <div class="prompt-btns"> |
| | | <div class="content"> |
| | |
| | | import MessageList from '../message/MessageList.vue' |
| | | import MessageListEmpty from '../message/MessageListEmpty.vue' |
| | | import MessageLoading from '../message/MessageLoading.vue' |
| | | import MessageNewConversation from '../message/MessageNewConversation.vue' |
| | | import { onClickOutside } from '@vueuse/core' |
| | | import * as authUtil from "@/utils/auth"; |
| | | import {refreshToken} from "@/api/login"; |
| | | import {formatToDateTime} from "@/utils/dateUtil"; |
| | | import { formatReasoningContent } from '@/views/ai/utils/utils' |
| | | |
| | | /** AI 聊天对话 列表 */ |
| | | defineOptions({ name: 'NormalConversation' }) |
| | | |
| | | const props = defineProps({ |
| | | data: { |
| | | type: Object, |
| | | default: () => null |
| | | } |
| | | }) |
| | | |
| | | const route = useRoute() // 路由 |
| | | const message = useMessage() // 消息弹窗 |
| | |
| | | isCollapsed.value = !isCollapsed.value |
| | | } |
| | | |
| | | const modelName = ref<string>('common') // 对话搜索 |
| | | |
| | | // 聊天对话 |
| | | const conversationListRef = ref() |
| | | const quickAccessFlag = ref(false) |
| | | const defaultMessage = ref<ChatMessageVO>() |
| | | const activeConversationId = ref<number | null>(null) // 选中的对话编号 |
| | | const activeConversation = ref<ChatConversationVO | null>(null) // 选中的 Conversation |
| | | const conversationInProgress = ref(false) // 对话是否正在进行中。目前只有【发送】消息时,会更新为 true,避免切换对话、删除对话等操作 |
| | |
| | | // 接收 Stream 消息 |
| | | const receiveMessageFullText = ref('') |
| | | const receiveMessageDisplayedText = ref('') |
| | | |
| | | |
| | | // =========== 【聊天对话】相关 =========== |
| | | |
| | |
| | | activeMessageList.value = await ChatMessageApi.getChatMessageListByConversationId( |
| | | activeConversationId.value |
| | | ) |
| | | |
| | | // 滚动到最下面 |
| | | await nextTick() |
| | | await scrollToBottom() |
| | |
| | | return [] |
| | | }) |
| | | |
| | | //处理调度推理结论 |
| | | const dealResult = (conversations: any) => { |
| | | const regex = /<think>(\n*)([\s\S]*?)(\n*)<\/think>(\n*)([\s\S]*)/; |
| | | conversations.forEach((conversation) => { |
| | | if(conversation.content.includes('<\/think>')) { |
| | | conversation.thinkingFlag = false |
| | | } else { |
| | | conversation.thinkingFlag = true |
| | | } |
| | | const match = conversation.content.match(regex); |
| | | if(match) { |
| | | conversation.thinking = match[2]; |
| | | conversation.conclusion = match[5] |
| | | //处理调度推理结论(微调大模型) |
| | | const dealResult = (messages: any) => { |
| | | messages.forEach((message) => { |
| | | if(message.type === 'assistant') { |
| | | const spliceText = message.content.includes("总结:") ? "总结:" : "结论:"; |
| | | // 创建同时捕获前后内容的正则表达式 |
| | | const regex = new RegExp(`^([\\s\\S]*?)${spliceText}([\\s\\S]*)$`); |
| | | const match = message.content.match(regex); |
| | | if(match) { |
| | | message.thinking = match[1]; |
| | | message.conclusion = match[2] |
| | | } else { |
| | | message.thinking = message.content |
| | | } |
| | | // 处理推理思路内容 |
| | | message.thinking = formatReasoningContent(message.thinking); |
| | | } |
| | | }) |
| | | } |
| | |
| | | message.error('发送失败,原因:内容为空!') |
| | | return |
| | | } |
| | | // 发送请求时如果accessToken过期,无法中断请求,暂时增加请求前刷新token |
| | | authUtil.setToken(await refreshToken()) |
| | | if (activeConversationId.value == null) { |
| | | message.error('还没创建对话,不能发送!') |
| | | return |
| | | await conversationListRef.value.createConversation(props.data?formatToDateTime(new Date(props.data.createTime)):null) |
| | | } |
| | | // 清空输入框 |
| | | prompt.value = '' |
| | | // 发送请求时如果accessToken过期,无法中断请求,暂时增加请求前刷新token |
| | | authUtil.setToken(await refreshToken()) |
| | | // 执行发送 |
| | | await doSendMessageStream({ |
| | | conversationId: activeConversationId.value, |
| | | content: content |
| | | } as ChatMessageVO) |
| | | setTimeout(() => { |
| | | // 执行发送 |
| | | doSendMessageStream({ |
| | | conversationId: activeConversationId.value, |
| | | content: content |
| | | } as ChatMessageVO) |
| | | }, 400) |
| | | } |
| | | |
| | | /** 真正执行【发送】消息操作 */ |
| | |
| | | conversationInProgress.value = true |
| | | // 设置为空 |
| | | receiveMessageFullText.value = '' |
| | | |
| | | try { |
| | | // 1.1 先添加两个假数据,等 stream 返回再替换 |
| | | activeMessageList.value.push({ |
| | |
| | | await nextTick() |
| | | await scrollToBottom() // 底部 |
| | | // 1.3 开始滚动 |
| | | textRoll() |
| | | |
| | | await textRoll() |
| | | // 2. 发送 event stream |
| | | let isFirstChunk = true // 是否是第一个 chunk 消息段 |
| | | await ChatMessageApi.sendChatMessageStream( |
| | |
| | | stopStream() |
| | | } |
| | | ) |
| | | } catch {} |
| | | } catch { |
| | | console.log('sendStream Exception') |
| | | } |
| | | } |
| | | |
| | | /** 停止 stream 流式调用 */ |
| | |
| | | |
| | | /** 初始化 **/ |
| | | onMounted(async () => { |
| | | // 如果有 conversationId 参数,则默认选中 |
| | | if (route.query.conversationId) { |
| | | const id = route.query.conversationId as unknown as number |
| | | activeConversationId.value = id |
| | | await getConversation(id) |
| | | defaultMessage.value = props.data |
| | | if(defaultMessage.value) { |
| | | prompt.value = defaultMessage.value.content |
| | | quickAccessFlag.value = true |
| | | } else { |
| | | // 获取列表数据 |
| | | activeMessageListLoading.value = true |
| | | await getMessageList() |
| | | } |
| | | }) |
| | | |
| | | // 获取列表数据 |
| | | activeMessageListLoading.value = true |
| | | await getMessageList() |
| | | onUnmounted(() => { |
| | | stopStream() |
| | | }) |
| | | </script> |
| | | |
| | |
| | | |
| | | .sidebar-toggle { |
| | | position: absolute; |
| | | left: 300px; // 初始展开位置 |
| | | left: 320px; // 初始展开位置 |
| | | top: 40%; |
| | | z-index: 1000; |
| | | z-index: 1; |
| | | width: 20px; |
| | | height: 80px; |
| | | background: rgba(115, 196, 255, 0.5); |
| | |
| | | left: 0; |
| | | top: 0; |
| | | bottom: 0; |
| | | width: 300px; |
| | | width: 320px; |
| | | background: rgba(13,28,58,0.9); |
| | | box-shadow: 2px 0 8px rgba(0,0,0,0.1); |
| | | transition: transform 0.3s ease, opacity 0.2s ease; |
| | | z-index: 999; |
| | | overflow: hidden; |
| | | overflow-x: hidden; |
| | | |
| | | &.collapsed { |
| | | transform: translateX(-100%); |
| | |
| | | // 头部 |
| | | .detail-container { |
| | | width: 100%; |
| | | height: 885px; |
| | | height: 910px; |
| | | margin-left: 5px; |
| | | background-color: rgba(0, 0, 0, 0); /* 透明背景 */ |
| | | transition: margin 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
| | |
| | | .footer-container { |
| | | display: flex; |
| | | flex-direction: column; |
| | | height: 114px; |
| | | height: 205px; |
| | | margin-left: 10px; |
| | | padding: 0; |
| | | |
| | |
| | | flex-direction: column; |
| | | padding: 9px 10px; |
| | | width: 876px; |
| | | height: 114px; |
| | | height: 205px; |
| | | background: rgba(115,196,255,0.05); |
| | | border-radius: 4px 4px 4px 4px; |
| | | border: 1px solid #73C4FF; |
| | |
| | | } |
| | | |
| | | .prompt-input { |
| | | width: 876px; |
| | | height: 113.55px; |
| | | width: 860px; |
| | | height: 203px; |
| | | font-weight: 400; |
| | | font-size: 14px; |
| | | background-color: rgba(219,238,255,0); |