uniapp h5项目AI会话聊天功能的实现

        最近公司h5项目需要做一个简单的ai聊天功能,用的智普ai。实现过程是将问题发给自家后端,后端返回一个答案获取接口,前端再调用答案接口,获取流式答案,并以类似打字的方式显示到聊天窗口中。

        聊天窗口代码参考的文章1,根据自己的需求稍稍做了调整;流式数据获取参考的文章2,主要是用到了fecthEventSource。感谢两位大神!

        AI接口文档地址:Docs

参考文章:

1、前端对接AI模型接口 实现ChatGPT流式数据并【实现打字效果】和【终止会话】_event:conversation.message.delta-CSDN博客

2、基于uniapp开发的智能客服(接入api,附完整代码)_uni-app 智能客服-CSDN博客 

         上代码:

<template>
	<div class="main">
		<div class="box">
			<div style=" text-align: center;" class="title">
				<!-- <img src="" alt class="logo" /> -->
				<!-- <span style="font-size: 40rpx; text-align: center;" class="title-hn">智能客服</span> -->
			</div>

			<div id="content">
				<!-- <div class="history_button" style="background-color: #f5f5f5;" v-if="isShow">
				<button style=" text-align: center; color: #bcbcbc; background-color: #f5f5f5;" @click="getHistory">查看历史记录</button>
			</div> -->
				<div v-for="(item,index) in info" :key="index">
					<div class="info_r info_default" v-if="item.type == 'leftinfo'">
						<!-- <span class="circle circle_r">
				  <img src class="pic_r" src="/static/images/kefu2.png"></img>
			  </span> -->
						<!-- <div class="time_r">{{item.time}}</div> -->

						<div class="con_r con_text">
							<div v-html="formatContent(item.content)"
							></div>
						</div>
					</div>

					<div class="info_l" v-if="item.type == 'rightinfo'">
						<!-- <div class="time_l">{{item.time}}</div> -->
						<div class="con_r con_text">
							<span class="con_l">{{item.content}}</span>
						</div>
					</div>
				</div>
			</div>

			<div class="setproblem">
				<!-- <textarea
					placeholder="请输入您的问题..." id="text"
					v-model="customerText" @keyup.enter="sentMsg()"
					@focus="scrollToView"
				></textarea> -->
				<input placeholder="请输入..." id="text"
					v-model="customerText"
					@focus="scrollToView"
				/>
				<button @click="sentMsg()" class="setproblems"
					style="background-color: #CC4A2D;"
				>
					<span style="vertical-align: 8px;font-size: 30rpx;">
						发 送
					</span>
				</button>
			</div>
		</div>
	</div>

</template>

<script>
	// 自家后端的接口,发送问题,返回ai答案获取接口
	import {
		getSign
	} from '@/api/aiChat/aiChat.js';
	// 安装fetch-event-source后引入,通过fetch-event-source获取流式答案数据
	import {
		fetchEventSource
	} from '@microsoft/fetch-event-source'
	
	export default {
		name: "onlineCustomer",
		components: {},
		computed: {},
		data() {
			return {
				userId: null,
				customerText: "",
				isShow: true,
				Sending: true,
				getHistoryId: "",
				info: [{
					type: "leftinfo",
					time: this.getTodayTime(),
					name: "robot",
					content: "你好,这里是智能匹配",
					question: [],
				}, ],
				timer: null,
				keyboardHeight: 0,

			};
		},
		created() {
			this.showTimer();
		},

		onLoad() {
			this.GetRequest();
		},
		mounted() {
			window.addEventListener('resize', this.handleResize);
		},
		beforeDestroy() {
			uni.offKeyboardHeightChange();
			window.removeEventListener('resize', this.handleResize);
		},
		watch: {
			keyboardHeight(newHeight) {
				this.onKeyboardShow(300);
				if (newHeight > 0) {
					// this.onKeyboardShow(230);
				} else {
					this.onKeyboardHide();
				}
			}
		},
		methods: {
			///换行符、加粗符更换为适配html
			formatContent(content) {
				// 处理换行符
				let formattedContent = content.replace(/\n/g, '<br>');
				// 处理加粗
				formattedContent = formattedContent.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
				return formattedContent;
			},

			handleResize() {
				this.keyboardHeight = 0; // 重置键盘高度
			},
			onKeyboardShow(height) {
				this.$nextTick(() => {
					const content = document.getElementById('content');
					content.style.paddingBottom = `${height}rpx`;
					content.scrollTop = content.scrollHeight;

				});
			},
			onKeyboardHide() {
				this.$nextTick(() => {
					const content = document.getElementById('content');
					content.style.paddingBottom = '0px';
				});
			},
			scrollToView() {
				this.$nextTick(() => {
					document.getElementById('text').scrollIntoView({
						behavior: 'smooth',
						block: 'center'
					});
				});
			},

			//获取历史数据
			getHistory() {
				var getHistory = uni.getStorageSync(this.getHistoryId);
				if (!getHistory) {
					uni.showToast({
						title: '暂无历史记录', 
						icon: 'none',
						duration: 2000
					});
					return;
				}
				console.log('获取历史数据');
				this.info = [];

				console.log(getHistory);
				var i = 0;
				for (i; i < getHistory.length; i++) {

					this.info.push(getHistory[i]);
				}
				// console.log(getHistory);
				this.isShow = false;
			},

			// 用户发送消息
			sentMsg() {
				clearTimeout(this.timer);
				this.showTimer();
				let text = this.customerText.trim();
				if (!this.Sending) {
					uni.showToast({
						title: '稍等片刻~',
						icon: 'none', 
						duration: 2000
					});
					return;
				}
				if (text != "") {
					var obj = {
						type: "rightinfo",
						time: this.getTodayTime(),
						content: text,
					};
					this.info.push(obj);

					this.saveChatRecord(obj);

					this.Sending = false; // 不能两个消息同时发
					this.appendRobotMsg(this.customerText);
					this.customerText = "";
					this.$nextTick(() => {
						var contentHeight = document.getElementById("content");
						contentHeight.scrollTop = contentHeight.scrollHeight;
					});
				}
			},
			// 机器人回答消息
			async appendRobotMsg(text) {
				clearTimeout(this.timer);
				this.showTimer();
				text = text.trim();
				
				// 获取ai答案接口
				var data = {
					question: text
				}
				const answerUrl = await getSign(data)
				
				let obj2 = {
					type: "leftinfo",
					time: this.getTodayTime(),
					name: "robot",
					content: '正在输入中...',
					question: [],
				};
				
				this.info.push(obj2);
				this.$nextTick(() => {			//滚动聊天到底部
				  var contentHeight = document.getElementById("content");
				  contentHeight.scrollTop = contentHeight.scrollHeight;
				});
				let abortController = new AbortController()
				
				// 存获取到的流式数据中要显示的内容
				let answerArr = []
				
				const that = this
				fetchEventSource(answerUrl, {
					method: 'post',
					headers: {
						Accept: 'text/event-stream',
						Authorization: 'Bearer <你的apikey>' // 智谱AI的key,后端提供
					},
					signal: abortController.signal,
					async onopen(response) {
						//建立连接的回调
						console.log('建立连接的回调')
					},
					onmessage(msg) {
						// console.log("流式数据", msg);
						//接收一次数据段时回调,因为是流式返回,所以这个回调会被调用多次
						if (msg.event == 'add' && JSON.parse(msg.data).msg) {
							//进行连接正常的操作
							try {
								const dataObj = JSON.parse(msg.data).msg
								
								// 存获取到的流式数据中要显示的内容
								answerArr.push(dataObj)
								
							} catch (e) {}
						}
					},
					onclose() {
						// 流式数据获取结束后的操作
						
						// that.info[that.info.length - 1].content = answerArr.join('')
						
						// 将获取到的流式数据要显示的内容以类似打字的方式展示到聊天窗口中
						let index = 0;
						let combinedString = '';
						const intervalId = setInterval(() => {
						  // 检查是否还有未处理的数组项
						  if (index < answerArr.length) {
						    // 将当前项添加到组合字符串中
						    combinedString += answerArr[index];
						    // 更新字符串a
						    that.info[that.info.length - 1].content = combinedString;
							that.$nextTick(() => { //滚动聊天到底部
								var contentHeight = document.getElementById("content");
								contentHeight.scrollTop = contentHeight.scrollHeight;
							});
						    index++;
						  } else {
                              // 答案都显示后,才能再次提问
							  that.Sending = true;
						    // 如果所有项都已处理完毕,清除定时器
						    clearInterval(intervalId);
						  }
						}, 200);
						

						that.$nextTick(() => { //滚动聊天到底部
							var contentHeight = document.getElementById("content");
							contentHeight.scrollTop = contentHeight.scrollHeight;
						});
					},
					onerror(err) {
						//连接出现异常回调
						// 取消请求
						throw err
					}
				})
			},

			saveChatRecord(newChatRecord) {
				// 获取当前存储的聊天记录列表,如果不存在则初始化为空数组
				let chatRecords = uni.getStorageSync(this.getHistoryId);

				// 如果不存在,则初始化为空数组
				if (!Array.isArray(chatRecords)) {
					chatRecords = [];
				}

				// 将新的聊天记录对象追加到聊天记录列表中
				chatRecords.push(newChatRecord);

				// 将更新后的聊天记录列表保存回本地存储
				uni.setStorageSync(this.getHistoryId, chatRecords);
			},
			// 结束语
			endMsg() {
				let happyEnding = {
					type: "leftinfo",
					time: this.getTodayTime(),
					content: "感谢您使用智能匹配,祝您生活愉快",
					question: [],
				};
				this.info.push(happyEnding);
				this.$nextTick(() => {
					var contentHeight = document.getElementById("content");
					contentHeight.scrollTop = contentHeight.scrollHeight;
				});

			},
			showTimer() {
				// this.timer = setTimeout(this.endMsg, 1000 * 180);
			},
			getTodayTime() {
				// 获取当前时间
				var day = new Date();
				let seconds = day.getSeconds();
				if (seconds < 10) {
					seconds = "0" + seconds;
				} else {
					seconds = seconds;
				}
				let minutes = day.getMinutes();
				if (minutes < 10) {
					minutes = "0" + minutes;
				} else {
					minutes = minutes;
				}
				let time =
					day.getFullYear() +
					"-" +
					(day.getMonth() + 1) +
					"-" +
					day.getDate() +
					" " +
					day.getHours() +
					":" +
					minutes +
					":" +
					seconds;
				return time;
			},

			GetRequest() {
				var url = location.search; //获取url中"?"符后的字串  
				console.log(url);

				var theRequest = new Object();
				if (url.indexOf("?") != -1) {
					var str = url.substr(1); // 去掉"?"前缀
					var params = str.split("&"); // 按照"&"分割成数组
					for (var i = 0; i < params.length; i++) {
						var keyValue = params[i].split("="); // 按照"="分割成键和值
						var key = keyValue[0]; // 键
						var value = keyValue[1]; // 值
						theRequest[key] = decodeURIComponent(value); // 将键和值存入对象
					}
				}
				// console.log(theRequest.userId);
				// console.log(typeof this.userId);		//object
				// this.userId = String(theRequest.userId);
				// this.getHistoryId = 'getHistory_' + this.userId;
				// console.log(typeof this.userId);		//String
				//  return theRequest["userId"];

			}
		},
		props: {},
		destroyed() {},
	};
