首页 > 其他分享 >大模型代码对接(fetchEventSource、hightlight.js

大模型代码对接(fetchEventSource、hightlight.js

时间:2024-09-12 20:15:30浏览次数:12  
标签:chatState flex js content item let background fetchEventSource hightlight

<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>

标签:chatState,flex,js,content,item,let,background,fetchEventSource,hightlight
From: https://www.cnblogs.com/openmind-ink/p/18410975

相关文章

  • uniapp vue3使用crypto-js加密解密
    开启crypto-js加密解密的研究历程如何查看crypto-js的版本号?检查crypto-js是否正常我是这样认为的Nativecryptomodulecouldnotbeusedtogetsecurerandomnumber.本机加密模块无法用于获取安全随机数。PC端调试好好的,然后在微信小程序,安卓模拟器,真机调试就......
  • js写法例子记录
    1.前端校验汉字、特殊字符、数字等1.判断字符长度://附言校验varpostscriptBlur=(rule,value,callback)=>{if(value==""||value==null){ callback(newError('必输项不能为空'));}else{ varlen=0; for(vari=0;i<value.length;i++){ //......
  • 面试-JS Web API-JSONP和cors
    JSONP(JSONwithPadding)JSONP是通过<script>标签来实现跨域数据传输的技术。它是为了绕过浏览器的同源策略限制而诞生的。访问一个网址,服务端一定返回一个html文件吗?---不是的服务器可以任意动态拼接数据返回的,只要符合html格式的要求就可以。JSONP的工作原理:客......
  • js | TypeError: Cannot read properties of null (reading ‘indexOf’) 【解决】
    js|TypeError:Cannotreadpropertiesofnull(reading‘indexOf’)【解决】描述概述在前端开发中,遇到TypeError:Cannotreadpropertiesofnull(reading'indexOf')这类错误并不罕见。这个错误通常表明你试图在一个null值上调用indexOf方法,而null是一......
  • 面试-JS Web API
    手写一个简易的Ajax跨域的常用实现方式GET请求//创建一个XMLHttpRequest对象constxhr=newXMLHttpRequest();//初始化一个GET请求//第三个参数true表示异步,一般都为truexhr.open('GET','/data/test.json',true);//设置事件处理函数,当readyState......
  • 一个用于管理多个 Node.js 版本的安装和切换开源工具
    大家好,今天给大家分享一个用于管理多个Node.js版本的工具 NVM(NodeVersionManager),它允许开发者在同一台机器上安装和使用不同版本的Node.js,解决了版本兼容性问题,为开发者提供了极大的便利。在开发环境中,特别是在处理多个项目时,每个项目可能依赖于不同版本的Node.js,NVM提供......
  • Langchain.js如何实现RAG
    前面介绍了Langchain的基本使用方法。仅仅是对GPT方法的封装还不足以让它赢得那么多的Start,以及获得融资。它还有另一个强大的功能-RAG(检索增强生成)。RAG是大模型跟企业内部业务落地的基石。是大模型的北斗导航,可以让大模型的结果更加精准。一、RAG的基本概念与实现流程基于大......
  • 基于Node.js+vue中心医院药品管理系统的设计与实现(开题+程序+论文) 计算机毕业设计
    本系统(程序+源码+数据库+调试部署+开发环境)带文档lw万字以上,文末可获取源码系统程序文件列表开题报告内容研究背景随着医疗技术的不断进步和人们对健康需求的日益增长,中心医院作为医疗服务的重要载体,其运营效率和管理水平直接影响到患者的治疗效果与满意度。药品作为医疗......
  • 基于Node.js+vue基于springboot的影视资讯管理系统(开题+程序+论文) 计算机毕业设计
    本系统(程序+源码+数据库+调试部署+开发环境)带文档lw万字以上,文末可获取源码系统程序文件列表开题报告内容研究背景随着互联网的飞速发展,影视行业迎来了前所未有的繁荣期。海量影视资源的涌现,使得用户对于高效、便捷地获取影视资讯的需求日益增长。传统的影视资讯管理方式......
  • 基于Node.js+vue在线新闻网站系统的设计与实现(开题+程序+论文) 计算机毕业设计
    本系统(程序+源码+数据库+调试部署+开发环境)带文档lw万字以上,文末可获取源码系统程序文件列表开题报告内容研究背景在信息爆炸的时代,新闻资讯的获取与传播速度成为了衡量一个社会信息化水平的重要指标。随着互联网技术的飞速发展,传统新闻媒体逐渐向数字化、网络化转型,在线......