mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 08:12:00 +02:00
🗓️ feat: Add Special Variables for Prompts & Agents, Prompt UI Improvements (#7123)
* wip: Add Instructions component for agent configuration
* ✨ feat: Implement DropdownPopup for variable insertion in instructions
* refactor: Enhance variable handling by exporting specialVariables and updating Markdown components
* feat: Add special variable support for current date and user in Instructions component
* refactor: Update handleAddVariable to include localized label
* feat: replace special variables in instructions presets
* chore: update parameter type for user in getListAgents function
* refactor: integrate dayjs for date handling and move replaceSpecialVars function to data-provider
* feat: enhance replaceSpecialVars to include day number in current date format
* feat: integrate replaceSpecialVars for processing agent instructions
* feat: add support for current date & time in replaceSpecialVars function
* feat: add iso_datetime support in replaceSpecialVars function
* fix: enforce text parameter to be a required field in replaceSpecialVars function
* feat: add ISO datetime support in translation file
* fix: disable eslint warning for autoFocus in TextareaAutosize component
* feat: add VariablesDropdown component and integrate it into CreatePromptForm and PromptEditor; update translation for special variables
* fix: CategorySelector and related localizations
* fix: add z-index class to LanguageSTTDropdown for proper stacking context
* fix: add max-height and overflow styles to OGDialogContent in VariableDialog and PreviewPrompt components
* fix: update variable detection logic to exclude special variables and improve regex matching
* fix: improve accessibility text for actions menu in ChatGroupItem component
* fix: adjust max-width and height styles for dialog components and improve markdown rendering for light vs. dark, height/widths, etc.
* fix: remove commented-out code for better readability in PromptVariableGfm component
* fix: handle undefined input parameter in setParams function call
* fix: update variable label types to use TSpecialVarLabel for consistency
* fix: remove outdated information from special variables description in translation file
* fix: enhance unused i18next keys detection for special variable keys
* fix: update color classes for consistency/a11y in category and prompt variable components
* fix: update PromptVariableGfm component and special variable styles for consistency
* fix: improve variable highlighting logic in VariableForm component
* fix: update background color classes for consistency in VariableForm component
* fix: add missing ref parameter to Dialog component in OriginalDialog
* refactor: move navigate call for new conversation to after setConversation update
* refactor: move message query hook to client workspace; fix: handle edge case for navigation from finalHandler creating race condition for response message DB save
* chore: bump librechat-data-provider to 0.7.793
* ci: add unit tests for replaceSpecialVars function
* fix: implement getToolkitKey function for image_gen_oai toolkit filtering/including
* ci: enhance dayjs mock for consistent date/time values in tests
* fix: MCP stdio server fail to start when passing env property
* fix: use optional chaining for clientRef dereferencing in AskController and EditController
feat: add context to saveMessage call in streamResponse utility
* fix: only save error messages if the userMessageId was initialized
* refactor: add isNotAppendable check to disable inputs in ChatForm and useTextarea
* feat: enhance error handling in useEventHandlers and update conversation state in useNewConvo
* refactor: prepend underscore to conversationId in newConversation template
* feat: log aborted conversations with minimal messages and use consistent conversationId generation
---------
Co-authored-by: Olivier Schiavo <olivier.schiavo@wengo.com>
Co-authored-by: aka012 <aka012@neowiz.com>
Co-authored-by: jiasheng <jiashengguo@outlook.com>
This commit is contained in:
parent
0e8041bcac
commit
55f5f2d11a
47 changed files with 707 additions and 195 deletions
35
.github/workflows/i18n-unused-keys.yml
vendored
35
.github/workflows/i18n-unused-keys.yml
vendored
|
@ -39,12 +39,35 @@ jobs:
|
||||||
# Check if each key is used in the source code
|
# Check if each key is used in the source code
|
||||||
for KEY in $KEYS; do
|
for KEY in $KEYS; do
|
||||||
FOUND=false
|
FOUND=false
|
||||||
for DIR in "${SOURCE_DIRS[@]}"; do
|
|
||||||
if grep -r --include=\*.{js,jsx,ts,tsx} -q "$KEY" "$DIR"; then
|
# Special case for dynamically constructed special variable keys
|
||||||
FOUND=true
|
if [[ "$KEY" == com_ui_special_var_* ]]; then
|
||||||
break
|
# 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
|
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
|
if [[ "$FOUND" == false ]]; then
|
||||||
UNUSED_KEYS+=("$KEY")
|
UNUSED_KEYS+=("$KEY")
|
||||||
|
@ -90,4 +113,4 @@ jobs:
|
||||||
|
|
||||||
- name: Fail workflow if unused keys found
|
- name: Fail workflow if unused keys found
|
||||||
if: env.unused_keys != '[]'
|
if: env.unused_keys != '[]'
|
||||||
run: exit 1
|
run: exit 1
|
||||||
|
|
|
@ -308,7 +308,7 @@ const getListAgents = async (searchParameter) => {
|
||||||
* This function also updates the corresponding projects to include or exclude the agent ID.
|
* 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 {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.agentId - The ID of the agent to update.
|
||||||
* @param {string[]} [params.projectIds] - Array of project IDs to add to the agent.
|
* @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.
|
* @param {string[]} [params.removeProjectIds] - Array of project IDs to remove from the agent.
|
||||||
|
|
|
@ -128,7 +128,7 @@ const AskController = async (req, res, next, initializeClient, addTitle) => {
|
||||||
clientRef = new WeakRef(client);
|
clientRef = new WeakRef(client);
|
||||||
|
|
||||||
getAbortData = () => {
|
getAbortData = () => {
|
||||||
const currentClient = clientRef.deref();
|
const currentClient = clientRef?.deref();
|
||||||
const currentText =
|
const currentText =
|
||||||
currentClient?.getStreamText != null ? currentClient.getStreamText() : getPartialText();
|
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);
|
logger.error('[AskController] Error handling request', error);
|
||||||
let partialText = '';
|
let partialText = '';
|
||||||
try {
|
try {
|
||||||
const currentClient = clientRef.deref();
|
const currentClient = clientRef?.deref();
|
||||||
partialText =
|
partialText =
|
||||||
currentClient?.getStreamText != null ? currentClient.getStreamText() : getPartialText();
|
currentClient?.getStreamText != null ? currentClient.getStreamText() : getPartialText();
|
||||||
} catch (getTextError) {
|
} catch (getTextError) {
|
||||||
|
@ -268,6 +268,7 @@ const AskController = async (req, res, next, initializeClient, addTitle) => {
|
||||||
conversationId: reqDataContext.conversationId,
|
conversationId: reqDataContext.conversationId,
|
||||||
messageId: reqDataContext.responseMessageId,
|
messageId: reqDataContext.responseMessageId,
|
||||||
parentMessageId: overrideParentMessageId ?? reqDataContext.userMessageId ?? parentMessageId,
|
parentMessageId: overrideParentMessageId ?? reqDataContext.userMessageId ?? parentMessageId,
|
||||||
|
userMessageId: reqDataContext.userMessageId,
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
logger.error('[AskController] Error in `handleAbortError` during catch block', err);
|
logger.error('[AskController] Error in `handleAbortError` during catch block', err);
|
||||||
|
|
|
@ -123,7 +123,7 @@ const EditController = async (req, res, next, initializeClient) => {
|
||||||
clientRef = new WeakRef(client);
|
clientRef = new WeakRef(client);
|
||||||
|
|
||||||
getAbortData = () => {
|
getAbortData = () => {
|
||||||
const currentClient = clientRef.deref();
|
const currentClient = clientRef?.deref();
|
||||||
const currentText =
|
const currentText =
|
||||||
currentClient?.getStreamText != null ? currentClient.getStreamText() : getPartialText();
|
currentClient?.getStreamText != null ? currentClient.getStreamText() : getPartialText();
|
||||||
|
|
||||||
|
@ -219,7 +219,7 @@ const EditController = async (req, res, next, initializeClient) => {
|
||||||
logger.error('[EditController] Error handling request', error);
|
logger.error('[EditController] Error handling request', error);
|
||||||
let partialText = '';
|
let partialText = '';
|
||||||
try {
|
try {
|
||||||
const currentClient = clientRef.deref();
|
const currentClient = clientRef?.deref();
|
||||||
partialText =
|
partialText =
|
||||||
currentClient?.getStreamText != null ? currentClient.getStreamText() : getPartialText();
|
currentClient?.getStreamText != null ? currentClient.getStreamText() : getPartialText();
|
||||||
} catch (getTextError) {
|
} catch (getTextError) {
|
||||||
|
@ -232,6 +232,7 @@ const EditController = async (req, res, next, initializeClient) => {
|
||||||
conversationId,
|
conversationId,
|
||||||
messageId: reqDataContext.responseMessageId,
|
messageId: reqDataContext.responseMessageId,
|
||||||
parentMessageId: overrideParentMessageId ?? userMessageId ?? parentMessageId,
|
parentMessageId: overrideParentMessageId ?? userMessageId ?? parentMessageId,
|
||||||
|
userMessageId,
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
logger.error('[EditController] Error in `handleAbortError` during catch block', err);
|
logger.error('[EditController] Error in `handleAbortError` during catch block', err);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
const { CacheKeys, AuthType } = require('librechat-data-provider');
|
const { CacheKeys, AuthType } = require('librechat-data-provider');
|
||||||
const { addOpenAPISpecs } = require('~/app/clients/tools/util/addOpenAPISpecs');
|
const { addOpenAPISpecs } = require('~/app/clients/tools/util/addOpenAPISpecs');
|
||||||
|
const { getToolkitKey } = require('~/server/services/ToolService');
|
||||||
const { getCustomConfig } = require('~/server/services/Config');
|
const { getCustomConfig } = require('~/server/services/Config');
|
||||||
const { availableTools } = require('~/app/clients/tools');
|
const { availableTools } = require('~/app/clients/tools');
|
||||||
const { getMCPManager } = require('~/config');
|
const { getMCPManager } = require('~/config');
|
||||||
|
@ -128,7 +129,7 @@ const getAvailableTools = async (req, res) => {
|
||||||
(plugin) =>
|
(plugin) =>
|
||||||
toolDefinitions[plugin.pluginKey] !== undefined ||
|
toolDefinitions[plugin.pluginKey] !== undefined ||
|
||||||
(plugin.toolkit === true &&
|
(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);
|
await cache.set(CacheKeys.TOOLS, tools);
|
||||||
|
|
|
@ -259,6 +259,7 @@ const AgentController = async (req, res, next, initializeClient, addTitle) => {
|
||||||
sender,
|
sender,
|
||||||
messageId: responseMessageId,
|
messageId: responseMessageId,
|
||||||
parentMessageId: overrideParentMessageId ?? userMessageId ?? parentMessageId,
|
parentMessageId: overrideParentMessageId ?? userMessageId ?? parentMessageId,
|
||||||
|
userMessageId,
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
logger.error('[api/server/controllers/agents/request] Error in `handleAbortError`', err);
|
logger.error('[api/server/controllers/agents/request] Error in `handleAbortError`', err);
|
||||||
|
|
|
@ -311,7 +311,7 @@ const handleAbortError = async (res, req, error, data) => {
|
||||||
} else {
|
} else {
|
||||||
logger.error('[handleAbortError] AI response error; aborting request:', error);
|
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')) {
|
if (error.stack && error.stack.includes('google')) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
|
@ -344,10 +344,10 @@ const handleAbortError = async (res, req, error, data) => {
|
||||||
parentMessageId,
|
parentMessageId,
|
||||||
text: errorText,
|
text: errorText,
|
||||||
user: req.user.id,
|
user: req.user.id,
|
||||||
shouldSaveMessage: true,
|
|
||||||
spec: endpointOption?.spec,
|
spec: endpointOption?.spec,
|
||||||
iconURL: endpointOption?.iconURL,
|
iconURL: endpointOption?.iconURL,
|
||||||
modelLabel: endpointOption?.modelLabel,
|
modelLabel: endpointOption?.modelLabel,
|
||||||
|
shouldSaveMessage: userMessageId != null,
|
||||||
model: endpointOption?.modelOptions?.model || req.body?.model,
|
model: endpointOption?.modelOptions?.model || req.body?.model,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -146,7 +146,7 @@ async function createActionTool({
|
||||||
/** @type {import('librechat-data-provider').ActionMetadataRuntime} */
|
/** @type {import('librechat-data-provider').ActionMetadataRuntime} */
|
||||||
const metadata = action.metadata;
|
const metadata = action.metadata;
|
||||||
const executor = requestBuilder.createExecutor();
|
const executor = requestBuilder.createExecutor();
|
||||||
const preparedExecutor = executor.setParams(toolInput);
|
const preparedExecutor = executor.setParams(toolInput ?? {});
|
||||||
|
|
||||||
if (metadata.auth && metadata.auth.type !== AuthTypeEnum.None) {
|
if (metadata.auth && metadata.auth.type !== AuthTypeEnum.None) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -6,6 +6,7 @@ const {
|
||||||
EToolResources,
|
EToolResources,
|
||||||
getResponseSender,
|
getResponseSender,
|
||||||
AgentCapabilities,
|
AgentCapabilities,
|
||||||
|
replaceSpecialVars,
|
||||||
providerEndpointMap,
|
providerEndpointMap,
|
||||||
} = require('librechat-data-provider');
|
} = require('librechat-data-provider');
|
||||||
const {
|
const {
|
||||||
|
@ -246,6 +247,13 @@ const initializeAgentOptions = async ({
|
||||||
agent.model_parameters.model = agent.model;
|
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 !== '') {
|
if (typeof agent.artifacts === 'string' && agent.artifacts !== '') {
|
||||||
agent.additional_instructions = generateArtifactsPrompt({
|
agent.additional_instructions = generateArtifactsPrompt({
|
||||||
endpoint: agent.provider,
|
endpoint: agent.provider,
|
||||||
|
|
|
@ -8,6 +8,7 @@ const {
|
||||||
ErrorTypes,
|
ErrorTypes,
|
||||||
ContentTypes,
|
ContentTypes,
|
||||||
imageGenTools,
|
imageGenTools,
|
||||||
|
EToolResources,
|
||||||
EModelEndpoint,
|
EModelEndpoint,
|
||||||
actionDelimiter,
|
actionDelimiter,
|
||||||
ImageVisionTool,
|
ImageVisionTool,
|
||||||
|
@ -36,6 +37,30 @@ const { redactMessage } = require('~/config/parsers');
|
||||||
const { sleep } = require('~/server/utils');
|
const { sleep } = require('~/server/utils');
|
||||||
const { logger } = require('~/config');
|
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.
|
* Loads and formats tools from the specified tool directory.
|
||||||
*
|
*
|
||||||
|
@ -108,7 +133,7 @@ function loadAndFormatTools({ directory, adminFilter = [], adminIncluded = [] })
|
||||||
tools.push(formattedTool);
|
tools.push(formattedTool);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Basic Tools; schema: { input: string } */
|
/** Basic Tools & Toolkits; schema: { input: string } */
|
||||||
const basicToolInstances = [
|
const basicToolInstances = [
|
||||||
new Calculator(),
|
new Calculator(),
|
||||||
...createOpenAIImageTools({ override: true }),
|
...createOpenAIImageTools({ override: true }),
|
||||||
|
@ -117,9 +142,7 @@ function loadAndFormatTools({ directory, adminFilter = [], adminIncluded = [] })
|
||||||
for (const toolInstance of basicToolInstances) {
|
for (const toolInstance of basicToolInstances) {
|
||||||
const formattedTool = formatToOpenAIAssistantTool(toolInstance);
|
const formattedTool = formatToOpenAIAssistantTool(toolInstance);
|
||||||
let toolName = formattedTool[Tools.function].name;
|
let toolName = formattedTool[Tools.function].name;
|
||||||
toolName = toolkits.some((toolkit) => toolName.startsWith(toolkit.pluginKey))
|
toolName = getToolkitKey(toolName) ?? toolName;
|
||||||
? toolName.split('_')[0]
|
|
||||||
: toolName;
|
|
||||||
if (filter.has(toolName) && included.size === 0) {
|
if (filter.has(toolName) && included.size === 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -682,6 +705,7 @@ async function loadAgentTools({ req, res, agent, tool_resources, openAIApiKey })
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
getToolkitKey,
|
||||||
loadAgentTools,
|
loadAgentTools,
|
||||||
loadAndFormatTools,
|
loadAndFormatTools,
|
||||||
processRequiredActions,
|
processRequiredActions,
|
||||||
|
|
|
@ -70,7 +70,13 @@ const sendError = async (req, res, options, callback) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldSaveMessage) {
|
if (shouldSaveMessage) {
|
||||||
await saveMessage(req, { ...errorMessage, user });
|
await saveMessage(
|
||||||
|
req,
|
||||||
|
{ ...errorMessage, user },
|
||||||
|
{
|
||||||
|
context: 'api/server/utils/streamResponse.js - sendError',
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!errorMessage.error) {
|
if (!errorMessage.error) {
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { memo, useMemo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { Constants } from 'librechat-data-provider';
|
import { Constants } from 'librechat-data-provider';
|
||||||
import { useGetMessagesByConvoId } from 'librechat-data-provider/react-query';
|
|
||||||
import type { TMessage } from 'librechat-data-provider';
|
import type { TMessage } from 'librechat-data-provider';
|
||||||
import type { ChatFormValues } from '~/common';
|
import type { ChatFormValues } from '~/common';
|
||||||
import { ChatContext, AddedChatContext, useFileMapContext, ChatFormProvider } from '~/Providers';
|
import { ChatContext, AddedChatContext, useFileMapContext, ChatFormProvider } from '~/Providers';
|
||||||
import { useChatHelpers, useAddedResponse, useSSE } from '~/hooks';
|
import { useChatHelpers, useAddedResponse, useSSE } from '~/hooks';
|
||||||
import ConversationStarters from './Input/ConversationStarters';
|
import ConversationStarters from './Input/ConversationStarters';
|
||||||
|
import { useGetMessagesByConvoId } from '~/data-provider';
|
||||||
import MessagesView from './Messages/MessagesView';
|
import MessagesView from './Messages/MessagesView';
|
||||||
import { Spinner } from '~/components/svg';
|
import { Spinner } from '~/components/svg';
|
||||||
import Presentation from './Presentation';
|
import Presentation from './Presentation';
|
||||||
|
|
|
@ -132,7 +132,13 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
||||||
setShowPlusPopover,
|
setShowPlusPopover,
|
||||||
setShowMentionPopover,
|
setShowMentionPopover,
|
||||||
});
|
});
|
||||||
const { handlePaste, handleKeyDown, handleCompositionStart, handleCompositionEnd } = useTextarea({
|
const {
|
||||||
|
isNotAppendable,
|
||||||
|
handlePaste,
|
||||||
|
handleKeyDown,
|
||||||
|
handleCompositionStart,
|
||||||
|
handleCompositionEnd,
|
||||||
|
} = useTextarea({
|
||||||
textAreaRef,
|
textAreaRef,
|
||||||
submitButtonRef,
|
submitButtonRef,
|
||||||
setIsScrollable,
|
setIsScrollable,
|
||||||
|
@ -251,7 +257,7 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
||||||
ref(e);
|
ref(e);
|
||||||
(textAreaRef as React.MutableRefObject<HTMLTextAreaElement | null>).current = e;
|
(textAreaRef as React.MutableRefObject<HTMLTextAreaElement | null>).current = e;
|
||||||
}}
|
}}
|
||||||
disabled={disableInputs}
|
disabled={disableInputs || isNotAppendable}
|
||||||
onPaste={handlePaste}
|
onPaste={handlePaste}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onKeyUp={handleKeyUp}
|
onKeyUp={handleKeyUp}
|
||||||
|
@ -271,7 +277,7 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
||||||
className={cn(
|
className={cn(
|
||||||
baseClasses,
|
baseClasses,
|
||||||
removeFocusRings,
|
removeFocusRings,
|
||||||
'transition-[max-height] duration-200',
|
'transition-[max-height] duration-200 disabled:cursor-not-allowed',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col items-start justify-start pt-1.5">
|
<div className="flex flex-col items-start justify-start pt-1.5">
|
||||||
|
@ -306,7 +312,7 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
||||||
methods={methods}
|
methods={methods}
|
||||||
ask={submitMessage}
|
ask={submitMessage}
|
||||||
textAreaRef={textAreaRef}
|
textAreaRef={textAreaRef}
|
||||||
disabled={disableInputs}
|
disabled={disableInputs || isNotAppendable}
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -318,7 +324,7 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
||||||
<SendButton
|
<SendButton
|
||||||
ref={submitButtonRef}
|
ref={submitButtonRef}
|
||||||
control={methods.control}
|
control={methods.control}
|
||||||
disabled={filesLoading || isSubmitting || disableInputs}
|
disabled={filesLoading || isSubmitting || disableInputs || isNotAppendable}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -201,7 +201,7 @@ function PromptsCommand({
|
||||||
<div className="popover border-token-border-light rounded-2xl border bg-surface-tertiary-alt p-2 shadow-lg">
|
<div className="popover border-token-border-light rounded-2xl border bg-surface-tertiary-alt p-2 shadow-lg">
|
||||||
<input
|
<input
|
||||||
// The user expects focus to transition to the input field when the popover is opened
|
// The user expects focus to transition to the input field when the popover is opened
|
||||||
|
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||||
autoFocus
|
autoFocus
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
placeholder={localize('com_ui_command_usage_placeholder')}
|
placeholder={localize('com_ui_command_usage_placeholder')}
|
||||||
|
|
|
@ -102,8 +102,8 @@ export default function LanguageSTTDropdown() {
|
||||||
onChange={handleSelect}
|
onChange={handleSelect}
|
||||||
options={languageOptions}
|
options={languageOptions}
|
||||||
sizeClasses="[--anchor-max-height:256px]"
|
sizeClasses="[--anchor-max-height:256px]"
|
||||||
anchor="bottom start"
|
|
||||||
testId="LanguageSTTDropdown"
|
testId="LanguageSTTDropdown"
|
||||||
|
className="z-50"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -28,9 +28,9 @@ const categoryColorMap: Record<string, string> = {
|
||||||
code: 'text-red-500',
|
code: 'text-red-500',
|
||||||
misc: 'text-blue-300',
|
misc: 'text-blue-300',
|
||||||
shop: 'text-purple-400',
|
shop: 'text-purple-400',
|
||||||
idea: 'text-yellow-300',
|
idea: 'text-yellow-500/90 dark:text-yellow-300 ',
|
||||||
write: 'text-purple-400',
|
write: 'text-purple-400',
|
||||||
travel: 'text-yellow-300',
|
travel: 'text-yellow-500/90 dark:text-yellow-300 ',
|
||||||
finance: 'text-orange-400',
|
finance: 'text-orange-400',
|
||||||
roleplay: 'text-orange-400',
|
roleplay: 'text-orange-400',
|
||||||
teach_or_explain: 'text-blue-300',
|
teach_or_explain: 'text-blue-300',
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
import { useFormContext, Controller } from 'react-hook-form';
|
import { useFormContext, Controller } from 'react-hook-form';
|
||||||
import { LocalStorageKeys } from 'librechat-data-provider';
|
import { LocalStorageKeys } from 'librechat-data-provider';
|
||||||
import { Dropdown } from '~/components/ui';
|
import { Dropdown } from '~/components/ui';
|
||||||
|
@ -15,6 +17,7 @@ const CategorySelector: React.FC<CategorySelectorProps> = ({
|
||||||
onValueChange,
|
onValueChange,
|
||||||
className = '',
|
className = '',
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const formContext = useFormContext();
|
const formContext = useFormContext();
|
||||||
const { categories, emptyCategory } = useCategories();
|
const { categories, emptyCategory } = useCategories();
|
||||||
|
|
||||||
|
@ -32,13 +35,25 @@ const CategorySelector: React.FC<CategorySelectorProps> = ({
|
||||||
[watchedCategory, categories, currentCategory, emptyCategory],
|
[watchedCategory, categories, currentCategory, emptyCategory],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const displayCategory = useMemo(() => {
|
||||||
|
if (!categoryOption.value && !('icon' in categoryOption)) {
|
||||||
|
return {
|
||||||
|
...categoryOption,
|
||||||
|
icon: (<span className="i-heroicons-tag" />) as ReactNode,
|
||||||
|
label: categoryOption.label || t('com_ui_empty_category'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return categoryOption;
|
||||||
|
}, [categoryOption, t]);
|
||||||
|
|
||||||
return formContext ? (
|
return formContext ? (
|
||||||
<Controller
|
<Controller
|
||||||
name="category"
|
name="category"
|
||||||
control={control}
|
control={control}
|
||||||
render={() => (
|
render={() => (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
value={categoryOption.value ?? ''}
|
value={displayCategory.value ?? ''}
|
||||||
|
label={displayCategory.value ? undefined : t('com_ui_category')}
|
||||||
onChange={(value: string) => {
|
onChange={(value: string) => {
|
||||||
setValue('category', value, { shouldDirty: false });
|
setValue('category', value, { shouldDirty: false });
|
||||||
localStorage.setItem(LocalStorageKeys.LAST_PROMPT_CATEGORY, value);
|
localStorage.setItem(LocalStorageKeys.LAST_PROMPT_CATEGORY, value);
|
||||||
|
@ -48,10 +63,12 @@ const CategorySelector: React.FC<CategorySelectorProps> = ({
|
||||||
ariaLabel="Prompt's category selector"
|
ariaLabel="Prompt's category selector"
|
||||||
className={className}
|
className={className}
|
||||||
options={categories || []}
|
options={categories || []}
|
||||||
renderValue={(option) => (
|
renderValue={() => (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
{option.icon != null && <span>{option.icon as React.ReactNode}</span>}
|
{'icon' in displayCategory && displayCategory.icon != null && (
|
||||||
<span>{option.label}</span>
|
<span>{displayCategory.icon as ReactNode}</span>
|
||||||
|
)}
|
||||||
|
<span>{displayCategory.label}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -68,10 +85,12 @@ const CategorySelector: React.FC<CategorySelectorProps> = ({
|
||||||
ariaLabel="Prompt's category selector"
|
ariaLabel="Prompt's category selector"
|
||||||
className={className}
|
className={className}
|
||||||
options={categories || []}
|
options={categories || []}
|
||||||
renderValue={(option) => (
|
renderValue={() => (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
{option.icon != null && <span>{option.icon as React.ReactNode}</span>}
|
{'icon' in displayCategory && displayCategory.icon != null && (
|
||||||
<span>{option.label}</span>
|
<span>{displayCategory.icon as ReactNode}</span>
|
||||||
|
)}
|
||||||
|
<span>{displayCategory.label}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -57,7 +57,7 @@ function ChatGroupItem({
|
||||||
snippet={
|
snippet={
|
||||||
typeof group.oneliner === 'string' && group.oneliner.length > 0
|
typeof group.oneliner === 'string' && group.oneliner.length > 0
|
||||||
? group.oneliner
|
? group.oneliner
|
||||||
: group.productionPrompt?.prompt ?? ''
|
: (group.productionPrompt?.prompt ?? '')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
|
@ -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"
|
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"
|
||||||
>
|
>
|
||||||
<MenuIcon className="icon-md text-text-secondary" aria-hidden="true" />
|
<MenuIcon className="icon-md text-text-secondary" aria-hidden="true" />
|
||||||
<span className="sr-only">Open actions menu for {group.name}</span>
|
<span className="sr-only">
|
||||||
|
{localize('com_ui_sr_actions_menu', { 0: group.name }) +
|
||||||
|
' ' +
|
||||||
|
localize('com_ui_prompt')}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
|
|
|
@ -7,6 +7,7 @@ import PromptVariables from '~/components/Prompts/PromptVariables';
|
||||||
import { Button, TextareaAutosize, Input } from '~/components/ui';
|
import { Button, TextareaAutosize, Input } from '~/components/ui';
|
||||||
import Description from '~/components/Prompts/Description';
|
import Description from '~/components/Prompts/Description';
|
||||||
import { useLocalize, useHasAccess } from '~/hooks';
|
import { useLocalize, useHasAccess } from '~/hooks';
|
||||||
|
import VariablesDropdown from '~/components/Prompts/VariablesDropdown';
|
||||||
import Command from '~/components/Prompts/Command';
|
import Command from '~/components/Prompts/Command';
|
||||||
import { useCreatePrompt } from '~/data-provider';
|
import { useCreatePrompt } from '~/data-provider';
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
|
@ -132,7 +133,8 @@ const CreatePromptForm = ({
|
||||||
<div className="flex w-full flex-col gap-4 md:mt-[1.075rem]">
|
<div className="flex w-full flex-col gap-4 md:mt-[1.075rem]">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="flex items-center justify-between rounded-t-lg border border-border-medium py-2 pl-4 pr-1 text-base font-semibold dark:text-gray-200">
|
<h2 className="flex items-center justify-between rounded-t-lg border border-border-medium py-2 pl-4 pr-1 text-base font-semibold dark:text-gray-200">
|
||||||
{localize('com_ui_prompt_text')}*
|
<span>{localize('com_ui_prompt_text')}*</span>
|
||||||
|
<VariablesDropdown fieldName="prompt" className="mr-2" />
|
||||||
</h2>
|
</h2>
|
||||||
<div className="min-h-32 rounded-b-lg border border-border-medium p-4 transition-all duration-150">
|
<div className="min-h-32 rounded-b-lg border border-border-medium p-4 transition-all duration-150">
|
||||||
<Controller
|
<Controller
|
||||||
|
|
|
@ -31,7 +31,7 @@ const VariableDialog: React.FC<VariableDialogProps> = ({ open, onClose, group })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OGDialog open={open} onOpenChange={handleOpenChange}>
|
<OGDialog open={open} onOpenChange={handleOpenChange}>
|
||||||
<OGDialogContent className="max-w-full bg-white dark:border-gray-700 dark:bg-gray-850 dark:text-gray-300 md:max-w-3xl">
|
<OGDialogContent className="max-h-[90vh] max-w-full overflow-y-auto bg-white dark:border-gray-700 dark:bg-gray-850 dark:text-gray-300 md:max-w-[60vw]">
|
||||||
<OGDialogTitle>{group.name}</OGDialogTitle>
|
<OGDialogTitle>{group.name}</OGDialogTitle>
|
||||||
<VariableForm group={group} onClose={onClose} />
|
<VariableForm group={group} onClose={onClose} />
|
||||||
</OGDialogContent>
|
</OGDialogContent>
|
||||||
|
|
|
@ -5,18 +5,14 @@ import supersub from 'remark-supersub';
|
||||||
import rehypeKatex from 'rehype-katex';
|
import rehypeKatex from 'rehype-katex';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import rehypeHighlight from 'rehype-highlight';
|
import rehypeHighlight from 'rehype-highlight';
|
||||||
|
import { replaceSpecialVars } from 'librechat-data-provider';
|
||||||
import { useForm, useFieldArray, Controller, useWatch } from 'react-hook-form';
|
import { useForm, useFieldArray, Controller, useWatch } from 'react-hook-form';
|
||||||
import type { TPromptGroup } from 'librechat-data-provider';
|
import type { TPromptGroup } from 'librechat-data-provider';
|
||||||
import {
|
import { cn, wrapVariable, defaultTextProps, extractVariableInfo } from '~/utils';
|
||||||
cn,
|
|
||||||
wrapVariable,
|
|
||||||
defaultTextProps,
|
|
||||||
replaceSpecialVars,
|
|
||||||
extractVariableInfo,
|
|
||||||
} from '~/utils';
|
|
||||||
import { codeNoExecution } from '~/components/Chat/Messages/Content/Markdown';
|
import { codeNoExecution } from '~/components/Chat/Messages/Content/Markdown';
|
||||||
import { TextareaAutosize, InputCombobox, Button } from '~/components/ui';
|
import { TextareaAutosize, InputCombobox, Button } from '~/components/ui';
|
||||||
import { useAuthContext, useLocalize, useSubmitMessage } from '~/hooks';
|
import { useAuthContext, useLocalize, useSubmitMessage } from '~/hooks';
|
||||||
|
import { PromptVariableGfm } from '../Markdown';
|
||||||
|
|
||||||
type FieldType = 'text' | 'select';
|
type FieldType = 'text' | 'select';
|
||||||
|
|
||||||
|
@ -115,9 +111,12 @@ export default function VariableForm({
|
||||||
allVariables.forEach((variable) => {
|
allVariables.forEach((variable) => {
|
||||||
const placeholder = `{{${variable}}}`;
|
const placeholder = `{{${variable}}}`;
|
||||||
const fieldIndex = variableIndexMap.get(variable) as string | number;
|
const fieldIndex = variableIndexMap.get(variable) as string | number;
|
||||||
const fieldValue = fieldValues[fieldIndex].value as string;
|
const fieldValue = fieldValues[fieldIndex].value as string | undefined;
|
||||||
const highlightText = fieldValue !== '' ? fieldValue : placeholder;
|
if (fieldValue === placeholder || fieldValue === '' || !fieldValue) {
|
||||||
tempText = tempText.replaceAll(placeholder, `**${highlightText}**`);
|
return;
|
||||||
|
}
|
||||||
|
const highlightText = fieldValue !== '' ? `**${fieldValue}**` : placeholder;
|
||||||
|
tempText = tempText.replaceAll(placeholder, highlightText);
|
||||||
});
|
});
|
||||||
return tempText;
|
return tempText;
|
||||||
};
|
};
|
||||||
|
@ -141,7 +140,7 @@ export default function VariableForm({
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto p-1 md:container">
|
<div className="mx-auto p-1 md:container">
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||||
<div className="mb-6 max-h-screen max-w-[90vw] overflow-auto rounded-md bg-gray-100 p-4 text-text-secondary dark:bg-gray-700/50 sm:max-w-full md:max-h-80">
|
<div className="mb-6 max-h-screen max-w-[90vw] overflow-auto rounded-md bg-surface-tertiary p-4 text-text-secondary dark:bg-surface-primary sm:max-w-full md:max-h-96">
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
/** @ts-ignore */
|
/** @ts-ignore */
|
||||||
remarkPlugins={[supersub, remarkGfm, [remarkMath, { singleDollarTextMath: true }]]}
|
remarkPlugins={[supersub, remarkGfm, [remarkMath, { singleDollarTextMath: true }]]}
|
||||||
|
@ -152,8 +151,8 @@ export default function VariableForm({
|
||||||
[rehypeHighlight, { ignoreMissing: true }],
|
[rehypeHighlight, { ignoreMissing: true }],
|
||||||
]}
|
]}
|
||||||
/** @ts-ignore */
|
/** @ts-ignore */
|
||||||
components={{ code: codeNoExecution }}
|
components={{ code: codeNoExecution, p: PromptVariableGfm }}
|
||||||
className="prose dark:prose-invert light dark:text-gray-70 my-1 max-h-[50vh] break-words"
|
className="markdown prose dark:prose-invert light my-1 max-h-[50vh] max-w-full break-words dark:text-text-secondary"
|
||||||
>
|
>
|
||||||
{generateHighlightedMarkdown()}
|
{generateHighlightedMarkdown()}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { handleDoubleClick } from '~/utils';
|
import { handleDoubleClick } from '~/utils';
|
||||||
|
|
||||||
export const CodeVariableGfm = ({ children }: { children: React.ReactNode }) => {
|
export const CodeVariableGfm: React.ElementType = ({ children }: { children: React.ReactNode }) => {
|
||||||
return (
|
return (
|
||||||
<code
|
<code
|
||||||
onDoubleClick={handleDoubleClick}
|
onDoubleClick={handleDoubleClick}
|
||||||
|
@ -29,7 +29,10 @@ export const PromptVariableGfm = ({
|
||||||
const parts = child.split(regex);
|
const parts = child.split(regex);
|
||||||
return parts.map((part, index) =>
|
return parts.map((part, index) =>
|
||||||
index % 2 === 1 ? (
|
index % 2 === 1 ? (
|
||||||
<b key={index} className="rounded-md bg-yellow-100/90 p-1 text-gray-700">
|
<b
|
||||||
|
key={index}
|
||||||
|
className="ml-[0.5] rounded-lg bg-amber-100 p-[1px] font-medium text-yellow-800 dark:border-yellow-500/50 dark:bg-transparent dark:text-yellow-500/90"
|
||||||
|
>
|
||||||
{`{{${part}}}`}
|
{`{{${part}}}`}
|
||||||
</b>
|
</b>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -13,7 +13,7 @@ const PreviewPrompt = ({
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<OGDialog open={open} onOpenChange={onOpenChange}>
|
<OGDialog open={open} onOpenChange={onOpenChange}>
|
||||||
<OGDialogContent className="w-11/12 max-w-5xl">
|
<OGDialogContent className="max-h-[90vh] w-11/12 max-w-full overflow-y-auto md:max-w-[60vw]">
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
<PromptDetails group={group} />
|
<PromptDetails group={group} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,13 +5,13 @@ import rehypeKatex from 'rehype-katex';
|
||||||
import remarkMath from 'remark-math';
|
import remarkMath from 'remark-math';
|
||||||
import supersub from 'remark-supersub';
|
import supersub from 'remark-supersub';
|
||||||
import rehypeHighlight from 'rehype-highlight';
|
import rehypeHighlight from 'rehype-highlight';
|
||||||
|
import { replaceSpecialVars } from 'librechat-data-provider';
|
||||||
import type { TPromptGroup } from 'librechat-data-provider';
|
import type { TPromptGroup } from 'librechat-data-provider';
|
||||||
import { codeNoExecution } from '~/components/Chat/Messages/Content/Markdown';
|
import { codeNoExecution } from '~/components/Chat/Messages/Content/Markdown';
|
||||||
import { useLocalize, useAuthContext } from '~/hooks';
|
import { useLocalize, useAuthContext } from '~/hooks';
|
||||||
import CategoryIcon from './Groups/CategoryIcon';
|
import CategoryIcon from './Groups/CategoryIcon';
|
||||||
import PromptVariables from './PromptVariables';
|
import PromptVariables from './PromptVariables';
|
||||||
import { PromptVariableGfm } from './Markdown';
|
import { PromptVariableGfm } from './Markdown';
|
||||||
import { replaceSpecialVars } from '~/utils';
|
|
||||||
import { Label } from '~/components/ui';
|
import { Label } from '~/components/ui';
|
||||||
import Description from './Description';
|
import Description from './Description';
|
||||||
import Command from './Command';
|
import Command from './Command';
|
||||||
|
@ -46,7 +46,7 @@ const PromptDetails = ({ group }: { group?: TPromptGroup }) => {
|
||||||
<div className="flex h-full max-h-screen flex-col overflow-y-auto md:flex-row">
|
<div className="flex h-full max-h-screen flex-col overflow-y-auto md:flex-row">
|
||||||
<div className="flex flex-1 flex-col gap-4 p-0 md:max-h-[calc(100vh-150px)] md:p-2">
|
<div className="flex flex-1 flex-col gap-4 p-0 md:max-h-[calc(100vh-150px)] md:p-2">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="flex items-center justify-between rounded-t-lg border border-border-light py-2 pl-4 text-base font-semibold text-text-primary ">
|
<h2 className="flex items-center justify-between rounded-t-lg border border-border-light py-2 pl-4 text-base font-semibold text-text-primary">
|
||||||
{localize('com_ui_prompt_text')}
|
{localize('com_ui_prompt_text')}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="group relative min-h-32 rounded-b-lg border border-border-light p-4 transition-all duration-150">
|
<div className="group relative min-h-32 rounded-b-lg border border-border-light p-4 transition-all duration-150">
|
||||||
|
@ -65,7 +65,7 @@ const PromptDetails = ({ group }: { group?: TPromptGroup }) => {
|
||||||
]}
|
]}
|
||||||
/** @ts-ignore */
|
/** @ts-ignore */
|
||||||
components={{ p: PromptVariableGfm, code: codeNoExecution }}
|
components={{ p: PromptVariableGfm, code: codeNoExecution }}
|
||||||
className="prose dark:prose-invert light dark:text-gray-70 my-1"
|
className="markdown prose dark:prose-invert light dark:text-gray-70 my-1 break-words"
|
||||||
>
|
>
|
||||||
{mainText}
|
{mainText}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
|
|
|
@ -12,6 +12,7 @@ import ReactMarkdown from 'react-markdown';
|
||||||
import { codeNoExecution } from '~/components/Chat/Messages/Content/Markdown';
|
import { codeNoExecution } from '~/components/Chat/Messages/Content/Markdown';
|
||||||
import AlwaysMakeProd from '~/components/Prompts/Groups/AlwaysMakeProd';
|
import AlwaysMakeProd from '~/components/Prompts/Groups/AlwaysMakeProd';
|
||||||
import { SaveIcon, CrossIcon } from '~/components/svg';
|
import { SaveIcon, CrossIcon } from '~/components/svg';
|
||||||
|
import VariablesDropdown from './VariablesDropdown';
|
||||||
import { TextareaAutosize } from '~/components/ui';
|
import { TextareaAutosize } from '~/components/ui';
|
||||||
import { PromptVariableGfm } from './Markdown';
|
import { PromptVariableGfm } from './Markdown';
|
||||||
import { PromptsEditorMode } from '~/common';
|
import { PromptsEditorMode } from '~/common';
|
||||||
|
@ -59,10 +60,11 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
|
||||||
<span className="max-w-[200px] truncate sm:max-w-none">
|
<span className="max-w-[200px] truncate sm:max-w-none">
|
||||||
{localize('com_ui_prompt_text')}
|
{localize('com_ui_prompt_text')}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex flex-shrink-0 flex-row gap-3 sm:gap-6">
|
<div className="flex flex-shrink-0 flex-row items-center gap-3 sm:gap-6">
|
||||||
{editorMode === PromptsEditorMode.ADVANCED && (
|
{editorMode === PromptsEditorMode.ADVANCED && (
|
||||||
<AlwaysMakeProd className="hidden sm:flex" />
|
<AlwaysMakeProd className="hidden sm:flex" />
|
||||||
)}
|
)}
|
||||||
|
<VariablesDropdown fieldName={name} />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsEditing((prev) => !prev)}
|
onClick={() => setIsEditing((prev) => !prev)}
|
||||||
|
@ -105,6 +107,7 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
|
||||||
isEditing ? (
|
isEditing ? (
|
||||||
<TextareaAutosize
|
<TextareaAutosize
|
||||||
{...field}
|
{...field}
|
||||||
|
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||||
autoFocus
|
autoFocus
|
||||||
className="w-full resize-none overflow-y-auto rounded bg-transparent text-sm text-text-primary focus:outline-none sm:text-base"
|
className="w-full resize-none overflow-y-auto rounded bg-transparent text-sm text-text-primary focus:outline-none sm:text-base"
|
||||||
minRows={3}
|
minRows={3}
|
||||||
|
@ -123,8 +126,8 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
|
||||||
style={{ minHeight: '4.5em', maxHeight: '21em', overflow: 'auto' }}
|
style={{ minHeight: '4.5em', maxHeight: '21em', overflow: 'auto' }}
|
||||||
>
|
>
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
/** @ts-ignore */
|
|
||||||
remarkPlugins={[
|
remarkPlugins={[
|
||||||
|
/** @ts-ignore */
|
||||||
supersub,
|
supersub,
|
||||||
remarkGfm,
|
remarkGfm,
|
||||||
[remarkMath, { singleDollarTextMath: true }],
|
[remarkMath, { singleDollarTextMath: true }],
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { Variable } from 'lucide-react';
|
import { Variable } from 'lucide-react';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
import { specialVariables } from 'librechat-data-provider';
|
||||||
import { cn, extractUniqueVariables } from '~/utils';
|
import { cn, extractUniqueVariables } from '~/utils';
|
||||||
import { CodeVariableGfm } from './Markdown';
|
import { CodeVariableGfm } from './Markdown';
|
||||||
import { Separator } from '~/components/ui';
|
import { Separator } from '~/components/ui';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
const specialVariables = {
|
|
||||||
current_date: true,
|
|
||||||
current_user: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const specialVariableClasses =
|
const specialVariableClasses =
|
||||||
'bg-yellow-500/25 text-yellow-600 dark:border-yellow-500/50 dark:bg-transparent dark:text-yellow-500/90';
|
'bg-amber-100 text-yellow-800 border-yellow-600 dark:border-yellow-500/50 dark:bg-transparent dark:text-yellow-500/90';
|
||||||
|
|
||||||
|
const components: {
|
||||||
|
[nodeType: string]: React.ElementType;
|
||||||
|
} = { code: CodeVariableGfm };
|
||||||
|
|
||||||
const PromptVariables = ({
|
const PromptVariables = ({
|
||||||
promptText,
|
promptText,
|
||||||
|
@ -52,7 +52,7 @@ const PromptVariables = ({
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-sm text-text-secondary">
|
<div className="text-sm text-text-secondary">
|
||||||
<ReactMarkdown components={{ code: CodeVariableGfm }}>
|
<ReactMarkdown components={components} className="markdown prose dark:prose-invert">
|
||||||
{localize('com_ui_variables_info')}
|
{localize('com_ui_variables_info')}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
|
@ -65,8 +65,8 @@ const PromptVariables = ({
|
||||||
{localize('com_ui_special_variables')}
|
{localize('com_ui_special_variables')}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-text-secondary">
|
<span className="text-sm text-text-secondary">
|
||||||
<ReactMarkdown components={{ code: CodeVariableGfm }}>
|
<ReactMarkdown components={components} className="markdown prose dark:prose-invert">
|
||||||
{localize('com_ui_special_variables_info')}
|
{localize('com_ui_special_variables_more_info')}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -75,7 +75,7 @@ const PromptVariables = ({
|
||||||
{localize('com_ui_dropdown_variables')}
|
{localize('com_ui_dropdown_variables')}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-text-secondary">
|
<span className="text-sm text-text-secondary">
|
||||||
<ReactMarkdown components={{ code: CodeVariableGfm }}>
|
<ReactMarkdown components={components} className="markdown prose dark:prose-invert">
|
||||||
{localize('com_ui_dropdown_variables_info')}
|
{localize('com_ui_dropdown_variables_info')}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
</span>
|
</span>
|
||||||
|
|
75
client/src/components/Prompts/VariablesDropdown.tsx
Normal file
75
client/src/components/Prompts/VariablesDropdown.tsx
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import { useState, useId } from 'react';
|
||||||
|
import { PlusCircle } from 'lucide-react';
|
||||||
|
import * as Menu from '@ariakit/react/menu';
|
||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
import { specialVariables } from 'librechat-data-provider';
|
||||||
|
import type { TSpecialVarLabel } from 'librechat-data-provider';
|
||||||
|
import { DropdownPopup } from '~/components';
|
||||||
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
|
interface VariableOption {
|
||||||
|
label: TSpecialVarLabel;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const variableOptions: VariableOption[] = Object.keys(specialVariables).map((key) => ({
|
||||||
|
label: `com_ui_special_var_${key}` as TSpecialVarLabel,
|
||||||
|
value: `{{${key}}}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface VariablesDropdownProps {
|
||||||
|
fieldName?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function VariablesDropdown({
|
||||||
|
fieldName = 'prompt',
|
||||||
|
className = '',
|
||||||
|
}: VariablesDropdownProps) {
|
||||||
|
const menuId = useId();
|
||||||
|
const localize = useLocalize();
|
||||||
|
const methods = useFormContext();
|
||||||
|
const { setValue, getValues } = methods;
|
||||||
|
|
||||||
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleAddVariable = (label: TSpecialVarLabel, value: string) => {
|
||||||
|
const currentText = getValues(fieldName) || '';
|
||||||
|
const spacer = currentText.length > 0 ? '\n\n' : '';
|
||||||
|
const prefix = localize(label);
|
||||||
|
setValue(fieldName, currentText + spacer + prefix + ': ' + value);
|
||||||
|
setIsMenuOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={className}
|
||||||
|
title={`${localize('com_ui_add')} ${localize('com_ui_special_variables')}`}
|
||||||
|
>
|
||||||
|
<DropdownPopup
|
||||||
|
portal={true}
|
||||||
|
mountByState={true}
|
||||||
|
unmountOnHide={true}
|
||||||
|
preserveTabOrder={true}
|
||||||
|
isOpen={isMenuOpen}
|
||||||
|
setIsOpen={setIsMenuOpen}
|
||||||
|
trigger={
|
||||||
|
<Menu.MenuButton
|
||||||
|
id="variables-menu-button"
|
||||||
|
aria-label={`${localize('com_ui_add')} ${localize('com_ui_special_variables')}`}
|
||||||
|
className="flex h-8 items-center gap-1 rounded-md border border-border-medium bg-surface-secondary px-2 py-0 text-sm text-text-primary transition-colors duration-200 hover:bg-surface-tertiary"
|
||||||
|
>
|
||||||
|
<PlusCircle className="mr-1 h-3 w-3 text-text-secondary" aria-hidden={true} />
|
||||||
|
{localize('com_ui_special_variables')}
|
||||||
|
</Menu.MenuButton>
|
||||||
|
}
|
||||||
|
items={variableOptions.map((option) => ({
|
||||||
|
label: localize(option.label) || option.label,
|
||||||
|
onClick: () => handleAddVariable(option.label, option.value),
|
||||||
|
}))}
|
||||||
|
menuId={menuId}
|
||||||
|
className="z-30"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import Action from '~/components/SidePanel/Builder/Action';
|
||||||
import { ToolSelectDialog } from '~/components/Tools';
|
import { ToolSelectDialog } from '~/components/Tools';
|
||||||
import { icons } from '~/hooks/Endpoint/Icons';
|
import { icons } from '~/hooks/Endpoint/Icons';
|
||||||
import { processAgentOption } from '~/utils';
|
import { processAgentOption } from '~/utils';
|
||||||
|
import Instructions from './Instructions';
|
||||||
import AgentAvatar from './AgentAvatar';
|
import AgentAvatar from './AgentAvatar';
|
||||||
import FileContext from './FileContext';
|
import FileContext from './FileContext';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
|
@ -228,39 +229,7 @@ export default function AgentConfig({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* Instructions */}
|
{/* Instructions */}
|
||||||
<div className="mb-4">
|
<Instructions />
|
||||||
<label className={labelClass} htmlFor="instructions">
|
|
||||||
{localize('com_ui_instructions')}
|
|
||||||
</label>
|
|
||||||
<Controller
|
|
||||||
name="instructions"
|
|
||||||
control={control}
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<>
|
|
||||||
<textarea
|
|
||||||
{...field}
|
|
||||||
value={field.value ?? ''}
|
|
||||||
// maxLength={32768}
|
|
||||||
className={cn(inputClass, 'min-h-[100px] resize-y')}
|
|
||||||
id="instructions"
|
|
||||||
placeholder={localize('com_agents_instructions_placeholder')}
|
|
||||||
rows={3}
|
|
||||||
aria-label="Agent instructions"
|
|
||||||
aria-required="true"
|
|
||||||
aria-invalid={error ? 'true' : 'false'}
|
|
||||||
/>
|
|
||||||
{error && (
|
|
||||||
<span
|
|
||||||
className="text-sm text-red-500 transition duration-300 ease-in-out"
|
|
||||||
role="alert"
|
|
||||||
>
|
|
||||||
{localize('com_ui_field_required')}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/* Model and Provider */}
|
{/* Model and Provider */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label className={labelClass} htmlFor="provider">
|
<label className={labelClass} htmlFor="provider">
|
||||||
|
|
127
client/src/components/SidePanel/Agents/Instructions.tsx
Normal file
127
client/src/components/SidePanel/Agents/Instructions.tsx
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
import React, { useState, useId } from 'react';
|
||||||
|
import { PlusCircle } from 'lucide-react';
|
||||||
|
import * as Menu from '@ariakit/react/menu';
|
||||||
|
import { specialVariables } from 'librechat-data-provider';
|
||||||
|
import type { TSpecialVarLabel } from 'librechat-data-provider';
|
||||||
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
import type { AgentForm } from '~/common';
|
||||||
|
import { cn, defaultTextProps, removeFocusOutlines } from '~/utils';
|
||||||
|
// import ControlCombobox from '~/components/ui/ControlCombobox';
|
||||||
|
import { DropdownPopup } from '~/components';
|
||||||
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
|
const inputClass = cn(
|
||||||
|
defaultTextProps,
|
||||||
|
'flex w-full px-3 py-2 border-border-light bg-surface-secondary focus-visible:ring-2 focus-visible:ring-ring-primary',
|
||||||
|
removeFocusOutlines,
|
||||||
|
);
|
||||||
|
|
||||||
|
interface VariableOption {
|
||||||
|
label: TSpecialVarLabel;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const variableOptions: VariableOption[] = Object.keys(specialVariables).map((key) => ({
|
||||||
|
label: `com_ui_special_var_${key}` as TSpecialVarLabel,
|
||||||
|
value: `{{${key}}}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function Instructions() {
|
||||||
|
const menuId = useId();
|
||||||
|
const localize = useLocalize();
|
||||||
|
const methods = useFormContext<AgentForm>();
|
||||||
|
const { control, setValue, getValues } = methods;
|
||||||
|
|
||||||
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleAddVariable = (label: TSpecialVarLabel, value: string) => {
|
||||||
|
const currentInstructions = getValues('instructions') || '';
|
||||||
|
const spacer = currentInstructions.length > 0 ? '\n' : '';
|
||||||
|
const prefix = localize(label);
|
||||||
|
setValue('instructions', currentInstructions + spacer + prefix + ': ' + value);
|
||||||
|
setIsMenuOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="mb-2 flex items-center">
|
||||||
|
<label className="text-token-text-primary flex-grow font-medium" htmlFor="instructions">
|
||||||
|
{localize('com_ui_instructions')}
|
||||||
|
</label>
|
||||||
|
<div className="ml-auto" title="Add variables to instructions">
|
||||||
|
{/* ControlCombobox implementation
|
||||||
|
<ControlCombobox
|
||||||
|
selectedValue=""
|
||||||
|
displayValue="Add variables"
|
||||||
|
items={variableOptions.map((option) => ({
|
||||||
|
label: option.label,
|
||||||
|
value: option.value,
|
||||||
|
}))}
|
||||||
|
setValue={handleAddVariable}
|
||||||
|
ariaLabel="Add variable to instructions"
|
||||||
|
searchPlaceholder="Search variables"
|
||||||
|
selectPlaceholder="Add"
|
||||||
|
isCollapsed={false}
|
||||||
|
SelectIcon={<PlusCircle className="h-3 w-3 text-text-secondary" />}
|
||||||
|
containerClassName="w-fit"
|
||||||
|
className="h-7 gap-1 rounded-md border border-border-medium bg-surface-secondary px-2 py-0 text-sm text-text-primary transition-colors duration-200 hover:bg-surface-tertiary"
|
||||||
|
iconSide="left"
|
||||||
|
showCarat={false}
|
||||||
|
/>
|
||||||
|
*/}
|
||||||
|
<DropdownPopup
|
||||||
|
portal={true}
|
||||||
|
mountByState={true}
|
||||||
|
unmountOnHide={true}
|
||||||
|
preserveTabOrder={true}
|
||||||
|
isOpen={isMenuOpen}
|
||||||
|
setIsOpen={setIsMenuOpen}
|
||||||
|
trigger={
|
||||||
|
<Menu.MenuButton
|
||||||
|
id="variables-menu-button"
|
||||||
|
aria-label="Add variable to instructions"
|
||||||
|
className="flex h-7 items-center gap-1 rounded-md border border-border-medium bg-surface-secondary px-2 py-0 text-sm text-text-primary transition-colors duration-200 hover:bg-surface-tertiary"
|
||||||
|
>
|
||||||
|
<PlusCircle className="mr-1 h-3 w-3 text-text-secondary" aria-hidden={true} />
|
||||||
|
{localize('com_ui_variables')}
|
||||||
|
</Menu.MenuButton>
|
||||||
|
}
|
||||||
|
items={variableOptions.map((option) => ({
|
||||||
|
label: localize(option.label) || option.label,
|
||||||
|
onClick: () => handleAddVariable(option.label, option.value),
|
||||||
|
}))}
|
||||||
|
menuId={menuId}
|
||||||
|
className="z-30"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Controller
|
||||||
|
name="instructions"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<>
|
||||||
|
<textarea
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ''}
|
||||||
|
className={cn(inputClass, 'min-h-[100px] resize-y')}
|
||||||
|
id="instructions"
|
||||||
|
placeholder={localize('com_agents_instructions_placeholder')}
|
||||||
|
rows={3}
|
||||||
|
aria-label="Agent instructions"
|
||||||
|
aria-required="true"
|
||||||
|
aria-invalid={error ? 'true' : 'false'}
|
||||||
|
/>
|
||||||
|
{error && (
|
||||||
|
<span
|
||||||
|
className="text-sm text-red-500 transition duration-300 ease-in-out"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
{localize('com_ui_field_required')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -8,7 +8,7 @@ interface OGDialogProps extends DialogPrimitive.DialogProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Dialog = React.forwardRef<HTMLDivElement, OGDialogProps>(
|
const Dialog = React.forwardRef<HTMLDivElement, OGDialogProps>(
|
||||||
({ children, triggerRef, onOpenChange, ...props }) => {
|
({ children, triggerRef, onOpenChange, ...props }, _ref) => {
|
||||||
const handleOpenChange = (open: boolean) => {
|
const handleOpenChange = (open: boolean) => {
|
||||||
if (!open && triggerRef?.current) {
|
if (!open && triggerRef?.current) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -71,6 +71,7 @@ const DialogContent = React.forwardRef<
|
||||||
{showCloseButton && (
|
{showCloseButton && (
|
||||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-ring-primary ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-ring-primary ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
|
{/* eslint-disable-next-line i18next/no-literal-string */}
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
// export * from './queries';
|
export * from './queries';
|
||||||
export * from './mutations';
|
export * from './mutations';
|
||||||
|
|
42
client/src/data-provider/Messages/queries.ts
Normal file
42
client/src/data-provider/Messages/queries.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import type { UseQueryOptions, QueryObserverResult } from '@tanstack/react-query';
|
||||||
|
import { QueryKeys, dataService } from 'librechat-data-provider';
|
||||||
|
import type * as t from 'librechat-data-provider';
|
||||||
|
import { logger } from '~/utils';
|
||||||
|
|
||||||
|
export const useGetMessagesByConvoId = <TData = t.TMessage[]>(
|
||||||
|
id: string,
|
||||||
|
config?: UseQueryOptions<t.TMessage[], unknown, TData>,
|
||||||
|
): QueryObserverResult<TData> => {
|
||||||
|
const location = useLocation();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useQuery<t.TMessage[], unknown, TData>(
|
||||||
|
[QueryKeys.messages, id],
|
||||||
|
async () => {
|
||||||
|
const result = await dataService.getMessagesByConvoId(id);
|
||||||
|
if (!location.pathname.includes('/c/new') && result?.length === 1) {
|
||||||
|
const currentMessages = queryClient.getQueryData<t.TMessage[]>([QueryKeys.messages, id]);
|
||||||
|
if (currentMessages?.length === 1) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (currentMessages && currentMessages?.length > 1) {
|
||||||
|
logger.warn(
|
||||||
|
'messages',
|
||||||
|
`Messages query for convo ${id} returned fewer than cache; path: "${location.pathname}"`,
|
||||||
|
result,
|
||||||
|
currentMessages,
|
||||||
|
);
|
||||||
|
return currentMessages;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
|
refetchOnMount: false,
|
||||||
|
...config,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
|
@ -6,6 +6,7 @@ import {
|
||||||
ContentTypes,
|
ContentTypes,
|
||||||
EModelEndpoint,
|
EModelEndpoint,
|
||||||
parseCompactConvo,
|
parseCompactConvo,
|
||||||
|
replaceSpecialVars,
|
||||||
isAssistantsEndpoint,
|
isAssistantsEndpoint,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import { useSetRecoilState, useResetRecoilState, useRecoilValue } from 'recoil';
|
import { useSetRecoilState, useResetRecoilState, useRecoilValue } from 'recoil';
|
||||||
|
@ -26,6 +27,7 @@ import { getArtifactsMode } from '~/utils/artifacts';
|
||||||
import { getEndpointField, logger } from '~/utils';
|
import { getEndpointField, logger } from '~/utils';
|
||||||
import useUserKey from '~/hooks/Input/useUserKey';
|
import useUserKey from '~/hooks/Input/useUserKey';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useAuthContext } from '~/hooks';
|
||||||
|
|
||||||
const logChatRequest = (request: Record<string, unknown>) => {
|
const logChatRequest = (request: Record<string, unknown>) => {
|
||||||
logger.log('=====================================\nAsk function called with:');
|
logger.log('=====================================\nAsk function called with:');
|
||||||
|
@ -66,19 +68,19 @@ export default function useChatFunctions({
|
||||||
setSubmission: SetterOrUpdater<TSubmission | null>;
|
setSubmission: SetterOrUpdater<TSubmission | null>;
|
||||||
setLatestMessage?: SetterOrUpdater<TMessage | null>;
|
setLatestMessage?: SetterOrUpdater<TMessage | null>;
|
||||||
}) {
|
}) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const getSender = useGetSender();
|
||||||
|
const { user } = useAuthContext();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const setFilesToDelete = useSetFilesToDelete();
|
||||||
const getEphemeralAgent = useGetEphemeralAgent();
|
const getEphemeralAgent = useGetEphemeralAgent();
|
||||||
|
const isTemporary = useRecoilValue(store.isTemporary);
|
||||||
const codeArtifacts = useRecoilValue(store.codeArtifacts);
|
const codeArtifacts = useRecoilValue(store.codeArtifacts);
|
||||||
const includeShadcnui = useRecoilValue(store.includeShadcnui);
|
const includeShadcnui = useRecoilValue(store.includeShadcnui);
|
||||||
const customPromptMode = useRecoilValue(store.customPromptMode);
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const resetLatestMultiMessage = useResetRecoilState(store.latestMessageFamily(index + 1));
|
|
||||||
const setShowStopButton = useSetRecoilState(store.showStopButtonByIndex(index));
|
|
||||||
const setFilesToDelete = useSetFilesToDelete();
|
|
||||||
const getSender = useGetSender();
|
|
||||||
const isTemporary = useRecoilValue(store.isTemporary);
|
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const { getExpiry } = useUserKey(conversation?.endpoint ?? '');
|
const { getExpiry } = useUserKey(conversation?.endpoint ?? '');
|
||||||
|
const customPromptMode = useRecoilValue(store.customPromptMode);
|
||||||
|
const setShowStopButton = useSetRecoilState(store.showStopButtonByIndex(index));
|
||||||
|
const resetLatestMultiMessage = useResetRecoilState(store.latestMessageFamily(index + 1));
|
||||||
|
|
||||||
const ask: TAskFunction = (
|
const ask: TAskFunction = (
|
||||||
{
|
{
|
||||||
|
@ -128,6 +130,13 @@ export default function useChatFunctions({
|
||||||
|
|
||||||
let currentMessages: TMessage[] | null = overrideMessages ?? getMessages() ?? [];
|
let currentMessages: TMessage[] | null = overrideMessages ?? getMessages() ?? [];
|
||||||
|
|
||||||
|
if (conversation?.promptPrefix) {
|
||||||
|
conversation.promptPrefix = replaceSpecialVars({
|
||||||
|
text: conversation.promptPrefix,
|
||||||
|
user,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// construct the query message
|
// construct the query message
|
||||||
// this is not a real messageId, it is used as placeholder before real messageId returned
|
// this is not a real messageId, it is used as placeholder before real messageId returned
|
||||||
text = text.trim();
|
text = text.trim();
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
|
||||||
import { QueryKeys } from 'librechat-data-provider';
|
import { QueryKeys } from 'librechat-data-provider';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { useRecoilState, useResetRecoilState, useSetRecoilState } from 'recoil';
|
import { useRecoilState, useResetRecoilState, useSetRecoilState } from 'recoil';
|
||||||
import { useGetMessagesByConvoId } from 'librechat-data-provider/react-query';
|
|
||||||
import type { TMessage } from 'librechat-data-provider';
|
import type { TMessage } from 'librechat-data-provider';
|
||||||
import useChatFunctions from '~/hooks/Chat/useChatFunctions';
|
import useChatFunctions from '~/hooks/Chat/useChatFunctions';
|
||||||
|
import { useGetMessagesByConvoId } from '~/data-provider';
|
||||||
import { useAuthContext } from '~/hooks/AuthContext';
|
import { useAuthContext } from '~/hooks/AuthContext';
|
||||||
import useNewConvo from '~/hooks/useNewConvo';
|
import useNewConvo from '~/hooks/useNewConvo';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
|
@ -96,13 +96,14 @@ export default function useTextarea({
|
||||||
return localize('com_endpoint_message_not_appendable');
|
return localize('com_endpoint_message_not_appendable');
|
||||||
}
|
}
|
||||||
|
|
||||||
const sender = isAssistant || isAgent
|
const sender =
|
||||||
? getEntityName({ name: entityName, isAgent, localize })
|
isAssistant || isAgent
|
||||||
: getSender(conversation as TEndpointOption);
|
? getEntityName({ name: entityName, isAgent, localize })
|
||||||
|
: getSender(conversation as TEndpointOption);
|
||||||
|
|
||||||
return `${localize(
|
return `${localize('com_endpoint_message_new', {
|
||||||
'com_endpoint_message_new', { 0: sender ? sender : localize('com_endpoint_ai') },
|
0: sender ? sender : localize('com_endpoint_ai'),
|
||||||
)}`;
|
})}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const placeholder = getPlaceholderText();
|
const placeholder = getPlaceholderText();
|
||||||
|
@ -237,7 +238,8 @@ export default function useTextarea({
|
||||||
textAreaRef,
|
textAreaRef,
|
||||||
handlePaste,
|
handlePaste,
|
||||||
handleKeyDown,
|
handleKeyDown,
|
||||||
handleCompositionStart,
|
isNotAppendable,
|
||||||
handleCompositionEnd,
|
handleCompositionEnd,
|
||||||
|
handleCompositionStart,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { Constants } from 'librechat-data-provider';
|
|
||||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
|
import { Constants, replaceSpecialVars } from 'librechat-data-provider';
|
||||||
import { useChatContext, useChatFormContext, useAddedChatContext } from '~/Providers';
|
import { useChatContext, useChatFormContext, useAddedChatContext } from '~/Providers';
|
||||||
import { useAuthContext } from '~/hooks/AuthContext';
|
import { useAuthContext } from '~/hooks/AuthContext';
|
||||||
import { replaceSpecialVars } from '~/utils';
|
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
const appendIndex = (index: number, value?: string) => {
|
const appendIndex = (index: number, value?: string) => {
|
||||||
|
|
|
@ -20,12 +20,13 @@ import type { TGenTitleMutation } from '~/data-provider';
|
||||||
import type { SetterOrUpdater, Resetter } from 'recoil';
|
import type { SetterOrUpdater, Resetter } from 'recoil';
|
||||||
import type { ConversationCursorData } from '~/utils';
|
import type { ConversationCursorData } from '~/utils';
|
||||||
import {
|
import {
|
||||||
|
logger,
|
||||||
scrollToEnd,
|
scrollToEnd,
|
||||||
|
getAllContentText,
|
||||||
addConvoToAllQueries,
|
addConvoToAllQueries,
|
||||||
updateConvoInAllQueries,
|
updateConvoInAllQueries,
|
||||||
removeConvoFromAllQueries,
|
removeConvoFromAllQueries,
|
||||||
findConversationInInfinite,
|
findConversationInInfinite,
|
||||||
getAllContentText,
|
|
||||||
} from '~/utils';
|
} from '~/utils';
|
||||||
import useAttachmentHandler from '~/hooks/SSE/useAttachmentHandler';
|
import useAttachmentHandler from '~/hooks/SSE/useAttachmentHandler';
|
||||||
import useContentHandler from '~/hooks/SSE/useContentHandler';
|
import useContentHandler from '~/hooks/SSE/useContentHandler';
|
||||||
|
@ -487,10 +488,6 @@ export default function useEventHandlers({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (setConversation && isAddedRequest !== true) {
|
if (setConversation && isAddedRequest !== true) {
|
||||||
if (location.pathname === '/c/new') {
|
|
||||||
navigate(`/c/${conversation.conversationId}`, { replace: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
setConversation((prevState) => {
|
setConversation((prevState) => {
|
||||||
const update = {
|
const update = {
|
||||||
...prevState,
|
...prevState,
|
||||||
|
@ -508,6 +505,9 @@ export default function useEventHandlers({
|
||||||
}
|
}
|
||||||
return update;
|
return update;
|
||||||
});
|
});
|
||||||
|
if (location.pathname === '/c/new') {
|
||||||
|
navigate(`/c/${conversation.conversationId}`, { replace: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
|
@ -536,6 +536,12 @@ export default function useEventHandlers({
|
||||||
const conversationId =
|
const conversationId =
|
||||||
userMessage.conversationId ?? submission.conversation?.conversationId ?? '';
|
userMessage.conversationId ?? submission.conversation?.conversationId ?? '';
|
||||||
|
|
||||||
|
const setErrorMessages = (convoId: string, errorMessage: TMessage) => {
|
||||||
|
const finalMessages: TMessage[] = [...messages, userMessage, errorMessage];
|
||||||
|
setMessages(finalMessages);
|
||||||
|
queryClient.setQueryData<TMessage[]>([QueryKeys.messages, convoId], finalMessages);
|
||||||
|
};
|
||||||
|
|
||||||
const parseErrorResponse = (data: TResData | Partial<TMessage>) => {
|
const parseErrorResponse = (data: TResData | Partial<TMessage>) => {
|
||||||
const metadata = data['responseMessage'] ?? data;
|
const metadata = data['responseMessage'] ?? data;
|
||||||
const errorMessage: Partial<TMessage> = {
|
const errorMessage: Partial<TMessage> = {
|
||||||
|
@ -553,7 +559,7 @@ export default function useEventHandlers({
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
const convoId = conversationId || v4();
|
const convoId = conversationId || `_${v4()}`;
|
||||||
const errorMetadata = parseErrorResponse({
|
const errorMetadata = parseErrorResponse({
|
||||||
text: 'Error connecting to server, try refreshing the page.',
|
text: 'Error connecting to server, try refreshing the page.',
|
||||||
...submission,
|
...submission,
|
||||||
|
@ -564,7 +570,7 @@ export default function useEventHandlers({
|
||||||
getMessages,
|
getMessages,
|
||||||
submission,
|
submission,
|
||||||
});
|
});
|
||||||
setMessages([...messages, userMessage, errorResponse]);
|
setErrorMessages(convoId, errorResponse);
|
||||||
if (newConversation) {
|
if (newConversation) {
|
||||||
newConversation({
|
newConversation({
|
||||||
template: { conversationId: convoId },
|
template: { conversationId: convoId },
|
||||||
|
@ -577,9 +583,9 @@ export default function useEventHandlers({
|
||||||
|
|
||||||
const receivedConvoId = data.conversationId ?? '';
|
const receivedConvoId = data.conversationId ?? '';
|
||||||
if (!conversationId && !receivedConvoId) {
|
if (!conversationId && !receivedConvoId) {
|
||||||
const convoId = v4();
|
const convoId = `_${v4()}`;
|
||||||
const errorResponse = parseErrorResponse(data);
|
const errorResponse = parseErrorResponse(data);
|
||||||
setMessages([...messages, userMessage, errorResponse]);
|
setErrorMessages(convoId, errorResponse);
|
||||||
if (newConversation) {
|
if (newConversation) {
|
||||||
newConversation({
|
newConversation({
|
||||||
template: { conversationId: convoId },
|
template: { conversationId: convoId },
|
||||||
|
@ -590,7 +596,7 @@ export default function useEventHandlers({
|
||||||
return;
|
return;
|
||||||
} else if (!receivedConvoId) {
|
} else if (!receivedConvoId) {
|
||||||
const errorResponse = parseErrorResponse(data);
|
const errorResponse = parseErrorResponse(data);
|
||||||
setMessages([...messages, userMessage, errorResponse]);
|
setErrorMessages(conversationId, errorResponse);
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -601,7 +607,7 @@ export default function useEventHandlers({
|
||||||
parentMessageId: userMessage.messageId,
|
parentMessageId: userMessage.messageId,
|
||||||
});
|
});
|
||||||
|
|
||||||
setMessages([...messages, userMessage, errorResponse]);
|
setErrorMessages(receivedConvoId, errorResponse);
|
||||||
if (receivedConvoId && paramId === Constants.NEW_CONVO && newConversation) {
|
if (receivedConvoId && paramId === Constants.NEW_CONVO && newConversation) {
|
||||||
newConversation({
|
newConversation({
|
||||||
template: { conversationId: receivedConvoId },
|
template: { conversationId: receivedConvoId },
|
||||||
|
@ -612,7 +618,15 @@ export default function useEventHandlers({
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
[setCompleted, setMessages, paramId, newConversation, setIsSubmitting, getMessages],
|
[
|
||||||
|
setCompleted,
|
||||||
|
setMessages,
|
||||||
|
paramId,
|
||||||
|
newConversation,
|
||||||
|
setIsSubmitting,
|
||||||
|
getMessages,
|
||||||
|
queryClient,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const abortConversation = useCallback(
|
const abortConversation = useCallback(
|
||||||
|
@ -649,9 +663,11 @@ export default function useEventHandlers({
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
} else if (!isAssistantsEndpoint(endpoint)) {
|
} else if (!isAssistantsEndpoint(endpoint)) {
|
||||||
|
const convoId = conversationId || `_${v4()}`;
|
||||||
|
logger.log('conversation', 'Aborted conversation with minimal messages, ID: ' + convoId);
|
||||||
if (newConversation) {
|
if (newConversation) {
|
||||||
newConversation({
|
newConversation({
|
||||||
template: { conversationId: conversationId || v4() },
|
template: { conversationId: convoId },
|
||||||
preset: tPresetSchema.parse(submission.conversation),
|
preset: tPresetSchema.parse(submission.conversation),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -151,12 +151,28 @@ const useNewConvo = (index = 0) => {
|
||||||
if (!(keepAddedConvos ?? false)) {
|
if (!(keepAddedConvos ?? false)) {
|
||||||
clearAllConversations(true);
|
clearAllConversations(true);
|
||||||
}
|
}
|
||||||
logger.log('conversation', 'Setting conversation from `useNewConvo`', conversation);
|
const isCancelled = conversation.conversationId?.startsWith('_');
|
||||||
setConversation(conversation);
|
if (isCancelled) {
|
||||||
|
logger.log(
|
||||||
|
'conversation',
|
||||||
|
'Cancelled conversation, setting to `new` in `useNewConvo`',
|
||||||
|
conversation,
|
||||||
|
);
|
||||||
|
setConversation({
|
||||||
|
...conversation,
|
||||||
|
conversationId: 'new',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.log('conversation', 'Setting conversation from `useNewConvo`', conversation);
|
||||||
|
setConversation(conversation);
|
||||||
|
}
|
||||||
setSubmission({} as TSubmission);
|
setSubmission({} as TSubmission);
|
||||||
if (!(keepLatestMessage ?? false)) {
|
if (!(keepLatestMessage ?? false)) {
|
||||||
clearAllLatestMessages();
|
clearAllLatestMessages();
|
||||||
}
|
}
|
||||||
|
if (isCancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const searchParamsString = searchParams?.toString();
|
const searchParamsString = searchParams?.toString();
|
||||||
const getParams = () => (searchParamsString ? `?${searchParamsString}` : '');
|
const getParams = () => (searchParamsString ? `?${searchParamsString}` : '');
|
||||||
|
|
|
@ -542,6 +542,7 @@
|
||||||
"com_ui_bulk_delete_error": "Failed to delete shared links",
|
"com_ui_bulk_delete_error": "Failed to delete shared links",
|
||||||
"com_ui_callback_url": "Callback URL",
|
"com_ui_callback_url": "Callback URL",
|
||||||
"com_ui_cancel": "Cancel",
|
"com_ui_cancel": "Cancel",
|
||||||
|
"com_ui_category": "Category",
|
||||||
"com_ui_chat": "Chat",
|
"com_ui_chat": "Chat",
|
||||||
"com_ui_chat_history": "Chat History",
|
"com_ui_chat_history": "Chat History",
|
||||||
"com_ui_clear": "Clear",
|
"com_ui_clear": "Clear",
|
||||||
|
@ -616,7 +617,7 @@
|
||||||
"com_ui_download_backup_tooltip": "Before you continue, download your backup codes. You will need them to regain access if you lose your authenticator device",
|
"com_ui_download_backup_tooltip": "Before you continue, download your backup codes. You will need them to regain access if you lose your authenticator device",
|
||||||
"com_ui_download_error": "Error downloading file. The file may have been deleted.",
|
"com_ui_download_error": "Error downloading file. The file may have been deleted.",
|
||||||
"com_ui_drag_drop": "something needs to go here. was empty",
|
"com_ui_drag_drop": "something needs to go here. was empty",
|
||||||
"com_ui_dropdown_variables": "Dropdown variables:",
|
"com_ui_dropdown_variables": "Dropdown variables",
|
||||||
"com_ui_dropdown_variables_info": "Create custom dropdown menus for your prompts: `{{variable_name:option1|option2|option3}}`",
|
"com_ui_dropdown_variables_info": "Create custom dropdown menus for your prompts: `{{variable_name:option1|option2|option3}}`",
|
||||||
"com_ui_duplicate": "Duplicate",
|
"com_ui_duplicate": "Duplicate",
|
||||||
"com_ui_duplication_error": "There was an error duplicating the conversation",
|
"com_ui_duplication_error": "There was an error duplicating the conversation",
|
||||||
|
@ -806,9 +807,14 @@
|
||||||
"com_ui_sign_in_to_domain": "Sign-in to {{0}}",
|
"com_ui_sign_in_to_domain": "Sign-in to {{0}}",
|
||||||
"com_ui_simple": "Simple",
|
"com_ui_simple": "Simple",
|
||||||
"com_ui_size": "Size",
|
"com_ui_size": "Size",
|
||||||
"com_ui_special_variables": "Special variables:",
|
"com_ui_special_var_current_date": "Current Date",
|
||||||
"com_ui_special_variables_info": "Use `{{current_date}}` for the current date, and `{{current_user}}` for your given account name.",
|
"com_ui_special_var_current_datetime": "Current Date & Time",
|
||||||
|
"com_ui_special_var_iso_datetime": "UTC ISO Datetime",
|
||||||
|
"com_ui_special_var_current_user": "Current User",
|
||||||
|
"com_ui_special_variables": "Special variables",
|
||||||
|
"com_ui_special_variables_more_info": "You can select special variables from the dropdown: `{{current_date}}` (today's date and day of week), `{{current_datetime}}` (local date and time), `{{utc_iso_datetime}}` (UTC ISO datetime), and `{{current_user}}` (your account name).",
|
||||||
"com_ui_speech_while_submitting": "Can't submit speech while a response is being generated",
|
"com_ui_speech_while_submitting": "Can't submit speech while a response is being generated",
|
||||||
|
"com_ui_sr_actions_menu": "Open actions menu for \"{{0}}\"",
|
||||||
"com_ui_stop": "Stop",
|
"com_ui_stop": "Stop",
|
||||||
"com_ui_storage": "Storage",
|
"com_ui_storage": "Storage",
|
||||||
"com_ui_submit": "Submit",
|
"com_ui_submit": "Submit",
|
||||||
|
@ -859,4 +865,4 @@
|
||||||
"com_ui_zoom": "Zoom",
|
"com_ui_zoom": "Zoom",
|
||||||
"com_user_message": "You",
|
"com_user_message": "You",
|
||||||
"com_warning_resubmit_unsupported": "Resubmitting the AI message is not supported for this endpoint."
|
"com_warning_resubmit_unsupported": "Resubmitting the AI message is not supported for this endpoint."
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,18 @@
|
||||||
import { format } from 'date-fns';
|
import { specialVariables } from 'librechat-data-provider';
|
||||||
import type { TUser, TPromptGroup } from 'librechat-data-provider';
|
import type { TPromptGroup } from 'librechat-data-provider';
|
||||||
|
|
||||||
export function replaceSpecialVars({ text, user }: { text: string; user?: TUser }) {
|
|
||||||
if (!text) {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentDate = format(new Date(), 'yyyy-MM-dd');
|
|
||||||
text = text.replace(/{{current_date}}/gi, currentDate);
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
const currentUser = user.name;
|
|
||||||
text = text.replace(/{{current_user}}/gi, currentUser);
|
|
||||||
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detects the presence of variables in the given text, excluding {{current_date}} and {{current_user}}.
|
* Detects the presence of variables in the given text, excluding those found in `specialVariables`.
|
||||||
*/
|
*/
|
||||||
export const detectVariables = (text: string): boolean => {
|
export const detectVariables = (text: string): boolean => {
|
||||||
const regex = /{{(?!current_date|current_user)[^{}]{1,}}}/gi;
|
// Extract all variables with a simple regex
|
||||||
return regex.test(text);
|
const allVariablesRegex = /{{([^{}]+?)}}/gi;
|
||||||
|
const matches = Array.from(text.matchAll(allVariablesRegex)).map((match) =>
|
||||||
|
match[1].trim().toLowerCase(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if any non-special variables exist
|
||||||
|
return matches.some((variable) => !specialVariables[variable]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const wrapVariable = (variable: string) => `{{${variable}}}`;
|
export const wrapVariable = (variable: string) => `{{${variable}}}`;
|
||||||
|
@ -94,11 +83,14 @@ export function formatDateTime(dateTimeString: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mapPromptGroups = (groups: TPromptGroup[]): Record<string, TPromptGroup> => {
|
export const mapPromptGroups = (groups: TPromptGroup[]): Record<string, TPromptGroup> => {
|
||||||
return groups.reduce((acc, group) => {
|
return groups.reduce(
|
||||||
if (!group._id) {
|
(acc, group) => {
|
||||||
|
if (!group._id) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
acc[group._id] = group;
|
||||||
return acc;
|
return acc;
|
||||||
}
|
},
|
||||||
acc[group._id] = group;
|
{} as Record<string, TPromptGroup>,
|
||||||
return acc;
|
);
|
||||||
}, {} as Record<string, TPromptGroup>);
|
|
||||||
};
|
};
|
||||||
|
|
9
package-lock.json
generated
9
package-lock.json
generated
|
@ -26585,6 +26585,12 @@
|
||||||
"url": "https://github.com/sponsors/kossnocorp"
|
"url": "https://github.com/sponsors/kossnocorp"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dayjs": {
|
||||||
|
"version": "1.11.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||||
|
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.0",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||||
|
@ -43594,10 +43600,11 @@
|
||||||
},
|
},
|
||||||
"packages/data-provider": {
|
"packages/data-provider": {
|
||||||
"name": "librechat-data-provider",
|
"name": "librechat-data-provider",
|
||||||
"version": "0.7.792",
|
"version": "0.7.793",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.8.2",
|
"axios": "^1.8.2",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "librechat-data-provider",
|
"name": "librechat-data-provider",
|
||||||
"version": "0.7.792",
|
"version": "0.7.793",
|
||||||
"description": "data services for librechat apps",
|
"description": "data services for librechat apps",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.es.js",
|
"module": "dist/index.es.js",
|
||||||
|
@ -40,6 +40,7 @@
|
||||||
"homepage": "https://librechat.ai",
|
"homepage": "https://librechat.ai",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.8.2",
|
"axios": "^1.8.2",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
|
|
125
packages/data-provider/specs/parsers.spec.ts
Normal file
125
packages/data-provider/specs/parsers.spec.ts
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
import { replaceSpecialVars } from '../src/parsers';
|
||||||
|
import { specialVariables } from '../src/config';
|
||||||
|
import type { TUser } from '../src/types';
|
||||||
|
|
||||||
|
// Mock dayjs module with consistent date/time values regardless of environment
|
||||||
|
jest.mock('dayjs', () => {
|
||||||
|
// Create a mock implementation that returns fixed values
|
||||||
|
const mockDayjs = () => ({
|
||||||
|
format: (format: string) => {
|
||||||
|
if (format === 'YYYY-MM-DD') {
|
||||||
|
return '2024-04-29';
|
||||||
|
}
|
||||||
|
if (format === 'YYYY-MM-DD HH:mm:ss') {
|
||||||
|
return '2024-04-29 12:34:56';
|
||||||
|
}
|
||||||
|
return format; // fallback
|
||||||
|
},
|
||||||
|
day: () => 1, // 1 = Monday
|
||||||
|
toISOString: () => '2024-04-29T16:34:56.000Z',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add any static methods needed
|
||||||
|
mockDayjs.extend = jest.fn();
|
||||||
|
|
||||||
|
return mockDayjs;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('replaceSpecialVars', () => {
|
||||||
|
// Create a partial user object for testing
|
||||||
|
const mockUser = {
|
||||||
|
name: 'Test User',
|
||||||
|
id: 'user123',
|
||||||
|
} as TUser;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return the original text if text is empty', () => {
|
||||||
|
expect(replaceSpecialVars({ text: '' })).toBe('');
|
||||||
|
expect(replaceSpecialVars({ text: null as unknown as string })).toBe(null);
|
||||||
|
expect(replaceSpecialVars({ text: undefined as unknown as string })).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should replace {{current_date}} with the current date', () => {
|
||||||
|
const result = replaceSpecialVars({ text: 'Today is {{current_date}}' });
|
||||||
|
// dayjs().day() returns 1 for Monday (April 29, 2024 is a Monday)
|
||||||
|
expect(result).toBe('Today is 2024-04-29 (1)');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should replace {{current_datetime}} with the current datetime', () => {
|
||||||
|
const result = replaceSpecialVars({ text: 'Now is {{current_datetime}}' });
|
||||||
|
expect(result).toBe('Now is 2024-04-29 12:34:56 (1)');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should replace {{iso_datetime}} with the ISO datetime', () => {
|
||||||
|
const result = replaceSpecialVars({ text: 'ISO time: {{iso_datetime}}' });
|
||||||
|
expect(result).toBe('ISO time: 2024-04-29T16:34:56.000Z');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should replace {{current_user}} with the user name if provided', () => {
|
||||||
|
const result = replaceSpecialVars({
|
||||||
|
text: 'Hello {{current_user}}!',
|
||||||
|
user: mockUser,
|
||||||
|
});
|
||||||
|
expect(result).toBe('Hello Test User!');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not replace {{current_user}} if user is not provided', () => {
|
||||||
|
const result = replaceSpecialVars({
|
||||||
|
text: 'Hello {{current_user}}!',
|
||||||
|
});
|
||||||
|
expect(result).toBe('Hello {{current_user}}!');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not replace {{current_user}} if user has no name', () => {
|
||||||
|
const result = replaceSpecialVars({
|
||||||
|
text: 'Hello {{current_user}}!',
|
||||||
|
user: { id: 'user123' } as TUser,
|
||||||
|
});
|
||||||
|
expect(result).toBe('Hello {{current_user}}!');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle multiple replacements in the same text', () => {
|
||||||
|
const result = replaceSpecialVars({
|
||||||
|
text: 'Hello {{current_user}}! Today is {{current_date}} and the time is {{current_datetime}}. ISO: {{iso_datetime}}',
|
||||||
|
user: mockUser,
|
||||||
|
});
|
||||||
|
expect(result).toBe(
|
||||||
|
'Hello Test User! Today is 2024-04-29 (1) and the time is 2024-04-29 12:34:56 (1). ISO: 2024-04-29T16:34:56.000Z',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should be case-insensitive when replacing variables', () => {
|
||||||
|
const result = replaceSpecialVars({
|
||||||
|
text: 'Date: {{CURRENT_DATE}}, User: {{Current_User}}',
|
||||||
|
user: mockUser,
|
||||||
|
});
|
||||||
|
expect(result).toBe('Date: 2024-04-29 (1), User: Test User');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should confirm all specialVariables from config.ts get parsed', () => {
|
||||||
|
// Create a text that includes all special variables
|
||||||
|
const specialVarsText = Object.keys(specialVariables)
|
||||||
|
.map((key) => `{{${key}}}`)
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
|
const result = replaceSpecialVars({
|
||||||
|
text: specialVarsText,
|
||||||
|
user: mockUser,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify none of the original variable placeholders remain in the result
|
||||||
|
Object.keys(specialVariables).forEach((key) => {
|
||||||
|
const placeholder = `{{${key}}}`;
|
||||||
|
expect(result).not.toContain(placeholder);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify the expected replacements
|
||||||
|
expect(result).toContain('2024-04-29 (1)'); // current_date
|
||||||
|
expect(result).toContain('2024-04-29 12:34:56 (1)'); // current_datetime
|
||||||
|
expect(result).toContain('2024-04-29T16:34:56.000Z'); // iso_datetime
|
||||||
|
expect(result).toContain('Test User'); // current_user
|
||||||
|
});
|
||||||
|
});
|
|
@ -1349,3 +1349,12 @@ export const providerEndpointMap = {
|
||||||
[EModelEndpoint.anthropic]: EModelEndpoint.anthropic,
|
[EModelEndpoint.anthropic]: EModelEndpoint.anthropic,
|
||||||
[EModelEndpoint.azureOpenAI]: EModelEndpoint.azureOpenAI,
|
[EModelEndpoint.azureOpenAI]: EModelEndpoint.azureOpenAI,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const specialVariables = {
|
||||||
|
current_date: true,
|
||||||
|
current_user: true,
|
||||||
|
iso_datetime: true,
|
||||||
|
current_datetime: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSpecialVarLabel = `com_ui_special_var_${keyof typeof specialVariables}`;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import type { ZodIssue } from 'zod';
|
import type { ZodIssue } from 'zod';
|
||||||
import type * as a from './types/assistants';
|
import type * as a from './types/assistants';
|
||||||
import type * as s from './schemas';
|
import type * as s from './schemas';
|
||||||
|
@ -418,3 +419,28 @@ export function findLastSeparatorIndex(text: string, separators = SEPARATORS): n
|
||||||
}
|
}
|
||||||
return lastIndex;
|
return lastIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function replaceSpecialVars({ text, user }: { text: string; user?: t.TUser | null }) {
|
||||||
|
let result = text;
|
||||||
|
if (!result) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// e.g., "2024-04-29 (1)" (1=Monday)
|
||||||
|
const currentDate = dayjs().format('YYYY-MM-DD');
|
||||||
|
const dayNumber = dayjs().day();
|
||||||
|
const combinedDate = `${currentDate} (${dayNumber})`;
|
||||||
|
result = result.replace(/{{current_date}}/gi, combinedDate);
|
||||||
|
|
||||||
|
const currentDatetime = dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
result = result.replace(/{{current_datetime}}/gi, `${currentDatetime} (${dayNumber})`);
|
||||||
|
|
||||||
|
const isoDatetime = dayjs().toISOString();
|
||||||
|
result = result.replace(/{{iso_datetime}}/gi, isoDatetime);
|
||||||
|
|
||||||
|
if (user && user.name) {
|
||||||
|
result = result.replace(/{{current_user}}/gi, user.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
|
@ -29,22 +29,6 @@ export const useAbortRequestWithMessage = (): UseMutationResult<
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetMessagesByConvoId = <TData = s.TMessage[]>(
|
|
||||||
id: string,
|
|
||||||
config?: UseQueryOptions<s.TMessage[], unknown, TData>,
|
|
||||||
): QueryObserverResult<TData> => {
|
|
||||||
return useQuery<s.TMessage[], unknown, TData>(
|
|
||||||
[QueryKeys.messages, id],
|
|
||||||
() => dataService.getMessagesByConvoId(id),
|
|
||||||
{
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
refetchOnReconnect: false,
|
|
||||||
refetchOnMount: false,
|
|
||||||
...config,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useGetSharedMessages = (
|
export const useGetSharedMessages = (
|
||||||
shareId: string,
|
shareId: string,
|
||||||
config?: UseQueryOptions<t.TSharedMessagesResponse>,
|
config?: UseQueryOptions<t.TSharedMessagesResponse>,
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||||
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
||||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
import {
|
||||||
|
StdioClientTransport,
|
||||||
|
getDefaultEnvironment,
|
||||||
|
} from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||||
import { WebSocketClientTransport } from '@modelcontextprotocol/sdk/client/websocket.js';
|
import { WebSocketClientTransport } from '@modelcontextprotocol/sdk/client/websocket.js';
|
||||||
import { ResourceListChangedNotificationSchema } from '@modelcontextprotocol/sdk/types.js';
|
import { ResourceListChangedNotificationSchema } from '@modelcontextprotocol/sdk/types.js';
|
||||||
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
||||||
|
@ -133,7 +136,9 @@ export class MCPConnection extends EventEmitter {
|
||||||
return new StdioClientTransport({
|
return new StdioClientTransport({
|
||||||
command: options.command,
|
command: options.command,
|
||||||
args: options.args,
|
args: options.args,
|
||||||
env: options.env,
|
// workaround bug of mcp sdk that can't pass env:
|
||||||
|
// https://github.com/modelcontextprotocol/typescript-sdk/issues/216
|
||||||
|
env: { ...getDefaultEnvironment(), ...(options.env ?? {}) },
|
||||||
});
|
});
|
||||||
|
|
||||||
case 'websocket':
|
case 'websocket':
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue