From c6e3c33979ab6ed1aa6a834f39c85356320c0f93 Mon Sep 17 00:00:00 2001
From: gerson <1405270578@qq.com>
Date: 星期五, 19 七月 2024 23:02:11 +0800
Subject: [PATCH] 语音对话

---
 src/views/project/ch/home/component/waterRight/top.vue         |    4 
 src/components/chat/Chat.vue                                   |   35 +++++++-
 src/components/chat/components/playBar/voicePage/VoicePage.vue |  116 +++++++++++++++++++++++++++--
 src/components/chat/components/playBar/PlayBar.vue             |   31 ++++++-
 src/stores/chatRoom.ts                                         |   16 +++
 src/components/chat/components/playBar/voicePage/types.ts      |   20 +++++
 6 files changed, 201 insertions(+), 21 deletions(-)

diff --git a/src/components/chat/Chat.vue b/src/components/chat/Chat.vue
index 9f3968b..cc219c9 100644
--- a/src/components/chat/Chat.vue
+++ b/src/components/chat/Chat.vue
@@ -102,9 +102,11 @@
 
 			<div class="sticky bottom-0 w-full p-6 pb-8 bg-[rgb(247,248,250)] flex justify-center">
 				<PlayBar
+					v-model:voicePageIsShow="voicePageIsShow"
 					:isTalking="isTalking"
+					:isHome="false"
 					v-model="messageContent.values"
-					@sendClick="sendChatMessage"
+					@sendClick="sendClick"
 					:style="{ width: chatWidth }"
 				></PlayBar>
 			</div>
@@ -124,7 +126,16 @@
 import { GetHistoryAnswer, QueryHistoryDetail, QuestionAi, SetHistoryAnswerState, getQuestionProcess } from '/@/api/ai/chat';
 import PlayBar from '/@/components/chat/components/playBar/PlayBar.vue';
 import router from '/@/router';
-import { activeChatRoom, activeLLMId, activeRoomId, activeSampleId, activeSectionAId, roomConfig } from '/@/stores/chatRoom';
+import {
+	activeChatRoom,
+	activeLLMId,
+	activeRoomId,
+	activeSampleId,
+	activeSectionAId,
+	roomConfig,
+	setRoomConfig,
+	getRoomConfig,
+} from '/@/stores/chatRoom';
 import { v4 as uuidv4 } from 'uuid';
 import _ from 'lodash';
 import { ErrorCode } from '/@/utils/request';
@@ -133,7 +144,7 @@
 import CustomDrawer from '/@/components/drawer/CustomDrawer.vue';
 
 const chatWidth = '75%';
-
+const voicePageIsShow = ref(false);
 let isTalking = ref(false);
 let messageContent = ref<ChatContent>({
 	type: AnswerType.Text,
@@ -299,9 +310,11 @@
 	});
 };
 