</script>
<style lang="scss" scoped>
	// html, body {
	//   height: 100%;
	//   margin: 0;
	//   padding: 0;
	// }
	body {
		-webkit-user-select: text;
		/* 兼容Webkit内核浏览器 */
		-moz-user-select: text;
		/* 兼容Firefox */
		-ms-user-select: text;
		/* 兼容IE */
		user-select: auto;
		/* 允许文本选择 */
	}

	.main {
		width: 100%;
		height: 90%;
		// background: linear-gradient(
		//     180deg,
		//     rgba(149, 179, 212, 1) 0%,
		//     rgba(74, 131, 194, 1) 100%
		// );
		background: #f5f5f5;
		overflow: hidden;

		// .contentoy {
		// 	width: 100%;
		// 	height: 20%;
		// 	// background-size: 100% auto;
		// 	// padding: 0;
		// }
		.box {
			width: 100%;
			max-width: 800rpx;
			/* width: 680px; */
			height: 97%;
			background-color: #fafafa;
			position: fixed;
			padding-left: 20rpx;
			padding-right: 20rpx;
			// padding: 1.25rem;

			#content {
				// height: calc(100% - 50rpx);
				height: 88%;
				overflow-y: scroll;
				font-size: 16px;
				width: 95%;
				-webkit-user-select: auto;

				// box-sizing: border-box; /* 确保 padding 和 border 在内容区内 */
				.circle {
					display: inline-block;
					width: 40px;
					height: 40px;
					border-radius: 50%;
				}

				.con_text {
					color: #333;
					margin-bottom: 5px;
					margin-left: 5px;
					word-break: break-all;
					/* 自动换行 */
					overflow-wrap: break-word;
					/* 避免长单词超出容器 */
					user-select: auto;
					/* 确保对话框文本可选择 */
				}

				.con_que {
					color: #1c88ff;
					height: 30px;
					line-height: 30px;
					cursor: pointer;
				}

				.info_r {
					position: relative;
					margin-top: 10px;

					.circle_r {
						position: absolute;
						left: 0%;
					}

					.pic_r {
						border-radius: 2px;
						width: 40px;
						height: 40px;
						margin: 2px;
					}

					.con_r {
						display: inline-block;
						/* max-width: 253px; */
						max-width: 70%;
						/* 限制对话框最大宽度 */
						min-width: 30px;
						/* 限制对话框最小宽度 */
						min-height: 40px;
						/* min-height: 20px; */
						background-color: #e2e2e2;
						border-radius: 6px;
						padding: 10px;
						// margin-left: 50px;
					}

					.time_r {
						margin-left: 10px;
						color: #999999;
						font-size: 12px;
					}
				}

				.info_l {
					text-align: right;

					color: #ffffff;
					color: #3163C5;
					margin-top: 10px;


					.pic_l {
						width: 13px;
						height: 17px;
						margin: 8px 10px;
					}

					.time_l {
						margin-right: 5px;
						color: #999999;
						font-size: 12px;
						margin-top: 5px;
					}

					.con_l {
						display: inline-block;

						max-width: 70%;
						/* 限制对话框最大宽度 */
						min-width: 30px;
						/* 限制对话框最小宽度 */

						background-color: #CC4A2D;
						border-radius: 6px;
						padding: 10px;
						text-align: left;
						color: #fff;
						margin-right: 5px;
						word-break: break-word;
						/* 确保长单词换行 */
					}
				}

				#question {
					cursor: pointer;
				}
			}
		}
	}

	.setproblem {
		width: 100%;
		height: 145rpx;
		// height: 10%;
		background-color: #ffffff;
		position: fixed;
		bottom: 10rpx;
		left: 0;
		padding: 20rpx;
		box-sizing: border-box;
	}
	
	.setproblem input {
		color: #000000;
		width: calc(100% - 7rem);
		/* 让textarea占据除按钮外的剩余空间 */
		height: 100%;
		padding: 10px;
		box-sizing: border-box;
		border: 1px solid #ccc;
		/* 添加边框 */
		border-radius: 5px;
		/* 添加圆角 */
		resize: none;
		/* 禁止调整大小 */
		-webkit-user-select: auto;
		/* 允许选择文本 */
	}

	.setproblem textarea {
		color: #000000;
		width: calc(100% - 7rem);
		/* 让textarea占据除按钮外的剩余空间 */
		height: 100%;
		padding: 10px;
		box-sizing: border-box;
		border: 1px solid #ccc;
		/* 添加边框 */
		border-radius: 5px;
		/* 添加圆角 */
		resize: none;
		/* 禁止调整大小 */
		-webkit-user-select: auto;
		/* 允许选择文本 */
	}

	.setproblem button {
		width: 5.875rem;
		height: 2.5rem;
		line-height: 2.5rem;
		background: #1FC59F;
		opacity: 1;
		border-radius: 4px;
		font-size: 10px;
		color: #ffffff;
		position: absolute;
		right: 30rpx;
		bottom: 50%;
		/* 让按钮垂直居中 */
		transform: translateY(50%);
		/* 垂直居中的兼容性写法 */
		cursor: pointer;
		border: none;
	}


	.czkj-item-title {
		line-height: 25px;
		border-bottom: 1px solid #ccc;
		padding-bottom: 5px;
		margin-bottom: 5px;
	}

	.czkj-item-question {
		cursor: pointer;
		display: block;
		padding: 8px;
		position: relative;
		border-bottom: 1px dashed #ccc;
		line-height: 20px;
		min-height: 20px;
		overflow: hidden;
	}

	.czkj-question-msg {
		float: left;
		font-size: 14px;
		color: #3163C5;
	}
	
	
