📋 模块概述

VoiceHelper前端应用基于Next.js 14构建的现代React应用,支持多模态交互(文本+语音)、实时通信、响应式设计和多平台部署。采用最新的App Router架构和服务端渲染技术。

🏗️ 前端架构图

graph TB
    subgraph "Next.js 14 前端应用架构"
        subgraph "用户界面层"
            WEB[Web浏览器<br/>React组件]
            MOBILE[移动浏览器<br/>响应式设计]
            PWA[PWA应用<br/>离线支持]
        end
        
        subgraph "组件层"
            CHAT[聊天组件<br/>StreamingChat]
            VOICE[语音组件<br/>VoiceChat]
            UPLOAD[文档组件<br/>DocumentUpload]
            DATASET[数据集组件<br/>DatasetManager]
        end
        
        subgraph "状态管理层"
            ZUSTAND[Zustand状态<br/>全局状态管理]
            REACT_QUERY[React Query<br/>服务端状态缓存]
            CONTEXT[React Context<br/>组件状态]
        end
        
        subgraph "通信层"
            SSE[Server-Sent Events<br/>流式文本接收]
            WS[WebSocket<br/>实时语音通信]
            HTTP[HTTP Client<br/>RESTful API]
            WEBRTC[WebRTC<br/>P2P音视频]
        end
        
        subgraph "服务层"
            API[API抽象层<br/>BaseStreamClient]
            AUDIO[音频处理<br/>AudioProcessor]
            STORAGE[本地存储<br/>LocalStorage]
            CACHE[缓存管理<br/>CacheManager]
        end
    end
    
    WEB --> CHAT
    MOBILE --> VOICE
    PWA --> UPLOAD
    
    CHAT --> ZUSTAND
    VOICE --> REACT_QUERY
    UPLOAD --> CONTEXT
    
    ZUSTAND --> SSE
    REACT_QUERY --> WS
    CONTEXT --> HTTP
    
    SSE --> API
    WS --> AUDIO
    HTTP --> STORAGE
    WEBRTC --> CACHE
    
    style WEB fill:#e3f2fd
    style CHAT fill:#f3e5f5
    style ZUSTAND fill:#e8f5e8
    style SSE fill:#fff3e0

🚀 核心组件详细分析

1. 流式聊天组件

文件位置: platforms/web/components/chat/StreamingChat.tsx

interface StreamingChatProps {
  conversationId?: string;          // 对话ID
  onVoiceTranscript?: (text: string) => void;  // 语音转录回调
  onVoiceResponse?: (audio: Blob) => void;      // 语音回复回调
  onVoiceReferences?: (refs: Reference[]) => void; // 引用资料回调
  className?: string;               // 自定义样式
}

/**
 * StreamingChat - 流式聊天核心组件
 * 
 * 功能特性:
 * - SSE流式接收: 实时显示AI回复内容
 * - 消息管理: 维护对话历史和状态
 * - 错误处理: 网络异常和重连机制
 * - 性能优化: 虚拟滚动和懒加载
 * - 可访问性: 键盘导航和屏幕阅读器支持
 */
export default function StreamingChat({
  conversationId,
  onVoiceTranscript,
  onVoiceResponse, 
  onVoiceReferences,
  className
}: StreamingChatProps) {
  
  // === 状态管理 ===
  const [messages, setMessages] = useState<Message[]>([]);         // 消息列表
  const [input, setInput] = useState('');                          // 输入内容
  const [isLoading, setIsLoading] = useState(false);              // 加载状态
  const [isConnected, setIsConnected] = useState(false);          // 连接状态
  const [currentStreamingMessage, setCurrentStreamingMessage] = useState<Message | null>(null);
  const [error, setError] = useState<string | null>(null);        // 错误状态
  
  // === Refs引用 ===
  const messagesEndRef = useRef<HTMLDivElement>(null);            // 消息滚动引用
  const eventSourceRef = useRef<EventSource | null>(null);       // SSE连接引用
  const abortControllerRef = useRef<AbortController | null>(null); // 请求取消控制器
  const requestIdRef = useRef<string>('');                        // 当前请求ID
  const inputRef = useRef<HTMLTextAreaElement>(null);             // 输入框引用
  
  /**
   * 建立SSE连接 - 用于接收流式响应
   * 
   * 功能说明:
   * - 创建EventSource连接到服务端
   * - 监听多种事件类型 (data, error, stream_end等)
   * - 处理连接异常和自动重连
   * - 解析NDJSON格式的响应数据
   * 
   * @returns cleanup函数,用于清理连接
   */
  const connectSSE = useCallback(() => {
    // 清理现有连接
    if (eventSourceRef.current) {
      eventSourceRef.current.close();
    }
    
    // 创建新的SSE连接
    const eventSource = new EventSource('/api/v1/sse/connect', {
      withCredentials: true  // 携带认证cookie
    });
    
    eventSourceRef.current = eventSource;
    
    // === SSE事件监听器 ===
    
    // 连接建立事件
    eventSource.onopen = (event) => {
      console.log('SSE连接已建立', event);
      setIsConnected(true);
      setError(null);
    };
    
    // 默认消息事件 (data事件)
    eventSource.onmessage = (event) => {
      try {
        const data = JSON.parse(event.data);
        handleSSEMessage(data);
      } catch (e) {
        console.error('SSE消息解析失败:', e, event.data);
      }
    };
    
    // 连接错误事件
    eventSource.onerror = (event) => {
      console.error('SSE连接错误:', event);
      setIsConnected(false);
      
      // 根据readyState判断错误类型
      if (eventSource.readyState === EventSource.CLOSED) {
        setError('连接已关闭,请刷新页面重试');
      } else if (eventSource.readyState === EventSource.CONNECTING) {
        setError('正在重新连接...');
        // 自动重连机制由浏览器处理
      }
    };
    
    // 自定义事件监听
    const eventTypes = [
      'retrieval_start',    // 检索开始
      'retrieval_progress', // 检索进度
      'retrieval_result',   // 检索结果
      'generation_start',   // 生成开始
      'generation_chunk',   // 生成片段
      'generation_done',    // 生成完成
      'stream_end',         // 流结束
      'error'               // 错误事件
    ];
    
    eventTypes.forEach(eventType => {
      eventSource.addEventListener(eventType, (event: MessageEvent) => {
        try {
          const data = JSON.parse(event.data);
          handleSSEMessage({ ...data, type: eventType });
        } catch (e) {
          console.error(`${eventType}事件解析失败:`, e);
        }
      });
    });
    
    // 返回清理函数
    return () => {
      eventSource.close();
      setIsConnected(false);
    };
  }, []);
  
  /**
   * 处理SSE消息的核心逻辑
   * 
   * @param data SSE消息数据对象
   */
  const handleSSEMessage = useCallback((data: any) => {
    const { type, request_id } = data;
    
    // 验证请求ID,防止处理过期消息
    if (request_id && request_id !== requestIdRef.current) {
      return;
    }
    
    switch (type) {
      case 'retrieval_start':
        // 检索开始,显示检索状态
        if (currentStreamingMessage) {
          setCurrentStreamingMessage(prev => prev ? {
            ...prev,
            metadata: { ...prev.metadata, retrieval_status: 'searching' }
          } : null);
        }
        break;
        
      case 'retrieval_result':
        // 检索结果,保存引用资料
        const { results, total_found } = data;
        if (currentStreamingMessage && results) {
          setCurrentStreamingMessage(prev => prev ? {
            ...prev,
            references: results.slice(0, 5), // 保留前5个引用
            metadata: { 
              ...prev.metadata, 
              retrieval_status: 'completed',
              total_results: total_found 
            }
          } : null);
        }
        break;
        
      case 'generation_start':
        // 生成开始,准备接收内容
        console.log('开始生成回复...');
        break;
        
      case 'generation_chunk':
        // 生成片段,实时更新消息内容
        const { text: chunkText } = data;
        if (currentStreamingMessage && chunkText) {
          setCurrentStreamingMessage(prev => prev ? {
            ...prev,
            content: prev.content + chunkText,
            updatedAt: new Date()
          } : null);
        }
        break;
        
      case 'generation_done':
        // 生成完成,保存完整消息
        const { full_text, total_time_ms, context_sources } = data;
        if (currentStreamingMessage) {
          const finalMessage: Message = {
            ...currentStreamingMessage,
            content: full_text || currentStreamingMessage.content,
            isStreaming: false,
            completedAt: new Date(),
            metadata: {
              ...currentStreamingMessage.metadata,
              response_time: total_time_ms,
              sources: context_sources
            }
          };
          
          // 将完成的消息添加到消息列表
          setMessages(prev => [...prev, finalMessage]);
          setCurrentStreamingMessage(null);
          setIsLoading(false);
          
          // 触发回调
          if (onVoiceReferences && finalMessage.references) {
            onVoiceReferences(finalMessage.references);
          }
        }
        break;
        
      case 'stream_end':
        // 流结束,清理状态
        console.log('消息流结束');
        setIsLoading(false);
        break;
        
      case 'error':
        // 错误处理
        const { error: errorMsg, code: errorCode } = data;
        console.error('SSE错误:', errorMsg, errorCode);
        
        setError(`处理失败: ${errorMsg}`);
        setIsLoading(false);
        setCurrentStreamingMessage(null);
        break;
        
      default:
        console.log('未知SSE事件类型:', type, data);
    }
  }, [currentStreamingMessage, onVoiceReferences]);
  
  /**
   * 发送消息的核心逻辑
   * 
   * 功能说明:
   * - 构建消息对象并添加到消息列表
   * - 发送HTTP POST请求到聊天API
   * - 创建流式响应消息占位符
   * - 处理请求异常和重试机制
   * - 支持请求取消和幂等性控制
   */
  const sendMessage = useCallback(async () => {
    // 参数验证
    if (!input.trim() || isLoading || !isConnected) {
      return;
    }
    
    // 生成请求ID用于幂等性控制
    const requestId = generateRequestId();
    requestIdRef.current = requestId;
    
    // 构建用户消息
    const userMessage: Message = {
      id: Date.now().toString(),
      role: 'user',
      content: input.trim(),
      timestamp: new Date(),
      modality: 'text'
    };
    
    // 立即添加用户消息到界面
    setMessages(prev => [...prev, userMessage]);
    setInput(''); // 清空输入框
    setIsLoading(true);
    setError(null);
    
    // 创建AI回复占位符
    const assistantMessage: Message = {
      id: (Date.now() + 1).toString(), 
      role: 'assistant',
      content: '',
      timestamp: new Date(),
      modality: 'text',
      isStreaming: true,
      references: []
    };
    setCurrentStreamingMessage(assistantMessage);
    
    try {
      // 创建取消控制器
      const abortController = new AbortController();
      abortControllerRef.current = abortController;
      
      // 发送HTTP请求
      const response = await fetch('/api/v1/chat/stream', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${getAuthToken()}`,
          'X-Request-ID': requestId,
        },
        body: JSON.stringify({
          message: userMessage.content,
          conversation_id: conversationId,
          request_id: requestId,
          stream_id: eventSourceRef.current ? 'current_stream' : undefined,
          context: {
            modality: 'text',
            timestamp: userMessage.timestamp.toISOString(),
            user_preferences: getUserPreferences()
          }
        }),
        signal: abortController.signal
      });
      
      // 检查响应状态
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }
      
      // 解析响应
      const result = await response.json();
      console.log('聊天请求已提交:', result);
      
    } catch (error: any) {
      if (error.name !== 'AbortError') {
        console.error('发送消息失败:', error);
        setError(`发送失败: ${error.message}`);
        setIsLoading(false);
        setCurrentStreamingMessage(null);
      }
    }
  }, [input, isLoading, isConnected, conversationId]);
  
  // === 副作用处理 ===
  
  // 建立SSE连接
  useEffect(() => {
    const cleanup = connectSSE();
    return cleanup;
  }, [connectSSE]);
  
  // 自动滚动到最新消息
  useEffect(() => {
    if (messagesEndRef.current) {
      messagesEndRef.current.scrollIntoView({ 
        behavior: 'smooth', 
        block: 'end' 
      });
    }
  }, [messages, currentStreamingMessage]);
  
  // 键盘快捷键处理
  useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      // Ctrl/Cmd + Enter 发送消息
      if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
        event.preventDefault();
        sendMessage();
      }
      
      // Escape 取消当前请求
      if (event.key === 'Escape' && isLoading) {
        if (abortControllerRef.current) {
          abortControllerRef.current.abort();
        }
      }
    };
    
    document.addEventListener('keydown', handleKeyDown);
    return () => document.removeEventListener('keydown', handleKeyDown);
  }, [sendMessage, isLoading]);
  
  // === 组件渲染 ===
  
  return (
    <div className={`flex flex-col h-full bg-white ${className}`}>
      {/* 连接状态指示器 */}
      <div className="flex items-center justify-between p-4 bg-gray-50 border-b">
        <h2 className="text-lg font-semibold text-gray-900">
          智能助手
        </h2>
        <div className="flex items-center space-x-2">
          <div className={`w-2 h-2 rounded-full ${
            isConnected ? 'bg-green-500' : 'bg-red-500'
          }`} />
          <span className="text-sm text-gray-600">
            {isConnected ? '已连接' : '连接中断'}
          </span>
        </div>
      </div>
      
      {/* 错误提示 */}
      {error && (
        <div className="p-4 bg-red-50 border-b border-red-200">
          <div className="flex items-center">
            <ExclamationTriangleIcon className="w-5 h-5 text-red-500 mr-2" />
            <span className="text-sm text-red-700">{error}</span>
            <button
              onClick={() => setError(null)}
              className="ml-auto text-red-500 hover:text-red-700"
            >
              
            </button>
          </div>
        </div>
      )}
      
      {/* 消息列表 */}
      <div className="flex-1 overflow-y-auto p-4 space-y-4">
        {messages.map((message) => (
          <MessageBubble
            key={message.id}
            message={message}
            isStreaming={false}
          />
        ))}
        
        {/* 流式消息 */}
        {currentStreamingMessage && (
          <MessageBubble
            message={currentStreamingMessage}
            isStreaming={true}
          />
        )}
        
        {/* 滚动锚点 */}
        <div ref={messagesEndRef} />
      </div>
      
      {/* 输入区域 */}
      <div className="p-4 bg-white border-t">
        <div className="flex items-end space-x-2">
          <div className="flex-1 relative">
            <textarea
              ref={inputRef}
              value={input}
              onChange={(e) => setInput(e.target.value)}
              onKeyDown={(e) => {
                if (e.key === 'Enter' && !e.shiftKey) {
                  e.preventDefault();
                  sendMessage();
                }
              }}
              placeholder="输入消息... (Ctrl+Enter发送)"
              className="w-full p-3 border border-gray-300 rounded-lg resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
              rows={1}
              disabled={isLoading || !isConnected}
              maxLength={2000}
            />
            
            {/* 字符计数 */}
            <div className="absolute bottom-2 right-2 text-xs text-gray-400">
              {input.length}/2000
            </div>
          </div>
          
          {/* 发送按钮 */}
          <button
            onClick={sendMessage}
            disabled={!input.trim() || isLoading || !isConnected}
            className={`p-3 rounded-lg transition-colors ${
              !input.trim() || isLoading || !isConnected
                ? 'bg-gray-300 text-gray-500 cursor-not-allowed'
                : 'bg-blue-600 text-white hover:bg-blue-700'
            }`}
            title="发送消息 (Ctrl+Enter)"
          >
            {isLoading ? (
              <div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
            ) : (
              <PaperAirplaneIcon className="w-5 h-5" />
            )}
          </button>
        </div>
      </div>
    </div>
  );
}

// === 工具函数 ===

/**
 * 生成请求ID用于幂等性控制
 * @returns 唯一请求标识符
 */
function generateRequestId(): string {
  return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}

/**
 * 获取认证令牌
 * @returns JWT认证令牌
 */
function getAuthToken(): string {
  return localStorage.getItem('authToken') || '';
}

/**
 * 获取用户偏好设置
 * @returns 用户配置对象
 */
function getUserPreferences(): any {
  const prefs = localStorage.getItem('userPreferences');
  return prefs ? JSON.parse(prefs) : {
    language: 'zh-CN',
    theme: 'light',
    enableVoice: true
  };
}

2. 语音聊天组件

文件位置: platforms/web/components/voice/VoiceChat.tsx

/**
 * VoiceChat - 语音交互核心组件
 * 
 * 功能特性:
 * - WebSocket实时通信: 双向音频流传输
 * - 实时语音识别: 边说边显示转录文本
 * - 语音活动检测: 自动识别说话开始和结束
 * - 音频处理: 降噪、回声消除、自动增益
 * - 多语言支持: 中英文及其他语言识别
 * - 离线备用: 网络异常时的降级处理
 */
export default function VoiceChat({
  conversationId,
  onTranscript,
  onResponse,
  onReferences,
  className
}: VoiceChatProps) {
  
  // === 状态管理 ===
  const [isRecording, setIsRecording] = useState(false);           // 录音状态
  const [isProcessing, setIsProcessing] = useState(false);         // 处理状态  
  const [isPlaying, setIsPlaying] = useState(false);              // 播放状态
  const [connectionStatus, setConnectionStatus] = useState<'disconnected' | 'connecting' | 'connected'>('disconnected');
  const [currentTranscript, setCurrentTranscript] = useState(''); // 当前转录文本
  const [volume, setVolume] = useState(0);                        // 音频音量
  const [error, setError] = useState<string | null>(null);        // 错误状态
  
  // === Refs引用 ===
  const wsRef = useRef<WebSocket | null>(null);                   // WebSocket连接
  const audioContextRef = useRef<AudioContext | null>(null);     // 音频上下文
  const mediaStreamRef = useRef<MediaStream | null>(null);       // 媒体流
  const recordingRef = useRef<MediaRecorder | null>(null);       // 录音器
  const audioChunksRef = useRef<Blob[]>([]);                     // 音频块缓存
  const vadRef = useRef<any>(null);                              // 语音活动检测
  const playbackQueueRef = useRef<ArrayBuffer[]>([]);            // 播放队列
  
  /**
   * 初始化WebSocket连接
   * 
   * 功能说明:
   * - 建立WebSocket连接到语音服务
   * - 配置消息监听和错误处理
   * - 实现自动重连机制
   * - 处理各种语音事件类型
   */
  const initializeWebSocket = useCallback(async () => {
    try {
      setConnectionStatus('connecting');
      
      // 构建WebSocket URL
      const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
      const wsUrl = `${wsProtocol}//${window.location.host}/api/v2/voice/ws?conversation_id=${conversationId}&language=zh-CN`;
      
      // 创建WebSocket连接
      const ws = new WebSocket(wsUrl);
      wsRef.current = ws;
      
      // === WebSocket事件监听 ===
      
      // 连接建立
      ws.onopen = (event) => {
        console.log('语音WebSocket连接已建立');
        setConnectionStatus('connected');
        setError(null);
      };
      
      // 接收消息
      ws.onmessage = async (event) => {
        try {
          const data = JSON.parse(event.data);
          await handleVoiceMessage(data);
        } catch (e) {
          console.error('语音消息解析失败:', e);
        }
      };
      
      // 连接错误
      ws.onerror = (event) => {
        console.error('WebSocket错误:', event);
        setError('语音连接异常,请检查网络');
      };
      
      // 连接关闭
      ws.onclose = (event) => {
        console.log('WebSocket连接关闭:', event.code, event.reason);
        setConnectionStatus('disconnected');
        
        // 自动重连逻辑
        if (!event.wasClean && event.code !== 1000) {
          setTimeout(() => {
            console.log('尝试重新连接...');
            initializeWebSocket();
          }, 3000);
        }
      };
      
    } catch (error) {
      console.error('初始化WebSocket失败:', error);
      setConnectionStatus('disconnected');
      setError('无法建立语音连接');
    }
  }, [conversationId]);
  
  /**
   * 处理语音WebSocket消息
   * 
   * @param data WebSocket消息数据
   */
  const handleVoiceMessage = useCallback(async (data: any) => {
    const { type } = data;
    
    switch (type) {
      case 'session_initialized':
        // 会话初始化完成
        console.log('语音会话已初始化:', data.session_id);
        break;
        
      case 'asr_partial':
        // 部分语音识别结果
        const partialText = data.text || '';
        setCurrentTranscript(partialText);
        
        if (onTranscript) {
          onTranscript(partialText, false); // false表示未完成
        }
        break;
        
      case 'asr_final':
        // 最终语音识别结果
        const finalText = data.text || '';
        setCurrentTranscript(finalText);
        
        if (onTranscript) {
          onTranscript(finalText, true); // true表示识别完成
        }
        break;
        
      case 'processing_start':
        // 开始处理用户请求
        setIsProcessing(true);
        console.log('开始处理语音请求...');
        break;
        
      case 'llm_response_chunk':
        // LLM回复文本片段
        const textChunk = data.text || '';
        console.log('收到文本回复片段:', textChunk);
        
        if (onResponse) {
          onResponse(textChunk, 'text', false);
        }
        break;
        
      case 'llm_response_final':
        // LLM完整回复
        const fullText = data.text || '';
        const references = data.references || [];
        
        console.log('收到完整文本回复:', fullText);
        
        if (onResponse) {
          onResponse(fullText, 'text', true);
        }
        
        if (onReferences && references.length > 0) {
          onReferences(references);
        }
        
        setIsProcessing(false);
        break;
        
      case 'tts_start':
        // TTS开始合成
        console.log('开始语音合成...');
        setIsPlaying(true);
        break;
        
      case 'tts_audio':
        // TTS音频数据
        const audioData = data.audio_data;
        const audioFormat = data.format || 'mp3';
        
        if (audioData) {
          await playAudioChunk(audioData, audioFormat);
        }
        break;
        
      case 'tts_complete':
        // TTS合成完成
        console.log('语音合成完成');
        setIsPlaying(false);
        break;
        
      case 'error':
        // 错误处理
        const errorMsg = data.error || '未知错误';
        console.error('语音处理错误:', errorMsg);
        setError(errorMsg);
        setIsProcessing(false);
        setIsPlaying(false);
        break;
        
      default:
        console.log('未知语音消息类型:', type, data);
    }
  }, [onTranscript, onResponse, onReferences]);
  
  /**
   * 播放音频块
   * 
   * @param audioData Base64编码的音频数据
   * @param format 音频格式
   */
  const playAudioChunk = useCallback(async (audioData: string, format: string) => {
    try {
      // Base64解码
      const binaryString = atob(audioData);
      const bytes = new Uint8Array(binaryString.length);
      
      for (let i = 0; i < binaryString.length; i++) {
        bytes[i] = binaryString.charCodeAt(i);
      }
      
      // 创建音频上下文
      if (!audioContextRef.current) {
        audioContextRef.current = new (window.AudioContext || (window as any).webkitAudioContext)();
      }
      
      const audioContext = audioContextRef.current;
      
      // 解码音频数据
      const audioBuffer = await audioContext.decodeAudioData(bytes.buffer.slice());
      
      // 创建音频源并播放
      const source = audioContext.createBufferSource();
      source.buffer = audioBuffer;
      source.connect(audioContext.destination);
      source.start();
      
      console.log(`播放音频块: ${format}, 时长: ${audioBuffer.duration.toFixed(2)}s`);
      
    } catch (error) {
      console.error('音频播放失败:', error);
    }
  }, []);
  
  /**
   * 开始录音
   */
  const startRecording = useCallback(async () => {
    try {
      // 请求麦克风权限
      const stream = await navigator.mediaDevices.getUserMedia({
        audio: {
          echoCancellation: true,    // 回声消除
          noiseSuppression: true,    // 噪声抑制
          autoGainControl: true,     // 自动增益控制
          sampleRate: 16000,         // 采样率
          channelCount: 1            // 单声道
        }
      });
      
      mediaStreamRef.current = stream;
      
      // 创建录音器
      const recorder = new MediaRecorder(stream, {
        mimeType: 'audio/webm;codecs=opus' // 使用Opus编码
      });
      
      recordingRef.current = recorder;
      audioChunksRef.current = [];
      
      // 录音数据处理
      recorder.ondataavailable = (event) => {
        if (event.data.size > 0) {
          audioChunksRef.current.push(event.data);
          
          // 实时发送音频数据
          if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
            const reader = new FileReader();
            reader.onload = () => {
              const arrayBuffer = reader.result as ArrayBuffer;
              const uint8Array = new Uint8Array(arrayBuffer);
              
              // 发送二进制音频数据
              wsRef.current?.send(uint8Array);
            };
            reader.readAsArrayBuffer(event.data);
          }
        }
      };
      
      // 开始录音,每500ms产生一个数据块
      recorder.start(500);
      setIsRecording(true);
      setError(null);
      
      console.log('开始录音...');
      
    } catch (error) {
      console.error('启动录音失败:', error);
      setError('无法访问麦克风,请检查权限设置');
    }
  }, []);
  
  /**
   * 停止录音
   */
  const stopRecording = useCallback(() => {
    if (recordingRef.current && isRecording) {
      recordingRef.current.stop();
      setIsRecording(false);
      
      // 关闭媒体流
      if (mediaStreamRef.current) {
        mediaStreamRef.current.getTracks().forEach(track => track.stop());
        mediaStreamRef.current = null;
      }
      
      console.log('录音已停止');
    }
  }, [isRecording]);
  
  /**
   * 切换录音状态
   */
  const toggleRecording = useCallback(() => {
    if (connectionStatus !== 'connected') {
      setError('语音连接未建立,请稍候重试');
      return;
    }
    
    if (isRecording) {
      stopRecording();
    } else {
      startRecording();
    }
  }, [isRecording, connectionStatus, startRecording, stopRecording]);
  
  // === 副作用处理 ===
  
  // 初始化WebSocket连接
  useEffect(() => {
    initializeWebSocket();
    
    return () => {
      // 清理资源
      if (wsRef.current) {
        wsRef.current.close();
      }
      if (mediaStreamRef.current) {
        mediaStreamRef.current.getTracks().forEach(track => track.stop());
      }
      if (audioContextRef.current) {
        audioContextRef.current.close();
      }
    };
  }, [initializeWebSocket]);
  
  // 键盘快捷键
  useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      // 空格键录音 (按住录音)
      if (event.code === 'Space' && !event.repeat) {
        event.preventDefault();
        if (!isRecording) {
          startRecording();
        }
      }
    };
    
    const handleKeyUp = (event: KeyboardEvent) => {
      if (event.code === 'Space') {
        event.preventDefault();
        if (isRecording) {
          stopRecording();
        }
      }
    };
    
    document.addEventListener('keydown', handleKeyDown);
    document.addEventListener('keyup', handleKeyUp);
    
    return () => {
      document.removeEventListener('keydown', handleKeyDown);
      document.removeEventListener('keyup', handleKeyUp);
    };
  }, [isRecording, startRecording, stopRecording]);
  
  // === 组件渲染 ===
  
  return (
    <div className={`flex flex-col items-center p-6 bg-gradient-to-b from-blue-50 to-white ${className}`}>
      {/* 连接状态 */}
      <div className="mb-4">
        <div className={`inline-flex items-center px-3 py-1 rounded-full text-sm ${
          connectionStatus === 'connected' 
            ? 'bg-green-100 text-green-800' 
            : connectionStatus === 'connecting'
            ? 'bg-yellow-100 text-yellow-800'
            : 'bg-red-100 text-red-800'
        }`}>
          <div className={`w-2 h-2 rounded-full mr-2 ${
            connectionStatus === 'connected' ? 'bg-green-500' : 
            connectionStatus === 'connecting' ? 'bg-yellow-500' : 'bg-red-500'
          }`} />
          {connectionStatus === 'connected' ? '语音已连接' :
           connectionStatus === 'connecting' ? '连接中...' : '连接断开'}
        </div>
      </div>
      
      {/* 错误提示 */}
      {error && (
        <div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg">
          <p className="text-sm text-red-700">{error}</p>
        </div>
      )}
      
      {/* 转录文本显示 */}
      <div className="mb-6 min-h-[60px] max-w-md">
        <div className="p-4 bg-white rounded-lg border shadow-sm">
          <p className="text-gray-600 text-sm mb-1">实时转录:</p>
          <p className="text-gray-900 min-h-[24px]">
            {currentTranscript || '开始说话,我会实时转录...'}
          </p>
        </div>
      </div>
      
      {/* 录音控制按钮 */}
      <div className="flex flex-col items-center space-y-4">
        <button
          onClick={toggleRecording}
          disabled={connectionStatus !== 'connected'}
          className={`relative w-20 h-20 rounded-full flex items-center justify-center transition-all duration-200 ${
            isRecording 
              ? 'bg-red-500 hover:bg-red-600 scale-110 shadow-lg' 
              : 'bg-blue-500 hover:bg-blue-600 shadow-md'
          } ${
            connectionStatus !== 'connected' 
              ? 'opacity-50 cursor-not-allowed' 
              : 'cursor-pointer'
          }`}
        >
          {isRecording ? (
            <div className="w-6 h-6 bg-white rounded-sm" />
          ) : (
            <MicrophoneIcon className="w-8 h-8 text-white" />
          )}
          
          {/* 录音动画 */}
          {isRecording && (
            <div className="absolute inset-0 rounded-full border-4 border-red-300 animate-ping" />
          )}
        </button>
        
        {/* 状态文本 */}
        <div className="text-center">
          {isRecording ? (
            <p className="text-red-600 font-medium">正在录音...</p>
          ) : isProcessing ? (
            <p className="text-blue-600 font-medium">正在处理...</p>
          ) : isPlaying ? (
            <p className="text-green-600 font-medium">正在播放...</p>
          ) : (
            <p className="text-gray-600">点击开始语音对话</p>
          )}
          
          <p className="text-xs text-gray-500 mt-1">
            或按住空格键录音
          </p>
        </div>
      </div>
      
      {/* 音量指示器 */}
      {isRecording && (
        <div className="mt-4 flex items-center space-x-1">
          {[...Array(10)].map((_, i) => (
            <div
              key={i}
              className={`w-1 rounded-full transition-all duration-100 ${
                i < volume * 10 ? 'bg-red-500 h-4' : 'bg-gray-300 h-2'
              }`}
            />
          ))}
        </div>
      )}
    </div>
  );
}

