update api menu and readme

This commit is contained in:
Jing Hua 2023-03-12 19:05:26 +08:00
parent a5cd36c1d1
commit 9f1529d07a
18 changed files with 281 additions and 128 deletions

View file

@ -22,8 +22,7 @@
<a href="https://github.com/ztjhz/ChatGPTFreeApp/pulls" target="blank"> <a href="https://github.com/ztjhz/ChatGPTFreeApp/pulls" target="blank">
<img src="https://img.shields.io/github/issues-pr/ztjhz/ChatGPTFreeApp?style=flat-square" alt="pull-requests"/> <img src="https://img.shields.io/github/issues-pr/ztjhz/ChatGPTFreeApp?style=flat-square" alt="pull-requests"/>
</a> </a>
<a href="https://twitter.com/intent/tweet?text=👋请检查这个惊人的存储库 https://github.com/ztjhz/ChatGPTFreeApp由@nikushii_创建。"><img src="https://img.shields.io/twitter/url?label=分享到推特 <a href="https://twitter.com/intent/tweet?text=👋请检查这个惊人的存储库 https://github.com/ztjhz/ChatGPTFreeApp由@nikushii_创建。"><img src="https://img.shields.io/twitter/url?label=%E5%88%86%E4%BA%AB%E5%88%B0%E6%8E%A8%E7%89%B9&style=social&url=https%3A%2F%2Fgithub.com%2Fztjhz%2FChatGPTFreeApp"></a>
&style=social&url=https%3A%2F%2Fgithub.com%ztjhz%2FChatGPTFreeApp"></a>
</p> </p>
<p align="center"> <p align="center">
@ -75,7 +74,7 @@
- 💾 所有聊天记录都会自动备份到您的浏览器本地存储器中 - 💾 所有聊天记录都会自动备份到您的浏览器本地存储器中
- 📥 轻松导入和导出聊天数据 JSON 文件。 - 📥 轻松导入和导出聊天数据 JSON 文件。
- 📥 下载您的整个聊天记录,以 markdownpdf 或图的形式。 - 📥 下载您的整个聊天记录,以 markdownpdf 或图的形式。
### UI / UX ### UI / UX

View file

@ -1,6 +1,7 @@
<h1 align="center"><b>ChatGPT Free App</b></h1> <h1 align="center"><b>ChatGPT Free App</b></h1>
<p align="center"> <p align="center">
English Version |
<a href="README-zh_CN.md"> <a href="README-zh_CN.md">
简体中文版 简体中文版
</a> </a>
@ -28,7 +29,7 @@
<a href="https://github.com/ztjhz/ChatGPTFreeApp/pulls" target="blank"> <a href="https://github.com/ztjhz/ChatGPTFreeApp/pulls" target="blank">
<img src="https://img.shields.io/github/issues-pr/ztjhz/ChatGPTFreeApp?style=flat-square" alt="pull-requests"/> <img src="https://img.shields.io/github/issues-pr/ztjhz/ChatGPTFreeApp?style=flat-square" alt="pull-requests"/>
</a> </a>
<a href="https://twitter.com/intent/tweet?text=👋%20Check%20this%20amazing%20repo%20https://github.com/ztjhz/ChatGPTFreeApp,%20created%20by%20@nikushii_"><img src="https://img.shields.io/twitter/url?label=Share%20on%20Twitter&style=social&url=https%3A%2F%2Fgithub.com%ztjhz%2FChatGPTFreeApp"></a> <a href="https://twitter.com/intent/tweet?text=👋%20Check%20this%20amazing%20repo%20https://github.com/ztjhz/ChatGPTFreeApp,%20created%20by%20@nikushii_"><img src="https://img.shields.io/twitter/url?label=Share%20on%20Twitter&style=social&url=https%3A%2F%2Fgithub.com%2Fztjhz%2FChatGPTFreeApp"></a>
</p> </p>
<p align="center"> <p align="center">

View file

@ -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.", "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": { "apiEndpoint": {
"option": "Use free API endpoint", "option": "Use for free",
"inputLabel": "Free API Endpoint", "inputLabel": "API Endpoint",
"description": "Use free API endpoint from <0>Ayaka</0>: https://chatgpt-api.shn.hk/v1/ or enter your own API endpoint" "description": "Thank you to <0>Ayaka</0> for providing the free API endpoint: https://chatgpt-api.shn.hk/v1/"
}, },
"apiKey": { "apiKey": {
"option": "Use your own API key", "option": "Use your own API key",
"howTo": "Get your personal API key <0>here</0>", "howTo": "Get your personal API key <0>here</0>",
"inputLabel": "API Key" "inputLabel": "API Key"
} },
"customEndpoint": "Use custom endpoint"
} }

