引言
随着人工智能技术的快速发展,基于大语言模型的聊天机器人正在成为前端开发领域的热门话题。ChatGPT作为OpenAI推出的革命性AI模型,为开发者提供了强大的自然语言处理能力。本文将详细介绍如何将ChatGPT API与React前端框架完美结合,构建一个功能完整的智能聊天机器人应用。
通过本文的学习,您将掌握:
- ChatGPT API的调用方法和最佳实践
- React状态管理的核心概念和实现方式
- 智能聊天机器人的用户界面设计
- 前端应用的部署和优化策略
项目概述
技术栈选择
本项目采用现代前端技术栈:
- React: 用于构建用户界面的核心框架
- TypeScript: 提供类型安全和更好的开发体验
- Axios: HTTP客户端库,用于API调用
- React Hooks: 状态管理和副作用处理
- CSS Modules/Styled Components: 样式管理
- Vite: 现代化的构建工具
功能特性
我们的聊天机器人应用将具备以下核心功能:
- 实时对话交互
- 消息历史记录
- 加载状态显示
- 错误处理机制
- 响应式设计
- 用户友好的界面体验
环境准备与项目初始化
1. 创建React项目
首先,使用Vite快速创建一个React项目:
npm create vite@latest chatgpt-chatbot --template react-ts
cd chatgpt-chatbot
npm install
2. 安装依赖包
npm install axios react-router-dom @types/react-router-dom
3. 获取ChatGPT API密钥
访问OpenAI官网注册账号并获取API密钥。确保在项目中安全地存储API密钥。
ChatGPT API集成
1. API调用基础概念
ChatGPT API基于HTTP请求,主要使用POST方法发送消息并接收响应。API端点为:
https://api.openai.com/v1/chat/completions
2. 创建API服务层
创建一个专门的API服务文件来管理所有与ChatGPT的交互:
// src/services/chatgptService.ts
import axios, { AxiosError } from 'axios';
interface ChatMessage {
role: 'user' | 'assistant' | 'system';
content: string;
}
interface ChatCompletionRequest {
model: string;
messages: ChatMessage[];
temperature?: number;
max_tokens?: number;
top_p?: number;
frequency_penalty?: number;
presence_penalty?: number;
}
interface ChatCompletionResponse {
id: string;
object: string;
created: number;
model: string;
choices: Array<{
index: number;
message: ChatMessage;
finish_reason: string;
}>;
usage: {
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
};
}
class ChatGPTService {
private apiKey: string;
private baseURL: string;
constructor() {
this.apiKey = import.meta.env.VITE_OPENAI_API_KEY || '';
this.baseURL = 'https://api.openai.com/v1';
if (!this.apiKey) {
console.error('OpenAI API key is not set');
}
}
async sendMessage(messages: ChatMessage[]): Promise<string> {
try {
const response = await axios.post<ChatCompletionResponse>(
`${this.baseURL}/chat/completions`,
{
model: 'gpt-3.5-turbo',
messages,
temperature: 0.7,
max_tokens: 1000,
} as ChatCompletionRequest,
{
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
},
}
);
return response.data.choices[0].message.content.trim();
} catch (error) {
if (axios.isAxiosError(error)) {
const axiosError = error as AxiosError;
console.error('API Error:', axiosError.message);
throw new Error(`API Error: ${axiosError.response?.data || axiosError.message}`);
}
throw new Error('Unknown error occurred');
}
}
}
export default new ChatGPTService();
3. 环境变量配置
在项目根目录创建.env文件:
# .env
VITE_OPENAI_API_KEY=your_api_key_here
VITE_APP_NAME=ChatGPT Chatbot
React组件架构设计
1. 应用主组件结构
// src/App.tsx
import React, { useState } from 'react';
import ChatContainer from './components/ChatContainer';
import Header from './components/Header';
import './App.css';
function App() {
const [isConnected, setIsConnected] = useState<boolean>(true);
return (
<div className="app">
<Header />
<main className="main-content">
<ChatContainer
isConnected={isConnected}
onConnectionChange={setIsConnected}
/>
</main>
</div>
);
}
export default App;
2. 聊天容器组件
// src/components/ChatContainer.tsx
import React, { useState, useEffect, useRef } from 'react';
import MessageList from './MessageList';
import MessageInput from './MessageInput';
import chatGPTService from '../services/chatgptService';
import { ChatMessage } from '../types';
interface ChatContainerProps {
isConnected: boolean;
onConnectionChange: (connected: boolean) => void;
}
const ChatContainer: React.FC<ChatContainerProps> = ({
isConnected,
onConnectionChange
}) => {
const [messages, setMessages] = useState<ChatMessage[]>([
{
id: '1',
role: 'assistant',
content: '你好!我是智能聊天机器人,有什么我可以帮助你的吗?',
timestamp: new Date(),
}
]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
// 自动滚动到底部
useEffect(() => {
scrollToBottom();
}, [messages]);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
const handleSendMessage = async (messageContent: string) => {
if (!isConnected || !messageContent.trim()) return;
// 添加用户消息
const userMessage: ChatMessage = {
id: Date.now().toString(),
role: 'user',
content: messageContent,
timestamp: new Date(),
};
setMessages(prev => [...prev, userMessage]);
setIsLoading(true);
try {
// 调用ChatGPT API
const response = await chatGPTService.sendMessage([
...messages.map(msg => ({
role: msg.role,
content: msg.content
})),
{
role: 'user',
content: messageContent
}
]);
// 添加AI回复
const aiMessage: ChatMessage = {
id: (Date.now() + 1).toString(),
role: 'assistant',
content: response,
timestamp: new Date(),
};
setMessages(prev => [...prev, aiMessage]);
} catch (error) {
console.error('Error sending message:', error);
const errorMessage: ChatMessage = {
id: (Date.now() + 1).toString(),
role: 'assistant',
content: '抱歉,我遇到了一些问题。请稍后再试。',
timestamp: new Date(),
};
setMessages(prev => [...prev, errorMessage]);
} finally {
setIsLoading(false);
}
};
return (
<div className="chat-container">
<MessageList messages={messages} />
<div ref={messagesEndRef} />
<MessageInput
onSendMessage={handleSendMessage}
isLoading={isLoading}
isConnected={isConnected}
/>
</div>
);
};
export default ChatContainer;
3. 消息列表组件
// src/components/MessageList.tsx
import React from 'react';
import MessageItem from './MessageItem';
import { ChatMessage } from '../types';
interface MessageListProps {
messages: ChatMessage[];
}
const MessageList: React.FC<MessageListProps> = ({ messages }) => {
return (
<div className="message-list">
{messages.map((message) => (
<MessageItem key={message.id} message={message} />
))}
</div>
);
};
export default MessageList;
4. 消息项组件
// src/components/MessageItem.tsx
import React from 'react';
import { ChatMessage } from '../types';
interface MessageItemProps {
message: ChatMessage;
}
const MessageItem: React.FC<MessageItemProps> = ({ message }) => {
const isUser = message.role === 'user';
return (
<div className={`message-item ${isUser ? 'user-message' : 'assistant-message'}`}>
<div className="message-content">
<div className="message-text">{message.content}</div>
<div className="message-timestamp">
{message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</div>
</div>
</div>
);
};
export default MessageItem;
5. 消息输入组件
// src/components/MessageInput.tsx
import React, { useState, useRef, useEffect } from 'react';
interface MessageInputProps {
onSendMessage: (content: string) => void;
isLoading: boolean;
isConnected: boolean;
}
const MessageInput: React.FC<MessageInputProps> = ({
onSendMessage,
isLoading,
isConnected
}) => {
const [inputValue, setInputValue] = useState<string>('');
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 && isConnected) {
onSendMessage(inputValue.trim());
setInputValue('');
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSubmit(e as any);
}
};
return (
<form className="message-input-form" onSubmit={handleSubmit}>
<div className="input-container">
<textarea
ref={textareaRef}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={isConnected ? "输入消息..." : "连接已断开"}
disabled={!isConnected || isLoading}
className="message-textarea"
/>
<button
type="submit"
disabled={!inputValue.trim() || isLoading || !isConnected}
className="send-button"
>
{isLoading ? (
<span className="loading-spinner">发送中...</span>
) : (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
<path
d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"
fill="currentColor"
/>
</svg>
)}
</button>
</div>
</form>
);
};
export default MessageInput;
状态管理优化
1. 使用Context进行全局状态管理
// src/context/ChatContext.tsx
import React, { createContext, useContext, useReducer } from 'react';
import { ChatMessage } from '../types';
interface ChatState {
messages: ChatMessage[];
isLoading: boolean;
isConnected: boolean;
}
interface ChatAction {
type: 'ADD_MESSAGE' | 'SET_LOADING' | 'SET_CONNECTION' | 'CLEAR_MESSAGES';
payload?: any;
}
const initialState: ChatState = {
messages: [
{
id: '1',
role: 'assistant',
content: '你好!我是智能聊天机器人,有什么我可以帮助你的吗?',
timestamp: new Date(),
}
],
isLoading: false,
isConnected: true,
};
const ChatContext = createContext<{
state: ChatState;
dispatch: React.Dispatch<ChatAction>;
}>({
state: initialState,
dispatch: () => null,
});
const chatReducer = (state: ChatState, action: ChatAction): ChatState => {
switch (action.type) {
case 'ADD_MESSAGE':
return {
...state,
messages: [...state.messages, action.payload],
};
case 'SET_LOADING':
return {
...state,
isLoading: action.payload,
};
case 'SET_CONNECTION':
return {
...state,
isConnected: action.payload,
};
case 'CLEAR_MESSAGES':
return {
...state,
messages: [],
};
default:
return state;
}
};
export const ChatProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [state, dispatch] = useReducer(chatReducer, initialState);
return (
<ChatContext.Provider value={{ state, dispatch }}>
{children}
</ChatContext.Provider>
);
};
export const useChat = () => {
const context = useContext(ChatContext);
if (!context) {
throw new Error('useChat must be used within a ChatProvider');
}
return context;
};
2. 重构组件使用Context
// src/components/ChatContainer.tsx (重构版本)
import React, { useEffect, useRef } from 'react';
import MessageList from './MessageList';
import MessageInput from './MessageInput';
import chatGPTService from '../services/chatgptService';
import { useChat } from '../context/ChatContext';
import { ChatMessage } from '../types';
const ChatContainer: React.FC = () => {
const { state, dispatch } = useChat();
const messagesEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
scrollToBottom();
}, [state.messages]);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
const handleSendMessage = async (messageContent: string) => {
if (!messageContent.trim() || state.isLoading || !state.isConnected) return;
// 添加用户消息
const userMessage: ChatMessage = {
id: Date.now().toString(),
role: 'user',
content: messageContent,
timestamp: new Date(),
};
dispatch({ type: 'ADD_MESSAGE', payload: userMessage });
dispatch({ type: 'SET_LOADING', payload: true });
try {
const response = await chatGPTService.sendMessage([
...state.messages.map(msg => ({
role: msg.role,
content: msg.content
})),
{
role: 'user',
content: messageContent
}
]);
const aiMessage: ChatMessage = {
id: (Date.now() + 1).toString(),
role: 'assistant',
content: response,
timestamp: new Date(),
};
dispatch({ type: 'ADD_MESSAGE', payload: aiMessage });
} catch (error) {
console.error('Error sending message:', error);
const errorMessage: ChatMessage = {
id: (Date.now() + 1).toString(),
role: 'assistant',
content: '抱歉,我遇到了一些问题。请稍后再试。',
timestamp: new Date(),
};
dispatch({ type: 'ADD_MESSAGE', payload: errorMessage });
} finally {
dispatch({ type: 'SET_LOADING', payload: false });
}
};
return (
<div className="chat-container">
<MessageList messages={state.messages} />
<div ref={messagesEndRef} />
<MessageInput
onSendMessage={handleSendMessage}
isLoading={state.isLoading}
isConnected={state.isConnected}
/>
</div>
);
};
export default ChatContainer;
用户界面设计与样式
1. CSS样式文件
/* src/App.css */
.app {
display: flex;
flex-direction: column;
height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
max-width: 800px;
margin: 0 auto;
width: 100%;
padding: 20px;
}
/* Header Styles */
.header {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.header-title {
font-size: 1.5rem;
font-weight: 600;
color: #333;
margin: 0;
}
.header-status {
display: flex;
align-items: center;
gap: 8px;
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
background-color: #4ade80;
}
.status-text {
font-size: 0.9rem;
color: #4b5563;
}
/* Chat Container Styles */
.chat-container {
flex: 1;
display: flex;
flex-direction: column;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
overflow: hidden;
margin-top: 20px;
}
.message-list {
flex: 1;
padding: 20px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 16px;
}
/* Message Styles */
.message-item {
max-width: 80%;
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.user-message {
align-self: flex-end;
}
.assistant-message {
align-self: flex-start;
}
.message-content {
background: rgba(255, 255, 255, 0.9);
border-radius: 18px;
padding: 16px 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
position: relative;
word-wrap: break-word;
}
.user-message .message-content {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-bottom-right-radius: 4px;
}
.assistant-message .message-content {
background: rgba(243, 244, 246, 0.9);
color: #374151;
border-bottom-left-radius: 4px;
}
.message-text {
font-size: 1rem;
line-height: 1.5;
margin-bottom: 8px;
}
.message-timestamp {
font-size: 0.75rem;
opacity: 0.7;
text-align: right;
}
/* Input Styles */
.message-input-form {
padding: 20px;
border-top: 1px solid rgba(0, 0, 0, 0.05);
background: rgba(255, 255, 255, 0.8);
}
.input-container {
display: flex;
gap: 12px;
align-items: flex-end;
}
.message-textarea {
flex: 1;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
padding: 14px 16px;
font-size: 1rem;
resize: none;
min-height: 56px;
max-height: 150px;
background: rgba(255, 255, 255, 0.9);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
transition: border-color 0.2s ease;
}
.message-textarea:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.2);
}
.send-button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 50%;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: white;
transition: transform 0.2s ease, box-shadow 0.2s ease;
flex-shrink: 0;
}
.send-button:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.send-button:disabled {
background: #9ca3af;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.loading-spinner {
display: flex;
align-items: center;
gap: 4px;
font-size: 0.8rem;
}
/* Responsive Design */
@media (max-width: 768px) {
.app {
padding: 10px;
}
.main-content {
padding: 10px;
}
.header {
padding: 1rem;
}
.message-item {
max-width: 90%;
}
.message-textarea {
min-height: 48px;
}
.send-button {
width: 40px;
height: 40px;
}
}
错误处理与用户体验优化
1. 完善的错误处理机制
// src/utils/errorHandler.ts
export const handleApiError = (error: any): string => {
if (error.response) {
// 服务器响应错误
switch (error.response.status) {
case 401:
return '认证失败,请检查API密钥是否正确';
case 429:
return '请求过于频繁,请稍后再试';
case 500:
return '服务器内部错误,请稍后再试';
default:
return `请求失败: ${error.response.data?.error?.message || error.response.statusText}`;
}
} else if (error.request) {
// 网络请求错误
return '网络连接失败,请检查网络设置';
} else {
// 其他错误
return `请求配置错误: ${error.message}`;
}
};
export const handleUserError = (message: string): void => {
console.error('User Error:', message);
// 可以在这里添加用户友好的提示逻辑
};
2. 网络状态检测
// src/hooks/useNetworkStatus.ts
import { useState, useEffect } from 'react';
export const useNetworkStatus = () => {
const [isOnline, setIsOnline] = useState<boolean>(navigator.onLine);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
};
3. 消息历史保存
// src/utils/storage.ts
export const saveMessagesToStorage = (messages: any[]) => {
try {
localStorage.setItem('chatMessages', JSON.stringify(messages));
} catch (error) {
console.error('Failed to save messages:', error);
}
};
export const loadMessagesFromStorage = (): any[] => {
try {
const stored = localStorage.getItem('chatMessages');
return stored ? JSON.parse(stored) : [];
} catch (error) {
console.error('Failed to load messages:', error);
return [];
}
};
性能优化策略
1. 虚拟滚动实现
对于大量消息的场景,可以实现虚拟滚动:
// src/components/VirtualizedMessageList.tsx
import React, { useState, useEffect, useRef } from 'react';
interface VirtualizedMessageListProps {
messages: any[];
itemHeight: number;
visibleCount: number;
}
const VirtualizedMessageList: React.FC<VirtualizedMessageListProps> = ({
messages,
itemHeight = 80,
visibleCount = 20
}) => {
const [scrollTop, setScrollTop] = useState(0);
const containerRef = useRef<HTMLDivElement>(null);
const totalHeight = messages.length * itemHeight;
const visibleHeight = visibleCount * itemHeight;
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
setScrollTop(e.currentTarget.scrollTop);
};
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = Math.min(startIndex + visibleCount, messages.length);
return (
<div
ref={containerRef}
className="virtualized-list"
onScroll={handleScroll}
style={{ height: visibleHeight, overflowY: 'auto' }}
>
<div style={{ height: totalHeight }}>
{messages.slice(startIndex, endIndex).map((message, index) => (
<div
key={message.id}
style={{
height: itemHeight,
transform: `translateY(${(startIndex + index) * itemHeight}px)`
}}
>
{/* 消息组件 */}
</div>
))}
</div>
</div>
);
};
export default VirtualizedMessageList;
2. 缓存机制优化
// src/services/cacheService.ts
class CacheService {
private cache: Map<string, { data: any; timestamp
评论 (0)