🔄 状态管理架构

Zustand全局状态管理

文件位置: platforms/web/src/store/useAppStore.ts

interface AppState {
  // 用户状态
  user: User | null;
  isAuthenticated: boolean;
  
  // 对话状态  
  currentConversation: Conversation | null;
  conversations: Conversation[];
  
  // UI状态
  sidebarOpen: boolean;
  theme: 'light' | 'dark';
  language: string;
  
  // 语音状态
  voiceEnabled: boolean;
  voiceSettings: VoiceSettings;
  
  // 系统状态
  connectionStatus: 'connected' | 'connecting' | 'disconnected';
  notifications: Notification[];
}

interface AppActions {
  // 用户操作
  setUser: (user: User | null) => void;
  login: (credentials: LoginCredentials) => Promise<void>;
  logout: () => void;
  
  // 对话操作
  setCurrentConversation: (conversation: Conversation | null) => void;
  addConversation: (conversation: Conversation) => void;
  updateConversation: (id: string, updates: Partial<Conversation>) => void;
  deleteConversation: (id: string) => void;
  
  // UI操作
  toggleSidebar: () => void;
  setTheme: (theme: 'light' | 'dark') => void;
  setLanguage: (language: string) => void;
  
  // 语音操作
  toggleVoice: () => void;
  updateVoiceSettings: (settings: Partial<VoiceSettings>) => void;
  
