ChatGPT与React结合的AI聊天机器人开发实战:从零构建智能对话系统

Nina190
Nina190 2026-02-03T15:05:04+08:00
0 0 1

引言

随着人工智能技术的快速发展,基于大语言模型的聊天机器人正在成为前端开发领域的重要趋势。本文将详细介绍如何使用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密钥:

  1. 访问 https://platform.openai.com/
  2. 登录账户后进入API密钥管理页面
  3. 点击"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)

    0/2000