View file

@ -21,5 +21,6 @@
"newChat": "New Chat", "newChat": "New Chat",
"lightMode": "Light Mode", "lightMode": "Light Mode",
"darkMode": "Dark Mode", "darkMode": "Dark Mode",
"setting": "Settings" "setting": "Settings",
"image": "Image"
} }

View file

@ -2,12 +2,13 @@
"securityMessage": "我们高度优先考虑您的 API 密钥的安全,并非常小心地处理它。您的密钥将专门存储在您的浏览器中,并且永远不会与任何第三方实体共享。它仅用于访问 OpenAI API 的预期用途,而不是用于任何其他未经授权的用途。", "securityMessage": "我们高度优先考虑您的 API 密钥的安全,并非常小心地处理它。您的密钥将专门存储在您的浏览器中,并且永远不会与任何第三方实体共享。它仅用于访问 OpenAI API 的预期用途,而不是用于任何其他未经授权的用途。",
"apiEndpoint": { "apiEndpoint": {
"option": "使用免费的 API 端点", "option": "使用免费的 API 端点",
"inputLabel": "免费的 API 端点", "inputLabel": "API 端点",
"description": "使用 <0>Ayaka</0> 提供的免费 API 端点https://chatgpt-api.shn.hk/v1/,或输入您自己的 API 端点" "description": "感谢 <0>Ayaka</0> 提供免费的 API 终端: https://chatgpt-api.shn.hk/v1/。"
}, },
"apiKey": { "apiKey": {
"option": "使用自己的 API 密钥", "option": "使用自己的 API 密钥",
"howTo": "在<0>这里</0>获取您的个人 API 密钥", "howTo": "在<0>这里</0>获取您的个人 API 密钥",
"inputLabel": "API 密钥" "inputLabel": "API 密钥"
} },
"customEndpoint": "使用自定义端点"
} }

View file

@ -21,5 +21,6 @@
"newChat": "新聊天", "newChat": "新聊天",
"lightMode": "亮色模式", "lightMode": "亮色模式",
"darkMode": "黑暗模式", "darkMode": "黑暗模式",
"setting": "设置" "setting": "设置",
"image": "图片"
} }

67
src/api/api.ts Normal file
View file

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

View file

@ -49,7 +49,7 @@ export const getChatCompletionStream = async (
let error = text; let error = text;
if (text.includes('insufficient_quota')) { if (text.includes('insufficient_quota')) {
error += 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); throw new Error(error);
} }

View file

