mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-22 19:30:15 +01:00
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:
parent
64f1557852
commit
4ca43fb53d
122 changed files with 1933 additions and 966 deletions
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { TModelSelectProps, ESide } from '~/common';
|
||||
import type { TModelSelectProps } from '~/common';
|
||||
import { ESide } from '~/common';
|
||||
import {
|
||||
Switch,
|
||||
SelectDropDown,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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=""
|
||||
|
|
|
|||
35
client/src/components/Input/SetKeyDialog/GoogleConfig.tsx
Normal file
35
client/src/components/Input/SetKeyDialog/GoogleConfig.tsx
Normal 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;
|
||||
|
|
@ -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>
|
||||
),
|
||||
};
|
||||
|
|
@ -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',
|
||||
|
|
@ -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'}
|
||||
/>
|
||||
18
client/src/components/Input/SetKeyDialog/OtherConfig.tsx
Normal file
18
client/src/components/Input/SetKeyDialog/OtherConfig.tsx
Normal 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;
|
||||
103
client/src/components/Input/SetKeyDialog/SetKeyDialog.tsx
Normal file
103
client/src/components/Input/SetKeyDialog/SetKeyDialog.tsx
Normal 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;
|
||||
1
client/src/components/Input/SetKeyDialog/index.ts
Normal file
1
client/src/components/Input/SetKeyDialog/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default as SetKeyDialog } from './SetKeyDialog';
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default as SetTokenDialog } from './SetTokenDialog';
|
||||
|
|
@ -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">
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
69
client/src/components/Nav/Settings.tsx
Normal file
69
client/src/components/Nav/Settings.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
70
client/src/components/Nav/SettingsTabs/DangerButton.tsx
Normal file
70
client/src/components/Nav/SettingsTabs/DangerButton.tsx
Normal 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);
|
||||
76
client/src/components/Nav/SettingsTabs/Data.tsx
Normal file
76
client/src/components/Nav/SettingsTabs/Data.tsx
Normal 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);
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,2 +1,4 @@
|
|||
export { default as General } from './General';
|
||||
export { ClearChatsButton } from './General';
|
||||
export { default as Data } from './Data';
|
||||
export { RevokeKeysButton } from './Data';
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
18
client/src/components/svg/DataIcon.tsx
Normal file
18
client/src/components/svg/DataIcon.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
45
client/src/components/ui/Tooltip.tsx
Normal file
45
client/src/components/ui/Tooltip.tsx
Normal 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 };
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
53
client/src/hooks/useLocalStorage.tsx
Normal file
53
client/src/hooks/useLocalStorage.tsx
Normal 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];
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
58
client/src/hooks/useUserKey.ts
Normal file
58
client/src/hooks/useUserKey.ts
Normal 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;
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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: '中文',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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: '破解',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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}`;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue