mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-19 18:00:15 +01:00
🔗 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:
parent
766657da83
commit
7987e04a2c
19 changed files with 370 additions and 48 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue