首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >AI聊天机器人前端实现——React + WebSocket深度集成

AI聊天机器人前端实现——React + WebSocket深度集成

作者头像
老猫-Bond
发布2026-03-10 17:33:23
发布2026-03-10 17:33:23
760
举报
文章被收录于专栏:前端大全前端大全

AI聊天机器人正成为现代Web应用的核心功能,通过React与WebSocket的深度集成,可以构建高性能、低延迟的实时对话体验。

介绍

  随着人工智能技术的飞速发展,AI聊天机器人已成为现代Web应用中不可或缺的功能。从客户服务到个人助理,从教育辅导到娱乐互动,AI聊天机器人正在改变用户与数字产品的交互方式。本文将深入探讨如何使用React和WebSocket技术构建一个高性能的AI聊天机器人前端界面,涵盖从基础组件设计到复杂状态管理的全方位技术要点。

核心架构设计

WebSocket连接管理

代码语言:javascript
复制
// WebSocket连接管理器
import { EventEmitter } from 'events';

class WebSocketManager extends EventEmitter {
  constructor(url, options = {}) {
    super();

    this.url = url;
    this.options = {
      reconnectInterval: 5000,
      maxReconnectAttempts: 5,
      heartbeatInterval: 30000,
      ...options
    };

    this.socket = null;
    this.isConnected = false;
    this.reconnectAttempts = 0;
    this.heartbeatTimer = null;
    this.messageQueue = [];
    this.requestId = 0;
    this.pendingRequests = new Map();

    this.setupEventHandlers();
  }

  setupEventHandlers() {
    // 重连机制
    this.on('reconnect', () => {
      if (this.reconnectAttempts < this.options.maxReconnectAttempts) {
        setTimeout(() => {
          this.connect();
          this.reconnectAttempts++;
        }, this.options.reconnectInterval);
      } else {
        console.error('Max reconnection attempts reached');
        this.emit('reconnect_failed');
      }
    });
  }

  connect() {
    if (this.socket && this.socket.readyState === WebSocket.OPEN) {
      return;
    }

    try {
      this.socket = new WebSocket(this.url);

      this.socket.onopen = (event) => {
        console.log('WebSocket connected');
        this.isConnected = true;
        this.reconnectAttempts = 0;
        this.emit('connected', event);
        this.startHeartbeat();
        this.flushMessageQueue();
      };

      this.socket.onmessage = (event) => {
        try {
          const data = JSON.parse(event.data);
          this.handleMessage(data);
        } catch (error) {
          console.error('Error parsing message:', error);
          this.emit('parse_error', event.data);
        }
      };

      this.socket.onclose = (event) => {
        console.log('WebSocket disconnected:', event.code, event.reason);
        this.isConnected = false;
        this.stopHeartbeat();
        this.emit('disconnected', event);

        if (!event.wasClean) {
          this.emit('reconnect');
        }
      };

      this.socket.onerror = (error) => {
        console.error('WebSocket error:', error);
        this.emit('error', error);
      };

    } catch (error) {
      console.error('Failed to create WebSocket connection:', error);
      this.emit('connection_error', error);
    }
  }

  handleMessage(data) {
    // 处理不同类型的响应
    switch (data.type) {
      case 'response':
        this.handleResponse(data);
        break;
      case 'stream_chunk':
        this.handleStreamChunk(data);
        break;
      case 'heartbeat':
        this.handleHeartbeat(data);
        break;
      case 'error':
        this.handleError(data);
        break;
      default:
        this.emit('message', data);
    }
  }

  handleResponse(data) {
    const request = this.pendingRequests.get(data.requestId);
    if (request) {
      request.resolve(data.payload);
      this.pendingRequests.delete(data.requestId);
    }
    this.emit('response', data.payload);
  }

  handleStreamChunk(data) {
    this.emit('stream_chunk', data.payload);
  }

  handleHeartbeat(data) {
    this.emit('heartbeat_received', data);
  }

  handleError(data) {
    const request = this.pendingRequests.get(data.requestId);
    if (request) {
      request.reject(data.payload);
      this.pendingRequests.delete(data.requestId);
    }
    this.emit('error_response', data.payload);
  }

  send(message, requestId = null) {
    if (!this.isConnected) {
      // 将消息加入队列,等待连接建立后发送
      this.messageQueue.push(message);
      return Promise.reject(new Error('WebSocket not connected'));
    }

    const messageData = {
      id: requestId || ++this.requestId,
      timestamp: Date.now(),
      ...message
    };

    try {
      this.socket.send(JSON.stringify(messageData));
      return Promise.resolve(messageData.id);
    } catch (error) {
      console.error('Failed to send message:', error);
      return Promise.reject(error);
    }
  }

  sendWithAck(message) {
    return new Promise((resolve, reject) => {
      const requestId = ++this.requestId;

      // 存储请求以便后续处理响应
      this.pendingRequests.set(requestId, { resolve, reject });

      // 发送消息
      this.send({ ...message, requestId })
        .catch(error => {
          this.pendingRequests.delete(requestId);
          reject(error);
        });
    });
  }

  flushMessageQueue() {
    while (this.messageQueue.length > 0) {
      const message = this.messageQueue.shift();
      this.send(message).catch(() => {
        // 如果发送失败,重新放入队列
        this.messageQueue.unshift(message);
      });
    }
  }

  startHeartbeat() {
    this.heartbeatTimer = setInterval(() => {
      if (this.isConnected) {
        this.send({ type: 'ping', timestamp: Date.now() });
      }
    }, this.options.heartbeatInterval);
  }

  stopHeartbeat() {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = null;
    }
  }

  disconnect() {
    this.isConnected = false;
    this.stopHeartbeat();

    if (this.socket) {
      this.socket.close();
      this.socket = null;
    }
  }
}

export default WebSocketManager;

聊天状态管理

代码语言:javascript
复制
// 聊天状态管理器
class ChatStateManager {
  constructor() {
    this.state = {
      messages: [],
      currentSession: null,
      isTyping: false,
      isConnected: false,
      error: null,
      settings: {
        autoScroll: true,
        showAvatars: true,
        enableSound: true,
        theme: 'light'
      }
    };

    this.listeners = [];
  }

  subscribe(listener) {
    this.listeners.push(listener);
    return () => {
      this.listeners = this.listeners.filter(l => l !== listener);
    };
  }

  notifySubscribers() {
    this.listeners.forEach(listener => listener(this.state));
  }

  setState(newState) {
    this.state = { ...this.state, ...newState };
    this.notifySubscribers();
  }

  addMessage(message) {
    const newMessages = [...this.state.messages, message];
    this.setState({ messages: newMessages });
  }

  updateMessage(id, updates) {
    const newMessages = this.state.messages.map(msg =>
      msg.id === id ? { ...msg, ...updates } : msg
    );
    this.setState({ messages: newMessages });
  }

  setTyping(typing) {
    this.setState({ isTyping: typing });
  }

  setConnected(connected) {
    this.setState({ isConnected: connected });
  }

  setError(error) {
    this.setState({ error });
  }

  clearMessages() {
    this.setState({ messages: [] });
  }

  setSettings(settings) {
    this.setState({
      settings: { ...this.state.settings, ...settings }
    });
  }

  getCurrentSession() {
    return this.state.currentSession;
  }

  createNewSession(sessionData) {
    const session = {
      id: Date.now().toString(),
      createdAt: new Date().toISOString(),
      ...sessionData
    };

    this.setState({ currentSession: session });
    return session;
  }
}

// 创建全局状态管理器实例
export const chatStateManager = new ChatStateManager();

React组件实现

主聊天界面组件

代码语言:javascript
复制
// ChatInterface.jsx
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { chatStateManager } from '../state/chatStateManager';
import WebSocketManager from '../utils/WebSocketManager';
import MessageBubble from './MessageBubble';
import ChatInput from './ChatInput';
import TypingIndicator from './TypingIndicator';
import SessionManager from './SessionManager';

const ChatInterface = ({ config }) => {
  const [state, setState] = useState(chatStateManager.state);
  const [inputValue, setInputValue] = useState('');
  const [selectedModel, setSelectedModel] = useState('gpt-4');
  const messagesEndRef = useRef(null);
  const chatContainerRef = useRef(null);
  const webSocketRef = useRef(null);

  // 初始化WebSocket连接
  useEffect(() => {
    webSocketRef.current = new WebSocketManager(config.websocketUrl);

    webSocketRef.current.on('connected', () => {
      chatStateManager.setConnected(true);
    });

    webSocketRef.current.on('disconnected', () => {
      chatStateManager.setConnected(false);
    });

    webSocketRef.current.on('response', (data) => {
      chatStateManager.updateMessage(data.messageId, {
        content: data.content,
        status: 'received'
      });
    });

    webSocketRef.current.on('stream_chunk', (data) => {
      chatStateManager.updateMessage(data.messageId, {
        content: prev => prev + data.chunk,
        status: 'streaming'
      });
    });

    webSocketRef.current.on('typing_start', () => {
      chatStateManager.setTyping(true);
    });

    webSocketRef.current.on('typing_end', () => {
      chatStateManager.setTyping(false);
    });

    webSocketRef.current.connect();

    return () => {
      webSocketRef.current.disconnect();
    };
  }, [config.websocketUrl]);

  // 订阅状态变化
  useEffect(() => {
    const unsubscribe = chatStateManager.subscribe(setState);
    return unsubscribe;
  }, []);

  // 自动滚动到底部
  useEffect(() => {
    scrollToBottom();
  }, [state.messages]);

  const scrollToBottom = useCallback(() => {
    if (state.settings.autoScroll && messagesEndRef.current) {
      messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
    }
  }, [state.settings.autoScroll]);

  const handleSendMessage = useCallback(async (message) => {
    if (!message.trim() || !webSocketRef.current?.isConnected) return;

    // 创建消息对象
    const messageObj = {
      id: `msg_${Date.now()}`,
      content: message,
      sender: 'user',
      timestamp: new Date().toISOString(),
      status: 'sending'
    };

    // 添加用户消息到界面
    chatStateManager.addMessage(messageObj);

    try {
      // 发送消息到服务器
      const response = await webSocketRef.current.sendWithAck({
        type: 'chat_message',
        payload: {
          content: message,
          model: selectedModel,
          sessionId: state.currentSession?.id
        }
      });

      // 服务器会通过响应事件更新消息状态
    } catch (error) {
      chatStateManager.setError(error.message);
      chatStateManager.updateMessage(messageObj.id, { status: 'error' });
    }
  }, [selectedModel, state.currentSession?.id]);

  const handleKeyDown = (e) => {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault();
      handleSendMessage(inputValue);
      setInputValue('');
    }
  };

  const renderMessages = () => {
    return state.messages.map((message) => (
      <MessageBubble
        key={message.id}
        message={message}
        isCurrentUser={message.sender === 'user'}
      />
    ));
  };

  return (
    <div className={`chat-interface ${state.settings.theme}`}>
      <div className="chat-header">
        <SessionManager />
        <div className="model-selector">
          <select
            value={selectedModel}
            onChange={(e) => setSelectedModel(e.target.value)}
            className="model-dropdown"
          >
            <option value="gpt-4">GPT-4</option>
            <option value="gpt-3.5-turbo">GPT-3.5 Turbo</option>
            <option value="claude-2">Claude 2</option>
          </select>
        </div>
      </div>

      <div className="chat-container" ref={chatContainerRef}>
        <div className="messages-area">
          {renderMessages()}
          {state.isTyping && <TypingIndicator />}
          <div ref={messagesEndRef} />
        </div>
      </div>

      <div className="chat-input-area">
        <ChatInput
          value={inputValue}
          onChange={setInputValue}
          onKeyDown={handleKeyDown}
          onSend={handleSendMessage}
          isConnected={state.isConnected}
          disabled={!state.isConnected}
        />
      </div>

      {state.error && (
        <div className="error-message">
          {state.error}
        </div>
      )}
    </div>
  );
};

export default ChatInterface;

消息气泡组件

