0%

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

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

介绍

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

核心架构设计

WebSocket连接管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
// 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;

聊天状态管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
// 聊天状态管理器
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组件实现

主聊天界面组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
// 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;

消息气泡组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
// 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;

输入组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
// 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;

高级功能实现

流式响应处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
// 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;

智能对话历史管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
// 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;

性能优化

虚拟滚动实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// 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;

消息缓存优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
// 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技术的不断发展,聊天机器人的功能会越来越强大,前端开发者需要持续关注新技术,不断提升用户体验和系统性能。

bulb