From d5be7bb1383fefba2990d4ffd8f31e8b96dcf68b Mon Sep 17 00:00:00 2001 From: chenxt <10125295+chen-xintao97@user.noreply.gitee.com> Date: Fri, 6 Feb 2026 08:57:05 +0800 Subject: [PATCH] ai --- src/mobile/Ai/AiChatStyles.css | 4 +++- src/mobile/Ai/AiChatStyles.less | 4 +++- src/mobile/Ai/newAi.jsx | 188 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------------------------------------------------- src/utils/config.js | 2 +- 4 files changed, 125 insertions(+), 73 deletions(-) diff --git a/src/mobile/Ai/AiChatStyles.css b/src/mobile/Ai/AiChatStyles.css index 56714a0..b1aab5c 100644 --- a/src/mobile/Ai/AiChatStyles.css +++ b/src/mobile/Ai/AiChatStyles.css @@ -460,6 +460,8 @@ body { background-color: #f3f3f3; z-index: 210; left: 50%; - top: 10%; + top: 2%; transform: translateX(-50%); + padding: 10px; + overflow-y: auto; } diff --git a/src/mobile/Ai/AiChatStyles.less b/src/mobile/Ai/AiChatStyles.less index 0f58a59..c8842bf 100644 --- a/src/mobile/Ai/AiChatStyles.less +++ b/src/mobile/Ai/AiChatStyles.less @@ -483,7 +483,9 @@ body { background-color: #f3f3f3; z-index: 210; left: 50%; - top: 10%; + top: 2%; transform: translateX(-50%); + padding: 10px; + overflow-y: auto; } } \ No newline at end of file diff --git a/src/mobile/Ai/newAi.jsx b/src/mobile/Ai/newAi.jsx index 91de968..d353a71 100644 --- a/src/mobile/Ai/newAi.jsx +++ b/src/mobile/Ai/newAi.jsx @@ -1,10 +1,11 @@ import React, { useState, useEffect, useRef, useCallback } from 'react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; -import { Button } from "antd-mobile"; -import { LocationOutline, PhoneFill } from "antd-mobile-icons"; +import { PhoneFill, LocationOutline } from "antd-mobile-icons"; import './AiChatStyles.less'; - +import { Toast, Input, Tabs, Selector, Grid, Image, Button, Checkbox, Switch, Dialog, Radio, Space, CenterPopup, ImageViewer, Collapse, CapsuleTabs } from "antd-mobile"; +import VConsole from 'vconsole'; +let vConsole; const ChatInterface = () => { // ==================== 状态管理 ==================== const [sessionId, setSessionId] = useState(''); @@ -16,13 +17,17 @@ const ChatInterface = () => { const [currentModel, setCurrentModel] = useState('general'); const [chatHistory, setChatHistory] = useState([]); const [welcomeContent, setWelcomeContent] = useState(''); + vConsole = new VConsole(); + // 语音输入状态 - const [isVoiceModel, setIsVoiceModel] = useState(false); const [isRecording, setIsRecording] = useState(false); + const [isRecordingModel, setIsRecordingModel] = useState(false); const [isWsConnected, setIsWsConnected] = useState(false); const [isVoiceMode, setIsVoiceMode] = useState(false); const [recordingDuration, setRecordingDuration] = useState(0); + const [isFlushing, setIsFlushing] = useState(false); + const silenceTimeoutRef = useRef(null); // 静默超时定时器 const messagesEndRef = useRef(null); const inputRef = useRef(null); @@ -87,15 +92,30 @@ const ChatInterface = () => { ws.onmessage = (event) => { try { const res = JSON.parse(event.data); - if (res.code === 0 && (res.msg === "success" || res.msg === "flush_success")) { - if (res.text && res.text.trim()) { + if (res.code === 0) { + // 处理普通识别结果 + if ((res.msg === "success" || res.msg === "partial") && res.text?.trim()) { + if (wsRef.current) { + wsRef.current.hasReceivedSpeech = true; + } setInputValue(prev => { const separator = prev && !prev.endsWith(' ') ? ' ' : ''; - const newValue = prev ? `${prev}${separator}${res.text}` : res.text; - console.log('语音识别结果:', res.text, '更新后:', newValue); - return newValue; + return prev ? `${prev}${separator}${res.text}` : res.text; }); } + + // 👇 新增:处理 flush 完成 + if (res.msg === "flush_success") { + console.log("Flush 完成,语音识别结束"); + // 延迟一点确保所有消息处理完毕 + setTimeout(() => { + setIsFlushing(false); + // 只有在语音模式下才清空(避免干扰手动输入) + if (isVoiceMode) { + setInputValue(''); + } + }, 100); + } } } catch (e) { console.error("WebSocket消息解析失败:", e); @@ -161,20 +181,18 @@ const ChatInterface = () => { // 开始录音 const startRecording = async () => { if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { - alert("浏览器不支持麦克风采集"); + Toast.show('浏览器不支持麦克风'); return; } try { - // 先连接WebSocket if (!isWsConnected) { connectWebSocket(); await new Promise(resolve => setTimeout(resolve, 1000)); } - // 检查 WebSocket 是否已连接 if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) { - alert("语音识别服务未连接,请重试"); + Toast.show('语音服务未连接'); return; } @@ -183,7 +201,6 @@ const ChatInterface = () => { inputNodeRef.current = audioContextRef.current.createMediaStreamSource(stream); scriptProcessorRef.current = audioContextRef.current.createScriptProcessor(2048, 1, 1); - // 使用 ref 来检查录音状态,避免闭包问题 scriptProcessorRef.current.onaudioprocess = (event) => { if (!isRecordingRef.current) return; const inputData = event.inputBuffer.getChannelData(0); @@ -197,19 +214,34 @@ const ChatInterface = () => { inputNodeRef.current.connect(scriptProcessorRef.current); scriptProcessorRef.current.connect(audioContextRef.current.destination); - // 关键:先设置 ref,再设置 state + // 👇 关键:重置标志 + 启动静默检测 + let hasReceivedSpeech = false; // 闭包变量,记录是否收到语音 + + // 保存到 ref,供 onmessage 使用 + wsRef.current.hasReceivedSpeech = false; + + // 设置 3 秒静默超时 + silenceTimeoutRef.current = setTimeout(() => { + if (!wsRef.current?.hasReceivedSpeech) { + console.log('3秒内未检测到语音,自动停止录音'); + Toast.show('未检测到语音,请重新尝试'); + handleSendMessage() + } + }, 3000); + + // 更新录音状态 isRecordingRef.current = true; setIsRecording(true); setIsVoiceMode(true); setRecordingDuration(0); - // 开始计时 recordingTimerRef.current = setInterval(() => { setRecordingDuration(prev => prev + 1); }, 1000); + } catch (e) { console.error("录音启动失败:", e); - alert("录音启动失败:" + e.message); + Toast.show('录音启动失败:' + (e.message || '未知错误')); isRecordingRef.current = false; setIsRecording(false); } @@ -218,10 +250,15 @@ const ChatInterface = () => { // 停止录音 const stopRecording = useCallback(() => { console.log("停止录音"); + // 👇 清理静默超时 + if (silenceTimeoutRef.current) { + clearTimeout(silenceTimeoutRef.current); + silenceTimeoutRef.current = null; + } isRecordingRef.current = false; setIsRecording(false); setIsVoiceMode(false); - + setIsFlushing(true); if (recordingTimerRef.current) { clearInterval(recordingTimerRef.current); recordingTimerRef.current = null; @@ -242,6 +279,9 @@ const ChatInterface = () => { // 发送刷新指令获取最终结果 sendCommand("flush"); + setTimeout(() => { + setInputValue('') + }, 500); }, [sendCommand]); // 切换录音状态(点击按钮) @@ -406,17 +446,17 @@ const ChatInterface = () => { } catch (error) { console.error('请求失败:', error); const errorMessage = ` - 抱歉,请求出现错误:${error.message} +抱歉,请求出现错误:${error.message} - **可能的原因:** - 1. Spring Boot 后端服务未启动 - 2. API 接口路径不正确 - 3. 网络连接问题 +**可能的原因:** +1. Spring Boot 后端服务未启动 +2. API 接口路径不正确 +3. 网络连接问题 - **检查步骤:** - 1. 确保后端服务在端口 8099 运行 - 2. 检查浏览器控制台查看详细错误 - 3. 刷新页面重试 +**检查步骤:** +1. 确保后端服务在端口 8099 运行 +2. 检查浏览器控制台查看详细错误 +3. 刷新页面重试 `; addMessage(errorMessage, 'ai', true); } finally { @@ -465,15 +505,32 @@ const ChatInterface = () => { const handleModelChange = (e) => { setCurrentModel(e.target.value); }; - // 录音弹窗 - const handlePhone = () => { - setIsVoiceModel(true) - startRecording(); - } - // ==================== 渲染 ==================== return (
+ {/* 头部 */} + {/*
+
+

小羚羊Ai-agent智能体

+

AI 印刷助手

+
+
+ + +
+
*/}
- )} */} + )}
{ disabled={isLoading} readOnly={isRecording} /> - - + { + setIsRecordingModel(true) + startRecording() + }} /> {/* 语音按钮 - 点击切换录音状态 */} @@ -618,9 +677,9 @@ const ChatInterface = () => { 点击录音 )} - */} + - {/*
+ { - isVoiceModel ? -
+ isRecordingModel ? +
- -
- {/* 语音模式提示 - 仅在录音时显示 */} - {isRecording && ( -
-
- - - - - + {inputValue.trim() && ( +
+
+
+ {inputValue} +
+
+ {getCurrentTime()} +
+
- - 正在录音 {formatDuration(recordingDuration)} - - -
- )} -
{ - stopRecording() - setIsVoiceModel(false) - }}> - + )} +
+
+ { + setIsRecordingModel(false) + stopRecording() + }} />
-
: '' } -
); }; diff --git a/src/utils/config.js b/src/utils/config.js index 41f86ca..81eb486 100644 --- a/src/utils/config.js +++ b/src/utils/config.js @@ -9,7 +9,7 @@ const API = process.env.API; const bHttps = false; export const webSite = { faceAddress: isDev ? '//km5cjx.gnway.cc:36867/xlyFace' : '//' + location.host + '/xlyFace', - ipAddress: localStorage.ipAddress ? localStorage.ipAddress : isDev ? '//118.178.19.35:9198/xlyEntry_saas/' : '//' + location.host + '/xlyEntry/', + ipAddress: localStorage.ipAddress ? localStorage.ipAddress : isDev ? '//118.178.19.35:9198/xlyEntry_saas/' : '//118.178.19.35:9198/xlyEntry_saas/', // ipAddress: localStorage.ipAddress ? localStorage.ipAddress : isDev ? '//118.178.19.35:8088/xlyEntry/' : '//' + location.host + '/xlyEntry/', // ipAddress: localStorage.ipAddress ? localStorage.ipAddress : isDev ? '//km5cjx.gnway.cc:36867/xlyEntry/' : '//' + location.host + '/xlyEntry/', -- libgit2 0.22.2