Преглед изворни кода

feat(message/chat): 接入AI

wangpx пре 1 година
родитељ
комит
a3654d4060
3 измењених фајлова са 331 додато и 109 уклоњено
  1. 71 0
      api/AI.js
  2. 256 105
      pages/message/chat/index.vue
  3. 4 4
      pages/message/index.vue

+ 71 - 0
api/AI.js

@@ -0,0 +1,71 @@
+import request from '@/utils/request.js'
+const preUrl = '/clientServices.do?iscrypt=1'
+
+export function sendMessageToAI(message) {
+	return request({
+		url: preUrl,
+		method: 'post',
+		data: {
+			serviceId: 'miniapp_ai_getReceive',
+			params: {
+				sessionId: message.sessionId,
+				prompt: message.prompt,
+				userId: message.userId,
+				sort: message.sort,
+			}
+		}
+	})
+}
+
+export function getSessionList(userId) {
+	return request({
+		url: preUrl,
+		method: 'post',
+		data: {
+			serviceId: 'miniapp_ai_getHistorySession',
+			params: {
+				userId
+			}
+		}
+	})
+}
+
+export function getMessageList(sessionId) {
+	return request({
+		url: preUrl,
+		method: 'post',
+		data: {
+			serviceId: 'miniapp_ai_getHistoryMessage',
+			params: {
+				sessionId
+			}
+		}
+	})
+}
+
+export function setSessionName({sessionId, sessionName}) {
+	return request({
+		url: preUrl,
+		method: 'post',
+		data: {
+			serviceId: 'miniapp_ai_setSessionName',
+			params: {
+				sessionId,
+				sessionName
+			}
+		}
+	})
+}
+
+export function delSession(sessionId) {
+	return request({
+		url: preUrl,
+		method: 'post',
+		data: {
+			serviceId: 'miniapp_ai_delSession',
+			params: {
+				sessionId
+			}
+		}
+	})
+}

+ 256 - 105
pages/message/chat/index.vue

@@ -2,83 +2,235 @@
 	<view class="container">
 		<scroll-view class="chat-messages" scroll-y="true" :scroll-with-animation="true"
 			:scroll-into-view="scrollToView"
-			:style="{ width: $scrollViewWidth + 'px', padding: $viewPadding + 'px',maxHeight: $viewMaxHeight + 'px'}">
+			:style="{ width: $scrollViewWidth + 'px', padding: $viewPadding + 'px', maxHeight: $viewMaxHeight + 'px' }">
 			<!-- 循环渲染消息 -->
 			<view v-for="(message, index) in messages" :key="index" :id="'msg' + index" style="clear: both;">
 				<view :class="['message', message.sender === 'user' ? 'user-message' : 'ai-message']">
-					<text>{{ message.content }}</text>
+					<text :user-select="true" :selectable="true">{{ message.content }}</text>
 				</view>
 			</view>
 		</scroll-view>
 		<view class="chat-input">
 			<textarea @linechange="chatMsgMaxHeightChange" auto-height v-model="inputMessage" placeholder="输入消息..."
 				class="input-field" />
-			<button class="send-button" @click="sendMessage">发送</button>
+			<button class="send-button" :disabled="!button_state" @click="sendMessage">发送</button>
 		</view>
 	</view>