  // 系统操作
  setConnectionStatus: (status: 'connected' | 'connecting' | 'disconnected') => void;
  addNotification: (notification: Notification) => void;
  removeNotification: (id: string) => void;
}

/**
 * Zustand应用状态管理
 * 
 * 特性:
 * - 类型安全: 完整的TypeScript类型定义
 * - 持久化: 关键状态自动保存到本地存储
 * - 中间件: 日志记录和状态同步
 * - 性能优化: 选择性订阅和浅比较
 */
export const useAppStore = create<AppState & AppActions>()(
  subscribeWithSelector(
    persist(
      immer((set, get) => ({
        // === 初始状态 ===
        user: null,
        isAuthenticated: false,
        currentConversation: null,
        conversations: [],
        sidebarOpen: true,
        theme: 'light',
        language: 'zh-CN',
        voiceEnabled: true,
        voiceSettings: {
          voice: 'zh-CN-XiaoxiaoNeural',
          rate: 1.0,
          pitch: 1.0,
          volume: 0.8
        },
        connectionStatus: 'disconnected',
        notifications: [],
        
        // === 用户操作 ===
        setUser: (user) => set((state) => {
          state.user = user;
          state.isAuthenticated = !!user;
        }),
        
        login: async (credentials) => {
          set((state) => {
            state.connectionStatus = 'connecting';
          });
          
          try {
            const response = await authApi.login(credentials);
            const { user, token } = response.data;
            
            // 保存认证信息
            localStorage.setItem('authToken', token);
            
            set((state) => {
              state.user = user;
              state.isAuthenticated = true;
              state.connectionStatus = 'connected';
            });
            
          } catch (error) {
            set((state) => {
              state.connectionStatus = 'disconnected';
            });
            throw error;
          }
        },
        
        logout: () => set((state) => {
          // 清理认证信息
          localStorage.removeItem('authToken');
          
          state.user = null;
          state.isAuthenticated = false;
          state.currentConversation = null;
          state.conversations = [];
          state.connectionStatus = 'disconnected';
        }),
        
        // === 对话操作 ===
        setCurrentConversation: (conversation) => set((state) => {
          state.currentConversation = conversation;
        }),
        
        addConversation: (conversation) => set((state) => {
          state.conversations.unshift(conversation);
        }),
        
        updateConversation: (id, updates) => set((state) => {
          const index = state.conversations.findIndex(c => c.id === id);
          if (index !== -1) {
            Object.assign(state.conversations[index], updates);
          }
          
          if (state.currentConversation?.id === id) {
            Object.assign(state.currentConversation, updates);
          }
        }),
        
        deleteConversation: (id) => set((state) => {
          state.conversations = state.conversations.filter(c => c.id !== id);
          
          if (state.currentConversation?.id === id) {
            state.currentConversation = null;
          }
        }),
        
        // === UI操作 ===
        toggleSidebar: () => set((state) => {
          state.sidebarOpen = !state.sidebarOpen;
        }),
        
        setTheme: (theme) => set((state) => {
          state.theme = theme;
          document.documentElement.setAttribute('data-theme', theme);
        }),
        
        setLanguage: (language) => set((state) => {
          state.language = language;
        }),
        
        // === 语音操作 ===
        toggleVoice: () => set((state) => {
          state.voiceEnabled = !state.voiceEnabled;
        }),
        
        updateVoiceSettings: (settings) => set((state) => {
          Object.assign(state.voiceSettings, settings);
        }),
        
        // === 系统操作 ===
        setConnectionStatus: (status) => set((state) => {
          state.connectionStatus = status;
        }),
        
        addNotification: (notification) => set((state) => {
          state.notifications.push({
            ...notification,
            id: notification.id || Date.now().toString(),
            timestamp: new Date()
          });
        }),
        
        removeNotification: (id) => set((state) => {
          state.notifications = state.notifications.filter(n => n.id !== id);
        })
      })),
      {
        name: 'voicehelper-app-store',
        // 持久化配置
        partialize: (state) => ({
          theme: state.theme,
          language: state.language,
          voiceEnabled: state.voiceEnabled,
          voiceSettings: state.voiceSettings,
          sidebarOpen: state.sidebarOpen
        }),
        // 存储版本管理
        version: 1,
        migrate: (persistedState: any, version: number) => {
          if (version === 0) {
            // 迁移逻辑
          }
          return persistedState;
        }
      }
    )
  )
);

// === 选择器钩子 ===

export const useUser = () => useAppStore(state => state.user);
export const useAuth = () => useAppStore(state => ({
  isAuthenticated: state.isAuthenticated,
  login: state.login,
  logout: state.logout
}));

export const useConversations = () => useAppStore(state => ({
  current: state.currentConversation,
  list: state.conversations,
  setCurrent: state.setCurrentConversation,
  add: state.addConversation,
  update: state.updateConversation,
  delete: state.deleteConversation
}));

export const useUI = () => useAppStore(state => ({
  sidebarOpen: state.sidebarOpen,
  theme: state.theme,
  language: state.language,
  toggleSidebar: state.toggleSidebar,
  setTheme: state.setTheme,
  setLanguage: state.setLanguage
}));

export const useVoice = () => useAppStore(state => ({
  enabled: state.voiceEnabled,
  settings: state.voiceSettings,
  toggle: state.toggleVoice,
  updateSettings: state.updateVoiceSettings
}));

export const useConnection = () => useAppStore(state => ({
  status: state.connectionStatus,
  setStatus: state.setConnectionStatus
}));

export const useNotifications = () => useAppStore(state => ({
  notifications: state.notifications,
  add: state.addNotification,
  remove: state.removeNotification
}));

🎯 性能优化最佳实践

