mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-03 14:50:19 +01:00
🌐 fix: Preserve URL Query Params Through Auth Refresh and Conversation Init (#12028)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
* 🔗 fix: Preserve URL query params during silent token refresh The silent token refresh on hard navigation was redirecting to '/c/new' without query params, wiping the URL before ChatRoute could read them. Now preserves the current URL (pathname + search) as the redirect fallback, with isSafeRedirect validation. * 🧭 fix: Apply URL query params in ChatRoute initialization ChatRoute now reads URL search params (endpoint, model, agent_id, etc.) and merges them into the preset passed to newConversation(), so the first conversation init already includes the URL param settings. This eliminates the race where useQueryParams fired too late. - Export processValidSettings from useQueryParams for reuse - Add getNewConvoPreset helper in ChatRoute (used in both NEW_CONVO branches) - Query params take precedence over model spec defaults - useQueryParams now waits for endpointsConfig before processing - Skip redundant newQueryConvo when settings are already applied - Clean all URL params via setSearchParams after processing * ✅ test: Update useQueryParams tests for new URL cleanup behavior - Assert setSearchParams called instead of window.history.replaceState - Mock endpoints config in deferred submission and timeout tests * ♻️ refactor: Move processValidSettings to ~/utils and address review findings - Move processValidSettings/parseQueryValue to createChatSearchParams.ts (pure utility, not hook-specific) - Fix processSubmission: use setSearchParams instead of replaceState, move URL cleanup outside data.text guard - Narrow endpointsConfig guard: only block settings application, not prompt-only flows - Convert areSettingsApplied to stable useCallback ([] deps) with conversationRef to avoid interval churn on conversation updates - Replace console.log with logger.log in production paths - Restore explanatory comment on pendingSubmitRef guard - Use for...of in processValidSettings (CLAUDE.md preference) - Remove unused imports from useQueryParams * 🔧 fix: Add areSettingsApplied to effect deps and fix test mocks - Restore areSettingsApplied in main effect deps (stable identity with [] deps, safe to include — satisfies exhaustive-deps lint rule) - Fix all test getQueryData mocks to properly distinguish between startupConfig and endpoints keys - Assert setSearchParams call arguments (URLSearchParams + replace:true) * ✅ test: Assert empty URLSearchParams in setSearchParams calls Tighten setSearchParams assertions to verify the params are empty (toString() === ''), not just that a URLSearchParams instance was passed. * 🔧 test: Update AuthContext tests to navigate to current URL for redirects - Modified test cases to assert navigation to the current URL instead of a hardcoded '/c/new' when no stored redirect exists or when falling back from unsafe stored redirects. - Enhanced test setup to define window.location for accurate simulation of redirect behavior.
This commit is contained in:
parent
7c71875da3
commit
b1771e0a6e
7 changed files with 180 additions and 137 deletions
|
|
@ -168,12 +168,11 @@ const AuthContextProvider = ({
|
|||
if (token) {
|
||||
const storedRedirect = sessionStorage.getItem(SESSION_KEY);
|
||||
sessionStorage.removeItem(SESSION_KEY);
|
||||
setUserContext({
|
||||
user,
|
||||
token,
|
||||
isAuthenticated: true,
|
||||
redirect: storedRedirect && isSafeRedirect(storedRedirect) ? storedRedirect : '/c/new',
|
||||
});
|
||||
const currentUrl = `${window.location.pathname}${window.location.search}`;
|
||||
const fallbackRedirect = isSafeRedirect(currentUrl) ? currentUrl : '/c/new';
|
||||
const redirect =
|
||||
storedRedirect && isSafeRedirect(storedRedirect) ? storedRedirect : fallbackRedirect;
|
||||
setUserContext({ user, token, isAuthenticated: true, redirect });
|
||||
return;
|
||||
}
|
||||
console.log('Token is not present. User is not authenticated.');
|
||||
|
|
|
|||
|
|
@ -220,9 +220,14 @@ describe('useQueryParams', () => {
|
|||
handleSubmit: jest.fn((callback) => () => callback({ text: 'test message' })),
|
||||
});
|
||||
|
||||
// Mock startup config to allow processing
|
||||
(useQueryClient as jest.Mock).mockReturnValue({
|
||||
getQueryData: jest.fn().mockReturnValue({ modelSpecs: { list: [] } }),
|
||||
getQueryData: jest.fn().mockImplementation((key) => {
|
||||
const k = Array.isArray(key) ? key[0] : key;
|
||||
if (k === 'startupConfig') {
|
||||
return { modelSpecs: { list: [] } };
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
});
|
||||
|
||||
setUrlParams({ q: 'hello world' });
|
||||
|
|
@ -241,7 +246,11 @@ describe('useQueryParams', () => {
|
|||
'hello world',
|
||||
expect.objectContaining({ shouldValidate: true }),
|
||||
);
|
||||
expect(window.history.replaceState).toHaveBeenCalled();
|
||||
const mockSetSearchParams = (useSearchParams as jest.Mock).mock.results[0].value[1];
|
||||
const [params, options] = mockSetSearchParams.mock.calls[0];
|
||||
expect(params).toBeInstanceOf(URLSearchParams);
|
||||
expect(params.toString()).toBe('');
|
||||
expect(options).toEqual(expect.objectContaining({ replace: true }));
|
||||
});
|
||||
|
||||
it('should auto-submit message when submit=true and no settings to apply', () => {
|
||||
|
|
@ -266,9 +275,14 @@ describe('useQueryParams', () => {
|
|||
submitMessage: mockSubmitMessage,
|
||||
});
|
||||
|
||||
// Mock startup config to allow processing
|
||||
(useQueryClient as jest.Mock).mockReturnValue({
|
||||
getQueryData: jest.fn().mockReturnValue({ modelSpecs: { list: [] } }),
|
||||
getQueryData: jest.fn().mockImplementation((key) => {
|
||||
const k = Array.isArray(key) ? key[0] : key;
|
||||
if (k === 'startupConfig') {
|
||||
return { modelSpecs: { list: [] } };
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
});
|
||||
|
||||
setUrlParams({ q: 'hello world', submit: 'true' });
|
||||
|
|
@ -304,13 +318,14 @@ describe('useQueryParams', () => {
|
|||
} as unknown as HTMLTextAreaElement,
|
||||
};
|
||||
|
||||
// Mock getQueryData to return array format for startupConfig
|
||||
// Mock getQueryData to return array format for startupConfig and endpoints
|
||||
const mockGetQueryData = jest.fn().mockImplementation((key) => {
|
||||
if (Array.isArray(key) && key[0] === 'startupConfig') {
|
||||
const k = Array.isArray(key) ? key[0] : key;
|
||||
if (k === 'startupConfig') {
|
||||
return { modelSpecs: { list: [] } };
|
||||
}
|
||||
if (key === 'startupConfig') {
|
||||
return { modelSpecs: { list: [] } };
|
||||
if (k === 'endpoints') {
|
||||
return {};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
|
@ -396,14 +411,15 @@ describe('useQueryParams', () => {
|
|||
newConversation: mockNewConversation,
|
||||
});
|
||||
|
||||
// Mock startup config to allow processing
|
||||
// Mock startup config and endpoints to allow processing
|
||||
(useQueryClient as jest.Mock).mockReturnValue({
|
||||
getQueryData: jest.fn().mockImplementation((key) => {
|
||||
if (Array.isArray(key) && key[0] === 'startupConfig') {
|
||||
const k = Array.isArray(key) ? key[0] : key;
|
||||
if (k === 'startupConfig') {
|
||||
return { modelSpecs: { list: [] } };
|
||||
}
|
||||
if (key === 'startupConfig') {
|
||||
return { modelSpecs: { list: [] } };
|
||||
if (k === 'endpoints') {
|
||||
return {};
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
|
|
@ -454,9 +470,14 @@ describe('useQueryParams', () => {
|
|||
submitMessage: mockSubmitMessage,
|
||||
});
|
||||
|
||||
// Mock startup config to allow processing
|
||||
(useQueryClient as jest.Mock).mockReturnValue({
|
||||
getQueryData: jest.fn().mockReturnValue({ modelSpecs: { list: [] } }),
|
||||
getQueryData: jest.fn().mockImplementation((key) => {
|
||||
const k = Array.isArray(key) ? key[0] : key;
|
||||
if (k === 'startupConfig') {
|
||||
return { modelSpecs: { list: [] } };
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
});
|
||||
|
||||
setUrlParams({ model: 'gpt-4' }); // No submit=true
|
||||
|
|
@ -500,9 +521,14 @@ describe('useQueryParams', () => {
|
|||
submitMessage: mockSubmitMessage,
|
||||
});
|
||||
|
||||
// Mock startup config to allow processing
|
||||
(useQueryClient as jest.Mock).mockReturnValue({
|
||||
getQueryData: jest.fn().mockReturnValue({ modelSpecs: { list: [] } }),
|
||||
getQueryData: jest.fn().mockImplementation((key) => {
|
||||
const k = Array.isArray(key) ? key[0] : key;
|
||||
if (k === 'startupConfig') {
|
||||
return { modelSpecs: { list: [] } };
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
});
|
||||
|
||||
setUrlParams({}); // Empty params
|
||||
|
|
@ -524,6 +550,10 @@ describe('useQueryParams', () => {
|
|||
expect(mockSetValue).not.toHaveBeenCalled();
|
||||
expect(mockHandleSubmit).not.toHaveBeenCalled();
|
||||
expect(mockSubmitMessage).not.toHaveBeenCalled();
|
||||
expect(window.history.replaceState).toHaveBeenCalled();
|
||||
const mockSetSearchParams = (useSearchParams as jest.Mock).mock.results[0].value[1];
|
||||
const [params, options] = mockSetSearchParams.mock.calls[0];
|
||||
expect(params).toBeInstanceOf(URLSearchParams);
|
||||
expect(params.toString()).toBe('');
|
||||
expect(options).toEqual(expect.objectContaining({ replace: true }));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,24 +2,17 @@ import { useEffect, useCallback, useRef } from 'react';
|
|||
import { useRecoilValue } from 'recoil';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { QueryClient, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
QueryKeys,
|
||||
EModelEndpoint,
|
||||
isAgentsEndpoint,
|
||||
tQueryParamsSchema,
|
||||
isAssistantsEndpoint,
|
||||
PermissionBits,
|
||||
} from 'librechat-data-provider';
|
||||
import { QueryKeys, EModelEndpoint, PermissionBits } from 'librechat-data-provider';
|
||||
import type {
|
||||
AgentListResponse,
|
||||
TEndpointsConfig,
|
||||
TStartupConfig,
|
||||
TPreset,
|
||||
} from 'librechat-data-provider';
|
||||
import type { ZodAny } from 'zod';
|
||||
import {
|
||||
clearModelForNonEphemeralAgent,
|
||||
removeUnavailableTools,
|
||||
processValidSettings,
|
||||
getModelSpecIconURL,
|
||||
getConvoSwitchLogic,
|
||||
logger,
|
||||
|
|
@ -29,62 +22,6 @@ import { useChatContext, useChatFormContext } from '~/Providers';
|
|||
import { useGetAgentByIdQuery } from '~/data-provider';
|
||||
import store from '~/store';
|
||||
|
||||
/**
|
||||
* Parses query parameter values, converting strings to their appropriate types.
|
||||
* Handles boolean strings, numbers, and preserves regular strings.
|
||||
*/
|
||||
const parseQueryValue = (value: string) => {
|
||||
if (value === 'true') {
|
||||
return true;
|
||||
}
|
||||
if (value === 'false') {
|
||||
return false;
|
||||
}
|
||||
if (!isNaN(Number(value))) {
|
||||
return Number(value);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Processes and validates URL query parameters using schema definitions.
|
||||
* Extracts valid settings based on tQueryParamsSchema and handles special endpoint cases
|
||||
* for assistants and agents.
|
||||
*/
|
||||
const processValidSettings = (queryParams: Record<string, string>) => {
|
||||
const validSettings = {} as TPreset;
|
||||
|
||||
Object.entries(queryParams).forEach(([key, value]) => {
|
||||
try {
|
||||
const schema = tQueryParamsSchema.shape[key] as ZodAny | undefined;
|
||||
if (schema) {
|
||||
const parsedValue = parseQueryValue(value);
|
||||
const validValue = schema.parse(parsedValue);
|
||||
validSettings[key] = validValue;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Invalid value for setting ${key}:`, error);
|
||||
}
|
||||
});
|
||||
|
||||
if (
|
||||
validSettings.assistant_id != null &&
|
||||
validSettings.assistant_id &&
|
||||
!isAssistantsEndpoint(validSettings.endpoint)
|
||||
) {
|
||||
validSettings.endpoint = EModelEndpoint.assistants;
|
||||
}
|
||||
if (
|
||||
validSettings.agent_id != null &&
|
||||
validSettings.agent_id &&
|
||||
!isAgentsEndpoint(validSettings.endpoint)
|
||||
) {
|
||||
validSettings.endpoint = EModelEndpoint.agents;
|
||||
}
|
||||
|
||||
return validSettings;
|
||||
};
|
||||
|
||||
const injectAgentIntoAgentsMap = (queryClient: QueryClient, agent: any) => {
|
||||
const editCacheKey = [QueryKeys.agents, { requiredPermission: PermissionBits.EDIT }];
|
||||
const editCache = queryClient.getQueryData<AgentListResponse>(editCacheKey);
|
||||
|
|
@ -244,13 +181,12 @@ export default function useQueryParams({
|
|||
],
|
||||
);
|
||||
|
||||
/**
|
||||
* Checks if all settings from URL parameters have been successfully applied to the conversation.
|
||||
* Compares values from validSettings against the current conversation state, handling special properties.
|
||||
* Returns true only when all relevant settings match the target values.
|
||||
*/
|
||||
const conversationRef = useRef(conversation);
|
||||
conversationRef.current = conversation;
|
||||
|
||||
const areSettingsApplied = useCallback(() => {
|
||||
if (!validSettingsRef.current || !conversation) {
|
||||
const convo = conversationRef.current;
|
||||
if (!validSettingsRef.current || !convo) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -259,13 +195,13 @@ export default function useQueryParams({
|
|||
continue;
|
||||
}
|
||||
|
||||
if (conversation[key] !== value) {
|
||||
if (convo[key] !== value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [conversation]);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Processes message submission exactly once, preventing duplicate submissions.
|
||||
|
|
@ -285,14 +221,12 @@ export default function useQueryParams({
|
|||
methods.handleSubmit((data) => {
|
||||
if (data.text?.trim()) {
|
||||
submitMessage(data);
|
||||
|
||||
const newUrl = window.location.pathname;
|
||||
window.history.replaceState({}, '', newUrl);
|
||||
|
||||
console.log('Message submitted with conversation state:', conversation);
|
||||
logger.log('conversation', 'Message submitted from query params');
|
||||
}
|
||||
})();
|
||||
}, [methods, submitMessage, conversation]);
|
||||
|
||||
setSearchParams(new URLSearchParams(), { replace: true });
|
||||
}, [methods, submitMessage, setSearchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
const processQueryParams = () => {
|
||||
|
|
@ -332,6 +266,7 @@ export default function useQueryParams({
|
|||
}
|
||||
|
||||
const { decodedPrompt, validSettings, shouldAutoSubmit } = processQueryParams();
|
||||
const hasSettings = Object.keys(validSettings).length > 0;
|
||||
|
||||
if (!shouldAutoSubmit) {
|
||||
submissionHandledRef.current = true;
|
||||
|
|
@ -339,45 +274,36 @@ export default function useQueryParams({
|
|||
|
||||
/** Mark processing as complete and clean up as needed */
|
||||
const success = () => {
|
||||
const paramString = searchParams.toString();
|
||||
const currentParams = new URLSearchParams(paramString);
|
||||
currentParams.delete('prompt');
|
||||
currentParams.delete('q');
|
||||
currentParams.delete('submit');
|
||||
|
||||
setSearchParams(currentParams, { replace: true });
|
||||
processedRef.current = true;
|
||||
console.log('Parameters processed successfully', paramString);
|
||||
logger.log('conversation', 'Query parameters processed successfully');
|
||||
clearInterval(intervalId);
|
||||
|
||||
// Only clean URL if there's no pending submission
|
||||
// Defer URL cleanup until after submission completes (processSubmission handles it)
|
||||
if (!pendingSubmitRef.current) {
|
||||
const newUrl = window.location.pathname;
|
||||
window.history.replaceState({}, '', newUrl);
|
||||
setSearchParams(new URLSearchParams(), { replace: true });
|
||||
}
|
||||
};
|
||||
|
||||
// Store settings for later comparison
|
||||
if (Object.keys(validSettings).length > 0) {
|
||||
if (hasSettings) {
|
||||
validSettingsRef.current = validSettings;
|
||||
}
|
||||
|
||||
// Save the prompt text for later use if needed
|
||||
if (decodedPrompt) {
|
||||
promptTextRef.current = decodedPrompt;
|
||||
}
|
||||
|
||||
// Handle auto-submission
|
||||
if (shouldAutoSubmit && decodedPrompt) {
|
||||
if (Object.keys(validSettings).length > 0) {
|
||||
if (hasSettings) {
|
||||
// Settings are changing, defer submission
|
||||
pendingSubmitRef.current = true;
|
||||
|
||||
// Set a timeout to handle the case where settings might never fully apply
|
||||
settingsTimeoutRef.current = setTimeout(() => {
|
||||
if (!submissionHandledRef.current && pendingSubmitRef.current) {
|
||||
console.warn(
|
||||
'Settings application timeout reached, proceeding with submission anyway',
|
||||
logger.log(
|
||||
'conversation',
|
||||
'Settings application timeout, proceeding with submission',
|
||||
);
|
||||
processSubmission();
|
||||
}
|
||||
|
|
@ -401,7 +327,7 @@ export default function useQueryParams({
|
|||
submissionHandledRef.current = true;
|
||||
}
|
||||
|
||||
if (Object.keys(validSettings).length > 0) {
|
||||
if (hasSettings && !areSettingsApplied()) {
|
||||
newQueryConvo(validSettings);
|
||||
}
|
||||
|
||||
|
|
@ -424,6 +350,7 @@ export default function useQueryParams({
|
|||
setSearchParams,
|
||||
queryClient,
|
||||
processSubmission,
|
||||
areSettingsApplied,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -438,9 +365,7 @@ export default function useQueryParams({
|
|||
return;
|
||||
}
|
||||
|
||||
const allSettingsApplied = areSettingsApplied();
|
||||
|
||||
if (allSettingsApplied) {
|
||||
if (areSettingsApplied()) {
|
||||
settingsAppliedRef.current = true;
|
||||
|
||||
if (pendingSubmitRef.current) {
|
||||
|
|
@ -449,7 +374,7 @@ export default function useQueryParams({
|
|||
settingsTimeoutRef.current = null;
|
||||
}
|
||||
|
||||
console.log('Settings fully applied, processing submission');
|
||||
logger.log('conversation', 'Settings fully applied, processing submission');
|
||||
processSubmission();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -313,8 +313,12 @@ describe('AuthContextProvider — silentRefresh post-login redirect', () => {
|
|||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('navigates to /c/new when no stored redirect exists', () => {
|
||||
it('navigates to current URL when no stored redirect exists', () => {
|
||||
jest.useFakeTimers();
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { ...window.location, pathname: '/c/new', search: '' },
|
||||
writable: true,
|
||||
});
|
||||
|
||||
renderProviderLive();
|
||||
|
||||
|
|
@ -361,8 +365,12 @@ describe('AuthContextProvider — silentRefresh post-login redirect', () => {
|
|||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('falls back to /c/new for unsafe stored redirect', () => {
|
||||
it('falls back to current URL for unsafe stored redirect', () => {
|
||||
jest.useFakeTimers();
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { ...window.location, pathname: '/c/new', search: '' },
|
||||
writable: true,
|
||||
});
|
||||
sessionStorage.setItem(SESSION_KEY, 'https://evil.com/steal');
|
||||
|
||||
renderProviderLive();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useRecoilCallback, useRecoilValue } from 'recoil';
|
||||
import { Spinner, useToastContext } from '@librechat/client';
|
||||
import { useParams, useSearchParams } from 'react-router-dom';
|
||||
import { Constants, EModelEndpoint } from 'librechat-data-provider';
|
||||
import { useGetModelsQuery } from 'librechat-data-provider/react-query';
|
||||
import type { TPreset } from 'librechat-data-provider';
|
||||
|
|
@ -13,7 +13,13 @@ import {
|
|||
useLocalize,
|
||||
} from '~/hooks';
|
||||
import { useGetConvoIdQuery, useGetStartupConfig, useGetEndpointsQuery } from '~/data-provider';
|
||||
import { getDefaultModelSpec, getModelSpecPreset, logger, isNotFoundError } from '~/utils';
|
||||
import {
|
||||
getDefaultModelSpec,
|
||||
getModelSpecPreset,
|
||||
processValidSettings,
|
||||
logger,
|
||||
isNotFoundError,
|
||||
} from '~/utils';
|
||||
import { ToolCallsMapProvider } from '~/Providers';
|
||||
import ChatView from '~/components/Chat/ChatView';
|
||||
import { NotificationSeverity } from '~/common';
|
||||
|
|
@ -36,6 +42,7 @@ export default function ChatRoute() {
|
|||
useAppStartup({ startupConfig, user });
|
||||
|
||||
const index = 0;
|
||||
const [searchParams] = useSearchParams();
|
||||
const { conversationId = '' } = useParams();
|
||||
useIdChangeEffect(conversationId);
|
||||
const { hasSetConversation, conversation } = store.useCreateConversationAtom(index);
|
||||
|
|
@ -80,14 +87,34 @@ export default function ChatRoute() {
|
|||
return;
|
||||
}
|
||||
|
||||
if (conversationId === Constants.NEW_CONVO && endpointsQuery.data && modelsQuery.data) {
|
||||
const isNewConvo = conversationId === Constants.NEW_CONVO;
|
||||
|
||||
const getNewConvoPreset = () => {
|
||||
const result = getDefaultModelSpec(startupConfig);
|
||||
const spec = result?.default ?? result?.last;
|
||||
const specPreset = spec ? getModelSpecPreset(spec) : undefined;
|
||||
|
||||
const queryParams: Record<string, string> = {};
|
||||
searchParams.forEach((value, key) => {
|
||||
if (key !== 'prompt' && key !== 'q' && key !== 'submit') {
|
||||
queryParams[key] = value;
|
||||
}
|
||||
});
|
||||
const querySettings = processValidSettings(queryParams);
|
||||
|
||||
return Object.keys(querySettings).length > 0
|
||||
? { ...specPreset, ...querySettings }
|
||||
: specPreset;
|
||||
};
|
||||
|
||||
if (isNewConvo && endpointsQuery.data && modelsQuery.data) {
|
||||
const preset = getNewConvoPreset();
|
||||
|
||||
logger.log('conversation', 'ChatRoute, new convo effect', conversation);
|
||||
newConversation({
|
||||
modelsData: modelsQuery.data,
|
||||
template: conversation ? conversation : undefined,
|
||||
...(spec ? { preset: getModelSpecPreset(spec) } : {}),
|
||||
...(preset ? { preset } : {}),
|
||||
});
|
||||
|
||||
hasSetConversation.current = true;
|
||||
|
|
@ -125,17 +152,17 @@ export default function ChatRoute() {
|
|||
});
|
||||
hasSetConversation.current = true;
|
||||
} else if (
|
||||
conversationId === Constants.NEW_CONVO &&
|
||||
isNewConvo &&
|
||||
assistantListMap[EModelEndpoint.assistants] &&
|
||||
assistantListMap[EModelEndpoint.azureAssistants]
|
||||
) {
|
||||
const result = getDefaultModelSpec(startupConfig);
|
||||
const spec = result?.default ?? result?.last;
|
||||
const preset = getNewConvoPreset();
|
||||
|
||||
logger.log('conversation', 'ChatRoute new convo, assistants effect', conversation);
|
||||
newConversation({
|
||||
modelsData: modelsQuery.data,
|
||||
template: conversation ? conversation : undefined,
|
||||
...(spec ? { preset: getModelSpecPreset(spec) } : {}),
|
||||
...(preset ? { preset } : {}),
|
||||
});
|
||||
hasSetConversation.current = true;
|
||||
} else if (
|
||||
|
|
|
|||
|
|
@ -1,11 +1,65 @@
|
|||
import {
|
||||
EModelEndpoint,
|
||||
isAgentsEndpoint,
|
||||
tQueryParamsSchema,
|
||||
isAssistantsEndpoint,
|
||||
} from 'librechat-data-provider';
|
||||
import type { TConversation, TPreset } from 'librechat-data-provider';
|
||||
import type { TPreset, TConversation } from 'librechat-data-provider';
|
||||
import type { ZodAny } from 'zod';
|
||||
import { isEphemeralAgent } from '~/common';
|
||||
|
||||
const parseQueryValue = (value: string) => {
|
||||
if (value === 'true') {
|
||||
return true;
|
||||
}
|
||||
if (value === 'false') {
|
||||
return false;
|
||||
}
|
||||
if (!isNaN(Number(value))) {
|
||||
return Number(value);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Processes and validates URL query parameters using schema definitions.
|
||||
* Extracts valid settings based on tQueryParamsSchema and handles special endpoint cases
|
||||
* for assistants and agents.
|
||||
*/
|
||||
export function processValidSettings(queryParams: Record<string, string>) {
|
||||
const validSettings = {} as TPreset;
|
||||
|
||||
for (const [key, value] of Object.entries(queryParams)) {
|
||||
try {
|
||||
const schema = tQueryParamsSchema.shape[key] as ZodAny | undefined;
|
||||
if (schema) {
|
||||
const parsedValue = parseQueryValue(value);
|
||||
const validValue = schema.parse(parsedValue);
|
||||
validSettings[key] = validValue;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Invalid value for setting ${key}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
validSettings.assistant_id != null &&
|
||||
validSettings.assistant_id &&
|
||||
!isAssistantsEndpoint(validSettings.endpoint)
|
||||
) {
|
||||
validSettings.endpoint = EModelEndpoint.assistants;
|
||||
}
|
||||
if (
|
||||
validSettings.agent_id != null &&
|
||||
validSettings.agent_id &&
|
||||
!isAgentsEndpoint(validSettings.endpoint)
|
||||
) {
|
||||
validSettings.endpoint = EModelEndpoint.agents;
|
||||
}
|
||||
|
||||
return validSettings;
|
||||
}
|
||||
|
||||
const allowedParams = Object.keys(tQueryParamsSchema.shape);
|
||||
export default function createChatSearchParams(
|
||||
input: TConversation | TPreset | Record<string, string> | null,
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export { default as getLoginError } from './getLoginError';
|
|||
export { default as cleanupPreset } from './cleanupPreset';
|
||||
export { default as buildDefaultConvo } from './buildDefaultConvo';
|
||||
export { default as getDefaultEndpoint } from './getDefaultEndpoint';
|
||||
export { default as createChatSearchParams } from './createChatSearchParams';
|
||||
export { default as createChatSearchParams, processValidSettings } from './createChatSearchParams';
|
||||
export { getThemeFromEnv } from './getThemeFromEnv';
|
||||
|
||||
export const languages = [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue