import React, { useState, useEffect, useRef, useCallback } from 'react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; 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(''); const [sUserId] = useState('user-001'); const [sUserType] = useState('sysadmin'); const [messages, setMessages] = useState([]); const [inputValue, setInputValue] = useState(''); const [isLoading, setIsLoading] = useState(false); const [currentModel, setCurrentModel] = useState('general'); const [chatHistory, setChatHistory] = useState([]); const [welcomeContent, setWelcomeContent] = useState(''); 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); const wsRef = useRef(null); const audioContextRef = useRef(null); const scriptProcessorRef = useRef(null); const inputNodeRef = useRef(null); const recordingTimerRef = useRef(null); const isRecordingRef = useRef(false); // ==================== 关键修复:使用 ref 存储最新输入值 ==================== const inputValueRef = useRef(inputValue); // ==================== 配置 ==================== const CONFIG = { backendUrl: 'http://localhost:8099/xlyAi', wsUrl: 'ws://121.43.128.225:10096', // 语音识别WebSocket地址 sampleRate: 16000, endpoints: { chat: '/api/v1/chat/query', process: '/api/v1/chat/query', }, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, maxHistory: 20, }; // ==================== 工具函数 ==================== const getCurrentTime = () => { const now = new Date(); return `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`; }; const generateRandomString = (length) => { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let result = ''; for (let i = 0; i < length; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)); } return result; }; const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; // ==================== WebSocket 语音识别 ==================== // 连接语音识别WebSocket const connectWebSocket = useCallback(() => { if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { return; } const ws = new WebSocket(CONFIG.wsUrl); ws.binaryType = "arraybuffer"; ws.onopen = () => { console.log("语音识别WebSocket连接成功"); setIsWsConnected(true); }; ws.onmessage = (event) => { try { const res = JSON.parse(event.data); if (res.code === 0) { // 处理普通识别结果 if ((res.msg === "success" || res.msg === "partial") && res.text?.trim()) { if (wsRef.current) { wsRef.current.hasReceivedSpeech = true; } // ==================== 关键修复:更新 ref 和 state ==================== const newValue = inputValueRef.current ? `${inputValueRef.current} ${res.text}`.trim() : res.text; inputValueRef.current = newValue; setInputValue(newValue); // 重置静默检测(收到语音后重新计时2秒) resetSilenceTimeout(); } // 👇 新增:处理 flush 完成 if (res.msg === "flush_success") { console.log("Flush 完成,语音识别结束"); // 延迟一点确保所有消息处理完毕 setTimeout(() => { setIsFlushing(false); // 只有在语音模式下才清空(避免干扰手动输入) if (isVoiceMode) { setInputValue(''); inputValueRef.current = ''; } }, 100); } } } catch (e) { console.error("WebSocket消息解析失败:", e); } }; ws.onclose = () => { console.log("语音识别WebSocket连接断开"); setIsWsConnected(false); if (isRecordingRef.current) { stopRecordingOnly(); } }; ws.onerror = (err) => { console.error("WebSocket错误:", err); setIsWsConnected(false); }; 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) { wsRef.current.close(); wsRef.current = null; } setIsWsConnected(false); }, []); // 发送指令 const sendCommand = useCallback((action) => { if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) { return; } const cmd = JSON.stringify({ action }); wsRef.current.send(cmd); }, []); // Float32 转 Int16 PCM const float32ToInt16 = (float32Array) => { const int16Array = new Int16Array(float32Array.length); for (let i = 0; i < float32Array.length; i++) { let s = Math.max(-1, Math.min(1, float32Array[i])); int16Array[i] = s < 0 ? s * 0x8000 : s * 0x7FFF; } return new Uint8Array(int16Array.buffer); }; // 音频重采样 const resampleAudio = (data, originalRate, targetRate) => { if (originalRate === targetRate) return data; const ratio = targetRate / originalRate; const newLength = Math.round(data.length * ratio); const result = new Float32Array(newLength); for (let i = 0; i < newLength; i++) { result[i] = data[Math.round(i / ratio)] || 0; } return result; }; // 开始录音(首次打开弹窗) const startRecording = async () => { if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { Toast.show('浏览器不支持麦克风'); return; } try { // 清空语音消息列表(新会话开始) setVoiceMessages([]); // 重置输入值 setInputValue(''); inputValueRef.current = ''; if (!isWsConnected) { 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(); // setIsRecordingModel(false); // 没有内容时关闭弹窗 Toast.show('未检测到语音输入'); } }, 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); } }; // 用于打断/连续对话,不清空 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("停止录音并关闭弹窗"); // 清理静默超时 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"); // 关闭弹窗并清空 // setIsRecordingModel(false); setVoiceMessages([]); setInputValue(''); inputValueRef.current = ''; }, [sendCommand]); // 切换录音状态(点击按钮) const toggleRecording = useCallback(() => { if (isRecordingRef.current) { // 正在录音,停止(手动停止也触发发送) const latestInput = inputValueRef.current.trim(); if (latestInput) { handleSendMessageWithContent(latestInput); } else { stopRecordingAndClose(); } } else { // 未录音,开始 startRecording(); } }, [stopRecordingAndClose, handleSendMessageWithContent]); // 取消录音并清空 const cancelRecording = useCallback(() => { if (isRecordingRef.current) { stopRecordingOnly(); } setInputValue(''); inputValueRef.current = ''; setIsVoiceMode(false); setIsRecordingModel(false); setVoiceMessages([]); }, [stopRecordingOnly]); // 格式化录音时长 const formatDuration = (seconds) => { const mins = Math.floor(seconds / 60).toString().padStart(2, '0'); const secs = (seconds % 60).toString().padStart(2, '0'); return `${mins}:${secs}`; }; // ==================== 初始化 ==================== useEffect(() => { setMessages([ { id: 'welcome', type: 'ai', content: '', time: getCurrentTime(), isWelcome: true } ]); const initSession = async () => { try { const initUrl = `${CONFIG.backendUrl}/api/v1/chat/init?sUserId=${sUserId}&sUserType=${sUserType}`; const response = await fetch(initUrl, { method: 'POST', headers: CONFIG.headers, body: JSON.stringify({}) }); const data = await response.json(); if (data.data) { setWelcomeContent(data.data); setMessages(prev => prev.map(msg => msg.id === 'welcome' ? { ...msg, content: data.data } : msg )); } } catch (error) { console.error('初始化失败:', error); } }; initSession(); inputRef.current?.focus(); const handleKeyDown = (e) => { if (e.ctrlKey && e.key === 'Enter') { handleSendMessage(); } if (e.key === 'Escape') { setInputValue(''); inputValueRef.current = ''; if (isRecordingRef.current) { stopRecordingAndClose(); } } }; document.addEventListener('keydown', handleKeyDown); return () => { document.removeEventListener('keydown', handleKeyDown); disconnectWebSocket(); if (recordingTimerRef.current) { clearInterval(recordingTimerRef.current); } if (silenceTimeoutRef.current) { clearTimeout(silenceTimeoutRef.current); } }; }, []); useEffect(() => { scrollToBottom(); }, [messages, isLoading, voiceMessages]); // ==================== 消息处理 ==================== const addMessage = (content, type, isError = false) => { const newMessage = { id: `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, type, content, time: getCurrentTime(), isError }; setMessages(prev => [...prev, newMessage]); return newMessage.id; }; // 原有的 handleSendMessage 保留给手动输入使用 const handleSendMessage = async () => { const message = inputValue.trim(); console.log("🚀 ~ handleSendMessage ~ message:", message); if (!message || isLoading) return; let currentSessionId = sessionId; if (!currentSessionId) { currentSessionId = generateRandomString(20); setSessionId(currentSessionId); } setInputValue(''); inputValueRef.current = ''; setIsLoading(true); addMessage(message, 'user'); const newHistoryItem = { role: 'user', content: message, 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, 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) { addMessage(data.data, 'ai'); 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. 刷新页面重试 `; addMessage(errorMessage, 'ai', true); } finally { setIsLoading(false); inputRef.current?.focus(); } }; const handleClearChat = () => { if (window.confirm('确定要清空当前对话吗?')) { setMessages([ { id: 'cleared', type: 'ai', content: '对话已清空,请开始新的对话。', time: getCurrentTime(), } ]); setChatHistory([]); setSessionId(''); setInputValue(''); inputValueRef.current = ''; if (isRecordingRef.current) { stopRecordingAndClose(); } } }; const handleCopyMessage = (content) => { navigator.clipboard.writeText(content).then(() => { alert('已复制到剪贴板'); }); }; const handleRegenerateMessage = (messageId, content) => { setChatHistory(prev => prev.filter(item => !(item.role === 'assistant' && item.content === content) )); 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 (
{/* 头部 */} {/* 主体 */}
{/* 消息区域 */}
{messages.map(renderMessage)} {/* 打字机效果 */} {isLoading && (
正在思考...
)}
{/* 输入区域 */}
{ if (!isRecording) { setInputValue(e.target.value); inputValueRef.current = e.target.value; } }} onKeyPress={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSendMessage(); } }} disabled={isLoading} readOnly={isRecording} /> { setIsRecordingModel(true) startRecording() }} />
{/* ==================== 语音对话弹窗 ==================== */} {isRecordingModel && (
{/* 消息展示区域 - 展示 voiceMessages 中的对话 */}
{voiceMessages.map(renderMessage)} {/* 当前正在输入的语音转文字(实时显示) */} {inputValue.trim() && isRecording && (
{inputValue}
{getCurrentTime()} (识别中...)
)} {/* AI 思考中效果 */} {isLoading && (
AI 正在思考...
)}
{/* 录音状态指示器 */} {isRecording && (
正在录音 {formatDuration(recordingDuration)}
)} {/* AI 说话时的打断按钮 */} {isAiRecording && ( )} {/* 挂断按钮 */}
{ stopRecordingAndClose(); setIsRecordingModel(false) setIsAiRecording(false) }} />
)}
); }; export default ChatInterface;