-
-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
🔗 feat: Convo Settings via URL Query Params & Mention Models (#5184)
* feat: first pass, convo settings from query params * feat: Enhance query parameter handling for assistants and agents endpoints * feat: Update message formatting and localization for AI responses, bring awareness to mention command * docs: Update translations README with detailed instructions for translation script usage and contribution guidelines * chore: update localizations * fix: missing agent_id assignment * feat: add models as initial mention option * feat: update query parameters schema to confine possible query params * fix: normalize custom endpoints * refactor: optimize custom endpoint type check
- Loading branch information
1 parent
766657d
commit 7987e04
Showing
19 changed files
with
367 additions
and
45 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,64 +1,213 @@ | ||
import { useEffect, useRef } from 'react'; | ||
import { useEffect, useCallback, useRef } from 'react'; | ||
import { useRecoilValue } from 'recoil'; | ||
import { useSearchParams } from 'react-router-dom'; | ||
import { useChatFormContext } from '~/Providers'; | ||
import { useQueryClient } from '@tanstack/react-query'; | ||
import { | ||
QueryKeys, | ||
EModelEndpoint, | ||
isAgentsEndpoint, | ||
tQueryParamsSchema, | ||
isAssistantsEndpoint, | ||
} from 'librechat-data-provider'; | ||
import type { TPreset, TEndpointsConfig } from 'librechat-data-provider'; | ||
import type { ZodAny } from 'zod'; | ||
import { getConvoSwitchLogic, removeUnavailableTools } from '~/utils'; | ||
import useDefaultConvo from '~/hooks/Conversations/useDefaultConvo'; | ||
import { useChatContext, useChatFormContext } from '~/Providers'; | ||
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; | ||
}; | ||
|
||
export default function useQueryParams({ | ||
textAreaRef, | ||
}: { | ||
textAreaRef: React.RefObject<HTMLTextAreaElement>; | ||
}) { | ||
const methods = useChatFormContext(); | ||
const [searchParams] = useSearchParams(); | ||
const maxAttempts = 50; | ||
const attemptsRef = useRef(0); | ||
const processedRef = useRef(false); | ||
const maxAttempts = 50; // 5 seconds maximum (50 * 100ms) | ||
const methods = useChatFormContext(); | ||
const [searchParams] = useSearchParams(); | ||
const getDefaultConversation = useDefaultConvo(); | ||
const modularChat = useRecoilValue(store.modularChat); | ||
const availableTools = useRecoilValue(store.availableTools); | ||
|
||
const queryClient = useQueryClient(); | ||
const { conversation, newConversation } = useChatContext(); | ||
|
||
const newQueryConvo = useCallback( | ||
(_newPreset?: TPreset) => { | ||
if (!_newPreset) { | ||
return; | ||
} | ||
|
||
const newPreset = removeUnavailableTools(_newPreset, availableTools); | ||
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, | ||
}); | ||
|
||
const isModular = isCurrentModular && isNewModular && shouldSwitch; | ||
if (isExistingConversation && isModular) { | ||
template.endpointType = newEndpointType as EModelEndpoint | undefined; | ||
|
||
const currentConvo = getDefaultConversation({ | ||
/* target endpointType is necessary to avoid endpoint mixing */ | ||
conversation: { ...(conversation ?? {}), endpointType: template.endpointType }, | ||
preset: template, | ||
}); | ||
|
||
/* We don't reset the latest message, only when changing settings mid-converstion */ | ||
newConversation({ | ||
template: currentConvo, | ||
preset: newPreset, | ||
keepLatestMessage: true, | ||
keepAddedConvos: true, | ||
}); | ||
return; | ||
} | ||
|
||
newConversation({ preset: newPreset, keepAddedConvos: true }); | ||
}, | ||
[ | ||
queryClient, | ||
modularChat, | ||
conversation, | ||
availableTools, | ||
newConversation, | ||
getDefaultConversation, | ||
], | ||
); | ||
|
||
useEffect(() => { | ||
const decodedPrompt = searchParams.get('prompt') ?? ''; | ||
if (!decodedPrompt) { | ||
return; | ||
} | ||
const processQueryParams = () => { | ||
const queryParams: Record<string, string> = {}; | ||
searchParams.forEach((value, key) => { | ||
queryParams[key] = value; | ||
}); | ||
|
||
const decodedPrompt = queryParams.prompt || ''; | ||
delete queryParams.prompt; | ||
const validSettings = processValidSettings(queryParams); | ||
|
||
return { decodedPrompt, validSettings }; | ||
}; | ||
|
||
const intervalId = setInterval(() => { | ||
// If already processed or max attempts reached, clear interval and stop | ||
if (processedRef.current || attemptsRef.current >= maxAttempts) { | ||
clearInterval(intervalId); | ||
if (attemptsRef.current >= maxAttempts) { | ||
console.warn('Max attempts reached, failed to process prompt'); | ||
console.warn('Max attempts reached, failed to process parameters'); | ||
} | ||
return; | ||
} | ||
|
||
attemptsRef.current += 1; | ||
|
||
if (textAreaRef.current) { | ||
const currentText = methods.getValues('text'); | ||
|
||
// Only update if the textarea is empty | ||
if (!currentText) { | ||
methods.setValue('text', decodedPrompt, { shouldValidate: true }); | ||
textAreaRef.current.focus(); | ||
textAreaRef.current.setSelectionRange(decodedPrompt.length, decodedPrompt.length); | ||
if (!textAreaRef.current) { | ||
return; | ||
} | ||
const { decodedPrompt, validSettings } = processQueryParams(); | ||
const currentText = methods.getValues('text'); | ||
|
||
// Remove the 'prompt' parameter from the URL | ||
searchParams.delete('prompt'); | ||
const newUrl = `${window.location.pathname}${ | ||
searchParams.toString() ? `?${searchParams.toString()}` : '' | ||
}`; | ||
window.history.replaceState({}, '', newUrl); | ||
/** Clean up URL parameters after successful processing */ | ||
const success = () => { | ||
const newUrl = window.location.pathname; | ||
window.history.replaceState({}, '', newUrl); | ||
processedRef.current = true; | ||
console.log('Parameters processed successfully'); | ||
clearInterval(intervalId); | ||
}; | ||
|
||
processedRef.current = true; | ||
console.log('Prompt processed successfully'); | ||
} | ||
if (!currentText && decodedPrompt) { | ||
methods.setValue('text', decodedPrompt, { shouldValidate: true }); | ||
textAreaRef.current.focus(); | ||
textAreaRef.current.setSelectionRange(decodedPrompt.length, decodedPrompt.length); | ||
} | ||
|
||
clearInterval(intervalId); | ||
if (Object.keys(validSettings).length > 0) { | ||
newQueryConvo(validSettings); | ||
} | ||
}, 100); // Check every 100ms | ||
|
||
// Clean up the interval on unmount | ||
success(); | ||
}, 100); | ||
|
||
return () => { | ||
clearInterval(intervalId); | ||
console.log('Cleanup: interval cleared'); | ||
console.log('Cleanup: `useQueryParams` interval cleared'); | ||
}; | ||
}, [searchParams, methods, textAreaRef]); | ||
}, [searchParams, methods, textAreaRef, newQueryConvo, newConversation]); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
This adds 'or type "@" to switch AI' even if 'Toggle command "@" for switching endpoints, models, presets, etc.' is disabled in the settings. Is that intentional?