src/views/ai/dashboard/components/conversation/CommonConversation.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/views/ai/dashboard/components/conversation/CommonConversationList.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/views/ai/dashboard/components/message/HistoryMessageList.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/views/ai/dashboard/components/message/MessageList.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/views/ai/dashboard/zhuanlu/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/views/ai/model/template/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/views/ai/dashboard/components/conversation/CommonConversation.vue
@@ -18,6 +18,9 @@ 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" @@ -55,9 +58,8 @@ <!-- 情况一:消息加载中 --> <MessageLoading v-if="activeMessageListLoading" /> <!-- 情况二:无聊天对话时 --> <MessageNewConversation <MessageListEmpty v-if="!activeConversation" @on-new-conversation="handleConversationCreate" /> <!-- 情况三:消息列表为空 --> <MessageListEmpty @@ -89,7 +91,7 @@ @input="handlePromptInput" @compositionstart="onCompositionstart" @compositionend="onCompositionend" placeholder="问我任何问题...(Shift+Enter 换行,按下 Enter 发送)" placeholder="请问我问题...(Shift+Enter 换行,按下 Enter 发送)" ></textarea> <div class="prompt-btns"> <div class="content"> @@ -141,13 +143,21 @@ 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 {ElLoading} from "element-plus"; /** AI 聊天对话 列表 */ defineOptions({ name: 'NormalConversation' }) const props = defineProps({ data: { type: Object, default: () => null } }) const route = useRoute() // 路由 const message = useMessage() // 消息弹窗 @@ -175,8 +185,12 @@ 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,避免切换对话、删除对话等操作 @@ -199,7 +213,6 @@ // 接收 Stream 消息 const receiveMessageFullText = ref('') const receiveMessageDisplayedText = ref('') // =========== 【聊天对话】相关 =========== @@ -298,7 +311,6 @@ activeMessageList.value = await ChatMessageApi.getChatMessageListByConversationId( activeConversationId.value ) // 滚动到最下面 await nextTick() await scrollToBottom() @@ -335,19 +347,37 @@ 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] // //处理调度推理结论(deepSeek) // 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 } } }) } @@ -455,19 +485,20 @@ 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) } /** 真正执行【发送】消息操作 */ @@ -478,7 +509,6 @@ conversationInProgress.value = true // 设置为空 receiveMessageFullText.value = '' try { // 1.1 先添加两个假数据,等 stream 返回再替换 activeMessageList.value.push({ @@ -499,8 +529,7 @@ await nextTick() await scrollToBottom() // 底部 // 1.3 开始滚动 textRoll() await textRoll() // 2. 发送 event stream let isFirstChunk = true // 是否是第一个 chunk 消息段 await ChatMessageApi.sendChatMessageStream( @@ -542,7 +571,9 @@ stopStream() } ) } catch {} } catch { console.log('sendStream Exception') } } /** 停止 stream 流式调用 */ @@ -632,16 +663,19 @@ /** 初始化 **/ 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> @@ -655,7 +689,7 @@ .sidebar-toggle { position: absolute; left: 300px; // 初始展开位置 left: 320px; // 初始展开位置 top: 40%; z-index: 1000; width: 20px; @@ -682,12 +716,12 @@ 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%); @@ -699,7 +733,7 @@ // 头部 .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); @@ -796,7 +830,7 @@ .footer-container { display: flex; flex-direction: column; height: 114px; height: 205px; margin-left: 10px; padding: 0; @@ -831,7 +865,7 @@ 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; @@ -842,8 +876,8 @@ } .prompt-input { width: 876px; height: 113.55px; width: 860px; height: 203px; font-weight: 400; font-size: 14px; background-color: rgba(219,238,255,0); src/views/ai/dashboard/components/conversation/CommonConversationList.vue
@@ -1,8 +1,8 @@ <!-- AI 对话 --> <template> <el-aside width="260px" class="conversation-container h-100%"> <el-aside width="280px" class="conversation-container h-100%"> <!-- 左顶部:对话 --> <div class="h-100%"> <div class="h-80%"> <div class="conversation-title"> <img src="@/assets/ai/zhuanlu/conversation_big.png" @@ -12,7 +12,7 @@ 对话列表 </div> <!-- <hr class="line"/>--> <el-button class="w-1/1 btn-new-conversation" type="primary" @click="createConversation"> <el-button class="w-1/1 btn-new-conversation" type="primary" @click="createNewConversation"> <img src="@/assets/ai/zhuanlu/conversation_big.png" class="mr-8px w-[1.5em] h-[1.5em]" @@ -82,8 +82,6 @@ </div> </div> </div> <!-- 底部占位 --> <div class="h-160px w-100%"></div> </div> </div> @@ -102,12 +100,13 @@ import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation' import { Bottom, Top } from '@element-plus/icons-vue' import roleAvatarDefaultImg from '@/assets/ai/zhuanlu/assistant.png' import {ChatMessageVO} from "@/api/ai/chat/message"; import {formatToDate, formatToDateTime} from "@/utils/dateUtil"; const message = useMessage() // 消息弹窗 // 定义属性 const searchName = ref<string>('') // 对话搜索 const modelName = ref<string>('common') // 对话搜索 const activeConversationId = ref<number | null>(null) // 选中的对话,默认为 null const hoverConversationId = ref<number | null>(null) // 悬浮上去的对话 const conversationList = ref([] as ChatConversationVO[]) // 对话列表 @@ -120,7 +119,16 @@ activeId: { type: String || null, required: true } }, modelName: { type: String || null, required: true }, quickAccess: { type: Boolean || null, required: true }, defaultMessage: {} }) // 定义钩子 @@ -169,10 +177,7 @@ }, 50) // 1.1 获取 对话数据 conversationList.value = await ChatConversationApi.getChatConversationEnergyList(modelName.value) if(conversationList.value.length == 0) { await createConversation() } conversationList.value = await ChatConversationApi.getChatConversationEnergyList(props.modelName) // 1.2 排序 conversationList.value.sort((a, b) => { return b.createTime - a.createTime @@ -184,7 +189,7 @@ return } // 2. 对话根据时间分组(置顶、今天、一天前、三天前、七天前、30 天前) // 2. 对话根据时间分组(置顶、今天、昨天、三天前、七天前、30 天前) conversationMap.value = await getConversationGroupByCreateTime(conversationList.value) } finally { // 清理定时器 @@ -203,7 +208,7 @@ const groupMap = { 置顶: [], 今天: [], 一天前: [], 昨天: [], 三天前: [], 七天前: [], 三十天前: [] @@ -212,9 +217,11 @@ const now = Date.now() // 定义时间间隔常量(单位:毫秒) const oneDay = 24 * 60 * 60 * 1000 const threeDays = 3 * oneDay const sevenDays = 7 * oneDay const thirtyDays = 30 * oneDay //今天 const today = formatToDate(new Date()) const yesterday = formatToDate(new Date().setDate(new Date().getDate() - 1)) for (const conversation of list) { // 置顶 if (conversation.pinned) { @@ -223,11 +230,13 @@ } // 计算时间差(单位:毫秒) const diff = now - conversation.createTime let conversationDate = formatToDate(conversation.createTime) let titleDate = conversation.title.split(' ')[0] // 根据时间间隔判断 if (diff < oneDay) { if (titleDate == today) { groupMap['今天'].push(conversation) } else if (diff < threeDays) { groupMap['一天前'].push(conversation) } else if (titleDate == yesterday) { groupMap['昨天'].push(conversation) } else if (diff < sevenDays) { groupMap['三天前'].push(conversation) } else if (diff < thirtyDays) { @@ -240,17 +249,22 @@ } /** 新建对话 */ const createConversation = async () => { const createNewConversation = async () => { await createConversation(null) } /** 新建对话 */ const createConversation = async (title: String) => { // 1. 新建对话 const conversationId = await ChatConversationApi.createChatConversationEnergy( {modelName: modelName.value} as unknown as ChatConversationVO {modelName: props.modelName, title: title} as unknown as ChatConversationVO ) // 2. 获取对话内容 await getChatConversationList() // 3. 选中对话 await handleConversationClick(conversationId) // 4. 回调 emits('onConversationCreate') // // 4. 回调 // emits('onConversationCreate') } /** 修改对话的标题 */ @@ -344,15 +358,28 @@ onMounted(async () => { // 获取 对话列表 await getChatConversationList() // 默认选中 if (props.activeId) { activeConversationId.value = props.activeId } else { // 首次默认选中第一个 if(!props.quickAccess) { // 默认选中 if (props.activeId) { activeConversationId.value = props.activeId } else { // 首次默认选中第一个 if (conversationList.value.length) { activeConversationId.value = conversationList.value[0].id // 回调 onConversationClick await emits('onConversationClick', conversationList.value[0]) } } } else if(props.defaultMessage) { let tempTitle = formatToDateTime(new Date(props.defaultMessage.createTime)) if (conversationList.value.length) { activeConversationId.value = conversationList.value[0].id // 回调 onConversationClick await emits('onConversationClick', conversationList.value[0]) conversationList.value.forEach((item) => { if(item.title === tempTitle) { activeConversationId.value = item.id // 回调 onConversationClick emits('onConversationClick', item) } }) } } }) @@ -395,16 +422,30 @@ } .conversation-list { overflow: auto; overflow-y: auto; height: 100%; margin-top: 10px; /* Firefox */ scrollbar-width: thin; scrollbar-color: rgba(0, 0, 0, 0.15) transparent; /* WebKit */ &::-webkit-scrollbar { width: 6px; background: transparent; } &::-webkit-scrollbar-thumb { border-radius: 4px; background: rgba(0, 0, 0, 0.15); transition: background 0.3s; &:hover { background: rgba(0, 0, 0, 0.25); } } .classify-title { padding-top: 10px; b { color: white; } } .conversation-item { margin-top: 5px; } @@ -443,10 +484,9 @@ } .title { padding: 2px 10px; padding: 2px 5px; max-width: 220px; font-size: 14px; font-weight: 400; font-size: 13px; color: rgba(0, 0, 0, 0.77); overflow: hidden; white-space: nowrap; @@ -478,17 +518,19 @@ } } // 角色仓库、清空未设置对话 // 清空未设置对话 .tool-box { bottom: 0; display: flex; padding: 0 20px; width: 90%; margin-left: 5%; background-color: rgba(69,133,255,0.2); box-shadow: 0 0 1px 1px rgba(69,133,255,0.4); line-height: 35px; justify-content: space-between; align-items: center; color: var(--el-text-color); border-radius: 2px; div { display: flex; margin-left: 20%; src/views/ai/dashboard/components/message/HistoryMessageList.vue
@@ -43,7 +43,7 @@ <div> <el-text class="time">{{ formatDate(item.createTime) }}</el-text> </div> <div class="right-text-container"> <div class="right-text-container question" @click="gotoManual(item)"> <div class="right-text">{{ item.content }}</div> </div> <div class="right-btns"> @@ -71,15 +71,12 @@ import { ArrowDownBold } from '@element-plus/icons-vue' import { ChatMessageApi, ChatMessageVO } from '@/api/ai/chat/message' import { ChatConversationVO } from '@/api/ai/chat/conversation' import { useUserStore } from '@/store/modules/user' import userAvatarDefaultImg from '@/assets/ai/zhuanlu/user.png' import roleAvatarDefaultImg from '@/assets/ai/zhuanlu/assistant.png' const dialogVisible = ref(false) // 弹窗的是否展示 const message = useMessage() // 消息弹窗 const { copy } = useClipboard() // 初始化 copy 到粘贴板 const userStore = useUserStore() // 判断“消息列表”滚动的位置(用于判断是否需要滚动到消息最下方) const messageContainer: any = ref(null) @@ -87,6 +84,7 @@ const userAvatar = computed(() => userAvatarDefaultImg) const roleAvatar = computed(() => props.conversation.roleAvatar ?? roleAvatarDefaultImg) // 定义 props const props = defineProps({ @@ -97,7 +95,9 @@ list: { type: Array as PropType<ChatMessageVO[]>, required: true } }, gotoManualMethod: Function }) const { list } = toRefs(props) // 消息列表 @@ -146,6 +146,12 @@ // ============ 处理消息操作 ============== const gotoManual = async (item: ChatMessageVO) => { if(props.gotoManualMethod) { props.gotoManualMethod(item) } } /** 复制 */ const copyContent = async (content) => { await copy(content) @@ -192,6 +198,11 @@ flex-direction: column; text-align: left; margin: 0 15px; .question:hover { cursor: pointer; background: rgba(40, 139, 255, 0.3); } .time { text-align: left; @@ -282,9 +293,15 @@ bottom: 0; right: 50%; .el-button { background: rgba(255,215,0,0.2); border: solid 1px rgba(255,215,0,0.8); color: rgba(255,215,0,0.8); background: rgba(255,255,255,0.1); border: solid 1px rgba(255,215,0,0.6); color: rgba(255,215,0,0.5); } .el-button:hover { cursor: pointer; background-color: rgba(255,255,255,0.4); border: solid 2px rgba(255,215,0); color: rgba(255,215,0); } } </style> src/views/ai/dashboard/components/message/MessageList.vue
@@ -240,7 +240,7 @@ overflow-wrap: break-word; background: rgba(115,196,255,0); border-radius: 4px 4px 4px 4px; padding: 20px 10px 5px 0; padding: 0 10px 0 0; .left-text { color: rgba(219,238,255,0.8); font-size: 1rem; @@ -295,11 +295,23 @@ } } // 回到底部 .to-bottom { position: absolute; z-index: 1000; bottom: 0; right: 50%; .el-button { background: rgba(255, 255, 255, 0.1); border: solid 1px rgba(255, 215, 0, 0.6); color: rgba(255, 215, 0, 0.5); } .el-button:hover { cursor: pointer; background-color: rgba(255, 255, 255, 0.4); border: solid 2px rgba(255, 215, 0); color: rgba(255, 215, 0); } } </style> src/views/ai/dashboard/zhuanlu/index.vue
@@ -62,7 +62,7 @@ <div class="gas-scheduling-center"> <div class="mode-switch"> <el-radio-group v-model="tabPosition" class="custom-radio-group"> <el-radio-group v-model="tabPosition" @change="handleChange" class="custom-radio-group"> <el-radio-button label="model">大模型模式</el-radio-button> <el-radio-button label="conversation">对话模式</el-radio-button> </el-radio-group> @@ -164,12 +164,15 @@ <!-- 历史建议 --> <HistoryMessageDialog ref="historyMessageRef" :conversation="activeConversation" :parentMethod="queryHistoryMessage" @gotoManualMethod="gotoManual" /> </div> <div v-else> <NormalConversation /> <NormalConversation :data="defaultMessage" /> </div> </div> @@ -265,6 +268,7 @@ import {round} from "lodash-es"; import {ArrowUpBold} from "@element-plus/icons-vue"; import * as authUtil from "@/utils/auth"; import HistoryMessageList from "@/views/ai/dashboard/components/message/HistoryMessageList.vue"; const mqhsList = ref([ { @@ -548,6 +552,7 @@ const messageRef = ref() const activeMessageList = ref<ChatMessageVO[]>([]) // 选中对话的消息列表 const activeHistoryMessageList = ref<ChatMessageVO[]>([]) // 历史建议列表 const activeHistoryMessageTotal = ref(0) // 历史建议总数 const activeMessageListLoading = ref<boolean>(false) // activeMessageList 是否正在加载中 const activeMessageListLoadingTimer = ref<any>() // activeMessageListLoading Timer 定时器。如果加载速度很快,就不进入加载中 // 消息滚动 @@ -573,8 +578,30 @@ const historyMessageRef = ref() const openHistoryMessage = async () => { // 刷新 message 列表 await getHistoryMessageList() historyMessageRef.value.open(activeHistoryMessageList.value, activeConversation.value) let resDate = await historyMessageRef.value.dealDate() await getHistoryMessageList(resDate) historyMessageRef.value.open(activeHistoryMessageList.value, activeConversation.value, activeHistoryMessageTotal.value) } const queryHistoryMessage = async (queryParams: ChatMessageVO) => { return await getHistoryMessageList(queryParams) } //切换对话模式判断 const handleChange = async () => { // 对话进行中,不允许切换 if (conversationInProgress.value) { message.alert('对话中,不允许切换!') return false } } // 默认选中消息 const defaultMessage = ref<ChatMessageVO>() const gotoManual = async (item: ChatMessageVO) => { defaultMessage.value = item tabPosition.value = 'conversation' } // =========== 【聊天对话】相关 =========== @@ -671,22 +698,23 @@ } /** 获取消息 message 列表 */ const getHistoryMessageList = async () => { const getHistoryMessageList = async (params: any) => { if (activeConversationId.value === null) { return } params.conversationId = activeConversationId.value // 获取消息列表 activeHistoryMessageList.value = await ChatMessageApi.getChatMessageListByConversationId( activeConversationId.value ) if (activeHistoryMessageList.value.length > 0) { let pageResult = await ChatMessageApi.getChatMessagePageListByConversationId(params) activeHistoryMessageList.value = pageResult.list activeHistoryMessageTotal.value = pageResult.total if (activeHistoryMessageList.value != null && activeHistoryMessageList.value.length > 0) { activeHistoryMessageList.value.forEach((message: ChatMessageVO) => { if(message.type != 'user') { dealResult(message) } }) return activeHistoryMessageList.value } return pageResult } //处理调度推理结论 const dealResult = (message: any) => { @@ -709,7 +737,6 @@ const messageList = computed(() => { if (activeMessageList.value.length > 0) { activeMessageList.value[1].thinking = dealResultAndData(activeMessageList.value[1].content) console.log(activeMessageList.value) return activeMessageList.value } // 没有消息时,如果有 systemMessage 则展示它 @@ -1143,11 +1170,11 @@ let returnValue = 0; if(type == 'max') { returnValue = computed(() => { return Math.max(...tank) + 20 return Number((Math.max(...tank) + 20).toFixed(0)) }) } else if(type == 'min') { returnValue = computed(() => { return Math.min(...tank) - 60 return Number((Math.min(...tank) - 60).toFixed(0)) }) } else if(type == 'average') { returnValue = computed(() => { @@ -1155,7 +1182,7 @@ tank.forEach((item) => { sum += item[0] }) return (sum / tank.length).toFixed(0); return Number((sum / tank.length).toFixed(0)); }) } return returnValue.value @@ -1582,6 +1609,7 @@ // 清理监听 onUnmounted(() => { console.log('stopStream') const events = ['fullscreenchange', 'webkitfullscreenchange', 'msfullscreenchange']; events.forEach(event => { document.removeEventListener(event, handleFullscreenChange); src/views/ai/model/template/index.vue
@@ -109,9 +109,7 @@ </template> <script lang="ts" setup> import {DICT_TYPE, getIntDictOptions} from '@/utils/dict' import {dateFormatter} from '@/utils/formatTime' import download from '@/utils/download' import { DICT_TYPE } from '@/utils/dict' import * as AiQuestionTemplateApi from '@/api/ai/questiontemplate' import TemplateForm from './templateForm.vue' import * as AiModelApi from "@/api/ai/model/model";