| 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; |
|
|