mirror of
https://github.com/NovaOSS/nova-betterchat.git
synced 2024-11-25 22:23:59 +01:00
refactor api menu
This commit is contained in:
parent
6222bb58f3
commit
72a89ada1f
|
@ -3,7 +3,6 @@ import useStore from '@store/store';
|
|||
|
||||
import Chat from './components/Chat';
|
||||
import Menu from './components/Menu';
|
||||
import ConfigMenu from './components/ConfigMenu';
|
||||
|
||||
import useInitialiseNewChat from '@hooks/useInitialiseNewChat';
|
||||
|
||||
|
@ -21,7 +20,6 @@ function App() {
|
|||
<div className='overflow-hidden w-full h-full relative'>
|
||||
<Menu />
|
||||
<Chat />
|
||||
<ConfigMenu />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
133
src/components/ApiMenu/ApiMenu.tsx
Normal file
133
src/components/ApiMenu/ApiMenu.tsx
Normal file
|
@ -0,0 +1,133 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import useStore from '@store/store';
|
||||
|
||||
import PopupModal from '@components/PopupModal';
|
||||
import { validateApiKey } from '@api/customApi';
|
||||
|
||||
const ApiMenu = ({
|
||||
isModalOpen,
|
||||
setIsModalOpen,
|
||||
}: {
|
||||
isModalOpen: boolean;
|
||||
setIsModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}) => {
|
||||
const apiKey = useStore((state) => state.apiKey);
|
||||
const setApiKey = useStore((state) => state.setApiKey);
|
||||
const apiFree = useStore((state) => state.apiFree);
|
||||
const setApiFree = useStore((state) => state.setApiFree);
|
||||
|
||||
const [_apiKey, _setApiKey] = useState<string>(apiKey || '');
|
||||
const [error, setError] = useState<boolean>(false);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (apiFree === true) {
|
||||
setIsModalOpen(false);
|
||||
} else {
|
||||
const valid = await validateApiKey(_apiKey);
|
||||
|
||||
if (valid) {
|
||||
setApiKey(_apiKey);
|
||||
setError(false);
|
||||
setIsModalOpen(false);
|
||||
} else {
|
||||
setError(true);
|
||||
setTimeout(() => {
|
||||
setError(false);
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (apiKey) {
|
||||
setApiFree(false);
|
||||
_setApiKey(apiKey);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return isModalOpen ? (
|
||||
<PopupModal
|
||||
title='API'
|
||||
setIsModalOpen={setIsModalOpen}
|
||||
handleConfirm={handleSave}
|
||||
>
|
||||
<div className='p-6 border-b border-gray-200 dark:border-gray-600'>
|
||||
<div className='flex items-center mb-2'>
|
||||
<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)}
|
||||
/>
|
||||
<label className='ml-2 text-sm font-medium text-gray-900 dark:text-gray-300'>
|
||||
Use free API from{' '}
|
||||
<a
|
||||
href='https://github.com/ayaka14732/ChatGPTAPIFree'
|
||||
className='underline dark:hover:text-white hover:text-black'
|
||||
target='_blank'
|
||||
>
|
||||
Ayaka
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center'>
|
||||
<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'
|
||||
onChange={() => setApiFree(false)}
|
||||
/>
|
||||
<label className='ml-2 text-sm font-medium text-gray-900 dark:text-gray-300'>
|
||||
Use your own API key
|
||||
</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
|
||||
</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 p-0 m-0 w-full mr-0 h-8 focus:outline-none'
|
||||
value={_apiKey}
|
||||
onChange={(e) => {
|
||||
_setApiKey(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{error && (
|
||||
<div className='bg-red-600/50 p-2 rounded-sm mt-3 text-gray-900 dark:text-gray-300 text-sm'>
|
||||
Invalid API key!
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<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='underline dark:hover:text-white hover:text-black'
|
||||
href='https://platform.openai.com/account/api-keys'
|
||||
target='_blank'
|
||||
>
|
||||
here
|
||||
</a>
|
||||
</div>
|
||||
<div className='min-w-fit text-gray-900 dark:text-gray-300 text-sm mt-4'>
|
||||
We prioritize 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 unauthorized
|
||||
use.
|
||||
</div>
|
||||
</div>
|
||||
</PopupModal>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiMenu;
|
1
src/components/ApiMenu/index.ts
Normal file
1
src/components/ApiMenu/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from './ApiMenu';
|
|
@ -1,162 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import useStore from '@store/store';
|
||||
|
||||
import CrossIcon2 from '@icon/CrossIcon2';
|
||||
import { validateApiKey } from '@api/customApi';
|
||||
|
||||
const ConfigMenu = () => {
|
||||
const apiKey = useStore((state) => state.apiKey);
|
||||
const setApiKey = useStore((state) => state.setApiKey);
|
||||
const apiFree = useStore((state) => state.apiFree);
|
||||
const setApiFree = useStore((state) => state.setApiFree);
|
||||
const openConfig = useStore((state) => state.openConfig);
|
||||
const setOpenConfig = useStore((state) => state.setOpenConfig);
|
||||
|
||||
const [_apiKey, _setApiKey] = useState<string>(apiKey || '');
|
||||
const [error, setError] = useState<boolean>(false);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (apiFree === true) {
|
||||
setOpenConfig(false);
|
||||
} else {
|
||||
const valid = await validateApiKey(_apiKey);
|
||||
|
||||
if (valid) {
|
||||
setApiKey(_apiKey);
|
||||
setError(false);
|
||||
setOpenConfig(false);
|
||||
} else {
|
||||
setError(true);
|
||||
setTimeout(() => {
|
||||
setError(false);
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setOpenConfig(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (apiKey) {
|
||||
setApiFree(false);
|
||||
_setApiKey(apiKey);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return openConfig ? (
|
||||
<div className='fixed top-0 left-0 z-[999] w-full p-4 overflow-x-hidden overflow-y-auto h-full flex justify-center items-center bg-gray-800/90'>
|
||||
<div className='relative w-full h-full max-w-2xl md:h-auto'>
|
||||
<div className='relative bg-white rounded-lg shadow dark:bg-gray-700'>
|
||||
<div className='flex items-center justify-between p-4 border-b rounded-t dark:border-gray-600'>
|
||||
<h3 className='ml-2 text-lg font-semibold text-gray-900 dark:text-white'>
|
||||
Config
|
||||
</h3>
|
||||
<button
|
||||
type='button'
|
||||
className='text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white'
|
||||
onClick={handleClose}
|
||||
>
|
||||
<CrossIcon2 />
|
||||
</button>
|
||||
</div>
|
||||
<div className='p-6 border-b border-gray-200 dark:border-gray-600'>
|
||||
<div className='flex items-center mb-2'>
|
||||
<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)}
|
||||
/>
|
||||
<label className='ml-2 text-sm font-medium text-gray-900 dark:text-gray-300'>
|
||||
Use free API from{' '}
|
||||
<a
|
||||
href='https://github.com/ayaka14732/ChatGPTAPIFree'
|
||||
className='underline dark:hover:text-white hover:text-black'
|
||||
target='_blank'
|
||||
>
|
||||
Ayaka
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center'>
|
||||
<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'
|
||||
onChange={() => setApiFree(false)}
|
||||
/>
|
||||
<label className='ml-2 text-sm font-medium text-gray-900 dark:text-gray-300'>
|
||||
Use your own API key
|
||||
</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
|
||||
</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 p-0 m-0 w-full mr-0 h-8 focus:outline-none'
|
||||
value={_apiKey}
|
||||
onChange={(e) => {
|
||||
_setApiKey(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{error && (
|
||||
<div className='bg-red-600/50 p-2 rounded-sm mt-3 text-gray-900 dark:text-gray-300 text-sm'>
|
||||
Invalid API key!
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<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='underline dark:hover:text-white hover:text-black'
|
||||
href='https://platform.openai.com/account/api-keys'
|
||||
target='_blank'
|
||||
>
|
||||
here
|
||||
</a>
|
||||
</div>
|
||||
<div className='min-w-fit text-gray-900 dark:text-gray-300 text-sm mt-4'>
|
||||
We prioritize 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 unauthorized use.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center justify-center p-6 gap-4'>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-primary'
|
||||
onClick={handleSave}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-neutral'
|
||||
onClick={handleClose}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfigMenu;
|
|
@ -1 +0,0 @@
|
|||
export { default } from './ConfigMenu';
|
25
src/components/Menu/MenuOptions/Api.tsx
Normal file
25
src/components/Menu/MenuOptions/Api.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import React, { useState } from 'react';
|
||||
import useStore from '@store/store';
|
||||
|
||||
import PersonIcon from '@icon/PersonIcon';
|
||||
import ApiMenu from '@components/ApiMenu';
|
||||
|
||||
const Config = () => {
|
||||
const apiFree = useStore((state) => state.apiFree);
|
||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||||
|
||||
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)}
|
||||
>
|
||||
<PersonIcon />
|
||||
API: {apiFree ? 'Free' : 'Personal'}
|
||||
</a>
|
||||
<ApiMenu isModalOpen={isModalOpen} setIsModalOpen={setIsModalOpen} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Config;
|
|
@ -1,21 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import useStore from '@store/store';
|
||||
|
||||
import PersonIcon from '@icon/PersonIcon';
|
||||
|
||||
const Config = () => {
|
||||
const apiFree = useStore((state) => state.apiFree);
|
||||
const setOpenConfig = useStore((state) => state.setOpenConfig);
|
||||
|
||||
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={() => setOpenConfig(true)}
|
||||
>
|
||||
<PersonIcon />
|
||||
API: {apiFree ? 'Free' : 'Personal'}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export default Config;
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
|
||||
import Account from './Account';
|
||||
import ClearConversation from './ClearConversation';
|
||||
import Config from './Config';
|
||||
import Api from './Api';
|
||||
import Logout from './Logout';
|
||||
import Me from './Me';
|
||||
import ThemeSwitcher from './ThemeSwitcher';
|
||||
|
@ -12,7 +12,7 @@ const MenuOptions = () => {
|
|||
return (
|
||||
<>
|
||||
<ClearConversation />
|
||||
<Config />
|
||||
<Api />
|
||||
<ThemeSwitcher />
|
||||
{/* <Account /> */}
|
||||
<Updates />
|
||||
|
|
Loading…
Reference in a new issue