<template>
	<view class="page">
		<view class="container">
			<view v-if="isWidescreen" class="header">uni-ai-chat</view>
			<text class="noData" v-if="msgLength === 0">没有对话记录</text>
			<scroll-view :scroll-into-view="scrollIntoView" scroll-y="true" class="msg-list" :enable-flex="true">
				<uni-ai-msg ref="msg" v-for="(msgIndex,index) in msgLength" :key="index" :msgIndex="index"
					:show-cursor="index == msgLength - 1 && msgLength%2 === 0 && sseIndex"></uni-ai-msg>
				<view class="tip-ai-ing" v-if="msgLength && msgLength%2 !== 0">
					<text>uni-ai正在思考中...</text>
					<view v-if="NODE_ENV == 'development' && !enableStream">
						如需提速，请开通<uni-link class="uni-link" href="https://uniapp.dcloud.net.cn/uniCloud/uni-ai-chat.html"
							text="[流式响应]"></uni-link>
					</view>
				</view>
				<view id="last-msg-item"></view>
			</scroll-view>

			<view class="foot-box">
				<view class="menu" v-if="isWidescreen">
					<view class="trash menu-item">
						<image @click="clear" src="@/static/remove.png" mode="heightFix"></image>
					</view>
				</view>

				<view class="foot-box-content">
					<view v-if="!isWidescreen" class="trash">
						<uni-icons @click="clear" type="trash" size="24" color="#888"></uni-icons>
					</view>
					<view class="textarea-box">
						<textarea v-model="content" :cursor-spacing="15" class="textarea" :auto-height="!isWidescreen"
							@keyup.shift="onKeyup('shift')" @keydown.shift="onKeydown('shift')"
							@keydown.enter="onKeydown('enter')" :disabled="inputBoxDisabled"
							:placeholder="placeholderText" :maxlength="-1" :focus="focus"
							placeholder-class="input-placeholder"></textarea>
					</view>
					<view class="send-btn-box">
						<text v-if="isWidescreen" class="send-btn-tip">↵ 发送 / shift + ↵ 换行</text>
						<button @click="beforeSendMsg" :disabled="inputBoxDisabled || !content" class="send"
							type="primary">发送</button>
					</view>
				</view>
			</view>
		</view>
	</view>
</template>

