diff --git a/.github/workflows/i18n-unused-keys.yml b/.github/workflows/i18n-unused-keys.yml index 5e29a8a8bd..f720a61783 100644 --- a/.github/workflows/i18n-unused-keys.yml +++ b/.github/workflows/i18n-unused-keys.yml @@ -39,12 +39,35 @@ jobs: # Check if each key is used in the source code for KEY in $KEYS; do FOUND=false - for DIR in "${SOURCE_DIRS[@]}"; do - if grep -r --include=\*.{js,jsx,ts,tsx} -q "$KEY" "$DIR"; then - FOUND=true - break + + # Special case for dynamically constructed special variable keys + if [[ "$KEY" == com_ui_special_var_* ]]; then + # Check if TSpecialVarLabel is used in the codebase + for DIR in "${SOURCE_DIRS[@]}"; do + if grep -r --include=\*.{js,jsx,ts,tsx} -q "TSpecialVarLabel" "$DIR"; then + FOUND=true + break + fi + done + + # Also check if the key is directly used somewhere + if [[ "$FOUND" == false ]]; then + for DIR in "${SOURCE_DIRS[@]}"; do + if grep -r --include=\*.{js,jsx,ts,tsx} -q "$KEY" "$DIR"; then + FOUND=true + break + fi + done fi - done + else + # Regular check for other keys + for DIR in "${SOURCE_DIRS[@]}"; do + if grep -r --include=\*.{js,jsx,ts,tsx} -q "$KEY" "$DIR"; then + FOUND=true + break + fi + done + fi if [[ "$FOUND" == false ]]; then UNUSED_KEYS+=("$KEY") @@ -90,4 +113,4 @@ jobs: - name: Fail workflow if unused keys found if: env.unused_keys != '[]' - run: exit 1 \ No newline at end of file + run: exit 1 diff --git a/api/models/Agent.js b/api/models/Agent.js index 0e4321df9a..9b34eeae65 100644 --- a/api/models/Agent.js +++ b/api/models/Agent.js @@ -308,7 +308,7 @@ const getListAgents = async (searchParameter) => { * This function also updates the corresponding projects to include or exclude the agent ID. * * @param {Object} params - Parameters for updating the agent's projects. - * @param {import('librechat-data-provider').TUser} params.user - Parameters for updating the agent's projects. + * @param {MongoUser} params.user - Parameters for updating the agent's projects. * @param {string} params.agentId - The ID of the agent to update. * @param {string[]} [params.projectIds] - Array of project IDs to add to the agent. * @param {string[]} [params.removeProjectIds] - Array of project IDs to remove from the agent. diff --git a/api/server/controllers/AskController.js b/api/server/controllers/AskController.js index 8904a57220..7676b78825 100644 --- a/api/server/controllers/AskController.js +++ b/api/server/controllers/AskController.js @@ -128,7 +128,7 @@ const AskController = async (req, res, next, initializeClient, addTitle) => { clientRef = new WeakRef(client); getAbortData = () => { - const currentClient = clientRef.deref(); + const currentClient = clientRef?.deref(); const currentText = currentClient?.getStreamText != null ? currentClient.getStreamText() : getPartialText(); @@ -255,7 +255,7 @@ const AskController = async (req, res, next, initializeClient, addTitle) => { logger.error('[AskController] Error handling request', error); let partialText = ''; try { - const currentClient = clientRef.deref(); + const currentClient = clientRef?.deref(); partialText = currentClient?.getStreamText != null ? currentClient.getStreamText() : getPartialText(); } catch (getTextError) { @@ -268,6 +268,7 @@ const AskController = async (req, res, next, initializeClient, addTitle) => { conversationId: reqDataContext.conversationId, messageId: reqDataContext.responseMessageId, parentMessageId: overrideParentMessageId ?? reqDataContext.userMessageId ?? parentMessageId, + userMessageId: reqDataContext.userMessageId, }) .catch((err) => { logger.error('[AskController] Error in `handleAbortError` during catch block', err); diff --git a/api/server/controllers/EditController.js b/api/server/controllers/EditController.js index e7ebc0785c..d142d474df 100644 --- a/api/server/controllers/EditController.js +++ b/api/server/controllers/EditController.js @@ -123,7 +123,7 @@ const EditController = async (req, res, next, initializeClient) => { clientRef = new WeakRef(client); getAbortData = () => { - const currentClient = clientRef.deref(); + const currentClient = clientRef?.deref(); const currentText = currentClient?.getStreamText != null ? currentClient.getStreamText() : getPartialText(); @@ -219,7 +219,7 @@ const EditController = async (req, res, next, initializeClient) => { logger.error('[EditController] Error handling request', error); let partialText = ''; try { - const currentClient = clientRef.deref(); + const currentClient = clientRef?.deref(); partialText = currentClient?.getStreamText != null ? currentClient.getStreamText() : getPartialText(); } catch (getTextError) { @@ -232,6 +232,7 @@ const EditController = async (req, res, next, initializeClient) => { conversationId, messageId: reqDataContext.responseMessageId, parentMessageId: overrideParentMessageId ?? userMessageId ?? parentMessageId, + userMessageId, }) .catch((err) => { logger.error('[EditController] Error in `handleAbortError` during catch block', err); diff --git a/api/server/controllers/PluginController.js b/api/server/controllers/PluginController.js index 71e7ed348e..8bb1df59e5 100644 --- a/api/server/controllers/PluginController.js +++ b/api/server/controllers/PluginController.js @@ -1,5 +1,6 @@ const { CacheKeys, AuthType } = require('librechat-data-provider'); const { addOpenAPISpecs } = require('~/app/clients/tools/util/addOpenAPISpecs'); +const { getToolkitKey } = require('~/server/services/ToolService'); const { getCustomConfig } = require('~/server/services/Config'); const { availableTools } = require('~/app/clients/tools'); const { getMCPManager } = require('~/config'); @@ -128,7 +129,7 @@ const getAvailableTools = async (req, res) => { (plugin) => toolDefinitions[plugin.pluginKey] !== undefined || (plugin.toolkit === true && - Object.keys(toolDefinitions).some((key) => key.startsWith(`${plugin.pluginKey}_`))), + Object.keys(toolDefinitions).some((key) => getToolkitKey(key) === plugin.pluginKey)), ); await cache.set(CacheKeys.TOOLS, tools); diff --git a/api/server/controllers/agents/request.js b/api/server/controllers/agents/request.js index 80b70a7450..fcee62edc7 100644 --- a/api/server/controllers/agents/request.js +++ b/api/server/controllers/agents/request.js @@ -259,6 +259,7 @@ const AgentController = async (req, res, next, initializeClient, addTitle) => { sender, messageId: responseMessageId, parentMessageId: overrideParentMessageId ?? userMessageId ?? parentMessageId, + userMessageId, }) .catch((err) => { logger.error('[api/server/controllers/agents/request] Error in `handleAbortError`', err); diff --git a/api/server/middleware/abortMiddleware.js b/api/server/middleware/abortMiddleware.js index b5a7c4eb90..bfc28f513d 100644 --- a/api/server/middleware/abortMiddleware.js +++ b/api/server/middleware/abortMiddleware.js @@ -311,7 +311,7 @@ const handleAbortError = async (res, req, error, data) => { } else { logger.error('[handleAbortError] AI response error; aborting request:', error); } - const { sender, conversationId, messageId, parentMessageId, partialText } = data; + const { sender, conversationId, messageId, parentMessageId, userMessageId, partialText } = data; if (error.stack && error.stack.includes('google')) { logger.warn( @@ -344,10 +344,10 @@ const handleAbortError = async (res, req, error, data) => { parentMessageId, text: errorText, user: req.user.id, - shouldSaveMessage: true, spec: endpointOption?.spec, iconURL: endpointOption?.iconURL, modelLabel: endpointOption?.modelLabel, + shouldSaveMessage: userMessageId != null, model: endpointOption?.modelOptions?.model || req.body?.model, }; diff --git a/api/server/services/ActionService.js b/api/server/services/ActionService.js index 1255b3db49..c8a7955427 100644 --- a/api/server/services/ActionService.js +++ b/api/server/services/ActionService.js @@ -146,7 +146,7 @@ async function createActionTool({ /** @type {import('librechat-data-provider').ActionMetadataRuntime} */ const metadata = action.metadata; const executor = requestBuilder.createExecutor(); - const preparedExecutor = executor.setParams(toolInput); + const preparedExecutor = executor.setParams(toolInput ?? {}); if (metadata.auth && metadata.auth.type !== AuthTypeEnum.None) { try { diff --git a/api/server/services/Endpoints/agents/initialize.js b/api/server/services/Endpoints/agents/initialize.js index e26ed08848..d0d29fa75e 100644 --- a/api/server/services/Endpoints/agents/initialize.js +++ b/api/server/services/Endpoints/agents/initialize.js @@ -6,6 +6,7 @@ const { EToolResources, getResponseSender, AgentCapabilities, + replaceSpecialVars, providerEndpointMap, } = require('librechat-data-provider'); const { @@ -246,6 +247,13 @@ const initializeAgentOptions = async ({ agent.model_parameters.model = agent.model; } + if (agent.instructions && agent.instructions !== '') { + agent.instructions = replaceSpecialVars({ + text: agent.instructions, + user: req.user, + }); + } + if (typeof agent.artifacts === 'string' && agent.artifacts !== '') { agent.additional_instructions = generateArtifactsPrompt({ endpoint: agent.provider, diff --git a/api/server/services/ToolService.js b/api/server/services/ToolService.js index f3e7df5a72..b71e97f742 100644 --- a/api/server/services/ToolService.js +++ b/api/server/services/ToolService.js @@ -8,6 +8,7 @@ const { ErrorTypes, ContentTypes, imageGenTools, + EToolResources, EModelEndpoint, actionDelimiter, ImageVisionTool, @@ -36,6 +37,30 @@ const { redactMessage } = require('~/config/parsers'); const { sleep } = require('~/server/utils'); const { logger } = require('~/config'); +/** + * @param {string} toolName + * @returns {string | undefined} toolKey + */ +function getToolkitKey(toolName) { + /** @type {string|undefined} */ + let toolkitKey; + for (const toolkit of toolkits) { + if (toolName.startsWith(EToolResources.image_edit)) { + const splitMatches = toolkit.pluginKey.split('_'); + const suffix = splitMatches[splitMatches.length - 1]; + if (toolName.endsWith(suffix)) { + toolkitKey = toolkit.pluginKey; + break; + } + } + if (toolName.startsWith(toolkit.pluginKey)) { + toolkitKey = toolkit.pluginKey; + break; + } + } + return toolkitKey; +} + /** * Loads and formats tools from the specified tool directory. * @@ -108,7 +133,7 @@ function loadAndFormatTools({ directory, adminFilter = [], adminIncluded = [] }) tools.push(formattedTool); } - /** Basic Tools; schema: { input: string } */ + /** Basic Tools & Toolkits; schema: { input: string } */ const basicToolInstances = [ new Calculator(), ...createOpenAIImageTools({ override: true }), @@ -117,9 +142,7 @@ function loadAndFormatTools({ directory, adminFilter = [], adminIncluded = [] }) for (const toolInstance of basicToolInstances) { const formattedTool = formatToOpenAIAssistantTool(toolInstance); let toolName = formattedTool[Tools.function].name; - toolName = toolkits.some((toolkit) => toolName.startsWith(toolkit.pluginKey)) - ? toolName.split('_')[0] - : toolName; + toolName = getToolkitKey(toolName) ?? toolName; if (filter.has(toolName) && included.size === 0) { continue; } @@ -682,6 +705,7 @@ async function loadAgentTools({ req, res, agent, tool_resources, openAIApiKey }) } module.exports = { + getToolkitKey, loadAgentTools, loadAndFormatTools, processRequiredActions, diff --git a/api/server/utils/streamResponse.js b/api/server/utils/streamResponse.js index 0f042339a9..bb8d63b229 100644 --- a/api/server/utils/streamResponse.js +++ b/api/server/utils/streamResponse.js @@ -70,7 +70,13 @@ const sendError = async (req, res, options, callback) => { } if (shouldSaveMessage) { - await saveMessage(req, { ...errorMessage, user }); + await saveMessage( + req, + { ...errorMessage, user }, + { + context: 'api/server/utils/streamResponse.js - sendError', + }, + ); } if (!errorMessage.error) { diff --git a/client/src/components/Chat/ChatView.tsx b/client/src/components/Chat/ChatView.tsx index bce6974c16..a554c5f7d1 100644 --- a/client/src/components/Chat/ChatView.tsx +++ b/client/src/components/Chat/ChatView.tsx @@ -1,14 +1,14 @@ -import { memo, useMemo, useCallback } from 'react'; +import { memo, useCallback } from 'react'; import { useRecoilValue } from 'recoil'; import { useForm } from 'react-hook-form'; import { useParams } from 'react-router-dom'; import { Constants } from 'librechat-data-provider'; -import { useGetMessagesByConvoId } from 'librechat-data-provider/react-query'; import type { TMessage } from 'librechat-data-provider'; import type { ChatFormValues } from '~/common'; import { ChatContext, AddedChatContext, useFileMapContext, ChatFormProvider } from '~/Providers'; import { useChatHelpers, useAddedResponse, useSSE } from '~/hooks'; import ConversationStarters from './Input/ConversationStarters'; +import { useGetMessagesByConvoId } from '~/data-provider'; import MessagesView from './Messages/MessagesView'; import { Spinner } from '~/components/svg'; import Presentation from './Presentation'; diff --git a/client/src/components/Chat/Input/ChatForm.tsx b/client/src/components/Chat/Input/ChatForm.tsx index 4e5e68c073..2570d213c6 100644 --- a/client/src/components/Chat/Input/ChatForm.tsx +++ b/client/src/components/Chat/Input/ChatForm.tsx @@ -132,7 +132,13 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => { setShowPlusPopover, setShowMentionPopover, }); - const { handlePaste, handleKeyDown, handleCompositionStart, handleCompositionEnd } = useTextarea({ + const { + isNotAppendable, + handlePaste, + handleKeyDown, + handleCompositionStart, + handleCompositionEnd, + } = useTextarea({ textAreaRef, submitButtonRef, setIsScrollable, @@ -251,7 +257,7 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => { ref(e); (textAreaRef as React.MutableRefObject).current = e; }} - disabled={disableInputs} + disabled={disableInputs || isNotAppendable} onPaste={handlePaste} onKeyDown={handleKeyDown} onKeyUp={handleKeyUp} @@ -271,7 +277,7 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => { className={cn( baseClasses, removeFocusRings, - 'transition-[max-height] duration-200', + 'transition-[max-height] duration-200 disabled:cursor-not-allowed', )} />
@@ -306,7 +312,7 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => { methods={methods} ask={submitMessage} textAreaRef={textAreaRef} - disabled={disableInputs} + disabled={disableInputs || isNotAppendable} isSubmitting={isSubmitting} /> )} @@ -318,7 +324,7 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => { ) )} diff --git a/client/src/components/Chat/Input/PromptsCommand.tsx b/client/src/components/Chat/Input/PromptsCommand.tsx index c34ffc0708..835c7cd31d 100644 --- a/client/src/components/Chat/Input/PromptsCommand.tsx +++ b/client/src/components/Chat/Input/PromptsCommand.tsx @@ -201,7 +201,7 @@ function PromptsCommand({
); diff --git a/client/src/components/Prompts/Groups/CategoryIcon.tsx b/client/src/components/Prompts/Groups/CategoryIcon.tsx index faa8df1c65..b9d37f1afc 100644 --- a/client/src/components/Prompts/Groups/CategoryIcon.tsx +++ b/client/src/components/Prompts/Groups/CategoryIcon.tsx @@ -28,9 +28,9 @@ const categoryColorMap: Record = { code: 'text-red-500', misc: 'text-blue-300', shop: 'text-purple-400', - idea: 'text-yellow-300', + idea: 'text-yellow-500/90 dark:text-yellow-300 ', write: 'text-purple-400', - travel: 'text-yellow-300', + travel: 'text-yellow-500/90 dark:text-yellow-300 ', finance: 'text-orange-400', roleplay: 'text-orange-400', teach_or_explain: 'text-blue-300', diff --git a/client/src/components/Prompts/Groups/CategorySelector.tsx b/client/src/components/Prompts/Groups/CategorySelector.tsx index 6e2245e8c3..3c57778924 100644 --- a/client/src/components/Prompts/Groups/CategorySelector.tsx +++ b/client/src/components/Prompts/Groups/CategorySelector.tsx @@ -1,4 +1,6 @@ import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import type { ReactNode } from 'react'; import { useFormContext, Controller } from 'react-hook-form'; import { LocalStorageKeys } from 'librechat-data-provider'; import { Dropdown } from '~/components/ui'; @@ -15,6 +17,7 @@ const CategorySelector: React.FC = ({ onValueChange, className = '', }) => { + const { t } = useTranslation(); const formContext = useFormContext(); const { categories, emptyCategory } = useCategories(); @@ -32,13 +35,25 @@ const CategorySelector: React.FC = ({ [watchedCategory, categories, currentCategory, emptyCategory], ); + const displayCategory = useMemo(() => { + if (!categoryOption.value && !('icon' in categoryOption)) { + return { + ...categoryOption, + icon: () as ReactNode, + label: categoryOption.label || t('com_ui_empty_category'), + }; + } + return categoryOption; + }, [categoryOption, t]); + return formContext ? ( ( { setValue('category', value, { shouldDirty: false }); localStorage.setItem(LocalStorageKeys.LAST_PROMPT_CATEGORY, value); @@ -48,10 +63,12 @@ const CategorySelector: React.FC = ({ ariaLabel="Prompt's category selector" className={className} options={categories || []} - renderValue={(option) => ( + renderValue={() => (
- {option.icon != null && {option.icon as React.ReactNode}} - {option.label} + {'icon' in displayCategory && displayCategory.icon != null && ( + {displayCategory.icon as ReactNode} + )} + {displayCategory.label}
)} /> @@ -68,10 +85,12 @@ const CategorySelector: React.FC = ({ ariaLabel="Prompt's category selector" className={className} options={categories || []} - renderValue={(option) => ( + renderValue={() => (
- {option.icon != null && {option.icon as React.ReactNode}} - {option.label} + {'icon' in displayCategory && displayCategory.icon != null && ( + {displayCategory.icon as ReactNode} + )} + {displayCategory.label}
)} /> diff --git a/client/src/components/Prompts/Groups/ChatGroupItem.tsx b/client/src/components/Prompts/Groups/ChatGroupItem.tsx index f9b2850af6..f8818b9f68 100644 --- a/client/src/components/Prompts/Groups/ChatGroupItem.tsx +++ b/client/src/components/Prompts/Groups/ChatGroupItem.tsx @@ -57,7 +57,7 @@ function ChatGroupItem({ snippet={ typeof group.oneliner === 'string' && group.oneliner.length > 0 ? group.oneliner - : group.productionPrompt?.prompt ?? '' + : (group.productionPrompt?.prompt ?? '') } >
@@ -83,7 +83,11 @@ function ChatGroupItem({ className="z-50 inline-flex h-8 w-8 items-center justify-center rounded-lg border border-border-medium bg-transparent p-0 text-sm font-medium transition-all duration-300 ease-in-out hover:border-border-heavy hover:bg-surface-hover focus:border-border-heavy focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" >