</style>

### 如何在 UniApp实现开源 AI 对话页面 #### 创建项目结构 为了构建一个基于 UniApp 的开源 AI 对话页面,首先需要初始化一个新的 UniApp 项目。可以使用 HBuilderX 或者命令行工具 `vue-cli` 来完成这一步骤。 ```bash npm install -g @vue/cli vue create my-universal-app --preset uni-app/preset cd my-universal-app ``` #### 配置环境变量 对于敏感信息如 API 密钥等,应该将其放置于 `.env.development` 文件中以保护隐私: ```plaintext VUE_APP_API_KEY=your_api_key_here VUE_APP_BASE_URL=https://api.example.com/v1/ ``` #### 设计界面布局 利用 Vue 组件化特性设计简洁直观的用户交互界面,在此过程中可借鉴已有的优秀案例[^3]。通常情况下,对话框由消息列表区和输入栏组成。 - **消息显示区域**:用于呈现历史记录及当前回复; - **输入框与发送按钮**:允许用户键入查询并提交给服务器处理; #### 连接后端服务 通过 HTTP 请求的方式调用远程API获取响应数据,并实时更新到前端视图上。这里推荐采用 axios 库简化网络通信操作[^4]。 ```javascript import axios from 'axios'; export default { data() { return { messages: [], newMessage: '' } }, methods: { async sendMessage() { try { const response = await axios.post(`${process.env.VUE_APP_BASE_URL}chat`, { message: this.newMessage }); this.messages.push({ text: this.newMessage, isUser: true }); this.messages.push({ text: response.data.reply, isUser: false }); this.newMessage = ''; } catch (error) { console.error('Error sending message:', error); } } } } ``` #### 流式接收 ChatGPT 数据 当涉及到像 ChatGPT 这样的大型语言模型时,可能希望模拟自然流畅的人类交流方式——即逐字符打印出机器人的回应而不是一次性加载整个字符串。为此可以在接收到部分结果之后立即渲染至屏幕上形成所谓的“打字机效应”。 ```html <template> <div class="message-bubble"> <p v-for="(char, index) in typingEffectText" :key="index">{{ char }}</p> </div> </template> <script> data(){ return{ typingEffectText:'' } }, methods:{ startTypingEffect(text){ let i = 0; function typeNextChar(){ if(i<=text.length){ self.typingEffectText+=text.charAt(i++); setTimeout(typeNextChar,Math.random()*75+25); //随机延迟时间让效果更加逼真 } } typeNextChar(); } } </script> ``` #### 添加终止会话功能 为了让用户体验更好,应当提供一种机制让用户主动结束正在进行中的对话流程。可以通过增加专门的控制选项来达成这一目的,比如设置一个明显的关闭图标或者文字链接[^1]。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值