<script>
	// 引入配置文件
	import config from '@/config.js';

	import {
		msgList
	} from '@/pages/chat/msgList.js';

	// 获取广告id
	const {
		adpid
	} = config
	// 初始化sse通道
	let sseChannel = false;

	// 是否通过回调，当用户点击清空后应当跳过前一次请求的回调
	let skip_callback = false;

	// 键盘的shift键是否被按下
	let shiftKeyPressed = false
	export default {
		data() {
			return {
				// 使聊天窗口滚动到指定元素id的值
				scrollIntoView: "",
				// 消息长度（个数）
				msgLength: 0,
				// 消息列表数据
				msgList: [],
				// 输入框的消息内容
				content: "",
				// 记录流式响应次数
				sseIndex: 0,
				// 是否启用流式响应模式
				enableStream: true,
				// 当前屏幕是否为宽屏
				isWidescreen: false,
				// 广告位id
				adpid,
				focus: false
			}
		},
		computed: {
			// 输入框是否禁用
			inputBoxDisabled() {
				// 如果正在等待流式响应，则禁用输入框
				if (this.sseIndex !== 0) {
					return true
				}
				// 如果消息列表长度为奇数，则禁用输入框
				return !!(this.msgLength && this.msgLength % 2 !== 0)
			},
			// 输入框占位符文本
			placeholderText() {
				// 如果输入框被禁用，则显示“uni-ai正在回复中”
				if (this.inputBoxDisabled) {
					return 'uni-ai正在回复中'
				} else {
					// #ifdef H5
					// 如果屏幕宽度大于960，则显示“请输入内容，ctrl + enter 发送”，否则显示“请输入要发给uni-ai的内容”
					return window.innerWidth > 960 ? '请输入内容，ctrl + enter 发送' : '请输入要发给uni-ai的内容'
					// #endif
					return '请输入要发给uni-ai的内容'
				}
			},
			// 获取当前环境
			NODE_ENV() {
				return process.env.NODE_ENV
			}
		},
		// 监听msgList变化，将其存储到本地缓存中
		watch: {
			// #ifdef H5
			inputBoxDisabled(val) {
				this.$nextTick(() => {
					this.focus = !val
					// console.log('this.focus', this.focus);
				})
			},
			// #endif
			msgList: {
				handler(msgList) {

					let msgLength = msgList.length
					if (msgLength != this.msgLength) {
						this.msgLength = msgLength
						this.$nextTick(() => {
							this.updateLastMsg(msgList[msgLength - 1])
						})
					}

					// 将msgList存储到本地缓存中
					uni.setStorage({
						"key": "uni-ai-msg",
						"data": msgList
					})
				},
				// 深度监听msgList变化
				deep: true
			}
		},
		async mounted() {

			// 如果存在广告位id且用户token未过期
			if (this.adpid && uniCloud.getCurrentUserInfo().tokenExpired > Date.now()) {
				// 查询当前用户的积分
				// 获取数据库对象
				let db = uniCloud.databaseForJQL();
				// 获取uni-id-users集合
				let res = await db.collection("uni-id-users")
					// 查询条件
					.where({
						// 当前用户id
						"_id": uniCloud.getCurrentUserInfo().uid
					})
					// 返回score字段
					.field('score')
					// 执行查询
					.get()
				// 输出当前用户积分
				console.log('当前用户有多少积分:', res.data[0] && res.data[0].score);
			}

			// for (let i = 0; i < 15; i++) {
			// 	this.msgList.push({
			// 		isAi: i % 2 == true,
			// 		content: "1-" + i
			// 	})
			// }


			let _msgList = uni.getStorageSync('uni-ai-msg') || [];
			if (_msgList.length) {
				msgList.push(..._msgList)
			}

			this.msgList = msgList

			// 如果上一次对话中 最后一条消息ai未回复。则一启动就自动重发。
			let length = this.msgList.length
			if (length) {
				let lastMsg = this.msgList[length - 1]
				if (!lastMsg.isAi) {
					this.retriesSendMsg()
				}
			}


			// this.msgList.pop()
			// console.log('this.msgList', this.msgList);

			// 在dom渲染完毕后 使聊天窗口滚动到最后一条消息
			this.$nextTick(() => {
				this.showLastMsg()
			})

			// #ifdef H5
			// 监听屏幕宽度变化，判断是否为宽屏 并设置isWidescreen的值
			uni.createMediaQueryObserver(this).observe({
				minWidth: 650,
			}, matches => {
				this.isWidescreen = matches;
			})
			// #endif


			// 兼容 Vue3下textarea不支持@keydown
			// #ifdef H5 && VUE3
			//获得消息输入框对象
			let adjunctKeydown = false
			const textareaDom = document.querySelector('.textarea-box textarea');
			if (textareaDom) {
				//键盘按下时
				textareaDom.onkeydown = e => {
					// console.log('onkeydown', e.keyCode)
					if ([16, 17, 18, 93].includes(e.keyCode)) {
						//按下了shift ctrl alt windows键
						adjunctKeydown = true;
					}
					if (e.keyCode == 13 && !adjunctKeydown) {
						// 延迟兼容 v-model的时机小于onkeydown的问题
						this.content = textareaDom.value
						// 执行发送
						this.beforeSendMsg();
					}
				};
				textareaDom.onkeyup = e => {
					//松开adjunct键
					if ([16, 17, 18, 93].includes(e.keyCode)) {
						adjunctKeydown = false;
					}
				};
			}
			// #endif
		},
		methods: {
			// #ifdef H5 && VUE2
			onKeydown(keyname) {
				if (keyname == 'shift') {
					//按下了shift键
					shiftKeyPressed = true;
				}
				// 按下了回车 且 之前没按下 shift
				if (keyname == 'enter' && !shiftKeyPressed) {
					this.$nextTick(() => {
						this.beforeSendMsg();
					})
				}
			},
			onKeyup(keyname) {
				if (keyname == 'shift') {
					//按下了shift键
					shiftKeyPressed = false;
				}
			},
			// #endif
			// 此(惰性)函数，检查是否开通uni-push;决定是否启用enableStream
			async checkIsOpenPush() {
				try {
					// 获取推送客户端id
					await uni.getPushClientId()
					// 如果获取成功，则将checkIsOpenPush函数重写为一个空函数
					this.checkIsOpenPush = () => {}
				} catch (err) {
					// 如果获取失败，则将enableStream设置为false
					this.enableStream = false
				}
			},
			// 更新最后一条消息
			updateLastMsg(param) {
				let length = this.msgLength
				if (length === 0) {
					return
				}
				let lastMsg = this.msgList[length - 1]

				// 如果param是函数，则将最后一条消息作为参数传入该函数
				if (typeof param == 'function') {
					let callback = param;
					callback(lastMsg)
				} else {
					// 否则，将参数解构为data和cover两个变量
					const [data, cover = false] = arguments
					if (cover) {
						lastMsg = data
					} else {
						lastMsg = Object.assign(lastMsg, data)
					}
				}
				this.msgList.splice(length - 1, 1, lastMsg)
			},
			// 广告关闭事件
			onAdClose(e) {
				console.log('onAdClose e.detail.isEnded', e.detail.isEnded);
				if (e.detail.isEnded) {
					//5次轮训查结果
					let i = 0;
					uni.showLoading({
						mask: true
					})
					let myIntive = setInterval(async e => {
						i++;
						// 获取云数据库实例
						const db = uniCloud.database();
						// 获取uni-id-users集合
						let res = await db.collection("uni-id-users")
							// 查询条件为_id等于当前用户id
							.where('"_id" == $cloudEnv_uid')
							// 只返回score字段
							.field('score')
							// 执行查询
							.get()
						// 解构出score字段的值，如果没有则默认为undefined
						let {
							score
						} = res.result.data[0] || {}
						if (score > 0 || i > 5) {
							// 清除轮询定时器
							clearInterval(myIntive)
							// 隐藏加载提示
							uni.hideLoading()
							if (score > 0) {
								// 移除最后一条消息
								this.msgList.pop()
								this.$nextTick(() => {
									// 重发消息
									this.retriesSendMsg()
									uni.showToast({
										title: '积分余额:' + score,
										icon: 'none'
									});
								})
							}
						}
					}, 2000);
				}
			},
			async retriesSendMsg() {
				// 检查是否开通uni-push;决定是否启用enableStream
				await this.checkIsOpenPush()
				// 更新最后一条消息的状态为0 表示消息正在发送中
				this.updateLastMsg({
					state: 0
				})
				// 发送消息
				this.send()
			},
			async beforeSendMsg() {
				// 如果开启了广告位需要登录
				if (this.adpid) {
					// 获取本地缓存的token
					let token = uni.getStorageSync('uni_id_token')
					// 如果token不存在
					if (!token) {
						// 弹出提示框
						return uni.showModal({
							// 提示内容
							content: '启用激励视频，客户端需登录并启用安全网络',
							// 不显示取消按钮
							showCancel: false,
							// 确认按钮文本
							confirmText: "查看详情",
							// 弹框关闭后执行的回调函数
							complete() {
								// 文档链接
								let url = "https://uniapp.dcloud.net.cn/uniCloud/uni-ai-chat.html#ad"
								// #ifndef H5
								// 将文档链接复制到剪贴板
								uni.setClipboardData({
									// 复制的内容
									data: url,
									// 不显示提示框
									showToast: false,
									// 复制成功后的回调函数
									success() {
										// 弹出提示框
										uni.showToast({
											// 提示内容
											title: '已复制文档链接，请到浏览器粘贴浏览',
											// 不显示图标
											icon: 'none',
											// 提示框持续时间
											duration: 5000
										});
									}
								})
								// #endif

								// #ifdef H5
								// 在新窗口打开文档链接
								window.open(url)
								// #endif
							}
						});
					}
				}

				// 检查是否开通uni-push;决定是否启用enableStream
				await this.checkIsOpenPush()

				// 如果内容为空
				if (!this.content) {
					// 弹出提示框
					return uni.showToast({
						// 提示内容
						title: '内容不能为空',
						// 不显示图标
						icon: 'none'
					});
				}

				// 将用户输入的消息添加到消息列表中
				this.msgList.push({
					// 标记为非人工智能机器人，即：为用户发送的消息
					isAi: false,
					// 消息内容
					content: this.content,
					// 消息状态为0，表示正在发送中
					state: 0,
					// 消息创建时间
					create_time: Date.now()
				})

				// 展示最后一条消息
				this.showLastMsg()
				// dom加载完成后 清空文本内容
				this.$nextTick(() => {
					this.content = ''
				})
				this.send() // 发送消息
			},
			async send() {
				let messages = []
				// 复制一份，消息列表数据
				let msgs = JSON.parse(JSON.stringify(this.msgList))
				// 带总结的消息 index
				let findIndex = [...msgs].reverse().findIndex(item => item.summarize)
				// console.log('findIndex', findIndex)
				if (findIndex != -1) {
					let aiSummaryIndex = msgs.length - findIndex - 1
					// console.log('aiSummaryIndex', aiSummaryIndex)
					// 将带总结的消息的 内容 更换成 总结
					msgs[aiSummaryIndex].content = msgs[aiSummaryIndex].summarize
					// 拿最后一条带直接的消息作为与ai对话的msg body
					msgs = msgs.splice(aiSummaryIndex, msgs.length - 1)
				} else {
					// 如果未总结过就直接从末尾拿10条
					msgs = msgs.splice(-10)
				}

				// 过滤涉敏问题
				msgs = msgs.filter(msg => !msg.illegal)

				// 根据数据内容设置角色
				messages = msgs.map(item => {
					// 角色默认为用户
					let role = "user"
					// 如果是ai再根据 是否有总结 来设置角色为 system 还是 assistant
					if (item.isAi) {
						role = item.summarize ? 'system' : 'assistant'
					}
					return {
						content: item.content,
						role
					}
				})

				// 在控制台输出 向ai机器人发送的完整消息内容
				console.log('send to ai messages:', messages);

				// 判断是否开启了流式响应模式
				if (this.enableStream) {
					// 创建消息通道
					sseChannel = new uniCloud.SSEChannel()
					// console.log('sseChannel',sseChannel);

					// 监听message事件
					sseChannel.on('message', (message) => {
						// console.log('on message', message);
						// 将从云端接收到的消息添加到消息列表中

						// 如果之前未添加过就添加，否则就执行更新最后一条消息
						if (this.sseIndex === 0) {
							this.msgList.push({
								isAi: true,
								content: message,
								create_time: Date.now()
							})
							this.showLastMsg()
						} else {
							this.updateLastMsg(lastMsg => {
								lastMsg.content += message
							})
							this.showLastMsg()
						}
						// 让流式响应计数值递增
						this.sseIndex++
					})

					// 监听end事件，如果云端执行end时传了message，会在客户端end事件内收到传递的消息
					sseChannel.on('end', (e) => {
						// console.log('on end', e);
						// 如果e存在且包含summarize或insufficientScore属性
						if (e) {
							// 更新最后一条消息
							this.updateLastMsg(lastMsg => {
								// 如果e包含illegal属性
								if (e.illegal) {
									// 将最后一条消息的illegal属性更新为e的illegal属性
									lastMsg.illegal = e.illegal
									lastMsg.content = "内容涉及敏感"
									// 倒数第二条（用户发问内容）也需要设置illegal的值
									this.msgList[this.msgList.length - 2].illegal = e.illegal
								}
								// 如果e包含summarize属性
								else if (e.summarize) {
									// 将最后一条消息的summarize属性更新为e的summarize属性
									lastMsg.summarize = e.summarize
								}
								// 如果e包含insufficientScore属性
								else if (e.insufficientScore) {
									// 将最后一条消息的insufficientScore属性更新为e的insufficientScore属性
									lastMsg.insufficientScore
								}
							})
						}

						// 结束流式响应 将流式响应计数值 设置为 0
						this.sseIndex = 0
						// 滚动窗口以显示最新的一条消息
						this.showLastMsg()
					})
					await sseChannel.open() // 等待通道开启
				}

				// 重置skip_callback为false，以便下一次请求可以正常回调
				skip_callback = false
				// 导入uni-ai-chat模块，并设置customUI为true
				const uniAiChat = uniCloud.importObject("uni-ai-chat", {
					customUI: true
				})

				// 发送消息给ai机器人
				uniAiChat.send({
						messages, // 消息列表
						sseChannel // 消息通道
					})
					.then(res => {
						// console.log(111,res);
						if (!sseChannel) {
							if (!res.data) {
								return
							}
							// 更新最后一条消息的状态为100（发送成功）
							this.updateLastMsg({
								state: 100
							})
							// console.log(res, res.reply);
							// 判断是否要跳过本次回调，防止请求未返回时，历史对话已被清空。引起对话顺序错误 导致 对话输入框卡住
							if (!skip_callback) {
								let {
									"reply": content,
									summarize,
									insufficientScore,
									illegal
								} = res.data
								if (illegal) {
									// 如果返回的数据包含illegal属性，就更新最后一条消息的illegal属性为true
									this.updateLastMsg({
										illegal: true
									})
								}
								// 将从云端接收到的消息添加到消息列表中
								this.msgList.push({
									// 添加消息创建时间
									create_time: Date.now(),
									// 标记消息为来自AI机器人
									isAi: true,
									// 添加消息内容
									content,
									// 添加消息总结
									summarize,
									// 添加消息分数不足标记
									insufficientScore,
									// 添加消息涉敏标记
									illegal
								})
								// 滚动窗口以显示最新的一条消息
								this.$nextTick(() => {
									this.showLastMsg()
								})
							} else {
								console.log('用户点击了清空按钮，跳过前一次请求的回调。内容：', res.data.reply);
							}
						} else {
							// 处理 sseChannel没结束 云函数提前结束的情况
							sseChannel.close()
							this.sseIndex = 0
						}
					})
					.catch(e => {
						console.log(e);
						// 获取消息列表长度
						let l = this.msgList.length
						// console.log(l,this.msgList[l-1]); 

						// 如果最后一条消息的来源是人工智能机器人 就将流式响应计数值设置为0
						if (l && sseChannel && this.msgList[l - 1].isAi) {
							sseChannel.close()
							this.sseIndex = 0
						}

						// 更新最后一条消息的状态为-100（发送失败）
						this.updateLastMsg({
							state: -100
						})
						// 弹框提示用户错误原因
						uni.showModal({
							content: JSON.stringify(e.message),
							showCancel: false
						});
					})
			},
			// 滚动窗口以显示最新的一条消息
			showLastMsg() {
				// 等待DOM更新
				this.$nextTick(() => {
					// 将scrollIntoView属性设置为"last-msg-item"，以便滚动窗口到最后一条消息
					this.scrollIntoView = "last-msg-item"
					// 等待DOM更新，即：滚动完成
					this.$nextTick(() => {
						// 将scrollIntoView属性设置为空，以便下次设置滚动条位置可被监听
						this.scrollIntoView = ""
					})
				})
			},
			// 根据消息状态返回对应的图标
			msgStateIcon(msg) {
				switch (msg.state) {
					case 0:
						//	发送中
						return 'spinner-cycle'
						break;
					case -100:
						//	发送失败
						return 'refresh-filled'
						break;
					case -200:
						//	禁止发送（内容不合法）
						return 'info-filled'
						break;
					default:
						// 默认不返回任何图标
						return false
						break;
				}
			},
			// 清空消息列表
			clear() {
				// 弹出确认清空聊天记录的提示框
				uni.showModal({
					title: "确认要清空聊天记录？",
					content: '本操作不可撤销',
					complete: (e) => {
						// 如果用户确认清空聊天记录
						if (e.confirm) {
							// 如果存在消息通道，就关闭消息通道
							if (sseChannel) {
								sseChannel.close()
							}
							// 将skip_callback设置为true，以便下一次请求可以正常回调
							skip_callback = true
							// 将流式响应计数值归零
							this.sseIndex = 0
							// 将消息列表清空 
							this.msgList.splice(0, this.msgLength);
						}
					}
				});
			}
		}
	}