+	<view class="fab_button">
+		<view class="history_button">
+			<uni-fab :pattern="{ icon: 'list' }" :popMenu="false" horizontal="right" vertical="bottom"
+				@fabClick="showHistorySession()"></uni-fab>
+		</view>
+	</view>
+	<view class="drawer_container">
+		<uni-drawer ref="showLeftDrawer" :width="220">
+			<view>
+				<scroll-view scroll-y="true" :style="{height: '100vh'}">
+					<uni-list>
+						<uni-list-item title="新建会话" :clickable="true" @click="createSession()">
+							<template v-slot:footer>
+								<uni-icons type="plus-filled" size="24"></uni-icons>
+							</template>
+						</uni-list-item>
+						<view v-for="(session,index) in sessionList" :key="index">
+							<uni-list-item :title="session.sessionName" :clickable="true"
+								@click="changeSession(session.sessionId)" :ellipsis="2">
+								<template v-slot:footer>
+									<uni-icons type="compose" size="24"
+										@click.stop.prevent="toEditSession(session)"></uni-icons>
+								</template>
+							</uni-list-item>
+						</view>
+					</uni-list>
+				</scroll-view>
+			</view>
+		</uni-drawer>
+	</view>
+	<view class="edit_popup">
+		<uni-popup ref="editPopup" type="center" :mask-click="true">
+			<uni-card title="编辑会话信息">
+				<uni-forms ref="editForm" :modelValue="editSession" :rules="rules">
+					<uni-forms-item label="会话名" name="sessionName">
+						<uni-easyinput type="text" v-model="editSession.sessionName" placeholder="请输入会话名" />
+					</uni-forms-item>
+					<uni-forms-item>
+						<button type="primary" :disabled="!editButtonState" @click="submitEditSession()">确认</button>
+					</uni-forms-item>
+					<uni-forms-item>
+						<button type="warn" :disabled="!editButtonState" @click="deleteSession()">删除</button>
+					</uni-forms-item>
+				</uni-forms>
+			</uni-card>
+		</uni-popup>
+	</view>
 </template>
 
 <script setup>
-	import {
-		ref,
-		nextTick,
-		onMounted
-	} from 'vue';
+import {
+	ref,
+	nextTick,
+	onMounted,
+	reactive
+} from 'vue';
 import $modal from '@/plugins/modal.js'
