引言
随着人工智能技术的快速发展,基于大语言模型的聊天机器人正在成为前端开发领域的重要趋势。本文将详细介绍如何使用React前端框架与OpenAI ChatGPT API相结合,打造一个功能完整的AI聊天机器人应用。通过本文的学习,开发者将掌握从零开始构建智能对话系统的完整技术栈和最佳实践。
技术栈概述
在开始开发之前,让我们先了解本次项目所涉及的技术栈:
- React: 现代前端开发的核心框架,提供组件化开发体验
- ChatGPT API: OpenAI提供的强大语言模型API接口
- JavaScript/TypeScript: 项目主要编程语言
- CSS/SCSS: 用户界面样式设计
- Axios: HTTP客户端库,用于API请求
- React Hooks: 状态管理和副作用处理
环境准备与项目初始化
1. 创建React项目
# 使用Create React App创建项目
npx create-react-app ai-chatbot --template typescript
cd ai-chatbot
# 安装必要的依赖
npm install axios @mui/material @emotion/react @emotion/styled
2. 获取ChatGPT API密钥
访问OpenAI官网注册账户并获取API密钥:
- 访问 https://platform.openai.com/
- 登录账户后进入API密钥管理页面
- 点击"Create new secret key"生成新的API密钥
3. 配置环境变量
创建 .env 文件:
REACT_APP_OPENAI_API_KEY=your_api_key_here
REACT_APP_OPENAI_API_URL=https://api.openai.com/v1/chat/completions
核心组件设计与实现
1. 聊天机器人主组件
// src/components/ChatBot.tsx
import React, { useState, useEffect, useRef } from 'react';
import axios from 'axios';
import Message from './Message';
import InputArea from './InputArea';
import './ChatBot.css';
interface Message {
id: string;
content: string;
role: 'user' | 'assistant';
timestamp: Date;
}
const ChatBot: React.FC = () => {
const [messages, setMessages] = useState<Message[]>([]);
const [isLoading, setIsLoading] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
// 滚动到底部
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
// 发送消息到ChatGPT
const sendMessage = async (content: string) => {
if (!content.trim() || isLoading) return;
// 添加用户消息
const userMessage: Message = {
id: Date.now().toString(),
content,
role: 'user',
timestamp: new Date(),
};
setMessages(prev => [...prev, userMessage]);
setIsLoading(true);
try {
const response = await axios.post(
process.env.REACT_APP_OPENAI_API_URL || '',
{
model: 'gpt-3.5-turbo',
messages: [
{ role: 'system', content: 'You are a helpful assistant.' },
...messages.map(msg => ({
role: msg.role,
content: msg.content
})),
{ role: 'user', content }
],
temperature: 0.7,
},
{
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.REACT_APP_OPENAI_API_KEY}`
}
}
);
const assistantMessage: Message = {
id: Date.now().toString(),
content: response.data.choices[0].message.content,
role: 'assistant',
timestamp: new Date(),
};
setMessages(prev => [...prev, assistantMessage]);
} catch (error) {
console.error('Error:', error);
const errorMessage: Message = {
id: Date.now().toString(),
content: '抱歉,我遇到了一些问题。请稍后再试。',
role: 'assistant',
timestamp: new Date(),
};
setMessages(prev => [...prev, errorMessage]);
} finally {
setIsLoading(false);
}
};
// 清空聊天记录
const clearChat = () => {
setMessages([]);
};
return (
<div className="chatbot-container">
<div className="chatbot-header">
<h2>AI聊天助手</h2>
<button onClick={clearChat} className="clear-button">
清空对话
</button>
</div>
<div className="messages-container">
{messages.length === 0 ? (
<div className="welcome-message">
<p>你好!我是AI助手,有什么我可以帮助你的吗?</p>
</div>
) : (
messages.map((message) => (
<Message key={message.id} message={message} />
))
)}
{isLoading && (
<div className="typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
)}
<div ref={messagesEndRef} />
</div>
<InputArea onSend={sendMessage} isLoading={isLoading} />
</div>
);
};
export default ChatBot;
2. 消息组件实现
// src/components/Message.tsx
import React from 'react';
import './Message.css';
interface MessageProps {
message: {
id: string;
content: string;
role: 'user' | 'assistant';
timestamp: Date;
};
}
const Message: React.FC<MessageProps> = ({ message }) => {
const formatTime = (date: Date) => {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
};
return (
<div className={`message ${message.role}`}>
<div className="message-content">
<div className="message-text" dangerouslySetInnerHTML={{ __html: message.content.replace(/\n/g, '<br />') }} />
<div className="message-time">{formatTime(message.timestamp)}</div>
</div>
</div>
);
};
export default Message;
3. 输入区域组件
// src/components/InputArea.tsx
import React, { useState, useRef, useEffect } from 'react';
import './InputArea.css';
interface InputAreaProps {
onSend: (content: string) => void;
isLoading: boolean;
}
const InputArea: React.FC<InputAreaProps> = ({ onSend, isLoading }) => {
const [inputValue, setInputValue] = useState('');
const textareaRef = useRef<HTMLTextAreaElement>(null);
// 自适应高度
useEffect(() => {
if (textareaRef.current) {
textareaRef.current.style.height = 'auto';
textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 150)}px`;
}
}, [inputValue]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (inputValue.trim() && !isLoading) {
onSend(inputValue.trim());
setInputValue('');
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSubmit(e as any);
}
};
return (
<form className="input-area" onSubmit={handleSubmit}>
<textarea
ref={textareaRef}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="输入你的消息..."
disabled={isLoading}
rows={1}
/>
<button
type="submit"
className="send-button"
disabled={!inputValue.trim() || isLoading}
>
发送
</button>
</form>
);
};
export default InputArea;
状态管理优化
1. 自定义Hook实现状态管理
// src/hooks/useChatState.ts
import { useState, useEffect } from 'react';
interface ChatState {
messages: Message[];
isLoading: boolean;
error: string | null;
}
const useChatState = () => {
const [state, setState] = useState<ChatState>({
messages: [],
isLoading: false,
error: null,
});
const addMessage = (message: Message) => {
setState(prev => ({
...prev,
messages: [...prev.messages, message]
}));
};
const setLoading = (loading: boolean) => {
setState(prev => ({ ...prev, isLoading: loading }));
};
const setError = (error: string | null) => {
setState(prev => ({ ...prev, error }));
};
const clearChat = () => {
setState({
messages: [],
isLoading: false,
error: null,
});
};
return {
...state,
addMessage,
setLoading,
setError,
clearChat
};
};
export default useChatState;
2. 增强的聊天组件
// src/components/EnhancedChatBot.tsx
import React, { useState, useEffect, useRef } from 'react';
import axios from 'axios';
import Message from './Message';
import InputArea from './InputArea';
import useChatState from '../hooks/useChatState';
import './ChatBot.css';
const EnhancedChatBot: React.FC = () => {
const {
messages,
isLoading,
error,
addMessage,
setLoading,
setError,
clearChat
} = useChatState();
const messagesEndRef = useRef<HTMLDivElement>(null);
// 滚动到底部
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
// 发送消息到ChatGPT
const sendMessage = async (content: string) => {
if (!content.trim() || isLoading) return;
try {
setLoading(true);
setError(null);
// 添加用户消息
const userMessage = {
id: Date.now().toString(),
content,
role: 'user' as const,
timestamp: new Date(),
};
addMessage(userMessage);
const response = await axios.post(
process.env.REACT_APP_OPENAI_API_URL || '',
{
model: 'gpt-3.5-turbo',
messages: [
{ role: 'system', content: 'You are a helpful assistant.' },
...messages.map(msg => ({
role: msg.role,
content: msg.content
})),
{ role: 'user', content }
],
temperature: 0.7,
},
{
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.REACT_APP_OPENAI_API_KEY}`
}
}
);
const assistantMessage = {
id: Date.now().toString(),
content: response.data.choices[0].message.content,
role: 'assistant' as const,
timestamp: new Date(),
};
addMessage(assistantMessage);
} catch (err) {
console.error('Error:', err);
setError('请求失败,请稍后重试');
const errorMessage = {
id: Date.now().toString(),
content: '抱歉,我遇到了一些问题。请稍后再试。',
role: 'assistant' as const,
timestamp: new Date(),
};
addMessage(errorMessage);
} finally {
setLoading(false);
}
};
return (
<div className="chatbot-container">
<div className="chatbot-header">
<h2>AI聊天助手</h2>
<button onClick={clearChat} className="clear-button">
清空对话
</button>
</div>
<div className="messages-container">
{messages.length === 0 ? (
<div className="welcome-message">
<p>你好!我是AI助手,有什么我可以帮助你的吗?</p>
</div>
) : (
messages.map((message) => (
<Message key={message.id} message={message} />
))
)}
{isLoading && (
<div className="typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
)}
{error && <div className="error-message">{error}</div>}
<div ref={messagesEndRef} />
</div>
<InputArea onSend={sendMessage} isLoading={isLoading} />
</div>
);
};
export default EnhancedChatBot;
UI样式设计
1. 聊天容器样式
/* src/components/ChatBot.css */
.chatbot-container {
display: flex;
flex-direction: column;
height: 100vh;
max-width: 800px;
margin: 0 auto;
background: #f5f5f5;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.chatbot-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: #ffffff;
border-bottom: 1px solid #e0e0e0;
}
.chatbot-header h2 {
margin: 0;
color: #333;
font-size: 1.2rem;
}
.clear-button {
padding: 8px 16px;
background: #f0f0f0;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
transition: background-color 0.2s;
}
.clear-button:hover {
background: #e0e0e0;
}
.messages-container {
flex: 1;
padding: 20px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 16px;
}
.welcome-message {
text-align: center;
padding: 40px 20px;
color: #666;
font-size: 1.1rem;
}
.typing-indicator {
display: flex;
align-items: center;
padding: 12px 16px;
background: #f0f0f0;
border-radius: 18px;
max-width: 80px;
}
.typing-indicator span {
height: 8px;
width: 8px;
background: #666;
border-radius: 50%;
margin: 0 3px;
animation: typing 1.4s infinite ease-in-out;
}
.typing-indicator span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-indicator span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typing {
0%, 60%, 100% { transform: translateY(0); }
30% { transform: translateY(-5px); }
}
.error-message {
padding: 12px 16px;
background: #ffebee;
border-radius: 8px;
color: #c62828;
border-left: 4px solid #c62828;
}
2. 消息样式
/* src/components/Message.css */
.message {
display: flex;
margin-bottom: 12px;
}
.message.user {
justify-content: flex-end;
}
.message-content {
max-width: 70%;
padding: 12px 16px;
border-radius: 18px;
line-height: 1.4;
position: relative;
word-wrap: break-word;
}
.message.user .message-content {
background: #007bff;
color: white;
border-bottom-right-radius: 4px;
}
.message.assistant .message-content {
background: #e9ecef;
color: #333;
border-bottom-left-radius: 4px;
}
.message-text {
margin: 0;
white-space: pre-wrap;
}
.message-time {
font-size: 0.7rem;
opacity: 0.7;
margin-top: 4px;
text-align: right;
}
.message.user .message-time {
color: rgba(255, 255, 255, 0.8);
}
.message.assistant .message-time {
color: #666;
}
3. 输入区域样式
/* src/components/InputArea.css */
.input-area {
display: flex;
padding: 16px;
background: #ffffff;
border-top: 1px solid #e0e0e0;
gap: 8px;
}
.input-area textarea {
flex: 1;
padding: 12px 16px;
border: 1px solid #ddd;
border-radius: 20px;
resize: none;
font-size: 1rem;
line-height: 1.4;
outline: none;
transition: border-color 0.2s;
}
.input-area textarea:focus {
border-color: #007bff;
}
.input-area textarea:disabled {
background: #f5f5f5;
cursor: not-allowed;
}
.send-button {
padding: 12px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 20px;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.2s;
min-width: 80px;
}
.send-button:hover:not(:disabled) {
background: #0056b3;
}
.send-button:disabled {
background: #ccc;
cursor: not-allowed;
}
高级功能实现
1. 历史记录保存
// src/hooks/useChatHistory.ts
import { useState, useEffect } from 'react';
const useChatHistory = () => {
const [history, setHistory] = useState<Message[][]>([]);
const [currentHistoryIndex, setCurrentHistoryIndex] = useState<number>(-1);
// 保存聊天记录
const saveConversation = (messages: Message[]) => {
const newHistory = [...history];
if (currentHistoryIndex < newHistory.length - 1) {
newHistory.splice(currentHistoryIndex + 1);
}
newHistory.push([...messages]);
setHistory(newHistory);
setCurrentHistoryIndex(newHistory.length - 1);
// 本地存储
localStorage.setItem('chatHistory', JSON.stringify(newHistory));
};
// 加载历史记录
const loadHistory = () => {
const saved = localStorage.getItem('chatHistory');
if (saved) {
setHistory(JSON.parse(saved));
}
};
// 撤销操作
const undo = () => {
if (currentHistoryIndex > 0) {
setCurrentHistoryIndex(currentHistoryIndex - 1);
return history[currentHistoryIndex - 1];
}
return null;
};
// 重做操作
const redo = () => {
if (currentHistoryIndex < history.length - 1) {
setCurrentHistoryIndex(currentHistoryIndex + 1);
return history[currentHistoryIndex + 1];
}
return null;
};
useEffect(() => {
loadHistory();
}, []);
return {
history,
currentHistoryIndex,
saveConversation,
undo,
redo
};
};
2. 多轮对话上下文管理
// src/utils/contextManager.ts
interface ContextManager {
context: Message[];
addMessage: (message: Message) => void;
clearContext: () => void;
getContext: () => Message[];
}
const createContextManager = (): ContextManager => {
let context: Message[] = [];
return {
context,
addMessage(message: Message) {
context.push(message);
// 保持上下文长度在合理范围内
if (context.length > 20) {
context = context.slice(-15);
}
},
clearContext() {
context = [];
},
getContext() {
return [...context];
}
};
};
export default createContextManager;
3. 响应式设计优化
/* src/components/ChatBot.css (响应式部分) */
@media (max-width: 768px) {
.chatbot-container {
height: 100vh;
margin: 0;
border-radius: 0;
}
.chatbot-header {
padding: 12px 16px;
}
.messages-container {
padding: 12px;
}
.message-content {
max-width: 85%;
}
.input-area {
padding: 12px;
}
}
@media (max-width: 480px) {
.chatbot-header h2 {
font-size: 1rem;
}
.clear-button {
padding: 6px 12px;
font-size: 0.8rem;
}
.message-content {
padding: 10px 12px;
font-size: 0.9rem;
}
}
错误处理与用户体验优化
1. 完善的错误处理机制
// src/components/ChatBotWithErrorHandling.tsx
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import Message from './Message';
import InputArea from './InputArea';
const ChatBotWithErrorHandling: React.FC = () => {
const [messages, setMessages] = useState<Message[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [retryCount, setRetryCount] = useState(0);
const sendMessage = async (content: string) => {
if (!content.trim() || isLoading) return;
// 添加用户消息
const userMessage: Message = {
id: Date.now().toString(),
content,
role: 'user',
timestamp: new Date(),
};
setMessages(prev => [...prev, userMessage]);
setIsLoading(true);
setError(null);
try {
const response = await axios.post(
process.env.REACT_APP_OPENAI_API_URL || '',
{
model: 'gpt-3.5-turbo',
messages: [
{ role: 'system', content: 'You are a helpful assistant.' },
...messages.map(msg => ({
role: msg.role,
content: msg.content
})),
{ role: 'user', content }
],
temperature: 0.7,
},
{
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.REACT_APP_OPENAI_API_KEY}`
},
timeout: 30000 // 30秒超时
}
);
const assistantMessage: Message = {
id: Date.now().toString(),
content: response.data.choices[0].message.content,
role: 'assistant',
timestamp: new Date(),
};
setMessages(prev => [...prev, assistantMessage]);
setRetryCount(0);
} catch (err) {
console.error('API Error:', err);
// 根据错误类型处理
if (axios.isAxiosError(err)) {
const errorResponse = err.response;
if (errorResponse?.status === 429) {
setError('请求过于频繁,请稍后再试');
} else if (errorResponse?.status === 401) {
setError('API密钥验证失败,请检查配置');
} else if (errorResponse?.status === 500) {
setError('服务器内部错误,请稍后重试');
} else {
setError(`请求失败: ${errorResponse?.status || '网络错误'}`);
}
} else {
setError('网络连接失败,请检查网络设置');
}
// 重试机制
if (retryCount < 3) {
setTimeout(() => {
setRetryCount(prev => prev + 1);
sendMessage(content);
}, 2000 * (retryCount + 1));
}
} finally {
setIsLoading(false);
}
};
const clearChat = () => {
setMessages([]);
setError(null);
setRetryCount(0);
};
return (
<div className="chatbot-container">
<div className="chatbot-header">
<h2>AI聊天助手</h2>
<button onClick={clearChat} className="clear-button">
清空对话
</button>
</div>
<div className="messages-container">
{messages.length === 0 ? (
<div className="welcome-message">
<p>你好!我是AI助手,有什么我可以帮助你的吗?</p>
</div>
) : (
messages.map((message) => (
<Message key={message.id} message={message} />
))
)}
{isLoading && (
<div className="typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
)}
{error && (
<div className="error-message">
<p>{error}</p>
{retryCount > 0 && (
<p>正在重试... ({retryCount}/3)</p>
)}
</div>
)}
</div>
<InputArea onSend={sendMessage} isLoading={isLoading} />
</div>
);
};
export default ChatBotWithErrorHandling;
2. 加载状态优化
// src/components/LoadingSpinner.tsx
import React from 'react';
import './LoadingSpinner.css';
const LoadingSpinner: React.FC = () => {
return (
<div className="loading-spinner">
<div className="spinner"></div>
<p>正在思考中...</p>
</div>
);
};
export default LoadingSpinner;
/* src/components/LoadingSpinner.css */
.loading-spinner {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
gap: 10px;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-spinner p {
margin: 0;
color: #666;
font-size: 0.9rem;
}

评论 (0)