mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-20 02:10: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
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue