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 };