
AI聊天机器人正成为现代Web应用的核心功能,通过React与WebSocket的深度集成,可以构建高性能、低延迟的实时对话体验。
随着人工智能技术的飞速发展,AI聊天机器人已成为现代Web应用中不可或缺的功能。从客户服务到个人助理,从教育辅导到娱乐互动,AI聊天机器人正在改变用户与数字产品的交互方式。本文将深入探讨如何使用React和WebSocket技术构建一个高性能的AI聊天机器人前端界面,涵盖从基础组件设计到复杂状态管理的全方位技术要点。
// 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;// 聊天状态管理器
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();// 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;// 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;// 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;// 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;// 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;// 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;// 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聊天应用。
关键技术点包括:
随着AI技术的不断发展,聊天机器人的功能会越来越强大,前端开发者需要持续关注新技术,不断提升用户体验和系统性能。