🔗 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
This commit is contained in:
Danny Avila 2025-01-04 20:36:12 -05:00 committed by GitHub
parent 766657da83
commit 7987e04a2c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 370 additions and 48 deletions

View file

@ -7,6 +7,7 @@ import {
import {
alternateName,
EModelEndpoint,
isAgentsEndpoint,
getConfigDefaults,
isAssistantsEndpoint,
} from 'librechat-data-provider';
@ -121,6 +122,26 @@ export default function useMentions({
if (!includeAssistants) {
validEndpoints = endpoints.filter((endpoint) => !isAssistantsEndpoint(endpoint));
}
const modelOptions = validEndpoints.flatMap((endpoint) => {
if (isAssistantsEndpoint(endpoint) || isAgentsEndpoint(endpoint)) {
return [];
}
const models = (modelsConfig?.[endpoint] ?? []).map((model) => ({
value: endpoint,
label: model,
type: 'model' as const,
icon: EndpointIcon({
conversation: { endpoint, model },
endpointsConfig,
context: 'menu-item',
size: 20,
}),
}));
return models;
});
const mentions = [
...(modelSpecs.length > 0 ? modelSpecs : []).map((modelSpec) => ({
value: modelSpec.name,
@ -169,6 +190,7 @@ export default function useMentions({
}),
type: 'preset' as const,
})) ?? []),
...modelOptions,
];
return mentions;
@ -178,6 +200,7 @@ export default function useMentions({
modelSpecs,
agentsList,
assistantMap,
modelsConfig,
endpointsConfig,
assistantListMap,
includeAssistants,

View file

@ -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);
// Remove the 'prompt' parameter from the URL
searchParams.delete('prompt');
const newUrl = `${window.location.pathname}${
searchParams.toString() ? `?${searchParams.toString()}` : ''
}`;
window.history.replaceState({}, '', newUrl);
processedRef.current = true;
console.log('Prompt processed successfully');
}
clearInterval(intervalId);
if (!textAreaRef.current) {
return;
}
}, 100); // Check every 100ms
const { decodedPrompt, validSettings } = processQueryParams();
const currentText = methods.getValues('text');
/** 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);
};
if (!currentText && decodedPrompt) {
methods.setValue('text', decodedPrompt, { shouldValidate: true });
textAreaRef.current.focus();
textAreaRef.current.setSelectionRange(decodedPrompt.length, decodedPrompt.length);
}
if (Object.keys(validSettings).length > 0) {
newQueryConvo(validSettings);
}
success();
}, 100);
// Clean up the interval on unmount
return () => {
clearInterval(intervalId);
console.log('Cleanup: interval cleared');
console.log('Cleanup: `useQueryParams` interval cleared');
};
}, [searchParams, methods, textAreaRef]);
}, [searchParams, methods, textAreaRef, newQueryConvo, newConversation]);
}

View file

@ -144,6 +144,10 @@ export default function useSelectMention({
if (assistant_id) {
template.assistant_id = assistant_id;
}
const agent_id = kwargs.agent_id ?? '';
if (agent_id) {
template.agent_id = agent_id;
}
if (isExistingConversation && isCurrentModular && isNewModular && shouldSwitch) {
template.endpointType = newEndpointType;

View file

@ -128,7 +128,10 @@ export default function useTextarea({
? getEntityName({ name: entityName, isAgent, localize })
: getSender(conversation as TEndpointOption);
return `${localize('com_endpoint_message')} ${sender ? sender : 'AI'}`;
return `${localize(
'com_endpoint_message_new',
sender ? sender : localize('com_endpoint_ai'),
)}`;
};
const placeholder = getPlaceholderText();