| | import React, { useState } from 'react'; |
| | import { useForm, FormProvider } from 'react-hook-form'; |
| | import { |
| | OGDialog, |
| | OGDialogContent, |
| | OGDialogHeader, |
| | OGDialogTitle, |
| | OGDialogFooter, |
| | Dropdown, |
| | useToastContext, |
| | Button, |
| | Label, |
| | OGDialogTrigger, |
| | Spinner, |
| | } from '@librechat/client'; |
| | import { EModelEndpoint, alternateName, isAssistantsEndpoint } from 'librechat-data-provider'; |
| | import { |
| | useRevokeAllUserKeysMutation, |
| | useRevokeUserKeyMutation, |
| | } from 'librechat-data-provider/react-query'; |
| | import type { TDialogProps } from '~/common'; |
| | import { useGetEndpointsQuery } from '~/data-provider'; |
| | import { useUserKey, useLocalize } from '~/hooks'; |
| | import { NotificationSeverity } from '~/common'; |
| | import CustomConfig from './CustomEndpoint'; |
| | import GoogleConfig from './GoogleConfig'; |
| | import OpenAIConfig from './OpenAIConfig'; |
| | import OtherConfig from './OtherConfig'; |
| | import HelpText from './HelpText'; |
| | import { logger } from '~/utils'; |
| |
|
| | const endpointComponents = { |
| | [EModelEndpoint.google]: GoogleConfig, |
| | [EModelEndpoint.openAI]: OpenAIConfig, |
| | [EModelEndpoint.custom]: CustomConfig, |
| | [EModelEndpoint.azureOpenAI]: OpenAIConfig, |
| | [EModelEndpoint.gptPlugins]: OpenAIConfig, |
| | [EModelEndpoint.assistants]: OpenAIConfig, |
| | [EModelEndpoint.azureAssistants]: OpenAIConfig, |
| | default: OtherConfig, |
| | }; |
| |
|
| | const formSet: Set<string> = new Set([ |
| | EModelEndpoint.openAI, |
| | EModelEndpoint.custom, |
| | EModelEndpoint.azureOpenAI, |
| | EModelEndpoint.gptPlugins, |
| | EModelEndpoint.assistants, |
| | EModelEndpoint.azureAssistants, |
| | ]); |
| |
|
| | const EXPIRY = { |
| | THIRTY_MINUTES: { label: 'in 30 minutes', value: 30 * 60 * 1000 }, |
| | TWO_HOURS: { label: 'in 2 hours', value: 2 * 60 * 60 * 1000 }, |
| | TWELVE_HOURS: { label: 'in 12 hours', value: 12 * 60 * 60 * 1000 }, |
| | ONE_DAY: { label: 'in 1 day', value: 24 * 60 * 60 * 1000 }, |
| | ONE_WEEK: { label: 'in 7 days', value: 7 * 24 * 60 * 60 * 1000 }, |
| | ONE_MONTH: { label: 'in 30 days', value: 30 * 24 * 60 * 60 * 1000 }, |
| | NEVER: { label: 'never', value: 0 }, |
| | }; |
| |
|
| | const RevokeKeysButton = ({ |
| | endpoint, |
| | disabled, |
| | setDialogOpen, |
| | }: { |
| | endpoint: string; |
| | disabled: boolean; |
| | setDialogOpen: (open: boolean) => void; |
| | }) => { |
| | const localize = useLocalize(); |
| | const [open, setOpen] = useState(false); |
| | const { showToast } = useToastContext(); |
| | const revokeKeyMutation = useRevokeUserKeyMutation(endpoint); |
| | const revokeKeysMutation = useRevokeAllUserKeysMutation(); |
| |
|
| | const handleSuccess = () => { |
| | showToast({ |
| | message: localize('com_ui_revoke_key_success'), |
| | status: NotificationSeverity.SUCCESS, |
| | }); |
| |
|
| | if (!setDialogOpen) { |
| | return; |
| | } |
| |
|
| | setDialogOpen(false); |
| | }; |
| |
|
| | const handleError = () => { |
| | showToast({ |
| | message: localize('com_ui_revoke_key_error'), |
| | status: NotificationSeverity.ERROR, |
| | }); |
| | }; |
| |
|
| | const onClick = () => { |
| | revokeKeyMutation.mutate( |
| | {}, |
| | { |
| | onSuccess: handleSuccess, |
| | onError: handleError, |
| | }, |
| | ); |
| | }; |
| |
|
| | const isLoading = revokeKeyMutation.isLoading || revokeKeysMutation.isLoading; |
| |
|
| | return ( |
| | <div className="flex items-center justify-between"> |
| | <OGDialog open={open} onOpenChange={setOpen}> |
| | <OGDialogTrigger asChild> |
| | <Button |
| | variant="destructive" |
| | className="flex items-center justify-center rounded-lg transition-colors duration-200" |
| | onClick={() => setOpen(true)} |
| | disabled={disabled} |
| | > |
| | {localize('com_ui_revoke')} |
| | </Button> |
| | </OGDialogTrigger> |
| | <OGDialogContent className="max-w-[450px]"> |
| | <OGDialogHeader> |
| | <OGDialogTitle>{localize('com_ui_revoke_key_endpoint', { 0: endpoint })}</OGDialogTitle> |
| | </OGDialogHeader> |
| | <div className="py-4"> |
| | <Label className="text-left text-sm font-medium"> |
| | {localize('com_ui_revoke_key_confirm')} |
| | </Label> |
| | </div> |
| | <OGDialogFooter> |
| | <Button variant="outline" onClick={() => setOpen(false)}> |
| | {localize('com_ui_cancel')} |
| | </Button> |
| | <Button |
| | variant="destructive" |
| | onClick={onClick} |
| | disabled={isLoading} |
| | className="bg-destructive text-white transition-all duration-200 hover:bg-destructive/80" |
| | > |
| | {isLoading ? <Spinner /> : localize('com_ui_revoke')} |
| | </Button> |
| | </OGDialogFooter> |
| | </OGDialogContent> |
| | </OGDialog> |
| | </div> |
| | ); |
| | }; |
| |
|
| | const SetKeyDialog = ({ |
| | open, |
| | onOpenChange, |
| | endpoint, |
| | endpointType, |
| | userProvideURL, |
| | }: Pick<TDialogProps, 'open' | 'onOpenChange'> & { |
| | endpoint: EModelEndpoint | string; |
| | endpointType?: EModelEndpoint; |
| | userProvideURL?: boolean | null; |
| | }) => { |
| | const methods = useForm({ |
| | defaultValues: { |
| | apiKey: '', |
| | baseURL: '', |
| | azureOpenAIApiKey: '', |
| | azureOpenAIApiInstanceName: '', |
| | azureOpenAIApiDeploymentName: '', |
| | azureOpenAIApiVersion: '', |
| | |
| | |
| | |
| | |
| | }, |
| | }); |
| |
|
| | const [userKey, setUserKey] = useState(''); |
| | const { data: endpointsConfig } = useGetEndpointsQuery(); |
| | const [expiresAtLabel, setExpiresAtLabel] = useState(EXPIRY.TWELVE_HOURS.label); |
| | const { getExpiry, saveUserKey } = useUserKey(endpoint); |
| | const { showToast } = useToastContext(); |
| | const localize = useLocalize(); |
| |
|
| | const expirationOptions = Object.values(EXPIRY); |
| |
|
| | const handleExpirationChange = (label: string) => { |
| | setExpiresAtLabel(label); |
| | }; |
| |
|
| | const submit = () => { |
| | const selectedOption = expirationOptions.find((option) => option.label === expiresAtLabel); |
| | let expiresAt: number | null; |
| |
|
| | if (selectedOption?.value === 0) { |
| | expiresAt = null; |
| | } else { |
| | expiresAt = Date.now() + (selectedOption ? selectedOption.value : 0); |
| | } |
| |
|
| | const saveKey = (key: string) => { |
| | try { |
| | saveUserKey(key, expiresAt); |
| | showToast({ |
| | message: localize('com_ui_save_key_success'), |
| | status: NotificationSeverity.SUCCESS, |
| | }); |
| | onOpenChange(false); |
| | } catch (error) { |
| | logger.error('Error saving user key:', error); |
| | showToast({ |
| | message: localize('com_ui_save_key_error'), |
| | status: NotificationSeverity.ERROR, |
| | }); |
| | } |
| | }; |
| |
|
| | if (formSet.has(endpoint) || formSet.has(endpointType ?? '')) { |
| | |
| | methods.handleSubmit((data) => { |
| | const isAzure = endpoint === EModelEndpoint.azureOpenAI; |
| | const isOpenAIBase = |
| | isAzure || |
| | endpoint === EModelEndpoint.openAI || |
| | endpoint === EModelEndpoint.gptPlugins || |
| | isAssistantsEndpoint(endpoint); |
| | if (isAzure) { |
| | data.apiKey = 'n/a'; |
| | } |
| |
|
| | const emptyValues = Object.keys(data).filter((key) => { |
| | if (!isAzure && key.startsWith('azure')) { |
| | return false; |
| | } |
| | if (isOpenAIBase && key === 'baseURL') { |
| | return false; |
| | } |
| | if (key === 'baseURL' && !(userProvideURL ?? false)) { |
| | return false; |
| | } |
| | return data[key] === ''; |
| | }); |
| |
|
| | if (emptyValues.length > 0) { |
| | showToast({ |
| | message: 'The following fields are required: ' + emptyValues.join(', '), |
| | status: 'error', |
| | }); |
| | onOpenChange(true); |
| | return; |
| | } |
| |
|
| | const { apiKey, baseURL, ...azureOptions } = data; |
| | const userProvidedData = { apiKey, baseURL }; |
| | if (isAzure) { |
| | userProvidedData.apiKey = JSON.stringify({ |
| | azureOpenAIApiKey: azureOptions.azureOpenAIApiKey, |
| | azureOpenAIApiInstanceName: azureOptions.azureOpenAIApiInstanceName, |
| | azureOpenAIApiDeploymentName: azureOptions.azureOpenAIApiDeploymentName, |
| | azureOpenAIApiVersion: azureOptions.azureOpenAIApiVersion, |
| | }); |
| | } |
| |
|
| | saveKey(JSON.stringify(userProvidedData)); |
| | methods.reset(); |
| | })(); |
| | return; |
| | } |
| |
|
| | if (!userKey.trim()) { |
| | showToast({ |
| | message: localize('com_ui_key_required'), |
| | status: NotificationSeverity.ERROR, |
| | }); |
| | return; |
| | } |
| |
|
| | saveKey(userKey); |
| | setUserKey(''); |
| | }; |
| |
|
| | const EndpointComponent = |
| | endpointComponents[endpointType ?? endpoint] ?? endpointComponents['default']; |
| | const expiryTime = getExpiry(); |
| | const config = endpointsConfig?.[endpoint]; |
| |
|
| | return ( |
| | <OGDialog open={open} onOpenChange={onOpenChange}> |
| | <OGDialogContent className="w-11/12 max-w-2xl"> |
| | <OGDialogHeader> |
| | <OGDialogTitle> |
| | {`${localize('com_endpoint_config_key_for')} ${alternateName[endpoint] ?? endpoint}`} |
| | </OGDialogTitle> |
| | </OGDialogHeader> |
| | <div className="grid w-full items-center gap-2 py-4"> |
| | <small className="text-red-600"> |
| | {expiryTime === 'never' |
| | ? localize('com_endpoint_config_key_never_expires') |
| | : `${localize('com_endpoint_config_key_encryption')} ${new Date( |
| | expiryTime ?? 0, |
| | ).toLocaleString()}`} |
| | </small> |
| | <Dropdown |
| | label="Expires " |
| | value={expiresAtLabel} |
| | onChange={handleExpirationChange} |
| | options={expirationOptions.map((option) => option.label)} |
| | sizeClasses="w-[185px]" |
| | portal={false} |
| | /> |
| | <div className="mt-2" /> |
| | <FormProvider {...methods}> |
| | <EndpointComponent |
| | userKey={userKey} |
| | setUserKey={setUserKey} |
| | endpoint={ |
| | endpoint === EModelEndpoint.gptPlugins && (config?.azure ?? false) |
| | ? EModelEndpoint.azureOpenAI |
| | : endpoint |
| | } |
| | userProvideURL={userProvideURL} |
| | /> |
| | </FormProvider> |
| | <HelpText endpoint={endpoint} /> |
| | </div> |
| | <OGDialogFooter> |
| | <RevokeKeysButton |
| | endpoint={endpoint} |
| | disabled={!(expiryTime ?? '')} |
| | setDialogOpen={onOpenChange} |
| | /> |
| | <Button variant="submit" onClick={submit}> |
| | {localize('com_ui_submit')} |
| | </Button> |
| | </OGDialogFooter> |
| | </OGDialogContent> |
| | </OGDialog> |
| | ); |
| | }; |
| |
|
| | export default SetKeyDialog; |
| |
|