diff --git a/api/server/routes/endpoints.js b/api/server/routes/endpoints.js index 5d97194989..33be7f95ad 100644 --- a/api/server/routes/endpoints.js +++ b/api/server/routes/endpoints.js @@ -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' } diff --git a/client/src/components/Input/NewConversationMenu/EndpointItem.jsx b/client/src/components/Input/NewConversationMenu/EndpointItem.jsx index 7b1f6eeb0c..fb8d94c5a0 100644 --- a/client/src/components/Input/NewConversationMenu/EndpointItem.jsx +++ b/client/src/components/Input/NewConversationMenu/EndpointItem.jsx @@ -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); diff --git a/client/src/components/Input/NewConversationMenu/FileUpload.jsx b/client/src/components/Input/NewConversationMenu/FileUpload.tsx similarity index 65% rename from client/src/components/Input/NewConversationMenu/FileUpload.jsx rename to client/src/components/Input/NewConversationMenu/FileUpload.tsx index 717c955f76..d221a2e4a5 100644 --- a/client/src/components/Input/NewConversationMenu/FileUpload.jsx +++ b/client/src/components/Input/NewConversationMenu/FileUpload.tsx @@ -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) => void; + className?: string; + successText?: string; + invalidText?: string; + validator?: ((data: any) => boolean) | null; + text?: string; + id?: string; +}; + +const FileUpload: React.FC = ({ 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('text-gray-600'); + const [status, setStatus] = useState(null); - const handleFileChange = (event) => { - const file = event.target.files[0]; + const handleFileChange = (event: React.ChangeEvent): 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} /> diff --git a/client/src/components/Input/SetTokenDialog/GoogleConfig.tsx b/client/src/components/Input/SetTokenDialog/GoogleConfig.tsx new file mode 100644 index 0000000000..d20d5248da --- /dev/null +++ b/client/src/components/Input/SetTokenDialog/GoogleConfig.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import FileUpload from '../NewConversationMenu/FileUpload'; + +const GoogleConfig = ({ setToken } : { setToken: React.Dispatch> }) => { + return ( + { + 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; diff --git a/client/src/components/Input/SetTokenDialog/HelpText.tsx b/client/src/components/Input/SetTokenDialog/HelpText.tsx new file mode 100644 index 0000000000..a9cf6b790f --- /dev/null +++ b/client/src/components/Input/SetTokenDialog/HelpText.tsx @@ -0,0 +1,82 @@ +import React from 'react'; + +function HelpText({ endpoint } : { endpoint: string }) { + const textMap = { + bingAI: ( + + {'To get your Access token for Bing, login to '} + + https://www.bing.com + + {`. Use dev tools or an extension while logged into the site to copy the content of the _U cookie. + If this fails, follow these `} + + instructions + + {' to provide the full cookie strings.'} + + ), + chatGPTBrowser: ( + + {'To get your Access token For ChatGPT \'Free Version\', login to '} + + https://chat.openai.com + + , then visit{' '} + + https://chat.openai.com/api/auth/session + + . Copy access token. + + ), + google: ( + + You need to{' '} + + Enable Vertex AI + {' '} + API on Google Cloud, then{' '} + + Create a Service Account + + {`. Make sure to click 'Create and Continue' to give at least the 'Vertex AI User' role. + Lastly, create a JSON key to import here.`} + + ) + + }; + + return textMap[endpoint] || null; +}; + +export default React.memo(HelpText); \ No newline at end of file diff --git a/client/src/components/Input/SetTokenDialog/InputWithLabel.jsx b/client/src/components/Input/SetTokenDialog/InputWithLabel.tsx similarity index 76% rename from client/src/components/Input/SetTokenDialog/InputWithLabel.jsx rename to client/src/components/Input/SetTokenDialog/InputWithLabel.tsx index fbe8e3e60f..fff1a6f3c0 100644 --- a/client/src/components/Input/SetTokenDialog/InputWithLabel.jsx +++ b/client/src/components/Input/SetTokenDialog/InputWithLabel.tsx @@ -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) => void; + label: string; + id: string; +} + +const InputWithLabel: FC = ({ 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'; diff --git a/client/src/components/Input/SetTokenDialog/OpenAIConfig.tsx b/client/src/components/Input/SetTokenDialog/OpenAIConfig.tsx new file mode 100644 index 0000000000..0c4251e3b7 --- /dev/null +++ b/client/src/components/Input/SetTokenDialog/OpenAIConfig.tsx @@ -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>; + 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 ? ( + <> + setToken(e.target.value || '')} + label={'OpenAI API Key'} + /> + + ) : ( + <> + setAzure('azureOpenAIApiInstanceName', e.target.value || '')} + label={'Azure OpenAI Instance Name'} + /> + + setAzure('azureOpenAIApiDeploymentName', e.target.value || '')} + label={'Azure OpenAI Deployment Name'} + /> + + setAzure('azureOpenAIApiVersion', e.target.value || '')} + label={'Azure OpenAI API Version'} + /> + + setAzure('azureOpenAIApiKey', e.target.value || '')} + label={'Azure OpenAI API Key'} + /> + + )} + { endpoint === 'gptPlugins' && ( +
+ setShowPanel(!showPanel)} + > + + + + + + +
+ )} + + ); +}; + +export default OpenAIConfig; diff --git a/client/src/components/Input/SetTokenDialog/OtherConfig.tsx b/client/src/components/Input/SetTokenDialog/OtherConfig.tsx new file mode 100644 index 0000000000..af0eb7eb0e --- /dev/null +++ b/client/src/components/Input/SetTokenDialog/OtherConfig.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import InputWithLabel from './InputWithLabel'; + +type ConfigProps = { + token: string; + setToken: React.Dispatch>; +}; + +const OtherConfig = ({ token, setToken } : ConfigProps) => { + return ( + ) => setToken(e.target.value || '')} + label={'Token Name'} + /> + ); +}; + +export default OtherConfig; diff --git a/client/src/components/Input/SetTokenDialog/SetTokenDialog.tsx b/client/src/components/Input/SetTokenDialog/SetTokenDialog.tsx new file mode 100644 index 0000000000..91cdc79d46 --- /dev/null +++ b/client/src/components/Input/SetTokenDialog/SetTokenDialog.tsx @@ -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 ( + + + + + Your token will be sent to the server, but not saved. + + + + } + selection={{ + selectHandler: submit, + selectClasses: 'bg-green-600 hover:bg-green-700 dark:hover:bg-green-800 text-white', + selectText: 'Submit' + }} + /> + + ); +}; + +export default SetTokenDialog; diff --git a/client/src/components/Input/SetTokenDialog/index.jsx b/client/src/components/Input/SetTokenDialog/index.jsx deleted file mode 100644 index 16af406832..0000000000 --- a/client/src/components/Input/SetTokenDialog/index.jsx +++ /dev/null @@ -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: ( - - {`To get your Access token for Bing, login to `} - - https://www.bing.com - - {`. Use dev tools or an extension while logged into the site to copy the content of the _U cookie. - If this fails, follow these `} - - instructions - - {` to provide the full cookie strings.`} - - ), - chatGPTBrowser: ( - - {`To get your Access token For ChatGPT 'Free Version', login to `} - - https://chat.openai.com - - , then visit{' '} - - https://chat.openai.com/api/auth/session - - . Copy access token. - - ), - google: ( - - You need to{' '} - - Enable Vertex AI - {' '} - API on Google Cloud, then{' '} - - Create a Service Account - - {`. Make sure to click 'Create and Continue' to give at least the 'Vertex AI User' role. - Lastly, create a JSON key to import here.`} - - ) - }; - - 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 ( - - - {endpoint === 'google' ? ( - { - 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 ? ( - <> - setToken(e.target.value || '')} - label={'OpenAI API Key'} - /> - - ) : ( - <> - setAzure('azureOpenAIApiInstanceName', e.target.value || '')} - label={'Azure OpenAI Instance Name'} - /> - - setAzure('azureOpenAIApiDeploymentName', e.target.value || '')} - label={'Azure OpenAI Deployment Name'} - /> - - setAzure('azureOpenAIApiVersion', e.target.value || '')} - label={'Azure OpenAI API Version'} - /> - - setAzure('azureOpenAIApiKey', e.target.value || '')} - label={'Azure OpenAI API Key'} - /> - - )} -
- setShowPanel(!showPanel)} - > - - - - - - -
- - ) : ( - <> - setToken(e.target.value || '')} - label={'Token Name'} - /> - - )} - - Your token will be sent to the server, but not saved. - - {helpText?.[endpoint]} - - } - selection={{ - selectHandler: submit, - selectClasses: 'bg-green-600 hover:bg-green-700 dark:hover:bg-green-800 text-white', - selectText: 'Submit' - }} - /> -
- ); -}; - -export default SetTokenDialog; diff --git a/client/src/components/Input/SetTokenDialog/index.ts b/client/src/components/Input/SetTokenDialog/index.ts new file mode 100644 index 0000000000..0244f5a168 --- /dev/null +++ b/client/src/components/Input/SetTokenDialog/index.ts @@ -0,0 +1 @@ +export { default as SetTokenDialog } from './SetTokenDialog'; \ No newline at end of file diff --git a/client/src/components/Input/SubmitButton.jsx b/client/src/components/Input/SubmitButton.jsx index f5d9a506ed..6f80277ff8 100644 --- a/client/src/components/Input/SubmitButton.jsx +++ b/client/src/components/Input/SubmitButton.jsx @@ -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, diff --git a/client/src/components/Messages/MessageHeader.jsx b/client/src/components/Messages/MessageHeader.jsx index 007e9873aa..e06b3b1924 100644 --- a/client/src/components/Messages/MessageHeader.jsx +++ b/client/src/components/Messages/MessageHeader.jsx @@ -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') { diff --git a/client/src/components/svg/index.ts b/client/src/components/svg/index.ts index c112267878..a5361ff25f 100644 --- a/client/src/components/svg/index.ts +++ b/client/src/components/svg/index.ts @@ -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'; \ No newline at end of file +export { default as MessagesSquared } from './MessagesSquared'; +export { default as StopGeneratingIcon } from './StopGeneratingIcon'; \ No newline at end of file diff --git a/client/src/utils/index.jsx b/client/src/utils/index.jsx index 1e2e9cf75a..6233e2a4bc 100644 --- a/client/src/utils/index.jsx +++ b/client/src/utils/index.jsx @@ -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' +}; +