| <template> |
| <a-modal |
| class="modal-container" |
| style="min-width: 1400px;" |
| :visible="modalState.visible" |
| :footer="null" |
| :bodyStyle="{padding: 0, borderRadius: '8px'}" |
| :loading="chatState.spinning" |
| @cancel="() => {modalState.visible = false}" |
| > |
| <template #closeIcon> |
| <CloseOutlined style="position: relative; top: -10px; right: -10px" /> |
| </template> |
| <a-spin :spinning="chatState.spinning"> |
| <main class="chat-box"> |
| <section class="left-chat-title"> |
| <div class="new-chat-box"> |
| <div class="new-chat-btn" @click="onNewChat"> |
| <span> |
| 新建对话 |
| </span> |
| </div> |
| </div> |
| <div class="left-title-box"> |
| <li :class="[menuState.activeKey === item.id ? 'chat-title chat-title-item-active' : 'chat-title' ]" |
| v-for="(item) in menuState.menuData" :key="item.id" @click="onTitleClick(item)"> |
| <i class="chat-title-logo"></i> |
| <span class="chat-title-desc">{{ item.title }}</span> |
| <span class="operate-btn-space" style="flex: 1"> |
| <div style="display: flex; justify-content: flex-end; width: 100%"> |
| <a-popconfirm |
| title="删除后无法恢复,是否继续删除?" |
| ok-text="确认" |
| cancel-text="取消" |
| @confirm="onDeleteMainChat(item)" |
| > |
| <delete-outlined |
| class="icon" |
| @click.stop="(e) => {e.preventDefault()}" |
| /> |
| </a-popconfirm> |
| </div> |
| </span> |
| </li> |
| </div> |
| </section> |
| <section class="right-chat-space"> |
| <section class="list-space"> |
| <div class="question-answer-item" v-if="!chatState.chatData.length"> |
| <div class="answer-item"> |
| <i class="chat-avatar"></i> |
| <div class="answer-content-info"> |
| <p> |
| <h3 style="font-weight: 600; font-size: 20px;margin-bottom: 16px;">你好,我是锐智大模型</h3> |
| <p style="color: #676c90; font-weight: 600">作为您的智能伙伴,非常高兴与您合作!</p> |
| </p> |
| </div> |
| </div> |
| </div> |
| <div class="question-answer-item" v-for="(qa, index) in chatState.chatData" :key="qa.id"> |
| <div class="question-item"> |
| <i class="question-avatar"></i> |
| <div class="question-content"> |
| {{ qa.answerData.desc }} |
| </div> |
| </div> |
| <div class="answer-item"> |
| <div style="height: 100%; display: flex; flex-direction: column; justify-content: flex-start"> |
| |
| <img |
| v-if="chatState.isChatting && index === chatState.chatData.length-1" |
| class="robot" style="height: 32px;width: 32px;" src="@/assets/bigmodel/loading.gif" alt="" /> |
| <i v-else class="chat-avatar"></i> |
| </div> |
| <div class="answer-content-space"> |
| <div class="answer-content"> |
| <div :id="`type-id-${index}`" style="padding: 0"> |
| <MarkdownIt :source="qa.questionData.desc" /> |
| </div> |
| </div> |
| <div> |
| <a-button type="link" style="font-size: 12px; font-weight: 400" |
| @click="onStopChat" |
| v-if="typeInstanceStatus === 'start' |
| && index === chatState.chatData.length - 1 |
| " |
| >停止生成 |
| </a-button> |
| |
| |
| |
| |
| |
| |
| </div> |
| </div> |
| <div |
| style="display: flex; flex-direction: column; justify-content: flex-start; height: 100%; position: relative; top: 10px;"> |
| <a-popconfirm |
| title="确认删除该对话?" |
| ok-text="确认" |
| cancel-text="取消" |
| @confirm="onDeleteSubChat(qa)" |
| > |
| <delete-outlined |
| class="icon" |
| @click.stop="(e) => {e.preventDefault()}" |
| /> |
| </a-popconfirm> |
| </div> |
| </div> |
| |
| </div> |
| </section> |
| <section class="foot-space" style="margin-top: 20px"> |
| <div class="input-wrapper"> |
| <a-textarea |
| class="chat-input-box" |
| placeholder="在此输入您想了解的内容" |
| :autosize="{ minRows: 6, maxRows: 5 }" |
| v-model:value="chatState.chatContent" |
| @pressEnter="onAnswerSend" |
| > |
| </a-textarea> |
| <a-button :loading="chatState.isChatting" class="enter-btn" type="primary" @click="onAnswerSend">发送 |
| </a-button> |
| </div> |
| </section> |
| </section> |
| |
| </main> |
| </a-spin> |
| </a-modal> |
| |
| </template> |
| |
| <script setup> |
| import { reactive, ref } from 'vue' |
| import TypeIt from 'typeit' |
| import { message } from 'ant-design-vue' |
| import { isEmpty } from 'lodash' |
| import { deleteMainChat, deleteSubChat, getCurChatData, listChatData, sendQuestion } from '@/api/bigmodel' |
| import { useResHandle } from '@/hooks/useResHandle' |
| import { CloseOutlined, DeleteOutlined } from '@ant-design/icons-vue' |
| import MarkdownIt from 'vue3-markdown-it' |
| |
| const loadLeftData = async () => { |
| const response = await listChatData() |
| let param = { |
| response, |
| successInfo: '', |
| failInfo: '数据请求失败', |
| callback: () => { |
| menuState.menuData = response.data.data |
| } |
| } |
| useResHandle(param) |
| } |
| |
| const menuState = reactive({ |
| activeKey: undefined, |
| menuData: [] |
| }) |
| const onNewChat = () => { |
| menuState.activeKey = undefined |
| chatState.chatData = [] |
| chatState.chatId = undefined |
| chatState.isChatting = false |
| } |
| |
| const modalState = reactive({ |
| visible: false |
| }) |
| |
| const chatState = reactive({ |
| isChatting: false, |
| chatContent: undefined, |
| chatId: undefined, |
| chatData: [], |
| spinning: false |
| }) |
| let typeInstance = ref(null) |
| let typeInstanceStatus = ref('') |
| const onTitleClick = async ({ id }) => { |
| menuState.activeKey = id |
| chatState.chatId = id |
| chatState.spinning = true |
| |
| const response = await getCurChatData({ chatId: id }) |
| let param = { |
| response, |
| successInfo: '', |
| failInfo: '', |
| callback: () => { |
| chatState.spinning = false |
| chatState.isChatting = false |
| chatState.chatData = response.data.data.map(res => { |
| return { |
| subChatId: res.id, |
| createTime: res.createTime, |
| questionData: { |
| desc: res.chatResponse |
| }, |
| answerData: { |
| desc: res.chatRequest |
| } |
| } |
| }) |
| } |
| } |
| useResHandle(param) |
| } |
| |
| const onAnswerSend = async () => { |
| let flag = checkChatContent() |
| if (!flag) { |
| return |
| } |
| chatState.isChatting = true |
| console.log('output-> chatState.chatContent::: ', chatState.chatContent) |
| let curChatContent = chatState.chatContent |
| chatState.chatData.push({ |
| answerData: { |
| desc: curChatContent |
| }, |
| questionData: { |
| desc: '' |
| } |
| }) |
| setTimeout(() => { |
| let content = document.querySelector('.list-space') |
| |
| content.scrollTop = content.scrollHeight; |
| |
| |
| |
| |
| |
| }, 0) |
| if (!chatState.chatId) { |
| menuState.menuData.unshift({ title: curChatContent }) |
| } |
| chatState.chatContent = undefined |
| let payload = { |
| chatRequest: curChatContent, |
| chatId: chatState.chatId |
| } |
| const response = await sendQuestion(payload) |
| let param = { |
| response, |
| successInfo: '', |
| failInfo: '', |
| callback: () => { |
| loadLeftData() |
| let resData = response.data.data |
| chatState.chatId = resData.chatId |
| chatState.chatData = chatState.chatData.map(item => { |
| if (item.answerData.desc === curChatContent) { |
| item.questionData.desc = resData.chatResponse |
| item.subChatId = resData.chatInfoId |
| item.chatId = resData.chatId |
| } |
| return item |
| }) |
| setTimeout(() => { |
| |
| let content = document.querySelector('.list-space') |
| content.scrollTop = content.scrollHeight; |
| }, 0) |
| setTimeout(() => { |
| typeInstance.value = new TypeIt(`#type-id-${chatState.chatData.length - 1}`, { |
| speed: .5, |
| lifeLike: true, |
| breakLines: true, |
| loop: false, |
| html: true, |
| beforeStep: async (instance) => { |
| chatState.isChatting = true |
| typeInstanceStatus.value = 'start' |
| }, |
| beforeString: async (characters, instance) => { |
| let content = document.querySelector('.list-space') |
| content.scrollTop = content.scrollHeight; |
| }, |
| afterStep: async (instance) => { |
| let content = document.querySelector('.list-space') |
| content.scrollTop = content.scrollHeight; |
| }, |
| afterString: async (characters, instance) => { |
| let content = document.querySelector('.list-space') |
| content.scrollTop = content.scrollHeight; |
| }, |
| afterComplete: async (instance) => { |
| chatState.isChatting = false |
| typeInstanceStatus.value = 'end' |
| instance.cursor.style = 'font-size: 0' |
| let content = document.querySelector('.list-space') |
| content.scrollTop = content.scrollHeight; |
| } |
| }).go() |
| console.log('output-> typeInstance::: ', typeInstance.value) |
| }, 0) |
| |
| } |
| } |
| useResHandle(param) |
| |
| } |
| const checkChatContent = () => { |
| console.log('output-> chatState.chatContent::: ', chatState.chatContent) |
| if (!chatState.chatContent || isEmpty(chatState.chatContent.replace(/^\s*|\s*$/g, ''))) { |
| message.warn('请输入对话内容哦~') |
| chatState.chatContent = '' |
| return false |
| } |
| if (!chatState.isChatting) { |
| chatState.isChatting = true |
| return true |
| } else { |
| message.warn('请等机器人回复后再发送哦~') |
| return false |
| } |
| } |
| const onStopChat = () => { |
| if (typeInstance.value) { |
| chatState.isChatting = false |
| typeInstance.value.freeze() |
| } |
| } |
| |
| const onDeleteMainChat = async (item) => { |
| if(!item.id) { |
| return; |
| } |
| |
| chatState.spinning = true |
| const response = await deleteMainChat({ id: item.id }) |
| let param = { |
| response, |
| successInfo: '删除成功', |
| failInfo: '删除失败', |
| callback: () => { |
| chatState.spinning = false |
| if(item.id === chatState.chatId) { |
| chatState.chatData = [] |
| } |
| } |
| } |
| useResHandle(param) |
| if (response.data.status === 200) { |
| await loadLeftData() |
| } |
| } |
| |
| const onDeleteSubChat = async (qa) => { |
| console.log('output-> qa', qa) |
| const response = await deleteSubChat({ id: qa.subChatId }) |
| let param = { |
| response, |
| successInfo: '删除成功', |
| failInfo: '删除失败', |
| callback: () => { |
| onTitleClick({ id: chatState.chatId }) |
| } |
| } |
| useResHandle(param) |
| } |
| |
| defineExpose({ |
| modalState, |
| loadLeftData |
| }) |
| |
| </script> |
| |
| <style lang="scss" scoped> |
| |
| .chat-box { |
| display: flex; |
| height: 800px; |
| min-height: calc(100vh - 500px); |
| |
| .left-chat-title { |
| background-color: #efecf7; |
| padding: 16px 16px 16px; |
| width: 20%; |
| overflow: hidden; |
| |
| .new-chat-box { |
| width: 100%; |
| display: flex; |
| justify-content: flex-start; |
| margin-bottom: 20px; |
| |
| .new-chat-btn { |
| background-image: linear-gradient(-20deg, #b721ff 0%, #21d4fd 100%); |
| position: relative; |
| padding: 1px; |
| display: inline-block; |
| border-radius: 7px; |
| |
| span { |
| display: inline-block; |
| background: #efecf7; |
| color: #5964f5; |
| text-transform: uppercase; |
| padding: 12px 50px; |
| border-radius: 5px; |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; |
| font-size: 14px; |
| } |
| } |
| |
| .new-chat-btn:hover { |
| cursor: pointer; |
| |
| span { |
| color: white; |
| background-image: linear-gradient(-20deg, #b721ff 0%, #21d4fd 100%); |
| } |
| |
| background: transparent; |
| } |
| |
| } |
| |
| .chat-title { |
| display: flex; |
| align-items: center; |
| padding: 16px 16px 16px; |
| border-radius: 8px; |
| |
| .operate-btn-space { |
| display: none; |
| } |
| |
| .chat-title-logo { |
| margin-right: 6px; |
| height: 18px; |
| width: 18px; |
| display: inline-block; |
| background-image: url('@/assets/bigmodel/chat-title.svg'); |
| background-repeat: no-repeat; |
| background-size: 100%; |
| } |
| |
| .chat-title-desc { |
| max-width: 150px; |
| white-space: nowrap; |
| overflow: hidden; |
| text-overflow: ellipsis; |
| } |
| |
| &:hover { |
| cursor: pointer; |
| background-color: #ffffff; |
| |
| .operate-btn-space { |
| display: inline; |
| } |
| } |
| } |
| |
| .chat-title-item-active { |
| background-color: #fff; |
| } |
| } |
| |
| .right-chat-space { |
| overflow: auto; |
| background-color: #f1f2f6; |
| flex: 1; |
| display: flex; |
| flex-direction: column; |
| justify-content: space-between; |
| |
| .list-space { |
| overflow: auto; |
| flex: 1; |
| width: 100%; |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| |
| .question-answer-item { |
| padding: 8px 2px 24px; |
| width: 68%; |
| display: flex; |
| flex-direction: column; |
| |
| .question-item { |
| width: 100%; |
| display: flex; |
| align-items: center; |
| |
| .question-avatar { |
| height: 32px; |
| width: 32px; |
| display: inline-block; |
| background-image: url('@/assets/bigmodel/user.svg'); |
| background-repeat: no-repeat; |
| background-size: 100%; |
| } |
| |
| .question-content { |
| flex: 1; |
| padding: 0 16px 0px; |
| max-width: 600px; |
| white-space: nowrap; |
| overflow: hidden; |
| text-overflow: ellipsis; |
| } |
| |
| margin-bottom: 12px; |
| } |
| |
| .answer-content-info { |
| margin-top: 8px; |
| margin-left: 8px; |
| width: 100%; |
| padding: 16px 16px 24px; |
| background-color: #fefefe; |
| border-radius: 8px 8px; |
| } |
| |
| .answer-item { |
| display: flex; |
| align-items: center; |
| |
| .chat-avatar { |
| height: 32px; |
| width: 32px; |
| display: inline-block; |
| background-image: url('@/assets/bigmodel/robot.png'); |
| background-repeat: no-repeat; |
| background-size: 100%; |
| } |
| |
| .answer-content-space { |
| flex: 1; |
| display: flex; |
| flex-direction: column; |
| |
| .answer-content { |
| margin-top: 8px; |
| margin-left: 8px; |
| width: 98%; |
| padding: 16px 16px 24px; |
| background-color: #fefefe; |
| border-radius: 8px 8px; |
| line-height: 25px; |
| } |
| } |
| } |
| } |
| } |
| |
| .foot-space { |
| width: 100%; |
| height: 210px; |
| display: flex; |
| justify-content: center; |
| |
| .input-wrapper { |
| width: 61%; |
| height: 80%; |
| position: relative; |
| left: 2px; |
| |
| .chat-input-box { |
| border-radius: 8px; |
| font-weight: 600; |
| font-size: 14px; |
| opacity: .8; |
| width: 100%; |
| height: 100%; |
| } |
| |
| .enter-btn { |
| position: absolute; |
| bottom: 35px; |
| right: 10px; |
| } |
| } |
| |
| } |
| } |
| } |
| |
| :deep(.ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected) { |
| background-color: white; |
| color: #4361ee; |
| } |
| |
| :deep(.ant-menu.ant-menu-inline .ant-menu-item, .ant-menu.ant-menu-inline .ant-menu-submenu-title) { |
| width: 98%; |
| } |
| |
| .chat-box { |
| max-height: 800px; |
| overflow: auto; |
| } |
| |
| .left-title-box { |
| overflow: auto; |
| height: 100%; |
| |
| &::-webkit-scrollbar { |
| width: 6px; |
| height: 1px; |
| } |
| |
| &::-webkit-scrollbar-thumb { |
| border-radius: 6px; |
| // -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2); |
| background: rgba(144, 147, 153, 0.5); |
| } |
| |
| &::-webkit-scrollbar-track { |
| // -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2); |
| border-radius: 5px; |
| background: transparent; |
| } |
| } |
| |
| .list-space { |
| &::-webkit-scrollbar { |
| width: 6px; |
| height: 1px; |
| } |
| |
| &::-webkit-scrollbar-thumb { |
| border-radius: 6px; |
| // -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2); |
| background: rgba(144, 147, 153, 0.5); |
| } |
| |
| &::-webkit-scrollbar-track { |
| // -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2); |
| border-radius: 5px; |
| background: transparent; |
| } |
| } |
| </style> |
| |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具
2023-03-21 对象数组相对于另一个数组进行过滤!
2023-03-21 如何一次将样式应用于多个类?
2021-03-21 SpringBoot集成Thymeleaf时遇到的坑🕳