Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🔗 feat: Convo Settings via URL Query Params & Mention Models #5184

Merged
merged 10 commits into from
Jan 5, 2025
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',
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
Loading