代码语言:javascript
复制
// MessageBubble.jsx
import React, { useState, useEffect } from 'react';
import Markdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { atomDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';

const MessageBubble = ({ message, isCurrentUser }) => {
  const [isVisible, setIsVisible] = useState(false);
  const [copied, setCopied] = useState(false);

  useEffect(() => {
    // 动画效果
    const timer = setTimeout(() => setIsVisible(true), 50);
    return () => clearTimeout(timer);
  }, []);

  const handleCopy = async () => {
    try {
      await navigator.clipboard.writeText(message.content);
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    } catch (err) {
      console.error('Failed to copy text: ', err);
    }
  };

  const formatTimestamp = (timestamp) => {
    return new Date(timestamp).toLocaleTimeString([], {
      hour: '2-digit',
      minute: '2-digit'
    });
  };

  // 自定义代码块渲染
  const CodeBlock = ({ node, inline, className, children, ...props }) => {
    const match = /language-(\w+)/.exec(className || '');

    if (inline) {
      return <code className={className} {...props}>{children}</code>;
    }

    return (
      <SyntaxHighlighter
        style={atomDark}
        language={match ? match[1] : 'text'}
        PreTag="div"
        className="code-block"
        {...props}
      >
        {String(children).replace(/\n$/, '')}
      </SyntaxHighlighter>
    );
  };

  return (
    <div className={`message-bubble ${isCurrentUser ? 'user-message' : 'ai-message'} ${isVisible ? 'visible' : ''}`}>
      <div className="message-content">
        <div className="message-text">
          <Markdown
            remarkPlugins={[remarkGfm]}
            components={{
              code: CodeBlock,
              p: ({ node, ...props }) => <p {...props} />,
              li: ({ node, ...props }) => <li {...props} />,
              strong: ({ node, ...props }) => <strong {...props} />,
              em: ({ node, ...props }) => <em {...props} />,
              h1: ({ node, ...props }) => <h1 className="message-h1" {...props} />,
              h2: ({ node, ...props }) => <h2 className="message-h2" {...props} />,
              h3: ({ node, ...props }) => <h3 className="message-h3" {...props} />,
              pre: ({ node, ...props }) => <pre className="message-pre" {...props} />,
              a: ({ node, ...props }) => <a className="message-link" target="_blank" rel="noopener noreferrer" {...props} />,
              ol: ({ node, ...props }) => <ol className="message-ol" {...props} />,
              ul: ({ node, ...props }) => <ul className="message-ul" {...props} />
            }}
          >
            {message.content}
          </Markdown>
        </div>

        <div className="message-actions">
          <button
            className="copy-button"
            onClick={handleCopy}
            title="Copy message"
          >
            {copied ? '✓ Copied!' : 'Copy'}
          </button>

          {isCurrentUser && (
            <span className="message-status">
              {message.status === 'sending' && 'Sending...'}
              {message.status === 'received' && '✓ Sent'}
              {message.status === 'error' && '✗ Failed'}
            </span>
          )}
        </div>

        <div className="message-footer">
          <span className="message-timestamp">
            {formatTimestamp(message.timestamp)}
          </span>
          {!isCurrentUser && (
            <span className="message-source">
              {message.model || 'AI Assistant'}
            </span>
          )}
        </div>
      </div>
    </div>
  );
};

export default MessageBubble;

输入组件

代码语言:javascript
复制
// ChatInput.jsx
import React, { useState, useRef, useEffect } from 'react';
import { Send, Mic, Paperclip, Smile } from 'lucide-react';

const ChatInput = ({
  value,
  onChange,
  onKeyDown,
  onSend,
  isConnected,
  disabled
}) => {
  const [inputHeight, setInputHeight] = useState('auto');
  const textareaRef = useRef(null);
  const fileInputRef = useRef(null);

  useEffect(() => {
    adjustHeight();
  }, [value]);

  const adjustHeight = () => {
    if (textareaRef.current) {
      const textarea = textareaRef.current;
      textarea.style.height = 'auto';
      textarea.style.height = Math.min(textarea.scrollHeight, 150) + 'px';
      setInputHeight(textarea.style.height);
    }
  };

  const handleSubmit = () => {
    if (value.trim() && !disabled) {
      onSend(value.trim());
      onChange('');
    }
  };

  const handleKeyDown = (e) => {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault();
      handleSubmit();
    }
    onKeyDown?.(e);
  };

  const handleFileUpload = () => {
    fileInputRef.current?.click();
  };

  const handleFileChange = (e) => {
    const file = e.target.files[0];
    if (file) {
      // 处理文件上传
      processFile(file);
    }
  };

  const processFile = (file) => {
    const reader = new FileReader();
    reader.onload = (e) => {
      const content = e.target.result;
      // 可以将文件内容附加到消息中
      onChange(prev => prev + `\n[File: ${file.name}]`);
    };
    reader.readAsText(file);
  };

  const insertEmoji = (emoji) => {
    onChange(prev => prev + emoji);
  };

  return (
    <div className="chat-input-container">
      <div className="input-tools">
        <button
          className="tool-button"
          onClick={handleFileUpload}
          disabled={disabled}
          title="Attach file"
        >
          <Paperclip size={18} />
        </button>

        <button
          className="tool-button"
          onClick={() => insertEmoji('😊')}
          disabled={disabled}
          title="Insert emoji"
        >
          <Smile size={18} />
        </button>
      </div>

      <div className="input-area">
        <textarea
          ref={textareaRef}
          value={value}
          onChange={(e) => onChange(e.target.value)}
          onKeyDown={handleKeyDown}
          placeholder={isConnected ? "Type your message..." : "Connecting..."}
          disabled={disabled}
          rows={1}
          style={{ height: inputHeight }}
          className="chat-textarea"
        />

        <button
          className={`send-button ${value.trim() ? 'active' : ''}`}
          onClick={handleSubmit}
          disabled={!value.trim() || disabled}
          title="Send message"
        >
          <Send size={20} />
        </button>
      </div>

      <input
        type="file"
        ref={fileInputRef}
        onChange={handleFileChange}
        className="file-input"
        multiple
      />

      <div className="input-hints">
        <span className="hint">Press Enter to send</span>
        <span className="hint">Press Shift+Enter for new line</span>
      </div>
    </div>
  );
};

export default ChatInput;

高级功能实现

流式响应处理

代码语言:javascript
复制
// StreamingResponseHandler.js
class StreamingResponseHandler {
  constructor(webSocketManager) {
    this.webSocketManager = webSocketManager;
    this.activeStreams = new Map();
    this.setupWebSocketHandlers();
  }

  setupWebSocketHandlers() {
    this.webSocketManager.on('stream_start', (data) => {
      this.handleStreamStart(data);
    });

    this.webSocketManager.on('stream_chunk', (data) => {
      this.handleStreamChunk(data);
    });

    this.webSocketManager.on('stream_end', (data) => {
      this.handleStreamEnd(data);
    });

    this.webSocketManager.on('stream_error', (data) => {
      this.handleStreamError(data);
    });
  }

  handleStreamStart(data) {
    const messageId = data.messageId;
    const streamData = {
      messageId,
      content: '',
      startTime: Date.now(),
      onProgress: data.onProgress || (() => {}),
      onComplete: data.onComplete || (() => {})
    };

    this.activeStreams.set(messageId, streamData);

    // 更新UI状态
    this.updateMessageStatus(messageId, 'streaming');
  }

  handleStreamChunk(data) {
    const stream = this.activeStreams.get(data.messageId);
    if (!stream) return;

    stream.content += data.chunk;

    // 更新UI
    this.updateMessageContent(data.messageId, stream.content);

    // 调用进度回调
    stream.onProgress({
      content: stream.content,
      elapsed: Date.now() - stream.startTime,
      isComplete: false
    });
  }

  handleStreamEnd(data) {
    const stream = this.activeStreams.get(data.messageId);
    if (!stream) return;

    // 调用完成回调
    stream.onComplete({
      content: stream.content,
      elapsed: Date.now() - stream.startTime,
      isComplete: true
    });

    // 更新UI状态
    this.updateMessageStatus(data.messageId, 'received');

    // 清理流
    this.activeStreams.delete(data.messageId);
  }

  handleStreamError(data) {
    const stream = this.activeStreams.get(data.messageId);
    if (!stream) return;

    // 调用错误回调
    stream.onComplete({
      error: data.error,
      content: stream.content,
      elapsed: Date.now() - stream.startTime,
      isComplete: true
    });

    // 更新UI状态
    this.updateMessageStatus(data.messageId, 'error');

    // 清理流
    this.activeStreams.delete(data.messageId);
  }

  // UI更新方法
  updateMessageStatus(messageId, status) {
    // 这里可以触发React状态更新
    // 例如通过Context或状态管理库
  }

  updateMessageContent(messageId, content) {
    // 更新消息内容
  }

  // 创建流式聊天请求
  async streamChat(prompt, options = {}) {
    const messageId = `stream_${Date.now()}_${Math.random()}`;

    // 发送流式请求
    await this.webSocketManager.send({
      type: 'stream_chat',
      payload: {
        messageId,
        prompt,
        model: options.model || 'gpt-4',
        temperature: options.temperature || 0.7,
        maxTokens: options.maxTokens || 2000,
        stream: true
      }
    });

    return new Promise((resolve, reject) => {
      // 设置完成回调
      const streamData = this.activeStreams.get(messageId);
      if (streamData) {
        streamData.onComplete = (result) => {
          if (result.error) {
            reject(new Error(result.error));
          } else {
            resolve(result);
          }
        };
      }
    });
  }

