From 9f1529d07acad826343c8c12167f76f3df695e1e Mon Sep 17 00:00:00 2001
From: Jing Hua
Date: Sun, 12 Mar 2023 19:05:26 +0800
Subject: [PATCH] update api menu and readme
---
README-zh_CN.md | 5 +-
README.md | 3 +-
public/locales/en/api.json | 9 +-
public/locales/en/main.json | 3 +-
public/locales/zh-CN/api.json | 7 +-
public/locales/zh-CN/main.json | 3 +-
src/api/api.ts | 67 ++++++
src/api/freeApi.ts | 2 +-
src/components/ApiMenu/ApiMenu.tsx | 198 +++++++++++-------
.../Chat/ChatContent/DownloadChat.tsx | 2 +-
src/components/Menu/MenuOptions/Api.tsx | 2 +-
src/constants/auth.ts | 7 +
src/hooks/useSubmit.ts | 33 ++-
src/i18n.ts | 14 +-
src/store/auth-slice.ts | 11 +-
src/store/migrate.ts | 16 +-
src/store/store.ts | 15 +-
src/types/chat.ts | 12 +-
18 files changed, 281 insertions(+), 128 deletions(-)
create mode 100644 src/api/api.ts
create mode 100644 src/constants/auth.ts
diff --git a/README-zh_CN.md b/README-zh_CN.md
index e0f74f7..fd3be47 100644
--- a/README-zh_CN.md
+++ b/README-zh_CN.md
@@ -22,8 +22,7 @@
-
+
@@ -75,7 +74,7 @@
- 💾 所有聊天记录都会自动备份到您的浏览器本地存储器中
- 📥 轻松导入和导出聊天数据 JSON 文件。
-- 📥 下载您的整个聊天记录,以 markdown,pdf 或图像的形式。
+- 📥 下载您的整个聊天记录,以 markdown,pdf 或图片的形式。
### UI / UX
diff --git a/README.md b/README.md
index d4add12..b9d5453 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,7 @@
ChatGPT Free App
+ English Version |
简体中文版
@@ -28,7 +29,7 @@
-
+
diff --git a/public/locales/en/api.json b/public/locales/en/api.json
index 778757c..a39d281 100644
--- a/public/locales/en/api.json
+++ b/public/locales/en/api.json
@@ -1,13 +1,14 @@
{
"securityMessage": "We prioritise the security of your API key and handle it with utmost care. Your key is exclusively stored on your browser and never shared with any third-party entity. It is solely used for the intended purpose of accessing the OpenAI API and not for any other unauthorised use.",
"apiEndpoint": {
- "option": "Use free API endpoint",
- "inputLabel": "Free API Endpoint",
- "description": "Use free API endpoint from <0>Ayaka0>: https://chatgpt-api.shn.hk/v1/ or enter your own API endpoint"
+ "option": "Use for free",
+ "inputLabel": "API Endpoint",
+ "description": "Thank you to <0>Ayaka0> for providing the free API endpoint: https://chatgpt-api.shn.hk/v1/"
},
"apiKey": {
"option": "Use your own API key",
"howTo": "Get your personal API key <0>here0>",
"inputLabel": "API Key"
- }
+ },
+ "customEndpoint": "Use custom endpoint"
}
diff --git a/public/locales/en/main.json b/public/locales/en/main.json
index f24f1c9..ad8ddd0 100644
--- a/public/locales/en/main.json
+++ b/public/locales/en/main.json
@@ -21,5 +21,6 @@
"newChat": "New Chat",
"lightMode": "Light Mode",
"darkMode": "Dark Mode",
- "setting": "Settings"
+ "setting": "Settings",
+ "image": "Image"
}
diff --git a/public/locales/zh-CN/api.json b/public/locales/zh-CN/api.json
index ade8390..795fb28 100644
--- a/public/locales/zh-CN/api.json
+++ b/public/locales/zh-CN/api.json
@@ -2,12 +2,13 @@
"securityMessage": "我们高度优先考虑您的 API 密钥的安全,并非常小心地处理它。您的密钥将专门存储在您的浏览器中,并且永远不会与任何第三方实体共享。它仅用于访问 OpenAI API 的预期用途,而不是用于任何其他未经授权的用途。",
"apiEndpoint": {
"option": "使用免费的 API 端点",
- "inputLabel": "免费的 API 端点",
- "description": "使用 <0>Ayaka0> 提供的免费 API 端点:https://chatgpt-api.shn.hk/v1/,或输入您自己的 API 端点"
+ "inputLabel": "API 端点",
+ "description": "感谢 <0>Ayaka0> 提供免费的 API 终端: https://chatgpt-api.shn.hk/v1/。"
},
"apiKey": {
"option": "使用自己的 API 密钥",
"howTo": "在<0>这里0>获取您的个人 API 密钥",
"inputLabel": "API 密钥"
- }
+ },
+ "customEndpoint": "使用自定义端点"
}
diff --git a/public/locales/zh-CN/main.json b/public/locales/zh-CN/main.json
index 8a4485c..a579ad9 100644
--- a/public/locales/zh-CN/main.json
+++ b/public/locales/zh-CN/main.json
@@ -21,5 +21,6 @@
"newChat": "新聊天",
"lightMode": "亮色模式",
"darkMode": "黑暗模式",
- "setting": "设置"
+ "setting": "设置",
+ "image": "图片"
}
diff --git a/src/api/api.ts b/src/api/api.ts
new file mode 100644
index 0000000..43322e4
--- /dev/null
+++ b/src/api/api.ts
@@ -0,0 +1,67 @@
+import { ConfigInterface, MessageInterface } from '@type/chat';
+
+export const getChatCompletion = async (
+ endpoint: string,
+ messages: MessageInterface[],
+ config: ConfigInterface,
+ apiKey?: string
+) => {
+ const headers: HeadersInit = {
+ 'Content-Type': 'application/json',
+ };
+ if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
+
+ const response = await fetch(endpoint, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify({
+ model: 'gpt-3.5-turbo',
+ messages,
+ ...config,
+ }),
+ });
+ if (!response.ok) throw new Error(await response.text());
+
+ const data = await response.json();
+ return data;
+};
+
+export const getChatCompletionStream = async (
+ endpoint: string,
+ messages: MessageInterface[],
+ config: ConfigInterface,
+ apiKey?: string
+) => {
+ const headers: HeadersInit = {
+ 'Content-Type': 'application/json',
+ };
+ if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
+
+ const response = await fetch(endpoint, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify({
+ model: 'gpt-3.5-turbo',
+ messages,
+ ...config,
+ stream: true,
+ }),
+ });
+ if (response.status === 404 || response.status === 405)
+ throw new Error(
+ 'Message from freechatgpt.chat:\nInvalid API endpoint! We recommend you to check your free API endpoint.'
+ );
+
+ if (response.status === 429 || !response.ok) {
+ const text = await response.text();
+ let error = text;
+ if (text.includes('insufficient_quota')) {
+ error +=
+ '\nMessage from freechatgpt.chat:\nToo many request! We recommend changing your API endpoint or API key';
+ }
+ throw new Error(error);
+ }
+
+ const stream = response.body;
+ return stream;
+};
diff --git a/src/api/freeApi.ts b/src/api/freeApi.ts
index 742fd72..25e6e7a 100644
--- a/src/api/freeApi.ts
+++ b/src/api/freeApi.ts
@@ -49,7 +49,7 @@ export const getChatCompletionStream = async (
let error = text;
if (text.includes('insufficient_quota')) {
error +=
- '\nMessage from freechatgpt.chat:\nWe recommend changing your API endpoint or API key';
+ '\nMessage from freechatgpt.chat:\nToo many request! We recommend changing your API endpoint or API key';
}
throw new Error(error);
}
diff --git a/src/components/ApiMenu/ApiMenu.tsx b/src/components/ApiMenu/ApiMenu.tsx
index df0539f..ba31a14 100644
--- a/src/components/ApiMenu/ApiMenu.tsx
+++ b/src/components/ApiMenu/ApiMenu.tsx
@@ -3,12 +3,12 @@ import { useTranslation, Trans } from 'react-i18next';
import useStore from '@store/store';
import PopupModal from '@components/PopupModal';
+import { availableEndpoints, defaultAPIEndpoint } from '@constants/auth';
+import DownChevronArrow from '@icon/DownChevronArrow';
const ApiMenu = ({
- isModalOpen,
setIsModalOpen,
}: {
- isModalOpen: boolean;
setIsModalOpen: React.Dispatch>;
}) => {
const { t } = useTranslation(['main', 'api']);
@@ -17,108 +17,92 @@ const ApiMenu = ({
const setApiKey = useStore((state) => state.setApiKey);
const apiFree = useStore((state) => state.apiFree);
const setApiFree = useStore((state) => state.setApiFree);
- const apiFreeEndpoint = useStore((state) => state.apiFreeEndpoint);
- const setApiFreeEndpoint = useStore((state) => state.setApiFreeEndpoint);
+ const apiEndpoint = useStore((state) => state.apiEndpoint);
+ const setApiEndpoint = useStore((state) => state.setApiEndpoint);
const [_apiFree, _setApiFree] = useState(apiFree);
const [_apiKey, _setApiKey] = useState(apiKey || '');
- const [_apiFreeEndpoint, _setApiFreeEndpoint] =
- useState(apiFreeEndpoint);
+ const [_apiEndpoint, _setApiEndpoint] = useState(apiEndpoint);
+ const [_customEndpoint, _setCustomEndpoint] = useState(
+ !availableEndpoints.includes(apiEndpoint)
+ );
- const handleSave = async () => {
- if (_apiFree === true) {
- setApiFreeEndpoint(_apiFreeEndpoint);
- setApiFree(true);
- setIsModalOpen(false);
- } else {
- setApiKey(_apiKey);
- setApiFree(false);
- setIsModalOpen(false);
- }
+ const handleSave = () => {
+ setApiFree(_apiFree);
+ setApiKey(_apiKey);
+ setApiEndpoint(_apiEndpoint);
+ setIsModalOpen(false);
};
- useEffect(() => {
- if (apiKey) {
- setApiFree(false);
- _setApiFree(false);
- _setApiKey(apiKey);
- }
- }, []);
-
- const handleClose = () => {
- _setApiFree(apiFree);
- _setApiFreeEndpoint(apiFreeEndpoint);
- apiKey && _setApiKey(apiKey);
+ const handleToggleCustomEndpoint = () => {
+ if (_customEndpoint) _setApiEndpoint(defaultAPIEndpoint);
+ else _setApiEndpoint('');
+ _setCustomEndpoint((prev) => !prev);
};
- return isModalOpen ? (
+ return (
-
+ {t('apiEndpoint.option', { ns: 'api' })}
+
- {_apiFree && (
-
-
- ,
- ]}
- />
-
-
-
- {t('apiEndpoint.inputLabel', { ns: 'api' })}
-
-
{
- _setApiFreeEndpoint(e.target.value);
- }}
- />
-
-
- )}
-
-
+
+ {t('apiKey.option', { ns: 'api' })}
+
{_apiFree === false && (
+
{t('securityMessage', { ns: 'api' })}
+
+
+ ,
+ ]}
+ />
+
- ) : (
- <>>
+ );
+};
+
+const ApiEndpointSelector = ({
+ _apiEndpoint,
+ _setApiEndpoint,
+}: {
+ _apiEndpoint: string;
+ _setApiEndpoint: React.Dispatch>;
+}) => {
+ const [dropDown, setDropDown] = useState(false);
+
+ return (
+
+
+
+
+ {availableEndpoints.map((endpoint) => (
+ - {
+ _setApiEndpoint(endpoint);
+ setDropDown(false);
+ }}
+ key={endpoint}
+ >
+ {endpoint}
+
+ ))}
+
+
+
);
};
diff --git a/src/components/Chat/ChatContent/DownloadChat.tsx b/src/components/Chat/ChatContent/DownloadChat.tsx
index cda4829..1591e41 100644
--- a/src/components/Chat/ChatContent/DownloadChat.tsx
+++ b/src/components/Chat/ChatContent/DownloadChat.tsx
@@ -30,7 +30,7 @@ const DownloadChat = React.memo(
{isModalOpen && (
diff --git a/src/components/Menu/MenuOptions/Api.tsx b/src/components/Menu/MenuOptions/Api.tsx
index 0e5772b..e3f4d0c 100644
--- a/src/components/Menu/MenuOptions/Api.tsx
+++ b/src/components/Menu/MenuOptions/Api.tsx
@@ -19,7 +19,7 @@ const Config = () => {
{t('api')}: {apiFree ? t('free') : t('personal')}
-
+ {isModalOpen &&
}
>
);
};
diff --git a/src/constants/auth.ts b/src/constants/auth.ts
new file mode 100644
index 0000000..4c719f5
--- /dev/null
+++ b/src/constants/auth.ts
@@ -0,0 +1,7 @@
+export const defaultAPIEndpoint = 'https://chatgpt-api.shn.hk/v1/';
+export const officialAPIEndpoint = 'https://api.openai.com/v1/chat/completions';
+
+export const availableEndpoints = [
+ 'https://chatgpt-api.shn.hk/v1/',
+ officialAPIEndpoint,
+];
diff --git a/src/hooks/useSubmit.ts b/src/hooks/useSubmit.ts
index 3412e9f..b4c2ab0 100644
--- a/src/hooks/useSubmit.ts
+++ b/src/hooks/useSubmit.ts
@@ -1,14 +1,7 @@
import React from 'react';
import useStore from '@store/store';
import { ChatInterface, MessageInterface } from '@type/chat';
-import {
- getChatCompletionStream as getChatCompletionStreamFree,
- getChatCompletion as getChatCompletionFree,
-} from '@api/freeApi';
-import {
- getChatCompletionStream as getChatCompletionStreamCustom,
- getChatCompletion as getChatCompletionCustom,
-} from '@api/customApi';
+import { getChatCompletion, getChatCompletionStream } from '@api/api';
import { parseEventSource } from '@api/helper';
import { limitMessageTokens } from '@utils/messageUtils';
import { defaultChatConfig } from '@constants/chat';
@@ -28,13 +21,18 @@ const useSubmit = () => {
): Promise
=> {
let data;
if (apiFree) {
- data = await getChatCompletionFree(
- useStore.getState().apiFreeEndpoint,
+ data = await getChatCompletion(
+ useStore.getState().apiEndpoint,
message,
defaultChatConfig
);
} else if (apiKey) {
- data = await getChatCompletionCustom(apiKey, message, defaultChatConfig);
+ data = await getChatCompletion(
+ useStore.getState().apiEndpoint,
+ message,
+ defaultChatConfig,
+ apiKey
+ );
}
return data.choices[0].message.content;
};
@@ -62,16 +60,17 @@ const useSubmit = () => {
if (messages.length === 0) throw new Error('Message exceed max token!');
if (apiFree) {
- stream = await getChatCompletionStreamFree(
- useStore.getState().apiFreeEndpoint,
+ stream = await getChatCompletionStream(
+ useStore.getState().apiEndpoint,
messages,
chats[currentChatIndex].config
);
} else if (apiKey) {
- stream = await getChatCompletionStreamCustom(
- apiKey,
+ stream = await getChatCompletionStream(
+ useStore.getState().apiEndpoint,
messages,
- chats[currentChatIndex].config
+ chats[currentChatIndex].config,
+ apiKey
);
} else {
throw new Error('No API key supplied! Please check your API settings.');
@@ -132,7 +131,7 @@ const useSubmit = () => {
content: `Generate a title in less than 6 words for the following message:\nUser: ${user_message}\nAssistant: ${assistant_message}`,
};
- let title = await generateTitle([message]);
+ let title = (await generateTitle([message])).trim();
if (title.startsWith('"') && title.endsWith('"')) {
title = title.slice(1, -1);
}
diff --git a/src/i18n.ts b/src/i18n.ts
index 0b1afeb..4b3f1bc 100644
--- a/src/i18n.ts
+++ b/src/i18n.ts
@@ -6,10 +6,14 @@ import LanguageDetector from 'i18next-browser-languagedetector';
export const i18nLanguages = ['en', 'zh-CN'];
-i18n.use(Backend).use(LanguageDetector).use(initReactI18next).init({
- fallbackLng: 'en',
- ns: 'main',
- defaultNS: 'main',
-});
+i18n
+ .use(Backend)
+ .use(LanguageDetector)
+ .use(initReactI18next)
+ .init({
+ fallbackLng: 'en',
+ ns: ['main', 'api', 'about', 'model'],
+ defaultNS: 'main',
+ });
export default i18n;
diff --git a/src/store/auth-slice.ts b/src/store/auth-slice.ts
index e99f841..675aa73 100644
--- a/src/store/auth-slice.ts
+++ b/src/store/auth-slice.ts
@@ -1,17 +1,18 @@
+import { defaultAPIEndpoint } from '@constants/auth';
import { StoreSlice } from './store';
export interface AuthSlice {
apiKey?: string;
apiFree: boolean;
- apiFreeEndpoint: string;
+ apiEndpoint: string;
setApiKey: (apiKey: string) => void;
setApiFree: (apiFree: boolean) => void;
- setApiFreeEndpoint: (apiFreeEndpoint: string) => void;
+ setApiEndpoint: (apiEndpoint: string) => void;
}
export const createAuthSlice: StoreSlice = (set, get) => ({
apiFree: true,
- apiFreeEndpoint: 'https://chatgpt-api.shn.hk/v1/',
+ apiEndpoint: defaultAPIEndpoint,
setApiKey: (apiKey: string) => {
set((prev: AuthSlice) => ({
...prev,
@@ -24,10 +25,10 @@ export const createAuthSlice: StoreSlice = (set, get) => ({
apiFree: apiFree,
}));
},
- setApiFreeEndpoint: (apiFreeEndpoint: string) => {
+ setApiEndpoint: (apiEndpoint: string) => {
set((prev: AuthSlice) => ({
...prev,
- apiFreeEndpoint: apiFreeEndpoint,
+ apiEndpoint: apiEndpoint,
}));
},
});
diff --git a/src/store/migrate.ts b/src/store/migrate.ts
index 3e53c2e..3b6dec4 100644
--- a/src/store/migrate.ts
+++ b/src/store/migrate.ts
@@ -1,9 +1,21 @@
-import { LocalStorageInterface } from '@type/chat';
+import {
+ LocalStorageInterfaceV0ToV1,
+ LocalStorageInterfaceV1ToV2,
+} from '@type/chat';
import { defaultChatConfig } from '@constants/chat';
+import { defaultAPIEndpoint, officialAPIEndpoint } from '@constants/auth';
-export const migrateV0 = (persistedState: LocalStorageInterface) => {
+export const migrateV0 = (persistedState: LocalStorageInterfaceV0ToV1) => {
persistedState.chats.forEach((chat) => {
chat.titleSet = false;
if (!chat.config) chat.config = { ...defaultChatConfig };
});
};
+
+export const migrateV1 = (persistedState: LocalStorageInterfaceV1ToV2) => {
+ if (persistedState.apiFree) {
+ persistedState.apiEndpoint = persistedState.apiFreeEndpoint;
+ } else {
+ persistedState.apiEndpoint = officialAPIEndpoint;
+ }
+};
diff --git a/src/store/store.ts b/src/store/store.ts
index 405f3c2..a70ba9a 100644
--- a/src/store/store.ts
+++ b/src/store/store.ts
@@ -4,8 +4,11 @@ import { ChatSlice, createChatSlice } from './chat-slice';
import { InputSlice, createInputSlice } from './input-slice';
import { AuthSlice, createAuthSlice } from './auth-slice';
import { ConfigSlice, createConfigSlice } from './config-slice';
-import { LocalStorageInterface } from '@type/chat';
-import { migrateV0 } from './migrate';
+import {
+ LocalStorageInterfaceV0ToV1,
+ LocalStorageInterfaceV1ToV2,
+} from '@type/chat';
+import { migrateV0, migrateV1 } from './migrate';
export type StoreState = ChatSlice & InputSlice & AuthSlice & ConfigSlice;
@@ -29,14 +32,16 @@ const useStore = create()(
currentChatIndex: state.currentChatIndex,
apiKey: state.apiKey,
apiFree: state.apiFree,
- apiFreeEndpoint: state.apiFreeEndpoint,
+ apiEndpoint: state.apiEndpoint,
theme: state.theme,
}),
- version: 1,
+ version: 2,
migrate: (persistedState, version) => {
switch (version) {
case 0:
- migrateV0(persistedState as LocalStorageInterface);
+ migrateV0(persistedState as LocalStorageInterfaceV0ToV1);
+ case 1:
+ migrateV1(persistedState as LocalStorageInterfaceV1ToV2);
break;
}
return persistedState as StoreState;
diff --git a/src/types/chat.ts b/src/types/chat.ts
index 118406c..912c2ce 100644
--- a/src/types/chat.ts
+++ b/src/types/chat.ts
@@ -20,7 +20,7 @@ export interface ConfigInterface {
presence_penalty: number;
}
-export interface LocalStorageInterface {
+export interface LocalStorageInterfaceV0ToV1 {
chats: ChatInterface[];
currentChatIndex: number;
apiKey: string;
@@ -28,3 +28,13 @@ export interface LocalStorageInterface {
apiFreeEndpoint: string;
theme: Theme;
}
+
+export interface LocalStorageInterfaceV1ToV2 {
+ chats: ChatInterface[];
+ currentChatIndex: number;
+ apiKey: string;
+ apiFree: boolean;
+ apiFreeEndpoint: string;
+ apiEndpoint?: string;
+ theme: Theme;
+}