</script>

<style lang="scss">
	/* #ifdef VUE3 && APP-PLUS */
	@import "@/components/uni-ai-msg/uni-ai-msg.scss";
	/* #endif */

	/* #ifndef APP-NVUE */
	view,
	textarea,
	button,
	.page {
		display: flex;
		box-sizing: border-box;
	}

	/* #endif */


	/* #ifndef APP-NVUE */
	page,
	/* #endif */
	.page,
	.container {
		background-color: #efefef;

		/* #ifdef APP-NVUE */
		flex: 1;
		/* #endif */

		/* #ifndef APP-NVUE */
		height: 100vh;
		/* #endif */

		/* #ifdef H5 */
		height: calc(100vh - 44px);
		/* #endif */

		flex-direction: column;
		align-items: center;
		justify-content: center;
	}

	/* #ifndef APP-NVUE */
	.container {
		background-color: #FAFAFA;
	}

	/* #endif */

	.foot-box {
		width: 750rpx;
		display: flex;
		flex-direction: column;
		padding: 10px 0px;
		background-color: #FFF;
	}

	.foot-box-content {
		justify-content: space-around;
	}

	.textarea-box {
		padding: 8px 10px;
		background-color: #f9f9f9;
		border-radius: 5px;
	}

	.textarea-box .textarea {
		max-height: 100px;
		font-size: 14px;
		/* #ifndef APP-NVUE */
		overflow: auto;
		/* #endif */
		width: 450rpx;
		font-size: 14px;
	}

	/* #ifdef H5 */
	/*隐藏滚动条*/
	.textarea-box .textarea::-webkit-scrollbar {
		width: 0;
	}

	/* #endif */

	.input-placeholder {
		color: #bbb;
		line-height: 18px;
	}

	.trash,
	.send {
		width: 50px;
		height: 30px;
		justify-content: center;
		align-items: center;
		flex-shrink: 0;
	}

	.trash {
		width: 30rpx;
		margin-left: 10rpx;
	}

	.send {
		color: #FFF;
		border-radius: 4px;
		display: flex;
		margin: 0;
		padding: 0;
		font-size: 14px;
		margin-right: 20rpx;
	}

	/* #ifndef APP-NVUE */
	.send::after {
		display: none;
	}

	/* #endif */


	.msg-list {
		flex: 1;
		height: 1px;
		width: 750rpx;
	}

	.noData {
		margin-top: 15px;
		text-align: center;
		width: 750rpx;
		color: #aaa;
		font-size: 12px;
		justify-content: center;
	}

	.tip-ai-ing {
		align-items: center;
		flex-direction: column;
		font-size: 14px;
		color: #919396;
		padding: 15px 0;
	}

	.uni-link {
		margin-left: 5px;
		line-height: 20px;
	}

	/* #ifdef H5 */
	@media screen and (min-width:650px) {
		.foot-box {
			border-top: solid 1px #dde0e2;
		}

		.page {
			width: 100vw;
			flex-direction: row;
		}

		.page * {
			max-width: 950px;
		}

		.container
		{
			box-shadow: 0 0 5px #e0e1e7;
			margin-top: 44px;
			border-radius: 10px;
			overflow: hidden;
		}

		.container .header {
			height: 44px;
			line-height: 44px;
			border-bottom: 1px solid #F0F0F0;
			width: 100vw;
			justify-content: center;
			font-weight: 500;
		}

		.content {
			background-color: #f9f9f9;
			position: relative;
			max-width: 90%;
		}

		// .copy {
		// 	color: #888888;
		// 	position: absolute;
		// 	right: 8px;
		// 	top: 8px;
		// 	font-size: 12px;
		// 	cursor:pointer;
		// }
		// .copy :hover{
		// 	color: #4b9e5f;
		// }

		.foot-box,
		.foot-box-content,
		.msg-list,
		.msg-item,
		// .create_time,
		.noData,
		.textarea-box,
		.textarea,
		textarea-box {
			width: 100% !important;
		}

		.textarea-box,
		.textarea,
		textarea,
		textarea-box {
			height: 120px;
		}

		.foot-box,
		.textarea-box {
			background-color: #FFF;
		}

		.foot-box-content {
			flex-direction: column;
			justify-content: center;
			align-items: flex-end;
			padding-bottom: 0;
		}

		.menu {
			padding: 0 10px;
		}

		.menu-item {
			height: 20px;
			justify-content: center;
			align-items: center;
			align-content: center;
			display: flex;
			margin-right: 10px;
			cursor: pointer;
		}

		.trash {
			opacity: 0.8;
		}

		.trash image {
			height: 15px;
		}


		.textarea-box,
		.textarea-box * {
			// border: 1px solid #000;
		}

		.send-btn-box .send-btn-tip {
			color: #919396;
			margin-right: 8px;
			font-size: 12px;
			line-height: 28px;
		}
	}

	/* #endif */
</style>