mirror of
https://github.com/NovaOSS/nova-betterchat.git
synced 2024-11-25 17:43:58 +01:00
parent
693ac35b03
commit
2bf7f2d710
|
@ -112,9 +112,13 @@ At `ChatGPTFreeApp`, we strive to provide you with useful and amazing features a
|
|||
|
||||
If you have enjoyed using our app, we kindly ask you to give this project a ⭐️. Your endorsement means a lot to us and encourages us to work harder towards delivering the best possible experience.
|
||||
|
||||
If you would like to support the team, consider buying us a coffee by clicking on the button below. Every contribution, no matter how small, helps us to maintain and improve our service.
|
||||
If you would like to support the team, consider sponsoring us through one of the methods below. Every contribution, no matter how small, helps us to maintain and improve our service.
|
||||
|
||||
[![support](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/freechatgpt)
|
||||
| Payment Method | Link |
|
||||
| -------------- | -------------------------------------------------------------------------------------- |
|
||||
| KoFi | [![support](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/freechatgpt) |
|
||||
| Alipay (Ayaka) | <img src="https://ayaka14732.github.io/sponsor/alipay.jpg" width=150 /> |
|
||||
| Wechat (Ayaka) | <img src="https://ayaka14732.github.io/sponsor/wechat.png" width=150 /> |
|
||||
|
||||
Thank you for being a part of our community, and we look forward to serving you better in the future.
|
||||
|
||||
|
|
|
@ -10,9 +10,13 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"html2canvas": "^1.4.1",
|
||||
"i18next": "^22.4.11",
|
||||
"i18next-browser-languagedetector": "^7.0.1",
|
||||
"i18next-http-backend": "^2.1.1",
|
||||
"jspdf": "^2.5.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^12.2.0",
|
||||
"react-markdown": "^8.0.5",
|
||||
"react-scroll-to-bottom": "^4.2.0",
|
||||
"rehype-highlight": "^6.0.0",
|
||||
|
|
22
public/locales/en/about.json
Normal file
22
public/locales/en/about.json
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"description": "Free ChatGPT is an amazing open-source web app that allows you to play with OpenAI's ChatGPT API for free!",
|
||||
"discordServer": {
|
||||
"title": "Discord Server",
|
||||
"paragraph1": "We invite you to join our Discord community! Our Discord server is a great place to exchange ChatGPT ideas and tips, and submit feature requests for Free ChatGPT. You'll have the opportunity to interact with the developers behind Free ChatGPT as well as other AI enthusiasts who share your passion.",
|
||||
"paragraph2": "To join our server, simply click on the following link: <0>https://discord.gg/g3Qnwy4V6A</0>. We can't wait to see you there!"
|
||||
},
|
||||
"privacyStatement": {
|
||||
"title": "Privacy Statement",
|
||||
"paragraph1": "We highly value your privacy and are committed to safeguarding the privacy of our users. We do not collect or store any text you enter or receive from the OpenAI server in any form. Our source code is available for your inspection to verify this statement.",
|
||||
"paragraph2": "We prioritise the security of your API key and handle it with utmost care. If you use your own API key, 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."
|
||||
},
|
||||
"support": {
|
||||
"title": "Support",
|
||||
"paragraph1": "At Free ChatGPT, we strive to provide you with useful and amazing features around the clock. And just like any project, your support and motivation will be instrumental in helping us keep moving forward!",
|
||||
"paragraph2": "If you have enjoyed using our app, we kindly ask you to give this <0>project</0> a ⭐️. Your endorsement means a lot to us and encourages us to work harder towards delivering the best possible experience.",
|
||||
"paragraph3": "If you would like to support the team, consider sponsoring us through one of the methods below. Every contribution, no matter how small, helps us to maintain and improve our service.",
|
||||
"paragraph4": "Thank you for being a part of our community, and we look forward to serving you better in the future.",
|
||||
"alipay": "Alipay",
|
||||
"wechatPay": "WeChat Pay"
|
||||
}
|
||||
}
|
13
public/locales/en/api.json
Normal file
13
public/locales/en/api.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"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"
|
||||
},
|
||||
"apiKey": {
|
||||
"option": "Use your own API key",
|
||||
"howTo": "Get your personal API key <0>here</0>",
|
||||
"inputLabel": "API Key"
|
||||
}
|
||||
}
|
25
public/locales/en/main.json
Normal file
25
public/locales/en/main.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"save": "Save",
|
||||
"saveAndSubmit": "Save & Submit",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"warning": "Warning",
|
||||
"clearMessageWarning": "Please be advised that by submitting this message, all subsequent messages will be deleted!",
|
||||
"clearConversationWarning": "Please be advised that by confirming this action, all messages will be deleted!",
|
||||
"clearConversation": "Clear Conversation",
|
||||
"import": "Import",
|
||||
"export": "Export",
|
||||
"author": "Made by Jing Hua",
|
||||
"about": "About",
|
||||
"api": "API",
|
||||
"personal": "Personal",
|
||||
"free": "Free",
|
||||
"downloadChat": "Download Chat",
|
||||
"user": "User",
|
||||
"assistant": "Assistant",
|
||||
"system": "System",
|
||||
"newChat": "New Chat",
|
||||
"lightMode": "Light Mode",
|
||||
"darkMode": "Dark Mode",
|
||||
"setting": "Settings"
|
||||
}
|
13
public/locales/en/model.json
Normal file
13
public/locales/en/model.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"configuration": "Configuration",
|
||||
"model": "Model",
|
||||
"default": "Default",
|
||||
"temperature": {
|
||||
"label": "Temperature",
|
||||
"description": "What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic (Default: 1)"
|
||||
},
|
||||
"presencePenalty": {
|
||||
"label": "Presence Penalty",
|
||||
"description": "Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics. (Default: 0)"
|
||||
}
|
||||
}
|
22
public/locales/zh-CN/about.json
Normal file
22
public/locales/zh-CN/about.json
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"description": "Free ChatGPT 是一个神奇的开源 Web 应用,允许您免费使用 OpenAI 的 ChatGPT API 进行对话!",
|
||||
"discordServer": {
|
||||
"title": "Discord 服务器",
|
||||
"paragraph1": "我们邀请您加入我们的 Discord 社区!我们的 Discord 服务器是一个很好的地方,可以交流 ChatGPT 的想法和技巧,并提交 Free ChatGPT 的功能请求。您将有机会与 Free ChatGPT 的开发人员以及其他分享您热情的人工智能爱好者互动。",
|
||||
"paragraph2": "要加入我们的服务器,只需单击以下链接:<0>https://discord.gg/g3Qnwy4V6A</0>。我们迫不及待地想见到您!"
|
||||
},
|
||||
"privacyStatement": {
|
||||
"title": "隐私声明",
|
||||
"paragraph1": "我们非常重视您的隐私,并致力于保护用户的隐私。我们不会以任何形式收集或存储您输入或从 OpenAI 服务器接收的任何文本。我们的源代码可以供您检查,以验证此声明。",
|
||||
"paragraph2": "我们高度优先考虑您的 API 密钥的安全,并非常小心地处理它。如果您使用自己的 API 密钥,您的密钥将专门存储在您的浏览器中,并且永远不会与任何第三方实体共享。它仅用于访问 OpenAI API 的预期用途,而不会用于任何其他未经授权的用途。"
|
||||
},
|
||||
"support": {
|
||||
"title": "支持",
|
||||
"paragraph1": "在 Free ChatGPT,我们致力于为您提供实用和惊人的功能。就像任何项目一样,您的支持和激励将对我们在保持前进方面起到至关重要的作用!",
|
||||
"paragraph2": "如果您喜欢使用我们的应用程序,我们恳请您给这个<0>项目</0>一个 ⭐️。您的认可对我们意义重大,鼓励我们更加努力,以提供最佳的体验。",
|
||||
"paragraph3": "如果您想支持我们的团队,请考虑通过以下方法之一赞助我们。每一份贡献,无论多小,都有助于我们维护和改善我们的服务。",
|
||||
"paragraph4": "感谢您成为我们社区的一员,我们期待着在未来为您提供更好的服务。",
|
||||
"alipay": "支付宝",
|
||||
"wechatPay": "微信支付"
|
||||
}
|
||||
}
|
13
public/locales/zh-CN/api.json
Normal file
13
public/locales/zh-CN/api.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"securityMessage": "我们高度优先考虑您的 API 密钥的安全,并非常小心地处理它。您的密钥将专门存储在您的浏览器中,并且永远不会与任何第三方实体共享。它仅用于访问 OpenAI API 的预期用途,而不是用于任何其他未经授权的用途。",
|
||||
"apiEndpoint": {
|
||||
"option": "使用免费的 API 端点",
|
||||
"inputLabel": "免费的 API 端点",
|
||||
"description": "使用 <0>Ayaka</0> 提供的免费 API 端点:https://chatgpt-api.shn.hk/v1/,或输入您自己的 API 端点"
|
||||
},
|
||||
"apiKey": {
|
||||
"option": "使用自己的 API 密钥",
|
||||
"howTo": "在<0>这里</0>获取您的个人 API 密钥",
|
||||
"inputLabel": "API 密钥"
|
||||
}
|
||||
}
|
25
public/locales/zh-CN/main.json
Normal file
25
public/locales/zh-CN/main.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"save": "保存",
|
||||
"saveAndSubmit": "保存并提交",
|
||||
"cancel": "取消",
|
||||
"confirm": "确认",
|
||||
"warning": "警告",
|
||||
"clearMessageWarning": "请注意,通过提交此消息,所有后续消息都将被删除!",
|
||||
"clearConversationWarning": "请注意,确认此操作将删除所有消息!",
|
||||
"clearConversation": "清除对话",
|
||||
"import": "导入",
|
||||
"export": "导出",
|
||||
"author": "由 Jing Hua 制作",
|
||||
"about": "关于",
|
||||
"api": "API",
|
||||
"personal": "个人",
|
||||
"free": "免费",
|
||||
"downloadChat": "下载聊天记录",
|
||||
"user": "用户",
|
||||
"assistant": "助手",
|
||||
"system": "系统",
|
||||
"newChat": "新聊天",
|
||||
"lightMode": "亮色模式",
|
||||
"darkMode": "黑暗模式",
|
||||
"setting": "设置"
|
||||
}
|
13
public/locales/zh-CN/model.json
Normal file
13
public/locales/zh-CN/model.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"configuration": "配置",
|
||||
"model": "模型",
|
||||
"default": "默认",
|
||||
"temperature": {
|
||||
"label": "采样温度",
|
||||
"description": "使用何种采样温度,值在 0 到 2 之间。较高的数值如 0.8 会使输出更加随机,而较低的数值如 0.2 会使输出更加集中和确定。(默认: 1)"
|
||||
},
|
||||
"presencePenalty": {
|
||||
"label": "存在惩罚",
|
||||
"description": "数值在 -2.0 到 2.0 之间。正值会根据新 token 是否已经出现在文本中来惩罚它们,增加模型谈论新话题的可能性。 (默认: 0)"
|
||||
}
|
||||
}
|
17
src/App.tsx
17
src/App.tsx
|
@ -1,8 +1,9 @@
|
|||
import React, { useEffect, useRef } from 'react';
|
||||
import React, { useEffect, Suspense } from 'react';
|
||||
import useStore from '@store/store';
|
||||
|
||||
import Chat from './components/Chat';
|
||||
import Menu from './components/Menu';
|
||||
import Chat from '@components/Chat';
|
||||
import Menu from '@components/Menu';
|
||||
import LoadingScreen from '@components/LoadingScreen';
|
||||
|
||||
import useInitialiseNewChat from '@hooks/useInitialiseNewChat';
|
||||
import { ChatInterface } from '@type/chat';
|
||||
|
@ -65,10 +66,12 @@ function App() {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<div className='overflow-hidden w-full h-full relative'>
|
||||
<Menu />
|
||||
<Chat />
|
||||
</div>
|
||||
<Suspense fallback={<LoadingScreen />}>
|
||||
<div className='overflow-hidden w-full h-full relative'>
|
||||
<Menu />
|
||||
<Chat />
|
||||
</div>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
import React from 'react';
|
||||
|
||||
const SettingIcon = () => {
|
||||
const SettingIcon = (props: React.SVGProps<SVGSVGElement>) => {
|
||||
return (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' fill='white' viewBox='0 0 512 512'>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
fill='white'
|
||||
viewBox='0 0 512 512'
|
||||
{...props}
|
||||
>
|
||||
<path d='M481.9 166.6c3.2 8.7 .5 18.4-6.4 24.6l-30.9 28.1c-7.7 7.1-11.4 17.5-10.9 27.9c.1 2.9 .2 5.8 .2 8.8s-.1 5.9-.2 8.8c-.5 10.5 3.1 20.9 10.9 27.9l30.9 28.1c6.9 6.2 9.6 15.9 6.4 24.6c-4.4 11.9-9.7 23.3-15.8 34.3l-4.7 8.1c-6.6 11-14 21.4-22.1 31.2c-5.9 7.2-15.7 9.6-24.5 6.8l-39.7-12.6c-10-3.2-20.8-1.1-29.7 4.6c-4.9 3.1-9.9 6.1-15.1 8.7c-9.3 4.8-16.5 13.2-18.8 23.4l-8.9 40.7c-2 9.1-9 16.3-18.2 17.8c-13.8 2.3-28 3.5-42.5 3.5s-28.7-1.2-42.5-3.5c-9.2-1.5-16.2-8.7-18.2-17.8l-8.9-40.7c-2.2-10.2-9.5-18.6-18.8-23.4c-5.2-2.7-10.2-5.6-15.1-8.7c-8.8-5.7-19.7-7.8-29.7-4.6L69.1 425.9c-8.8 2.8-18.6 .3-24.5-6.8c-8.1-9.8-15.5-20.2-22.1-31.2l-4.7-8.1c-6.1-11-11.4-22.4-15.8-34.3c-3.2-8.7-.5-18.4 6.4-24.6l30.9-28.1c7.7-7.1 11.4-17.5 10.9-27.9c-.1-2.9-.2-5.8-.2-8.8s.1-5.9 .2-8.8c.5-10.5-3.1-20.9-10.9-27.9L8.4 191.2c-6.9-6.2-9.6-15.9-6.4-24.6c4.4-11.9 9.7-23.3 15.8-34.3l4.7-8.1c6.6-11 14-21.4 22.1-31.2c5.9-7.2 15.7-9.6 24.5-6.8l39.7 12.6c10 3.2 20.8 1.1 29.7-4.6c4.9-3.1 9.9-6.1 15.1-8.7c9.3-4.8 16.5-13.2 18.8-23.4l8.9-40.7c2-9.1 9-16.3 18.2-17.8C213.3 1.2 227.5 0 242 0s28.7 1.2 42.5 3.5c9.2 1.5 16.2 8.7 18.2 17.8l8.9 40.7c2.2 10.2 9.4 18.6 18.8 23.4c5.2 2.7 10.2 5.6 15.1 8.7c8.8 5.7 19.7 7.7 29.7 4.6l39.7-12.6c8.8-2.8 18.6-.3 24.5 6.8c8.1 9.8 15.5 20.2 22.1 31.2l4.7 8.1c6.1 11 11.4 22.4 15.8 34.3zM242 336a80 80 0 1 0 0-160 80 80 0 1 0 0 160z' />
|
||||
</svg>
|
||||
);
|
||||
|
|
24
src/assets/icons/SpinnerIcon.tsx
Normal file
24
src/assets/icons/SpinnerIcon.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import React from 'react';
|
||||
|
||||
const SpinnerIcon = (props: React.SVGProps<SVGSVGElement>) => {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden='true'
|
||||
viewBox='0 0 100 101'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d='M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
<path
|
||||
d='M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z'
|
||||
fill='currentFill'
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpinnerIcon;
|
|
@ -1,9 +1,11 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
import PopupModal from '@components/PopupModal';
|
||||
import AboutIcon from '@icon/AboutIcon';
|
||||
import Updates from '@components/Menu/MenuOptions/Updates';
|
||||
|
||||
const AboutMenu = () => {
|
||||
const { t } = useTranslation(['main', 'about']);
|
||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
|
@ -17,7 +19,7 @@ const AboutMenu = () => {
|
|||
<div>
|
||||
<AboutIcon />
|
||||
</div>
|
||||
About
|
||||
{t('about')}
|
||||
</a>
|
||||
{isModalOpen && (
|
||||
<PopupModal
|
||||
|
@ -27,26 +29,81 @@ const AboutMenu = () => {
|
|||
>
|
||||
<div className='p-6 border-b border-gray-200 dark:border-gray-600'>
|
||||
<div className='min-w-fit text-gray-900 dark:text-gray-300 text-sm flex flex-col gap-2'>
|
||||
<p>Free ChatGPT is an amazing open-source web app that allows you to play with OpenAI's ChatGPT API for free!</p>
|
||||
<p>{t('description', { ns: 'about' })}</p>
|
||||
<Updates isButton />
|
||||
|
||||
<h2 className='text-lg font-bold'>Discord Server</h2>
|
||||
<p>We invite you to join our Discord community! Our Discord server is a great place to exchange ChatGPT ideas and tips, and submit feature requests for Free ChatGPT. You'll have the opportunity to interact with the developers behind Free ChatGPT as well as other AI enthusiasts who share your passion.</p>
|
||||
<h2 className='text-lg font-bold'>
|
||||
{t('discordServer.title', { ns: 'about' })}
|
||||
</h2>
|
||||
<p>{t('discordServer.paragraph1', { ns: 'about' })}</p>
|
||||
|
||||
<p>To join our server, simply click on the following link: <a className='link' href='https://discord.gg/g3Qnwy4V6A' target='_blank'>https://discord.gg/g3Qnwy4V6A</a>. We can't wait to see you there!</p>
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey='discordServer.paragraph2'
|
||||
ns='about'
|
||||
components={[
|
||||
<a
|
||||
className='link'
|
||||
href='https://discord.gg/g3Qnwy4V6A'
|
||||
target='_blank'
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<h2 className='text-lg font-bold'>Privacy Statement</h2>
|
||||
<p>We highly value your privacy and are committed to safeguarding the privacy of our users. We do not collect or store any text you enter or receive from the OpenAI server in any form. Our source code is available for your inspection to verify this statement.</p>
|
||||
<h2 className='text-lg font-bold'>
|
||||
{t('privacyStatement.title', { ns: 'about' })}
|
||||
</h2>
|
||||
<p>{t('privacyStatement.paragraph1', { ns: 'about' })}</p>
|
||||
|
||||
<p>We prioritise the security of your API key and handle it with utmost care. If you use your own API key, 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.</p>
|
||||
<h2 className='text-lg font-bold'>Support</h2>
|
||||
<p>At freechatgpt.chat, we strive to provide you with useful and amazing features around the clock. And just like any project, your support and motivation will be instrumental in helping us keep moving forward!</p>
|
||||
<p>If you have enjoyed using our app, we kindly ask you to give this <a href="https://github.com/ztjhz/ChatGPTFreeApp/" target="_blank" className="link">project</a> a ⭐️. Your endorsement means a lot to us and encourages us to work harder towards delivering the best possible experience.</p>
|
||||
<p>If you would like to support the team, consider buying us a coffee by clicking on the button below. Every contribution, no matter how small, helps us to maintain and improve our service.</p>
|
||||
<a href="https://ko-fi.com/freechatgpt" target="_blank">
|
||||
<img src="/kofi.svg" alt="Support us through the Ko-fi platform." />
|
||||
</a>
|
||||
<p>Thank you for being a part of our community, and we look forward to serving you better in the future.</p>
|
||||
<p>{t('privacyStatement.paragraph2', { ns: 'about' })}</p>
|
||||
<h2 className='text-lg font-bold'>
|
||||
{t('support.title', { ns: 'about' })}
|
||||
</h2>
|
||||
<p>{t('support.paragraph1', { ns: 'about' })}</p>
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey='support.paragraph2'
|
||||
ns='about'
|
||||
components={[
|
||||
<a
|
||||
href='https://github.com/ztjhz/ChatGPTFreeApp/'
|
||||
target='_blank'
|
||||
className='link'
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</p>
|
||||
<p>{t('support.paragraph3', { ns: 'about' })}</p>
|
||||
<div className='flex flex-col items-center gap-4 my-4'>
|
||||
<a href='https://ko-fi.com/freechatgpt' target='_blank'>
|
||||
<img
|
||||
src='/kofi.svg'
|
||||
alt='Support us through the Ko-fi platform.'
|
||||
/>
|
||||
</a>
|
||||
<div className='flex gap-x-10 gap-y-4 flex-wrap justify-center'>
|
||||
<div className='flex flex-col items-center justify-center gap-1'>
|
||||
<div>{t('support.alipay', { ns: 'about' })} (Ayaka)</div>
|
||||
<img
|
||||
className='rounded-md w-32 h-32'
|
||||
src='https://ayaka14732.github.io/sponsor/alipay.jpg'
|
||||
alt='Support us through Alipay'
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col items-center justify-center gap-1'>
|
||||
<div>{t('support.wechatPay', { ns: 'about' })} (Ayaka)</div>
|
||||
<img
|
||||
className='rounded-md w-32 h-32'
|
||||
src='https://ayaka14732.github.io/sponsor/wechat.png'
|
||||
alt='Support us through WeChat Pay'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className='text-center'>
|
||||
{t('support.paragraph4', { ns: 'about' })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</PopupModal>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
import useStore from '@store/store';
|
||||
|
||||
import PopupModal from '@components/PopupModal';
|
||||
|
@ -10,6 +11,8 @@ const ApiMenu = ({
|
|||
isModalOpen: boolean;
|
||||
setIsModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}) => {
|
||||
const { t } = useTranslation(['main', 'api']);
|
||||
|
||||
const apiKey = useStore((state) => state.apiKey);
|
||||
const setApiKey = useStore((state) => state.setApiKey);
|
||||
const apiFree = useStore((state) => state.apiFree);
|
||||
|
@ -50,7 +53,7 @@ const ApiMenu = ({
|
|||
|
||||
return isModalOpen ? (
|
||||
<PopupModal
|
||||
title='API'
|
||||
title={t('api') as string}
|
||||
setIsModalOpen={setIsModalOpen}
|
||||
handleConfirm={handleSave}
|
||||
handleClose={handleClose}
|
||||
|
@ -64,26 +67,28 @@ const ApiMenu = ({
|
|||
onChange={() => _setApiFree(true)}
|
||||
/>
|
||||
<label className='ml-2 text-sm font-medium text-gray-900 dark:text-gray-300'>
|
||||
Use free API endpoint
|
||||
{t('apiEndpoint.option', { 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'>
|
||||
Use free API endpoint from{' '}
|
||||
<a
|
||||
href='https://github.com/ayaka14732/ChatGPTAPIFree'
|
||||
className='underline dark:hover:text-white hover:text-black'
|
||||
target='_blank'
|
||||
>
|
||||
Ayaka
|
||||
</a>
|
||||
: https://chatgpt-api.shn.hk/v1/ or enter your own API endpoint
|
||||
<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'>
|
||||
Free API Endpoint
|
||||
{t('apiEndpoint.inputLabel', { ns: 'api' })}
|
||||
</div>
|
||||
<input
|
||||
type='text'
|
||||
|
@ -106,14 +111,14 @@ const ApiMenu = ({
|
|||
onChange={() => _setApiFree(false)}
|
||||
/>
|
||||
<label className='ml-2 text-sm font-medium text-gray-900 dark:text-gray-300'>
|
||||
Use your own API key
|
||||
{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'>
|
||||
API Key
|
||||
{t('apiEndpoint.inputLabel', { ns: 'api' })}
|
||||
</div>
|
||||
<input
|
||||
type='text'
|
||||
|
@ -127,21 +132,20 @@ const ApiMenu = ({
|
|||
)}
|
||||
|
||||
<div className='min-w-fit text-gray-900 dark:text-gray-300 text-sm mt-4 text-center'>
|
||||
Get your personal API key{' '}
|
||||
<a
|
||||
className='link'
|
||||
href='https://platform.openai.com/account/api-keys'
|
||||
target='_blank'
|
||||
>
|
||||
here
|
||||
</a>
|
||||
<Trans
|
||||
i18nKey='apiKey.howTo'
|
||||
ns='api'
|
||||
components={[
|
||||
<a
|
||||
href='https://platform.openai.com/account/api-keys'
|
||||
className='link'
|
||||
target='_blank'
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className='min-w-fit text-gray-900 dark:text-gray-300 text-sm mt-4'>
|
||||
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.
|
||||
{t('securityMessage', { ns: 'api' })}
|
||||
</div>
|
||||
</div>
|
||||
</PopupModal>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import useStore from '@store/store';
|
||||
import ConfigMenu from '@components/ConfigMenu';
|
||||
|
@ -6,6 +7,7 @@ import { ChatInterface, ConfigInterface } from '@type/chat';
|
|||
import { defaultChatConfig } from '@constants/chat';
|
||||
|
||||
const ChatTitle = React.memo(() => {
|
||||
const { t } = useTranslation('model');
|
||||
const config = useStore(
|
||||
(state) =>
|
||||
state.chats &&
|
||||
|
@ -47,13 +49,13 @@ const ChatTitle = React.memo(() => {
|
|||
}}
|
||||
>
|
||||
<div className='text-center p-1 rounded-md bg-gray-900/10 hover:bg-gray-900/50'>
|
||||
Model: Default
|
||||
{t('model')}: {t('default')}
|
||||
</div>
|
||||
<div className='text-center p-1 rounded-md bg-gray-900/10 hover:bg-gray-900/50'>
|
||||
Temperature: {config.temperature}
|
||||
{t('temperature.label')}: {config.temperature}
|
||||
</div>
|
||||
<div className='text-center p-1 rounded-md bg-gray-900/10 hover:bg-gray-900/50'>
|
||||
PresencePenalty: {config.presence_penalty}
|
||||
{t('presencePenalty.label')}: {config.presence_penalty}
|
||||
</div>
|
||||
</div>
|
||||
{isModalOpen && (
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useStore from '@store/store';
|
||||
import PopupModal from '@components/PopupModal';
|
||||
import {
|
||||
|
@ -14,6 +15,7 @@ import MarkdownIcon from '@icon/MarkdownIcon';
|
|||
|
||||
const DownloadChat = React.memo(
|
||||
({ saveRef }: { saveRef: React.RefObject<HTMLDivElement> }) => {
|
||||
const { t } = useTranslation();
|
||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||||
return (
|
||||
<>
|
||||
|
@ -23,7 +25,7 @@ const DownloadChat = React.memo(
|
|||
setIsModalOpen(true);
|
||||
}}
|
||||
>
|
||||
Download Chat
|
||||
{t('downloadChat')}
|
||||
</button>
|
||||
{isModalOpen && (
|
||||
<PopupModal
|
||||
|
|
|
@ -4,6 +4,7 @@ import React, {
|
|||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { CodeProps, ReactMarkdownProps } from 'react-markdown/lib/ast-to-react';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
|
@ -303,6 +304,8 @@ const EditView = ({
|
|||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||||
const textareaRef = React.createRef<HTMLTextAreaElement>();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const resetTextAreaHeight = () => {
|
||||
if (textareaRef.current) textareaRef.current.style.height = 'auto';
|
||||
};
|
||||
|
@ -403,8 +406,8 @@ const EditView = ({
|
|||
{isModalOpen && (
|
||||
<PopupModal
|
||||
setIsModalOpen={setIsModalOpen}
|
||||
title='Warning'
|
||||
message='Please be advised that by submitting this message, all subsequent messages will be deleted!'
|
||||
title={t('warning') as string}
|
||||
message={t('clearMessageWarning') as string}
|
||||
handleConfirm={handleSaveAndSubmit}
|
||||
/>
|
||||
)}
|
||||
|
@ -426,6 +429,8 @@ const EditViewButtons = React.memo(
|
|||
setIsModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setIsEdit: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className='text-center mt-2 flex justify-center'>
|
||||
{sticky && (
|
||||
|
@ -434,7 +439,7 @@ const EditViewButtons = React.memo(
|
|||
onClick={handleSaveAndSubmit}
|
||||
>
|
||||
<div className='flex items-center justify-center gap-2'>
|
||||
Save & Submit
|
||||
{t('saveAndSubmit')}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
@ -445,7 +450,9 @@ const EditViewButtons = React.memo(
|
|||
}`}
|
||||
onClick={handleSave}
|
||||
>
|
||||
<div className='flex items-center justify-center gap-2'>Save</div>
|
||||
<div className='flex items-center justify-center gap-2'>
|
||||
{t('save')}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{sticky || (
|
||||
|
@ -456,7 +463,7 @@ const EditViewButtons = React.memo(
|
|||
}}
|
||||
>
|
||||
<div className='flex items-center justify-center gap-2'>
|
||||
Save & Submit
|
||||
{t('saveAndSubmit')}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
@ -466,7 +473,9 @@ const EditViewButtons = React.memo(
|
|||
className='btn relative btn-neutral'
|
||||
onClick={() => setIsEdit(false)}
|
||||
>
|
||||
<div className='flex items-center justify-center gap-2'>Cancel</div>
|
||||
<div className='flex items-center justify-center gap-2'>
|
||||
{t('cancel')}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useStore from '@store/store';
|
||||
|
||||
import DownChevronArrow from '@icon/DownChevronArrow';
|
||||
|
@ -14,6 +15,7 @@ const RoleSelector = React.memo(
|
|||
messageIndex: number;
|
||||
sticky?: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const setInputRole = useStore((state) => state.setInputRole);
|
||||
const setChats = useStore((state) => state.setChats);
|
||||
const currentChatIndex = useStore((state) => state.currentChatIndex);
|
||||
|
@ -27,7 +29,7 @@ const RoleSelector = React.memo(
|
|||
type='button'
|
||||
onClick={() => setDropDown((prev) => !prev)}
|
||||
>
|
||||
{role.charAt(0).toUpperCase() + role.slice(1)}
|
||||
{t(role)}
|
||||
<DownChevronArrow />
|
||||
</button>
|
||||
<div
|
||||
|
@ -58,7 +60,7 @@ const RoleSelector = React.memo(
|
|||
}}
|
||||
key={r}
|
||||
>
|
||||
{r.charAt(0).toUpperCase() + r.slice(1)}
|
||||
{t(r)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import PopupModal from '@components/PopupModal';
|
||||
import { ConfigInterface } from '@type/chat';
|
||||
|
||||
|
@ -15,6 +16,7 @@ const ConfigMenu = ({
|
|||
const [_presencePenalty, _setPresencePenalty] = useState<number>(
|
||||
config.presence_penalty
|
||||
);
|
||||
const { t } = useTranslation('model');
|
||||
|
||||
const handleConfirm = () => {
|
||||
setConfig({
|
||||
|
@ -26,14 +28,14 @@ const ConfigMenu = ({
|
|||
|
||||
return (
|
||||
<PopupModal
|
||||
title='Configuration'
|
||||
title={t('configuration') as string}
|
||||
setIsModalOpen={setIsModalOpen}
|
||||
handleConfirm={handleConfirm}
|
||||
>
|
||||
<div className='p-6 border-b border-gray-200 dark:border-gray-600'>
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-900 dark:text-white'>
|
||||
Temperature: {_temperature}
|
||||
{t('temperature.label')}: {_temperature}
|
||||
</label>
|
||||
<input
|
||||
id='default-range'
|
||||
|
@ -48,14 +50,12 @@ const ConfigMenu = ({
|
|||
className='w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer'
|
||||
/>
|
||||
<div className='min-w-fit text-gray-500 dark:text-gray-300 text-sm mt-2'>
|
||||
What sampling temperature to use, between 0 and 2. Higher values
|
||||
like 0.8 will make the output more random, while lower values like
|
||||
0.2 will make it more focused and deterministic.
|
||||
{t('temperature.description')}
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-5 pt-5 border-t border-gray-500'>
|
||||
<label className='block text-sm font-medium text-gray-900 dark:text-white'>
|
||||
Presence Penalty: {_presencePenalty}
|
||||
{t('presencePenalty.label')}: {_presencePenalty}
|
||||
</label>
|
||||
<input
|
||||
id='default-range'
|
||||
|
@ -70,9 +70,7 @@ const ConfigMenu = ({
|
|||
className='w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer'
|
||||
/>
|
||||
<div className='min-w-fit text-gray-500 dark:text-gray-300 text-sm mt-2'>
|
||||
Number between -2.0 and 2.0. Positive values penalize new tokens
|
||||
based on whether they appear in the text so far, increasing the
|
||||
model's likelihood to talk about new topics.
|
||||
{t('presencePenalty.description')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useStore from '@store/store';
|
||||
|
||||
import ExportIcon from '@icon/ExportIcon';
|
||||
|
@ -8,6 +9,7 @@ import PopupModal from '@components/PopupModal';
|
|||
import { isChats } from '@utils/chat';
|
||||
|
||||
const ImportExportChat = () => {
|
||||
const { t } = useTranslation();
|
||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
|
@ -19,11 +21,11 @@ const ImportExportChat = () => {
|
|||
}}
|
||||
>
|
||||
<ExportIcon className='w-4 h-4' />
|
||||
Import / Export
|
||||
{t('import')} / {t('export')}
|
||||
</a>
|
||||
{isModalOpen && (
|
||||
<PopupModal
|
||||
title='Import / Export'
|
||||
title={`${t('import')} / ${t('export')}`}
|
||||
setIsModalOpen={setIsModalOpen}
|
||||
cancelButton={false}
|
||||
>
|
||||
|
@ -38,6 +40,7 @@ const ImportExportChat = () => {
|
|||
};
|
||||
|
||||
const ImportChat = () => {
|
||||
const { t } = useTranslation();
|
||||
const setChats = useStore.getState().setChats;
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [alert, setAlert] = useState<{
|
||||
|
@ -74,7 +77,7 @@ const ImportChat = () => {
|
|||
return (
|
||||
<>
|
||||
<label className='block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300'>
|
||||
Import (JSON)
|
||||
{t('import')} (JSON)
|
||||
</label>
|
||||
<input
|
||||
className='w-full text-sm file:p-2 text-gray-800 file:text-gray-700 dark:text-gray-300 dark:file:text-gray-200 rounded-md cursor-pointer focus:outline-none bg-gray-50 file:bg-gray-100 dark:bg-gray-800 dark:file:bg-gray-700 file:border-0 border border-gray-300 dark:border-gray-600 placeholder-gray-900 dark:placeholder-gray-300 file:cursor-pointer'
|
||||
|
@ -85,7 +88,7 @@ const ImportChat = () => {
|
|||
className='btn btn-small btn-primary mt-3'
|
||||
onClick={handleFileUpload}
|
||||
>
|
||||
Import
|
||||
{t('import')}
|
||||
</button>
|
||||
{alert && (
|
||||
<div
|
||||
|
@ -103,11 +106,12 @@ const ImportChat = () => {
|
|||
};
|
||||
|
||||
const ExportChat = () => {
|
||||
const { t } = useTranslation();
|
||||
const chats = useStore.getState().chats;
|
||||
return (
|
||||
<div className='mt-6'>
|
||||
<div className='block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300'>
|
||||
Export (JSON)
|
||||
{t('export')} (JSON)
|
||||
</div>
|
||||
<button
|
||||
className='btn btn-small btn-primary'
|
||||
|
@ -115,7 +119,7 @@ const ExportChat = () => {
|
|||
if (chats) downloadFile(chats, getToday());
|
||||
}}
|
||||
>
|
||||
Export
|
||||
{t('export')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
|
49
src/components/LanguageSelector/LanguageSelector.tsx
Normal file
49
src/components/LanguageSelector/LanguageSelector.tsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import DownChevronArrow from '@icon/DownChevronArrow';
|
||||
import { i18nLanguages } from '@src/i18n';
|
||||
|
||||
const LanguageSelector = () => {
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const [dropDown, setDropDown] = useState<boolean>(false);
|
||||
return (
|
||||
<div className='prose dark:prose-invert relative'>
|
||||
<button
|
||||
className='btn btn-neutral btn-small flex w-32 flex justify-between'
|
||||
type='button'
|
||||
onClick={() => setDropDown((prev) => !prev)}
|
||||
>
|
||||
{i18n.language}
|
||||
<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`}
|
||||
>
|
||||
<ul
|
||||
className='text-sm text-gray-700 dark:text-gray-200 p-0 m-0'
|
||||
aria-labelledby='dropdownDefaultButton'
|
||||
>
|
||||
{i18nLanguages.map((lang) => (
|
||||
<li
|
||||
className='px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white cursor-pointer'
|
||||
onClick={() => {
|
||||
i18n.changeLanguage(lang);
|
||||
setDropDown(false);
|
||||
}}
|
||||
key={lang}
|
||||
>
|
||||
{lang}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LanguageSelector;
|
1
src/components/LanguageSelector/index.ts
Normal file
1
src/components/LanguageSelector/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from './LanguageSelector';
|
12
src/components/LoadingScreen/LoadingScreen.tsx
Normal file
12
src/components/LoadingScreen/LoadingScreen.tsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
import React from 'react';
|
||||
import SpinnerIcon from '@icon/SpinnerIcon';
|
||||
|
||||
const LoadingScreen = () => {
|
||||
return (
|
||||
<div className='w-full h-full flex justify-center items-center bg-gray-800 text-gray-100'>
|
||||
<SpinnerIcon className='inline w-12 h-12 text-gray-400 fill-green-500/60 animate-spin' />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadingScreen;
|
1
src/components/LoadingScreen/index.ts
Normal file
1
src/components/LoadingScreen/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from './LoadingScreen';
|
|
@ -1,10 +1,12 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useStore from '@store/store';
|
||||
|
||||
import PersonIcon from '@icon/PersonIcon';
|
||||
import ApiMenu from '@components/ApiMenu';
|
||||
|
||||
const Config = () => {
|
||||
const { t } = useTranslation();
|
||||
const apiFree = useStore((state) => state.apiFree);
|
||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||||
|
||||
|
@ -15,7 +17,7 @@ const Config = () => {
|
|||
onClick={() => setIsModalOpen(true)}
|
||||
>
|
||||
<PersonIcon />
|
||||
API: {apiFree ? 'Free' : 'Personal'}
|
||||
{t('api')}: {apiFree ? t('free') : t('personal')}
|
||||
</a>
|
||||
<ApiMenu isModalOpen={isModalOpen} setIsModalOpen={setIsModalOpen} />
|
||||
</>
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import PopupModal from '@components/PopupModal';
|
||||
import DeleteIcon from '@icon/DeleteIcon';
|
||||
import useInitialiseNewChat from '@hooks/useInitialiseNewChat';
|
||||
|
||||
const ClearConversation = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const initialiseNewChat = useInitialiseNewChat();
|
||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||||
|
||||
|
@ -22,13 +25,13 @@ const ClearConversation = () => {
|
|||
}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
Clear conversations
|
||||
{t('clearConversation')}
|
||||
</a>
|
||||
{isModalOpen && (
|
||||
<PopupModal
|
||||
setIsModalOpen={setIsModalOpen}
|
||||
title='Warning'
|
||||
message='Please be advised that by confirming this action, all messages will be deleted!'
|
||||
title={t('warning') as string}
|
||||
message={t('clearConversationWarning') as string}
|
||||
handleConfirm={handleConfirm}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import HeartIcon from '@icon/HeartIcon';
|
||||
|
||||
const Me = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<a
|
||||
className='flex py-3 px-3 items-center gap-3 rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white cursor-pointer text-sm'
|
||||
|
@ -10,7 +12,7 @@ const Me = () => {
|
|||
target='_blank'
|
||||
>
|
||||
<HeartIcon />
|
||||
Made by Jing Hua
|
||||
{t('author')}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -3,12 +3,10 @@ import React from 'react';
|
|||
import Account from './Account';
|
||||
import ClearConversation from './ClearConversation';
|
||||
import Api from './Api';
|
||||
import Logout from './Logout';
|
||||
import Me from './Me';
|
||||
import ThemeSwitcher from './ThemeSwitcher';
|
||||
import Updates from './Updates';
|
||||
import AboutMenu from '@components/AboutMenu';
|
||||
import ImportExportChat from '@components/ImportExportChat';
|
||||
import SettingsMenu from '@components/SettingsMenu';
|
||||
|
||||
const MenuOptions = () => {
|
||||
return (
|
||||
|
@ -17,11 +15,8 @@ const MenuOptions = () => {
|
|||
<ClearConversation />
|
||||
<ImportExportChat />
|
||||
<Api />
|
||||
<ThemeSwitcher />
|
||||
{/* <Account /> */}
|
||||
{/* <Updates /> */}
|
||||
<SettingsMenu />
|
||||
<Me />
|
||||
{/* <Logout /> */}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useStore from '@store/store';
|
||||
import SunIcon from '@icon/SunIcon';
|
||||
import MoonIcon from '@icon/MoonIcon';
|
||||
|
@ -12,6 +13,7 @@ const getOppositeTheme = (theme: Theme): Theme => {
|
|||
}
|
||||
};
|
||||
const ThemeSwitcher = () => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useStore((state) => state.theme);
|
||||
const setTheme = useStore((state) => state.setTheme);
|
||||
|
||||
|
@ -24,15 +26,13 @@ const ThemeSwitcher = () => {
|
|||
}, [theme]);
|
||||
|
||||
return theme ? (
|
||||
<a
|
||||
className='flex py-3 px-3 items-center gap-3 rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white cursor-pointer text-sm'
|
||||
<button
|
||||
className='items-center gap-3 btn btn-neutral'
|
||||
onClick={switchTheme}
|
||||
>
|
||||
{theme === 'dark' ? <SunIcon /> : <MoonIcon />}
|
||||
{getOppositeTheme(theme).charAt(0).toUpperCase() +
|
||||
getOppositeTheme(theme).slice(1)}{' '}
|
||||
mode
|
||||
</a>
|
||||
{t(getOppositeTheme(theme) + 'Mode')}
|
||||
</button>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import PlusIcon from '@icon/PlusIcon';
|
||||
|
||||
import useAddChat from '@hooks/useAddChat';
|
||||
|
||||
const NewChat = () => {
|
||||
const { t } = useTranslation();
|
||||
const addChat = useAddChat();
|
||||
|
||||
return (
|
||||
|
@ -13,7 +15,9 @@ const NewChat = () => {
|
|||
onClick={addChat}
|
||||
>
|
||||
<PlusIcon />{' '}
|
||||
<span className='hidden md:inline-flex text-white text-sm'>New chat</span>
|
||||
<span className='hidden md:inline-flex text-white text-sm'>
|
||||
{t('newChat')}
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import CrossIcon2 from '@icon/CrossIcon2';
|
||||
|
||||
|
@ -21,6 +22,7 @@ const PopupModal = ({
|
|||
children?: React.ReactElement;
|
||||
}) => {
|
||||
const modalRoot = document.getElementById('modal-root');
|
||||
const { t } = useTranslation();
|
||||
|
||||
const _handleClose = () => {
|
||||
handleClose && handleClose();
|
||||
|
@ -62,7 +64,7 @@ const PopupModal = ({
|
|||
className='btn btn-primary'
|
||||
onClick={handleConfirm}
|
||||
>
|
||||
Confirm
|
||||
{t('confirm')}
|
||||
</button>
|
||||
)}
|
||||
{cancelButton && (
|
||||
|
@ -71,7 +73,7 @@ const PopupModal = ({
|
|||
className='btn btn-neutral'
|
||||
onClick={_handleClose}
|
||||
>
|
||||
Cancel
|
||||
{t('cancel')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
|
45
src/components/SettingsMenu/SettingsMenu.tsx
Normal file
45
src/components/SettingsMenu/SettingsMenu.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useStore from '@store/store';
|
||||
|
||||
import PopupModal from '@components/PopupModal';
|
||||
import SettingIcon from '@icon/SettingIcon';
|
||||
import ThemeSwitcher from '@components/Menu/MenuOptions/ThemeSwitcher';
|
||||
import LanguageSelector from '@components/LanguageSelector';
|
||||
|
||||
const SettingsMenu = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const theme = useStore.getState().theme;
|
||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.className = theme;
|
||||
}, [theme]);
|
||||
return (
|
||||
<>
|
||||
<a
|
||||
className='flex py-3 px-3 items-center gap-3 rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white cursor-pointer text-sm'
|
||||
onClick={() => {
|
||||
setIsModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<SettingIcon className='w-4 h-4' /> {t('setting') as string}
|
||||
</a>
|
||||
{isModalOpen && (
|
||||
<PopupModal
|
||||
setIsModalOpen={setIsModalOpen}
|
||||
title={t('setting') as string}
|
||||
cancelButton={false}
|
||||
>
|
||||
<div className='p-6 border-b border-gray-200 dark:border-gray-600 flex flex-col items-center gap-4'>
|
||||
<LanguageSelector />
|
||||
<ThemeSwitcher />
|
||||
</div>
|
||||
</PopupModal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsMenu;
|
1
src/components/SettingsMenu/index.ts
Normal file
1
src/components/SettingsMenu/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from './SettingsMenu';
|
16
src/i18n.ts
Normal file
16
src/i18n.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
import Backend from 'i18next-http-backend';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
|
||||
export const i18nLanguages = ['en', 'zh-CN'];
|
||||
|
||||
i18n.use(Backend).use(LanguageDetector).use(initReactI18next).init({
|
||||
fallbackLng: 'en',
|
||||
debug: true,
|
||||
ns: 'main',
|
||||
defaultNS: 'main',
|
||||
});
|
||||
|
||||
export default i18n;
|
|
@ -3,6 +3,8 @@ import ReactDOM from 'react-dom/client';
|
|||
import App from './App';
|
||||
import './main.css';
|
||||
|
||||
import './i18n';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
|
|
|
@ -24,7 +24,8 @@
|
|||
"@constants/*": ["./src/constants/*"],
|
||||
"@api/*": ["./src/api/*"],
|
||||
"@components/*": ["./src/components/*"],
|
||||
"@utils/*": ["./src/utils/*"]
|
||||
"@utils/*": ["./src/utils/*"],
|
||||
"@src/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
|
|
|
@ -14,6 +14,7 @@ export default defineConfig({
|
|||
'@api/': new URL('./src/api/', import.meta.url).pathname,
|
||||
'@components/': new URL('./src/components/', import.meta.url).pathname,
|
||||
'@utils/': new URL('./src/utils/', import.meta.url).pathname,
|
||||
'@src/': new URL('./src/', import.meta.url).pathname,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
75
yarn.lock
75
yarn.lock
|
@ -43,7 +43,7 @@
|
|||
core-js-pure "^3.25.1"
|
||||
regenerator-runtime "^0.13.11"
|
||||
|
||||
"@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.18.3":
|
||||
"@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.19.4", "@babel/runtime@^7.20.6":
|
||||
version "7.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673"
|
||||
integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==
|
||||
|
@ -672,6 +672,13 @@ cosmiconfig@^7.0.0:
|
|||
path-type "^4.0.0"
|
||||
yaml "^1.10.0"
|
||||
|
||||
cross-fetch@3.1.5:
|
||||
version "3.1.5"
|
||||
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f"
|
||||
integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==
|
||||
dependencies:
|
||||
node-fetch "2.6.7"
|
||||
|
||||
css-line-break@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-2.1.0.tgz#bfef660dfa6f5397ea54116bb3cb4873edbc4fa0"
|
||||
|
@ -961,6 +968,13 @@ highlight.js@~11.7.0:
|
|||
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.7.0.tgz#3ff0165bc843f8c9bce1fd89e2fda9143d24b11e"
|
||||
integrity sha512-1rRqesRFhMO/PRF+G86evnyJkCgaZFOI+Z6kdj15TA18funfoqJXvgPCLSf0SWq3SRfg1j3HlDs8o4s3EGq1oQ==
|
||||
|
||||
html-parse-stringify@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2"
|
||||
integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==
|
||||
dependencies:
|
||||
void-elements "3.1.0"
|
||||
|
||||
html2canvas@^1.0.0-rc.5, html2canvas@^1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543"
|
||||
|
@ -969,6 +983,27 @@ html2canvas@^1.0.0-rc.5, html2canvas@^1.4.1:
|
|||
css-line-break "^2.1.0"
|
||||
text-segmentation "^1.0.3"
|
||||
|
||||
i18next-browser-languagedetector@^7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.0.1.tgz#ead34592edc96c6c3a618a51cb57ad027c5b5d87"
|
||||
integrity sha512-Pa5kFwaczXJAeHE56CHG2aWzFBMJNUNghf0Pm4SwSrEMps/PTKqW90EYWlIvhuYStf3Sn1K0vw+gH3+TLdkH1g==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.19.4"
|
||||
|
||||
i18next-http-backend@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/i18next-http-backend/-/i18next-http-backend-2.1.1.tgz#72a21d61c2e96eea9ad45ba1b9dd0090e119709a"
|
||||
integrity sha512-jByfUCDVgQ8+/Wens7queQhYYvMcGTW/lR4IJJNEDDXnmqjLrwi8ubXKpmp76/JIWEZHffNdWqnxFJcTVGeaOw==
|
||||
dependencies:
|
||||
cross-fetch "3.1.5"
|
||||
|
||||
i18next@^22.4.11:
|
||||
version "22.4.11"
|
||||
resolved "https://registry.yarnpkg.com/i18next/-/i18next-22.4.11.tgz#8b6c9be95176de90d3f10a78af125d95d3a3258d"
|
||||
integrity sha512-ShfTzXVMjXdF2iPiT/wbizOrssLh9Ab6VpuVROihLCAu+u25KbZiEYVgsA0W6g0SgjPa/JmGWcUEV/g6cKzEjQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.20.6"
|
||||
|
||||
import-fresh@^3.2.1:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
|
||||
|
@ -1596,6 +1631,13 @@ nanoid@^3.3.4:
|
|||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
|
||||
integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
|
||||
|
||||
node-fetch@2.6.7:
|
||||
version "2.6.7"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
|
||||
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
|
||||
dependencies:
|
||||
whatwg-url "^5.0.0"
|
||||
|
||||
node-releases@^2.0.8:
|
||||
version "2.0.10"
|
||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f"
|
||||
|
@ -1782,6 +1824,14 @@ react-dom@^18.2.0:
|
|||
loose-envify "^1.1.0"
|
||||
scheduler "^0.23.0"
|
||||
|
||||
react-i18next@^12.2.0:
|
||||
version "12.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-12.2.0.tgz#010e3f6070b8d700442947233352ebe4b252d7a1"
|
||||
integrity sha512-5XeVgSygaGfyFmDd2WcXvINRw2WEC1XviW1LXY/xLOEMzsCFRwKqfnHN+hUjla8ZipbVJR27GCMSuTr0BhBBBQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.20.6"
|
||||
html-parse-stringify "^3.0.1"
|
||||
|
||||
react-is@^16.13.1, react-is@^16.8.1:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
|
@ -2089,6 +2139,11 @@ to-regex-range@^5.0.1:
|
|||
dependencies:
|
||||
is-number "^7.0.0"
|
||||
|
||||
tr46@~0.0.3:
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
|
||||
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
|
||||
|
||||
trim-lines@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338"
|
||||
|
@ -2249,11 +2304,29 @@ vite@^4.1.0:
|
|||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
void-elements@3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09"
|
||||
integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==
|
||||
|
||||
web-namespaces@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692"
|
||||
integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==
|
||||
|
||||
webidl-conversions@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
||||
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
|
||||
|
||||
whatwg-url@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
|
||||
integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
|
||||
dependencies:
|
||||
tr46 "~0.0.3"
|
||||
webidl-conversions "^3.0.0"
|
||||
|
||||
xtend@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
||||
|
|
Loading…
Reference in a new issue