refactor: Encrypt & Expire User Provided Keys, feat: Rate Limiting (#874)

* docs: make_your_own.md formatting fix for mkdocs

* feat: add express-mongo-sanitize
feat: add login/registration rate limiting

* chore: remove unnecessary console log

* wip: remove token handling from localStorage to encrypted DB solution

* refactor: minor change to UserService

* fix mongo query and add keys route to server

* fix backend controllers and simplify schema/crud

* refactor: rename token to key to separate from access/refresh tokens, setTokenDialog -> setKeyDialog

* refactor(schemas): TEndpointOption token -> key

* refactor(api): use new encrypted key retrieval system

* fix(SetKeyDialog): fix key prop error

* fix(abortMiddleware): pass random UUID if messageId is not generated yet for proper error display on frontend

* fix(getUserKey): wrong prop passed in arg, adds error handling

* fix: prevent message without conversationId from saving to DB, prevents branching on the frontend to a new top-level branch

* refactor: change wording of multiple display messages

* refactor(checkExpiry -> checkUserKeyExpiry): move to UserService file

* fix: type imports from common

* refactor(SubmitButton): convert to TS

* refactor(key.ts): change localStorage map key name

* refactor: add new custom tailwind classes to better match openAI colors

* chore: remove unnecessary warning and catch ScreenShot error

* refactor: move userKey frontend logic to hooks and remove use of localStorage and instead query the DB

* refactor: invalidate correct query key, memoize userKey hook, conditionally render SetKeyDialog to avoid unnecessary calls, refactor SubmitButton props and useEffect for showing 'provide key first'

* fix(SetKeyDialog): use enum-like object for expiry values
feat(Dropdown): add optionsClassName to dynamically change dropdown options container classes

* fix: handle edge case where user had provided a key but the server changes to env variable for keys

* refactor(OpenAI/titleConvo): move titling to client to retain authorized credentials in message lifecycle for titling

* fix(azure): handle user_provided keys correctly for azure

* feat: send user Id to OpenAI to differentiate users in completion requests

* refactor(OpenAI/titleConvo): adding tokens helps minimize LLM from using the language in title response

* feat: add delete endpoint for keys

* chore: remove throttling of title

* feat: add 'Data controls' to Settings, add 'Revoke' keys feature in Key Dialog and Data controls

* refactor: reorganize PluginsClient files in langchain format

* feat: use langchain for titling convos

* chore: cleanup titling convo, with fallback to original method, escape braces, use only snippet for language detection

* refactor: move helper functions to appropriate langchain folders for reusability

* fix: userProvidesKey handling for gptPlugins

* fix: frontend handling of plugins key

* chore: cleanup logging and ts-ignore SSE

* fix: forwardRef misuse in DangerButton

* fix(GoogleConfig/FileUpload): localize errors and simplify validation with zod

* fix: cleanup google logging and fix user provided key handling

* chore: remove titling from google

* chore: removing logging from browser endpoint

* wip: fix menu flicker

* feat: useLocalStorage hook

* feat: add Tooltip for UI

* refactor(EndpointMenu): utilize Tooltip and useLocalStorage, remove old 'New Chat' slide-over

* fix(e2e): use testId for endpoint menu trigger

* chore: final touches to EndpointMenu before future refactor to declutter component

* refactor(localization): change select endpoint to open menu and add translations

* chore: add final prop to error message response

* ci: minor edits to facilitate testing

* ci: new e2e test which tests for new key setting/revoking features
This commit is contained in:
Danny Avila 2023-09-06 10:46:27 -04:00 committed by GitHub
parent 64f1557852
commit 4ca43fb53d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
122 changed files with 1933 additions and 966 deletions

View file

@ -1,4 +1,4 @@
import { TConversation, TMessage, TPreset } from 'librechat-data-provider';
import type { TConversation, TMessage, TPreset, TMutation } from 'librechat-data-provider';
export type TSetOption = (param: number | string) => (newValue: number | string | boolean) => void;
export type TSetExample = (
@ -120,3 +120,29 @@ export type TDisplayProps = TText &
Pick<TAdditionalProps, 'isCreatedByUser' | 'message'> & {
showCursor?: boolean;
};
export type TConfigProps = {
userKey: string;
setUserKey: React.Dispatch<React.SetStateAction<string>>;
endpoint: string;
};
export type TDangerButtonProps = {
id: string;
confirmClear: boolean;
className?: string;
disabled?: boolean;
showText?: boolean;
mutation?: TMutation;
onClick: () => void;
infoTextCode: string;
actionTextCode: string;
dataTestIdInitial: string;
dataTestIdConfirm: string;
confirmActionTextCode?: string;
};
export type TDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
};

View file

@ -30,7 +30,9 @@ function Login() {
className="relative mt-4 rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700"
role="alert"
>
{localize('com_auth_error_login')}
{error?.includes('429')
? localize('com_auth_error_login_rl')
: localize('com_auth_error_login')}
</div>
)}
<LoginForm onSubmit={login} />

View file

@ -3,7 +3,7 @@ import { useEffect } from 'react';
import filenamify from 'filenamify';
import exportFromJSON from 'export-from-json';
import { useSetRecoilState, useRecoilState, useRecoilValue } from 'recoil';
import { TEditPresetProps } from '~/common';
import type { TEditPresetProps } from '~/common';
import { useSetOptions, useLocalize } from '~/hooks';
import { Input, Label, Dropdown, Dialog, DialogClose, DialogButton } from '~/components/';
import DialogTemplate from '~/components/ui/DialogTemplate';

View file

@ -1,4 +1,5 @@
import { TModelSelectProps, ESide } from '~/common';
import type { TModelSelectProps } from '~/common';
import { ESide } from '~/common';
import {
Switch,
SelectDropDown,

View file

@ -1,6 +1,7 @@
import React from 'react';
import TextareaAutosize from 'react-textarea-autosize';
import { ESide, TModelSelectProps } from '~/common';
import type { TModelSelectProps } from '~/common';
import { ESide } from '~/common';
import {
Input,
Label,

View file

@ -1,6 +1,7 @@
import React from 'react';
import TextareaAutosize from 'react-textarea-autosize';
import { ESide, TModelSelectProps } from '~/common';
import type { TModelSelectProps } from '~/common';
import { ESide } from '~/common';
import {
SelectDropDown,
Input,

View file

@ -1,5 +1,6 @@
import TextareaAutosize from 'react-textarea-autosize';
import { ESide, TModelSelectProps } from '~/common';
import type { TModelSelectProps } from '~/common';
import { ESide } from '~/common';
import {
SelectDropDown,
Input,

View file

@ -9,7 +9,8 @@ import {
HoverCardTrigger,
} from '~/components';
import OptionHover from './OptionHover';
import { ESide, TModelSelectProps } from '~/common';
import type { TModelSelectProps } from '~/common';
import { ESide } from '~/common';
import { cn, defaultTextProps, optionText, removeFocusOutlines } from '~/utils/';
import { useLocalize } from '~/hooks';

View file

@ -3,7 +3,7 @@ import { useRecoilValue } from 'recoil';
import { Settings } from 'lucide-react';
import { DropdownMenuRadioItem } from '~/components';
import { getIcon } from '~/components/Endpoints';
import { SetTokenDialog } from '../SetTokenDialog';
import { SetKeyDialog } from '../SetKeyDialog';
import { useLocalize } from '~/hooks';
import store from '~/store';
@ -18,7 +18,7 @@ export default function ModelItem({
value: string;
isSelected: boolean;
}) {
const [setTokenDialogOpen, setSetTokenDialogOpen] = useState(false);
const [isDialogOpen, setDialogOpen] = useState(false);
const endpointsConfig = useRecoilValue(store.endpointsConfig);
const icon = getIcon({
@ -29,7 +29,7 @@ export default function ModelItem({
message: false,
});
const isUserProvided = endpointsConfig?.[endpoint]?.userProvide;
const userProvidesKey = endpointsConfig?.[endpoint]?.userProvide;
const localize = useLocalize();
// regular model
@ -52,7 +52,7 @@ export default function ModelItem({
</span>
)}
<div className="flex w-4 flex-1" />
{isUserProvided ? (
{userProvidesKey ? (
<button
className={cn(
'invisible m-0 mr-1 flex-initial rounded-md p-0 text-xs font-medium text-gray-400 hover:text-gray-700 group-hover:visible dark:font-normal dark:text-gray-400 dark:hover:text-gray-200',
@ -60,19 +60,17 @@ export default function ModelItem({
)}
onClick={(e) => {
e.preventDefault();
setSetTokenDialogOpen(true);
setDialogOpen(true);
}}
>
<Settings className="mr-1 inline-block w-[16px] items-center stroke-1" />
{localize('com_endpoint_config_token')}
{localize('com_endpoint_config_key')}
</button>
) : null}
</DropdownMenuRadioItem>
<SetTokenDialog
open={setTokenDialogOpen}
onOpenChange={setSetTokenDialogOpen}
endpoint={endpoint}
/>
{userProvidesKey && (
<SetKeyDialog open={isDialogOpen} onOpenChange={setDialogOpen} endpoint={endpoint} />
)}
</>
);
}

View file

@ -17,20 +17,24 @@ import {
DropdownMenuTrigger,
Dialog,
DialogTrigger,
TooltipProvider,
Tooltip,
TooltipTrigger,
TooltipContent,
} from '~/components/ui/';
import DialogTemplate from '~/components/ui/DialogTemplate';
import { cn, cleanupPreset, getDefaultConversation } from '~/utils';
import { useLocalize } from '~/hooks';
import { useLocalize, useLocalStorage } from '~/hooks';
import store from '~/store';
export default function NewConversationMenu() {
const localize = useLocalize();
const [menuOpen, setMenuOpen] = useState(false);
const [showPresets, setShowPresets] = useState(true);
const [showEndpoints, setShowEndpoints] = useState(true);
const [presetModelVisible, setPresetModelVisible] = useState(false);
const [preset, setPreset] = useState(false);
const [conversation, setConversation] = useRecoilState(store.conversation) || {};
const [conversation, setConversation] = useRecoilState(store.conversation) ?? {};
const [messages, setMessages] = useRecoilState(store.messages);
const availableEndpoints = useRecoilValue(store.availableEndpoints);
const endpointsConfig = useRecoilValue(store.endpointsConfig);
@ -71,24 +75,17 @@ export default function NewConversationMenu() {
}
}, [availableEndpoints]);
// save selected model to localStorage
// save states to localStorage
const [newUser, setNewUser] = useLocalStorage('newUser', true);
const [lastModel, setLastModel] = useLocalStorage('lastSelectedModel', {});
const setLastConvo = useLocalStorage('lastConversationSetup', {})[1];
const [lastBingSettings, setLastBingSettings] = useLocalStorage('lastBingSettings', {});
useEffect(() => {
if (endpoint) {
const lastSelectedModel = JSON.parse(localStorage.getItem('lastSelectedModel')) || {};
localStorage.setItem(
'lastSelectedModel',
JSON.stringify({ ...lastSelectedModel, [endpoint]: conversation.model }),
);
localStorage.setItem('lastConversationSetup', JSON.stringify(conversation));
}
if (endpoint === 'bingAI') {
const lastBingSettings = JSON.parse(localStorage.getItem('lastBingSettings')) || {};
if (endpoint && endpoint !== 'bingAI') {
setLastModel({ ...lastModel, [endpoint]: conversation?.model }), setLastConvo(conversation);
} else if (endpoint === 'bingAI') {
const { jailbreak, toneStyle } = conversation;
localStorage.setItem(
'lastBingSettings',
JSON.stringify({ ...lastBingSettings, jailbreak, toneStyle }),
);
setLastBingSettings({ ...lastBingSettings, jailbreak, toneStyle });
}
}, [conversation]);
@ -150,124 +147,131 @@ export default function NewConversationMenu() {
button: true,
});
const localize = useLocalize();
const onOpenChange = (open) => {
setMenuOpen(open);
if (newUser) {
setNewUser(false);
}
};
return (
<Dialog className="z-[100]">
<DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>
<DropdownMenuTrigger asChild>
<Button
id="new-conversation-menu"
data-testid="new-conversation-menu"
variant="outline"
className={
'group relative mb-[-12px] ml-1 mt-[-8px] items-center rounded-md border-0 p-1 outline-none focus:ring-0 focus:ring-offset-0 dark:data-[state=open]:bg-opacity-50 md:left-1 md:ml-0 md:ml-[-12px] md:pl-1'
}
>
{icon}
<span className="max-w-0 overflow-hidden whitespace-nowrap px-0 text-slate-600 transition-all group-data-[state=open]:max-w-[80px] group-data-[state=open]:px-2 dark:text-slate-300">
{localize('com_endpoint_new_topic')}
</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="z-[100] w-[375px] dark:bg-gray-900 md:w-96"
onCloseAutoFocus={(event) => event.preventDefault()}
>
<DropdownMenuLabel
className="cursor-pointer dark:text-gray-300"
onClick={() => setShowEndpoints((prev) => !prev)}
>
{showEndpoints ? localize('com_endpoint_hide') : localize('com_endpoint_show')}{' '}
{localize('com_endpoint')}
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup
value={endpoint}
onValueChange={onSelectEndpoint}
className="flex flex-col gap-1 overflow-y-auto"
>
{showEndpoints &&
(availableEndpoints.length ? (
<EndpointItems
selectedEndpoint={endpoint}
endpoints={availableEndpoints}
onSelect={onSelectEndpoint}
/>
) : (
<DropdownMenuLabel className="dark:text-gray-300">
{localize('com_endpoint_not_available')}
</DropdownMenuLabel>
))}
</DropdownMenuRadioGroup>
<div className="mt-2 w-full" />
<DropdownMenuLabel className="flex items-center dark:text-gray-300">
<span
className="mr-auto cursor-pointer "
onClick={() => setShowPresets((prev) => !prev)}
>
{showPresets ? localize('com_endpoint_hide') : localize('com_endpoint_show')}{' '}
{localize('com_endpoint_presets')}
</span>
<FileUpload onFileSelected={onFileSelected} />
<Dialog>
<DialogTrigger asChild>
<label
htmlFor="file-upload"
className="mr-1 flex h-[32px] h-auto cursor-pointer items-center rounded bg-transparent px-2 py-1 text-xs font-medium font-normal text-gray-600 transition-colors hover:bg-slate-200 hover:text-red-700 dark:bg-transparent dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-green-500"
<TooltipProvider delayDuration={300}>
<Tooltip>
<Dialog className="z-[100]">
<DropdownMenu open={menuOpen} onOpenChange={onOpenChange}>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button
id="new-conversation-menu"
data-testid="new-conversation-menu"
variant="outline"
className={
'group relative mb-[-12px] ml-1 mt-[-8px] items-center rounded-md border-0 p-1 outline-none focus:ring-0 focus:ring-offset-0 dark:data-[state=open]:bg-opacity-50 md:left-1 md:ml-0 md:ml-[-12px] md:pl-1'
}
>
{/* <Button
type="button"
className="h-auto bg-transparent px-2 py-1 text-xs font-medium font-normal text-red-700 hover:bg-slate-200 hover:text-red-700 dark:bg-transparent dark:text-red-400 dark:hover:bg-gray-800 dark:hover:text-red-400"
> */}
<Trash2 className="mr-1 flex w-[22px] items-center stroke-1" />
{localize('com_ui_clear')} {localize('com_ui_all')}
{/* </Button> */}
</label>
</DialogTrigger>
<DialogTemplate
title={`${localize('com_ui_clear')} ${localize('com_endpoint_presets')}`}
description={localize('com_endpoint_presets_clear_warning')}
selection={{
selectHandler: clearAllPresets,
selectClasses: 'bg-red-600 hover:bg-red-700 dark:hover:bg-red-800 text-white',
selectText: localize('com_ui_clear'),
}}
className="max-w-[500px]"
/>
</Dialog>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup
onValueChange={onSelectPreset}
className={cn(
'overflow-y-auto overflow-x-hidden',
showEndpoints ? 'max-h-[210px]' : 'max-h-[315px]',
)}
>
{showPresets &&
(presets.length ? (
<PresetItems
presets={presets}
onSelect={onSelectPreset}
onChangePreset={onChangePreset}
onDeletePreset={onDeletePreset}
/>
) : (
<DropdownMenuLabel className="dark:text-gray-300">
{localize('com_endpoint_no_presets')}
</DropdownMenuLabel>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<EditPresetDialog
open={presetModelVisible}
onOpenChange={setPresetModelVisible}
preset={preset}
/>
</Dialog>
{icon}
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent forceMount={newUser} sideOffset={5}>
{localize('com_endpoint_open_menu')}
</TooltipContent>
<DropdownMenuContent
className="z-[100] w-[375px] dark:bg-gray-900 md:w-96"
onCloseAutoFocus={(event) => event.preventDefault()}
side="top"
>
<DropdownMenuLabel
className="cursor-pointer dark:text-gray-300"
onClick={() => setShowEndpoints((prev) => !prev)}
>
{showEndpoints ? localize('com_endpoint_hide') : localize('com_endpoint_show')}{' '}
{localize('com_endpoint')}
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup
value={endpoint}
onValueChange={onSelectEndpoint}
className="flex flex-col gap-1 overflow-y-auto"
>
{showEndpoints &&
(availableEndpoints.length ? (
<EndpointItems
selectedEndpoint={endpoint}
endpoints={availableEndpoints}
onSelect={onSelectEndpoint}
/>
) : (
<DropdownMenuLabel className="dark:text-gray-300">
{localize('com_endpoint_not_available')}
</DropdownMenuLabel>
))}
</DropdownMenuRadioGroup>
<div className="mt-2 w-full" />
<DropdownMenuLabel className="flex items-center dark:text-gray-300">
<span
className="mr-auto cursor-pointer "
onClick={() => setShowPresets((prev) => !prev)}
>
{showPresets ? localize('com_endpoint_hide') : localize('com_endpoint_show')}{' '}
{localize('com_endpoint_presets')}
</span>
<FileUpload onFileSelected={onFileSelected} />
<Dialog>
<DialogTrigger asChild>
<label
htmlFor="file-upload"
className="mr-1 flex h-[32px] h-auto cursor-pointer items-center rounded bg-transparent px-2 py-1 text-xs font-medium font-normal text-gray-600 transition-colors hover:bg-slate-200 hover:text-red-700 dark:bg-transparent dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-green-500"
>
<Trash2 className="mr-1 flex w-[22px] items-center stroke-1" />
{localize('com_ui_clear')} {localize('com_ui_all')}
</label>
</DialogTrigger>
<DialogTemplate
title={`${localize('com_ui_clear')} ${localize('com_endpoint_presets')}`}
description={localize('com_endpoint_presets_clear_warning')}
selection={{
selectHandler: clearAllPresets,
selectClasses: 'bg-red-600 hover:bg-red-700 dark:hover:bg-red-800 text-white',
selectText: localize('com_ui_clear'),
}}
className="max-w-[500px]"
/>
</Dialog>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup
onValueChange={onSelectPreset}
className={cn(
'overflow-y-auto overflow-x-hidden',
showEndpoints ? 'max-h-[210px]' : 'max-h-[315px]',
)}
>
{showPresets &&
(presets.length ? (
<PresetItems
presets={presets}
onSelect={onSelectPreset}
onChangePreset={onChangePreset}
onDeletePreset={onDeletePreset}
/>
) : (
<DropdownMenuLabel className="dark:text-gray-300">
{localize('com_endpoint_no_presets')}
</DropdownMenuLabel>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<EditPresetDialog
open={presetModelVisible}
onOpenChange={setPresetModelVisible}
preset={preset}
/>
</Dialog>
</Tooltip>
</TooltipProvider>
);
}

View file

@ -51,6 +51,15 @@ const FileUpload: React.FC<FileUploadProps> = ({
reader.readAsText(file);
};
let statusText: string;
if (!status) {
statusText = text ?? localize('com_endpoint_import');
} else if (status === 'success') {
statusText = successText ?? localize('com_ui_upload_success');
} else {
statusText = invalidText ?? localize('com_ui_upload_invalid');
}
return (
<label
htmlFor={`file-upload-${id}`}
@ -60,13 +69,7 @@ const FileUpload: React.FC<FileUploadProps> = ({
)}
>
<FileUp className="mr-1 flex w-[22px] items-center stroke-1" />
<span className="flex text-xs ">
{!status
? text || localize('com_endpoint_import')
: status === localize('com_ui_succes')
? successText
: invalidText}
</span>
<span className="flex text-xs ">{statusText}</span>
<input
id={`file-upload-${id}`}
value=""

View file

@ -0,0 +1,35 @@
import React from 'react';
import { object, string } from 'zod';
import type { TConfigProps } from '~/common';
import FileUpload from '../EndpointMenu/FileUpload';
import { useLocalize } from '~/hooks';
const CredentialsSchema = object({
client_email: string().email().min(3),
project_id: string().min(3),
private_key: string().min(601),
});
const validateCredentials = (credentials: Record<string, unknown>) => {
const result = CredentialsSchema.safeParse(credentials);
return result.success;
};
const GoogleConfig = ({ setUserKey }: Pick<TConfigProps, 'setUserKey'>) => {
const localize = useLocalize();
return (
<FileUpload
id="googleKey"
className="w-full"
text={localize('com_endpoint_config_key_import_json_key')}
successText={localize('com_endpoint_config_key_import_json_key_success')}
invalidText={localize('com_endpoint_config_key_import_json_key_invalid')}
validator={validateCredentials}
onFileSelected={(data) => {
setUserKey(JSON.stringify(data));
}}
/>
);
};
export default GoogleConfig;

View file

@ -6,7 +6,7 @@ function HelpText({ endpoint }: { endpoint: string }) {
const textMap = {
bingAI: (
<small className="break-all text-gray-600">
{localize('com_endpoint_config_token_get_edge_key')}{' '}
{localize('com_endpoint_config_key_get_edge_key')}{' '}
<a
target="_blank"
href="https://www.bing.com"
@ -16,21 +16,21 @@ function HelpText({ endpoint }: { endpoint: string }) {
https://www.bing.com
</a>
{'. '}
{localize('com_endpoint_config_token_get_edge_key_dev_tool')}{' '}
{localize('com_endpoint_config_key_get_edge_key_dev_tool')}{' '}
<a
target="_blank"
href="https://github.com/waylaidwanderer/node-chatgpt-api/issues/378#issuecomment-1559868368"
rel="noreferrer"
className="text-blue-600 underline"
>
{localize('com_endpoint_config_token_edge_instructions')}
{localize('com_endpoint_config_key_edge_instructions')}
</a>{' '}
{localize('com_endpoint_config_token_edge_full_token_string')}
{localize('com_endpoint_config_key_edge_full_token_string')}
</small>
),
chatGPTBrowser: (
<small className="break-all text-gray-600">
{localize('com_endpoint_config_token_chatgpt')}{' '}
{localize('com_endpoint_config_key_chatgpt')}{' '}
<a
target="_blank"
href="https://chat.openai.com"
@ -40,7 +40,7 @@ function HelpText({ endpoint }: { endpoint: string }) {
https://chat.openai.com
</a>
{', '}
{localize('com_endpoint_config_token_chatgpt_then_visit')}{' '}
{localize('com_endpoint_config_key_chatgpt_then_visit')}{' '}
<a
target="_blank"
href="https://chat.openai.com/api/auth/session"
@ -50,31 +50,31 @@ function HelpText({ endpoint }: { endpoint: string }) {
https://chat.openai.com/api/auth/session
</a>
{'. '}
{localize('com_endpoint_config_token_chatgpt_copy_token')}
{localize('com_endpoint_config_key_chatgpt_copy_token')}
</small>
),
google: (
<small className="break-all text-gray-600">
{localize('com_endpoint_config_token_google_need_to')}{' '}
{localize('com_endpoint_config_key_google_need_to')}{' '}
<a
target="_blank"
href="https://console.cloud.google.com/vertex-ai"
rel="noreferrer"
className="text-blue-600 underline"
>
{localize('com_endpoint_config_token_google_vertex_ai')}
{localize('com_endpoint_config_key_google_vertex_ai')}
</a>{' '}
{localize('com_endpoint_config_token_google_vertex_api')}{' '}
{localize('com_endpoint_config_key_google_vertex_api')}{' '}
<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"
>
{localize('com_endpoint_config_token_google_service_account')}
{localize('com_endpoint_config_key_google_service_account')}
</a>
{'. '}
{localize('com_endpoint_config_token_google_vertex_api_role')}
{localize('com_endpoint_config_key_google_vertex_api_role')}
</small>
),
};

View file

@ -21,9 +21,10 @@ const InputWithLabel: FC<InputWithLabelProps> = ({ value, onChange, label, id })
<Input
id={id}
value={value || ''}
data-testid={`input-${id}`}
value={value ?? ''}
onChange={onChange}
placeholder={`${localize('com_ui_enter')} ${label}`}
placeholder={`${localize('com_endpoint_config_value')} ${label}`}
className={cn(
defaultTextPropsLabel,
'flex h-10 max-h-10 w-full resize-none px-3 py-2',

View file

@ -4,7 +4,7 @@ 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';
import type { TConfigProps } from '~/common';
function isJson(str: string) {
try {
@ -15,56 +15,48 @@ function isJson(str: string) {
return true;
}
type OpenAIConfigProps = {
token: string;
setToken: React.Dispatch<React.SetStateAction<string>>;
endpoint: string;
};
const OpenAIConfig = ({ token, setToken, endpoint }: OpenAIConfigProps) => {
const OpenAIConfig = ({ userKey, setUserKey, endpoint }: TConfigProps) => {
const [showPanel, setShowPanel] = useState(endpoint === 'azureOpenAI');
const { getToken } = store.useToken(endpoint);
useEffect(() => {
const oldToken = getToken();
if (isJson(token)) {
if (isJson(userKey)) {
setShowPanel(true);
}
setToken(oldToken ?? '');
setUserKey('');
}, []);
useEffect(() => {
if (!showPanel && isJson(token)) {
setToken('');
if (!showPanel && isJson(userKey)) {
setUserKey('');
}
}, [showPanel]);
function getAzure(name: string) {
if (isJson(token)) {
const newToken = JSON.parse(token);
return newToken[name];
if (isJson(userKey)) {
const newKey = JSON.parse(userKey);
return newKey[name];
} else {
return '';
}
}
function setAzure(name: string, value: number | string | boolean) {
let newToken = {};
if (isJson(token)) {
newToken = JSON.parse(token);
let newKey = {};
if (isJson(userKey)) {
newKey = JSON.parse(userKey);
}
newToken[name] = value;
newKey[name] = value;
setToken(JSON.stringify(newToken));
setUserKey(JSON.stringify(newKey));
}
return (
<>
{!showPanel ? (
<>
<InputWithLabel
id={'chatGPTLabel'}
value={token || ''}
onChange={(e: { target: { value: string } }) => setToken(e.target.value || '')}
id={endpoint}
value={userKey ?? ''}
onChange={(e: { target: { value: string } }) => setUserKey(e.target.value ?? '')}
label={'OpenAI API Key'}
/>
</>
@ -72,36 +64,36 @@ const OpenAIConfig = ({ token, setToken, endpoint }: OpenAIConfigProps) => {
<>
<InputWithLabel
id={'instanceNameLabel'}
value={getAzure('azureOpenAIApiInstanceName') || ''}
value={getAzure('azureOpenAIApiInstanceName') ?? ''}
onChange={(e: { target: { value: string } }) =>
setAzure('azureOpenAIApiInstanceName', e.target.value || '')
setAzure('azureOpenAIApiInstanceName', e.target.value ?? '')
}
label={'Azure OpenAI Instance Name'}
/>
<InputWithLabel
id={'deploymentNameLabel'}
value={getAzure('azureOpenAIApiDeploymentName') || ''}
value={getAzure('azureOpenAIApiDeploymentName') ?? ''}
onChange={(e: { target: { value: string } }) =>
setAzure('azureOpenAIApiDeploymentName', e.target.value || '')
setAzure('azureOpenAIApiDeploymentName', e.target.value ?? '')
}
label={'Azure OpenAI Deployment Name'}
/>
<InputWithLabel
id={'versionLabel'}
value={getAzure('azureOpenAIApiVersion') || ''}
value={getAzure('azureOpenAIApiVersion') ?? ''}
onChange={(e: { target: { value: string } }) =>
setAzure('azureOpenAIApiVersion', e.target.value || '')
setAzure('azureOpenAIApiVersion', e.target.value ?? '')
}
label={'Azure OpenAI API Version'}
/>
<InputWithLabel
id={'apiKeyLabel'}
value={getAzure('azureOpenAIApiKey') || ''}
value={getAzure('azureOpenAIApiKey') ?? ''}
onChange={(e: { target: { value: string } }) =>
setAzure('azureOpenAIApiKey', e.target.value || '')
setAzure('azureOpenAIApiKey', e.target.value ?? '')
}
label={'Azure OpenAI API Key'}
/>

View file

@ -0,0 +1,18 @@
import React from 'react';
import InputWithLabel from './InputWithLabel';
import type { TConfigProps } from '~/common';
import { useLocalize } from '~/hooks';
const OtherConfig = ({ userKey, setUserKey, endpoint }: TConfigProps) => {
const localize = useLocalize();
return (
<InputWithLabel
id={endpoint}
value={userKey ?? ''}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setUserKey(e.target.value ?? '')}
label={localize('com_endpoint_config_key_name')}
/>
);
};
export default OtherConfig;

View file

@ -0,0 +1,103 @@
import React, { useState } from 'react';
import type { TDialogProps } from '~/common';
import { Dialog, Dropdown } from '~/components/ui';
import DialogTemplate from '~/components/ui/DialogTemplate';
import { RevokeKeysButton } from '~/components/Nav';
import { cn, defaultTextProps, removeFocusOutlines, alternateName } from '~/utils';
import { useUserKey, useLocalize } from '~/hooks';
import GoogleConfig from './GoogleConfig';
import OpenAIConfig from './OpenAIConfig';
import OtherConfig from './OtherConfig';
import HelpText from './HelpText';
const endpointComponents = {
google: GoogleConfig,
openAI: OpenAIConfig,
azureOpenAI: OpenAIConfig,
gptPlugins: OpenAIConfig,
default: OtherConfig,
};
const EXPIRY = {
THIRTY_MINUTES: { display: 'in 30 minutes', value: 30 * 60 * 1000 },
TWO_HOURS: { display: 'in 2 hours', value: 2 * 60 * 60 * 1000 },
TWELVE_HOURS: { display: 'in 12 hours', value: 12 * 60 * 60 * 1000 },
ONE_DAY: { display: 'in 1 day', value: 24 * 60 * 60 * 1000 },
ONE_WEEK: { display: 'in 7 days', value: 7 * 24 * 60 * 60 * 1000 },
ONE_MONTH: { display: 'in 30 days', value: 30 * 24 * 60 * 60 * 1000 },
};
const SetKeyDialog = ({
open,
onOpenChange,
endpoint,
}: Pick<TDialogProps, 'open' | 'onOpenChange'> & {
endpoint: string;
}) => {
const [userKey, setUserKey] = useState('');
const [expiresAtLabel, setExpiresAtLabel] = useState(EXPIRY.TWELVE_HOURS.display);
const { getExpiry, saveUserKey } = useUserKey(endpoint);
const localize = useLocalize();
const expirationOptions = Object.values(EXPIRY);
const handleExpirationChange = (label: string) => {
setExpiresAtLabel(label);
};
const submit = () => {
const selectedOption = expirationOptions.find((option) => option.display === expiresAtLabel);
const expiresAt = Date.now() + (selectedOption ? selectedOption.value : 0);
saveUserKey(userKey, expiresAt);
onOpenChange(false);
setUserKey('');
};
const EndpointComponent = endpointComponents[endpoint] ?? endpointComponents['default'];
const expiryTime = getExpiry();
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogTemplate
title={`${localize('com_endpoint_config_key_for')} ${alternateName[endpoint] ?? endpoint}`}
className="w-full max-w-[650px] sm:w-3/4 md:w-3/4 lg:w-3/4"
main={
<div className="grid w-full items-center gap-2">
<small className="text-red-600">
{`${localize('com_endpoint_config_key_encryption')} ${
!expiryTime
? localize('com_endpoint_config_key_expiry')
: `${new Date(expiryTime).toLocaleString()}`
}`}
</small>
<Dropdown
label="Expires "
value={expiresAtLabel}
onChange={handleExpirationChange}
options={expirationOptions.map((option) => option.display)}
className={cn(
defaultTextProps,
'flex h-full w-full resize-none',
removeFocusOutlines,
)}
optionsClassName="max-h-72"
containerClassName="flex w-1/2 md:w-1/3 resize-none z-[51]"
/>
<EndpointComponent userKey={userKey} setUserKey={setUserKey} endpoint={endpoint} />
<HelpText endpoint={endpoint} />
</div>
}
selection={{
selectHandler: submit,
selectClasses: 'bg-green-600 hover:bg-green-700 dark:hover:bg-green-800 text-white',
selectText: localize('com_ui_submit'),
}}
leftButtons={
<RevokeKeysButton endpoint={endpoint} showText={false} disabled={!expiryTime} />
}
/>
</Dialog>
);
};
export default SetKeyDialog;

View file

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

View file

@ -1,52 +0,0 @@
import React from 'react';
import FileUpload from '../EndpointMenu/FileUpload';
import { useLocalize } from '~/hooks';
const GoogleConfig = ({ setToken }: { setToken: React.Dispatch<React.SetStateAction<string>> }) => {
const localize = useLocalize();
return (
<FileUpload
id="googleKey"
className="w-full"
text={localize('com_endpoint_config_token_import_json_key')}
successText={localize('com_endpoint_config_token_import_json_key_succesful')}
invalidText={localize('com_endpoint_config_token_import_json_key_invalid')}
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

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

View file

@ -1,56 +0,0 @@
import React, { useState } from 'react';
import HelpText from './HelpText';
import GoogleConfig from './GoogleConfig';
import OpenAIConfig from './OpenAIConfig';
import OtherConfig from './OtherConfig';
import { Dialog } from '~/components/ui';
import DialogTemplate from '~/components/ui/DialogTemplate';
import { alternateName } from '~/utils';
import store from '~/store';
import { useLocalize } from '~/hooks';
const SetTokenDialog = ({ open, onOpenChange, endpoint }) => {
const localize = useLocalize();
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={`${localize('com_endpoint_config_token_for')} ${
alternateName[endpoint] ?? endpoint
}`}
className="w-full max-w-[650px] sm:w-3/4 md:w-3/4 lg:w-3/4"
main={
<div className="grid w-full items-center gap-2">
<EndpointComponent token={token} setToken={setToken} endpoint={endpoint} />
<small className="text-red-600">{localize('com_endpoint_config_token_server')}</small>
<HelpText endpoint={endpoint} />
</div>
}
selection={{
selectHandler: submit,
selectClasses: 'bg-green-600 hover:bg-green-700 dark:hover:bg-green-800 text-white',
selectText: localize('com_ui_submit'),
}}
/>
</Dialog>
);
};
export default SetTokenDialog;

View file

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

View file

@ -1,33 +1,43 @@
import React, { useState } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { StopGeneratingIcon } from '~/components';
import { Settings } from 'lucide-react';
import { SetTokenDialog } from './SetTokenDialog';
import store from '~/store';
import { useLocalize } from '~/hooks';
import { SetKeyDialog } from './SetKeyDialog';
import { useUserKey, useLocalize } from '~/hooks';
export default function SubmitButton({
endpoint,
conversation,
submitMessage,
handleStopGenerating,
disabled,
isSubmitting,
endpointsConfig,
userProvidesKey,
}) {
const [setTokenDialogOpen, setSetTokenDialogOpen] = useState(false);
const { getToken } = store.useToken(endpoint);
const isTokenProvided = endpointsConfig?.[endpoint]?.userProvide ? !!getToken() : true;
const endpointsToHideSetTokens = new Set(['openAI', 'azureOpenAI', 'bingAI']);
const { endpoint } = conversation;
const [isDialogOpen, setDialogOpen] = useState(false);
const { checkExpiry } = useUserKey(endpoint);
const [isKeyProvided, setKeyProvided] = useState(userProvidesKey ? checkExpiry() : true);
const isKeyActive = checkExpiry();
const localize = useLocalize();
const clickHandler = (e) => {
e.preventDefault();
submitMessage();
};
useEffect(() => {
if (userProvidesKey) {
setKeyProvided(isKeyActive);
} else {
setKeyProvided(true);
}
}, [checkExpiry, endpoint, userProvidesKey, isKeyActive]);
const setToken = () => {
setSetTokenDialogOpen(true);
};
const clickHandler = useCallback(
(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
submitMessage();
},
[submitMessage],
);
const setKey = useCallback(() => {
setDialogOpen(true);
}, []);
if (isSubmitting) {
return (
@ -41,26 +51,24 @@ export default function SubmitButton({
</div>
</button>
);
} else if (!isTokenProvided && !endpointsToHideSetTokens.has(endpoint)) {
} else if (!isKeyProvided) {
return (
<>
<button
onClick={setToken}
onClick={setKey}
type="button"
className="group absolute bottom-0 right-0 z-[101] flex h-[100%] w-auto items-center justify-center bg-transparent pr-1 text-gray-500"
>
<div className="flex items-center justify-center rounded-md text-xs group-hover:bg-gray-100 group-disabled:hover:bg-transparent dark:group-hover:bg-gray-900 dark:group-hover:text-gray-400 dark:group-disabled:hover:bg-transparent">
<div className="m-0 mr-0 flex items-center justify-center rounded-md p-2 sm:p-2">
<Settings className="mr-1 inline-block h-auto w-[18px]" />
{localize('com_endpoint_config_token_name_placeholder')}
{localize('com_endpoint_config_key_name_placeholder')}
</div>
</div>
</button>
<SetTokenDialog
open={setTokenDialogOpen}
onOpenChange={setSetTokenDialogOpen}
endpoint={endpoint}
/>
{userProvidesKey && (
<SetKeyDialog open={isDialogOpen} onOpenChange={setDialogOpen} endpoint={endpoint} />
)}
</>
);
} else {
@ -68,6 +76,7 @@ export default function SubmitButton({
<button
onClick={clickHandler}
disabled={disabled}
data-testid="submit-button"
className="group absolute bottom-0 right-0 z-[101] flex h-[100%] w-[50px] items-center justify-center bg-transparent p-1 text-gray-500"
>
<div className="m-1 mr-0 rounded-md pb-[9px] pl-[9.5px] pr-[7px] pt-[11px] group-hover:bg-gray-100 group-disabled:hover:bg-transparent dark:group-hover:bg-gray-900 dark:group-hover:text-gray-400 dark:group-disabled:hover:bg-transparent">

View file

@ -169,12 +169,12 @@ export default function TextChat({ isSearchView = false }) {
className="m-0 flex h-auto max-h-52 flex-1 resize-none overflow-auto border-0 bg-transparent p-0 pl-2 pr-12 leading-6 placeholder:text-sm placeholder:text-gray-600 focus:outline-none focus:ring-0 focus-visible:ring-0 dark:bg-transparent dark:placeholder:text-gray-500 md:pl-2"
/>
<SubmitButton
conversation={conversation}
submitMessage={submitMessage}
handleStopGenerating={handleStopGenerating}
disabled={disabled || isNotAppendable}
isSubmitting={isSubmitting}
endpointsConfig={endpointsConfig}
endpoint={conversation?.endpoint}
userProvidesKey={endpointsConfig?.[conversation.endpoint]?.userProvide}
/>
</div>
</div>

View file

@ -22,7 +22,7 @@ const DisplayMessage = ({ text, isCreatedByUser, message, showCursor }: TDisplay
<div
className={cn(
'markdown prose dark:prose-invert light w-full break-words',
isCreatedByUser ? 'whitespace-pre-wrap' : '',
isCreatedByUser ? 'whitespace-pre-wrap dark:text-gray-20' : 'dark:text-gray-70',
)}
>
{!isCreatedByUser ? (

View file

@ -96,7 +96,7 @@ const Plugin: React.FC<PluginProps> = ({ plugin }) => {
<>
<div
className={cn(
plugin.loading ? 'bg-green-100' : 'bg-[#ECECF1]',
plugin.loading ? 'bg-green-100' : 'bg-gray-20',
'flex items-center rounded p-3 text-xs text-gray-900',
)}
>

View file

@ -11,6 +11,7 @@ import SiblingSwitch from './SiblingSwitch';
import { getIcon } from '~/components/Endpoints';
import { useMessageHandler } from '~/hooks';
import type { TMessageProps } from '~/common';
import { cn } from '~/utils';
import store from '~/store';
export default function Message({
@ -78,9 +79,14 @@ export default function Message({
}
};
const commonClasses =
'w-full border-b text-gray-800 group border-black/10 dark:border-gray-900/50 dark:text-gray-100';
const uniqueClasses = isCreatedByUser
? 'bg-white dark:bg-gray-800 dark:text-gray-20'
: 'bg-gray-50 dark:bg-gray-1000 dark:text-gray-70';
const props = {
className:
'w-full border-b border-black/10 dark:border-gray-900/50 text-gray-800 bg-white dark:text-gray-100 group dark:bg-gray-800',
className: cn(commonClasses, uniqueClasses),
titleclass: '',
};
@ -90,11 +96,6 @@ export default function Message({
model: message?.model ?? conversation?.model,
});
if (!isCreatedByUser) {
props.className =
'w-full border-b border-black/10 bg-gray-50 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group bg-gray-100 dark:bg-gray-1000';
}
if (message?.bg && searchResult) {
props.className = message?.bg?.split('hover')[0];
props.titleclass = message?.bg?.split(props.className)[1] + ' cursor-pointer';

View file

@ -119,7 +119,13 @@ export default function ExportModel({ open, onOpenChange }) {
};
const exportScreenshot = async () => {
const data = await captureScreenshot();
let data;
try {
data = await captureScreenshot();
} catch (err) {
console.error('Failed to capture screenshot');
return console.error(err);
}
download(data, `${filename}.png`, 'image/png');
};

View file

@ -1,88 +0,0 @@
import * as Tabs from '@radix-ui/react-tabs';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../ui';
import { General } from './SettingsTabs';
import { CogIcon } from '~/components/svg';
import { useEffect, useState } from 'react';
import { cn } from '~/utils/';
import { useRecoilValue } from 'recoil';
import { localize } from '~/localization/Translation';
import store from '~/store';
export default function Settings({ open, onOpenChange }) {
const [confirmClear, setConfirmClear] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const lang = useRecoilValue(store.lang);
// check if mobile dynamically and update
useEffect(() => {
const checkMobile = () => {
if (window.innerWidth <= 768) {
setIsMobile(true);
} else {
setIsMobile(false);
}
};
checkMobile();
window.addEventListener('resize', checkMobile);
}, []);
useEffect(() => {
// If the user clicks in the dialog when confirmClear is true, set it to false
const handleClick = (e) => {
if (confirmClear) {
if (e.target.id === 'clearConvosBtn' || e.target.id === 'clearConvosTxt') {
return;
}
setConfirmClear(false);
}
};
window.addEventListener('click', handleClick);
return () => window.removeEventListener('click', handleClick);
}, [confirmClear]);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className={cn('shadow-2xl dark:bg-gray-900 dark:text-white md:w-[680px] ')}>
<DialogHeader>
<DialogTitle className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-200">
{localize(lang, 'com_nav_settings')}
</DialogTitle>
</DialogHeader>
<div className="px-6">
<Tabs.Root
defaultValue="general"
className="flex flex-col gap-6 md:flex-row"
orientation="vertical"
>
<Tabs.List
aria-label="Settings"
role="tablist"
aria-orientation="vertical"
className={cn(
'-ml-[8px] flex min-w-[180px] flex-shrink-0 flex-col',
isMobile && 'flex-row rounded-lg bg-gray-100 p-1 dark:bg-gray-800/30',
)}
style={{ outline: 'none' }}
>
<Tabs.Trigger
className={cn(
'flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm radix-state-active:bg-gray-800 radix-state-active:text-white',
isMobile &&
'group flex-1 items-center justify-center text-sm dark:text-gray-500 dark:radix-state-active:text-white',
)}
value="general"
>
<CogIcon />
{localize(lang, 'com_nav_setting_general')}
</Tabs.Trigger>
</Tabs.List>
<General />
</Tabs.Root>
</div>
</DialogContent>
</Dialog>
);
}

View file

@ -0,0 +1,69 @@
import * as Tabs from '@radix-ui/react-tabs';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '~/components/ui';
import { CogIcon, DataIcon } from '~/components/svg';
import { useMediaQuery, useLocalize } from '~/hooks';
import type { TDialogProps } from '~/common';
import { General, Data } from './SettingsTabs';
import { cn } from '~/utils';
export default function Settings({ open, onOpenChange }: TDialogProps) {
const isSmallScreen = useMediaQuery('(max-width: 768px)');
const localize = useLocalize();
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className={cn('shadow-2xl dark:bg-gray-900 dark:text-white md:w-[680px] ')}>
<DialogHeader>
<DialogTitle className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-200">
{localize('com_nav_settings')}
</DialogTitle>
</DialogHeader>
<div className="px-6">
<Tabs.Root
defaultValue="general"
className="flex flex-col gap-6 md:flex-row"
orientation="vertical"
>
<Tabs.List
aria-label="Settings"
role="tablist"
aria-orientation="vertical"
className={cn(
'-ml-[8px] flex min-w-[180px] flex-shrink-0 flex-col',
isSmallScreen ? 'flex-row rounded-lg bg-gray-100 p-1 dark:bg-gray-800/30' : '',
)}
style={{ outline: 'none' }}
>
<Tabs.Trigger
className={cn(
'group flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm radix-state-active:bg-gray-800 radix-state-active:text-white',
isSmallScreen
? 'flex-1 items-center justify-center text-sm dark:text-gray-500 dark:radix-state-active:text-white'
: '',
)}
value="general"
>
<CogIcon className="fill-gray-800" />
{localize('com_nav_setting_general')}
</Tabs.Trigger>
<Tabs.Trigger
className={cn(
'group flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm radix-state-active:bg-gray-800 radix-state-active:text-white',
isSmallScreen
? 'flex-1 items-center justify-center text-sm dark:text-gray-500 dark:radix-state-active:text-white'
: '',
)}
value="data"
>
<DataIcon />
{localize('com_nav_setting_data')}
</Tabs.Trigger>
</Tabs.List>
<General />
<Data />
</Tabs.Root>
</div>
</DialogContent>
</Dialog>
);
}

View file

@ -0,0 +1,70 @@
import { forwardRef } from 'react';
import type { ForwardedRef } from 'react';
import { CheckIcon } from 'lucide-react';
import { DialogButton } from '~/components/ui';
import { Spinner } from '~/components/svg';
import type { TDangerButtonProps } from '~/common';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
const DangerButton = (props: TDangerButtonProps, ref: ForwardedRef<HTMLButtonElement>) => {
const {
id,
onClick,
mutation,
disabled,
confirmClear,
infoTextCode,
actionTextCode,
className = '',
showText = true,
dataTestIdInitial,
dataTestIdConfirm,
confirmActionTextCode = 'com_ui_confirm_action',
} = props;
const localize = useLocalize();
const renderMutation = (node: React.ReactNode | string) => {
if (mutation && mutation.isLoading) {
return <Spinner className="h-5 w-5" />;
}
return node;
};
return (
<div className="flex items-center justify-between">
{showText && <div>{localize(infoTextCode)}</div>}
<DialogButton
id={id}
ref={ref}
disabled={disabled}
onClick={onClick}
className={cn(
' btn btn-danger relative border-none bg-red-700 text-white hover:bg-red-800 dark:hover:bg-red-800',
className,
)}
>
{confirmClear ? (
<div
className="flex w-full items-center justify-center gap-2"
id={`${id}-text`}
data-testid={dataTestIdConfirm}
>
{renderMutation(<CheckIcon className="h-5 w-5" />)}
{mutation && mutation.isLoading ? null : localize(confirmActionTextCode)}
</div>
) : (
<div
className="flex w-full items-center justify-center gap-2"
id={`${id}-text`}
data-testid={dataTestIdInitial}
>
{renderMutation(localize(actionTextCode))}
</div>
)}
</DialogButton>
</div>
);
};
export default forwardRef(DangerButton);

View file

@ -0,0 +1,76 @@
import * as Tabs from '@radix-ui/react-tabs';
import { useRevokeAllUserKeysMutation, useRevokeUserKeyMutation } from 'librechat-data-provider';
import React, { useState, useCallback, useRef } from 'react';
import { useOnClickOutside } from '~/hooks';
import DangerButton from './DangerButton';
export const RevokeKeysButton = ({
showText = true,
endpoint = '',
all = false,
disabled = false,
}: {
showText?: boolean;
endpoint?: string;
all?: boolean;
disabled?: boolean;
}) => {
const [confirmClear, setConfirmClear] = useState(false);
const revokeKeyMutation = useRevokeUserKeyMutation(endpoint);
const revokeKeysMutation = useRevokeAllUserKeysMutation();
const contentRef = useRef(null);
useOnClickOutside(contentRef, () => confirmClear && setConfirmClear(false), []);
const revokeAllUserKeys = useCallback(() => {
if (confirmClear) {
revokeKeysMutation.mutate({});
setConfirmClear(false);
} else {
setConfirmClear(true);
}
}, [confirmClear, revokeKeysMutation]);
const revokeUserKey = useCallback(() => {
if (!endpoint) {
return;
} else if (confirmClear) {
revokeKeyMutation.mutate({});
setConfirmClear(false);
} else {
setConfirmClear(true);
}
}, [confirmClear, revokeKeyMutation, endpoint]);
const onClick = all ? revokeAllUserKeys : revokeUserKey;
return (
<DangerButton
ref={contentRef}
showText={showText}
onClick={onClick}
disabled={disabled}
confirmClear={confirmClear}
id={'revoke-all-user-keys'}
actionTextCode={'com_ui_revoke'}
infoTextCode={'com_ui_revoke_info'}
dataTestIdInitial={'revoke-all-keys-initial'}
dataTestIdConfirm={'revoke-all-keys-confirm'}
mutation={all ? revokeKeysMutation : revokeKeyMutation}
/>
);
};
function Data() {
return (
<Tabs.Content value="data" role="tabpanel" className="w-full md:min-h-[300px]">
<div className="flex flex-col gap-3 text-sm text-gray-600 dark:text-gray-300">
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<RevokeKeysButton all={true} />
</div>
</div>
</Tabs.Content>
);
}
export default React.memo(Data);

View file

@ -1,13 +1,11 @@
import * as Tabs from '@radix-ui/react-tabs';
import { CheckIcon } from 'lucide-react';
import { DialogButton } from '~/components/ui';
import React, { useState, useContext, useEffect, useCallback } from 'react';
import { useClearConversationsMutation } from 'librechat-data-provider';
import { useRecoilState } from 'recoil';
import * as Tabs from '@radix-ui/react-tabs';
import React, { useState, useContext, useEffect, useCallback, useRef } from 'react';
import { useClearConversationsMutation } from 'librechat-data-provider';
import { ThemeContext, useLocalize, useOnClickOutside } from '~/hooks';
import type { TDangerButtonProps } from '~/common';
import DangerButton from './DangerButton';
import store from '~/store';
import { ThemeContext } from '~/hooks';
import { cn } from '~/utils';
import { useLocalize } from '~/hooks';
export const ThemeSelector = ({
theme,
@ -38,53 +36,26 @@ export const ClearChatsButton = ({
confirmClear,
className = '',
showText = true,
mutation,
onClick,
}: {
confirmClear: boolean;
className?: string;
showText: boolean;
onClick: () => void;
}) => {
const localize = useLocalize();
}: Pick<
TDangerButtonProps,
'confirmClear' | 'mutation' | 'className' | 'showText' | 'onClick'
>) => {
return (
<div className="flex items-center justify-between">
{showText && <div>{localize('com_nav_clear_all_chats')}</div>}
<DialogButton
id="clearConvosBtn"
onClick={onClick}
className={cn(
' btn btn-danger relative border-none bg-red-700 text-white hover:bg-red-800 dark:hover:bg-red-800',
className,
)}
>
{/* <button
className="btn mt-2 inline-flex h-10 items-center justify-center rounded-md relative bg-red-600 text-white hover:bg-red-800"
type="button"
id="clearConvosBtn"
onClick={onClick}
> */}
{confirmClear ? (
<div
className="flex w-full items-center justify-center gap-2"
id="clearConvosTxt"
data-testid="clear-convos-confirm"
>
<CheckIcon className="h-5 w-5" /> {localize('com_nav_confirm_clear')}
</div>
) : (
<div
className="flex w-full items-center justify-center gap-2"
id="clearConvosTxt"
data-testid="clear-convos-initial"
>
{localize('com_ui_clear')}
</div>
)}
{/* </button> */}
</DialogButton>
</div>
<DangerButton
id="clearConvosBtn"
mutation={mutation}
confirmClear={confirmClear}
className={className}
showText={showText}
infoTextCode="com_nav_clear_all_chats"
actionTextCode="com_ui_clear"
confirmActionTextCode="com_nav_confirm_clear"
dataTestIdInitial="clear-convos-initial"
dataTestIdConfirm="clear-convos-confirm"
onClick={onClick}
/>
);
};
@ -127,6 +98,9 @@ function General() {
const { newConversation } = store.useConversation();
const { refreshConversations } = store.useConversations();
const contentRef = useRef(null);
useOnClickOutside(contentRef, () => confirmClear && setConfirmClear(false), []);
useEffect(() => {
if (clearConvosMutation.isSuccess) {
newConversation();
@ -159,7 +133,12 @@ function General() {
);
return (
<Tabs.Content value="general" role="tabpanel" className="w-full md:min-h-[300px]">
<Tabs.Content
value="general"
role="tabpanel"
className="w-full md:min-h-[300px]"
ref={contentRef}
>
<div className="flex flex-col gap-3 text-sm text-gray-600 dark:text-gray-300">
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<ThemeSelector theme={theme} onChange={changeTheme} />
@ -168,7 +147,12 @@ function General() {
<LangSelector langcode={langcode} onChange={changeLang} />
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
<ClearChatsButton confirmClear={confirmClear} onClick={clearConvos} showText={true} />
<ClearChatsButton
confirmClear={confirmClear}
onClick={clearConvos}
showText={true}
mutation={clearConvosMutation}
/>
</div>
</div>
</Tabs.Content>

View file

@ -1,2 +1,4 @@
export { default as General } from './General';
export { ClearChatsButton } from './General';
export { default as Data } from './Data';
export { RevokeKeysButton } from './Data';

View file

@ -1,13 +1,16 @@
import * as React from 'react';
import { cn } from '~/utils';
export default function CogIcon() {
export default function CogIcon({ className = '' }) {
return (
<svg
stroke="currentColor"
fill="currentColor"
strokeWidth="0"
viewBox="0 0 20 20"
className="group-radix-state-active:fill-white h-4 h-5 w-4 w-5 fill-white dark:fill-gray-500"
className={cn(
'h-4 h-5 w-4 w-5 fill-white group-radix-state-active:fill-white dark:fill-gray-500',
className,
)}
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"

View file

@ -0,0 +1,18 @@
export default function DataIcon() {
return (
<svg
stroke="currentColor"
fill="currentColor"
strokeWidth="0"
viewBox="0 0 20 20"
className="h-4 h-5 w-4 w-5 fill-gray-800 group-radix-state-active:fill-white dark:fill-gray-500"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M3 12v3c0 1.657 3.134 3 7 3s7-1.343 7-3v-3c0 1.657-3.134 3-7 3s-7-1.343-7-3z" />
<path d="M3 7v3c0 1.657 3.134 3 7 3s7-1.343 7-3V7c0 1.657-3.134 3-7 3S3 8.657 3 7z" />
<path d="M17 5c0 1.657-3.134 3-7 3S3 6.657 3 5s3.134-3 7-3 7 1.343 7 3z" />
</svg>
);
}

View file

@ -2,6 +2,7 @@ export { default as Plugin } from './Plugin';
export { default as GPTIcon } from './GPTIcon';
export { default as EditIcon } from './EditIcon';
export { default as CogIcon } from './CogIcon';
export { default as DataIcon } from './DataIcon';
export { default as Panel } from './Panel';
export { default as Spinner } from './Spinner';
export { default as Clipboard } from './Clipboard';

View file

@ -3,7 +3,15 @@ import CheckMark from '../svg/CheckMark';
import { Listbox } from '@headlessui/react';
import { cn } from '~/utils/';
function Dropdown({ value, onChange, options, className, containerClassName }) {
function Dropdown({
value,
label = '',
onChange,
options,
className,
containerClassName,
optionsClassName = '',
}) {
const currentOption =
options.find((element) => element === value || element?.value === value) ?? value;
return (
@ -18,7 +26,7 @@ function Dropdown({ value, onChange, options, className, containerClassName }) {
>
<span className="inline-flex w-full truncate">
<span className="flex h-6 items-center gap-1 truncate text-sm text-black dark:text-white">
{currentOption?.display ?? value}
{`${label}${currentOption?.display ?? value}`}
</span>
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
@ -38,12 +46,17 @@ function Dropdown({ value, onChange, options, className, containerClassName }) {
</svg>
</span>
</Listbox.Button>
<Listbox.Options className="absolute z-50 mt-2 max-h-60 w-full overflow-auto rounded bg-white text-base text-xs ring-1 ring-black/10 focus:outline-none dark:bg-gray-800 dark:ring-white/20 dark:last:border-0 md:w-[100%] ">
<Listbox.Options
className={cn(
'absolute z-50 mt-2 max-h-60 w-full overflow-auto rounded bg-white text-base text-xs ring-1 ring-black/10 focus:outline-none dark:bg-gray-800 dark:ring-white/20 dark:last:border-0 md:w-[100%] ',
optionsClassName,
)}
>
{options.map((item, i) => (
<Listbox.Option
key={i}
value={item?.value ?? item}
className="group relative flex h-[42px] cursor-pointer select-none items-center overflow-hidden border-b border-black/10 pl-3 pr-9 text-gray-900 last:border-0 hover:bg-[#ECECF1] dark:border-white/20 dark:text-white dark:hover:bg-gray-700"
className="group relative flex h-[42px] cursor-pointer select-none items-center overflow-hidden border-b border-black/10 pl-3 pr-9 text-gray-900 last:border-0 hover:bg-gray-20 dark:border-white/20 dark:text-white dark:hover:bg-gray-700"
>
<span className="flex items-center gap-1.5 truncate">
<span

View file

@ -1,8 +1,7 @@
import * as React from 'react';
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import { Check, ChevronRight, Circle } from 'lucide-react';
import { cn } from '../../utils';
import { cn } from '~/utils';
const DropdownMenu = DropdownMenuPrimitive.Root;

View file

@ -147,7 +147,7 @@ function MultiSelectDropDown({
<Listbox.Option
key={i}
value={option[optionValueKey]}
className="group relative flex h-[42px] cursor-pointer select-none items-center overflow-hidden border-b border-black/10 pl-3 pr-9 text-gray-900 last:border-0 hover:bg-[#ECECF1] dark:border-white/20 dark:text-white dark:hover:bg-gray-700"
className="group relative flex h-[42px] cursor-pointer select-none items-center overflow-hidden border-b border-black/10 pl-3 pr-9 text-gray-900 last:border-0 hover:bg-gray-20 dark:border-white/20 dark:text-white dark:hover:bg-gray-700"
>
<span className="flex items-center gap-1.5 truncate">
{!option.isButton && (

View file

@ -107,7 +107,7 @@ function SelectDropDown({
<Listbox.Option
key={i}
value={option}
className="group relative flex h-[42px] cursor-pointer select-none items-center overflow-hidden border-b border-black/10 pl-3 pr-9 text-gray-900 last:border-0 hover:bg-[#ECECF1] dark:border-white/20 dark:text-white dark:hover:bg-gray-700"
className="group relative flex h-[42px] cursor-pointer select-none items-center overflow-hidden border-b border-black/10 pl-3 pr-9 text-gray-900 last:border-0 hover:bg-gray-20 dark:border-white/20 dark:text-white dark:hover:bg-gray-700"
>
<span className="flex items-center gap-1.5 truncate">
<span

View file

@ -0,0 +1,45 @@
import * as React from 'react';
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
import { cn } from '~/utils';
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Trigger>
>((props, ref) => <TooltipPrimitive.Trigger ref={ref} {...props} />);
TooltipTrigger.displayName = TooltipPrimitive.Trigger.displayName;
const TooltipPortal = TooltipPrimitive.Portal;
const TooltipArrow = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Arrow>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Arrow>
>((props, ref) => <TooltipPrimitive.Arrow ref={ref} {...props} />);
TooltipArrow.displayName = TooltipPrimitive.Arrow.displayName;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className = '', forceMount, children, ...props }, ref) => (
<TooltipPortal forceMount={forceMount}>
<TooltipPrimitive.Content
className={cn(
'shadow-xs relative max-w-xs rounded-lg border border-black/10 bg-black p-1 transition-opacity',
className,
)}
ref={ref}
{...props}
>
<span className="flex items-center whitespace-pre-wrap px-2 py-1 text-center text-sm font-medium normal-case text-white">
{children}
<TooltipArrow className="TooltipArrow" />
</span>
</TooltipPrimitive.Content>
</TooltipPortal>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
const TooltipProvider = TooltipPrimitive.Provider;
export { Tooltip, TooltipTrigger, TooltipPortal, TooltipContent, TooltipArrow, TooltipProvider };

View file

@ -14,6 +14,7 @@ export * from './Switch';
export * from './Tabs';
export * from './Templates';
export * from './Textarea';
export * from './Tooltip';
export { default as Dropdown } from './Dropdown';
export { default as SelectDropDown } from './SelectDropDown';
export { default as MultiSelectDropDown } from './MultiSelectDropDown';

View file

@ -2,11 +2,14 @@ export * from './AuthContext';
export * from './ThemeContext';
export * from './ScreenshotContext';
export * from './ApiErrorBoundaryContext';
export { default as useUserKey } from './useUserKey';
export { default as useDebounce } from './useDebounce';
export { default as useLocalize } from './useLocalize';
export { default as useMediaQuery } from './useMediaQuery';
export { default as useSetOptions } from './useSetOptions';
export { default as useGenerations } from './useGenerations';
export { default as useScrollToRef } from './useScrollToRef';
export { default as useLocalStorage } from './useLocalStorage';
export { default as useServerStream } from './useServerStream';
export { default as useOnClickOutside } from './useOnClickOutside';
export { default as useMessageHandler } from './useMessageHandler';

View file

@ -0,0 +1,53 @@
/* `useLocalStorage`
*
* Features:
* - JSON Serializing
* - Also value will be updated everywhere, when value updated (via `storage` event)
*/
import { useEffect, useState } from 'react';
export default function useLocalStorage<T>(key: string, defaultValue: T): [T, (value: T) => void] {
const [value, setValue] = useState(defaultValue);
useEffect(() => {
const item = localStorage.getItem(key);
if (!item) {
localStorage.setItem(key, JSON.stringify(defaultValue));
}
setValue(item ? JSON.parse(item) : defaultValue);
function handler(e: StorageEvent) {
if (e.key !== key) {
return;
}
const lsi = localStorage.getItem(key);
setValue(JSON.parse(lsi ?? ''));
}
window.addEventListener('storage', handler);
return () => {
window.removeEventListener('storage', handler);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const setValueWrap = (value: T) => {
try {
setValue(value);
localStorage.setItem(key, JSON.stringify(value));
if (typeof window !== 'undefined') {
window.dispatchEvent(new StorageEvent('storage', { key }));
}
} catch (e) {
console.error(e);
}
};
return [value, setValueWrap];
}

View file

@ -3,6 +3,7 @@ import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { parseConvo, getResponseSender } from 'librechat-data-provider';
import type { TMessage, TSubmission } from 'librechat-data-provider';
import type { TAskFunction } from '~/common';
import useUserKey from './useUserKey';
import store from '~/store';
const useMessageHandler = () => {
@ -16,7 +17,7 @@ const useMessageHandler = () => {
const endpointsConfig = useRecoilValue(store.endpointsConfig);
const [messages, setMessages] = useRecoilState(store.messages);
const { endpoint } = currentConversation;
const { getToken } = store.useToken(endpoint ?? '');
const { getExpiry } = useUserKey(endpoint ?? '');
const ask: TAskFunction = (
{ text, parentMessageId = null, conversationId = null, messageId = null },
@ -49,14 +50,13 @@ const useMessageHandler = () => {
}
const isEditOrContinue = isEdited || isContinued;
const { userProvide } = endpointsConfig[endpoint] ?? {};
// set the endpoint option
const convo = parseConvo(endpoint, currentConversation);
const endpointOption = {
endpoint,
...convo,
token: userProvide ? getToken() : null,
key: getExpiry(),
};
const responseSender = getResponseSender(endpointOption);

View file

@ -1,5 +1,6 @@
import { useEffect } from 'react';
import { useResetRecoilState, useSetRecoilState } from 'recoil';
/* @ts-ignore */
import { SSE, createPayload, tMessageSchema, tConversationSchema } from 'librechat-data-provider';
import type { TResPlugin, TMessage, TConversation, TSubmission } from 'librechat-data-provider';
import { useAuthContext } from '~/hooks/AuthContext';

View file

@ -0,0 +1,58 @@
import { useRecoilValue } from 'recoil';
import { useMemo, useCallback } from 'react';
import { useUpdateUserKeysMutation, useUserKeyQuery } from 'librechat-data-provider';
import store from '~/store';
const useUserKey = (endpoint: string) => {
const endpointsConfig = useRecoilValue(store.endpointsConfig);
const config = endpointsConfig[endpoint];
const { azure } = config ?? {};
let keyEndpoint = endpoint;
if (azure) {
keyEndpoint = 'azureOpenAI';
} else if (keyEndpoint === 'gptPlugins') {
keyEndpoint = 'openAI';
}
const updateKey = useUpdateUserKeysMutation();
const checkUserKey = useUserKeyQuery(keyEndpoint);
const getExpiry = useCallback(() => {
if (checkUserKey.data) {
return checkUserKey.data.expiresAt;
}
}, [checkUserKey.data]);
const checkExpiry = useCallback(() => {
const expiresAt = getExpiry();
if (!expiresAt) {
return false;
}
const expiresAtDate = new Date(expiresAt);
if (expiresAtDate < new Date()) {
return false;
}
return true;
}, [getExpiry]);
const saveUserKey = useCallback(
(value: string, expiresAt: number) => {
const dateStr = new Date(expiresAt).toISOString();
updateKey.mutate({
name: keyEndpoint,
value,
expiresAt: dateStr,
});
},
[updateKey, keyEndpoint],
);
return useMemo(
() => ({ getExpiry, checkExpiry, saveUserKey }),
[getExpiry, checkExpiry, saveUserKey],
);
};
export default useUserKey;

View file

@ -84,6 +84,7 @@ export default {
com_auth_to_try_again: 'para tentar novamente.',
com_auth_submit_registration: 'Enviar registro',
com_auth_welcome_back: 'Bem-vindo(a) de volta',
com_endpoint_open_menu: 'Abrir Menu',
com_endpoint_bing_enable_sydney: 'Habilitar Sydney',
com_endpoint_bing_to_enable_sydney: 'Para habilitar Sydney',
com_endpoint_bing_jailbreak: 'Jailbreak',

View file

@ -81,6 +81,7 @@ export default {
com_auth_to_try_again: 'um es nochmal zu versuchen.',
com_auth_submit_registration: 'Registrieren',
com_auth_welcome_back: 'Willkommen zurück!',
com_endpoint_open_menu: 'Öffne Menü',
com_endpoint_bing_enable_sydney: 'Aktiviere Sydney',
com_endpoint_bing_to_enable_sydney: 'Um Sydney zu aktivieren',
com_endpoint_bing_jailbreak: 'Jailbreak',

View file

@ -30,6 +30,8 @@ export default {
com_ui_pay_per_call: 'All AI conversations in one place. Pay per call and not per month',
com_ui_enter: 'Enter',
com_ui_submit: 'Submit',
com_ui_upload_success: 'Successfully uploaded file',
com_ui_upload_invalid: 'Invalid file for upload',
com_ui_cancel: 'Cancel',
com_ui_save: 'Save',
com_ui_copy_to_clipboard: 'Copy to clipboard',
@ -40,12 +42,17 @@ export default {
com_ui_success: 'Success',
com_ui_all: 'all',
com_ui_clear: 'Clear',
com_ui_revoke: 'Revoke',
com_ui_revoke_info: 'Revoke all user provided credentials.',
com_ui_confirm_action: 'Confirm Action',
com_ui_chats: 'chats',
com_ui_delete: 'Delete',
com_ui_delete_conversation: 'Delete chat?',
com_ui_delete_conversation_confirm: 'This will delete',
com_auth_error_login:
'Unable to login with the information provided. Please check your credentials and try again.',
com_auth_error_login_rl:
'Too many login attempts from this IP in a short amount of time. Please try again later.',
com_auth_no_account: 'Don\'t have an account?',
com_auth_sign_up: 'Sign up',
com_auth_sign_in: 'Sign in',
@ -96,6 +103,7 @@ export default {
com_auth_to_try_again: 'to try again.',
com_auth_submit_registration: 'Submit registration',
com_auth_welcome_back: 'Welcome back',
com_endpoint_open_menu: 'Open Menu',
com_endpoint_bing_enable_sydney: 'Enable Sydney',
com_endpoint_bing_to_enable_sydney: 'To enable Sydney',
com_endpoint_bing_jailbreak: 'Jailbreak',
@ -189,29 +197,30 @@ export default {
com_endpoint_func_hover: 'Enable use of Plugins as OpenAI Functions',
com_endpoint_skip_hover:
'Enable skipping the completion step, which reviews the final answer and generated steps',
com_endpoint_config_token: 'Config Token',
com_endpoint_config_token_for: 'Config Token for',
com_endpoint_config_token_name: 'Token Name',
com_endpoint_config_token_name_placeholder: 'Set token first',
com_endpoint_config_token_server: 'Your token will be sent to the server, but not saved.',
com_endpoint_config_token_import_json_key: 'Import Service Account JSON Key.',
com_endpoint_config_token_import_json_key_succesful: 'Import Service Account JSON Key.',
com_endpoint_config_token_import_json_key_invalid:
com_endpoint_config_key: 'Set API Key',
com_endpoint_config_key_for: 'Set API Key for',
com_endpoint_config_key_name: 'Key',
com_endpoint_config_value: 'Enter value for',
com_endpoint_config_key_name_placeholder: 'Set API key first',
com_endpoint_config_key_encryption: 'Your key will be encrypted and deleted at',
com_endpoint_config_key_expiry: 'the expiry time',
com_endpoint_config_key_import_json_key: 'Import Service Account JSON Key.',
com_endpoint_config_key_import_json_key_success: 'Successfully Imported Service Account JSON Key',
com_endpoint_config_key_import_json_key_invalid:
'Invalid Service Account JSON Key, Did you import the correct file?',
com_endpoint_config_token_get_edge_key: 'To get your Access token for Bing, login to',
com_endpoint_config_token_get_edge_key_dev_tool:
com_endpoint_config_key_get_edge_key: 'To get your Access token for Bing, login to',
com_endpoint_config_key_get_edge_key_dev_tool:
'Use dev tools or an extension while logged into the site to copy the content of the _U cookie. If this fails, follow these',
com_endpoint_config_token_edge_instructions: 'instructions',
com_endpoint_config_token_edge_full_token_string: 'to provide the full cookie strings.',
com_endpoint_config_token_chatgpt:
'To get your Access token For ChatGPT \'Free Version\', login to',
com_endpoint_config_token_chatgpt_then_visit: 'then visit',
com_endpoint_config_token_chatgpt_copy_token: 'Copy access token.',
com_endpoint_config_token_google_need_to: 'You need to',
com_endpoint_config_token_google_vertex_ai: 'Enable Vertex AI',
com_endpoint_config_token_google_vertex_api: 'API on Google Cloud, then',
com_endpoint_config_token_google_service_account: 'Create a Service Account',
com_endpoint_config_token_google_vertex_api_role:
com_endpoint_config_key_edge_instructions: 'instructions',
com_endpoint_config_key_edge_full_key_string: 'to provide the full cookie strings.',
com_endpoint_config_key_chatgpt: 'To get your Access token For ChatGPT \'Free Version\', login to',
com_endpoint_config_key_chatgpt_then_visit: 'then visit',
com_endpoint_config_key_chatgpt_copy_token: 'Copy access token.',
com_endpoint_config_key_google_need_to: 'You need to',
com_endpoint_config_key_google_vertex_ai: 'Enable Vertex AI',
com_endpoint_config_key_google_vertex_api: 'API on Google Cloud, then',
com_endpoint_config_key_google_service_account: 'Create a Service Account',
com_endpoint_config_key_google_vertex_api_role:
'Make sure to click \'Create and Continue\' to give at least the \'Vertex AI User\' role. Lastly, create a JSON key to import here.',
com_nav_export_filename: 'Filename',
com_nav_export_filename_placeholder: 'Set the filename',
@ -240,6 +249,7 @@ export default {
com_nav_settings: 'Settings',
com_nav_search_placeholder: 'Search messages',
com_nav_setting_general: 'General',
com_nav_setting_data: 'Data controls',
com_nav_language: 'Language',
com_nav_lang_english: 'English',
com_nav_lang_chinese: '中文',

View file

@ -29,7 +29,8 @@ export default {
com_ui_showing: 'Mostrando',
com_ui_of: 'de',
com_ui_entries: 'Entradas',
com_ui_pay_per_call: 'Todas las conversaciones de IA en un solo lugar. Pague por llamada y no por mes.',
com_ui_pay_per_call:
'Todas las conversaciones de IA en un solo lugar. Pague por llamada y no por mes.',
com_ui_delete: 'Eliminar',
com_ui_delete_conversation: '¿Eliminar conversación?',
com_ui_delete_conversation_confirm: 'Esto eliminará',
@ -84,6 +85,7 @@ export default {
com_auth_to_try_again: 'para intentar nuevamente.',
com_auth_submit_registration: 'Enviar registro',
com_auth_welcome_back: 'Bienvenido(a) de vuelta',
com_endpoint_open_menu: 'Abrir Menú',
com_endpoint_bing_enable_sydney: 'Habilitar Sydney',
com_endpoint_bing_to_enable_sydney: 'Para habilitar Sydney',
com_endpoint_bing_jailbreak: 'Jailbreak',

View file

@ -83,6 +83,7 @@ export default {
com_auth_to_try_again: 'pour réessayer.',
com_auth_submit_registration: 'Soumettre l\'inscription',
com_auth_welcome_back: 'Bienvenue à nouveau',
com_endpoint_open_menu: 'Ouvrir le menu',
com_endpoint_bing_enable_sydney: 'Activer Sydney',
com_endpoint_bing_to_enable_sydney: 'Pour activer Sydney',
com_endpoint_bing_jailbreak: 'Jailbreak',

View file

@ -96,6 +96,7 @@ export default {
com_auth_to_try_again: 'per riprovare.',
com_auth_submit_registration: 'Invia registrazione',
com_auth_welcome_back: 'Bentornato',
com_endpoint_open_menu: 'Apri menu',
com_endpoint_bing_enable_sydney: 'Abilita Sydney',
com_endpoint_bing_to_enable_sydney: 'Per abilitare Sydney',
com_endpoint_bing_jailbreak: 'Jailbreak',

View file

@ -80,6 +80,7 @@ export default {
com_auth_to_try_again: 'aby spróbować ponownie.',
com_auth_submit_registration: 'Zarejestruj się',
com_auth_welcome_back: 'Witamy z powrotem',
com_endpoint_open_menu: 'Otwórz menu',
com_endpoint_bing_enable_sydney: 'Aktywuj Sydney',
com_endpoint_bing_to_enable_sydney: 'Aby aktywować Sydney',
com_endpoint_bing_jailbreak: 'Odblokuj',

View file

@ -80,6 +80,7 @@ export default {
com_auth_to_try_again: 'чтобы попробовать снова.',
com_auth_submit_registration: 'Отправить регистрацию',
com_auth_welcome_back: 'С возвращением',
com_endpoint_open_menu: 'Открыть меню',
com_endpoint_bing_enable_sydney: 'Включить Сидней',
com_endpoint_bing_to_enable_sydney: 'Чтобы включить Сидней',
com_endpoint_bing_jailbreak: 'Jailbreak',

View file

@ -74,6 +74,7 @@ export default {
com_auth_to_try_again: '再试一次.',
com_auth_submit_registration: '注册提交',
com_auth_welcome_back: '欢迎',
com_endpoint_open_menu: '打开菜单',
com_endpoint_bing_enable_sydney: '启用 Sydney',
com_endpoint_bing_to_enable_sydney: '启用 Sydney',
com_endpoint_bing_jailbreak: '破解',

View file

@ -6,7 +6,6 @@ import text from './text';
import submission from './submission';
import search from './search';
import preset from './preset';
import token from './token';
import lang from './language';
import optionSettings from './optionSettings';
@ -19,7 +18,6 @@ export default {
...submission,
...search,
...preset,
...token,
...lang,
...optionSettings,
};

View file

@ -1,21 +0,0 @@
import { atom, useSetRecoilState } from 'recoil';
const tokenRefreshHints = atom<number>({
key: 'tokenRefreshHints',
default: 1,
});
const useToken = (endpoint: string) => {
const setHints = useSetRecoilState(tokenRefreshHints);
const getToken = () => localStorage.getItem(`${endpoint}_token`);
const saveToken = (value: string) => {
localStorage.setItem(`${endpoint}_token`, value);
setHints((prev) => prev + 1);
};
return { token: getToken(), getToken, saveToken };
};
export default {
useToken,
};

View file

@ -18,10 +18,10 @@ const getError = (text: string) => {
} else if (json.type === 'insufficient_quota') {
return 'We apologize for any inconvenience caused. The default API key has reached its limit. To continue using this service, please set up your own API key. You can do this by clicking on the model logo in the left corner of the textbox and selecting "Set Token" for the current selected endpoint. Thank you for your understanding.';
} else {
return `Oops! Something went wrong. Please try again in a few moments. Here's the specific error message we encountered: ${errorMessage}`;
return `Something went wrong. Here's the specific error message we encountered: ${errorMessage}`;
}
} else {
return `Oops! Something went wrong. Please try again in a few moments. Here's the specific error message we encountered: ${errorMessage}`;
return `Something went wrong. Here's the specific error message we encountered: ${errorMessage}`;
}
};