|
|
@@ -65,23 +65,40 @@
|
|
|
<!-- 输入框和发送按钮 -->
|
|
|
<div class="input-section">
|
|
|
<input
|
|
|
- type="text"
|
|
|
- v-model="prompt"
|
|
|
- placeholder="问我任何问题..."
|
|
|
- @keyup.enter="handleSendByKeydown"
|
|
|
+ type="text"
|
|
|
+ v-model="prompt"
|
|
|
+ placeholder="问我任何问题..."
|
|
|
+ @keyup.enter="handleSendByKeydown"
|
|
|
/>
|
|
|
<!-- 添加语音输入按钮 -->
|
|
|
<button
|
|
|
- @click="toggleSpeechInput"
|
|
|
- class="speech-btn"
|
|
|
- :class="{ 'recording': isRecording }"
|
|
|
+ @click="toggleSpeechInput"
|
|
|
+ class="speech-btn"
|
|
|
+ :class="{ recording: isRecording }"
|
|
|
>
|
|
|
<el-icon v-if="!isRecording"><Microphone /></el-icon>
|
|
|
<el-icon v-else><Mute /></el-icon>
|
|
|
<!-- 显示倒计时(仅录音时显示) -->
|
|
|
- <span v-if="isRecording" class="countdown-text">{{ countdown }}s</span>
|
|
|
+ <span v-if="isRecording" class="countdown-text"
|
|
|
+ >{{ countdown }}s</span
|
|
|
+ >
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <!-- 终止问答按钮 -->
|
|
|
+ <div
|
|
|
+ v-if="conversationInProgress"
|
|
|
+ @click="stopStream"
|
|
|
+ class="stop-btn"
|
|
|
+ title="终止问答"
|
|
|
+ >
|
|
|
+ <img :src="stopicon" alt="停止" />
|
|
|
+ </div>
|
|
|
+ <button
|
|
|
+ v-if="!conversationInProgress"
|
|
|
+ @click="handleSendByButton"
|
|
|
+ >
|
|
|
+ 发送
|
|
|
</button>
|
|
|
- <button @click="handleSendByButton">发送</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -91,13 +108,15 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import {ref, onMounted, computed, watch, nextTick} from "vue";
|
|
|
+import { ref, onMounted, computed, watch, nextTick } from "vue";
|
|
|
import { CreateDialogue, sendChatMessageStream } from "@/api/questions.js";
|
|
|
import { useRouter, useRoute } from "vue-router";
|
|
|
-import { saveRecord } from '@/api/personalized/index.js'
|
|
|
+import { saveRecord } from "@/api/personalized/index.js";
|
|
|
// 导入全局状态
|
|
|
-import { globalState } from '@/utils/globalState.js'
|
|
|
+import { globalState } from "@/utils/globalState.js";
|
|
|
|
|
|
+// 终止按钮
|
|
|
+import stopicon from "@/assets/icon/stopicon.png";
|
|
|
|
|
|
import MarkdownView from "@/components/MarkdownView/index.vue";
|
|
|
import {
|
|
|
@@ -113,23 +132,18 @@ import {
|
|
|
Picture,
|
|
|
Tickets,
|
|
|
User,
|
|
|
+ Search, // 使用Search图标作为替代
|
|
|
} from "@element-plus/icons-vue";
|
|
|
|
|
|
import DefaultMessage from "@/components/DefaultMessage/index.vue";
|
|
|
|
|
|
-// 导入图片
|
|
|
-// import question from '@/assets/icon/question.png'
|
|
|
-// import painting from '@/assets/icon/painting.png'
|
|
|
-// import human from '@/assets/icon/human.png'
|
|
|
|
|
|
// 语音图标
|
|
|
import { Microphone, Mute } from "@element-plus/icons-vue";
|
|
|
|
|
|
-
|
|
|
import LeftPanel from "@/components/LeftPanel.vue";
|
|
|
const leftPanelRef = ref(null);
|
|
|
|
|
|
-
|
|
|
// 语音输入响应式变量
|
|
|
const isRecording = ref(false); // 录音状态
|
|
|
const recognition = ref(null); // 语音识别实例
|
|
|
@@ -195,14 +209,13 @@ const textRoleRunning = ref(false); // Typing speed in milliseconds
|
|
|
const isComposing = ref(false); // 判断用户是否在输入
|
|
|
const conversationInAbortController = ref(); // 对话进行中 abort 控制器(控制 stream 对话)
|
|
|
const inputTimeout = ref(); // 处理输入中回车的定时器
|
|
|
-const prompt = ref(''); // prompt
|
|
|
+const prompt = ref(""); // prompt
|
|
|
const enableContext = ref(true); // 是否开启上下文
|
|
|
// 接收 Stream 消息
|
|
|
const receiveMessageFullText = ref("");
|
|
|
const receiveMessageDisplayedText = ref("");
|
|
|
const messageListRef = ref(null);
|
|
|
|
|
|
-
|
|
|
// =========== 【聊天对话】相关 ===========
|
|
|
|
|
|
/** 获取对话信息 */
|
|
|
@@ -223,14 +236,15 @@ const getConversation = async (id) => {
|
|
|
// =========== 【语音录入】相关 ===========
|
|
|
// 初始化语音识别
|
|
|
const initSpeechRecognition = () => {
|
|
|
- const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
|
+ const SpeechRecognition =
|
|
|
+ window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
|
if (!SpeechRecognition) {
|
|
|
alert("当前浏览器不支持语音输入功能");
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
const instance = new SpeechRecognition();
|
|
|
- instance.lang = 'zh-CN';
|
|
|
+ instance.lang = "zh-CN";
|
|
|
instance.interimResults = false;
|
|
|
|
|
|
instance.onresult = (event) => {
|
|
|
@@ -247,10 +261,10 @@ const initSpeechRecognition = () => {
|
|
|
};
|
|
|
|
|
|
instance.onerror = (event) => {
|
|
|
- console.error('语音识别错误:', event.error);
|
|
|
+ console.error("语音识别错误:", event.error);
|
|
|
clearInterval(countdownTimer.value); // 出错时清除定时器
|
|
|
isRecording.value = false;
|
|
|
- alert('语音输入失败,请重试');
|
|
|
+ alert("语音输入失败,请重试");
|
|
|
countdown.value = 0;
|
|
|
};
|
|
|
|
|
|
@@ -276,29 +290,30 @@ const toggleSpeechInput = () => {
|
|
|
recognition.value = initSpeechRecognition();
|
|
|
if (!recognition.value) return;
|
|
|
|
|
|
- navigator.mediaDevices.getUserMedia({ audio: true })
|
|
|
- .then(() => {
|
|
|
- recognition.value.start();
|
|
|
- isRecording.value = true;
|
|
|
-
|
|
|
- // 启动新的倒计时定时器
|
|
|
- countdownTimer.value = setInterval(() => {
|
|
|
- countdown.value--;
|
|
|
- if (countdown.value <= 0) {
|
|
|
- clearInterval(countdownTimer.value); // 倒计时结束清除
|
|
|
- recognition.value.stop();
|
|
|
- isRecording.value = false;
|
|
|
- countdown.value = 0;
|
|
|
- }
|
|
|
- }, 1000);
|
|
|
- })
|
|
|
- .catch((err) => {
|
|
|
- console.error('麦克风权限获取失败:', err);
|
|
|
- alert('请允许麦克风权限以使用语音输入');
|
|
|
- // 出错时重置状态
|
|
|
- isRecording.value = false;
|
|
|
- countdown.value = 0;
|
|
|
- });
|
|
|
+ navigator.mediaDevices
|
|
|
+ .getUserMedia({ audio: true })
|
|
|
+ .then(() => {
|
|
|
+ recognition.value.start();
|
|
|
+ isRecording.value = true;
|
|
|
+
|
|
|
+ // 启动新的倒计时定时器
|
|
|
+ countdownTimer.value = setInterval(() => {
|
|
|
+ countdown.value--;
|
|
|
+ if (countdown.value <= 0) {
|
|
|
+ clearInterval(countdownTimer.value); // 倒计时结束清除
|
|
|
+ recognition.value.stop();
|
|
|
+ isRecording.value = false;
|
|
|
+ countdown.value = 0;
|
|
|
+ }
|
|
|
+ }, 1000);
|
|
|
+ })
|
|
|
+ .catch((err) => {
|
|
|
+ console.error("麦克风权限获取失败:", err);
|
|
|
+ alert("请允许麦克风权限以使用语音输入");
|
|
|
+ // 出错时重置状态
|
|
|
+ isRecording.value = false;
|
|
|
+ countdown.value = 0;
|
|
|
+ });
|
|
|
}
|
|
|
};
|
|
|
|
|
|
@@ -364,12 +379,12 @@ const onCompositionend = () => {
|
|
|
|
|
|
// 保存记录
|
|
|
// 年级ID相关
|
|
|
-const gradeId = ref('')
|
|
|
+const gradeId = ref("");
|
|
|
// 添加消息计数器变量
|
|
|
-const messageCount = ref(0)
|
|
|
+const messageCount = ref(0);
|
|
|
onMounted(() => {
|
|
|
- // 从全局状态初始化年级ID
|
|
|
- gradeId.value = globalState.initGradeId()
|
|
|
+ // 从全局状态初始化年级ID
|
|
|
+ gradeId.value = globalState.initGradeId();
|
|
|
});
|
|
|
|
|
|
/** 真正执行【发送】消息操作 */
|
|
|
@@ -383,17 +398,17 @@ const doSendMessage = async (content) => {
|
|
|
console.error("还没创建对话,不能发送!");
|
|
|
return;
|
|
|
}
|
|
|
- // 递增消息计数器
|
|
|
- messageCount.value++
|
|
|
+ // 递增消息计数器
|
|
|
+ messageCount.value++;
|
|
|
// 发送saveRecord请求 保存消息次数
|
|
|
- try{
|
|
|
+ try {
|
|
|
await saveRecord({
|
|
|
- brpNjId: gradeId.value,
|
|
|
- brpType: "aiCount",
|
|
|
- brpProgress: messageCount.value
|
|
|
- });
|
|
|
- }catch(error){
|
|
|
- console.error('保存记录失败:', error);
|
|
|
+ brpNjId: gradeId.value,
|
|
|
+ brpType: "aiCount",
|
|
|
+ brpProgress: messageCount.value,
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ console.error("保存记录失败:", error);
|
|
|
}
|
|
|
// 清空输入框
|
|
|
prompt.value = "";
|
|
|
@@ -404,8 +419,7 @@ const doSendMessage = async (content) => {
|
|
|
});
|
|
|
};
|
|
|
|
|
|
-
|
|
|
-import { useAudioPlayer } from '@/components/TTS/useAudioPlayer';
|
|
|
+import { useAudioPlayer } from "@/components/TTS/useAudioPlayer";
|
|
|
|
|
|
const { playAudioChunk } = useAudioPlayer();
|
|
|
|
|
|
@@ -454,11 +468,10 @@ const doSendMessageStream = async (userMessage) => {
|
|
|
}
|
|
|
|
|
|
// 根据事件类型处理
|
|
|
- if (data.eventType === 'TEXT') {
|
|
|
-
|
|
|
+ if (data.eventType === "TEXT") {
|
|
|
// 如果内容为空,就不处理。
|
|
|
- if (data.receive?.content === '') {
|
|
|
- return
|
|
|
+ if (data.receive?.content === "") {
|
|
|
+ return;
|
|
|
}
|
|
|
|
|
|
// 处理文本消息
|
|
|
@@ -474,7 +487,7 @@ const doSendMessageStream = async (userMessage) => {
|
|
|
activeMessageList.value.push(data.send);
|
|
|
activeMessageList.value.push(data.receive);
|
|
|
}
|
|
|
- } else if (data.eventType === 'AUDIO') {
|
|
|
+ } else if (data.eventType === "AUDIO") {
|
|
|
// 处理音频消息
|
|
|
await playAudioChunk(data.audioData);
|
|
|
}
|
|
|
@@ -567,7 +580,8 @@ const textRoll = async () => {
|
|
|
}
|
|
|
|
|
|
if (index < receiveMessageFullText.value.length) {
|
|
|
- receiveMessageDisplayedText.value += receiveMessageFullText.value[index];
|
|
|
+ receiveMessageDisplayedText.value +=
|
|
|
+ receiveMessageFullText.value[index];
|
|
|
index++;
|
|
|
|
|
|
// 更新 message
|
|
|
@@ -593,7 +607,6 @@ const textRoll = async () => {
|
|
|
} catch {}
|
|
|
};
|
|
|
|
|
|
-
|
|
|
/** 初始化 **/
|
|
|
onMounted(async () => {
|
|
|
if (personId.value) {
|
|
|
@@ -612,9 +625,37 @@ onMounted(async () => {
|
|
|
// activeMessageListLoading.value = true
|
|
|
});
|
|
|
|
|
|
+// 路由参数变化监听
|
|
|
+watch(
|
|
|
+ () => route.query,
|
|
|
+ (newQuery, oldQuery) => {
|
|
|
+ // 只有当id变化时才更新数据,避免不必要的刷新
|
|
|
+ if (newQuery.id && newQuery.id !== oldQuery?.id) {
|
|
|
+ // 更新相关数据
|
|
|
+ personId.value = newQuery.id;
|
|
|
+ personName.value = newQuery.name;
|
|
|
+ personIntroduce.value = newQuery.message;
|
|
|
+ personImage.value = newQuery.image;
|
|
|
+ selectedImage.value = newQuery.image;
|
|
|
+
|
|
|
+ // 重新初始化对话
|
|
|
+ CreateDialogue({ roleId: newQuery.id })
|
|
|
+ .then((res) => {
|
|
|
+ activeConversationId.value = res.data;
|
|
|
+ })
|
|
|
+ .catch((error) => {
|
|
|
+ console.error("请求出错:", error);
|
|
|
+ });
|
|
|
|
|
|
+ getConversation(newQuery.id);
|
|
|
|
|
|
-
|
|
|
+ // 重置消息列表和默认消息显示状态
|
|
|
+ activeMessageList.value = [];
|
|
|
+ showDefaultMessages.value = true;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ { immediate: true, deep: true }
|
|
|
+);
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
@@ -827,7 +868,7 @@ onMounted(async () => {
|
|
|
.input-section {
|
|
|
display: flex;
|
|
|
padding: rpx(10);
|
|
|
- gap: rpx(10);
|
|
|
+ gap: rpx(5);
|
|
|
|
|
|
.speech-btn {
|
|
|
padding: rpx(5) rpx(10);
|
|
|
@@ -852,6 +893,17 @@ onMounted(async () => {
|
|
|
color: #666;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ // 终止按钮样式
|
|
|
+ .stop-btn {
|
|
|
+ cursor: pointer;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ img {
|
|
|
+ width: rpx(20);
|
|
|
+ height: rpx(20);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
.input-section input {
|
|
|
flex: 1;
|
|
|
@@ -872,6 +924,6 @@ onMounted(async () => {
|
|
|
font-size: rpx(7);
|
|
|
border-radius: rpx(5);
|
|
|
cursor: pointer;
|
|
|
- box-shadow: 0 4px 8px rgba(202, 52, 52, 0.3);
|
|
|
+ box-shadow: 0 0px 2px rgba(0, 0, 0, 0.3);
|
|
|
}
|
|
|
</style>
|