Compare commits
6 Commits
feat/docum
...
fix/openid
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f7dc13c30 | ||
|
|
ac2e1b1586 | ||
|
|
45e4e70986 | ||
|
|
deb8a00e27 | ||
|
|
b45ff8e4ed | ||
|
|
fc8d24fa5b |
@@ -1,6 +1,8 @@
|
||||
// file deepcode ignore NoRateLimitingForLogin: Rate limiting is handled by the `loginLimiter` middleware
|
||||
const express = require('express');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const passport = require('passport');
|
||||
const client = require('openid-client');
|
||||
const {
|
||||
checkBan,
|
||||
logHeaders,
|
||||
@@ -19,6 +21,8 @@ const domains = {
|
||||
server: process.env.DOMAIN_SERVER,
|
||||
};
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || process.env.OPENID_SESSION_SECRET;
|
||||
|
||||
router.use(logHeaders);
|
||||
router.use(loginLimiter);
|
||||
|
||||
@@ -103,20 +107,71 @@ router.get(
|
||||
/**
|
||||
* OpenID Routes
|
||||
*/
|
||||
router.get(
|
||||
'/openid',
|
||||
passport.authenticate('openid', {
|
||||
session: false,
|
||||
}),
|
||||
);
|
||||
router.get('/openid', (req, res, next) => {
|
||||
const state = client.randomState();
|
||||
|
||||
try {
|
||||
const stateToken = jwt.sign(
|
||||
{
|
||||
state: state,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
JWT_SECRET,
|
||||
{ expiresIn: '10m' },
|
||||
);
|
||||
|
||||
res.cookie('oauth_state', stateToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
signed: false,
|
||||
maxAge: 10 * 60 * 1000,
|
||||
sameSite: 'lax',
|
||||
});
|
||||
passport.authenticate('openid', {
|
||||
session: false,
|
||||
state: state,
|
||||
})(req, res, next);
|
||||
} catch (error) {
|
||||
logger.error('Error creating state token for OpenID authentication', error);
|
||||
return res.redirect(`${domains.client}/oauth/error`);
|
||||
}
|
||||
});
|
||||
|
||||
router.get(
|
||||
'/openid/callback',
|
||||
passport.authenticate('openid', {
|
||||
failureRedirect: `${domains.client}/oauth/error`,
|
||||
failureMessage: true,
|
||||
session: false,
|
||||
}),
|
||||
(req, res, next) => {
|
||||
if (!req.query.state) {
|
||||
logger.error('Missing state parameter in OpenID callback');
|
||||
return res.redirect(`${domains.client}/oauth/error`);
|
||||
}
|
||||
|
||||
const stateToken = req.cookies.oauth_state;
|
||||
if (!stateToken) {
|
||||
logger.error('No state cookie found for OpenID callback');
|
||||
return res.redirect(`${domains.client}/oauth/error`);
|
||||
}
|
||||
|
||||
try {
|
||||
const decodedState = jwt.verify(stateToken, JWT_SECRET);
|
||||
if (req.query.state !== decodedState.state) {
|
||||
logger.error('Invalid state parameter in OpenID callback', {
|
||||
received: req.query.state,
|
||||
expected: decodedState.state,
|
||||
});
|
||||
return res.redirect(`${domains.client}/oauth/error`);
|
||||
}
|
||||
res.clearCookie('oauth_state');
|
||||
passport.authenticate('openid', {
|
||||
failureRedirect: `${domains.client}/oauth/error`,
|
||||
failureMessage: true,
|
||||
session: false,
|
||||
})(req, res, next);
|
||||
} catch (error) {
|
||||
logger.error('Invalid or expired state token in OpenID callback', error);
|
||||
res.clearCookie('oauth_state');
|
||||
return res.redirect(`${domains.client}/oauth/error`);
|
||||
}
|
||||
},
|
||||
setBalanceConfig,
|
||||
oauthHandler,
|
||||
);
|
||||
|
||||
@@ -28,6 +28,17 @@ class CustomOpenIDStrategy extends OpenIDStrategy {
|
||||
const hostAndProtocol = process.env.DOMAIN_SERVER;
|
||||
return new URL(`${hostAndProtocol}${req.originalUrl ?? req.url}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override to ensure proper authorization request parameters
|
||||
*/
|
||||
authorizationRequestParams(req, options) {
|
||||
const params = super.authorizationRequestParams?.(req, options) || {};
|
||||
if (options?.state != null && options.state && !params.has('state')) {
|
||||
params.set('state', options.state);
|
||||
}
|
||||
return params;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
"react-i18next": "^15.4.0",
|
||||
"react-lazy-load-image-component": "^1.6.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-resizable-panels": "^2.1.8",
|
||||
"react-resizable-panels": "^3.0.2",
|
||||
"react-router-dom": "^6.11.2",
|
||||
"react-speech-recognition": "^3.10.0",
|
||||
"react-textarea-autosize": "^8.4.0",
|
||||
@@ -139,6 +139,7 @@
|
||||
"postcss": "^8.4.31",
|
||||
"postcss-loader": "^7.1.0",
|
||||
"postcss-preset-env": "^8.2.0",
|
||||
"rollup-plugin-visualizer": "^6.0.0",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"ts-jest": "^29.2.5",
|
||||
"typescript": "^5.3.3",
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function PopoverButtons({
|
||||
buttonClass?: string;
|
||||
iconClass?: string;
|
||||
endpoint?: EModelEndpoint | string;
|
||||
endpointType?: EModelEndpoint | string;
|
||||
endpointType?: EModelEndpoint | string | null;
|
||||
model?: string | null;
|
||||
}) {
|
||||
const {
|
||||
|
||||
@@ -10,10 +10,16 @@ import {
|
||||
mapEndpoints,
|
||||
getConvoSwitchLogic,
|
||||
} from '~/utils';
|
||||
import { Input, Label, SelectDropDown, Dialog, DialogClose, DialogButton } from '~/components';
|
||||
import {
|
||||
Input,
|
||||
Label,
|
||||
OGDialog,
|
||||
OGDialogTitle,
|
||||
SelectDropDown,
|
||||
OGDialogContent,
|
||||
} from '~/components';
|
||||
import { useSetIndexOptions, useLocalize, useDebouncedInput } from '~/hooks';
|
||||
import PopoverButtons from '~/components/Chat/Input/PopoverButtons';
|
||||
import DialogTemplate from '~/components/ui/DialogTemplate';
|
||||
import { EndpointSettings } from '~/components/Endpoints';
|
||||
import { useGetEndpointsQuery } from '~/data-provider';
|
||||
import { useChatContext } from '~/Providers';
|
||||
@@ -117,111 +123,107 @@ const EditPresetDialog = ({
|
||||
[queryClient, setOptions],
|
||||
);
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
setPresetModalVisible(open);
|
||||
if (!open) {
|
||||
setPreset(null);
|
||||
}
|
||||
};
|
||||
|
||||
const { endpoint: _endpoint, endpointType, model } = preset || {};
|
||||
const endpoint = _endpoint ?? '';
|
||||
|
||||
if (!endpoint) {
|
||||
return null;
|
||||
} else if (isAgentsEndpoint(endpoint)) {
|
||||
}
|
||||
|
||||
if (isAgentsEndpoint(endpoint)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={presetModalVisible}
|
||||
onOpenChange={(open) => {
|
||||
setPresetModalVisible(open);
|
||||
if (!open) {
|
||||
setPreset(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTemplate
|
||||
title={`${localize('com_ui_edit') + ' ' + localize('com_endpoint_preset')} - ${
|
||||
preset?.title
|
||||
}`}
|
||||
className="h-full max-w-full overflow-y-auto pb-4 sm:w-[680px] sm:pb-0 md:h-[720px] md:w-[750px] md:overflow-y-hidden lg:w-[950px] xl:h-[720px]"
|
||||
main={
|
||||
<div className="flex w-full flex-col items-center gap-2 md:h-[550px] md:overflow-y-auto">
|
||||
<div className="grid w-full">
|
||||
<div className="col-span-4 flex flex-col items-start justify-start gap-6 pb-4 md:flex-row">
|
||||
<div className="flex w-full flex-col">
|
||||
<Label htmlFor="preset-name" className="mb-1 text-left text-sm font-medium">
|
||||
{localize('com_endpoint_preset_name')}
|
||||
</Label>
|
||||
<Input
|
||||
id="preset-name"
|
||||
value={(title as string | undefined) ?? ''}
|
||||
onChange={onTitleChange}
|
||||
placeholder={localize('com_endpoint_set_custom_name')}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex h-10 max-h-10 w-full resize-none px-3 py-2',
|
||||
removeFocusOutlines,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full flex-col">
|
||||
<Label htmlFor="endpoint" className="mb-1 text-left text-sm font-medium">
|
||||
{localize('com_endpoint')}
|
||||
</Label>
|
||||
<SelectDropDown
|
||||
value={endpoint || ''}
|
||||
setValue={switchEndpoint}
|
||||
showLabel={false}
|
||||
emptyTitle={true}
|
||||
searchPlaceholder={localize('com_endpoint_search')}
|
||||
availableValues={availableEndpoints}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 flex items-start justify-between gap-4 sm:col-span-4">
|
||||
<div className="flex w-full flex-col">
|
||||
<Label
|
||||
htmlFor="endpoint"
|
||||
className="mb-1 hidden text-left text-sm font-medium sm:block"
|
||||
>
|
||||
{'ㅤ'}
|
||||
</Label>
|
||||
<PopoverButtons
|
||||
buttonClass="ml-0 w-full border border-border-medium p-2 h-[40px] justify-center mt-0"
|
||||
iconClass="hidden lg:block w-4 "
|
||||
endpoint={endpoint}
|
||||
endpointType={endpointType}
|
||||
model={model}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<OGDialog open={presetModalVisible} onOpenChange={handleOpenChange}>
|
||||
<OGDialogContent className="h-[100dvh] max-h-[100dvh] w-full max-w-full overflow-y-auto bg-white dark:border-gray-700 dark:bg-gray-850 dark:text-gray-300 md:h-auto md:max-h-[90vh] md:max-w-[75vw] md:rounded-lg lg:max-w-[950px]">
|
||||
<OGDialogTitle>
|
||||
{`${localize('com_ui_edit')} ${localize('com_endpoint_preset')} - ${preset?.title}`}
|
||||
</OGDialogTitle>
|
||||
|
||||
<div className="flex w-full flex-col gap-2 px-1 pb-4 md:gap-4">
|
||||
{/* Header section with preset name and endpoint */}
|
||||
<div className="grid w-full gap-2 md:grid-cols-2 md:gap-4">
|
||||
<div className="flex w-full flex-col">
|
||||
<Label htmlFor="preset-name" className="mb-1 text-left text-sm font-medium">
|
||||
{localize('com_endpoint_preset_name')}
|
||||
</Label>
|
||||
<Input
|
||||
id="preset-name"
|
||||
value={(title as string | undefined) ?? ''}
|
||||
onChange={onTitleChange}
|
||||
placeholder={localize('com_endpoint_set_custom_name')}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex h-10 max-h-10 w-full resize-none px-3 py-2',
|
||||
removeFocusOutlines,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="my-4 w-full border-t border-border-medium" />
|
||||
<div className="w-full p-0">
|
||||
<EndpointSettings
|
||||
conversation={preset}
|
||||
setOption={setOption}
|
||||
isPreset={true}
|
||||
className="h-full text-text-primary md:mb-4 md:h-[440px]"
|
||||
<div className="flex w-full flex-col">
|
||||
<Label htmlFor="endpoint" className="mb-1 text-left text-sm font-medium">
|
||||
{localize('com_endpoint')}
|
||||
</Label>
|
||||
<SelectDropDown
|
||||
value={endpoint || ''}
|
||||
setValue={switchEndpoint}
|
||||
showLabel={false}
|
||||
emptyTitle={true}
|
||||
searchPlaceholder={localize('com_endpoint_search')}
|
||||
availableValues={availableEndpoints}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
buttons={
|
||||
<div className="mb-6 md:mb-2">
|
||||
<DialogButton
|
||||
|
||||
{/* PopoverButtons section */}
|
||||
<div className="flex w-full">
|
||||
<PopoverButtons
|
||||
buttonClass="ml-0 w-full border border-border-medium p-2 h-[40px] justify-center mt-0"
|
||||
iconClass="hidden lg:block w-4"
|
||||
endpoint={endpoint}
|
||||
endpointType={endpointType}
|
||||
model={model}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Separator */}
|
||||
<div className="w-full border-t border-border-medium" />
|
||||
|
||||
{/* Settings section */}
|
||||
<div className="w-full flex-1">
|
||||
<EndpointSettings
|
||||
conversation={preset}
|
||||
setOption={setOption}
|
||||
isPreset={true}
|
||||
className="text-text-primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex justify-end gap-2 border-t border-border-medium pt-2 md:pt-4">
|
||||
<button
|
||||
onClick={exportPreset}
|
||||
className="border-gray-100 hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-600"
|
||||
className="rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700 md:px-4"
|
||||
>
|
||||
{localize('com_endpoint_export')}
|
||||
</DialogButton>
|
||||
<DialogClose
|
||||
</button>
|
||||
<button
|
||||
onClick={submitPreset}
|
||||
className="ml-2 bg-green-500 text-white hover:bg-green-600 dark:hover:bg-green-600"
|
||||
className="rounded-md bg-green-500 px-3 py-2 text-sm font-medium text-white hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 md:px-4"
|
||||
>
|
||||
{localize('com_ui_save')}
|
||||
</DialogClose>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
footerClassName="bg-white dark:bg-gray-700"
|
||||
/>
|
||||
</Dialog>
|
||||
</div>
|
||||
</OGDialogContent>
|
||||
</OGDialog>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ const SidePanel = ({
|
||||
setFullCollapse(true);
|
||||
localStorage.setItem('fullPanelCollapse', 'true');
|
||||
panelRef.current?.collapse();
|
||||
}, []);
|
||||
}, [panelRef, setMinSize, setIsCollapsed, setFullCollapse, setCollapsedSize]);
|
||||
|
||||
const Links = useSideNavLinks({
|
||||
endpoint,
|
||||
@@ -107,7 +107,17 @@ const SidePanel = ({
|
||||
} else {
|
||||
panelRef.current?.expand();
|
||||
}
|
||||
}, [isCollapsed, newUser, setNewUser, navCollapsedSize]);
|
||||
}, [
|
||||
newUser,
|
||||
panelRef,
|
||||
setNewUser,
|
||||
setMinSize,
|
||||
isCollapsed,
|
||||
setIsCollapsed,
|
||||
setFullCollapse,
|
||||
setCollapsedSize,
|
||||
navCollapsedSize,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -137,7 +147,7 @@ const SidePanel = ({
|
||||
<ResizablePanel
|
||||
tagName="nav"
|
||||
id="controls-nav"
|
||||
order={hasArtifacts != null ? 3 : 2}
|
||||
order={hasArtifacts ? 3 : 2}
|
||||
aria-label={localize('com_ui_controls')}
|
||||
role="navigation"
|
||||
collapsedSize={collapsedSize}
|
||||
|
||||
@@ -60,11 +60,12 @@ const SidePanelGroup = ({
|
||||
|
||||
const currentLayout = useMemo(() => normalizeLayout(calculateLayout()), [calculateLayout]);
|
||||
|
||||
const throttledSaveLayout = useCallback(
|
||||
throttle((sizes: number[]) => {
|
||||
const normalizedSizes = normalizeLayout(sizes);
|
||||
localStorage.setItem('react-resizable-panels:layout', JSON.stringify(normalizedSizes));
|
||||
}, 350),
|
||||
const throttledSaveLayout = useMemo(
|
||||
() =>
|
||||
throttle((sizes: number[]) => {
|
||||
const normalizedSizes = normalizeLayout(sizes);
|
||||
localStorage.setItem('react-resizable-panels:layout', JSON.stringify(normalizedSizes));
|
||||
}, 350),
|
||||
[],
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
useRef,
|
||||
useMemo,
|
||||
useState,
|
||||
useEffect,
|
||||
@@ -6,10 +7,10 @@ import {
|
||||
useContext,
|
||||
useCallback,
|
||||
createContext,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { debounce } from 'lodash';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { setTokenHeader, SystemRoles } from 'librechat-data-provider';
|
||||
import type * as t from 'librechat-data-provider';
|
||||
import {
|
||||
@@ -47,27 +48,31 @@ const AuthContextProvider = ({
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const setUserContext = useCallback(
|
||||
(userContext: TUserContext) => {
|
||||
const { token, isAuthenticated, user, redirect } = userContext;
|
||||
setUser(user);
|
||||
setToken(token);
|
||||
//@ts-ignore - ok for token to be undefined initially
|
||||
setTokenHeader(token);
|
||||
setIsAuthenticated(isAuthenticated);
|
||||
// Use a custom redirect if set
|
||||
const finalRedirect = logoutRedirectRef.current || redirect;
|
||||
// Clear the stored redirect
|
||||
logoutRedirectRef.current = undefined;
|
||||
if (finalRedirect == null) {
|
||||
return;
|
||||
}
|
||||
if (finalRedirect.startsWith('http://') || finalRedirect.startsWith('https://')) {
|
||||
window.location.href = finalRedirect;
|
||||
} else {
|
||||
navigate(finalRedirect, { replace: true });
|
||||
}
|
||||
},
|
||||
const setUserContext = useMemo(
|
||||
() =>
|
||||
debounce((userContext: TUserContext) => {
|
||||
const { token, isAuthenticated, user, redirect } = userContext;
|
||||
setUser(user);
|
||||
setToken(token);
|
||||
//@ts-ignore - ok for token to be undefined initially
|
||||
setTokenHeader(token);
|
||||
setIsAuthenticated(isAuthenticated);
|
||||
|
||||
// Use a custom redirect if set
|
||||
const finalRedirect = logoutRedirectRef.current || redirect;
|
||||
// Clear the stored redirect
|
||||
logoutRedirectRef.current = undefined;
|
||||
|
||||
if (finalRedirect == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (finalRedirect.startsWith('http://') || finalRedirect.startsWith('https://')) {
|
||||
window.location.href = finalRedirect;
|
||||
} else {
|
||||
navigate(finalRedirect, { replace: true });
|
||||
}
|
||||
}, 50),
|
||||
[navigate, setUser],
|
||||
);
|
||||
const doSetError = useTimeout({ callback: (error) => setError(error as string | undefined) });
|
||||
|
||||
@@ -34,20 +34,20 @@ const assistantMapFn =
|
||||
assistantMap: TAssistantsMap;
|
||||
endpointsConfig: TEndpointsConfig;
|
||||
}) =>
|
||||
({ id, name, description }) => ({
|
||||
type: endpoint,
|
||||
label: name ?? '',
|
||||
value: id,
|
||||
description: description ?? '',
|
||||
icon: EndpointIcon({
|
||||
conversation: { assistant_id: id, endpoint },
|
||||
containerClassName: 'shadow-stroke overflow-hidden rounded-full',
|
||||
endpointsConfig: endpointsConfig,
|
||||
context: 'menu-item',
|
||||
assistantMap,
|
||||
size: 20,
|
||||
}),
|
||||
});
|
||||
({ id, name, description }) => ({
|
||||
type: endpoint,
|
||||
label: name ?? '',
|
||||
value: id,
|
||||
description: description ?? '',
|
||||
icon: EndpointIcon({
|
||||
conversation: { assistant_id: id, endpoint },
|
||||
containerClassName: 'shadow-stroke overflow-hidden rounded-full',
|
||||
endpointsConfig: endpointsConfig,
|
||||
context: 'menu-item',
|
||||
assistantMap,
|
||||
size: 20,
|
||||
}),
|
||||
});
|
||||
|
||||
export default function useMentions({
|
||||
assistantMap,
|
||||
@@ -226,7 +226,7 @@ export default function useMentions({
|
||||
assistantListMap,
|
||||
includeAssistants,
|
||||
interfaceConfig.presets,
|
||||
interfaceConfig.endpointsMenu,
|
||||
interfaceConfig.modelSelect,
|
||||
]);
|
||||
|
||||
return {
|
||||
|
||||
224
client/src/utils/__tests__/cleanupPreset.test.ts
Normal file
224
client/src/utils/__tests__/cleanupPreset.test.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
import { EModelEndpoint } from 'librechat-data-provider';
|
||||
import cleanupPreset from '../cleanupPreset';
|
||||
import type { TPreset } from 'librechat-data-provider';
|
||||
|
||||
// Mock parseConvo since we're focusing on testing the chatGptLabel migration logic
|
||||
jest.mock('librechat-data-provider', () => ({
|
||||
...jest.requireActual('librechat-data-provider'),
|
||||
parseConvo: jest.fn((input) => {
|
||||
// Return a simplified mock that passes through most properties
|
||||
const { conversation } = input;
|
||||
return {
|
||||
...conversation,
|
||||
model: conversation?.model || 'gpt-3.5-turbo',
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('cleanupPreset', () => {
|
||||
const basePreset = {
|
||||
presetId: 'test-preset-id',
|
||||
title: 'Test Preset',
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
model: 'gpt-4',
|
||||
temperature: 0.7,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('chatGptLabel migration', () => {
|
||||
it('should migrate chatGptLabel to modelLabel when only chatGptLabel exists', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
chatGptLabel: 'Custom ChatGPT Label',
|
||||
};
|
||||
|
||||
const result = cleanupPreset({ preset });
|
||||
|
||||
expect(result.modelLabel).toBe('Custom ChatGPT Label');
|
||||
expect(result.chatGptLabel).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should prioritize modelLabel over chatGptLabel when both exist', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
chatGptLabel: 'Old ChatGPT Label',
|
||||
modelLabel: 'New Model Label',
|
||||
};
|
||||
|
||||
const result = cleanupPreset({ preset });
|
||||
|
||||
expect(result.modelLabel).toBe('New Model Label');
|
||||
expect(result.chatGptLabel).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should keep modelLabel when only modelLabel exists', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
modelLabel: 'Existing Model Label',
|
||||
};
|
||||
|
||||
const result = cleanupPreset({ preset });
|
||||
|
||||
expect(result.modelLabel).toBe('Existing Model Label');
|
||||
expect(result.chatGptLabel).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle preset without either label', () => {
|
||||
const preset = { ...basePreset };
|
||||
|
||||
const result = cleanupPreset({ preset });
|
||||
|
||||
expect(result.modelLabel).toBeUndefined();
|
||||
expect(result.chatGptLabel).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle empty chatGptLabel', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
chatGptLabel: '',
|
||||
modelLabel: 'Valid Model Label',
|
||||
};
|
||||
|
||||
const result = cleanupPreset({ preset });
|
||||
|
||||
expect(result.modelLabel).toBe('Valid Model Label');
|
||||
expect(result.chatGptLabel).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not migrate empty string chatGptLabel when modelLabel exists', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
chatGptLabel: '',
|
||||
};
|
||||
|
||||
const result = cleanupPreset({ preset });
|
||||
|
||||
expect(result.modelLabel).toBeUndefined();
|
||||
expect(result.chatGptLabel).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('presetOverride handling', () => {
|
||||
it('should apply presetOverride and then handle label migration', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
chatGptLabel: 'Original Label',
|
||||
presetOverride: {
|
||||
modelLabel: 'Override Model Label',
|
||||
temperature: 0.9,
|
||||
},
|
||||
};
|
||||
|
||||
const result = cleanupPreset({ preset });
|
||||
|
||||
expect(result.modelLabel).toBe('Override Model Label');
|
||||
expect(result.chatGptLabel).toBeUndefined();
|
||||
expect(result.temperature).toBe(0.9);
|
||||
});
|
||||
|
||||
it('should handle label migration in presetOverride', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
presetOverride: {
|
||||
chatGptLabel: 'Override ChatGPT Label',
|
||||
},
|
||||
};
|
||||
|
||||
const result = cleanupPreset({ preset });
|
||||
|
||||
expect(result.modelLabel).toBe('Override ChatGPT Label');
|
||||
expect(result.chatGptLabel).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should handle undefined preset', () => {
|
||||
const result = cleanupPreset({ preset: undefined });
|
||||
|
||||
expect(result).toEqual({
|
||||
endpoint: null,
|
||||
presetId: null,
|
||||
title: 'New Preset',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle preset with null endpoint', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
endpoint: null,
|
||||
};
|
||||
|
||||
const result = cleanupPreset({ preset });
|
||||
|
||||
expect(result).toEqual({
|
||||
endpoint: null,
|
||||
presetId: 'test-preset-id',
|
||||
title: 'Test Preset',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle preset with empty string endpoint', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
endpoint: '',
|
||||
};
|
||||
|
||||
const result = cleanupPreset({ preset });
|
||||
|
||||
expect(result).toEqual({
|
||||
endpoint: null,
|
||||
presetId: 'test-preset-id',
|
||||
title: 'Test Preset',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('normal preset properties', () => {
|
||||
it('should preserve all other preset properties', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
promptPrefix: 'Custom prompt:',
|
||||
temperature: 0.8,
|
||||
top_p: 0.9,
|
||||
modelLabel: 'Custom Model',
|
||||
tools: ['plugin1', 'plugin2'],
|
||||
};
|
||||
|
||||
const result = cleanupPreset({ preset });
|
||||
|
||||
expect(result.presetId).toBe('test-preset-id');
|
||||
expect(result.title).toBe('Test Preset');
|
||||
expect(result.endpoint).toBe(EModelEndpoint.openAI);
|
||||
expect(result.modelLabel).toBe('Custom Model');
|
||||
expect(result.promptPrefix).toBe('Custom prompt:');
|
||||
expect(result.temperature).toBe(0.8);
|
||||
expect(result.top_p).toBe(0.9);
|
||||
expect(result.tools).toEqual(['plugin1', 'plugin2']);
|
||||
});
|
||||
|
||||
it('should generate default title when title is missing', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
title: undefined,
|
||||
};
|
||||
|
||||
const result = cleanupPreset({ preset });
|
||||
|
||||
expect(result.title).toBe('New Preset');
|
||||
});
|
||||
|
||||
it('should handle null presetId', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
presetId: null,
|
||||
};
|
||||
|
||||
const result = cleanupPreset({ preset });
|
||||
|
||||
expect(result.presetId).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
362
client/src/utils/__tests__/presets.test.ts
Normal file
362
client/src/utils/__tests__/presets.test.ts
Normal file
@@ -0,0 +1,362 @@
|
||||
import { EModelEndpoint } from 'librechat-data-provider';
|
||||
import { getPresetTitle, removeUnavailableTools } from '../presets';
|
||||
import type { TPreset, TPlugin } from 'librechat-data-provider';
|
||||
|
||||
describe('presets utils', () => {
|
||||
describe('getPresetTitle', () => {
|
||||
const basePreset: TPreset = {
|
||||
presetId: 'test-id',
|
||||
title: 'Test Preset',
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
model: 'gpt-4',
|
||||
};
|
||||
|
||||
describe('with modelLabel', () => {
|
||||
it('should use modelLabel as the label', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
modelLabel: 'Custom Model Name',
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset);
|
||||
|
||||
expect(result).toBe('Test Preset: gpt-4 (Custom Model Name)');
|
||||
});
|
||||
|
||||
it('should prioritize modelLabel over deprecated chatGptLabel', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
modelLabel: 'New Model Label',
|
||||
chatGptLabel: 'Old ChatGPT Label',
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset);
|
||||
|
||||
expect(result).toBe('Test Preset: gpt-4 (New Model Label)');
|
||||
});
|
||||
|
||||
it('should handle title that includes the label', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
title: 'Custom Model Name Settings',
|
||||
modelLabel: 'Custom Model Name',
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset);
|
||||
|
||||
expect(result).toBe('Custom Model Name Settings: gpt-4 (Custom Model Name)');
|
||||
});
|
||||
|
||||
it('should handle case-insensitive title matching', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
title: 'custom model name preset',
|
||||
modelLabel: 'Custom Model Name',
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset);
|
||||
|
||||
expect(result).toBe('custom model name preset: gpt-4 (Custom Model Name)');
|
||||
});
|
||||
|
||||
it('should use label as title when label includes the title', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
title: 'GPT',
|
||||
modelLabel: 'Custom GPT Assistant',
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset);
|
||||
|
||||
expect(result).toBe('Custom GPT Assistant: gpt-4');
|
||||
});
|
||||
});
|
||||
|
||||
describe('without modelLabel', () => {
|
||||
it('should work without modelLabel', () => {
|
||||
const preset = { ...basePreset };
|
||||
|
||||
const result = getPresetTitle(preset);
|
||||
|
||||
expect(result).toBe('Test Preset: gpt-4');
|
||||
});
|
||||
|
||||
it('should handle empty modelLabel', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
modelLabel: '',
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset);
|
||||
|
||||
expect(result).toBe('Test Preset: gpt-4');
|
||||
});
|
||||
|
||||
it('should handle null modelLabel', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
modelLabel: null,
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset);
|
||||
|
||||
expect(result).toBe('Test Preset: gpt-4');
|
||||
});
|
||||
});
|
||||
|
||||
describe('title variations', () => {
|
||||
it('should handle missing title', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
title: null,
|
||||
modelLabel: 'Custom Model',
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset);
|
||||
|
||||
expect(result).toBe('gpt-4 (Custom Model)');
|
||||
});
|
||||
|
||||
it('should handle empty title', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
title: '',
|
||||
modelLabel: 'Custom Model',
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset);
|
||||
|
||||
expect(result).toBe('gpt-4 (Custom Model)');
|
||||
});
|
||||
|
||||
it('should handle "New Chat" title', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
title: 'New Chat',
|
||||
modelLabel: 'Custom Model',
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset);
|
||||
|
||||
expect(result).toBe('gpt-4 (Custom Model)');
|
||||
});
|
||||
|
||||
it('should handle title with whitespace', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
title: ' ',
|
||||
modelLabel: 'Custom Model',
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset);
|
||||
|
||||
expect(result).toBe(': gpt-4 (Custom Model)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('mention mode', () => {
|
||||
it('should return mention format with all components', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
modelLabel: 'Custom Model',
|
||||
promptPrefix: 'You are a helpful assistant',
|
||||
tools: ['plugin1', 'plugin2'] as string[],
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset, true);
|
||||
|
||||
expect(result).toBe(
|
||||
'gpt-4 | Custom Model | You are a helpful assistant | plugin1, plugin2',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle mention format with object tools', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
modelLabel: 'Custom Model',
|
||||
tools: [
|
||||
{ pluginKey: 'plugin1', name: 'Plugin 1' } as TPlugin,
|
||||
{ pluginKey: 'plugin3', name: 'Plugin 3' } as TPlugin,
|
||||
] as TPlugin[],
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset, true);
|
||||
|
||||
expect(result).toBe('gpt-4 | Custom Model | plugin1, plugin3');
|
||||
});
|
||||
|
||||
it('should handle mention format with minimal data', () => {
|
||||
const preset = { ...basePreset };
|
||||
|
||||
const result = getPresetTitle(preset, true);
|
||||
|
||||
expect(result).toBe('gpt-4');
|
||||
});
|
||||
|
||||
it('should handle mention format with only modelLabel', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
modelLabel: 'Custom Model',
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset, true);
|
||||
|
||||
expect(result).toBe('gpt-4 | Custom Model');
|
||||
});
|
||||
|
||||
it('should handle mention format with only promptPrefix', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
promptPrefix: 'Custom prompt',
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset, true);
|
||||
|
||||
expect(result).toBe('gpt-4 | Custom prompt');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle missing model', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
model: null,
|
||||
modelLabel: 'Custom Model',
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset);
|
||||
|
||||
expect(result).toBe('Test Preset: (Custom Model)');
|
||||
});
|
||||
|
||||
it('should handle undefined model', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
model: undefined,
|
||||
modelLabel: 'Custom Model',
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset);
|
||||
|
||||
expect(result).toBe('Test Preset: (Custom Model)');
|
||||
});
|
||||
|
||||
it('should trim the final result', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
title: '',
|
||||
model: '',
|
||||
modelLabel: '',
|
||||
};
|
||||
|
||||
const result = getPresetTitle(preset);
|
||||
|
||||
expect(result).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeUnavailableTools', () => {
|
||||
const basePreset: TPreset = {
|
||||
presetId: 'test-id',
|
||||
title: 'Test Preset',
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
model: 'gpt-4',
|
||||
};
|
||||
|
||||
const availableTools: Record<string, TPlugin | undefined> = {
|
||||
plugin1: { pluginKey: 'plugin1', name: 'Plugin 1' } as TPlugin,
|
||||
plugin2: { pluginKey: 'plugin2', name: 'Plugin 2' } as TPlugin,
|
||||
plugin3: { pluginKey: 'plugin3', name: 'Plugin 3' } as TPlugin,
|
||||
};
|
||||
|
||||
it('should remove unavailable tools from string array', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
tools: ['plugin1', 'unavailable-plugin', 'plugin2'] as string[],
|
||||
};
|
||||
|
||||
const result = removeUnavailableTools(preset, availableTools);
|
||||
|
||||
expect(result.tools).toEqual(['plugin1', 'plugin2']);
|
||||
});
|
||||
|
||||
it('should remove unavailable tools from object array', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
tools: [
|
||||
{ pluginKey: 'plugin1', name: 'Plugin 1' } as TPlugin,
|
||||
{ pluginKey: 'unavailable-plugin', name: 'Unavailable' } as TPlugin,
|
||||
{ pluginKey: 'plugin2', name: 'Plugin 2' } as TPlugin,
|
||||
] as TPlugin[],
|
||||
};
|
||||
|
||||
const result = removeUnavailableTools(preset, availableTools);
|
||||
|
||||
expect(result.tools).toEqual(['plugin1', 'plugin2']);
|
||||
});
|
||||
|
||||
it('should handle preset without tools', () => {
|
||||
const preset = { ...basePreset };
|
||||
|
||||
const result = removeUnavailableTools(preset, availableTools);
|
||||
|
||||
expect(result).toEqual(preset);
|
||||
});
|
||||
|
||||
it('should handle preset with empty tools array', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
tools: [] as string[],
|
||||
};
|
||||
|
||||
const result = removeUnavailableTools(preset, availableTools);
|
||||
|
||||
expect(result.tools).toEqual([]);
|
||||
});
|
||||
|
||||
it('should remove all tools when none are available', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
tools: ['unavailable1', 'unavailable2'] as string[],
|
||||
};
|
||||
|
||||
const result = removeUnavailableTools(preset, {});
|
||||
|
||||
expect(result.tools).toEqual([]);
|
||||
});
|
||||
|
||||
it('should preserve all other preset properties', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
tools: ['plugin1'] as string[],
|
||||
modelLabel: 'Custom Model',
|
||||
temperature: 0.8,
|
||||
promptPrefix: 'Test prompt',
|
||||
};
|
||||
|
||||
const result = removeUnavailableTools(preset, availableTools);
|
||||
|
||||
expect(result.presetId).toBe(preset.presetId);
|
||||
expect(result.title).toBe(preset.title);
|
||||
expect(result.endpoint).toBe(preset.endpoint);
|
||||
expect(result.model).toBe(preset.model);
|
||||
expect(result.modelLabel).toBe(preset.modelLabel);
|
||||
expect(result.temperature).toBe(preset.temperature);
|
||||
expect(result.promptPrefix).toBe(preset.promptPrefix);
|
||||
expect(result.tools).toEqual(['plugin1']);
|
||||
});
|
||||
|
||||
it('should not mutate the original preset', () => {
|
||||
const preset = {
|
||||
...basePreset,
|
||||
tools: ['plugin1', 'unavailable-plugin'] as string[],
|
||||
};
|
||||
const originalTools = [...preset.tools];
|
||||
|
||||
removeUnavailableTools(preset, availableTools);
|
||||
|
||||
expect(preset.tools).toEqual(originalTools);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -20,6 +20,21 @@ const cleanupPreset = ({ preset: _preset }: TCleanupPreset): TPreset => {
|
||||
const { presetOverride = {}, ...rest } = _preset ?? {};
|
||||
const preset = { ...rest, ...presetOverride };
|
||||
|
||||
// Handle deprecated chatGptLabel field
|
||||
// If both chatGptLabel and modelLabel exist, prioritize modelLabel and remove chatGptLabel
|
||||
// If only chatGptLabel exists, migrate it to modelLabel
|
||||
if (preset.chatGptLabel && preset.modelLabel) {
|
||||
// Both exist: prioritize modelLabel, remove chatGptLabel
|
||||
delete preset.chatGptLabel;
|
||||
} else if (preset.chatGptLabel && !preset.modelLabel) {
|
||||
// Only chatGptLabel exists: migrate to modelLabel
|
||||
preset.modelLabel = preset.chatGptLabel;
|
||||
delete preset.chatGptLabel;
|
||||
} else if ('chatGptLabel' in preset) {
|
||||
// chatGptLabel exists but is empty/falsy: remove it
|
||||
delete preset.chatGptLabel;
|
||||
}
|
||||
|
||||
/* @ts-ignore: endpoint can be a custom defined name */
|
||||
const parsedPreset = parseConvo({ endpoint, endpointType, conversation: preset });
|
||||
|
||||
|
||||
@@ -17,18 +17,10 @@ export const getPresetTitle = (preset: TPreset, mention?: boolean) => {
|
||||
let title = '';
|
||||
let label = '';
|
||||
|
||||
const usesChatGPTLabel: TEndpoints = [
|
||||
EModelEndpoint.azureOpenAI,
|
||||
EModelEndpoint.openAI,
|
||||
EModelEndpoint.custom,
|
||||
];
|
||||
const usesModelLabel: TEndpoints = [EModelEndpoint.google, EModelEndpoint.anthropic];
|
||||
|
||||
if (endpoint != null && endpoint && usesChatGPTLabel.includes(endpoint)) {
|
||||
label = chatGptLabel ?? '';
|
||||
} else if (endpoint != null && endpoint && usesModelLabel.includes(endpoint)) {
|
||||
label = modelLabel ?? '';
|
||||
if (modelLabel) {
|
||||
label = modelLabel;
|
||||
}
|
||||
|
||||
if (
|
||||
label &&
|
||||
presetTitle != null &&
|
||||
@@ -47,13 +39,13 @@ export const getPresetTitle = (preset: TPreset, mention?: boolean) => {
|
||||
}${
|
||||
tools
|
||||
? ` | ${tools
|
||||
.map((tool: TPlugin | string) => {
|
||||
if (typeof tool === 'string') {
|
||||
return tool;
|
||||
}
|
||||
return tool.pluginKey;
|
||||
})
|
||||
.join(', ')}`
|
||||
.map((tool: TPlugin | string) => {
|
||||
if (typeof tool === 'string') {
|
||||
return tool;
|
||||
}
|
||||
return tool.pluginKey;
|
||||
})
|
||||
.join(', ')}`
|
||||
: ''
|
||||
}`;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import path, { resolve } from 'path';
|
||||
import path from 'path';
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { VitePWA } from 'vite-plugin-pwa';
|
||||
import { defineConfig } from 'vite';
|
||||
import { nodePolyfills } from 'vite-plugin-node-polyfills';
|
||||
import { visualizer } from 'rollup-plugin-visualizer';
|
||||
import { compression } from 'vite-plugin-compression2';
|
||||
import { nodePolyfills } from 'vite-plugin-node-polyfills';
|
||||
import type { Plugin } from 'vite';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
@@ -84,7 +85,15 @@ export default defineConfig({
|
||||
compression({
|
||||
threshold: 10240,
|
||||
}),
|
||||
],
|
||||
process.env.VITE_BUNDLE_ANALYSIS === 'true' &&
|
||||
visualizer({
|
||||
filename: 'dist/bundle-analysis.html',
|
||||
open: true,
|
||||
gzipSize: true,
|
||||
brotliSize: true,
|
||||
template: 'treemap', // 'treemap' | 'sunburst' | 'network'
|
||||
}),
|
||||
].filter(Boolean),
|
||||
publicDir: './public',
|
||||
build: {
|
||||
sourcemap: process.env.NODE_ENV === 'development',
|
||||
@@ -128,6 +137,41 @@ export default defineConfig({
|
||||
return 'security-ui';
|
||||
}
|
||||
|
||||
if (id.includes('@codemirror/view')) {
|
||||
return 'codemirror-view';
|
||||
}
|
||||
if (id.includes('@codemirror/state')) {
|
||||
return 'codemirror-state';
|
||||
}
|
||||
if (id.includes('@codemirror/language')) {
|
||||
return 'codemirror-language';
|
||||
}
|
||||
if (id.includes('@codemirror')) {
|
||||
return 'codemirror-core';
|
||||
}
|
||||
|
||||
if (id.includes('react-markdown') || id.includes('remark-') || id.includes('rehype-')) {
|
||||
return 'markdown-processing';
|
||||
}
|
||||
if (id.includes('monaco-editor') || id.includes('@monaco-editor')) {
|
||||
return 'code-editor';
|
||||
}
|
||||
if (id.includes('react-window') || id.includes('react-virtual')) {
|
||||
return 'virtualization';
|
||||
}
|
||||
if (id.includes('zod') || id.includes('yup') || id.includes('joi')) {
|
||||
return 'validation';
|
||||
}
|
||||
if (id.includes('axios') || id.includes('ky') || id.includes('fetch')) {
|
||||
return 'http-client';
|
||||
}
|
||||
if (id.includes('react-spring') || id.includes('react-transition-group')) {
|
||||
return 'animations';
|
||||
}
|
||||
if (id.includes('react-select') || id.includes('downshift')) {
|
||||
return 'advanced-inputs';
|
||||
}
|
||||
|
||||
// Existing chunks
|
||||
if (id.includes('@radix-ui')) {
|
||||
return 'radix-ui';
|
||||
@@ -138,7 +182,10 @@ export default defineConfig({
|
||||
if (id.includes('node_modules/highlight.js')) {
|
||||
return 'markdown_highlight';
|
||||
}
|
||||
if (id.includes('node_modules/hast-util-raw') || id.includes('node_modules/katex')) {
|
||||
if (id.includes('katex') || id.includes('node_modules/katex')) {
|
||||
return 'math-katex';
|
||||
}
|
||||
if (id.includes('node_modules/hast-util-raw')) {
|
||||
return 'markdown_large';
|
||||
}
|
||||
if (id.includes('@tanstack')) {
|
||||
|
||||
83
package-lock.json
generated
83
package-lock.json
generated
@@ -2579,7 +2579,7 @@
|
||||
"react-i18next": "^15.4.0",
|
||||
"react-lazy-load-image-component": "^1.6.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-resizable-panels": "^2.1.8",
|
||||
"react-resizable-panels": "^3.0.2",
|
||||
"react-router-dom": "^6.11.2",
|
||||
"react-speech-recognition": "^3.10.0",
|
||||
"react-textarea-autosize": "^8.4.0",
|
||||
@@ -2631,6 +2631,7 @@
|
||||
"postcss": "^8.4.31",
|
||||
"postcss-loader": "^7.1.0",
|
||||
"postcss-preset-env": "^8.2.0",
|
||||
"rollup-plugin-visualizer": "^6.0.0",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"ts-jest": "^29.2.5",
|
||||
"typescript": "^5.3.3",
|
||||
@@ -4623,6 +4624,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"client/node_modules/react-resizable-panels": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-3.0.2.tgz",
|
||||
"integrity": "sha512-j4RNII75fnHkLnbsTb5G5YsDvJsSEZrJK2XSF2z0Tc2jIonYlIVir/Yh/5LvcUFCfs1HqrMAoiBFmIrRjC4XnA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"client/node_modules/ts-jest": {
|
||||
"version": "29.2.5",
|
||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz",
|
||||
@@ -36879,9 +36890,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/open": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz",
|
||||
"integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==",
|
||||
"version": "10.1.2",
|
||||
"resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz",
|
||||
"integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"default-browser": "^5.2.1",
|
||||
@@ -39588,16 +39599,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-resizable-panels": {
|
||||
"version": "2.1.8",
|
||||
"resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.8.tgz",
|
||||
"integrity": "sha512-oDvD0sw34Ecx00cQFLiRJpAE2fCgNLBr8DMrBzkrsaUiLpAycIQoY3eAWfMblDql3pTIMZ60wJ/P89RO1htM2w==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "6.22.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.0.tgz",
|
||||
@@ -40961,6 +40962,60 @@
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup-plugin-visualizer": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-6.0.0.tgz",
|
||||
"integrity": "sha512-9aXBJh1uzI6XNmAeATox2z5MWrEPzL6hQgEMOYxTltWOy5x2ycQCec0Y9fC19sBgf3FvIF36aG9DrvUcdnXPew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"open": "^10.1.2",
|
||||
"picomatch": "^4.0.2",
|
||||
"source-map": "^0.7.4",
|
||||
"yargs": "^17.5.1"
|
||||
},
|
||||
"bin": {
|
||||
"rollup-plugin-visualizer": "dist/bin/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rolldown": "1.x",
|
||||
"rollup": "2.x || 3.x || 4.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"rolldown": {
|
||||
"optional": true
|
||||
},
|
||||
"rollup": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/rollup-plugin-visualizer/node_modules/picomatch": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup-plugin-visualizer/node_modules/source-map": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
|
||||
"integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/router": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
||||
|
||||
Reference in New Issue
Block a user