首页 > 其他分享 >Vue3、typeit、vue3-markdown-it仿文心一言前端代码对接大模型

Vue3、typeit、vue3-markdown-it仿文心一言前端代码对接大模型

时间:2024-03-21 19:14:20浏览次数:16  
标签:chatState flex markdown content width 文心 background Vue3 let

相关依赖

"typeit": "^8.8.3",
"vue3-markdown-it": "^1.0.10",

示例效果

核心代码

<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>
										<!--									<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="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'
// Load left main chat data
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, // 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 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;
		// let targetEle = document.querySelector(`#type-id-${chatState.chatData.length - 1}`)
		// content.scrollTo({
		// 	top: targetEle.offsetTop,
		// 	behavior: 'smooth'
		// })
	}, 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(() => {
				// scroll to bottom
				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>

标签:chatState,flex,markdown,content,width,文心,background,Vue3,let
From: https://www.cnblogs.com/openmind-ink/p/18088058

相关文章

  • 【前端Vue】Vue3+Pinia小兔鲜电商项目第2篇:什么是pinia,1. 创建空Vue项目【附代码文档
    全套笔记资料代码移步:前往gitee仓库查看感兴趣的小伙伴可以自取哦,欢迎大家点赞转发~全套教程部分目录:部分文件图片:什么是piniaPinia是Vue的专属状态管理库,可以实现跨组件或页面共享状态,是vuex状态管理工具的替代品,和Vuex相比,具备以下优势提供更加简单的API(......
  • vue3使用qrcodejs2-fix生成背景透明的二维码
    qrcodejs官方仓库:GitHub-davidshimjs/qrcodejs:Cross-browserQRCodegeneratorforjavascriptqrcodejs2-fix 是一个用于生成QR码的JavaScript库,使用的时候先安装,然后通过设置前景色和背景色可以控制显示的二维码效果。想生成透明背景的二维码也可以,我通过下面配置前景......
  • VUE3学习笔记
    参考链接https://blog.csdn.net/m0_66100833/article/details/134294781生命周期setup():这是一个新的入口点,在beforeCreate和created之前调用onBeforeMount/onMounted:组件挂载前/后的生命周期钩子。onBeforeUpdate/onUpdated:组件更新前/后的生命周期钩子。onBeforeUnmount/onU......
  • Markdown学习
    Markdown学习标题二级标题三级标题字体Hello,WorldHello,WorldHello,WorldHello,World引用引用>+空格分割线图片超链接博客地址列表ABCABC表格名字性别生日测试男1999.10.10代码publichello......
  • vite+vue3+vuex 加密
    1.安装JSEncrypt  npminstalljsencrypt2.加密方法//加密算法import{JSEncrypt}from'jsencrypt';//加密functionencryptText(text){ constpublicKey='MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCh5Nk2GLiyQFMIU+h3OEA4UeFbu3dCH5sjd/sLTxxvwjXq7JLqJbt2rC......
  • 尚硅谷Vue3入门到实战,最新版vue3+TypeScript前端开发教程
    1.创建Vue3工程npmcreatevue@latest或者npminitvue@latest输入项目名和需要的工具后进入项目如果项目报错使用命令安装Node.js的项目依赖包npmi启动vue项目,查看项目是否创建完成npmrundev直接删掉src然后创建src文件夹,在该文件夹中创建main.ts和App.vue文件......
  • VUE3 十种组件通信的方式(附详细代码)
    props用途:可以实现父子组件、子父组件、甚至兄弟组件通信父组件<template><div><Son:money="money"></Son></div></template><scriptsetuplang="ts">importSonfrom'./son.vue'import{re......
  • VUE3 ECharts5 快速上手(附详细步骤)
    安装pnpminstallecharts引入EChartsimport*asechartsfrom'echarts';设置容器注意:虽然echarts可以在配置时设置宽高,但还是推荐在配置前直接为容器设置宽高<template><divid="main"class="echart-style"></div></template><style......
  • pinia——vue3的状态管理工具
    简介Pinia是Vue的专属状态管理库,它允许你跨组件或页面共享状态。主要优点Vue2和Vue3都支持,这让我们同时使用Vue2和Vue3的小伙伴都能很快上手。pinia中只有state、getter、action,抛弃了Vuex中的Mutation,Vuex中mutation一直都不太受小伙伴们的待见,pinia直接抛弃它了,这无疑......
  • VsCode中高效书写Vue3代码的插件
    Vue-Official(原Volar)就是原先的Volar,现已弃用。Vue-Official提供的功能:语法高亮:Vue-Official扩展可以为Vue单文件组件(.vue文件)中的HTML、CSS和JavaScript部分提供语法高亮,使代码更易于阅读和编写。代码片段:Vue-Official扩展提供了丰富的Vue.js相关的......