mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 00:40:14 +01:00
* 🗑️ chore: Remove unused Legacy Provider clients and related helpers * Deleted OpenAIClient and GoogleClient files along with their associated tests. * Removed references to these clients in the clients index file. * Cleaned up typedefs by removing the OpenAISpecClient export. * Updated chat controllers to use the OpenAI SDK directly instead of the removed client classes. * chore/remove-openapi-specs * 🗑️ chore: Remove unused mergeSort and misc utility functions * Deleted mergeSort.js and misc.js files as they are no longer needed. * Removed references to cleanUpPrimaryKeyValue in messages.js and adjusted related logic. * Updated mongoMeili.ts to eliminate local implementations of removed functions. * chore: remove legacy endpoints * chore: remove all plugins endpoint related code * chore: remove unused prompt handling code and clean up imports * Deleted handleInputs.js and instructions.js files as they are no longer needed. * Removed references to these files in the prompts index.js. * Updated docker-compose.yml to simplify reverse proxy configuration. * chore: remove unused LightningIcon import from Icons.tsx * chore: clean up translation.json by removing deprecated and unused keys * chore: update Jest configuration and remove unused mock file * Simplified the setupFiles array in jest.config.js by removing the fetchEventSource mock. * Deleted the fetchEventSource.js mock file as it is no longer needed. * fix: simplify endpoint type check in Landing and ConversationStarters components * Updated the endpoint type check to use strict equality for better clarity and performance. * Ensured consistency in the handling of the azureOpenAI endpoint across both components. * chore: remove unused dependencies from package.json and package-lock.json * chore: remove legacy EditController, associated routes and imports * chore: update banResponse logic to refine request handling for banned users * chore: remove unused validateEndpoint middleware and its references * chore: remove unused 'res' parameter from initializeClient in multiple endpoint files * chore: remove unused 'isSmallScreen' prop from BookmarkNav and NewChat components; clean up imports in ArchivedChatsTable and useSetIndexOptions hooks; enhance localization in PromptVersions * chore: remove unused import of Constants and TMessage from MobileNav; retain only necessary QueryKeys import * chore: remove unused TResPlugin type and related references; clean up imports in types and schemas
327 lines
9.8 KiB
TypeScript
327 lines
9.8 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { useForm, FormProvider } from 'react-hook-form';
|
|
import { EModelEndpoint, alternateName, isAssistantsEndpoint } from 'librechat-data-provider';
|
|
import {
|
|
useRevokeUserKeyMutation,
|
|
useRevokeAllUserKeysMutation,
|
|
} from 'librechat-data-provider/react-query';
|
|
import {
|
|
Label,
|
|
Button,
|
|
Spinner,
|
|
OGDialog,
|
|
Dropdown,
|
|
OGDialogTitle,
|
|
OGDialogHeader,
|
|
OGDialogFooter,
|
|
OGDialogContent,
|
|
useToastContext,
|
|
OGDialogTrigger,
|
|
} from '@librechat/client';
|
|
import type { TDialogProps } from '~/common';
|
|
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.assistants]: OpenAIConfig,
|
|
[EModelEndpoint.azureAssistants]: OpenAIConfig,
|
|
default: OtherConfig,
|
|
};
|
|
|
|
const formSet: Set<string> = new Set([
|
|
EModelEndpoint.openAI,
|
|
EModelEndpoint.custom,
|
|
EModelEndpoint.azureOpenAI,
|
|
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: '',
|
|
// TODO: allow endpoint definitions from user
|
|
// name: '',
|
|
// TODO: add custom endpoint models defined by user
|
|
// models: '',
|
|
},
|
|
});
|
|
|
|
const [userKey, setUserKey] = useState('');
|
|
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 ?? '')) {
|
|
// TODO: handle other user provided options besides baseURL and apiKey
|
|
methods.handleSubmit((data) => {
|
|
const isAzure = endpoint === EModelEndpoint.azureOpenAI;
|
|
const isOpenAIBase =
|
|
isAzure || endpoint === EModelEndpoint.openAI || 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();
|
|
|
|
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}
|
|
endpoint={endpoint}
|
|
setUserKey={setUserKey}
|
|
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;
|