From 804bafc5e50c0b3f2513cd3502ae5669a6889e62 Mon Sep 17 00:00:00 2001 From: chenxt <10125295+chen-xintao97@user.noreply.gitee.com> Date: Fri, 6 Feb 2026 14:47:19 +0800 Subject: [PATCH] ai --- src/mobile/Ai/AiChatStyles.css | 25 ++++++++++++++++--------- src/mobile/Ai/AiChatStyles.less | 28 ++++++++++++++++++---------- src/mobile/Ai/newAi.jsx | 748 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 3 files changed, 599 insertions(+), 202 deletions(-) diff --git a/src/mobile/Ai/AiChatStyles.css b/src/mobile/Ai/AiChatStyles.css index b1aab5c..4fa8748 100644 --- a/src/mobile/Ai/AiChatStyles.css +++ b/src/mobile/Ai/AiChatStyles.css @@ -370,6 +370,11 @@ body { border-radius: 25px; margin-bottom: 10px; color: white; + z-index: 201; + position: absolute; + bottom: 120px; + left: 50%; + transform: translateX(-50%); } .voice-mode-indicator .voice-wave { display: flex; @@ -404,18 +409,20 @@ body { font-size: 14px; font-weight: 500; } -.voice-mode-indicator .voice-cancel-btn { - padding: 4px 12px; - background: rgba(255, 255, 255, 0.2); +.voice-cancel-btn { + padding: 8px 16px; + background: linear-gradient(90deg, #667eea, #764ba2); border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: 12px; + border-radius: 16px; color: white; - font-size: 12px; + font-size: 16px; cursor: pointer; transition: all 0.2s; -} -.voice-mode-indicator .voice-cancel-btn:hover { - background: rgba(255, 255, 255, 0.3); + z-index: 210; + position: absolute; + bottom: 120px; + left: 50%; + transform: translateX(-50%); } @keyframes wave { 0%, @@ -447,7 +454,7 @@ body { } .phone-model .phone-phone { position: absolute; - bottom: 40px; + bottom: 20px; left: 50%; transform: translateX(-50%); font-size: 50px; diff --git a/src/mobile/Ai/AiChatStyles.less b/src/mobile/Ai/AiChatStyles.less index c8842bf..e455700 100644 --- a/src/mobile/Ai/AiChatStyles.less +++ b/src/mobile/Ai/AiChatStyles.less @@ -402,6 +402,11 @@ body { border-radius: 25px; margin-bottom: 10px; color: white; + z-index: 201; + position: absolute; + bottom: 120px; + left: 50%; + transform: translateX(-50%); .voice-wave { display: flex; @@ -430,22 +435,25 @@ body { font-weight: 500; } + +} .voice-cancel-btn { - padding: 4px 12px; - background: rgba(255,255,255,0.2); + padding: 8px 16px; + background: linear-gradient(90deg, #667eea, #764ba2); border: 1px solid rgba(255,255,255,0.3); - border-radius: 12px; + border-radius: 16px; color: white; - font-size: 12px; + font-size: 16px; cursor: pointer; transition: all 0.2s; + z-index: 210; + position: absolute; + bottom: 120px; + left: 50%; + transform: translateX(-50%); - &:hover { - background: rgba(255,255,255,0.3); - } + } -} - @keyframes wave { 0%, 100% { height: 20%; } 50% { height: 100%; } @@ -470,7 +478,7 @@ body { } .phone-phone{ position: absolute; - bottom: 40px; + bottom: 20px; left: 50%; transform: translateX(-50%); font-size: 50px; diff --git a/src/mobile/Ai/newAi.jsx b/src/mobile/Ai/newAi.jsx index d353a71..5f99947 100644 --- a/src/mobile/Ai/newAi.jsx +++ b/src/mobile/Ai/newAi.jsx @@ -17,17 +17,24 @@ const ChatInterface = () => { const [currentModel, setCurrentModel] = useState('general'); const [chatHistory, setChatHistory] = useState([]); const [welcomeContent, setWelcomeContent] = useState(''); - vConsole = new VConsole(); + const phoneContentRef = useRef(null); + // vConsole = new VConsole(); // 语音输入状态 const [isRecording, setIsRecording] = useState(false); + const [isAiRecording, setIsAiRecording] = 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 [voiceMessages, setVoiceMessages] = useState([]); // 存储语音模式下的对话 + const silenceTimeoutRef = useRef(null); // 静默超时定时器 + const resetSilenceTimeoutRef = useRef(null); const messagesEndRef = useRef(null); const inputRef = useRef(null); @@ -37,6 +44,8 @@ const ChatInterface = () => { const inputNodeRef = useRef(null); const recordingTimerRef = useRef(null); const isRecordingRef = useRef(false); + // ==================== 关键修复:使用 ref 存储最新输入值 ==================== + const inputValueRef = useRef(inputValue); // ==================== 配置 ==================== const CONFIG = { @@ -98,10 +107,15 @@ const ChatInterface = () => { if (wsRef.current) { wsRef.current.hasReceivedSpeech = true; } - setInputValue(prev => { - const separator = prev && !prev.endsWith(' ') ? ' ' : ''; - return prev ? `${prev}${separator}${res.text}` : res.text; - }); + // ==================== 关键修复:更新 ref 和 state ==================== + const newValue = inputValueRef.current + ? `${inputValueRef.current} ${res.text}`.trim() + : res.text; + inputValueRef.current = newValue; + setInputValue(newValue); + + // 重置静默检测(收到语音后重新计时2秒) + resetSilenceTimeout(); } // 👇 新增:处理 flush 完成 @@ -113,6 +127,7 @@ const ChatInterface = () => { // 只有在语音模式下才清空(避免干扰手动输入) if (isVoiceMode) { setInputValue(''); + inputValueRef.current = ''; } }, 100); } @@ -126,7 +141,7 @@ const ChatInterface = () => { console.log("语音识别WebSocket连接断开"); setIsWsConnected(false); if (isRecordingRef.current) { - stopRecording(); + stopRecordingOnly(); } }; @@ -138,6 +153,278 @@ const ChatInterface = () => { wsRef.current = ws; }, []); + // ==================== 关键修复:重置静默检测定时器 ==================== + const resetSilenceTimeout = useCallback(() => { + // 清除旧的定时器 + if (silenceTimeoutRef.current) { + clearTimeout(silenceTimeoutRef.current); + } + + // 重新设置2秒静默检测 + silenceTimeoutRef.current = setTimeout(() => { + console.log('2秒内未检测到语音,自动处理'); + const latestInput = inputValueRef.current.trim(); + console.log("🚀 ~ 当前输入值:", latestInput); + + if (latestInput) { + handleSendMessageWithContent(latestInput); + } else { + // 没有内容:仅停止当前录音,不清空弹窗,也不关闭 isRecordingModel + stopRecordingOnly(); + Toast.show('未检测到语音,请继续说话'); + + // 👇 关键:2秒后自动开始下一轮录音(保持连续对话) + setTimeout(() => { + if (isRecordingModel) { + // startRecordingForContinue(); + } + }, 500); + } + }, 3000); + }, []); + + // ==================== 关键修复:独立的发送消息函数(接收参数) ==================== + const handleSendMessageWithContent = useCallback(async (messageContent) => { + if (!messageContent || isLoading) return; + + // 先停止录音(不关闭弹窗) + stopRecordingOnly(); + + let currentSessionId = sessionId; + if (!currentSessionId) { + currentSessionId = generateRandomString(20); + setSessionId(currentSessionId); + } + + setIsLoading(true); + setIsAiRecording(true); + + // ==================== 修改:添加到主消息列表和语音消息列表 ==================== + const userMsg = { + id: `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + type: 'user', + content: messageContent, + time: getCurrentTime(), + isError: false + }; + + // 添加到主消息列表(后台记录) + setMessages(prev => [...prev, userMsg]); + // 添加到语音弹窗的消息列表(展示用) + setVoiceMessages(prev => [...prev, userMsg]); + + const newHistoryItem = { role: 'user', content: messageContent, timestamp: Date.now() }; + setChatHistory(prev => { + const updated = [...prev, newHistoryItem]; + return updated.length > CONFIG.maxHistory ? updated.slice(-CONFIG.maxHistory) : updated; + }); + + try { + const endpoint = currentModel === 'process' ? CONFIG.endpoints.process : CONFIG.endpoints.chat; + const requestData = { + message: messageContent, + modelType: currentModel, + sUserId, + sUserType, + sessionId: currentSessionId + }; + + const response = await fetch(`${CONFIG.backendUrl}${endpoint}`, { + method: 'POST', + headers: CONFIG.headers, + body: JSON.stringify(requestData) + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + + if (data.data) { + const aiMsg = { + id: `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + type: 'ai', + content: data.data, + time: getCurrentTime(), + isError: false + }; + + // 添加到主消息列表 + setMessages(prev => [...prev, aiMsg]); + // 添加到语音弹窗的消息列表(展示用) + setVoiceMessages(prev => [...prev, aiMsg]); + + setChatHistory(prev => { + const updated = [...prev, { role: 'assistant', content: data.data, timestamp: Date.now() }]; + return updated.length > CONFIG.maxHistory ? updated.slice(-CONFIG.maxHistory) : updated; + }); + } + } catch (error) { + console.error('请求失败:', error); + const errorMessage = ` +抱歉,请求出现错误:${error.message} + +**可能的原因:** +1. Spring Boot 后端服务未启动 +2. API 接口路径不正确 +3. 网络连接问题 + +**检查步骤:** +1. 确保后端服务在端口 8099 运行 +2. 检查浏览器控制台查看详细错误 +3. 刷新页面重试 + `; + const errorMsg = { + id: `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + type: 'ai', + content: errorMessage, + time: getCurrentTime(), + isError: true + }; + + setMessages(prev => [...prev, errorMsg]); + setVoiceMessages(prev => [...prev, errorMsg]); + } finally { + setIsLoading(false); + // setIsAiRecording(false); + // ==================== 关键:不关闭 isRecordingModel,只清空输入准备下一轮 ==================== + setInputValue(''); + inputValueRef.current = ''; + // 重新开始录音,实现连续对话 + setTimeout(() => { + if (isRecordingModel) { + // startRecordingForContinue(); + } + }, 500); + } + }, [sessionId, currentModel, sUserId, sUserType, isLoading, isRecordingModel]); + + // ==================== 连续对话的录音启动(不重复显示弹窗) ==================== + const startRecordingForContinue = async () => { + if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { + Toast.show('浏览器不支持麦克风'); + return; + } + + try { + // 重置输入值 + setInputValue(''); + inputValueRef.current = ''; + + if (!isWsConnected || !wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) { + connectWebSocket(); + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) { + Toast.show('语音服务未连接'); + return; + } + + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + audioContextRef.current = new (window.AudioContext || window.webkitAudioContext)(); + inputNodeRef.current = audioContextRef.current.createMediaStreamSource(stream); + scriptProcessorRef.current = audioContextRef.current.createScriptProcessor(2048, 1, 1); + + scriptProcessorRef.current.onaudioprocess = (event) => { + if (!isRecordingRef.current) return; + const inputData = event.inputBuffer.getChannelData(0); + const resampledData = resampleAudio(inputData, audioContextRef.current.sampleRate, CONFIG.sampleRate); + const pcmData = float32ToInt16(resampledData); + if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { + wsRef.current.send(pcmData); + } + }; + + inputNodeRef.current.connect(scriptProcessorRef.current); + scriptProcessorRef.current.connect(audioContextRef.current.destination); + + // 关键:重置标志 + wsRef.current.hasReceivedSpeech = false; + + // 启动首次静默检测 + silenceTimeoutRef.current = setTimeout(() => { + console.log('2秒内未检测到语音,自动处理(连续对话)'); + const latestInput = inputValueRef.current.trim(); + console.log("🚀 ~ 当前输入值:", latestInput); + + if (latestInput) { + handleSendMessageWithContent(latestInput); + } else { + stopRecordingOnly(); + Toast.show('请继续说话...'); + + // 👇 自动开始下一轮录音 + setTimeout(() => { + if (isRecordingModel) { + // startRecordingForContinue(); + } + }, 500); + } + }, 2000); + + // 更新录音状态 + isRecordingRef.current = true; + setIsRecording(true); + setIsVoiceMode(true); + setRecordingDuration(0); + + recordingTimerRef.current = setInterval(() => { + setRecordingDuration(prev => prev + 1); + }, 1000); + + } catch (e) { + console.error("录音启动失败:", e); + Toast.show('录音启动失败:' + (e.message || '未知错误')); + isRecordingRef.current = false; + setIsRecording(false); + } + }; + useEffect(() => { + // 确保在语音模式下才滚动 + if (isRecordingModel) { + // 使用微任务或小延迟确保 DOM 已更新 + setTimeout(scrollToPhoneBottom, 50); + } + }, [voiceMessages, inputValue, isLoading, isRecordingModel]); + // ==================== 关键修复:仅停止录音(不发送消息,不关闭弹窗) ==================== + const stopRecordingOnly = 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; + } + + if (inputNodeRef.current) { + inputNodeRef.current.disconnect(); + inputNodeRef.current = null; + } + if (scriptProcessorRef.current) { + scriptProcessorRef.current.disconnect(); + scriptProcessorRef.current = null; + } + if (audioContextRef.current) { + audioContextRef.current.close(); + audioContextRef.current = null; + } + + // 发送刷新指令获取最终结果 + sendCommand("flush"); + }, [sendCommand]); + // 断开WebSocket const disconnectWebSocket = useCallback(() => { if (wsRef.current) { @@ -178,7 +465,7 @@ const ChatInterface = () => { return result; }; - // 开始录音 + // 开始录音(首次打开弹窗) const startRecording = async () => { if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { Toast.show('浏览器不支持麦克风'); @@ -186,6 +473,12 @@ const ChatInterface = () => { } try { + // 清空语音消息列表(新会话开始) + setVoiceMessages([]); + // 重置输入值 + setInputValue(''); + inputValueRef.current = ''; + if (!isWsConnected) { connectWebSocket(); await new Promise(resolve => setTimeout(resolve, 1000)); @@ -214,18 +507,21 @@ const ChatInterface = () => { inputNodeRef.current.connect(scriptProcessorRef.current); scriptProcessorRef.current.connect(audioContextRef.current.destination); - // 👇 关键:重置标志 + 启动静默检测 - let hasReceivedSpeech = false; // 闭包变量,记录是否收到语音 - - // 保存到 ref,供 onmessage 使用 + // 关键:重置标志 wsRef.current.hasReceivedSpeech = false; - // 设置 3 秒静默超时 + // ==================== 关键修复:启动首次静默检测 ==================== silenceTimeoutRef.current = setTimeout(() => { - if (!wsRef.current?.hasReceivedSpeech) { - console.log('3秒内未检测到语音,自动停止录音'); - Toast.show('未检测到语音,请重新尝试'); - handleSendMessage() + console.log('2秒内未检测到语音,自动发送并停止录音'); + const latestInput = inputValueRef.current.trim(); + console.log("🚀 ~ 当前输入值:", latestInput); + + if (latestInput) { + handleSendMessageWithContent(latestInput); + } else { + stopRecordingOnly(); + // setIsRecordingModel(false); // 没有内容时关闭弹窗 + Toast.show('未检测到语音输入'); } }, 3000); @@ -246,19 +542,94 @@ const ChatInterface = () => { setIsRecording(false); } }; + // 用于打断/连续对话,不清空 voiceMessages + const startRecordingWithoutClear = async () => { + if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { + Toast.show('浏览器不支持麦克风'); + return; + } + + try { + // ❌ 不清空 voiceMessages! + // 重置输入值(但保留历史语音消息) + setInputValue(''); + inputValueRef.current = ''; + + if (!isWsConnected || !wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) { + connectWebSocket(); + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) { + Toast.show('语音服务未连接'); + return; + } + + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + audioContextRef.current = new (window.AudioContext || window.webkitAudioContext)(); + inputNodeRef.current = audioContextRef.current.createMediaStreamSource(stream); + scriptProcessorRef.current = audioContextRef.current.createScriptProcessor(2048, 1, 1); + + scriptProcessorRef.current.onaudioprocess = (event) => { + if (!isRecordingRef.current) return; + const inputData = event.inputBuffer.getChannelData(0); + const resampledData = resampleAudio(inputData, audioContextRef.current.sampleRate, CONFIG.sampleRate); + const pcmData = float32ToInt16(resampledData); + if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { + wsRef.current.send(pcmData); + } + }; + + inputNodeRef.current.connect(scriptProcessorRef.current); + scriptProcessorRef.current.connect(audioContextRef.current.destination); + + wsRef.current.hasReceivedSpeech = false; + + // 静默检测(可复用) + silenceTimeoutRef.current = setTimeout(() => { + const latestInput = inputValueRef.current.trim(); + if (latestInput) { + handleSendMessageWithContent(latestInput); + } else { + stopRecordingOnly(); + setIsAiRecording(true) + // Toast.show('请继续说话...'); + // startRecordingForContinue() + // 可选:自动重试 + } + }, 3000); + + isRecordingRef.current = true; + setIsRecording(true); + setIsVoiceMode(true); + setRecordingDuration(0); + + recordingTimerRef.current = setInterval(() => { + setRecordingDuration(prev => prev + 1); + }, 1000); + + } catch (e) { + console.error("录音启动失败:", e); + Toast.show('录音启动失败:' + (e.message || '未知错误')); + isRecordingRef.current = false; + setIsRecording(false); + } + }; + // 停止录音并关闭弹窗(手动挂断) + const stopRecordingAndClose = useCallback(() => { + console.log("停止录音并关闭弹窗"); - // 停止录音 - 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; @@ -277,32 +648,42 @@ const ChatInterface = () => { audioContextRef.current = null; } - // 发送刷新指令获取最终结果 sendCommand("flush"); - setTimeout(() => { - setInputValue('') - }, 500); + + // 关闭弹窗并清空 + // setIsRecordingModel(false); + setVoiceMessages([]); + setInputValue(''); + inputValueRef.current = ''; }, [sendCommand]); // 切换录音状态(点击按钮) const toggleRecording = useCallback(() => { if (isRecordingRef.current) { - // 正在录音,停止 - stopRecording(); + // 正在录音,停止(手动停止也触发发送) + const latestInput = inputValueRef.current.trim(); + if (latestInput) { + handleSendMessageWithContent(latestInput); + } else { + stopRecordingAndClose(); + } } else { // 未录音,开始 startRecording(); } - }, [stopRecording]); + }, [stopRecordingAndClose, handleSendMessageWithContent]); // 取消录音并清空 const cancelRecording = useCallback(() => { if (isRecordingRef.current) { - stopRecording(); + stopRecordingOnly(); } setInputValue(''); + inputValueRef.current = ''; setIsVoiceMode(false); - }, [stopRecording]); + setIsRecordingModel(false); + setVoiceMessages([]); + }, [stopRecordingOnly]); // 格式化录音时长 const formatDuration = (seconds) => { @@ -352,9 +733,9 @@ const ChatInterface = () => { } if (e.key === 'Escape') { setInputValue(''); + inputValueRef.current = ''; if (isRecordingRef.current) { - stopRecording(); - setIsVoiceMode(false); + stopRecordingAndClose(); } } }; @@ -367,12 +748,15 @@ const ChatInterface = () => { if (recordingTimerRef.current) { clearInterval(recordingTimerRef.current); } + if (silenceTimeoutRef.current) { + clearTimeout(silenceTimeoutRef.current); + } }; }, []); useEffect(() => { scrollToBottom(); - }, [messages, isLoading]); + }, [messages, isLoading, voiceMessages]); // ==================== 消息处理 ==================== const addMessage = (content, type, isError = false) => { @@ -387,15 +771,12 @@ const ChatInterface = () => { return newMessage.id; }; + // 原有的 handleSendMessage 保留给手动输入使用 const handleSendMessage = async () => { const message = inputValue.trim(); - if (!message || isLoading) return; + console.log("🚀 ~ handleSendMessage ~ message:", message); - // 如果正在录音,先停止 - if (isRecordingRef.current) { - stopRecording(); - setIsVoiceMode(false); - } + if (!message || isLoading) return; let currentSessionId = sessionId; if (!currentSessionId) { @@ -404,6 +785,7 @@ const ChatInterface = () => { } setInputValue(''); + inputValueRef.current = ''; setIsLoading(true); addMessage(message, 'user'); @@ -478,9 +860,9 @@ const ChatInterface = () => { setChatHistory([]); setSessionId(''); setInputValue(''); + inputValueRef.current = ''; if (isRecordingRef.current) { - stopRecording(); - setIsVoiceMode(false); + stopRecordingAndClose(); } } }; @@ -499,38 +881,76 @@ const ChatInterface = () => { setMessages(prev => prev.filter(msg => msg.id !== messageId)); setInputValue(content); + inputValueRef.current = content; setTimeout(() => handleSendMessage(), 100); }; const handleModelChange = (e) => { setCurrentModel(e.target.value); }; + + // ==================== 渲染消息组件(复用) ==================== + const renderMessage = (msg) => ( +
+
+
+ {msg.type === 'ai' ? ( + ( + inline ? ( + + {children} + + ) : ( +
+                      {children}
+                    
+ ) + ) + }} + > + {msg.content} +
+ ) : ( + msg.content + )} +
+
+ {msg.time} + {msg.type === 'ai' && !msg.isWelcome && ( +
+ + +
+ )} +
+
+
+ ); + const scrollToPhoneBottom = () => { + if (phoneContentRef.current) { + phoneContentRef.current.scrollTop = phoneContentRef.current.scrollHeight; + } + }; // ==================== 渲染 ==================== return (
{/* 头部 */} - {/*
-
-

小羚羊Ai-agent智能体

-

AI 印刷助手

-
-
- - -
-
*/} - -
- )} - - - - ))} + {messages.map(renderMessage)} {/* 打字机效果 */} {isLoading && ( @@ -613,28 +982,6 @@ const ChatInterface = () => { {/* 输入区域 */}
- {/* 语音模式提示 - 仅在录音时显示 */} - {isRecording && ( -
-
- - - - - -
- - 正在录音 {formatDuration(recordingDuration)} - - -
- )} -
{ onChange={(e) => { if (!isRecording) { setInputValue(e.target.value); + inputValueRef.current = e.target.value; } }} onKeyPress={(e) => { @@ -662,61 +1010,95 @@ const ChatInterface = () => { }} /> - {/* 语音按钮 - 点击切换录音状态 */} - {/* - - */}
- - { - isRecordingModel ? -
-
-
- {inputValue.trim() && ( -
-
-
- {inputValue} -
-
- {getCurrentTime()} -
+ + {/* ==================== 语音对话弹窗 ==================== */} + {isRecordingModel && ( +
+
+ + {/* 消息展示区域 - 展示 voiceMessages 中的对话 */} +
+ {voiceMessages.map(renderMessage)} + + {/* 当前正在输入的语音转文字(实时显示) */} + {inputValue.trim() && isRecording && ( +
+
+
+ {inputValue} +
+
+ {getCurrentTime()} + + (识别中...) +
- )} +
+ )} + + {/* AI 思考中效果 */} + {isLoading && ( +
+
+
+
+
+ AI 正在思考... +
+
+ )} + +
+
+ + {/* 录音状态指示器 */} + {isRecording && ( +
+
+ + + + + +
+ + 正在录音 {formatDuration(recordingDuration)} +
-
- { + )} + + {/* AI 说话时的打断按钮 */} + {isAiRecording && ( + + )} + + {/* 挂断按钮 */} +
+ { + stopRecordingAndClose(); setIsRecordingModel(false) - stopRecording() - }} /> -
-
: '' - } + }} + /> +
+
+ )}
); }; -- libgit2 0.22.2