| 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 type { |
| TPreset, |
| TEndpointsConfig, |
| TStartupConfig, |
| AgentListResponse, |
| } from 'librechat-data-provider'; |
| import type { ZodAny } from 'zod'; |
| import { getConvoSwitchLogic, getModelSpecIconURL, removeUnavailableTools, logger } from '~/utils'; |
| import { useAuthContext, useAgentsMap, useDefaultConvo, useSubmitMessage } from '~/hooks'; |
| import { useChatContext, useChatFormContext } from '~/Providers'; |
| import { useGetAgentByIdQuery } from '~/data-provider'; |
| import store from '~/store'; |
|
|
| |
| |
| |
| |
| const parseQueryValue = (value: string) => { |
| if (value === 'true') { |
| return true; |
| } |
| if (value === 'false') { |
| return false; |
| } |
| if (!isNaN(Number(value))) { |
| return Number(value); |
| } |
| return value; |
| }; |
|
|
| |
| |
| |
| |
| |
| 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); |
|
|
| if (editCache?.data && !editCache.data.some((cachedAgent) => cachedAgent.id === agent.id)) { |
| |
| const updatedCache = { |
| ...editCache, |
| data: [agent, ...editCache.data], |
| }; |
| queryClient.setQueryData(editCacheKey, updatedCache); |
| logger.log('agent', 'Injected URL agent into cache:', agent); |
| } |
| }; |
|
|
| |
| |
| |
| |
| |
| export default function useQueryParams({ |
| textAreaRef, |
| }: { |
| textAreaRef: React.RefObject<HTMLTextAreaElement>; |
| }) { |
| const maxAttempts = 50; |
| const attemptsRef = useRef(0); |
| const MAX_SETTINGS_WAIT_MS = 3000; |
| const processedRef = useRef(false); |
| const pendingSubmitRef = useRef(false); |
| const settingsAppliedRef = useRef(false); |
| const submissionHandledRef = useRef(false); |
| const promptTextRef = useRef<string | null>(null); |
| const validSettingsRef = useRef<TPreset | null>(null); |
| const settingsTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null); |
|
|
| const methods = useChatFormContext(); |
| const [searchParams, setSearchParams] = useSearchParams(); |
| const getDefaultConversation = useDefaultConvo(); |
| const modularChat = useRecoilValue(store.modularChat); |
| const availableTools = useRecoilValue(store.availableTools); |
| const { submitMessage } = useSubmitMessage(); |
|
|
| const queryClient = useQueryClient(); |
| const { conversation, newConversation } = useChatContext(); |
|
|
| const urlAgentId = searchParams.get('agent_id') || ''; |
| const { data: urlAgent } = useGetAgentByIdQuery(urlAgentId); |
|
|
| |
| |
| |
| |
| |
| const newQueryConvo = useCallback( |
| (_newPreset?: TPreset) => { |
| if (!_newPreset) { |
| return; |
| } |
| let newPreset = removeUnavailableTools(_newPreset, availableTools); |
| if (newPreset.spec != null && newPreset.spec !== '') { |
| const startupConfig = queryClient.getQueryData<TStartupConfig>([QueryKeys.startupConfig]); |
| const modelSpecs = startupConfig?.modelSpecs?.list ?? []; |
| const spec = modelSpecs.find((s) => s.name === newPreset.spec); |
| if (!spec) { |
| return; |
| } |
| const { preset } = spec; |
| preset.iconURL = getModelSpecIconURL(spec); |
| preset.spec = spec.name; |
| newPreset = preset; |
| } |
|
|
| let newEndpoint = newPreset.endpoint ?? ''; |
| const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]); |
|
|
| if (newEndpoint && endpointsConfig && !endpointsConfig[newEndpoint]) { |
| const normalizedNewEndpoint = newEndpoint.toLowerCase(); |
| for (const [key, value] of Object.entries(endpointsConfig)) { |
| if ( |
| value && |
| value.type === EModelEndpoint.custom && |
| key.toLowerCase() === normalizedNewEndpoint |
| ) { |
| newEndpoint = key; |
| newPreset.endpoint = key; |
| newPreset.endpointType = EModelEndpoint.custom; |
| break; |
| } |
| } |
| } |
|
|
| const { |
| template, |
| shouldSwitch, |
| isNewModular, |
| newEndpointType, |
| isCurrentModular, |
| isExistingConversation, |
| } = getConvoSwitchLogic({ |
| newEndpoint, |
| modularChat, |
| conversation, |
| endpointsConfig, |
| }); |
|
|
| let resetParams = {}; |
| if (newPreset.spec == null) { |
| template.spec = null; |
| template.iconURL = null; |
| template.modelLabel = null; |
| resetParams = { spec: null, iconURL: null, modelLabel: null }; |
| newPreset = { ...newPreset, ...resetParams }; |
| } |
|
|
| const isModular = isCurrentModular && isNewModular && shouldSwitch; |
| if (isExistingConversation && isModular) { |
| template.endpointType = newEndpointType as EModelEndpoint | undefined; |
|
|
| const currentConvo = getDefaultConversation({ |
| |
| conversation: { |
| ...(conversation ?? {}), |
| endpointType: template.endpointType, |
| ...resetParams, |
| }, |
| preset: template, |
| cleanOutput: newPreset.spec != null && newPreset.spec !== '', |
| }); |
|
|
| |
| logger.log('conversation', 'Switching conversation from query params', currentConvo); |
| newConversation({ |
| template: currentConvo, |
| preset: newPreset, |
| keepLatestMessage: true, |
| keepAddedConvos: true, |
| }); |
| return; |
| } |
|
|
| newConversation({ preset: newPreset, keepAddedConvos: true }); |
| }, |
| [ |
| queryClient, |
| modularChat, |
| conversation, |
| availableTools, |
| newConversation, |
| getDefaultConversation, |
| ], |
| ); |
|
|
| |
| |
| |
| |
| |
| const areSettingsApplied = useCallback(() => { |
| if (!validSettingsRef.current || !conversation) { |
| return false; |
| } |
|
|
| for (const [key, value] of Object.entries(validSettingsRef.current)) { |
| if (['presetOverride', 'iconURL', 'spec', 'modelLabel'].includes(key)) { |
| continue; |
| } |
|
|
| if (conversation[key] !== value) { |
| return false; |
| } |
| } |
|
|
| return true; |
| }, [conversation]); |
|
|
| |
| |
| |
| |
| |
| const processSubmission = useCallback(() => { |
| if (submissionHandledRef.current || !pendingSubmitRef.current || !promptTextRef.current) { |
| return; |
| } |
|
|
| submissionHandledRef.current = true; |
| pendingSubmitRef.current = false; |
|
|
| methods.setValue('text', promptTextRef.current, { shouldValidate: true }); |
|
|
| 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); |
| } |
| })(); |
| }, [methods, submitMessage, conversation]); |
|
|
| useEffect(() => { |
| const processQueryParams = () => { |
| const queryParams: Record<string, string> = {}; |
| searchParams.forEach((value, key) => { |
| queryParams[key] = value; |
| }); |
|
|
| |
| const decodedPrompt = queryParams.prompt || queryParams.q || ''; |
| const shouldAutoSubmit = queryParams.submit?.toLowerCase() === 'true'; |
| delete queryParams.prompt; |
| delete queryParams.q; |
| delete queryParams.submit; |
| const validSettings = processValidSettings(queryParams); |
|
|
| return { decodedPrompt, validSettings, shouldAutoSubmit }; |
| }; |
|
|
| const intervalId = setInterval(() => { |
| if (processedRef.current || attemptsRef.current >= maxAttempts) { |
| clearInterval(intervalId); |
| if (attemptsRef.current >= maxAttempts) { |
| console.warn('Max attempts reached, failed to process parameters'); |
| } |
| return; |
| } |
|
|
| attemptsRef.current += 1; |
|
|
| if (!textAreaRef.current) { |
| return; |
| } |
| const startupConfig = queryClient.getQueryData<TStartupConfig>([QueryKeys.startupConfig]); |
| if (!startupConfig) { |
| return; |
| } |
|
|
| const { decodedPrompt, validSettings, shouldAutoSubmit } = processQueryParams(); |
|
|
| if (!shouldAutoSubmit) { |
| submissionHandledRef.current = true; |
| } |
|
|
| |
| 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); |
| clearInterval(intervalId); |
|
|
| |
| if (!pendingSubmitRef.current) { |
| const newUrl = window.location.pathname; |
| window.history.replaceState({}, '', newUrl); |
| } |
| }; |
|
|
| |
| if (Object.keys(validSettings).length > 0) { |
| validSettingsRef.current = validSettings; |
| } |
|
|
| |
| if (decodedPrompt) { |
| promptTextRef.current = decodedPrompt; |
| } |
|
|
| |
| if (shouldAutoSubmit && decodedPrompt) { |
| if (Object.keys(validSettings).length > 0) { |
| |
| pendingSubmitRef.current = true; |
|
|
| |
| settingsTimeoutRef.current = setTimeout(() => { |
| if (!submissionHandledRef.current && pendingSubmitRef.current) { |
| console.warn( |
| 'Settings application timeout reached, proceeding with submission anyway', |
| ); |
| processSubmission(); |
| } |
| }, MAX_SETTINGS_WAIT_MS); |
| } else { |
| methods.setValue('text', decodedPrompt, { shouldValidate: true }); |
| textAreaRef.current.focus(); |
| textAreaRef.current.setSelectionRange(decodedPrompt.length, decodedPrompt.length); |
|
|
| methods.handleSubmit((data) => { |
| if (data.text?.trim()) { |
| submitMessage(data); |
| } |
| })(); |
| } |
| } else if (decodedPrompt) { |
| methods.setValue('text', decodedPrompt, { shouldValidate: true }); |
| textAreaRef.current.focus(); |
| textAreaRef.current.setSelectionRange(decodedPrompt.length, decodedPrompt.length); |
| } else { |
| submissionHandledRef.current = true; |
| } |
|
|
| if (Object.keys(validSettings).length > 0) { |
| newQueryConvo(validSettings); |
| } |
|
|
| success(); |
| }, 100); |
|
|
| return () => { |
| clearInterval(intervalId); |
| if (settingsTimeoutRef.current) { |
| clearTimeout(settingsTimeoutRef.current); |
| } |
| }; |
| }, [ |
| searchParams, |
| methods, |
| textAreaRef, |
| newQueryConvo, |
| newConversation, |
| submitMessage, |
| setSearchParams, |
| queryClient, |
| processSubmission, |
| ]); |
|
|
| useEffect(() => { |
| |
| if ( |
| !processedRef.current || |
| submissionHandledRef.current || |
| settingsAppliedRef.current || |
| !validSettingsRef.current || |
| !conversation |
| ) { |
| return; |
| } |
|
|
| const allSettingsApplied = areSettingsApplied(); |
|
|
| if (allSettingsApplied) { |
| settingsAppliedRef.current = true; |
|
|
| if (pendingSubmitRef.current) { |
| if (settingsTimeoutRef.current) { |
| clearTimeout(settingsTimeoutRef.current); |
| settingsTimeoutRef.current = null; |
| } |
|
|
| console.log('Settings fully applied, processing submission'); |
| processSubmission(); |
| } |
| } |
| }, [conversation, processSubmission, areSettingsApplied]); |
|
|
| const { isAuthenticated } = useAuthContext(); |
| const agentsMap = useAgentsMap({ isAuthenticated }); |
| useEffect(() => { |
| if (urlAgent) { |
| injectAgentIntoAgentsMap(queryClient, urlAgent); |
| } |
| }, [urlAgent, queryClient, agentsMap]); |
| } |
|
|