| | 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]); |
| | } |
| |
|