大模型代码对接(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>
学而不思则罔,思而不学则殆!