mirror of
https://github.com/NovaOSS/nova-betterchat.git
synced 2024-11-25 19:33:59 +01:00
update api menu and readme
This commit is contained in:
parent
a5cd36c1d1
commit
9f1529d07a
|
@ -22,8 +22,7 @@
|
|||
<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"/>
|
||||
</a>
|
||||
<a href="https://twitter.com/intent/tweet?text=👋请检查这个惊人的存储库 https://github.com/ztjhz/ChatGPTFreeApp,由@nikushii_创建。"><img src="https://img.shields.io/twitter/url?label=分享到推特
|
||||
&style=social&url=https%3A%2F%2Fgithub.com%ztjhz%2FChatGPTFreeApp"></a>
|
||||
<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>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
|
@ -75,7 +74,7 @@
|
|||
|
||||
- 💾 所有聊天记录都会自动备份到您的浏览器本地存储器中
|
||||
- 📥 轻松导入和导出聊天数据 JSON 文件。
|
||||
- 📥 下载您的整个聊天记录,以 markdown,pdf 或图像的形式。
|
||||
- 📥 下载您的整个聊天记录,以 markdown,pdf 或图片的形式。
|
||||
|
||||
### UI / UX
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<h1 align="center"><b>ChatGPT Free App</b></h1>
|
||||
|
||||
<p align="center">
|
||||
English Version |
|
||||
<a href="README-zh_CN.md">
|
||||
简体中文版
|
||||
</a>
|
||||
|
@ -28,7 +29,7 @@
|
|||
<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"/>
|
||||
</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 align="center">
|
||||
|
|
|
@ -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>Ayaka</0>: https://chatgpt-api.shn.hk/v1/ or enter your own API endpoint"
|
||||
"option": "Use for free",
|
||||
"inputLabel": "API Endpoint",
|
||||
"description": "Thank you to <0>Ayaka</0> 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>here</0>",
|
||||
"inputLabel": "API Key"
|
||||
}
|
||||
},
|
||||
"customEndpoint": "Use custom endpoint"
|
||||
}
|
||||
|
|
|
@ -21,5 +21,6 @@
|
|||
"newChat": "New Chat",
|
||||
"lightMode": "Light Mode",
|
||||
"darkMode": "Dark Mode",
|
||||
"setting": "Settings"
|
||||
"setting": "Settings",
|
||||
"image": "Image"
|
||||
}
|
||||
|
|
|
@ -2,12 +2,13 @@
|
|||
"securityMessage": "我们高度优先考虑您的 API 密钥的安全,并非常小心地处理它。您的密钥将专门存储在您的浏览器中,并且永远不会与任何第三方实体共享。它仅用于访问 OpenAI API 的预期用途,而不是用于任何其他未经授权的用途。",
|
||||
"apiEndpoint": {
|
||||
"option": "使用免费的 API 端点",
|
||||
"inputLabel": "免费的 API 端点",
|
||||
"description": "使用 <0>Ayaka</0> 提供的免费 API 端点:https://chatgpt-api.shn.hk/v1/,或输入您自己的 API 端点"
|
||||
"inputLabel": "API 端点",
|
||||
"description": "感谢 <0>Ayaka</0> 提供免费的 API 终端: https://chatgpt-api.shn.hk/v1/。"
|
||||
},
|
||||
"apiKey": {
|
||||
"option": "使用自己的 API 密钥",
|
||||
"howTo": "在<0>这里</0>获取您的个人 API 密钥",
|
||||
"inputLabel": "API 密钥"
|
||||
}
|
||||
},
|
||||
"customEndpoint": "使用自定义端点"
|
||||
}
|
||||
|
|
|
@ -21,5 +21,6 @@
|
|||
"newChat": "新聊天",
|
||||
"lightMode": "亮色模式",
|
||||
"darkMode": "黑暗模式",
|
||||
"setting": "设置"
|
||||
"setting": "设置",
|
||||
"image": "图片"
|
||||
}
|
||||
|
|
67
src/api/api.ts
Normal file
67
src/api/api.ts
Normal 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;
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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<React.SetStateAction<boolean>>;
|
||||
}) => {
|
||||
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<boolean>(apiFree);
|
||||
const [_apiKey, _setApiKey] = useState<string>(apiKey || '');
|
||||
const [_apiFreeEndpoint, _setApiFreeEndpoint] =
|
||||
useState<string>(apiFreeEndpoint);
|
||||
const [_apiEndpoint, _setApiEndpoint] = useState<string>(apiEndpoint);
|
||||
const [_customEndpoint, _setCustomEndpoint] = useState<boolean>(
|
||||
!availableEndpoints.includes(apiEndpoint)
|
||||
);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (_apiFree === true) {
|
||||
setApiFreeEndpoint(_apiFreeEndpoint);
|
||||
setApiFree(true);
|
||||
setIsModalOpen(false);
|
||||
} else {
|
||||
const handleSave = () => {
|
||||
setApiFree(_apiFree);
|
||||
setApiKey(_apiKey);
|
||||
setApiFree(false);
|
||||
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 (
|
||||
<PopupModal
|
||||
title={t('api') as string}
|
||||
setIsModalOpen={setIsModalOpen}
|
||||
handleConfirm={handleSave}
|
||||
handleClose={handleClose}
|
||||
>
|
||||
<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='radio'
|
||||
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'
|
||||
onChange={() => _setApiFree(true)}
|
||||
type='checkbox'
|
||||
checked={_customEndpoint}
|
||||
className='w-4 h-4'
|
||||
onChange={handleToggleCustomEndpoint}
|
||||
/>
|
||||
<label className='ml-2 text-sm font-medium text-gray-900 dark:text-gray-300'>
|
||||
{t('apiEndpoint.option', { ns: 'api' })}
|
||||
{t('customEndpoint', { ns: 'api' })}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{_apiFree && (
|
||||
<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='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={_apiFreeEndpoint}
|
||||
value={_apiEndpoint}
|
||||
placeholder='https://chatgpt-api.shn.hk/v1/'
|
||||
onChange={(e) => {
|
||||
_setApiFreeEndpoint(e.target.value);
|
||||
_setApiEndpoint(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ApiEndpointSelector
|
||||
_apiEndpoint={_apiEndpoint}
|
||||
_setApiEndpoint={_setApiEndpoint}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='flex items-center'>
|
||||
<label className='flex items-center mb-2 gap-2 text-sm font-medium text-gray-900 dark:text-gray-300'>
|
||||
<input
|
||||
type='radio'
|
||||
checked={_apiFree === true}
|
||||
className='w-4 h-4'
|
||||
onChange={() => _setApiFree(true)}
|
||||
/>
|
||||
{t('apiEndpoint.option', { ns: 'api' })}
|
||||
</label>
|
||||
|
||||
<label className='flex items-center gap-2 text-sm font-medium text-gray-900 dark:text-gray-300'>
|
||||
<input
|
||||
type='radio'
|
||||
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)}
|
||||
/>
|
||||
<label className='ml-2 text-sm font-medium text-gray-900 dark:text-gray-300'>
|
||||
{t('apiKey.option', { ns: 'api' })}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{_apiFree === false && (
|
||||
<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'>
|
||||
{t('apiEndpoint.inputLabel', { ns: 'api' })}
|
||||
{t('apiKey.inputLabel', { ns: 'api' })}
|
||||
</div>
|
||||
<input
|
||||
type='text'
|
||||
|
@ -144,13 +128,73 @@ const ApiMenu = ({
|
|||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='min-w-fit text-gray-900 dark:text-gray-300 text-sm mt-4'>
|
||||
{t('securityMessage', { ns: 'api' })}
|
||||
</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>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ const DownloadChat = React.memo(
|
|||
{isModalOpen && (
|
||||
<PopupModal
|
||||
setIsModalOpen={setIsModalOpen}
|
||||
title='Download Chat'
|
||||
title={t('downloadChat') as string}
|
||||
cancelButton={false}
|
||||
>
|
||||
<div className='p-6 border-b border-gray-200 dark:border-gray-600 flex gap-4'>
|
||||
|
|
|
@ -19,7 +19,7 @@ const Config = () => {
|
|||
<PersonIcon />
|
||||
{t('api')}: {apiFree ? t('free') : t('personal')}
|
||||
</a>
|
||||
<ApiMenu isModalOpen={isModalOpen} setIsModalOpen={setIsModalOpen} />
|
||||
{isModalOpen && <ApiMenu setIsModalOpen={setIsModalOpen} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
7
src/constants/auth.ts
Normal file
7
src/constants/auth.ts
Normal 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,
|
||||
];
|
|
@ -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<string> => {
|
||||
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);
|
||||
}
|
||||
|
|
10
src/i18n.ts
10
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({
|
||||
i18n
|
||||
.use(Backend)
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
fallbackLng: 'en',
|
||||
ns: 'main',
|
||||
ns: ['main', 'api', 'about', 'model'],
|
||||
defaultNS: 'main',
|
||||
});
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
|
|
|
@ -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<AuthSlice> = (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<AuthSlice> = (set, get) => ({
|
|||
apiFree: apiFree,
|
||||
}));
|
||||
},
|
||||
setApiFreeEndpoint: (apiFreeEndpoint: string) => {
|
||||
setApiEndpoint: (apiEndpoint: string) => {
|
||||
set((prev: AuthSlice) => ({
|
||||
...prev,
|
||||
apiFreeEndpoint: apiFreeEndpoint,
|
||||
apiEndpoint: apiEndpoint,
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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<StoreState>()(
|
|||
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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue