diff --git a/.gitignore b/.gitignore index 17a1f49..ceb7b5b 100644 --- a/.gitignore +++ b/.gitignore @@ -160,3 +160,7 @@ dist .yarn/install-state.gz .pnp.* 1097d3adbfbe8745c7109358ddb3635d08fb7c47 + +# Documentation files (temporary setup guides) +API_SETUP.md +QUOTA_SOLUTION.md diff --git a/eslint.config.js b/eslint.config.js index d2336c8..ca3725a 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -42,6 +42,10 @@ export default [ HTMLTextAreaElement: 'readonly', MouseEvent: 'readonly', Node: 'readonly', + process: 'readonly', + fetch: 'readonly', + RequestInit: 'readonly', + Response: 'readonly', }, }, plugins: { diff --git a/src/MainLayout/NavBar/NavBar.tsx b/src/MainLayout/NavBar/NavBar.tsx index 8e2d769..545a495 100644 --- a/src/MainLayout/NavBar/NavBar.tsx +++ b/src/MainLayout/NavBar/NavBar.tsx @@ -32,7 +32,7 @@ const NavBar: React.FC = () => { }; const handleLogout = () => { - // 这里可以添加登出逻辑 + // Add logout logic here // TODO: Implement logout logic }; diff --git a/src/MainLayout/Sidebar/Sidebar.tsx b/src/MainLayout/Sidebar/Sidebar.tsx index 5924ad2..53ae71c 100644 --- a/src/MainLayout/Sidebar/Sidebar.tsx +++ b/src/MainLayout/Sidebar/Sidebar.tsx @@ -142,7 +142,7 @@ const Sidebar: React.FC = () => { )} - {/* 搜索弹窗 */} + {/* Search modal */} {showSearch && (
setShowSearch(false)}>
e.stopPropagation()}> diff --git a/src/Pages/Chat/Chat.module.css b/src/Pages/Chat/Chat.module.css index 776b015..f4762f9 100644 --- a/src/Pages/Chat/Chat.module.css +++ b/src/Pages/Chat/Chat.module.css @@ -56,6 +56,28 @@ font-weight: 400; } +.apiWarning { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + margin-top: 16px; + padding: 12px 16px; + background: #fef3c7; + border: 1px solid #f59e0b; + border-radius: 8px; + color: #92400e; + font-size: 0.875rem; + max-width: 500px; + margin-left: auto; + margin-right: auto; +} + +.apiWarning svg { + flex-shrink: 0; + color: #f59e0b; +} + .chatInputContainer { margin-bottom: 40px; } diff --git a/src/Pages/Chat/index.tsx b/src/Pages/Chat/index.tsx index 7e2e526..eac936f 100644 --- a/src/Pages/Chat/index.tsx +++ b/src/Pages/Chat/index.tsx @@ -1,8 +1,9 @@ import React, { useState, useRef, useEffect } from 'react'; -import { Send, Mic, Paperclip, Smile, Sparkles, Plus } from 'lucide-react'; +import { Send, Mic, Paperclip, Smile, Sparkles, Plus, AlertCircle } from 'lucide-react'; import styles from './Chat.module.css'; import { useChatStore, useCurrentMessages, Message } from '../../stores/chatStore'; import { useUserStore } from '../../stores/userStore'; +import openAIService from '../../services/openaiService'; const Chat: React.FC = () => { const [inputValue, setInputValue] = useState(''); @@ -14,7 +15,9 @@ const Chat: React.FC = () => { // translation function const t = (key: string) => { - const translations: { [key: string]: { [key: string]: string } } = { + const translations: { + [key: string]: { [key: string]: string }; + } = { 'en-US': { 'chat.placeholder': 'Message ThinkML...', 'chat.welcome.title': 'ThinkML', @@ -25,6 +28,11 @@ const Chat: React.FC = () => { 'chat.welcome.suggestion3': 'How to optimize website performance?', 'chat.welcome.suggestion4': 'Write a simple todo app', 'nav.newChat': 'New Chat', + 'chat.error.api': 'API Error', + 'chat.error.noApiKey': + 'OpenAI API key not configured. Please set REACT_APP_OPENAI_API_KEY in your environment variables.', + 'chat.error.network': 'Network error. Please check your connection.', + 'chat.error.unknown': 'An unknown error occurred.', }, 'zh-CN': { 'chat.placeholder': '发送消息给 ThinkML...', @@ -36,6 +44,11 @@ const Chat: React.FC = () => { 'chat.welcome.suggestion3': '如何优化网站性能?', 'chat.welcome.suggestion4': '写一个简单的待办应用', 'nav.newChat': '新建聊天', + 'chat.error.api': 'API 错误', + 'chat.error.noApiKey': + 'OpenAI API 密钥未配置。请在环境变量中设置 REACT_APP_OPENAI_API_KEY。', + 'chat.error.network': '网络错误。请检查您的连接。', + 'chat.error.unknown': '发生未知错误。', }, 'ja-JP': { 'chat.placeholder': 'ThinkMLにメッセージを送信...', @@ -47,6 +60,11 @@ const Chat: React.FC = () => { 'chat.welcome.suggestion3': 'ウェブサイトのパフォーマンスを最適化する方法は?', 'chat.welcome.suggestion4': 'シンプルなTodoアプリを作成して', 'nav.newChat': '新しいチャット', + 'chat.error.api': 'API エラー', + 'chat.error.noApiKey': + 'OpenAI API キーが設定されていません。環境変数で REACT_APP_OPENAI_API_KEY を設定してください。', + 'chat.error.network': 'ネットワークエラー。接続を確認してください。', + 'chat.error.unknown': '不明なエラーが発生しました。', }, }; @@ -83,17 +101,66 @@ const Chat: React.FC = () => { setInputValue(''); setLoading(true); - // Simulate AI response - setTimeout(() => { + try { + // check api key + if (!openAIService.isConfigured()) { + throw new Error(t('chat.error.noApiKey')); + } + + // api messages + const apiMessages = messages.map((msg) => ({ + role: msg.role as 'user' | 'assistant', + content: msg.content, + })); + + // add current user message + apiMessages.push({ + role: 'user', + content: userMessage.content, + }); + + // call ChatGPT API + const response = await openAIService.chatCompletion(apiMessages, { + model: 'gpt-3.5-turbo', + maxTokens: 1000, + temperature: 0.7, + }); + const assistantMessage: Message = { id: (Date.now() + 1).toString(), - content: `I received your message: "${userMessage.content}". This is a simulated response. In a real application, this would call an AI API.`, + content: response, role: 'assistant', timestamp: new Date(), }; addMessage(assistantMessage); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Chat API Error:', error); + + let errorMessage = t('chat.error.unknown'); + if (error instanceof Error) { + if (error.message.includes('API key')) { + errorMessage = t('chat.error.noApiKey'); + } else if (error.message.includes('fetch') || error.message.includes('network')) { + errorMessage = t('chat.error.network'); + } else if (error.message.includes('quota')) { + errorMessage = 'API 配额已用完,已切换到模拟模式。请检查你的 OpenAI 账户余额。'; + } else { + errorMessage = error.message; + } + } + + // add error message to chat + const errorResponse: Message = { + id: (Date.now() + 1).toString(), + content: `❌ ${errorMessage}`, + role: 'assistant', + timestamp: new Date(), + }; + addMessage(errorResponse); + } finally { setLoading(false); - }, 1000); + } }; const handleKeyPress = (e: React.KeyboardEvent) => { @@ -104,13 +171,16 @@ const Chat: React.FC = () => { }; const handleNewChat = () => { - // 由Sidebar控制新建会话 + // new chat by sidebar setInputValue(''); setTimeout(() => { inputRef.current?.focus(); }, 100); }; + // check API configuration status + const apiConfigStatus = openAIService.getConfigurationStatus(); + return (
{!hasStartedChat ? ( @@ -123,6 +193,14 @@ const Chat: React.FC = () => {

{t('chat.welcome.title')}

{t('chat.welcome.subtitle')}

+ + {/* API configuration status hint */} + {!apiConfigStatus.configured && ( +
+ + {apiConfigStatus.message} +
+ )}
diff --git a/src/services/openaiService.ts b/src/services/openaiService.ts new file mode 100644 index 0000000..247f26f --- /dev/null +++ b/src/services/openaiService.ts @@ -0,0 +1,199 @@ +// Declare global variable types +declare global { + var process: { + env: { + REACT_APP_OPENAI_API_KEY?: string; + }; + }; +} + +interface ChatMessage { + role: 'user' | 'assistant' | 'system'; + content: string; +} + +interface ChatCompletionRequest { + model: string; + messages: ChatMessage[]; + max_tokens?: number; + temperature?: number; + stream?: boolean; +} + +interface ChatCompletionResponse { + id: string; + object: string; + created: number; + model: string; + choices: Array<{ + index: number; + message: { + role: string; + content: string; + }; + finish_reason: string; + }>; + usage: { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; + }; +} + +class OpenAIService { + private apiKey: string; + private baseURL: string; + private useMockMode: boolean = false; + + constructor() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this.apiKey = (process as any).env.REACT_APP_OPENAI_API_KEY || ''; + this.baseURL = 'https://api.openai.com/v1'; + } + + private async makeRequest(endpoint: string, options: RequestInit = {}): Promise { + const url = `${this.baseURL}${endpoint}`; + + const defaultHeaders = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.apiKey}`, + }; + + const response = await fetch(url, { + ...options, + headers: { + ...defaultHeaders, + ...options.headers, + }, + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error( + errorData.error?.message || + `API request failed with status ${response.status}: ${response.statusText}`, + ); + } + + return response; + } + + private generateMockResponse(userMessage: string): string { + const mockResponses = [ + `I understand your question: "${userMessage}". This is a great question! Let me provide you with some suggestions...`, + `Regarding "${userMessage}", I can share some insights. First, we need to consider several key factors...`, + `Very interesting question! "${userMessage}" involves multiple aspects. Let me explain in detail...`, + `For the question "${userMessage}", I suggest thinking from the following angles...`, + `This is a very deep question. "${userMessage}" is indeed worth exploring in depth...`, + ]; + + const randomIndex = Math.floor(Math.random() * mockResponses.length); + return mockResponses[randomIndex]; + } + + async chatCompletion( + messages: ChatMessage[], + options: { + model?: string; + maxTokens?: number; + temperature?: number; + } = {}, + ): Promise { + const { model = 'gpt-3.5-turbo', maxTokens = 1000, temperature = 0.7 } = options; + + // If mock mode is enabled, return mock response + if (this.useMockMode) { + const lastUserMessage = messages[messages.length - 1]; + if (lastUserMessage && lastUserMessage.role === 'user') { + return this.generateMockResponse(lastUserMessage.content); + } + return 'This is a mock response. Please check your OpenAI API quota.'; + } + + const requestBody: ChatCompletionRequest = { + model, + messages, + max_tokens: maxTokens, + temperature, + }; + + try { + const response = await this.makeRequest('/chat/completions', { + method: 'POST', + body: JSON.stringify(requestBody), + }); + + const data: ChatCompletionResponse = await response.json(); + + if (data.choices && data.choices.length > 0) { + return data.choices[0].message.content; + } else { + throw new Error('No response content received from API'); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error('OpenAI API Error:', error); + + // If it's a quota error, enable mock mode + if (error instanceof Error && error.message.includes('quota')) { + this.useMockMode = true; + const lastUserMessage = messages[messages.length - 1]; + if (lastUserMessage && lastUserMessage.role === 'user') { + return this.generateMockResponse(lastUserMessage.content); + } + return 'API quota exceeded, switched to mock mode. Please check your OpenAI account balance.'; + } + + throw error; + } + } + + // Check if API key is configured + isConfigured(): boolean { + return !!this.apiKey; + } + + // Get configuration status information + getConfigurationStatus(): { configured: boolean; message: string } { + if (!this.apiKey) { + return { + configured: false, + message: + 'OpenAI API key not configured. Please set REACT_APP_OPENAI_API_KEY in your environment variables.', + }; + } + + if (this.useMockMode) { + return { + configured: true, + message: 'API quota exceeded, using mock mode. Please check your OpenAI account balance.', + }; + } + + return { + configured: true, + message: 'OpenAI API is configured and ready to use.', + }; + } + + // Check if in mock mode + isMockMode(): boolean { + return this.useMockMode; + } + + // Manually enable mock mode + enableMockMode(): void { + this.useMockMode = true; + } + + // Manually disable mock mode + disableMockMode(): void { + this.useMockMode = false; + } +} + +// Create singleton instance +const openAIService = new OpenAIService(); + +export default openAIService; +export type { ChatMessage, ChatCompletionRequest, ChatCompletionResponse };