@ -3,12 +3,12 @@ import { useTranslation, Trans } from 'react-i18next';
import useStore from '@store/store'; import useStore from '@store/store';
import PopupModal from '@components/PopupModal'; import PopupModal from '@components/PopupModal';
import { availableEndpoints, defaultAPIEndpoint } from '@constants/auth';
import DownChevronArrow from '@icon/DownChevronArrow';
const ApiMenu = ({ const ApiMenu = ({
isModalOpen,
setIsModalOpen, setIsModalOpen,
}: { }: {
isModalOpen: boolean;
setIsModalOpen: React.Dispatch<React.SetStateAction<boolean>>; setIsModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
}) => { }) => {
const { t } = useTranslation(['main', 'api']); const { t } = useTranslation(['main', 'api']);
@ -17,108 +17,92 @@ const ApiMenu = ({
const setApiKey = useStore((state) => state.setApiKey); const setApiKey = useStore((state) => state.setApiKey);
const apiFree = useStore((state) => state.apiFree); const apiFree = useStore((state) => state.apiFree);
const setApiFree = useStore((state) => state.setApiFree); const setApiFree = useStore((state) => state.setApiFree);
const apiFreeEndpoint = useStore((state) => state.apiFreeEndpoint); const apiEndpoint = useStore((state) => state.apiEndpoint);
const setApiFreeEndpoint = useStore((state) => state.setApiFreeEndpoint); const setApiEndpoint = useStore((state) => state.setApiEndpoint);
const [_apiFree, _setApiFree] = useState<boolean>(apiFree); const [_apiFree, _setApiFree] = useState<boolean>(apiFree);
const [_apiKey, _setApiKey] = useState<string>(apiKey || ''); const [_apiKey, _setApiKey] = useState<string>(apiKey || '');
const [_apiFreeEndpoint, _setApiFreeEndpoint] = const [_apiEndpoint, _setApiEndpoint] = useState<string>(apiEndpoint);
useState<string>(apiFreeEndpoint); const [_customEndpoint, _setCustomEndpoint] = useState<boolean>(
!availableEndpoints.includes(apiEndpoint)
);
const handleSave = async () => { const handleSave = () => {
if (_apiFree === true) { setApiFree(_apiFree);
setApiFreeEndpoint(_apiFreeEndpoint); setApiKey(_apiKey);
setApiFree(true); setApiEndpoint(_apiEndpoint);
setIsModalOpen(false); setIsModalOpen(false);
} else {
setApiKey(_apiKey);
setApiFree(false);
setIsModalOpen(false);
}
}; };
useEffect(() => { const handleToggleCustomEndpoint = () => {
if (apiKey) { if (_customEndpoint) _setApiEndpoint(defaultAPIEndpoint);
setApiFree(false); else _setApiEndpoint('');
_setApiFree(false); _setCustomEndpoint((prev) => !prev);
_setApiKey(apiKey);
}
}, []);
const handleClose = () => {
_setApiFree(apiFree);
_setApiFreeEndpoint(apiFreeEndpoint);
apiKey && _setApiKey(apiKey);
}; };
return isModalOpen ? ( return (
<PopupModal <PopupModal
title={t('api') as string} title={t('api') as string}
setIsModalOpen={setIsModalOpen} setIsModalOpen={setIsModalOpen}
handleConfirm={handleSave} handleConfirm={handleSave}
handleClose={handleClose}
> >
<div className='p-6 border-b border-gray-200 dark:border-gray-600'> <div className='p-6 border-b border-gray-200 dark:border-gray-600'>
<div className='flex items-center mb-2'> <label className='flex gap-2 text-gray-900 dark:text-gray-300 text-sm items-center mb-4'>
<input
type='checkbox'
checked={_customEndpoint}
className='w-4 h-4'
onChange={handleToggleCustomEndpoint}
/>
{t('customEndpoint', { ns: 'api' })}
</label>
<div className='flex gap-2 items-center justify-center mb-6'>
<div className='min-w-fit text-gray-900 dark:text-gray-300 text-sm'>
{t('apiEndpoint.inputLabel', { ns: 'api' })}
</div>
{_customEndpoint ? (
<input
type='text'
className='text-gray-800 dark:text-white p-3 text-sm border-none bg-gray-200 dark:bg-gray-600 rounded-md m-0 w-full mr-0 h-8 focus:outline-none'
value={_apiEndpoint}
placeholder='https://chatgpt-api.shn.hk/v1/'
onChange={(e) => {
_setApiEndpoint(e.target.value);
}}
/>
) : (
<ApiEndpointSelector
_apiEndpoint={_apiEndpoint}
_setApiEndpoint={_setApiEndpoint}
/>
)}
</div>
<label className='flex items-center mb-2 gap-2 text-sm font-medium text-gray-900 dark:text-gray-300'>
<input <input
type='radio' type='radio'
checked={_apiFree === true} checked={_apiFree === true}
className='w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600' className='w-4 h-4'
onChange={() => _setApiFree(true)} onChange={() => _setApiFree(true)}
/> />
<label className='ml-2 text-sm font-medium text-gray-900 dark:text-gray-300'> {t('apiEndpoint.option', { ns: 'api' })}
{t('apiEndpoint.option', { ns: 'api' })} </label>
</label>
</div>
{_apiFree && ( <label className='flex items-center gap-2 text-sm font-medium text-gray-900 dark:text-gray-300'>
<div className='mt-2 mb-6'>
<div className='text-sm font-medium text-gray-900 dark:text-gray-300 mb-2'>
<Trans
i18nKey='apiEndpoint.description'
ns='api'
components={[
<a
href='https://github.com/ayaka14732/ChatGPTAPIFree'
className='link'
target='_blank'
/>,
]}
/>
</div>
<div className='flex gap-2 items-center justify-center'>
<div className='min-w-fit text-gray-900 dark:text-gray-300 text-sm'>
{t('apiEndpoint.inputLabel', { ns: 'api' })}
</div>
<input
type='text'
className='text-gray-800 dark:text-white p-3 text-sm border-none bg-gray-200 dark:bg-gray-600 rounded-md m-0 w-full mr-0 h-8 focus:outline-none'
value={_apiFreeEndpoint}
placeholder='https://chatgpt-api.shn.hk/v1/'
onChange={(e) => {
_setApiFreeEndpoint(e.target.value);
}}
/>
</div>
</div>
)}
<div className='flex items-center'>
<input <input
type='radio' type='radio'
checked={_apiFree === false} checked={_apiFree === false}
className='w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600' className='w-4 h-4'
onChange={() => _setApiFree(false)} onChange={() => _setApiFree(false)}
/> />
<label className='ml-2 text-sm font-medium text-gray-900 dark:text-gray-300'> {t('apiKey.option', { ns: 'api' })}
{t('apiKey.option', { ns: 'api' })} </label>
</label>
</div>
{_apiFree === false && ( {_apiFree === false && (
<div className='flex gap-2 items-center justify-center mt-2'> <div className='flex gap-2 items-center justify-center mt-2'>
<div className='min-w-fit text-gray-900 dark:text-gray-300 text-sm'> <div className='min-w-fit text-gray-900 dark:text-gray-300 text-sm'>
{t('apiEndpoint.inputLabel', { ns: 'api' })} {t('apiKey.inputLabel', { ns: 'api' })}
</div> </div>
<input <input
type='text' type='text'
@ -144,13 +128,73 @@ const ApiMenu = ({
]} ]}
/> />
</div> </div>
<div className='min-w-fit text-gray-900 dark:text-gray-300 text-sm mt-4'> <div className='min-w-fit text-gray-900 dark:text-gray-300 text-sm mt-4'>
{t('securityMessage', { ns: 'api' })} {t('securityMessage', { ns: 'api' })}
</div> </div>
<div className='mt-4 p-1 border border-gray-500 rounded-md text-sm font-medium text-gray-900 dark:text-gray-300 text-center'>
<Trans
i18nKey='apiEndpoint.description'
ns='api'
components={[
<a
href='https://github.com/ayaka14732/ChatGPTAPIFree'
className='link'
target='_blank'
/>,
]}
/>
</div>
</div> </div>
</PopupModal> </PopupModal>
) : ( );
<></> };
const ApiEndpointSelector = ({
_apiEndpoint,
_setApiEndpoint,
}: {
_apiEndpoint: string;
_setApiEndpoint: React.Dispatch<React.SetStateAction<string>>;
}) => {
const [dropDown, setDropDown] = useState<boolean>(false);
return (
<div className='w-full relative'>
<button
className='btn btn-neutral btn-small flex w-32 flex justify-between w-full'
type='button'
onClick={() => setDropDown((prev) => !prev)}
>
{_apiEndpoint}
<DownChevronArrow />
</button>
<div
id='dropdown'
className={`${
dropDown ? '' : 'hidden'
} absolute top-100 bottom-100 z-10 bg-white rounded-lg shadow-xl border-b border-black/10 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group dark:bg-gray-800 opacity-90 w-32 w-full`}
>
<ul
className='text-sm text-gray-700 dark:text-gray-200 p-0 m-0'
aria-labelledby='dropdownDefaultButton'
>
{availableEndpoints.map((endpoint) => (
<li
className='px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white cursor-pointer'
onClick={() => {
_setApiEndpoint(endpoint);
setDropDown(false);
}}
key={endpoint}
>
{endpoint}
</li>
))}
</ul>
</div>
</div>
); );
}; };

View file

@ -30,7 +30,7 @@ const DownloadChat = React.memo(
{isModalOpen && ( {isModalOpen && (
<PopupModal <PopupModal
setIsModalOpen={setIsModalOpen} setIsModalOpen={setIsModalOpen}
title='Download Chat' title={t('downloadChat') as string}
cancelButton={false} cancelButton={false}
> >
<div className='p-6 border-b border-gray-200 dark:border-gray-600 flex gap-4'> <div className='p-6 border-b border-gray-200 dark:border-gray-600 flex gap-4'>

View file

@ -19,7 +19,7 @@ const Config = () => {
<PersonIcon /> <PersonIcon />
{t('api')}: {apiFree ? t('free') : t('personal')} {t('api')}: {apiFree ? t('free') : t('personal')}
</a> </a>
<ApiMenu isModalOpen={isModalOpen} setIsModalOpen={setIsModalOpen} /> {isModalOpen && <ApiMenu setIsModalOpen={setIsModalOpen} />}
</> </>
); );
}; };

7
src/constants/auth.ts Normal file
View file

@ -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,
];

View file

@ -1,14 +1,7 @@
import React from 'react'; import React from 'react';
import useStore from '@store/store'; import useStore from '@store/store';
import { ChatInterface, MessageInterface } from '@type/chat'; import { ChatInterface, MessageInterface } from '@type/chat';
import { import { getChatCompletion, getChatCompletionStream } from '@api/api';
getChatCompletionStream as getChatCompletionStreamFree,
getChatCompletion as getChatCompletionFree,
} from '@api/freeApi';
import {
getChatCompletionStream as getChatCompletionStreamCustom,
getChatCompletion as getChatCompletionCustom,
} from '@api/customApi';
import { parseEventSource } from '@api/helper'; import { parseEventSource } from '@api/helper';
import { limitMessageTokens } from '@utils/messageUtils'; import { limitMessageTokens } from '@utils/messageUtils';
import { defaultChatConfig } from '@constants/chat'; import { defaultChatConfig } from '@constants/chat';
@ -28,13 +21,18 @@ const useSubmit = () => {
): Promise<string> => { ): Promise<string> => {
let data; let data;
if (apiFree) { if (apiFree) {
data = await getChatCompletionFree( data = await getChatCompletion(
useStore.getState().apiFreeEndpoint, useStore.getState().apiEndpoint,
message, message,
defaultChatConfig defaultChatConfig
); );
} else if (apiKey) { } else if (apiKey) {
data = await getChatCompletionCustom(apiKey, message, defaultChatConfig); data = await getChatCompletion(
useStore.getState().apiEndpoint,
message,
defaultChatConfig,
apiKey
);
} }
return data.choices[0].message.content; return data.choices[0].message.content;
}; };
@ -62,16 +60,17 @@ const useSubmit = () => {
if (messages.length === 0) throw new Error('Message exceed max token!'); if (messages.length === 0) throw new Error('Message exceed max token!');
if (apiFree) { if (apiFree) {
stream = await getChatCompletionStreamFree( stream = await getChatCompletionStream(
useStore.getState().apiFreeEndpoint, useStore.getState().apiEndpoint,
messages, messages,
chats[currentChatIndex].config chats[currentChatIndex].config
); );
} else if (apiKey) { } else if (apiKey) {
stream = await getChatCompletionStreamCustom( stream = await getChatCompletionStream(
apiKey, useStore.getState().apiEndpoint,
messages, messages,
chats[currentChatIndex].config chats[currentChatIndex].config,
apiKey
); );
} else { } else {
throw new Error('No API key supplied! Please check your API settings.'); 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}`, 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('"')) { if (title.startsWith('"') && title.endsWith('"')) {
title = title.slice(1, -1); title = title.slice(1, -1);
} }

View file

@ -6,10 +6,14 @@ import LanguageDetector from 'i18next-browser-languagedetector';
export const i18nLanguages = ['en', 'zh-CN']; export const i18nLanguages = ['en', 'zh-CN'];
i18n.use(Backend).use(LanguageDetector).use(initReactI18next).init({ i18n
fallbackLng: 'en', .use(Backend)
ns: 'main', .use(LanguageDetector)
defaultNS: 'main', .use(initReactI18next)
}); .init({
fallbackLng: 'en',
ns: ['main', 'api', 'about', 'model'],
defaultNS: 'main',
});
export default i18n; export default i18n;

View file

@ -1,17 +1,18 @@
import { defaultAPIEndpoint } from '@constants/auth';
import { StoreSlice } from './store'; import { StoreSlice } from './store';
export interface AuthSlice { export interface AuthSlice {
apiKey?: string; apiKey?: string;
apiFree: boolean; apiFree: boolean;
apiFreeEndpoint: string; apiEndpoint: string;
setApiKey: (apiKey: string) => void; setApiKey: (apiKey: string) => void;
setApiFree: (apiFree: boolean) => void; setApiFree: (apiFree: boolean) => void;
setApiFreeEndpoint: (apiFreeEndpoint: string) => void; setApiEndpoint: (apiEndpoint: string) => void;
} }
export const createAuthSlice: StoreSlice<AuthSlice> = (set, get) => ({ export const createAuthSlice: StoreSlice<AuthSlice> = (set, get) => ({
apiFree: true, apiFree: true,
apiFreeEndpoint: 'https://chatgpt-api.shn.hk/v1/', apiEndpoint: defaultAPIEndpoint,
setApiKey: (apiKey: string) => { setApiKey: (apiKey: string) => {
set((prev: AuthSlice) => ({ set((prev: AuthSlice) => ({
...prev, ...prev,
@ -24,10 +25,10 @@ export const createAuthSlice: StoreSlice<AuthSlice> = (set, get) => ({
apiFree: apiFree, apiFree: apiFree,
})); }));
}, },
setApiFreeEndpoint: (apiFreeEndpoint: string) => { setApiEndpoint: (apiEndpoint: string) => {
set((prev: AuthSlice) => ({ set((prev: AuthSlice) => ({
...prev, ...prev,
apiFreeEndpoint: apiFreeEndpoint, apiEndpoint: apiEndpoint,
})); }));
}, },
}); });

View file

@ -1,9 +1,21 @@
import { LocalStorageInterface } from '@type/chat'; import {
LocalStorageInterfaceV0ToV1,
LocalStorageInterfaceV1ToV2,
} from '@type/chat';
import { defaultChatConfig } from '@constants/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) => { persistedState.chats.forEach((chat) => {
chat.titleSet = false; chat.titleSet = false;
if (!chat.config) chat.config = { ...defaultChatConfig }; if (!chat.config) chat.config = { ...defaultChatConfig };
}); });
}; };
export const migrateV1 = (persistedState: LocalStorageInterfaceV1ToV2) => {
if (persistedState.apiFree) {
persistedState.apiEndpoint = persistedState.apiFreeEndpoint;
} else {
persistedState.apiEndpoint = officialAPIEndpoint;
}
};

View file

@ -4,8 +4,11 @@ import { ChatSlice, createChatSlice } from './chat-slice';
import { InputSlice, createInputSlice } from './input-slice'; import { InputSlice, createInputSlice } from './input-slice';
import { AuthSlice, createAuthSlice } from './auth-slice'; import { AuthSlice, createAuthSlice } from './auth-slice';
import { ConfigSlice, createConfigSlice } from './config-slice'; import { ConfigSlice, createConfigSlice } from './config-slice';
import { LocalStorageInterface } from '@type/chat'; import {
import { migrateV0 } from './migrate'; LocalStorageInterfaceV0ToV1,
LocalStorageInterfaceV1ToV2,
} from '@type/chat';
import { migrateV0, migrateV1 } from './migrate';
export type StoreState = ChatSlice & InputSlice & AuthSlice & ConfigSlice; export type StoreState = ChatSlice & InputSlice & AuthSlice & ConfigSlice;
@ -29,14 +32,16 @@ const useStore = create<StoreState>()(
currentChatIndex: state.currentChatIndex, currentChatIndex: state.currentChatIndex,
apiKey: state.apiKey, apiKey: state.apiKey,
apiFree: state.apiFree, apiFree: state.apiFree,
apiFreeEndpoint: state.apiFreeEndpoint, apiEndpoint: state.apiEndpoint,
theme: state.theme, theme: state.theme,
}), }),
version: 1, version: 2,
migrate: (persistedState, version) => { migrate: (persistedState, version) => {
switch (version) { switch (version) {
case 0: case 0:
migrateV0(persistedState as LocalStorageInterface); migrateV0(persistedState as LocalStorageInterfaceV0ToV1);
case 1:
migrateV1(persistedState as LocalStorageInterfaceV1ToV2);
break; break;
} }
return persistedState as StoreState; return persistedState as StoreState;

View file

@ -20,7 +20,7 @@ export interface ConfigInterface {
presence_penalty: number; presence_penalty: number;
} }
export interface LocalStorageInterface { export interface LocalStorageInterfaceV0ToV1 {
chats: ChatInterface[]; chats: ChatInterface[];
currentChatIndex: number; currentChatIndex: number;
apiKey: string; apiKey: string;
@ -28,3 +28,13 @@ export interface LocalStorageInterface {
apiFreeEndpoint: string; apiFreeEndpoint: string;
theme: Theme; theme: Theme;
} }
export interface LocalStorageInterfaceV1ToV2 {
chats: ChatInterface[];
currentChatIndex: number;
apiKey: string;
apiFree: boolean;
apiFreeEndpoint: string;
apiEndpoint?: string;
theme: Theme;
}