  // 流式文本生成
  async streamGenerateText(prompt, options = {}) {
    return this.streamChat(prompt, {
      ...options,
      type: 'text_generation'
    });
  }

  // 流式代码生成
  async streamGenerateCode(prompt, options = {}) {
    return this.streamChat(prompt, {
      ...options,
      type: 'code_generation'
    });
  }
}

export default StreamingResponseHandler;

智能对话历史管理

代码语言:javascript
复制
// ConversationHistoryManager.js
class ConversationHistoryManager {
  constructor(storageKey = 'chat_history') {
    this.storageKey = storageKey;
    this.history = this.loadHistory();
    this.maxHistoryLength = 1000; // 最大历史记录数量
    this.maxContextLength = 4096; // 最大上下文长度
  }

  loadHistory() {
    try {
      const saved = localStorage.getItem(this.storageKey);
      return saved ? JSON.parse(saved) : { sessions: [], currentSessionId: null };
    } catch (error) {
      console.error('Failed to load chat history:', error);
      return { sessions: [], currentSessionId: null };
    }
  }

  saveHistory() {
    try {
      localStorage.setItem(this.storageKey, JSON.stringify(this.history));
    } catch (error) {
      console.error('Failed to save chat history:', error);
    }
  }

  createSession(title = 'New Chat') {
    const session = {
      id: `session_${Date.now()}`,
      title,
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
      messages: [],
      model: 'gpt-4',
      settings: {
        temperature: 0.7,
        maxTokens: 2000
      }
    };

    this.history.sessions.push(session);
    this.history.currentSessionId = session.id;
    this.saveHistory();

    return session;
  }

  getCurrentSession() {
    return this.history.sessions.find(s => s.id === this.history.currentSessionId);
  }

  switchSession(sessionId) {
    const session = this.history.sessions.find(s => s.id === sessionId);
    if (session) {
      this.history.currentSessionId = sessionId;
      this.saveHistory();
      return session;
    }
    return null;
  }

  addMessage(sessionId, message) {
    const session = this.history.sessions.find(s => s.id === sessionId);
    if (!session) return;

    session.messages.push({
      ...message,
      id: message.id || `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
    });

    session.updatedAt = new Date().toISOString();
    this.trimSessionHistory(session);
    this.saveHistory();
  }

  updateMessage(sessionId, messageId, updates) {
    const session = this.history.sessions.find(s => s.id === sessionId);
    if (!session) return false;

    const message = session.messages.find(m => m.id === messageId);
    if (!message) return false;

    Object.assign(message, updates);
    session.updatedAt = new Date().toISOString();
    this.saveHistory();

    return true;
  }

  deleteMessage(sessionId, messageId) {
    const session = this.history.sessions.find(s => s.id === sessionId);
    if (!session) return false;

    const index = session.messages.findIndex(m => m.id === messageId);
    if (index === -1) return false;

    session.messages.splice(index, 1);
    session.updatedAt = new Date().toISOString();
    this.saveHistory();

    return true;
  }

  trimSessionHistory(session) {
    // 按消息数量限制
    if (session.messages.length > this.maxHistoryLength) {
      session.messages = session.messages.slice(-this.maxHistoryLength);
    }

    // 按上下文长度限制
    this.trimByContextLength(session);
  }

  trimByContextLength(session) {
    let totalLength = 0;
    const trimmedMessages = [];

    // 从最新的消息开始计算
    for (let i = session.messages.length - 1; i >= 0; i--) {
      const message = session.messages[i];
      const messageLength = this.getMessageLength(message);

      if (totalLength + messageLength > this.maxContextLength) {
        break;
      }

      trimmedMessages.unshift(message);
      totalLength += messageLength;
    }

    session.messages = trimmedMessages;
  }

  getMessageLength(message) {
    // 简单的消息长度计算,实际可能需要更复杂的token计算
    return (message.content || '').length + (message.sender || '').length;
  }

  getSessionContext(sessionId, maxLength = 2048) {
    const session = this.history.sessions.find(s => s.id === sessionId);
    if (!session) return [];

    let totalLength = 0;
    const context = [];

    // 从最新的消息开始构建上下文
    for (let i = session.messages.length - 1; i >= 0; i--) {
      const message = session.messages[i];
      const messageLength = this.getMessageLength(message);

      if (totalLength + messageLength > maxLength) {
        break;
      }

      context.unshift(message);
      totalLength += messageLength;
    }

    return context;
  }

  exportSession(sessionId) {
    const session = this.history.sessions.find(s => s.id === sessionId);
    if (!session) return null;

    return {
      ...session,
      exportDate: new Date().toISOString()
    };
  }

  importSession(sessionData) {
    // 验证导入的数据
    if (!sessionData.id || !sessionData.messages) {
      throw new Error('Invalid session data');
    }

    // 检查是否已存在
    const existingIndex = this.history.sessions.findIndex(s => s.id === sessionData.id);
    if (existingIndex !== -1) {
      // 可以选择覆盖或生成新ID
      sessionData.id = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
    }

    this.history.sessions.push(sessionData);
    this.saveHistory();

    return sessionData;
  }

  deleteSession(sessionId) {
    const index = this.history.sessions.findIndex(s => s.id === sessionId);
    if (index === -1) return false;

    this.history.sessions.splice(index, 1);

    if (this.history.currentSessionId === sessionId) {
      this.history.currentSessionId = this.history.sessions[0]?.id || null;
    }

    this.saveHistory();
    return true;
  }

  searchSessions(query) {
    const lowerQuery = query.toLowerCase();
    return this.history.sessions.filter(session =>
      session.title.toLowerCase().includes(lowerQuery) ||
      session.messages.some(msg =>
        msg.content.toLowerCase().includes(lowerQuery)
      )
    );
  }

  // 获取会话统计信息
  getSessionStats(sessionId) {
    const session = this.history.sessions.find(s => s.id === sessionId);
    if (!session) return null;

    const stats = {
      totalMessages: session.messages.length,
      totalTokens: 0, // 这里需要实现token计算
      startDate: session.createdAt,
      lastActive: session.updatedAt,
      userMessages: session.messages.filter(m => m.sender === 'user').length,
      aiMessages: session.messages.filter(m => m.sender === 'ai').length
    };

    return stats;
  }

  // 清理旧的历史记录
  cleanupOldHistory(maxAgeDays = 30) {
    const cutoffDate = new Date();
    cutoffDate.setDate(cutoffDate.getDate() - maxAgeDays);

    this.history.sessions = this.history.sessions.filter(session => {
      const sessionDate = new Date(session.updatedAt);
      return sessionDate >= cutoffDate;
    });

    this.saveHistory();
  }
}

export default ConversationHistoryManager;

性能优化

虚拟滚动实现

代码语言:javascript
复制
// VirtualMessageList.jsx
import React, { useRef, useEffect, useCallback } from 'react';

const VirtualMessageList = ({
  messages,
  itemHeight = 100,
  containerHeight = 600,
  overscan = 5,
  renderItem
}) => {
  const containerRef = useRef(null);
  const [scrollTop, setScrollTop] = React.useState(0);
  const [visibleStart, setVisibleStart] = React.useState(0);
  const [visibleEnd, setVisibleEnd] = React.useState(0);

  // 计算可视区域
  const calculateVisibleRange = useCallback(() => {
    const start = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
    const end = Math.min(
      messages.length,
      Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan
    );

    setVisibleStart(start);
    setVisibleEnd(end);
  }, [scrollTop, containerHeight, itemHeight, overscan, messages.length]);

  useEffect(() => {
    calculateVisibleRange();
  }, [calculateVisibleRange]);

  const handleScroll = useCallback((e) => {
    setScrollTop(e.target.scrollTop);
  }, []);

  const visibleMessages = messages.slice(visibleStart, visibleEnd);
  const offsetY = visibleStart * itemHeight;
  const totalHeight = messages.length * itemHeight;

  return (
    <div
      ref={containerRef}
      className="virtual-scroll-container"
      style={{ height: containerHeight, overflow: 'auto' }}
      onScroll={handleScroll}
    >
      <div style={{ height: totalHeight, position: 'relative' }}>
        <div style={{ transform: `translateY(${offsetY}px)` }}>
          {visibleMessages.map((message, index) => (
            <div
              key={message.id}
              style={{ height: itemHeight }}
            >
              {renderItem(message, visibleStart + index)}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
};

// 使用虚拟滚动的聊天组件
const OptimizedChatInterface = ({ messages }) => {
  const renderItem = (message, index) => (
    <MessageBubble
      key={message.id}
      message={message}
      isCurrentUser={message.sender === 'user'}
    />
  );

  return (
    <VirtualMessageList
      messages={messages}
      itemHeight={150}
      containerHeight={600}
      renderItem={renderItem}
    />
  );
};

export default VirtualMessageList;

消息缓存优化

代码语言:javascript
复制
// MessageCache.js
class MessageCache {
  constructor(options = {}) {
    this.maxSize = options.maxSize || 1000;
    this.ttl = options.ttl || 5 * 60 * 1000; // 5分钟
    this.cache = new Map();
    this.accessTimes = new Map();
  }

  get(key) {
    if (!this.cache.has(key)) {
      return undefined;
    }

    const now = Date.now();
    const accessTime = this.accessTimes.get(key);

    if (now - accessTime > this.ttl) {
      this.delete(key);
      return undefined;
    }

    // 更新访问时间(LRU)
    this.accessTimes.set(key, now);
    return this.cache.get(key);
  }

  set(key, value) {
    if (this.cache.size >= this.maxSize) {
      this.evictOldest();
    }

    this.cache.set(key, value);
    this.accessTimes.set(key, Date.now());
  }

  delete(key) {
    this.cache.delete(key);
    this.accessTimes.delete(key);
  }

  evictOldest() {
    let oldestKey = null;
    let oldestTime = Date.now();

    for (const [key, time] of this.accessTimes) {
      if (time < oldestTime) {
        oldestTime = time;
        oldestKey = key;
      }
    }

    if (oldestKey) {
      this.delete(oldestKey);
    }
  }

  // 批量操作
  getBatch(keys) {
    return keys.map(key => this.get(key));
  }

  setBatch(entries) {
    entries.forEach(([key, value]) => this.set(key, value));
  }

  // 清理过期项
  cleanup() {
    const now = Date.now();
    for (const [key, accessTime] of this.accessTimes) {
      if (now - accessTime > this.ttl) {
        this.delete(key);
      }
    }
  }

  // 获取缓存统计
  getStats() {
    return {
      size: this.cache.size,
      maxSize: this.maxSize,
      utilization: this.cache.size / this.maxSize,
      expired: Array.from(this.accessTimes).filter(([_, time]) =>
        Date.now() - time > this.ttl
      ).length
    };
  }
}

// 在聊天组件中使用缓存
class CachedChatManager {
  constructor() {
    this.cache = new MessageCache({ maxSize: 500, ttl: 10 * 60 * 1000 }); // 10分钟
  }

  getMessage(id) {
    let message = this.cache.get(id);
    if (!message) {
      message = this.fetchMessageFromState(id);
      if (message) {
        this.cache.set(id, message);
      }
    }
    return message;
  }

  setMessage(id, message) {
    this.cache.set(id, message);
  }

  // 清理缓存
  clearCache() {
    this.cache = new MessageCache({ maxSize: 500, ttl: 10 * 60 * 1000 });
  }
}

AI聊天机器人的前端实现需要平衡用户体验、性能和功能完整性。通过合理的架构设计和优化策略,可以构建出高效、流畅的对话界面。

总结

  本文深入探讨了AI聊天机器人前端的完整实现方案,涵盖了WebSocket实时通信、React组件设计、状态管理、性能优化等核心技术要点。通过WebSocket与React的深度集成,我们可以构建出具有实时交互能力、流畅用户体验的AI聊天应用。

  关键技术点包括:

  1. WebSocket连接管理:实现可靠的实时通信,包括重连、心跳、错误处理
  2. 状态管理:使用全局状态管理器统一管理聊天状态
  3. 组件架构:设计可复用、可扩展的React组件
  4. 流式响应:处理AI模型的流式输出,提供实时打字效果
  5. 性能优化:实现虚拟滚动、消息缓存等优化策略

  随着AI技术的不断发展,聊天机器人的功能会越来越强大,前端开发者需要持续关注新技术,不断提升用户体验和系统性能。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-08-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 介绍
  • 核心架构设计
    • WebSocket连接管理
    • 聊天状态管理
  • React组件实现
    • 主聊天界面组件
    • 消息气泡组件
    • 输入组件
  • 高级功能实现
    • 流式响应处理
    • 智能对话历史管理
  • 性能优化
    • 虚拟滚动实现
    • 消息缓存优化
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档