|
|
@@ -1,7 +1,6 @@
|
|
|
<template>
|
|
|
<!-- 智能绘画 -->
|
|
|
<div class="home-container">
|
|
|
-
|
|
|
<!-- 展开收起侧边栏 -->
|
|
|
<div
|
|
|
class="icon-expand"
|
|
|
@@ -146,7 +145,28 @@
|
|
|
placeholder="描述任何画面..."
|
|
|
@keyup.enter="sendMessage"
|
|
|
/>
|
|
|
- <button @click="sendMessage">发送</button>
|
|
|
+ <!-- 语音输入按钮 -->
|
|
|
+ <button
|
|
|
+ @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>
|
|
|
+ </button>
|
|
|
+ <!-- 终止按钮 -->
|
|
|
+ <div
|
|
|
+ v-if="conversationInProgress"
|
|
|
+ @click="stopStream"
|
|
|
+ class="stop-btn"
|
|
|
+ title="终止问答"
|
|
|
+ >
|
|
|
+ <img :src="stopicon" alt="停止" />
|
|
|
+ </div>
|
|
|
+ <button v-if="!conversationInProgress"
|
|
|
+ @click="sendMessage">发送</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -182,6 +202,22 @@ import { saveRecord } from '@/api/personalized/index.js'
|
|
|
|
|
|
// 导入全局状态
|
|
|
import { globalState } from '@/utils/globalState.js'
|
|
|
+// 语音图标
|
|
|
+import { Microphone, Mute } from "@element-plus/icons-vue";
|
|
|
+// 终止按钮
|
|
|
+import stopicon from "@/assets/icon/stopicon.png";
|
|
|
+// 消息组件
|
|
|
+import {Message} from "@/utils/message/Message.js";
|
|
|
+
|
|
|
+// 语音输入响应式变量
|
|
|
+const isRecording = ref(false); // 录音状态
|
|
|
+const recognition = ref(null); // 语音识别实例
|
|
|
+const countdown = ref(0); // 倒计时剩余秒数
|
|
|
+const countdownTimer = ref(null); // 倒计时定
|
|
|
+// 对话状态变量
|
|
|
+const conversationInProgress = ref(false); // 对话是否正在进行中
|
|
|
+const conversationInAbortController = ref(); // 对话进行中 abort 控制器
|
|
|
+
|
|
|
|
|
|
// 返回上一页
|
|
|
const goBack = () => {
|
|
|
@@ -198,6 +234,11 @@ import human from '@/assets/icon/human.png'
|
|
|
import LeftPanel from '@/components/LeftPanel.vue'
|
|
|
const leftPanelRef = ref(null)
|
|
|
|
|
|
+
|
|
|
+// tts 语音
|
|
|
+import { useAudioPlayer } from '@/api/tts/useAudioPlayer';
|
|
|
+const { playAudioChunk } = useAudioPlayer();
|
|
|
+
|
|
|
// 添加抽屉显示状态
|
|
|
const drawerVisible = ref(true)
|
|
|
// 添加切换抽屉显示状态的函数
|
|
|
@@ -205,45 +246,6 @@ const toggleDrawer = () => {
|
|
|
drawerVisible.value = !drawerVisible.value
|
|
|
}
|
|
|
|
|
|
-// 跳转智能问答
|
|
|
-const navigateToAI = (group) => {
|
|
|
- if (group.title === "智能问答") {
|
|
|
- let person = { id: 10, name: '小智', image: NumberPeople00, message: '您好,我是您的AI智能助手小智,我会尽力回答您的问题或提供有用的建议!!!!' };
|
|
|
- router.push({
|
|
|
- // 跳转问答页面
|
|
|
- path: '/ai-questions',
|
|
|
- query: { id: person.id, name: person.name, image: person.image, message: person.message }
|
|
|
- });
|
|
|
- }
|
|
|
- if (group.title === "智能绘画") {
|
|
|
- router.push('/ai-painting')
|
|
|
- }
|
|
|
- if (group.title === '数字人老师') {
|
|
|
- router.push('/ai-laboratory') // 添加跳转到AI实验室的逻辑
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 渲染侧边栏
|
|
|
-const groupList = ref([
|
|
|
- {
|
|
|
- icon: question,
|
|
|
- title: '智能问答'
|
|
|
- },
|
|
|
- {
|
|
|
- icon: painting,
|
|
|
- title: '智能绘画'
|
|
|
- },
|
|
|
- {
|
|
|
- icon: human,
|
|
|
- title: '数字人老师'
|
|
|
- }
|
|
|
-])
|
|
|
-
|
|
|
-// 处理菜单展开和关闭事件
|
|
|
-const handleOpen = () => {}
|
|
|
-const handleClose = () => {}
|
|
|
-
|
|
|
-
|
|
|
const demoImageList = [demo1, demo2, demo3, demo4]
|
|
|
|
|
|
// 年级ID相关
|
|
|
@@ -267,11 +269,106 @@ onMounted(async () => {
|
|
|
|
|
|
// 消息列表和输入内容的响应式变量
|
|
|
const messages = ref([])
|
|
|
-
|
|
|
const inputMessage = ref('')
|
|
|
+
|
|
|
+// 初始化语音识别
|
|
|
+const initSpeechRecognition = () => {
|
|
|
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
|
+ if (!SpeechRecognition) {
|
|
|
+ alert("当前浏览器不支持语音输入功能");
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ const instance = new SpeechRecognition();
|
|
|
+ instance.lang = 'zh-CN';
|
|
|
+ instance.interimResults = false;
|
|
|
+
|
|
|
+ instance.onresult = (event) => {
|
|
|
+ if (event.results?.[0]?.[0]) {
|
|
|
+ inputMessage.value += event.results[0][0].transcript;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 识别器结束时清除定时器
|
|
|
+ instance.onend = () => {
|
|
|
+ clearInterval(countdownTimer.value);
|
|
|
+ isRecording.value = false;
|
|
|
+ countdown.value = 0;
|
|
|
+ };
|
|
|
+
|
|
|
+ instance.onerror = (event) => {
|
|
|
+ console.error('语音识别错误:', event.error);
|
|
|
+ clearInterval(countdownTimer.value); // 出错时清除定时器
|
|
|
+ isRecording.value = false;
|
|
|
+ Message().error('语音输入失败,请重试!', true)
|
|
|
+ countdown.value = 0;
|
|
|
+ };
|
|
|
+
|
|
|
+ return instance;
|
|
|
+};
|
|
|
+
|
|
|
+// 切换录音状态
|
|
|
+const toggleSpeechInput = () => {
|
|
|
+ // 清除可能存在的旧定时器
|
|
|
+ clearInterval(countdownTimer.value);
|
|
|
+ countdownTimer.value = null;
|
|
|
+
|
|
|
+ if (isRecording.value) {
|
|
|
+ // 手动停止时重置状态
|
|
|
+ countdown.value = 0;
|
|
|
+ recognition.value?.stop();
|
|
|
+ isRecording.value = false;
|
|
|
+ } else {
|
|
|
+ // 初始化倒计时前再次清除定时器(防止快速点击)
|
|
|
+ clearInterval(countdownTimer.value);
|
|
|
+ countdown.value = 10; // 重置为10秒
|
|
|
+
|
|
|
+ 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;
|
|
|
+ });
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 停止操作函数
|
|
|
+const stopStream = async () => {
|
|
|
+ // tip:如果 stream 进行中的 message,就需要调用 controller 结束
|
|
|
+ if (conversationInAbortController.value) {
|
|
|
+ conversationInAbortController.value.abort();
|
|
|
+ }
|
|
|
+ // 设置为 false
|
|
|
+ conversationInProgress.value = false;
|
|
|
+};
|
|
|
+
|
|
|
// 发送消息函数
|
|
|
const sendMessage = async() => {
|
|
|
if (inputMessage.value.trim()) {
|
|
|
+ // 创建 AbortController 实例,以便中止请求
|
|
|
+ conversationInAbortController.value = new AbortController();
|
|
|
+ // 标记对话进行中
|
|
|
+ conversationInProgress.value = true;
|
|
|
// messages.value.push(inputMessage.value.trim())
|
|
|
// 先保存内容 再置空输入框
|
|
|
let content = inputMessage.value;
|
|
|
@@ -286,7 +383,7 @@ const sendMessage = async() => {
|
|
|
})
|
|
|
|
|
|
// 递增消息计数器
|
|
|
- messageCount.value++
|
|
|
+ messageCount.value++
|
|
|
// 发送saveRecord请求 保存消息次数
|
|
|
try{
|
|
|
await saveRecord({
|
|
|
@@ -297,21 +394,30 @@ const sendMessage = async() => {
|
|
|
console.log('保存记录成功,消息次数:', messageCount.value);
|
|
|
}catch(error){
|
|
|
console.error('保存记录失败:', error);
|
|
|
+ conversationInProgress.value = false;
|
|
|
}
|
|
|
|
|
|
- CreatePainting({
|
|
|
- "modelId": 57,
|
|
|
- "prompt":content,
|
|
|
- "width":1024,
|
|
|
- "height":1024
|
|
|
- }).then(res=>{
|
|
|
- console.log("生成图片",res)
|
|
|
- //目前写死调用已生成的图片,全部通了后再改
|
|
|
- inProgressImageMap.value[res.data] = {id:res.data,status:AiImageStatusEnum.IN_PROGRESS}
|
|
|
- // inProgressImageMap.value[260] = {id:260,status:AiImageStatusEnum.IN_PROGRESS}
|
|
|
- })
|
|
|
+ try {
|
|
|
+ CreatePainting({
|
|
|
+ "modelId": 57,
|
|
|
+ "prompt":content,
|
|
|
+ "width":1024,
|
|
|
+ "height":1024
|
|
|
+ }).then(res=>{
|
|
|
+ console.log("生成图片",res)
|
|
|
+ //目前写死调用已生成的图片,全部通了后再改
|
|
|
+ inProgressImageMap.value[res.data] = {id:res.data,status:AiImageStatusEnum.IN_PROGRESS}
|
|
|
+ // inProgressImageMap.value[260] = {id:260,status:AiImageStatusEnum.IN_PROGRESS}
|
|
|
+ }).finally(() => {
|
|
|
+ // 图片生成请求完成后更新状态
|
|
|
+ conversationInProgress.value = false;
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ console.error('生成图片失败:', error);
|
|
|
+ conversationInProgress.value = false;
|
|
|
+ }
|
|
|
}
|
|
|
-}
|
|
|
+};
|
|
|
// 生成图片
|
|
|
import { ElIcon } from 'element-plus'
|
|
|
import {
|
|
|
@@ -593,10 +699,53 @@ const inProgressTimerFun = () => {
|
|
|
border-radius: rpx(5);
|
|
|
text-align: left; // 文字左对齐
|
|
|
}
|
|
|
+.image-list {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+.content-demo {
|
|
|
+ background-color: #f4f2fa;
|
|
|
+ border-radius: 15px;
|
|
|
+ padding: 30px 10px;
|
|
|
+}
|
|
|
+
|
|
|
.input-section {
|
|
|
display: flex;
|
|
|
padding: rpx(10);
|
|
|
- gap: rpx(10);
|
|
|
+ gap: rpx(5);
|
|
|
+ .speech-btn {
|
|
|
+ padding: rpx(5) rpx(10);
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #ffce1b;
|
|
|
+ border-radius: rpx(5);
|
|
|
+ cursor: pointer;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ &.recording {
|
|
|
+ background: #ffeeba;
|
|
|
+ border-color: #ffc107;
|
|
|
+
|
|
|
+ .el-icon {
|
|
|
+ color: #dc3545;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .el-icon {
|
|
|
+ font-size: rpx(8);
|
|
|
+ color: #666;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 终止按钮样式
|
|
|
+ .stop-btn {
|
|
|
+ cursor: pointer;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ img {
|
|
|
+ width: rpx(20);
|
|
|
+ height: rpx(20);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
.input-section input {
|
|
|
flex: 1;
|
|
|
@@ -617,17 +766,7 @@ const inProgressTimerFun = () => {
|
|
|
font-size: rpx(7);
|
|
|
border-radius: rpx(5);
|
|
|
cursor: pointer;
|
|
|
- box-shadow: 0 4px 8px rgba(202, 52, 52, 0.3);
|
|
|
-}
|
|
|
-.image-list {
|
|
|
- display: flex;
|
|
|
- flex-wrap: wrap;
|
|
|
-}
|
|
|
+ box-shadow: 0 0px 2px rgba(0, 0, 0, 0.3);
|
|
|
|
|
|
-
|
|
|
-.content-demo {
|
|
|
- background-color: #f4f2fa;
|
|
|
- border-radius: 15px;
|
|
|
- padding: 30px 10px;
|
|
|
}
|
|
|
</style>
|