refactor(SetTokenDialog): refactor to TS, modularize config logic into separate components

This commit is contained in:
Danny Avila 2023-07-06 11:47:08 -04:00 committed by Danny Avila
parent b6f21af69b
commit 8b91145953
15 changed files with 385 additions and 306 deletions

View file

@ -31,7 +31,7 @@ router.get('/', async function (req, res) {
key = require('../../data/auth.json');
} catch (e) {
if (i === 0) {
console.log("No 'auth.json' file (service account key) found in /api/data/ for PaLM models");
console.log('No \'auth.json\' file (service account key) found in /api/data/ for PaLM models');
i++;
}
}
@ -50,6 +50,7 @@ router.get('/', async function (req, res) {
: false;
const openAIApiKey = process.env.OPENAI_API_KEY;
const azureOpenAIApiKey = process.env.AZURE_API_KEY;
const userProvidedOpenAI = openAIApiKey ? openAIApiKey === 'user_provided' : azureOpenAIApiKey === 'user_provided';
const openAI = openAIApiKey
? { availableModels: getOpenAIModels(), userProvide: openAIApiKey === 'user_provided' }
: false;
@ -57,7 +58,7 @@ router.get('/', async function (req, res) {
? { availableModels: getOpenAIModels({ azure: true}), userProvide: azureOpenAIApiKey === 'user_provided' }
: false;
const gptPlugins = openAIApiKey || azureOpenAIApiKey
? { availableModels: getPluginModels(), availableTools, availableAgents: ['classic', 'functions'] }
? { availableModels: getPluginModels(), availableTools, availableAgents: ['classic', 'functions'], userProvide: userProvidedOpenAI }
: false;
const bingAI = process.env.BINGAI_TOKEN
? { userProvide: process.env.BINGAI_TOKEN == 'user_provided' }

View file

@ -1,21 +1,12 @@
import { useState } from 'react';
import { DropdownMenuRadioItem } from '../../ui/DropdownMenu.tsx';
import { DropdownMenuRadioItem } from '~/components';
import { Settings } from 'lucide-react';
import getIcon from '~/utils/getIcon';
import { useRecoilValue } from 'recoil';
import SetTokenDialog from '../SetTokenDialog';
import { SetTokenDialog } from '../SetTokenDialog';
import store from '../../../store';
import { cn } from '~/utils/index.jsx';
const alternateName = {
openAI: 'OpenAI',
azureOpenAI: 'Azure OpenAI',
bingAI: 'Bing',
chatGPTBrowser: 'ChatGPT',
gptPlugins: 'Plugins',
google: 'PaLM'
};
import store from '~/store';
import { cn, alternateName } from '~/utils';
export default function ModelItem({ endpoint, value, isSelected }) {
const [setTokenDialogOpen, setSetTokenDialogOpen] = useState(false);

View file

@ -1,25 +1,36 @@
import { useState } from 'react';
import React, { useState } from 'react';
import { FileUp } from 'lucide-react';
import { cn } from '~/utils/';
const FileUpload = ({
type FileUploadProps = {
onFileSelected: (event: React.ChangeEvent<HTMLInputElement>) => void;
className?: string;
successText?: string;
invalidText?: string;
validator?: ((data: any) => boolean) | null;
text?: string;
id?: string;
};
const FileUpload: React.FC<FileUploadProps> = ({
onFileSelected,
className = '',
successText = null,
invalidText = null,
validator = null,
text = null,
id = '1'
}) => {
const [statusColor, setStatusColor] = useState('text-gray-600');
const [status, setStatus] = useState(null);
const [statusColor, setStatusColor] = useState<string>('text-gray-600');
const [status, setStatus] = useState<null | string>(null);
const handleFileChange = (event) => {
const file = event.target.files[0];
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
const file = event.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
const jsonData = JSON.parse(e.target.result);
const jsonData = JSON.parse(e.target?.result as string);
if (validator && !validator(jsonData)) {
setStatus('invalid');
setStatusColor('text-red-600');
@ -52,7 +63,7 @@ const FileUpload = ({
id={`file-upload-${id}`}
value=""
type="file"
className="hidden "
className={cn('hidden ', className)}
accept=".json"
onChange={handleFileChange}
/>

View file

@ -0,0 +1,50 @@
import React from 'react';
import FileUpload from '../NewConversationMenu/FileUpload';
const GoogleConfig = ({ setToken } : { setToken: React.Dispatch<React.SetStateAction<string>> }) => {
return (
<FileUpload
id="googleKey"
className="w-full"
text="Import Service Account JSON Key"
successText="Successfully Imported Service Account JSON Key"
invalidText="Invalid Service Account JSON Key, Did you import the correct file?"
validator={(credentials) => {
if (!credentials) {
return false;
}
if (
!credentials.client_email ||
typeof credentials.client_email !== 'string' ||
credentials.client_email.length <= 2
) {
return false;
}
if (
!credentials.project_id ||
typeof credentials.project_id !== 'string' ||
credentials.project_id.length <= 2
) {
return false;
}
if (
!credentials.private_key ||
typeof credentials.private_key !== 'string' ||
credentials.private_key.length <= 600
) {
return false;
}
return true;
}}
onFileSelected={(data) => {
setToken(JSON.stringify(data));
}}
/>
);
};
export default GoogleConfig;

View file

@ -0,0 +1,82 @@
import React from 'react';
function HelpText({ endpoint } : { endpoint: string }) {
const textMap = {
bingAI: (
<small className="break-all text-gray-600">
{'To get your Access token for Bing, login to '}
<a
target="_blank"
href="https://www.bing.com"
rel="noreferrer"
className="text-blue-600 underline"
>
https://www.bing.com
</a>
{`. Use dev tools or an extension while logged into the site to copy the content of the _U cookie.
If this fails, follow these `}
<a
target="_blank"
href="https://github.com/waylaidwanderer/node-chatgpt-api/issues/378#issuecomment-1559868368"
rel="noreferrer"
className="text-blue-600 underline"
>
instructions
</a>
{' to provide the full cookie strings.'}
</small>
),
chatGPTBrowser: (
<small className="break-all text-gray-600">
{'To get your Access token For ChatGPT \'Free Version\', login to '}
<a
target="_blank"
href="https://chat.openai.com"
rel="noreferrer"
className="text-blue-600 underline"
>
https://chat.openai.com
</a>
, then visit{' '}
<a
target="_blank"
href="https://chat.openai.com/api/auth/session"
rel="noreferrer"
className="text-blue-600 underline"
>
https://chat.openai.com/api/auth/session
</a>
. Copy access token.
</small>
),
google: (
<small className="break-all text-gray-600">
You need to{' '}
<a
target="_blank"
href="https://console.cloud.google.com/vertex-ai"
rel="noreferrer"
className="text-blue-600 underline"
>
Enable Vertex AI
</a>{' '}
API on Google Cloud, then{' '}
<a
target="_blank"
href="https://console.cloud.google.com/projectselector/iam-admin/serviceaccounts/create?walkthrough_id=iam--create-service-account#step_index=1"
rel="noreferrer"
className="text-blue-600 underline"
>
Create a Service Account
</a>
{`. Make sure to click 'Create and Continue' to give at least the 'Vertex AI User' role.
Lastly, create a JSON key to import here.`}
</small>
)
};
return textMap[endpoint] || null;
};
export default React.memo(HelpText);

View file

@ -1,9 +1,15 @@
import React from 'react';
import { Input } from '../../ui/Input.tsx';
import { Label } from '../../ui/Label.tsx';
import React, { ChangeEvent, FC } from 'react';
import { Input, Label } from '~/components';
import { cn } from '~/utils/';
function InputWithLabel({ value, onChange, label, id }) {
interface InputWithLabelProps {
value: string;
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
label: string;
id: string;
}
const InputWithLabel: FC<InputWithLabelProps> = ({ value, onChange, label, id }) => {
const defaultTextProps =
'rounded-md border border-gray-300 bg-transparent text-sm shadow-[0_0_10px_rgba(0,0,0,0.10)] outline-none placeholder:text-gray-400 focus:outline-none focus:ring-gray-400 focus:ring-opacity-20 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-400 dark:bg-gray-700 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:border-gray-400 dark:focus:outline-none dark:focus:ring-0 dark:focus:ring-gray-400 dark:focus:ring-offset-0';

View file

@ -0,0 +1,126 @@
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useEffect, useState } from 'react';
import * as Checkbox from '@radix-ui/react-checkbox';
import { CheckIcon } from '@radix-ui/react-icons';
import InputWithLabel from './InputWithLabel';
import store from '~/store';
function isJson(str: string) {
try {
JSON.parse(str);
} catch (e) {
return false;
}
return true;
}
type OpenAIConfigProps = {
token: string;
setToken: React.Dispatch<React.SetStateAction<string>>;
endpoint: string;
};
const OpenAIConfig = ({ token, setToken, endpoint } : OpenAIConfigProps) => {
const [showPanel, setShowPanel] = useState(endpoint === 'azureOpenAI');
const { getToken } = store.useToken(endpoint);
useEffect(() => {
let oldToken = getToken();
if (isJson(token)) {
setShowPanel(true);
}
setToken(oldToken ?? '');
}, []);
useEffect(() => {
if (!showPanel && isJson(token)) {
setToken('');
}
}, [showPanel]);
function getAzure(name: string) {
if (isJson(token)) {
let newToken = JSON.parse(token);
return newToken[name];
} else {
return '';
}
}
function setAzure(name: string, value: any) {
let newToken = {};
if (isJson(token)) {
newToken = JSON.parse(token);
}
newToken[name] = value;
setToken(JSON.stringify(newToken));
}
return (
<>
{!showPanel ? (
<>
<InputWithLabel
id={'chatGPTLabel'}
value={token || ''}
onChange={(e: { target: { value: any; }; }) => setToken(e.target.value || '')}
label={'OpenAI API Key'}
/>
</>
) : (
<>
<InputWithLabel
id={'instanceNameLabel'}
value={getAzure('azureOpenAIApiInstanceName') || ''}
onChange={(e: { target: { value: any; }; }) => setAzure('azureOpenAIApiInstanceName', e.target.value || '')}
label={'Azure OpenAI Instance Name'}
/>
<InputWithLabel
id={'deploymentNameLabel'}
value={getAzure('azureOpenAIApiDeploymentName') || ''}
onChange={(e: { target: { value: any; }; }) => setAzure('azureOpenAIApiDeploymentName', e.target.value || '')}
label={'Azure OpenAI Deployment Name'}
/>
<InputWithLabel
id={'versionLabel'}
value={getAzure('azureOpenAIApiVersion') || ''}
onChange={(e: { target: { value: any; }; }) => setAzure('azureOpenAIApiVersion', e.target.value || '')}
label={'Azure OpenAI API Version'}
/>
<InputWithLabel
id={'apiKeyLabel'}
value={getAzure('azureOpenAIApiKey') || ''}
onChange={(e: { target: { value: any; }; }) => setAzure('azureOpenAIApiKey', e.target.value || '')}
label={'Azure OpenAI API Key'}
/>
</>
)}
{ endpoint === 'gptPlugins' && (
<div className="flex items-center">
<Checkbox.Root
className="flex h-[20px] w-[20px] appearance-none items-center justify-center rounded-[4px] bg-gray-100 text-white outline-none hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-900"
id="azureOpenAI"
checked={showPanel}
onCheckedChange={() => setShowPanel(!showPanel)}
>
<Checkbox.Indicator className="flex h-[20px] w-[20px] items-center justify-center rounded-[3.5px] bg-green-600">
<CheckIcon />
</Checkbox.Indicator>
</Checkbox.Root>
<label
className="pl-[8px] text-[15px] leading-none dark:text-white"
htmlFor="azureOpenAI"
>
Use Azure OpenAI.
</label>
</div>
)}
</>
);
};
export default OpenAIConfig;

View file

@ -0,0 +1,20 @@
import React from 'react';
import InputWithLabel from './InputWithLabel';
type ConfigProps = {
token: string;
setToken: React.Dispatch<React.SetStateAction<string>>;
};
const OtherConfig = ({ token, setToken } : ConfigProps) => {
return (
<InputWithLabel
id={'chatGPTLabel'}
value={token || ''}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setToken(e.target.value || '')}
label={'Token Name'}
/>
);
};
export default OtherConfig;

View file

@ -0,0 +1,52 @@
import React, { useState }from 'react';
import HelpText from './HelpText';
import GoogleConfig from './GoogleConfig';
import OpenAIConfig from './OpenAIConfig';
import OtherConfig from './OtherConfig';
import { Dialog, DialogTemplate } from '~/components';
import { alternateName } from '~/utils';
import store from '~/store';
const SetTokenDialog = ({ open, onOpenChange, endpoint }) => {
const [token, setToken] = useState('');
const { saveToken } = store.useToken(endpoint);
const submit = () => {
saveToken(token);
onOpenChange(false);
};
const endpointComponents = {
'google': GoogleConfig,
'openAI': OpenAIConfig,
'azureOpenAI': OpenAIConfig,
'gptPlugins': OpenAIConfig,
'default': OtherConfig
};
const EndpointComponent = endpointComponents[endpoint] || endpointComponents['default'];
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogTemplate
title={`Set Token for ${alternateName[endpoint] ?? endpoint}`}
main={
<div className="grid w-full items-center gap-2">
<EndpointComponent token={token} setToken={setToken} endpoint={endpoint}/>
<small className="text-red-600">
Your token will be sent to the server, but not saved.
</small>
<HelpText endpoint={endpoint}/>
</div>
}
selection={{
selectHandler: submit,
selectClasses: 'bg-green-600 hover:bg-green-700 dark:hover:bg-green-800 text-white',
selectText: 'Submit'
}}
/>
</Dialog>
);
};
export default SetTokenDialog;

View file

@ -1,272 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { useEffect, useState } from 'react';
import { Dialog, DialogTemplate } from '../../ui';
import * as Checkbox from '@radix-ui/react-checkbox';
import { CheckIcon } from '@radix-ui/react-icons';
import FileUpload from '../NewConversationMenu/FileUpload';
import store from '~/store';
import InputWithLabel from './InputWithLabel';
function isJson(str) {
try {
JSON.parse(str);
} catch (e) {
return false;
}
return true;
}
const SetTokenDialog = ({ open, onOpenChange, endpoint }) => {
const [token, setToken] = useState('');
const [showPanel, setShowPanel] = useState(false);
const { getToken, saveToken } = store.useToken(endpoint);
const submit = () => {
saveToken(token);
onOpenChange(false);
};
useEffect(() => {
let oldToken = getToken();
if (isJson(token)) {
setShowPanel(true);
}
setToken(oldToken ?? '');
}, [open]);
useEffect(() => {
if (!showPanel && isJson(token)) {
setToken('');
}
}, [showPanel]);
const helpText = {
bingAI: (
<small className="break-all text-gray-600">
{`To get your Access token for Bing, login to `}
<a
target="_blank"
href="https://www.bing.com"
rel="noreferrer"
className="text-blue-600 underline"
>
https://www.bing.com
</a>
{`. Use dev tools or an extension while logged into the site to copy the content of the _U cookie.
If this fails, follow these `}
<a
target="_blank"
href="https://github.com/waylaidwanderer/node-chatgpt-api/issues/378#issuecomment-1559868368"
rel="noreferrer"
className="text-blue-600 underline"
>
instructions
</a>
{` to provide the full cookie strings.`}
</small>
),
chatGPTBrowser: (
<small className="break-all text-gray-600">
{`To get your Access token For ChatGPT 'Free Version', login to `}
<a
target="_blank"
href="https://chat.openai.com"
rel="noreferrer"
className="text-blue-600 underline"
>
https://chat.openai.com
</a>
, then visit{' '}
<a
target="_blank"
href="https://chat.openai.com/api/auth/session"
rel="noreferrer"
className="text-blue-600 underline"
>
https://chat.openai.com/api/auth/session
</a>
. Copy access token.
</small>
),
google: (
<small className="break-all text-gray-600">
You need to{' '}
<a
target="_blank"
href="https://console.cloud.google.com/vertex-ai"
rel="noreferrer"
className="text-blue-600 underline"
>
Enable Vertex AI
</a>{' '}
API on Google Cloud, then{' '}
<a
target="_blank"
href="https://console.cloud.google.com/projectselector/iam-admin/serviceaccounts/create?walkthrough_id=iam--create-service-account#step_index=1"
rel="noreferrer"
className="text-blue-600 underline"
>
Create a Service Account
</a>
{`. Make sure to click 'Create and Continue' to give at least the 'Vertex AI User' role.
Lastly, create a JSON key to import here.`}
</small>
)
};
function getAzure(name) {
if (isJson(token)) {
let newToken = JSON.parse(token);
return newToken[name];
} else {
return '';
}
}
function setAzure(name, value) {
let newToken = {};
if (isJson(token)) {
newToken = JSON.parse(token);
}
newToken[name] = value;
setToken(JSON.stringify(newToken));
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogTemplate
title={`Set Token of ${endpoint}`}
main={
<div className="grid w-full items-center gap-2">
{endpoint === 'google' ? (
<FileUpload
id="googleKey"
className="w-full"
text="Import Service Account JSON Key"
successText="Successfully Imported Service Account JSON Key"
invalidText="Invalid Service Account JSON Key, Did you import the correct file?"
validator={(credentials) => {
if (!credentials) {
return false;
}
if (
!credentials.client_email ||
typeof credentials.client_email !== 'string' ||
credentials.client_email.length <= 2
) {
return false;
}
if (
!credentials.project_id ||
typeof credentials.project_id !== 'string' ||
credentials.project_id.length <= 2
) {
return false;
}
if (
!credentials.private_key ||
typeof credentials.private_key !== 'string' ||
credentials.private_key.length <= 600
) {
return false;
}
return true;
}}
onFileSelected={(data) => {
setToken(JSON.stringify(data));
}}
/>
) : endpoint === 'openAI' || endpoint === 'azureOpenAI' ? (
<>
{!showPanel ? (
<>
<InputWithLabel
id={'chatGPTLabel'}
value={token || ''}
onChange={(e) => setToken(e.target.value || '')}
label={'OpenAI API Key'}
/>
</>
) : (
<>
<InputWithLabel
id={'instanceNameLabel'}
value={getAzure('azureOpenAIApiInstanceName') || ''}
onChange={(e) => setAzure('azureOpenAIApiInstanceName', e.target.value || '')}
label={'Azure OpenAI Instance Name'}
/>
<InputWithLabel
id={'deploymentNameLabel'}
value={getAzure('azureOpenAIApiDeploymentName') || ''}
onChange={(e) => setAzure('azureOpenAIApiDeploymentName', e.target.value || '')}
label={'Azure OpenAI Deployment Name'}
/>
<InputWithLabel
id={'versionLabel'}
value={getAzure('azureOpenAIApiVersion') || ''}
onChange={(e) => setAzure('azureOpenAIApiVersion', e.target.value || '')}
label={'Azure OpenAI API Version'}
/>
<InputWithLabel
id={'apiKeyLabel'}
value={getAzure('azureOpenAIApiKey') || ''}
onChange={(e) => setAzure('azureOpenAIApiKey', e.target.value || '')}
label={'Azure OpenAI API Key'}
/>
</>
)}
<div className="flex items-center">
<Checkbox.Root
className="flex h-[20px] w-[20px] appearance-none items-center justify-center rounded-[4px] bg-gray-100 text-white outline-none hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-900"
id="azureOpenAI"
checked={showPanel && endpoint === 'azureOpenAI'}
onCheckedChange={() => setShowPanel(!showPanel)}
>
<Checkbox.Indicator className="flex h-[20px] w-[20px] items-center justify-center rounded-[3.5px] bg-green-600">
<CheckIcon />
</Checkbox.Indicator>
</Checkbox.Root>
<label
className="pl-[8px] text-[15px] leading-none dark:text-white"
htmlFor="azureOpenAI"
>
Use Azure OpenAI.
</label>
</div>
</>
) : (
<>
<InputWithLabel
id={'chatGPTLabel'}
value={token || ''}
onChange={(e) => setToken(e.target.value || '')}
label={'Token Name'}
/>
</>
)}
<small className="text-red-600">
Your token will be sent to the server, but not saved.
</small>
{helpText?.[endpoint]}
</div>
}
selection={{
selectHandler: submit,
selectClasses: 'bg-green-600 hover:bg-green-700 dark:hover:bg-green-800 text-white',
selectText: 'Submit'
}}
/>
</Dialog>
);
};
export default SetTokenDialog;

View file

@ -0,0 +1 @@
export { default as SetTokenDialog } from './SetTokenDialog';

View file

@ -1,8 +1,8 @@
import React, { useState } from 'react';
import StopGeneratingIcon from '../svg/StopGeneratingIcon';
import { StopGeneratingIcon } from '~/components';
import { Settings } from 'lucide-react';
import SetTokenDialog from './SetTokenDialog';
import store from '../../store';
import { SetTokenDialog } from './SetTokenDialog';
import store from '~/store';
export default function SubmitButton({
endpoint,

View file

@ -2,7 +2,7 @@ import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import { Plugin } from '~/components/svg';
import EndpointOptionsDialog from '../Endpoints/EndpointOptionsDialog';
import { cn } from '~/utils/';
import { cn, alternateName } from '~/utils/';
import store from '~/store';
@ -28,7 +28,7 @@ const MessageHeader = ({ isSearchView = false }) => {
const getConversationTitle = () => {
if (isSearchView) return `Search: ${searchQuery}`;
else {
let _title = `${endpoint}`;
let _title = `${alternateName[endpoint] ?? endpoint}`;
if (endpoint === 'azureOpenAI' || endpoint === 'openAI') {
const { chatGptLabel } = conversation;
@ -42,7 +42,7 @@ const MessageHeader = ({ isSearchView = false }) => {
} else if (endpoint === 'bingAI') {
const { jailbreak, toneStyle } = conversation;
if (toneStyle) _title += `: ${toneStyle}`;
if (jailbreak) _title += ` as Sydney`;
if (jailbreak) _title += ' as Sydney';
} else if (endpoint === 'chatGPTBrowser') {
if (model) _title += `: ${model}`;
} else if (endpoint === 'gptPlugins') {

View file

@ -3,4 +3,5 @@ export { default as GPTIcon } from './GPTIcon';
export { default as BingIcon } from './BingIcon';
export { default as CogIcon } from './CogIcon';
export { default as Spinner } from './Spinner';
export { default as MessagesSquared } from './MessagesSquared';
export { default as MessagesSquared } from './MessagesSquared';
export { default as StopGeneratingIcon } from './StopGeneratingIcon';

View file

@ -34,3 +34,13 @@ export const languages = [
'perl',
'pascal'
];
export const alternateName = {
openAI: 'OpenAI',
azureOpenAI: 'Azure OpenAI',
bingAI: 'Bing',
chatGPTBrowser: 'ChatGPT',
gptPlugins: 'Plugins',
google: 'PaLM'
};