-	const inputMessage = ref('');
-	const messages = ref([{
-			sender: 'ai',
-			content: '您好,我是ai助手'
-		},
+import { useUserStore } from '@/store/user.js'
+import { sendMessageToAI, getSessionList, getMessageList, setSessionName, delSession } from '@/api/AI.js'
 
+onMounted(() => {
+	// 在组件挂载后获取窗口高度
+	const res = uni.getWindowInfo();
+	// 计算滚动视图宽度
+	$scrollViewWidth.value = res.windowWidth - $viewPadding.value * 2;
+	$windowHeight.value = res.windowHeight;
+	$viewMaxHeight.value = res.windowHeight - 100;
+	initHistorySession()
+});
+// 弹出会话列表侧边栏
+const showLeftDrawer = ref(null)
+function showHistorySession() {
+	showLeftDrawer.value.open()
+}
+// 初始化会话列表
+const userStore = useUserStore()
+const sessionList = reactive([])
+function initHistorySession() {
+	sessionList.length = 0
+	getSessionList(userStore.user.useId).then(({ returnParams }) => {
+		sessionList.push(...returnParams)
+	})
+}
+// 新建会话
+function createSession() {
+	messages.length = 0
+	thisSessionId.value = 0
+	showLeftDrawer.value.close()
+}
+// 切换会话
+function changeSession(sessionId) {
+	thisSessionId.value = sessionId
+	initHistoryMessage(sessionId)
+}
+// 修改会话信息
+const editSession = ref({})
+const editPopup = ref(null)
+const editButtonState = ref(true)
+function toEditSession(session) {
+	showLeftDrawer.value.close()
+	editSession.value = session
+	editPopup.value.open()
+}
+function submitEditSession() {
+	editButtonState.value = false
+	setSessionName(editSession.value).then(res => {
+		initHistorySession()
+		editPopup.value.close()
+		editButtonState.value = true
+	})
+}
+function deleteSession() {
+	editButtonState.value = false
+	$modal.confirm("确认删除").then(res => {
+		delSession(editSession.value.sessionId).then(res => {
+			initHistorySession()
+			createSession()
+			editPopup.value.close()
+				editButtonState.value = true
+			})
+		})
+		.catch(res => {
+			editButtonState.value = true
+		})
+	}
+	
+	const messages = reactive([{
+			sender: 'assistant',
+			content: '您好,我是ai助手'
+		}
 	]);
+function initHistoryMessage(sessionId) {
+	getMessageList(sessionId).then(res => {
+		const returnParams = res.returnParams
+		messages.length = 0
+		for (let i = 0; i < returnParams.length; i++) {
+			const item = returnParams[i]
+			messages.push({
+				sender: 'user',
+				content: item.sendContent
+			})
+			messages.push({
+				sender: 'assistant',
+				content: item.receiveContent
+			})
+		}
+	})
+}
+
+	const thisSessionId = ref(0);
+	const inputMessage = ref('');
 	const scrollToView = ref('');// 滚动到特定消息的标识
+	const button_state = ref(true)
 
 	// 发送消息函数
 	function sendMessage() {
+		button_state.value = false
 		if (inputMessage.value.trim() === '') {
+			button_state.value = true
 			return;
 		}
-		messages.value.push({
+		messages.push({
 			sender: 'user',
 			content: inputMessage.value
 		});
-		inputMessage.value = ''; // 清空输入框
-		scrollToBottom(); // 滚动到底部
-		getAIResponse(); // 获取AI的回复
-	};
-
-	// 获取AI回复的函数
-	function getAIResponse() {
-		// 这里调用AI客服接口,获取回复
-		uni.request({
-			url: 'http://api.qingyunke.com/api.php?key=free&appid=0&msg=' + messages.value[messages.value.length -
-				1].content,
-			success: (res) => {
-				messages.value.push({
-					sender: 'ai',
-					content: res.data.content
-				});
-			},
-			fail: (err) => {
-				console.log('err', err);
-				$modal.alert('err'+ JSON.stringify(err))
-				// $modal.msg(err)
-				messages.value.push({
-					sender: 'ai',
-					content: '小客服不在线'
-				});
-			},
-			complete: () => {
-				scrollToBottom();
+		
+		const message = {
+			sessionId: thisSessionId.value,
+			prompt: inputMessage.value,
+			userId: userStore.user.useId,
+			sort: messages.filter(item => item.sender == 'user').length
+		}
+		sendMessageToAI(message).then((res) => {
+			const returnParams = res.returnParams
+			if (thisSessionId.value == 0) { // 添加新会话
+				thisSessionId.value = returnParams.sessionId // 新会话ID
+				sessionList.unshift({
+					sessionName: returnParams.sessionName,
+					sessionId: returnParams.sessionId
+				})
+			} else {
+				initHistorySession()
 			}
+			inputMessage.value = ''; // 清空输入框
+			if (thisSessionId.value != returnParams.sessionId) {
+				button_state.value = true;
+				return
+			}
+			let content = ""
+			const index = messages.length // 新增消息索引
+			messages.push({ // 
+				sender: 'assistant',
+				content: content
+			});
+			scrollToBottom(); // 滚动到底部
+			let i = 0
+			const charLength = 3 // 每次显示字数
+			const messageLength = returnParams.receiveContent.length
+			const interval = setInterval(() => { //模拟流式输出
+				if (i < messageLength) {
+					let sliceLength = i + charLength > messageLength?messageLength:i + charLength // 当前显示字数
+					messages[index].content += returnParams.receiveContent.slice(i, sliceLength); // 每次输出 3 个字符
+					i += charLength;
+				} else {
+					clearInterval(interval);
+					button_state.value = true;
+				}
+			}, 50); // 50ms 一次
 		})
 	};
 
 	// 滚动到底部的函数
 	function scrollToBottom() {
 		nextTick(() => {
-			scrollToView.value = 'msg' + (messages.value.length - 1); // 更新滚动到的消息
+			scrollToView.value = 'msg' + (messages.length - 1); // 更新滚动到的消息
 		});
 	};
 
@@ -87,84 +239,83 @@ import $modal from '@/plugins/modal.js'
 	const $viewPadding = ref(10); // 滚动视图内边距
 	const $windowHeight = ref(0); // 窗口高度
 
-	// 使用 onMounted 钩子在组件挂载后获取窗口高度
-	onMounted(() => {
-		const res = uni.getWindowInfo();
-		// 计算滚动视图宽度
-		$scrollViewWidth.value = res.windowWidth - $viewPadding.value * 2;
-		$windowHeight.value = res.windowHeight;
-		$viewMaxHeight.value = res.windowHeight - 100;
-	});
-
 	// 输入框行数变化时更新最大高度
 	function chatMsgMaxHeightChange(e) {
-		console.log($windowHeight,e.detail.height);
+		// console.log($windowHeight,e.detail.height);
 		$viewMaxHeight.value = $windowHeight.value - e.detail.height - 70;
 	}
 </script>
 
 
-<style scoped>
-	.container {
-		display: flex;
-		flex-direction: column;
-		height: 100vh;
-		position: relative;
+<style lang="scss" scoped>
+.container {
+	display: flex;
+	flex-direction: column;
+	height: 100vh;
+	position: relative;
 
-	}
+}
 
-	.chat-messages {
-		flex: 1;
-		background-color: #f3f3f3;
-		overflow-y: auto;
-		
-	}
+.chat-messages {
+	flex: 1;
+	background-color: #f3f3f3;
+	overflow-y: auto;
 
-	.message {
-		max-width: 70%;
-		margin: 5px 0;
-		padding: 8px;
-		border-radius: 10px;
-		display: inline-block;
-		word-wrap: break-word;
-	}
+}
 
-	.user-message {
-		background-color: #DCF8C6;
-		align-self: flex-end;
-		float: right;
-	}
+.message {
+	max-width: 70%;
+	margin: 5px 0;
+	padding: 8px;
+	border-radius: 10px;
+	display: inline-block;
+	word-wrap: break-word;
+}
 
-	.ai-message {
-		background-color: #ECECEC;
-		align-self: flex-start;
-	}
+.user-message {
+	background-color: #DCF8C6;
+	align-self: flex-end;
+	float: right;
+}
 
-	.chat-input {
-		display: flex;
-		padding: 10px;
-		background-color: #FFFFFF;
-		border-top: 1px solid #E0E0E0;
-		position: absolute;
-		bottom: 0;
-		left: 0;
-		right: 0;
-	}
+.ai-message {
+	background-color: #ECECEC;
+	align-self: flex-start;
+}
 
-	.input-field {
-		flex: 1;
-		border: 1px solid #E0E0E0;
-		border-radius: 20px;
-		padding: 12px;
-		margin-right: 10px;
-	}
+.chat-input {
+	display: flex;
+	padding: 10px;
+	background-color: #FFFFFF;
+	border-top: 1px solid #E0E0E0;
+	position: absolute;
+	bottom: 0;
+	left: 0;
+	right: 0;
+}
 
-	.send-button {
-		border: none;
-		border-radius: 20px;
-		padding: 0 20px;
-		background-color: #007AFF;
+.input-field {
+	flex: 1;
+	border: 1px solid #E0E0E0;
+	border-radius: 20px;
+	padding: 12px;
+	margin-right: 10px;
+}
+
+.send-button {
+	border: none;
+	border-radius: 20px;
+	padding: 0 20px;
+	background-color: #007AFF;
 		color: white;
 		height: 46px;
 	}
+	
+	.fab_button {
+		::v-deep .history_button {
+			.uni-fab__circle {
+				bottom: 100px !important;
+			}
+		}
+	}
 </style>

+ 4 - 4
pages/message/index.vue

@@ -61,9 +61,9 @@
 					@fabClick="toClockInBtn"></uni-fab>
 			</view>
 			<!-- AI咨询按钮 -->
-			<!-- <view> -->
-			<!-- <uni-fab :pattern="{ icon: 'headphones' }" :popMenu="false" horizontal="right" @fabClick="clickFabButton"></uni-fab> -->
-			<!-- </view> -->
+			<view>
+				<uni-fab :pattern="{ icon: 'headphones' }" :popMenu="false" horizontal="right" @fabClick="clickFabButton"></uni-fab>
+			</view>
 		</view>
 	</view>
 </template>
@@ -250,7 +250,7 @@ function handleToMessageDetail({ messageid, universalid }) {
 
 // AI咨询按钮
 function clickFabButton() {
-	console.log('clickFabButton');
+	// console.log('clickFabButton');
 	$tab.navigateTo('/pages/message/chat/index')
 }
 //跳转打卡页