1. React性能优化

// 使用React.memo优化组件渲染
const MessageBubble = React.memo<MessageBubbleProps>(({
  message,
  isStreaming
}) => {
  return (
    <div className={`message-bubble ${message.role}`}>
      {/* 组件内容 */}
    </div>
  );
}, (prevProps, nextProps) => {
  // 自定义比较函数
  return (
    prevProps.message.id === nextProps.message.id &&
    prevProps.message.content === nextProps.message.content &&
    prevProps.isStreaming === nextProps.isStreaming
  );
});

// 使用useMemo优化计算
const processedMessages = useMemo(() => {
  return messages.map(message => ({
    ...message,
    formattedContent: formatMessageContent(message.content)
  }));
}, [messages]);

// 使用useCallback优化函数引用
const handleMessageSend = useCallback((content: string) => {
  if (!content.trim()) return;
  
  sendMessage(content);
}, [sendMessage]);

2. Bundle优化

// 动态导入减少初始包大小
const VoiceChat = lazy(() => import('./components/voice/VoiceChat'));
const DocumentUpload = lazy(() => import('./components/documents/DocumentUpload'));

// 使用Suspense处理加载状态
<Suspense fallback={<LoadingSpinner />}>
  <VoiceChat conversationId={conversationId} />
</Suspense>

// 预加载关键组件
const preloadVoiceChat = () => import('./components/voice/VoiceChat');

useEffect(() => {
  // 在用户可能需要时预加载
  const timer = setTimeout(preloadVoiceChat, 2000);
  return () => clearTimeout(timer);
}, []);

这份前端应用分析涵盖了React组件架构、状态管理、通信机制、性能优化等核心方面,为开发者提供了构建现代Web应用的完整指南。