-const sendChatMessage = async (content: ChatContent = messageContent.value) => {
+const sendChatMessage = async (content: ChatContent = messageContent.value, cb?: any) => {
 	if (!content?.values) return;
-	if (messageList.value.length === 0) {
+
+	const isNewChat = messageList.value.length === 0
+	if (isNewChat) {
 		if (activeSampleId.value) {
 			currentSampleId = activeSampleId.value;
 		}
@@ -329,9 +342,17 @@
 
 		let resMsgContent: ChatContent = null;
 		resMsgContent = await questionAi(content.values);
+		if (isNewChat) {
+			const firstResCb = getRoomConfig(currentRouteId, 'firstResCb');
+			firstResCb(resMsgContent);
+		} else {
+			cb?.(resMsgContent);
+		}
 		userItem.historyId = questionRes.history_id;
 		assistantItem.historyId = questionRes.history_id;
 		appendLastMessageContent(resMsgContent);
+
+		return resMsgContent;
 	} catch (error: any) {
 		// appendLastMessageContent({
 		// 	type: AnswerType.Text,
@@ -341,6 +362,10 @@
 		isTalking.value = false;
 	}
 };
+
+const sendClick = (cb) => {
+	sendChatMessage(messageContent.value, cb);
+};
 const appendLastMessageContent = (content: ChatContent) => {
 	if (messageList.value.at(-1)) {
 		messageList.value.at(-1).content = content;
diff --git a/src/components/chat/components/playBar/PlayBar.vue b/src/components/chat/components/playBar/PlayBar.vue
index c21331c..fa6337d 100644
--- a/src/components/chat/components/playBar/PlayBar.vue
+++ b/src/components/chat/components/playBar/PlayBar.vue
@@ -35,20 +35,35 @@
 				</div>
 			</div>
 		</div>
-		<VoicePage v-model:isShow="voicePageIsShow" v-show="voicePageIsShow" />
+		<VoicePage
+			v-model:isShow="voicePageIsShow"
+			v-show="voicePageIsShow"
+			@submit="(cb) => emits('sendClick', cb)"
+			@updateInputValue="updateInputValue"
+			:isHome = "isHome"
+		/>
 	</div>
 </template>
 
 <script setup lang="ts">
+import { ElMessage } from 'element-plus';
 import { ref } from 'vue';
 import VoicePage from './voicePage/VoicePage.vue';
 const emits = defineEmits(['sendClick']);
 
-const props = defineProps(['isTalking']);
-const voicePageIsShow = ref(false);
+const props = defineProps(['isTalking','isHome']);
+
+const voicePageIsShow = defineModel('voicePageIsShow', {
+	type: Boolean,
+	default: false,
+});
 const inputValue = defineModel({
 	type: String,
 });
+
+const updateInputValue = (val) => {
+	inputValue.value = val;
+};
 
 const enterInput = (e) => {
 	if (props.isTalking) return;
@@ -61,7 +76,15 @@
 	}
 };
 const audioChangeWord = () => {
-	voicePageIsShow.value = true;
+	navigator.getUserMedia(
+		{ audio: true },
+		function onSuccess(stream) {
+			voicePageIsShow.value = true;
+		},
+		function onError(error) {
+			ElMessage.warning('璇锋墦寮�楹﹀厠椋庢潈闄�');
+		}
+	);
 };
 </script>
 <style scoped lang="scss">
diff --git a/src/components/chat/components/playBar/voicePage/VoicePage.vue b/src/components/chat/components/playBar/voicePage/VoicePage.vue
index 1987afd..62af532 100644
--- a/src/components/chat/components/playBar/voicePage/VoicePage.vue
+++ b/src/components/chat/components/playBar/voicePage/VoicePage.vue
@@ -24,7 +24,9 @@
 						><span :style="{ 'animation-play-state': animationPlayState }"></span>
 					</div>
 				</div>
-				<div class="mt-5">璇峰紑濮嬭璇�</div>
+				<div class="mt-5" :class="{ 'cursor-pointer': currentVoiceType === VoiceTipType.Speak }" @click="voiceTipClick">
+					{{ voiceTipMap[currentVoiceType] }}
+				</div>
 
 				<div class="flex items-center justify-between bottom-16 absolute left-1/2 -translate-x-1/2 space-x-16">
 					<div class="size-[35px] flex items-center justify-center bg-[#292929] rounded-full cursor-pointer" @click="togglePlayClick">
@@ -43,30 +45,102 @@
 </template>
 
 <script setup lang="ts">
-import { computed, ref, watch } from 'vue';
+import { computed, nextTick, ref, watch } from 'vue';
+import type { ChatContent } from '../../../model/types';
+import { AnswerType } from '../../../model/types';
+import { VoiceRecognitionErrorType, VoiceTipType, voiceTipMap } from './types';
+import router from '/@/router';
+import { setRoomConfig } from '/@/stores/chatRoom';
 
 const animationPlayState = ref<'paused' | 'running'>('running');
 
 const playIcon = computed(() => (animationPlayState.value === 'running' ? 'icon-zanting' : 'icon-bofang'));
-
+const isSpeak = ref(false);
 const togglePlayClick = () => {
 	animationPlayState.value = animationPlayState.value === 'running' ? 'paused' : 'running';
+	if (currentVoiceType.value === VoiceTipType.Speak) {
+		if (isSpeak.value) {
+			window.speechSynthesis.pause();
+		} else {
+			window.speechSynthesis.resume();
+		}
+		isSpeak.value = !isSpeak.value;
+	}
 };
 
+const props = defineProps(['isHome']);
+const emit = defineEmits(['submit', 'updateInputValue']);
 const isShow = defineModel('isShow', {
 	type: Boolean,
 });
+
+const resetToListenVoice = () => {
+	currentVoiceType.value = VoiceTipType.NoSpeech;
+	audioChangeWord();
+};
 const isListening = ref(false);
 
-const inputValue = ref('');
 const closeClick = () => {
 	isShow.value = false;
 };
+
+let recognition = null;
+let speech = null;
+const currentVoiceType = ref<VoiceTipType>(VoiceTipType.NoSpeech);
+const handleAnswerRes = (res: ChatContent) => {
+	if (!res) {
+		return;
+	}
+	let text = '';
+
+	if (res.type === AnswerType.Text || res.type === AnswerType.Knowledge) {
+		if (res.type === AnswerType.Knowledge) {
+			text = res.values?.map((item) => item.answer) ?? '';
+		} else {
+			text = res.values;
+		}
+	} else {
+		text = '鎶辨瓑锛屾垜鏃犳硶鍙h堪鍥炵瓟姝ら棶棰樼殑锛岄渶瑕佹煡鐪嬭鍏抽棴姝よ闊冲璇濈晫闈�';
+	}
+
+	currentVoiceType.value = VoiceTipType.Speak;
+	isSpeak.value = true;
+	var speech = new SpeechSynthesisUtterance();
+	speech.text = text; // 鍐呭
+	speech.lang = 'zh-cn'; // 璇█
+	speech.voiceURI = 'Microsoft Huihui - Chinese (Simplified, PRC)'; // 澹伴煶鍜屾湇鍔�
+	// eslint-disable-next-line no-irregular-whitespace
+	speech.volume = 0.7; // 澹伴煶鐨勯煶閲忓尯闂磋寖鍥存槸鈥嬧��0鈥嬧�嬧�嬪埌鈥嬧��1榛樿鏄�嬧��1鈥嬧��
+	// eslint-disable-next-line no-irregular-whitespace
+	speech.rate = 1; // 璇�燂紝鏁板�硷紝榛樿鍊兼槸鈥嬧��1鈥嬧�嬧�嬶紝鑼冨洿鏄�嬧��0.1鈥嬧�嬧�嬪埌鈥嬧��10鈥嬧�嬧�嬶紝琛ㄧず璇�熺殑鍊嶆暟锛屼緥濡傗�嬧��2鈥嬧�嬭〃绀烘甯歌閫熺殑涓ゅ��
+	// eslint-disable-next-line no-irregular-whitespace
+	speech.pitch = 1; // 琛ㄧず璇磋瘽鐨勯煶楂橈紝鏁板�硷紝鑼冨洿浠庘�嬧��0鈥嬧�嬧�嬶紙鏈�灏忥級鍒扳�嬧��2鈥嬧�嬧�嬶紙鏈�澶э級銆傞粯璁ゅ�间负鈥嬧��1鈥嬧�嬨��
+
+	speech.onend = () => {
+		resetToListenVoice();
+	};
+	window.speechSynthesis.speak(speech);
+};
+
+const voiceTipClick = () => {
+	switch (currentVoiceType.value) {
+		case VoiceTipType.Speak:
+			window.speechSynthesis.cancel();
+			setTimeout(() => {
+				resetToListenVoice();
+			}, 0);
+
+			break;
+		default:
+			break;
+	}
+	window.speechSynthesis.cancel();
+};
 const audioChangeWord = () => {
-	inputValue.value = '';
+	emit('updateInputValue', '');
 	// 鍒涘缓SpeechRecognition瀵硅薄
 	// eslint-disable-next-line no-undef
-	var recognition = new webkitSpeechRecognition();
+	recognition = new webkitSpeechRecognition();
 	if (!recognition) {
 		// eslint-disable-next-line no-undef
 		recognition = new SpeechRecognition();
@@ -84,14 +158,35 @@
 	recognition.onresult = function (event) {
 		var result = event.results[0][0].transcript;
 		console.log('鐩戝惉缁撴灉:', result);
-		inputValue.value = result;
+
+		emit('updateInputValue', result);
+		currentVoiceType.value = VoiceTipType.Think;
+		if (!props.isHome) {
+			emit('submit', handleAnswerRes);
+		} else {
+			setRoomConfig(router.currentRoute.value.query.id as string, 'firstResCb', handleAnswerRes);
+			emit('submit');
+		}
+	};
+	recognition.onspeechstart = (event) => {
+		currentVoiceType.value = VoiceTipType.Speech;
 	};
 
 	// 鐩戝惉閿欒浜嬩欢
 	recognition.onerror = function (event) {
 		isListening.value = false;
-		ElMessage.error('鐩戝惉璇煶澶辫触');
+		// ElMessage.error('鐩戝惉璇煶澶辫触');
 		console.error(event.error);
+		switch (event.error) {
+			case VoiceRecognitionErrorType.NoSpeech:
+				if (isShow.value) {
+					resetToListenVoice();
+				}
+				break;
+
+			default:
+				break;
+		}
 	};
 	// 鐩戝惉缁撴潫浜嬩欢锛堝寘鎷瘑鍒垚鍔熴�佽瘑鍒敊璇拰鐢ㄦ埛鍋滄锛�
 	recognition.onend = function () {
@@ -101,7 +196,10 @@
 };
 
 const resetStatus = () => {
+	currentVoiceType.value = VoiceTipType.NoSpeech;
 	animationPlayState.value = 'running';
+	recognition?.abort();
+	window.speechSynthesis.cancel();
 };
 
 watch(
@@ -109,6 +207,8 @@
 	(val) => {
 		if (!val) {
 			resetStatus();
+		} else {
+			resetToListenVoice();
 		}
 	}
 );
diff --git a/src/components/chat/components/playBar/voicePage/types.ts b/src/components/chat/components/playBar/voicePage/types.ts
new file mode 100644
index 0000000..4005c8a
--- /dev/null
+++ b/src/components/chat/components/playBar/voicePage/types.ts
@@ -0,0 +1,20 @@
+export const enum VoiceRecognitionErrorType {
+	/** @description 娌℃湁澹伴煶 */
+	NoSpeech = 'no-speech',
+	/** @description 涓 */
+	Abort = 'abort',
+}
+
+export const enum VoiceTipType {
+	NoSpeech,
+	Speech,
+	Think,
+	Speak,
+}
+
+export const voiceTipMap = {
+	[VoiceTipType.NoSpeech]: '璇峰紑濮嬭璇�',
+	[VoiceTipType.Speech]: '鎴戞鍦ㄥ惉',
+	[VoiceTipType.Think]: '鎬濊�冧腑...',
+	[VoiceTipType.Speak]: '鐐瑰嚮鎵撴柇',
+};
diff --git a/src/stores/chatRoom.ts b/src/stores/chatRoom.ts
index 5d86755..c947009 100644
--- a/src/stores/chatRoom.ts
+++ b/src/stores/chatRoom.ts
@@ -1,10 +1,11 @@
-import { computed, ref, watch } from 'vue';
+import { computed, ref } from 'vue';
 import type { ChatRoomItem } from '../layout/component/sidebar/components/types';
-import { Local } from '../utils/storage';
 
 export type RoomConfig = {
 	/** 鏄惁鐩存帴璋冪敤澶фā鍨嬶紙閫氫箟鍗冮棶锛夊洖绛� */
 	isAnswerByLLM: boolean;
+	/** @description 浠庨椤佃繘鍘昏幏鍙栫殑绗竴涓洖澶嶏紝鍥炶皟鍑芥暟 */
+	firstResCb: any;
 };
 
 export type RoomConfigKey = keyof RoomConfig;
@@ -23,6 +24,17 @@
 	}
 };
 
+export const getRoomConfig = <T extends RoomConfigKey>(roomId: string, key: T) => {
+	if (!roomConfig.value) {
+		return null;
+	}
+	if (!roomConfig.value[roomId]) {
+		return null;
+	} else {
+		return roomConfig.value[roomId][key];
+	}
+};
+
 export const chatRoomList = ref<ChatRoomItem[]>([]);
 
 export const activeRoomId = ref(null);
diff --git a/src/views/project/ch/home/component/waterRight/top.vue b/src/views/project/ch/home/component/waterRight/top.vue
index 8216001..e2b3583 100644
--- a/src/views/project/ch/home/component/waterRight/top.vue
+++ b/src/views/project/ch/home/component/waterRight/top.vue
@@ -17,7 +17,7 @@
 			</div>
 		</div> -->
 	</div>
-	<PlayBar v-model="inputValue" @send-click="sendClick" />
+	<PlayBar v-model="inputValue" @send-click="sendClick" :is-home="true"/>
 </template>
 
 <script setup lang="ts">
@@ -32,7 +32,7 @@
 const emits = defineEmits(['sendClick']);
 const inputValue = ref('浣犳槸璋侊紵');
 
-const sendClick = async () => {
+const sendClick = async (cb) => {
 	if (!inputValue.value) return;
 	if (!activeSectionAId.value) {
 		ElMessage.warning('璇烽�夋嫨搴旂敤鍦烘櫙');

--
Gitblit v1.9.3