大模型代码对接(fetchEventSource、hightlight.js
<template> <a-modal class="modal-container" style="min-width: 1180px;" :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 v-for="(item) in menuState.menuData" :key="item.id" :class="[menuState.activeKey === item.id ? 'chat-title chat-title-item-active' : 'chat-title' ]" @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="color: #fff; font-weight: 600; font-size: 20px;margin-bottom: 16px;">你好,我是锐智大模型</h3> <p style="color: #E0E0E0; 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> <!-- <a-button type="link" style="font-size: 12px; font-weight: 400"--> <!-- @click="onStopChat"--> <!-- v-if="--> <!-- typeInstanceStatus === 'end' && 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.stop="onAnswerSend">发送 </a-button> </div> </section> </section> </main> </a-spin> </a-modal> </template> <script setup> import { onMounted, onUnmounted, 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' import { fetchEventSource } from '@microsoft/fetch-event-source' onUnmounted(() => { }); // 页面加载时 onMounted(() => { loadLeftData() }); // Load left main chat data const loadLeftData = async () => { chatState.spinning = true const response = await listChatData() let param = { response, successInfo: '', failInfo: '数据请求失败', callback: () => { chatState.spinning = false 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, // the big model is chatting or not 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 // load right data by main id 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 controller = new AbortController(); const signal = controller.signal; const onAnswerSend = () => { // 聊天框内容列表 let chatData = [] 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: '' } }) if (!chatState.chatId) { menuState.menuData.unshift({ title: curChatContent }) } chatState.chatContent = undefined // ======================对话请求=================== let reqUrl = '/streamChat/v2/chat/completions' // let reqUrl = 'http://10.2.164.106:8085/v2/chat/completions' let reqData = { messages: [ { role: 'user', content: curChatContent } ], model: "ayenaspring-advanced-001", stream: true, } let eventSource = fetchEventSource(reqUrl, { signal, openWhenHidden: true, method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer D3A93A0F076AAE0A9548BFED152CD4BF', 'Accept': '*/*' }, body: JSON.stringify(reqData), async onmessage(evt) { console.log('output-> evt::: ', evt) let resData = JSON.parse(evt.data) console.log('output-> resData', resData) let resChoices = resData.choices[0] console.log('output-> resChoices?.finish_reason::: ', resChoices?.finish_reason) if (resChoices?.finish_reason === 'stop' || resChoices.delta?.content === '[]') { chatState.isChatting = false let payload = { chatRequest: curChatContent, chatId: chatState.chatId, chatResponse: chatData.join() } const response = await sendQuestion(payload) let resData = response.data.data chatState.chatData.map(item => { if (item.answerData.desc === curChatContent) { item.subChatId = resData.chatInfoId item.chatId = resData.chatId if(isEmpty(menuState.menuData[0]?.id)) { menuState.menuData[0].id = resData.chatId chatState.chatId = resData.chatId menuState.activeKey = resData.chatId } } return item }) return } let streamContent = resChoices.delta.content console.log('output-> streamContent::: ', streamContent) let perStreamContents = streamContent.split('') if(!isEmpty(perStreamContents)) { perStreamContents.forEach(char => { setTimeout(() => { chatData = [ ...chatData, char, ] chatState.chatData = chatState.chatData.map(item => { if (item.answerData.desc === curChatContent) { item.questionData.desc = chatData.join('') } return item }) }, 200) }) } setTimeout(() => { let content = document.querySelector('.list-space') // scroll to bottom content.scrollTop = content.scrollHeight; }, 0) // setTimeout(() => { // typeInstance.value = new TypeIt(`#type-id-${chatState.chatData.length - 1}`, { // strings: streamContent.split(''), // speed: .5, // lifeLike: true, // breakLines: true, // 控制是将多个字符串打印在彼此之上,还是删除这些字符串并相互替换 // loop: false, // html: true, // beforeStep: async (instance) => { // 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) => { // 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) }, onerror() { controller.abort() } }) } 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 = () => { controller.abort(); 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: #374A60; 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: 16px; } } .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 { color: #E0E0E0; max-width: 150px; white-space: nowrap; /* 防止文本换行 */ overflow: hidden; /* 隐藏超出部分 */ text-overflow: ellipsis; /* 显示省略号 */ } &:hover { cursor: pointer; background-color: #3B5468; .operate-btn-space { display: inline; } } } .chat-title-item-active { background-color: #3B5468; } } .right-chat-space { overflow: auto; background-color: #293246; 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: #374A60; 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: #374A60; 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: 1px solid #575e73; 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生成工具