Skip to content

Commit

Permalink
🔗 feat: Convo Settings via URL Query Params & Mention Models (#5184)
Browse files Browse the repository at this point in the history
* 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
danny-avila authored Jan 5, 2025
1 parent 766657d commit 7987e04
Show file tree
Hide file tree
Showing 19 changed files with 367 additions and 45 deletions.
23 changes: 23 additions & 0 deletions client/src/hooks/Input/useMentions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
import {
alternateName,
EModelEndpoint,
isAgentsEndpoint,
getConfigDefaults,
isAssistantsEndpoint,
} from 'librechat-data-provider';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -169,6 +190,7 @@ export default function useMentions({
}),
type: 'preset' as const,
})) ?? []),
...modelOptions,
];

return mentions;
Expand All @@ -178,6 +200,7 @@ export default function useMentions({
modelSpecs,
agentsList,
assistantMap,
modelsConfig,
endpointsConfig,
assistantListMap,
includeAssistants,
Expand Down
215 changes: 182 additions & 33 deletions client/src/hooks/Input/useQueryParams.ts
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]);
}
4 changes: 4 additions & 0 deletions client/src/hooks/Input/useSelectMention.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 4 additions & 1 deletion client/src/hooks/Input/useTextarea.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
12 changes: 10 additions & 2 deletions client/src/localization/languages/Ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -871,7 +871,8 @@ export default {
com_error_invalid_action_error: 'تم رفض الطلب: نطاق الإجراء المحدد غير مسموح به',
com_agents_code_interpreter_title: 'واجهة برمجة مُفسِّر الشفرة',
com_agents_by_librechat: 'بواسطة LibreChat',
com_agents_code_interpreter: 'عند التمكين، يسمح للوكيل الخاص بك باستخدام واجهة برمجة التطبيقات لمفسر الشفرة LibreChat لتشغيل الشفرة المُنشأة، بما في ذلك معالجة الملفات، بشكل آمن. يتطلب مفتاح API صالح.',
com_agents_code_interpreter:
'عند التمكين، يسمح للوكيل الخاص بك باستخدام واجهة برمجة التطبيقات لمفسر الشفرة LibreChat لتشغيل الشفرة المُنشأة، بما في ذلك معالجة الملفات، بشكل آمن. يتطلب مفتاح API صالح.',
com_ui_export_convo_modal: 'نافذة تصدير المحادثة',
com_ui_endpoints_available: 'نقاط النهاية المتاحة',
com_ui_endpoint_menu: 'قائمة نقطة نهاية LLM',
Expand All @@ -885,7 +886,8 @@ export default {
com_ui_upload_code_files: 'تحميل لمفسر الكود',
com_ui_zoom: 'تكبير',
com_ui_role_select: 'الدور',
com_ui_admin_access_warning: 'قد يؤدي تعطيل وصول المسؤول إلى هذه الميزة إلى مشاكل غير متوقعة في واجهة المستخدم تتطلب تحديث الصفحة. في حالة الحفظ، الطريقة الوحيدة للتراجع هي عبر إعداد الواجهة في ملف librechat.yaml والذي يؤثر على جميع الأدوار.',
com_ui_admin_access_warning:
'قد يؤدي تعطيل وصول المسؤول إلى هذه الميزة إلى مشاكل غير متوقعة في واجهة المستخدم تتطلب تحديث الصفحة. في حالة الحفظ، الطريقة الوحيدة للتراجع هي عبر إعداد الواجهة في ملف librechat.yaml والذي يؤثر على جميع الأدوار.',
com_ui_run_code_error: 'حدث خطأ أثناء تشغيل الكود',
com_ui_duplication_success: 'تم نسخ المحادثة بنجاح',
com_ui_duplication_processing: 'جارِ نسخ المحادثة...',
Expand All @@ -905,4 +907,10 @@ export default {
com_ui_enter_openapi_schema: 'أدخل مخطط OpenAPI هنا',
com_ui_delete_shared_link: 'حذف الرابط المشترك؟',
com_nav_welcome_agent: 'الرجاء اختيار مساعد',
com_ui_bookmarks_edit: 'تعديل الإشارة المرجعية',
com_ui_page: 'صفحة',
com_ui_bookmarks_add: 'إضافة إشارات مرجعية',
com_endpoint_ai: 'الذكاء الاصطناعي',
com_endpoint_message_new: 'الرسالة {0} أو اكتب "@" للتبديل إلى الذكاء الاصطناعي',
com_nav_maximize_chat_space: 'تكبير مساحة الدردشة',
};
6 changes: 6 additions & 0 deletions client/src/localization/languages/De.ts
Original file line number Diff line number Diff line change
Expand Up @@ -939,4 +939,10 @@ export default {
com_nav_welcome_agent: 'Bitte wähle einen Agenten',
com_endpoint_agent_placeholder: 'Bitte wähle einen Agenten aus',
com_ui_delete_shared_link: 'Geteilten Link löschen?',
com_ui_bookmarks_edit: 'Lesezeichen bearbeiten',
com_endpoint_ai: 'KI',
com_ui_page: 'Seite',
com_ui_bookmarks_add: 'Lesezeichen hinzufügen',
com_endpoint_message_new: 'Nachricht {0} oder "@" eingeben, um KI zu wechseln',
com_nav_maximize_chat_space: 'Chat-Bereich maximieren',
};
2 changes: 2 additions & 0 deletions client/src/localization/languages/Eng.ts
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,8 @@ export default {
'WARNING: Misuse of this feature can get you BANNED from using Bing! Click on \'System Message\' for full instructions and the default message if omitted, which is the \'Sydney\' preset that is considered safe.',
com_endpoint_system_message: 'System Message',
com_endpoint_message: 'Message',
com_endpoint_ai: 'AI',
com_endpoint_message_new: 'Message {0} or type "@" to switch AI',

This comment has been minimized.

Copy link
@AetherMagee

AetherMagee Jan 7, 2025

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?

com_endpoint_message_not_appendable: 'Edit your message or Regenerate.',
com_endpoint_default_blank: 'default: blank',
com_endpoint_default_false: 'default: false',
Expand Down
6 changes: 6 additions & 0 deletions client/src/localization/languages/Es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1195,4 +1195,10 @@ export default {
com_endpoint_agent_placeholder: 'Por favor seleccione un Agente',
com_nav_welcome_agent: 'Seleccione un agente',
com_ui_delete_shared_link: '¿Eliminar enlace compartido?',
com_ui_bookmarks_add: 'Agregar Marcadores',
com_ui_page: 'Página',
com_ui_bookmarks_edit: 'Editar Marcador',
com_endpoint_message_new: 'Mensaje {0} o escriba "@" para cambiar de IA',
com_nav_maximize_chat_space: 'Maximizar espacio del chat',
com_endpoint_ai: 'IA',
};
6 changes: 6 additions & 0 deletions client/src/localization/languages/Fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -957,4 +957,10 @@ export default {
com_endpoint_agent_placeholder: 'Veuillez sélectionner un Agent',
com_nav_welcome_agent: 'Veuillez sélectionner un agent',
com_ui_delete_shared_link: 'Supprimer le lien partagé ?',
com_ui_bookmarks_add: 'Ajouter des signets',
com_ui_bookmarks_edit: 'Modifier le signet',
com_endpoint_ai: 'IA',
com_nav_maximize_chat_space: 'Maximiser l\'espace de discussion',
com_endpoint_message_new: 'Message {0} ou tapez "@" pour changer d\'IA',
com_ui_page: 'Page',
};
Loading

0 comments on commit 7987e04

Please sign in to comment.