🎉 feat: Code Interpreter API and Agents Release (#4860)

* feat: Code Interpreter API & File Search Agent Uploads

chore: add back code files

wip: first pass, abstract key dialog

refactor: influence checkbox on key changes

refactor: update localization keys for 'execute code' to 'run code'

wip: run code button

refactor: add throwError parameter to loadAuthValues and getUserPluginAuthValue functions

feat: first pass, API tool calling

fix: handle missing toolId in callTool function and return 404 for non-existent tools

feat: show code outputs

fix: improve error handling in callTool function and log errors

fix: handle potential null value for filepath in attachment destructuring

fix: normalize language before rendering and prevent null return

fix: add loading indicator in RunCode component while executing code

feat: add support for conditional code execution in Markdown components

feat: attachments

refactor: remove bash

fix: pass abort signal to graph/run

refactor: debounce and rate limit tool call

refactor: increase debounce delay for execute function

feat: set code output attachments

feat: image attachments

refactor: apply message context

refactor: pass `partIndex`

feat: toolCall schema/model/methods

feat: block indexing

feat: get tool calls

chore: imports

chore: typing

chore: condense type imports

feat: get tool calls

fix: block indexing

chore: typing

refactor: update tool calls mapping to support multiple results

fix: add unique key to nav link for rendering

wip: first pass, tool call results

refactor: update query cache from successful tool call mutation

style: improve result switcher styling

chore: note on using \`.toObject()\`

feat: add agent_id field to conversation schema

chore: typing

refactor: rename agentMap to agentsMap for consistency

feat: Agent Name as chat input placeholder

chore: bump agents

📦 chore: update @langchain dependencies to latest versions to match agents package

📦 chore: update @librechat/agents dependency to version 1.8.0

fix: Aborting agent stream removes sender; fix(bedrock): completion removes preset name label

refactor: remove direct file parameter to use req.file, add `processAgentFileUpload` for image uploads

feat: upload menu

feat: prime message_file resources

feat: implement conversation access validation in chat route

refactor: remove file parameter from processFileUpload and use req.file instead

feat: add savedMessageIds set to track saved message IDs in BaseClient, to prevent unnecessary double-write to db

feat: prevent duplicate message saves by checking savedMessageIds in AgentController

refactor: skip legacy RAG API handling for agents

feat: add files field to convoSchema

refactor: update request type annotations from Express.Request to ServerRequest in file processing functions

feat: track conversation files

fix: resendFiles, addPreviousAttachments handling

feat: add ID validation for session_id and file_id in download route

feat: entity_id for code file uploads/downloads

fix: code file edge cases

feat: delete related tool calls

feat: add stream rate handling for LLM configuration

feat: enhance system content with attached file information

fix: improve error logging in resource priming function

* WIP: PoC, sequential agents

WIP: PoC Sequential Agents, first pass content data + bump agents package

fix: package-lock

WIP: PoC, o1 support, refactor bufferString

feat: convertJsonSchemaToZod

fix: form issues and schema defining erroneous model

fix: max length issue on agent form instructions, limit conversation messages to sequential agents

feat: add abort signal support to createRun function and AgentClient

feat: PoC, hide prior sequential agent steps

fix: update parameter naming from config to metadata in event handlers for clarity, add model to usage data

refactor: use only last contentData, track model for usage data

chore: bump agents package

fix: content parts issue

refactor: filter contentParts to include tool calls and relevant indices

feat: show function calls

refactor: filter context messages to exclude tool calls when no tools are available to the agent

fix: ensure tool call content is not undefined in formatMessages

feat: add agent_id field to conversationPreset schema

feat: hide sequential agents

feat: increase upload toast duration to 10 seconds

* refactor: tool context handling & update Code API Key Dialog

feat: toolContextMap

chore: skipSpecs -> useSpecs

ci: fix handleTools tests

feat: API Key Dialog

* feat: Agent Permissions Admin Controls

feat: replace label with button for prompt permission toggle

feat: update agent permissions

feat: enable experimental agents and streamline capability configuration

feat: implement access control for agents and enhance endpoint menu items

feat: add welcome message for agent selection in localization

feat: add agents permission to access control and update version to 0.7.57

* fix: update types in useAssistantListMap and useMentions hooks for better null handling

* feat: mention agents

* fix: agent tool resource race conditions when deleting agent tool resource files

* feat: add error handling for code execution with user feedback

* refactor: rename AdminControls to AdminSettings for clarity

* style: add gap to button in AdminSettings for improved layout

* refactor: separate agent query hooks and check access to enable fetching

* fix: remove unused provider from agent initialization options, creates issue with custom endpoints

* refactor: remove redundant/deprecated modelOptions from AgentClient processes

* chore: update @librechat/agents to version 1.8.5 in package.json and package-lock.json

* fix: minor styling issues + agent panel uniformity

* fix: agent edge cases when set endpoint is no longer defined

* refactor: remove unused cleanup function call from AppService

* fix: update link in ApiKeyDialog to point to pricing page

* fix: improve type handling and layout calculations in SidePanel component

* fix: add missing localization string for agent selection in SidePanel

* chore: form styling and localizations for upload filesearch/code interpreter

* fix: model selection placeholder logic in AgentConfig component

* style: agent capabilities

* fix: add localization for provider selection and improve dropdown styling in ModelPanel

* refactor: use gpt-4o-mini > gpt-3.5-turbo

* fix: agents configuration for loadDefaultInterface and update related tests

* feat: DALLE Agents support
This commit is contained in:
Danny Avila 2024-12-04 15:48:13 -05:00 committed by GitHub
parent affcebd48c
commit 1a815f5e19
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
189 changed files with 5056 additions and 1815 deletions

View file

@ -0,0 +1,34 @@
import { createContext, useContext, ReactNode, useCallback, useRef } from 'react';
type TCodeBlockContext = {
getNextIndex: (skip: boolean) => number;
resetCounter: () => void;
// codeBlocks: Map<number, string>;
};
export const CodeBlockContext = createContext<TCodeBlockContext>({} as TCodeBlockContext);
export const useCodeBlockContext = () => useContext(CodeBlockContext);
export function CodeBlockProvider({ children }: { children: ReactNode }) {
const counterRef = useRef(0);
// const codeBlocks = useRef(new Map<number, string>()).current;
const getNextIndex = useCallback((skip: boolean) => {
if (skip) {
return counterRef.current;
}
const nextIndex = counterRef.current;
counterRef.current += 1;
return nextIndex;
}, []);
const resetCounter = useCallback(() => {
counterRef.current = 0;
}, []);
return (
<CodeBlockContext.Provider value={{ getNextIndex, resetCounter }}>
{children}
</CodeBlockContext.Provider>
);
}

View file

@ -0,0 +1,9 @@
import { createContext, useContext } from 'react';
type MessageContext = {
messageId: string;
partIndex?: number;
conversationId?: string | null;
};
export const MessageContext = createContext<MessageContext>({} as MessageContext);
export const useMessageContext = () => useContext(MessageContext);

View file

@ -0,0 +1,21 @@
import { createContext, useContext } from 'react';
import useToolCallsMap from '~/hooks/Plugins/useToolCallsMap';
type ToolCallsMapContextType = ReturnType<typeof useToolCallsMap>;
export const ToolCallsMapContext = createContext<ToolCallsMapContextType>(
{} as ToolCallsMapContextType,
);
export const useToolCallsMapContext = () => useContext(ToolCallsMapContext);
interface ToolCallsMapProviderProps {
children: React.ReactNode;
conversationId: string;
}
export function ToolCallsMapProvider({ children, conversationId }: ToolCallsMapProviderProps) {
const toolCallsMap = useToolCallsMap({ conversationId });
return (
<ToolCallsMapContext.Provider value={toolCallsMap}>{children}</ToolCallsMapContext.Provider>
);
}

View file

@ -9,9 +9,12 @@ export * from './FileMapContext';
export * from './AddedChatContext';
export * from './ChatFormContext';
export * from './BookmarkContext';
export * from './MessageContext';
export * from './DashboardContext';
export * from './AssistantsContext';
export * from './AgentsContext';
export * from './AssistantsMapContext';
export * from './AnnouncerContext';
export * from './AgentsMapContext';
export * from './CodeBlockContext';
export * from './ToolCallsMapContext';

View file

@ -11,6 +11,8 @@ export type TAgentOption = OptionWithIcon &
export type TAgentCapabilities = {
[AgentCapabilities.execute_code]: boolean;
[AgentCapabilities.file_search]: boolean;
[AgentCapabilities.end_after_tools]?: boolean;
[AgentCapabilities.hide_sequential_outputs]?: boolean;
};
export type AgentForm = {
@ -23,4 +25,5 @@ export type AgentForm = {
model_parameters: AgentModelParameters;
tools?: string[];
provider?: AgentProvider | OptionWithIcon;
agent_ids?: string[];
} & TAgentCapabilities;

View file

@ -1,5 +1,6 @@
export * from './a11y';
export * from './artifacts';
export * from './types';
export * from './tools';
export * from './assistants-types';
export * from './agents-types';

View file

@ -0,0 +1,6 @@
import type { AuthType } from 'librechat-data-provider';
export type ApiKeyFormData = {
apiKey: string;
authType?: string | AuthType;
};

View file

@ -1,36 +1,21 @@
import React from 'react';
import { RefObject } from 'react';
import { FileSources } from 'librechat-data-provider';
import type * as InputNumberPrimitive from 'rc-input-number';
import type { ColumnDef } from '@tanstack/react-table';
import type { SetterOrUpdater } from 'recoil';
import type {
TRole,
TUser,
Agent,
Action,
TPreset,
TPlugin,
TMessage,
Assistant,
TResPlugin,
TLoginUser,
AuthTypeEnum,
TModelsConfig,
TConversation,
TStartupConfig,
EModelEndpoint,
TEndpointsConfig,
ActionMetadata,
AssistantDocument,
AssistantsEndpoint,
TMessageContentParts,
AuthorizationTypeEnum,
TSetOption as SetOption,
TokenExchangeMethodEnum,
} from 'librechat-data-provider';
import type * as t from 'librechat-data-provider';
import type { UseMutationResult } from '@tanstack/react-query';
import type { LucideIcon } from 'lucide-react';
export type CodeBarProps = {
lang: string;
error?: boolean;
plugin?: boolean;
blockIndex?: number;
allowExecution?: boolean;
codeRef: RefObject<HTMLElement>;
};
export enum PromptsEditorMode {
SIMPLE = 'simple',
ADVANCED = 'advanced',
@ -65,21 +50,21 @@ export type AudioChunk = {
export type AssistantListItem = {
id: string;
name: string;
metadata: Assistant['metadata'];
metadata: t.Assistant['metadata'];
model: string;
};
export type AgentListItem = {
id: string;
name: string;
avatar: Agent['avatar'];
avatar: t.Agent['avatar'];
};
export type TPluginMap = Record<string, TPlugin>;
export type TPluginMap = Record<string, t.TPlugin>;
export type GenericSetter<T> = (value: T | ((currentValue: T) => T)) => void;
export type LastSelectedModels = Record<EModelEndpoint, string>;
export type LastSelectedModels = Record<t.EModelEndpoint, string>;
export type LocalizeFunction = (phraseKey: string, ...values: string[]) => string;
@ -145,11 +130,11 @@ export type FileSetter =
export type ActionAuthForm = {
/* General */
type: AuthTypeEnum;
type: t.AuthTypeEnum;
saved_auth_fields: boolean;
/* API key */
api_key: string; // not nested
authorization_type: AuthorizationTypeEnum;
authorization_type: t.AuthorizationTypeEnum;
custom_auth_header: string;
/* OAuth */
oauth_client_id: string; // not nested
@ -157,23 +142,23 @@ export type ActionAuthForm = {
authorization_url: string;
client_url: string;
scope: string;
token_exchange_method: TokenExchangeMethodEnum;
token_exchange_method: t.TokenExchangeMethodEnum;
};
export type ActionWithNullableMetadata = Omit<Action, 'metadata'> & {
metadata: ActionMetadata | null;
export type ActionWithNullableMetadata = Omit<t.Action, 'metadata'> & {
metadata: t.ActionMetadata | null;
};
export type AssistantPanelProps = {
index?: number;
action?: ActionWithNullableMetadata;
actions?: Action[];
actions?: t.Action[];
assistant_id?: string;
activePanel?: string;
endpoint: AssistantsEndpoint;
endpoint: t.AssistantsEndpoint;
version: number | string;
documentsMap: Map<string, AssistantDocument> | null;
setAction: React.Dispatch<React.SetStateAction<Action | undefined>>;
documentsMap: Map<string, t.AssistantDocument> | null;
setAction: React.Dispatch<React.SetStateAction<t.Action | undefined>>;
setCurrentAssistantId: React.Dispatch<React.SetStateAction<string | undefined>>;
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
};
@ -182,11 +167,11 @@ export type AgentPanelProps = {
index?: number;
agent_id?: string;
activePanel?: string;
action?: Action;
actions?: Action[];
action?: t.Action;
actions?: t.Action[];
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
setAction: React.Dispatch<React.SetStateAction<Action | undefined>>;
endpointsConfig?: TEndpointsConfig;
setAction: React.Dispatch<React.SetStateAction<t.Action | undefined>>;
endpointsConfig?: t.TEndpointsConfig;
setCurrentAgentId: React.Dispatch<React.SetStateAction<string | undefined>>;
};
@ -199,7 +184,7 @@ export type AgentModelPanelProps = {
export type AugmentedColumnDef<TData, TValue> = ColumnDef<TData, TValue> & DataColumnMeta;
export type TSetOption = SetOption;
export type TSetOption = t.TSetOption;
export type TSetExample = (
i: number,
@ -234,7 +219,7 @@ export type TShowToast = {
};
export type TBaseSettingsProps = {
conversation: TConversation | TPreset | null;
conversation: t.TConversation | t.TPreset | null;
className?: string;
isPreset?: boolean;
readonly?: boolean;
@ -255,7 +240,7 @@ export type TModelSelectProps = TSettingsProps & TModels;
export type TEditPresetProps = {
open: boolean;
onOpenChange: React.Dispatch<React.SetStateAction<boolean>>;
preset: TPreset;
preset: t.TPreset;
title?: string;
};
@ -266,18 +251,18 @@ export type TSetOptionsPayload = {
addExample: () => void;
removeExample: () => void;
setAgentOption: TSetOption;
// getConversation: () => TConversation | TPreset | null;
// getConversation: () => t.TConversation | t.TPreset | null;
checkPluginSelection: (value: string) => boolean;
setTools: (newValue: string, remove?: boolean) => void;
setOptions?: TSetOptions;
};
export type TPresetItemProps = {
preset: TPreset;
value: TPreset;
onSelect: (preset: TPreset) => void;
onChangePreset: (preset: TPreset) => void;
onDeletePreset: (preset: TPreset) => void;
preset: t.TPreset;
value: t.TPreset;
onSelect: (preset: t.TPreset) => void;
onChangePreset: (preset: t.TPreset) => void;
onDeletePreset: (preset: t.TPreset) => void;
};
export type TOnClick = (e: React.MouseEvent<HTMLButtonElement>) => void;
@ -302,16 +287,16 @@ export type TOptions = {
isRegenerate?: boolean;
isContinued?: boolean;
isEdited?: boolean;
overrideMessages?: TMessage[];
overrideMessages?: t.TMessage[];
};
export type TAskFunction = (props: TAskProps, options?: TOptions) => void;
export type TMessageProps = {
conversation?: TConversation | null;
conversation?: t.TConversation | null;
messageId?: string | null;
message?: TMessage;
messagesTree?: TMessage[];
message?: t.TMessage;
messagesTree?: t.TMessage[];
currentEditId: string | number | null;
isSearchView?: boolean;
siblingIdx?: number;
@ -330,7 +315,7 @@ export type TInitialProps = {
};
export type TAdditionalProps = {
ask: TAskFunction;
message: TMessage;
message: t.TMessage;
isCreatedByUser: boolean;
siblingIdx: number;
enterEdit: (cancel: boolean) => void;
@ -354,7 +339,7 @@ export type TDisplayProps = TText &
export type TConfigProps = {
userKey: string;
setUserKey: React.Dispatch<React.SetStateAction<string>>;
endpoint: EModelEndpoint | string;
endpoint: t.EModelEndpoint | string;
};
export type TDangerButtonProps = {
@ -389,18 +374,18 @@ export type TResError = {
};
export type TAuthContext = {
user: TUser | undefined;
user: t.TUser | undefined;
token: string | undefined;
isAuthenticated: boolean;
error: string | undefined;
login: (data: TLoginUser) => void;
login: (data: t.TLoginUser) => void;
logout: () => void;
setError: React.Dispatch<React.SetStateAction<string | undefined>>;
roles?: Record<string, TRole | null | undefined>;
roles?: Record<string, t.TRole | null | undefined>;
};
export type TUserContext = {
user?: TUser | undefined;
user?: t.TUser | undefined;
token: string | undefined;
isAuthenticated: boolean;
redirect?: string;
@ -411,16 +396,16 @@ export type TAuthConfig = {
test?: boolean;
};
export type IconProps = Pick<TMessage, 'isCreatedByUser' | 'model'> &
Pick<TConversation, 'chatGptLabel' | 'modelLabel' | 'jailbreak'> & {
export type IconProps = Pick<t.TMessage, 'isCreatedByUser' | 'model'> &
Pick<t.TConversation, 'chatGptLabel' | 'modelLabel' | 'jailbreak'> & {
size?: number;
button?: boolean;
iconURL?: string;
message?: boolean;
className?: string;
iconClassName?: string;
endpoint?: EModelEndpoint | string | null;
endpointType?: EModelEndpoint | null;
endpoint?: t.EModelEndpoint | string | null;
endpointType?: t.EModelEndpoint | null;
assistantName?: string;
agentName?: string;
error?: boolean;
@ -440,7 +425,7 @@ export type VoiceOption = {
export type TMessageAudio = {
messageId?: string;
content?: TMessageContentParts[] | string;
content?: t.TMessageContentParts[] | string;
className?: string;
isLast: boolean;
index: number;
@ -482,12 +467,12 @@ export interface ExtendedFile {
export type ContextType = { navVisible: boolean; setNavVisible: (visible: boolean) => void };
export interface SwitcherProps {
endpoint?: EModelEndpoint | null;
endpoint?: t.EModelEndpoint | null;
endpointKeyProvided: boolean;
isCollapsed: boolean;
}
export type TLoginLayoutContext = {
startupConfig: TStartupConfig | null;
startupConfig: t.TStartupConfig | null;
startupConfigError: unknown;
isFetching: boolean;
error: string | null;
@ -497,34 +482,34 @@ export type TLoginLayoutContext = {
};
export type NewConversationParams = {
template?: Partial<TConversation>;
preset?: Partial<TPreset>;
modelsData?: TModelsConfig;
template?: Partial<t.TConversation>;
preset?: Partial<t.TPreset>;
modelsData?: t.TModelsConfig;
buildDefault?: boolean;
keepLatestMessage?: boolean;
keepAddedConvos?: boolean;
};
export type ConvoGenerator = (params: NewConversationParams) => void | TConversation;
export type ConvoGenerator = (params: NewConversationParams) => void | t.TConversation;
export type TBaseResData = {
plugin?: TResPlugin;
plugin?: t.TResPlugin;
final?: boolean;
initial?: boolean;
previousMessages?: TMessage[];
conversation: TConversation;
previousMessages?: t.TMessage[];
conversation: t.TConversation;
conversationId?: string;
runMessages?: TMessage[];
runMessages?: t.TMessage[];
};
export type TResData = TBaseResData & {
requestMessage: TMessage;
responseMessage: TMessage;
requestMessage: t.TMessage;
responseMessage: t.TMessage;
};
export type TFinalResData = TBaseResData & {
requestMessage?: TMessage;
responseMessage?: TMessage;
requestMessage?: t.TMessage;
responseMessage?: t.TMessage;
};
export type TVectorStore = {

View file

@ -5,7 +5,6 @@ import { useChatContext, useAddedChatContext } from '~/Providers';
import { TooltipAnchor } from '~/components';
import { mainTextareaId } from '~/common';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
function AddMultiConvo() {
const { conversation } = useChatContext();

View file

@ -0,0 +1,100 @@
import * as Ariakit from '@ariakit/react';
import React, { useRef, useState } from 'react';
import { FileSearch, ImageUpIcon, TerminalSquareIcon } from 'lucide-react';
import { EToolResources } from 'librechat-data-provider';
import { FileUpload, TooltipAnchor, DropdownPopup } from '~/components/ui';
import { AttachmentIcon } from '~/components/svg';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
interface AttachFileProps {
isRTL: boolean;
disabled?: boolean | null;
handleFileChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
setToolResource?: React.Dispatch<React.SetStateAction<string | undefined>>;
}
const AttachFile = ({ isRTL, disabled, setToolResource, handleFileChange }: AttachFileProps) => {
const localize = useLocalize();
const isUploadDisabled = disabled ?? false;
const inputRef = useRef<HTMLInputElement>(null);
const [isPopoverActive, setIsPopoverActive] = useState(false);
const handleUploadClick = (isImage?: boolean) => {
if (!inputRef.current) {
return;
}
inputRef.current.value = '';
inputRef.current.accept = isImage === true ? 'image/*' : '';
inputRef.current.click();
inputRef.current.accept = '';
};
const dropdownItems = [
{
label: localize('com_ui_upload_image_input'),
onClick: () => {
setToolResource?.(undefined);
handleUploadClick(true);
},
icon: <ImageUpIcon className="icon-md" />,
},
{
label: localize('com_ui_upload_file_search'),
onClick: () => {
setToolResource?.(EToolResources.file_search);
handleUploadClick();
},
icon: <FileSearch className="icon-md" />,
},
{
label: localize('com_ui_upload_code_files'),
onClick: () => {
setToolResource?.(EToolResources.execute_code);
handleUploadClick();
},
icon: <TerminalSquareIcon className="icon-md" />,
},
];
const menuTrigger = (
<TooltipAnchor
render={
<Ariakit.MenuButton
disabled={isUploadDisabled}
id="attach-file-menu-button"
aria-label="Attach File Options"
className={cn(
'absolute flex size-[35px] items-center justify-center rounded-full p-1 transition-colors hover:bg-surface-hover focus:outline-none focus:ring-2 focus:ring-primary focus:ring-opacity-50',
isRTL ? 'bottom-2 right-2' : 'bottom-2 left-1 md:left-2',
)}
>
<div className="flex w-full items-center justify-center gap-2">
<AttachmentIcon />
</div>
</Ariakit.MenuButton>
}
id="attach-file-menu-button"
description={localize('com_sidepanel_attach_files')}
disabled={isUploadDisabled}
/>
);
return (
<FileUpload ref={inputRef} handleFileChange={handleFileChange}>
<div className="relative">
<DropdownPopup
menuId="attach-file-menu"
isOpen={isPopoverActive}
setIsOpen={setIsPopoverActive}
modal={true}
trigger={menuTrigger}
items={dropdownItems}
iconClassName="mr-0"
/>
</div>
</FileUpload>
);
};
export default React.memo(AttachFile);

View file

@ -1,12 +1,14 @@
import { memo } from 'react';
import { memo, useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import {
supportsFiles,
mergeFileConfig,
isAgentsEndpoint,
EndpointFileConfig,
fileConfig as defaultFileConfig,
} from 'librechat-data-provider';
import { useGetFileConfig } from '~/data-provider';
import AttachFileMenu from './AttachFileMenu';
import { useChatContext } from '~/Providers';
import { useFileHandling } from '~/hooks';
import AttachFile from './AttachFile';
@ -20,23 +22,46 @@ function FileFormWrapper({
disableInputs: boolean;
children?: React.ReactNode;
}) {
const { handleFileChange, abortUpload } = useFileHandling();
const chatDirection = useRecoilValue(store.chatDirection).toLowerCase();
const { files, setFiles, conversation, setFilesLoading } = useChatContext();
const { endpoint: _endpoint, endpointType } = conversation ?? { endpoint: null };
const isAgents = useMemo(() => isAgentsEndpoint(_endpoint), [_endpoint]);
const { handleFileChange, abortUpload, setToolResource } = useFileHandling();
const { data: fileConfig = defaultFileConfig } = useGetFileConfig({
select: (data) => mergeFileConfig(data),
});
const isRTL = chatDirection === 'rtl';
const { endpoint: _endpoint, endpointType } = conversation ?? { endpoint: null };
const endpointFileConfig = fileConfig.endpoints[_endpoint ?? ''] as
| EndpointFileConfig
| undefined;
const endpointSupportsFiles: boolean = supportsFiles[endpointType ?? _endpoint ?? ''] ?? false;
const isUploadDisabled = (disableInputs || endpointFileConfig?.disabled) ?? false;
const renderAttachFile = () => {
if (isAgents) {
return (
<AttachFileMenu
isRTL={isRTL}
disabled={disableInputs}
setToolResource={setToolResource}
handleFileChange={handleFileChange}
/>
);
}
if (endpointSupportsFiles && !isUploadDisabled) {
return (
<AttachFile isRTL={isRTL} disabled={disableInputs} handleFileChange={handleFileChange} />
);
}
return null;
};
return (
<>
<FileRow
@ -50,9 +75,7 @@ function FileFormWrapper({
)}
/>
{children}
{endpointSupportsFiles && !isUploadDisabled && (
<AttachFile isRTL={isRTL} disabled={disableInputs} handleFileChange={handleFileChange} />
)}
{renderAttachFile()}
</>
);
}

View file

@ -26,8 +26,15 @@ export default function Mention({
}) {
const localize = useLocalize();
const assistantMap = useAssistantsMapContext();
const { options, presets, modelSpecs, modelsConfig, endpointsConfig, assistantListMap } =
useMentions({ assistantMap: assistantMap || {}, includeAssistants });
const {
options,
presets,
modelSpecs,
agentsList,
modelsConfig,
endpointsConfig,
assistantListMap,
} = useMentions({ assistantMap: assistantMap || {}, includeAssistants });
const { onSelectMention } = useSelectMention({
presets,
modelSpecs,
@ -62,18 +69,23 @@ export default function Mention({
}
};
if (mention.type === 'endpoint' && mention.value === EModelEndpoint.assistants) {
if (mention.type === 'endpoint' && mention.value === EModelEndpoint.agents) {
setSearchValue('');
setInputOptions(assistantListMap[EModelEndpoint.assistants]);
setInputOptions(agentsList ?? []);
setActiveIndex(0);
inputRef.current?.focus();
} else if (mention.type === 'endpoint' && mention.value === EModelEndpoint.assistants) {
setSearchValue('');
setInputOptions(assistantListMap[EModelEndpoint.assistants] ?? []);
setActiveIndex(0);
inputRef.current?.focus();
} else if (mention.type === 'endpoint' && mention.value === EModelEndpoint.azureAssistants) {
setSearchValue('');
setInputOptions(assistantListMap[EModelEndpoint.azureAssistants]);
setInputOptions(assistantListMap[EModelEndpoint.azureAssistants] ?? []);
setActiveIndex(0);
inputRef.current?.focus();
} else if (mention.type === 'endpoint') {
const models = (modelsConfig?.[mention.value ?? ''] ?? []).map((model) => ({
const models = (modelsConfig?.[mention.value || ''] ?? []).map((model) => ({
value: mention.value,
label: model,
type: 'model',

View file

@ -1,47 +1,57 @@
import type { FC } from 'react';
import { Close } from '@radix-ui/react-popover';
import { EModelEndpoint, alternateName } from 'librechat-data-provider';
import {
EModelEndpoint,
alternateName,
PermissionTypes,
Permissions,
} from 'librechat-data-provider';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import MenuSeparator from '../UI/MenuSeparator';
import { getEndpointField } from '~/utils';
import { useHasAccess } from '~/hooks';
import MenuItem from './MenuItem';
const EndpointItems: FC<{
endpoints: EModelEndpoint[];
endpoints: Array<EModelEndpoint | undefined>;
selected: EModelEndpoint | '';
}> = ({ endpoints, selected }) => {
}> = ({ endpoints = [], selected }) => {
const hasAccessToAgents = useHasAccess({
permissionType: PermissionTypes.AGENTS,
permission: Permissions.USE,
});
const { data: endpointsConfig } = useGetEndpointsQuery();
return (
<>
{endpoints &&
endpoints.map((endpoint, i) => {
if (!endpoint) {
return null;
} else if (!endpointsConfig?.[endpoint]) {
return null;
}
const userProvidesKey: boolean | null | undefined = getEndpointField(
endpointsConfig,
endpoint,
'userProvide',
);
return (
<Close asChild key={`endpoint-${endpoint}`}>
<div key={`endpoint-${endpoint}`}>
<MenuItem
key={`endpoint-item-${endpoint}`}
title={alternateName[endpoint] || endpoint}
value={endpoint}
selected={selected === endpoint}
data-testid={`endpoint-item-${endpoint}`}
userProvidesKey={!!userProvidesKey}
// description="With DALL·E, browsing and analysis"
/>
{i !== endpoints.length - 1 && <MenuSeparator />}
</div>
</Close>
);
})}
{endpoints.map((endpoint, i) => {
if (!endpoint) {
return null;
} else if (!endpointsConfig?.[endpoint]) {
return null;
}
if (endpoint === EModelEndpoint.agents && !hasAccessToAgents) {
return null;
}
const userProvidesKey: boolean | null | undefined =
getEndpointField(endpointsConfig, endpoint, 'userProvide') ?? false;
return (
<Close asChild key={`endpoint-${endpoint}`}>
<div key={`endpoint-${endpoint}`}>
<MenuItem
key={`endpoint-item-${endpoint}`}
title={alternateName[endpoint] || endpoint}
value={endpoint}
selected={selected === endpoint}
data-testid={`endpoint-item-${endpoint}`}
userProvidesKey={!!userProvidesKey}
// description="With DALL·E, browsing and analysis"
/>
{i !== endpoints.length - 1 && <MenuSeparator />}
</div>
</Close>
);
})}
</>
);
};

View file

@ -4,12 +4,14 @@ import { ContentTypes } from 'librechat-data-provider';
import type { TMessageContentParts, TAttachment, Agents } from 'librechat-data-provider';
import EditTextPart from './Parts/EditTextPart';
import { mapAttachments } from '~/utils/map';
import { MessageContext } from '~/Providers';
import store from '~/store';
import Part from './Part';
type ContentPartsProps = {
content: Array<TMessageContentParts | undefined> | undefined;
messageId: string;
conversationId?: string | null;
attachments?: TAttachment[];
isCreatedByUser: boolean;
isLast: boolean;
@ -27,6 +29,7 @@ const ContentParts = memo(
({
content,
messageId,
conversationId,
attachments,
isCreatedByUser,
isLast,
@ -79,15 +82,23 @@ const ContentParts = memo(
const attachments = attachmentMap[toolCallId];
return (
<Part
part={part}
isSubmitting={isSubmitting}
attachments={attachments}
key={`display-${messageId}-${idx}`}
showCursor={idx === content.length - 1 && isLast}
messageId={messageId}
isCreatedByUser={isCreatedByUser}
/>
<MessageContext.Provider
key={`provider-${messageId}-${idx}`}
value={{
messageId,
conversationId,
partIndex: idx,
}}
>
<Part
part={part}
attachments={attachments}
isSubmitting={isSubmitting}
key={`part-${messageId}-${idx}`}
isCreatedByUser={isCreatedByUser}
showCursor={idx === content.length - 1 && isLast}
/>
</MessageContext.Provider>
);
})}
</>

View file

@ -1,4 +1,4 @@
import React, { memo, useMemo } from 'react';
import React, { memo, useMemo, useRef, useEffect } from 'react';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import supersub from 'remark-supersub';
@ -10,10 +10,10 @@ import remarkDirective from 'remark-directive';
import type { Pluggable } from 'unified';
import { Artifact, artifactPlugin } from '~/components/Artifacts/Artifact';
import { langSubset, preprocessLaTeX, handleDoubleClick } from '~/utils';
import { useToastContext, CodeBlockProvider, useCodeBlockContext } from '~/Providers';
import CodeBlock from '~/components/Messages/Content/CodeBlock';
import { useFileDownload } from '~/data-provider';
import useLocalize from '~/hooks/useLocalize';
import { useToastContext } from '~/Providers';
import store from '~/store';
type TCodeProps = {
@ -25,6 +25,32 @@ type TCodeProps = {
export const code: React.ElementType = memo(({ className, children }: TCodeProps) => {
const match = /language-(\w+)/.exec(className ?? '');
const lang = match && match[1];
const isMath = lang === 'math';
const isSingleLine = typeof children === 'string' && children.split('\n').length === 1;
const { getNextIndex, resetCounter } = useCodeBlockContext();
const blockIndex = useRef(getNextIndex(isMath || isSingleLine)).current;
useEffect(() => {
resetCounter();
}, [children, resetCounter]);
if (isMath) {
return children;
} else if (isSingleLine) {
return (
<code onDoubleClick={handleDoubleClick} className={className}>
{children}
</code>
);
} else {
return <CodeBlock lang={lang ?? 'text'} codeChildren={children} blockIndex={blockIndex} />;
}
});
export const codeNoExecution: React.ElementType = memo(({ className, children }: TCodeProps) => {
const match = /language-(\w+)/.exec(className ?? '');
const lang = match && match[1];
if (lang === 'math') {
return children;
@ -35,7 +61,7 @@ export const code: React.ElementType = memo(({ className, children }: TCodeProps
</code>
);
} else {
return <CodeBlock lang={lang ?? 'text'} codeChildren={children} />;
return <CodeBlock lang={lang ?? 'text'} codeChildren={children} allowExecution={false} />;
}
});
@ -45,7 +71,11 @@ export const a: React.ElementType = memo(
const { showToast } = useToastContext();
const localize = useLocalize();
const { file_id, filename, filepath } = useMemo(() => {
const {
file_id = '',
filename = '',
filepath,
} = useMemo(() => {
const pattern = new RegExp(`(?:files|outputs)/${user?.id}/([^\\s]+)`);
const match = href.match(pattern);
if (match && match[0]) {
@ -164,25 +194,27 @@ const Markdown = memo(({ content = '', showCursor, isLatestMessage }: TContentPr
: [supersub, remarkGfm, [remarkMath, { singleDollarTextMath: true }]];
return (
<ReactMarkdown
/** @ts-ignore */
remarkPlugins={remarkPlugins}
/* @ts-ignore */
rehypePlugins={rehypePlugins}
// linkTarget="_new"
components={
{
code,
a,
p,
artifact: Artifact,
} as {
[nodeType: string]: React.ElementType;
<CodeBlockProvider>
<ReactMarkdown
/** @ts-ignore */
remarkPlugins={remarkPlugins}
/* @ts-ignore */
rehypePlugins={rehypePlugins}
// linkTarget="_new"
components={
{
code,
a,
p,
artifact: Artifact,
} as {
[nodeType: string]: React.ElementType;
}
}
}
>
{isLatestMessage && showCursor === true ? currentContent + cursor : currentContent}
</ReactMarkdown>
>
{isLatestMessage && showCursor === true ? currentContent + cursor : currentContent}
</ReactMarkdown>
</CodeBlockProvider>
);
});

View file

@ -6,40 +6,51 @@ import supersub from 'remark-supersub';
import ReactMarkdown from 'react-markdown';
import rehypeHighlight from 'rehype-highlight';
import type { PluggableList } from 'unified';
import { code, codeNoExecution, a, p } from './Markdown';
import { CodeBlockProvider } from '~/Providers';
import { langSubset } from '~/utils';
import { code, a, p } from './Markdown';
const MarkdownLite = memo(({ content = '' }: { content?: string }) => {
const rehypePlugins: PluggableList = [
[rehypeKatex, { output: 'mathml' }],
[
rehypeHighlight,
{
detect: true,
ignoreMissing: true,
subset: langSubset,
},
],
];
return (
<ReactMarkdown
remarkPlugins={[supersub, remarkGfm, [remarkMath, { singleDollarTextMath: true }]]}
rehypePlugins={rehypePlugins}
// linkTarget="_new"
components={
const MarkdownLite = memo(
({ content = '', codeExecution = true }: { content?: string; codeExecution?: boolean }) => {
const rehypePlugins: PluggableList = [
[rehypeKatex, { output: 'mathml' }],
[
rehypeHighlight,
{
code,
a,
p,
} as {
[nodeType: string]: React.ElementType;
}
}
>
{content}
</ReactMarkdown>
);
});
detect: true,
ignoreMissing: true,
subset: langSubset,
},
],
];
return (
<CodeBlockProvider>
<ReactMarkdown
remarkPlugins={[
/** @ts-ignore */
supersub,
remarkGfm,
[remarkMath, { singleDollarTextMath: true }],
]}
/** @ts-ignore */
rehypePlugins={rehypePlugins}
// linkTarget="_new"
components={
{
code: codeExecution ? code : codeNoExecution,
a,
p,
} as {
[nodeType: string]: React.ElementType;
}
}
>
{content}
</ReactMarkdown>
</CodeBlockProvider>
);
},
);
export default MarkdownLite;

View file

@ -21,143 +21,130 @@ type PartProps = {
part?: TMessageContentParts;
isSubmitting: boolean;
showCursor: boolean;
messageId: string;
isCreatedByUser: boolean;
attachments?: TAttachment[];
};
const Part = memo(
({ part, isSubmitting, attachments, showCursor, messageId, isCreatedByUser }: PartProps) => {
attachments && console.log(attachments);
if (!part) {
const Part = memo(({ part, isSubmitting, attachments, showCursor, isCreatedByUser }: PartProps) => {
if (!part) {
return null;
}
if (part.type === ContentTypes.ERROR) {
return <ErrorMessage text={part[ContentTypes.TEXT].value} className="my-2" />;
} else if (part.type === ContentTypes.TEXT) {
const text = typeof part.text === 'string' ? part.text : part.text.value;
if (typeof text !== 'string') {
return null;
}
if (part.tool_call_ids != null && !text) {
return null;
}
return (
<Container>
<Text text={text} isCreatedByUser={isCreatedByUser} showCursor={showCursor} />
</Container>
);
} else if (part.type === ContentTypes.TOOL_CALL) {
const toolCall = part[ContentTypes.TOOL_CALL];
if (!toolCall) {
return null;
}
if (part.type === ContentTypes.ERROR) {
return <ErrorMessage text={part[ContentTypes.TEXT].value} className="my-2" />;
} else if (part.type === ContentTypes.TEXT) {
const text = typeof part.text === 'string' ? part.text : part.text.value;
if (typeof text !== 'string') {
return null;
}
if (part.tool_call_ids != null && !text) {
return null;
}
const isToolCall =
'args' in toolCall && (!toolCall.type || toolCall.type === ToolCallTypes.TOOL_CALL);
if (isToolCall && toolCall.name === Tools.execute_code) {
return (
<Container>
<Text
text={text}
isCreatedByUser={isCreatedByUser}
messageId={messageId}
showCursor={showCursor}
/>
</Container>
<ExecuteCode
args={typeof toolCall.args === 'string' ? toolCall.args : ''}
output={toolCall.output ?? ''}
initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting}
attachments={attachments}
/>
);
} else if (part.type === ContentTypes.TOOL_CALL) {
const toolCall = part[ContentTypes.TOOL_CALL];
if (!toolCall) {
} else if (isToolCall) {
return (
<ToolCall
args={toolCall.args ?? ''}
name={toolCall.name || ''}
output={toolCall.output ?? ''}
initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting}
attachments={attachments}
/>
);
} else if (toolCall.type === ToolCallTypes.CODE_INTERPRETER) {
const code_interpreter = toolCall[ToolCallTypes.CODE_INTERPRETER];
return (
<CodeAnalyze
initialProgress={toolCall.progress ?? 0.1}
code={code_interpreter.input}
outputs={code_interpreter.outputs ?? []}
isSubmitting={isSubmitting}
/>
);
} else if (
toolCall.type === ToolCallTypes.RETRIEVAL ||
toolCall.type === ToolCallTypes.FILE_SEARCH
) {
return (
<RetrievalCall initialProgress={toolCall.progress ?? 0.1} isSubmitting={isSubmitting} />
);
} else if (
toolCall.type === ToolCallTypes.FUNCTION &&
ToolCallTypes.FUNCTION in toolCall &&
imageGenTools.has(toolCall.function.name)
) {
return (
<ImageGen
initialProgress={toolCall.progress ?? 0.1}
args={toolCall.function.arguments as string}
/>
);
} else if (toolCall.type === ToolCallTypes.FUNCTION && ToolCallTypes.FUNCTION in toolCall) {
if (isImageVisionTool(toolCall)) {
if (isSubmitting && showCursor) {
return (
<Container>
<Text text={''} isCreatedByUser={isCreatedByUser} showCursor={showCursor} />
</Container>
);
}
return null;
}
const isToolCall =
'args' in toolCall && (!toolCall.type || toolCall.type === ToolCallTypes.TOOL_CALL);
if (isToolCall && toolCall.name === Tools.execute_code) {
return (
<ExecuteCode
args={typeof toolCall.args === 'string' ? toolCall.args : ''}
output={toolCall.output ?? ''}
initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting}
attachments={attachments}
/>
);
} else if (isToolCall) {
return (
<ToolCall
args={toolCall.args ?? ''}
name={toolCall.name ?? ''}
output={toolCall.output ?? ''}
initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting}
/>
);
} else if (toolCall.type === ToolCallTypes.CODE_INTERPRETER) {
const code_interpreter = toolCall[ToolCallTypes.CODE_INTERPRETER];
return (
<CodeAnalyze
initialProgress={toolCall.progress ?? 0.1}
code={code_interpreter.input}
outputs={code_interpreter.outputs ?? []}
isSubmitting={isSubmitting}
/>
);
} else if (
toolCall.type === ToolCallTypes.RETRIEVAL ||
toolCall.type === ToolCallTypes.FILE_SEARCH
) {
return (
<RetrievalCall initialProgress={toolCall.progress ?? 0.1} isSubmitting={isSubmitting} />
);
} else if (
toolCall.type === ToolCallTypes.FUNCTION &&
ToolCallTypes.FUNCTION in toolCall &&
imageGenTools.has(toolCall.function.name)
) {
return (
<ImageGen
initialProgress={toolCall.progress ?? 0.1}
args={toolCall.function.arguments as string}
/>
);
} else if (toolCall.type === ToolCallTypes.FUNCTION && ToolCallTypes.FUNCTION in toolCall) {
if (isImageVisionTool(toolCall)) {
if (isSubmitting && showCursor) {
return (
<Container>
<Text
text={''}
isCreatedByUser={isCreatedByUser}
messageId={messageId}
showCursor={showCursor}
/>
</Container>
);
}
return null;
}
return (
<ToolCall
initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting}
args={toolCall.function.arguments as string}
name={toolCall.function.name}
output={toolCall.function.output}
/>
);
}
} else if (part.type === ContentTypes.IMAGE_FILE) {
const imageFile = part[ContentTypes.IMAGE_FILE];
const height = imageFile.height ?? 1920;
const width = imageFile.width ?? 1080;
return (
<Image
imagePath={imageFile.filepath}
height={height}
width={width}
altText={imageFile.filename ?? 'Uploaded Image'}
placeholderDimensions={{
height: height + 'px',
width: width + 'px',
}}
<ToolCall
initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting}
args={toolCall.function.arguments as string}
name={toolCall.function.name}
output={toolCall.function.output}
/>
);
}
} else if (part.type === ContentTypes.IMAGE_FILE) {
const imageFile = part[ContentTypes.IMAGE_FILE];
const height = imageFile.height ?? 1920;
const width = imageFile.width ?? 1080;
return (
<Image
imagePath={imageFile.filepath}
height={height}
width={width}
altText={imageFile.filename ?? 'Uploaded Image'}
placeholderDimensions={{
height: height + 'px',
width: width + 'px',
}}
/>
);
}
return null;
},
);
return null;
});
export default Part;

View file

@ -0,0 +1,19 @@
import { imageExtRegex } from 'librechat-data-provider';
import type { TAttachment, TFile, TAttachmentMetadata } from 'librechat-data-provider';
import Image from '~/components/Chat/Messages/Content/Image';
export default function Attachment({ attachment }: { attachment?: TAttachment }) {
if (!attachment) {
return null;
}
const { width, height, filepath = null } = attachment as TFile & TAttachmentMetadata;
const isImage =
imageExtRegex.test(attachment.filename) && width != null && height != null && filepath != null;
if (isImage) {
return (
<Image altText={attachment.filename} imagePath={filepath} height={height} width={width} />
);
}
return null;
}

View file

@ -1,12 +1,11 @@
import React, { useMemo, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { CodeInProgress } from './CodeProgress';
import { imageExtRegex } from 'librechat-data-provider';
import type { TFile, TAttachment, TAttachmentMetadata } from 'librechat-data-provider';
import type { TAttachment } from 'librechat-data-provider';
import ProgressText from '~/components/Chat/Messages/Content/ProgressText';
import FinishedIcon from '~/components/Chat/Messages/Content/FinishedIcon';
import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite';
import Image from '~/components/Chat/Messages/Content/Image';
import { CodeInProgress } from './CodeProgress';
import Attachment from './Attachment';
import LogContent from './LogContent';
import { useProgress } from '~/hooks';
import store from '~/store';
@ -86,7 +85,10 @@ export default function ExecuteCode({
</div>
{showCode && (
<div className="code-analyze-block mb-3 mt-0.5 overflow-hidden rounded-xl bg-black">
<MarkdownLite content={code ? `\`\`\`${lang}\n${code}\n\`\`\`` : ''} />
<MarkdownLite
content={code ? `\`\`\`${lang}\n${code}\n\`\`\`` : ''}
codeExecution={false}
/>
{output.length > 0 && (
<div className="bg-gray-700 p-4 text-xs">
<div
@ -103,25 +105,9 @@ export default function ExecuteCode({
)}
</div>
)}
{attachments?.map((attachment, index) => {
const { width, height, filepath } = attachment as TFile & TAttachmentMetadata;
const isImage =
imageExtRegex.test(attachment.filename) &&
width != null &&
height != null &&
filepath != null;
if (isImage) {
return (
<Image
key={index}
altText={attachment.filename}
imagePath={filepath}
height={height}
width={width}
/>
);
}
})}
{attachments?.map((attachment, index) => (
<Attachment attachment={attachment} key={index} />
))}
</>
);
}

View file

@ -1,17 +1,26 @@
import { isAfter } from 'date-fns';
import React, { useMemo } from 'react';
import { imageExtRegex } from 'librechat-data-provider';
import type { TAttachment } from 'librechat-data-provider';
import type { TFile, TAttachment, TAttachmentMetadata } from 'librechat-data-provider';
import Image from '~/components/Chat/Messages/Content/Image';
import { useLocalize } from '~/hooks';
import LogLink from './LogLink';
interface LogContentProps {
output?: string;
renderImages?: boolean;
attachments?: TAttachment[];
}
const LogContent: React.FC<LogContentProps> = ({ output = '', attachments }) => {
type ImageAttachment = TFile &
TAttachmentMetadata & {
height: number;
width: number;
};
const LogContent: React.FC<LogContentProps> = ({ output = '', renderImages, attachments }) => {
const localize = useLocalize();
const processedContent = useMemo(() => {
if (!output) {
return '';
@ -21,8 +30,29 @@ const LogContent: React.FC<LogContentProps> = ({ output = '', attachments }) =>
return parts[0].trim();
}, [output]);
const nonImageAttachments =
attachments?.filter((file) => !imageExtRegex.test(file.filename)) || [];
const { imageAttachments, nonImageAttachments } = useMemo(() => {
const imageAtts: ImageAttachment[] = [];
const nonImageAtts: TAttachment[] = [];
attachments?.forEach((attachment) => {
const { width, height, filepath = null } = attachment as TFile & TAttachmentMetadata;
const isImage =
imageExtRegex.test(attachment.filename) &&
width != null &&
height != null &&
filepath != null;
if (isImage) {
imageAtts.push(attachment as ImageAttachment);
} else {
nonImageAtts.push(attachment);
}
});
return {
imageAttachments: renderImages === true ? imageAtts : null,
nonImageAttachments: nonImageAtts,
};
}, [attachments, renderImages]);
const renderAttachment = (file: TAttachment) => {
const now = new Date();
@ -59,6 +89,18 @@ const LogContent: React.FC<LogContentProps> = ({ output = '', attachments }) =>
))}
</div>
)}
{imageAttachments?.map((attachment, index) => {
const { width, height, filepath } = attachment;
return (
<Image
key={index}
altText={attachment.filename}
imagePath={filepath}
height={height}
width={width}
/>
);
})}
</>
);
};

View file

@ -2,15 +2,14 @@ import { memo, useMemo, ReactElement } from 'react';
import { useRecoilValue } from 'recoil';
import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite';
import Markdown from '~/components/Chat/Messages/Content/Markdown';
import { useChatContext } from '~/Providers';
import { useChatContext, useMessageContext } from '~/Providers';
import { cn } from '~/utils';
import store from '~/store';
type TextPartProps = {
text: string;
isCreatedByUser: boolean;
messageId: string;
showCursor: boolean;
isCreatedByUser: boolean;
};
type ContentType =
@ -18,7 +17,8 @@ type ContentType =
| ReactElement<React.ComponentProps<typeof MarkdownLite>>
| ReactElement;
const TextPart = memo(({ text, isCreatedByUser, messageId, showCursor }: TextPartProps) => {
const TextPart = memo(({ text, isCreatedByUser, showCursor }: TextPartProps) => {
const { messageId } = useMessageContext();
const { isSubmitting, latestMessage } = useChatContext();
const enableUserMsgMarkdown = useRecoilValue(store.enableUserMsgMarkdown);
const showCursorState = useMemo(() => showCursor && isSubmitting, [showCursor, isSubmitting]);

View file

@ -1,9 +1,11 @@
import { useMemo } from 'react';
import { actionDelimiter, actionDomainSeparator, Constants } from 'librechat-data-provider';
import * as Popover from '@radix-ui/react-popover';
import { actionDelimiter, actionDomainSeparator, Constants } from 'librechat-data-provider';
import type { TAttachment } from 'librechat-data-provider';
import useLocalize from '~/hooks/useLocalize';
import ProgressCircle from './ProgressCircle';
import InProgressCall from './InProgressCall';
import Attachment from './Parts/Attachment';
import CancelledIcon from './CancelledIcon';
import ProgressText from './ProgressText';
import FinishedIcon from './FinishedIcon';
@ -18,12 +20,14 @@ export default function ToolCall({
name,
args: _args = '',
output,
attachments,
}: {
initialProgress: number;
isSubmitting: boolean;
name: string;
args: string | Record<string, unknown>;
output?: string | null;
attachments?: TAttachment[];
}) {
const localize = useLocalize();
const progress = useProgress(initialProgress);
@ -106,6 +110,9 @@ export default function ToolCall({
/>
)}
</div>
{attachments?.map((attachment, index) => (
<Attachment attachment={attachment} key={index} />
))}
</Popover.Root>
);
}

View file

@ -33,7 +33,7 @@ export default function ToolPopover({
<div tabIndex={-1}>
<div className="bg-token-surface-primary max-w-sm rounded-md p-2 shadow-[0_0_24px_0_rgba(0,0,0,0.05),inset_0_0.5px_0_0_rgba(0,0,0,0.05),0_2px_8px_0_rgba(0,0,0,0.05)]">
<div className="mb-2 text-sm font-medium dark:text-gray-100">
{domain
{domain != null && domain
? localize('com_assistants_domain_info', domain)
: localize('com_assistants_function_use', function_name)}
</div>
@ -42,7 +42,7 @@ export default function ToolPopover({
<code className="!whitespace-pre-wrap ">{formatText(input)}</code>
</div>
</div>
{output && (
{output != null && output && (
<>
<div className="mb-2 mt-2 text-sm font-medium dark:text-gray-100">
{localize('com_ui_result')}

View file

@ -82,11 +82,12 @@ export default function Message(props: TMessageProps) {
<div className="flex-col gap-1 md:gap-3">
<div className="flex max-w-full flex-grow flex-col gap-0">
<ContentParts
content={message.content as Array<TMessageContentParts | undefined>}
messageId={message.messageId}
isCreatedByUser={message.isCreatedByUser}
isLast={isLast}
isSubmitting={isSubmitting}
messageId={message.messageId}
isCreatedByUser={message.isCreatedByUser}
conversationId={conversation?.conversationId}
content={message.content as Array<TMessageContentParts | undefined>}
/>
</div>
</div>

View file

@ -9,6 +9,7 @@ import HoverButtons from '~/components/Chat/Messages/HoverButtons';
import Icon from '~/components/Chat/Messages/MessageIcon';
import { Plugin } from '~/components/Messages/Content';
import SubRow from '~/components/Chat/Messages/SubRow';
import { MessageContext } from '~/Providers';
import { useMessageActions } from '~/hooks';
import { cn, logger } from '~/utils';
import store from '~/store';
@ -59,9 +60,10 @@ const MessageRender = memo(
const fontSize = useRecoilValue(store.fontSize);
const handleRegenerateMessage = useCallback(() => regenerateMessage(), [regenerateMessage]);
const { isCreatedByUser, error, unfinished } = msg ?? {};
const hasNoChildren = !(msg?.children?.length ?? 0);
const isLast = useMemo(
() => !msg?.children?.length && (msg?.depth === latestMessage?.depth || msg?.depth === -1),
[msg?.children, msg?.depth, latestMessage?.depth],
() => hasNoChildren && (msg?.depth === latestMessage?.depth || msg?.depth === -1),
[hasNoChildren, msg?.depth, latestMessage?.depth],
);
if (!msg) {
@ -122,24 +124,31 @@ const MessageRender = memo(
<h2 className={cn('select-none font-semibold', fontSize)}>{messageLabel}</h2>
<div className="flex-col gap-1 md:gap-3">
<div className="flex max-w-full flex-grow flex-col gap-0">
{msg.plugin && <Plugin plugin={msg.plugin} />}
<MessageContent
ask={ask}
edit={edit}
isLast={isLast}
text={msg.text || ''}
message={msg}
enterEdit={enterEdit}
error={!!(error ?? false)}
isSubmitting={isSubmitting}
unfinished={unfinished ?? false}
isCreatedByUser={isCreatedByUser ?? true}
siblingIdx={siblingIdx ?? 0}
setSiblingIdx={setSiblingIdx ?? (() => ({}))}
/>
<MessageContext.Provider
value={{
messageId: msg.messageId,
conversationId: conversation?.conversationId,
}}
>
{msg.plugin && <Plugin plugin={msg.plugin} />}
<MessageContent
ask={ask}
edit={edit}
isLast={isLast}
text={msg.text || ''}
message={msg}
enterEdit={enterEdit}
error={!!(error ?? false)}
isSubmitting={isSubmitting}
unfinished={unfinished ?? false}
isCreatedByUser={isCreatedByUser ?? true}
siblingIdx={siblingIdx ?? 0}
setSiblingIdx={setSiblingIdx ?? (() => ({}))}
/>
</MessageContext.Provider>
</div>
</div>
{!msg.children?.length && (isSubmittingFamily === true || isSubmitting) ? (
{hasNoChildren && (isSubmittingFamily === true || isSubmitting) ? (
<PlaceholderRow isCard={isCard} />
) : (
<SubRow classes="text-xs">

View file

@ -28,7 +28,7 @@ const SaveAsPresetDialog = ({ open, onOpenChange, preset }: TEditPresetProps) =>
createPresetMutation.mutate(_preset, {
onSuccess: () => {
showToast({
message: `${toastTitle} ${localize('com_endpoint_preset_saved')}`,
message: `${toastTitle} ${localize('com_ui_saved')}`,
});
onOpenChange(false); // Close the dialog on success
},

View file

@ -1,81 +1,133 @@
import copy from 'copy-to-clipboard';
import { InfoIcon } from 'lucide-react';
import React, { useRef, useState, RefObject } from 'react';
import { Tools } from 'librechat-data-provider';
import React, { useRef, useState, useMemo, useEffect } from 'react';
import type { CodeBarProps } from '~/common';
import LogContent from '~/components/Chat/Messages/Content/Parts/LogContent';
import ResultSwitcher from '~/components/Messages/Content/ResultSwitcher';
import { useToolCallsMapContext, useMessageContext } from '~/Providers';
import RunCode from '~/components/Messages/Content/RunCode';
import Clipboard from '~/components/svg/Clipboard';
import CheckMark from '~/components/svg/CheckMark';
import useLocalize from '~/hooks/useLocalize';
import cn from '~/utils/cn';
type CodeBarProps = {
lang: string;
codeRef: RefObject<HTMLElement>;
plugin?: boolean;
error?: boolean;
};
type CodeBlockProps = Pick<CodeBarProps, 'lang' | 'plugin' | 'error'> & {
type CodeBlockProps = Pick<
CodeBarProps,
'lang' | 'plugin' | 'error' | 'allowExecution' | 'blockIndex'
> & {
codeChildren: React.ReactNode;
classProp?: string;
};
const CodeBar: React.FC<CodeBarProps> = React.memo(({ lang, codeRef, error, plugin = null }) => {
const localize = useLocalize();
const [isCopied, setIsCopied] = useState(false);
return (
<div className="relative flex items-center rounded-tl-md rounded-tr-md bg-gray-700 px-4 py-2 font-sans text-xs text-gray-200 dark:bg-gray-700">
<span className="">{lang}</span>
{plugin === true ? (
<InfoIcon className="ml-auto flex h-4 w-4 gap-2 text-white/50" />
) : (
<button
type="button"
className={cn(
'ml-auto flex gap-2',
error === true ? 'h-4 w-4 items-start text-white/50' : '',
)}
onClick={async () => {
const codeString = codeRef.current?.textContent;
if (codeString != null) {
setIsCopied(true);
copy(codeString.trim(), { format: 'text/plain' });
const CodeBar: React.FC<CodeBarProps> = React.memo(
({ lang, error, codeRef, blockIndex, plugin = null, allowExecution = true }) => {
const localize = useLocalize();
const [isCopied, setIsCopied] = useState(false);
return (
<div className="relative flex items-center justify-between rounded-tl-md rounded-tr-md bg-gray-700 px-4 py-2 font-sans text-xs text-gray-200 dark:bg-gray-700">
<span className="">{lang}</span>
{plugin === true ? (
<InfoIcon className="ml-auto flex h-4 w-4 gap-2 text-white/50" />
) : (
<div className="flex items-center justify-center gap-4">
{allowExecution === true && (
<RunCode lang={lang} codeRef={codeRef} blockIndex={blockIndex} />
)}
<button
type="button"
className={cn(
'ml-auto flex gap-2',
error === true ? 'h-4 w-4 items-start text-white/50' : '',
)}
onClick={async () => {
const codeString = codeRef.current?.textContent;
if (codeString != null) {
setIsCopied(true);
copy(codeString.trim(), { format: 'text/plain' });
setTimeout(() => {
setIsCopied(false);
}, 3000);
}
}}
>
{isCopied ? (
<>
<CheckMark className="h-[18px] w-[18px]" />
{error === true ? '' : localize('com_ui_copied')}
</>
) : (
<>
<Clipboard />
{error === true ? '' : localize('com_ui_copy_code')}
</>
)}
</button>
)}
</div>
);
});
setTimeout(() => {
setIsCopied(false);
}, 3000);
}
}}
>
{isCopied ? (
<>
<CheckMark className="h-[18px] w-[18px]" />
{error === true ? '' : localize('com_ui_copied')}
</>
) : (
<>
<Clipboard />
{error === true ? '' : localize('com_ui_copy_code')}
</>
)}
</button>
</div>
)}
</div>
);
},
);
const CodeBlock: React.FC<CodeBlockProps> = ({
lang,
blockIndex,
codeChildren,
classProp = '',
allowExecution = true,
plugin = null,
error,
}) => {
const codeRef = useRef<HTMLElement>(null);
const toolCallsMap = useToolCallsMapContext();
const { messageId, partIndex } = useMessageContext();
const key = allowExecution
? `${messageId}_${partIndex ?? 0}_${blockIndex ?? 0}_${Tools.execute_code}`
: '';
const [currentIndex, setCurrentIndex] = useState(0);
const fetchedToolCalls = toolCallsMap?.[key];
const [toolCalls, setToolCalls] = useState(toolCallsMap?.[key] ?? null);
useEffect(() => {
if (fetchedToolCalls) {
setToolCalls(fetchedToolCalls);
setCurrentIndex(fetchedToolCalls.length - 1);
}
}, [fetchedToolCalls]);
const currentToolCall = useMemo(() => toolCalls?.[currentIndex], [toolCalls, currentIndex]);
const next = () => {
if (!toolCalls) {
return;
}
if (currentIndex < toolCalls.length - 1) {
setCurrentIndex(currentIndex + 1);
}
};
const previous = () => {
if (currentIndex > 0) {
setCurrentIndex(currentIndex - 1);
}
};
const isNonCode = !!(plugin === true || error === true);
const language = isNonCode ? 'json' : lang;
return (
<div className="w-full rounded-md bg-gray-900 text-xs text-white/80">
<CodeBar lang={lang} codeRef={codeRef} plugin={plugin === true} error={error} />
<CodeBar
lang={lang}
error={error}
codeRef={codeRef}
blockIndex={blockIndex}
plugin={plugin === true}
allowExecution={allowExecution}
/>
<div className={cn(classProp, 'overflow-y-auto p-4')}>
<code
ref={codeRef}
@ -86,6 +138,34 @@ const CodeBlock: React.FC<CodeBlockProps> = ({
{codeChildren}
</code>
</div>
{allowExecution === true && toolCalls && toolCalls.length > 0 && (
<>
<div className="bg-gray-700 p-4 text-xs">
<div
className="prose flex flex-col-reverse text-white"
style={{
color: 'white',
}}
>
<pre className="shrink-0">
<LogContent
output={(currentToolCall?.result as string | undefined) ?? ''}
attachments={currentToolCall?.attachments ?? []}
renderImages={true}
/>
</pre>
</div>
</div>
{toolCalls.length > 1 && (
<ResultSwitcher
currentIndex={currentIndex}
totalCount={toolCalls.length}
onPrevious={previous}
onNext={next}
/>
)}
</>
)}
</div>
);
};

View file

@ -0,0 +1,69 @@
interface ResultSwitcherProps {
currentIndex: number;
totalCount: number;
onPrevious: () => void;
onNext: () => void;
}
const ResultSwitcher: React.FC<ResultSwitcherProps> = ({
currentIndex,
totalCount,
onPrevious,
onNext,
}) => {
if (totalCount <= 1) {
return null;
}
return (
<div className="flex items-center justify-start gap-1 self-center bg-gray-700 pb-2 text-xs">
<button
className="hover-button rounded-md p-1 text-gray-400 hover:bg-gray-700 hover:text-gray-200 disabled:hover:text-gray-400"
type="button"
onClick={onPrevious}
disabled={currentIndex === 0}
>
<svg
stroke="currentColor"
fill="none"
strokeWidth="1.5"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<polyline points="15 18 9 12 15 6" />
</svg>
</button>
<span className="flex-shrink-0 tabular-nums">
{currentIndex + 1} / {totalCount}
</span>
<button
className="hover-button rounded-md p-1 text-gray-400 hover:bg-gray-700 hover:text-gray-200 disabled:hover:text-gray-400"
type="button"
onClick={onNext}
disabled={currentIndex === totalCount - 1}
>
<svg
stroke="currentColor"
fill="none"
strokeWidth="1.5"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<polyline points="9 18 15 12 9 6" />
</svg>
</button>
</div>
);
};
export default ResultSwitcher;

View file

@ -0,0 +1,109 @@
import debounce from 'lodash/debounce';
import { Tools, AuthType } from 'librechat-data-provider';
import { TerminalSquareIcon, Loader } from 'lucide-react';
import React, { useMemo, useCallback, useEffect } from 'react';
import type { CodeBarProps } from '~/common';
import { useVerifyAgentToolAuth, useToolCallMutation } from '~/data-provider';
import ApiKeyDialog from '~/components/SidePanel/Agents/Code/ApiKeyDialog';
import { useLocalize, useCodeApiKeyForm } from '~/hooks';
import { useMessageContext } from '~/Providers';
import { cn, normalizeLanguage } from '~/utils';
import { useToastContext } from '~/Providers';
const RunCode: React.FC<CodeBarProps> = React.memo(({ lang, codeRef, blockIndex }) => {
const localize = useLocalize();
const { showToast } = useToastContext();
const execute = useToolCallMutation(Tools.execute_code, {
onError: () => {
showToast({ message: localize('com_ui_run_code_error'), status: 'error' });
},
});
const { messageId, conversationId, partIndex } = useMessageContext();
const normalizedLang = useMemo(() => normalizeLanguage(lang), [lang]);
const { data } = useVerifyAgentToolAuth({ toolId: Tools.execute_code });
const authType = useMemo(() => data?.message ?? false, [data?.message]);
const isAuthenticated = useMemo(() => data?.authenticated ?? false, [data?.authenticated]);
const { methods, onSubmit, isDialogOpen, setIsDialogOpen, handleRevokeApiKey } =
useCodeApiKeyForm({});
const handleExecute = useCallback(async () => {
if (!isAuthenticated) {
setIsDialogOpen(true);
return;
}
const codeString: string = codeRef.current?.textContent ?? '';
if (
typeof codeString !== 'string' ||
codeString.length === 0 ||
typeof normalizedLang !== 'string' ||
normalizedLang.length === 0
) {
return;
}
execute.mutate({
partIndex,
messageId,
blockIndex,
conversationId: conversationId ?? '',
lang: normalizedLang,
code: codeString,
});
}, [
codeRef,
execute,
partIndex,
messageId,
blockIndex,
conversationId,
normalizedLang,
setIsDialogOpen,
isAuthenticated,
]);
const debouncedExecute = useMemo(
() => debounce(handleExecute, 1000, { leading: true }),
[handleExecute],
);
useEffect(() => {
return () => {
debouncedExecute.cancel();
};
}, [debouncedExecute]);
if (typeof normalizedLang !== 'string' || normalizedLang.length === 0) {
return null;
}
return (
<>
<button
type="button"
className={cn('ml-auto flex gap-2')}
onClick={debouncedExecute}
disabled={execute.isLoading}
>
{execute.isLoading ? (
<Loader className="animate-spin" size={18} />
) : (
<TerminalSquareIcon size={18} />
)}
{localize('com_ui_run_code')}
</button>
<ApiKeyDialog
onSubmit={onSubmit}
isOpen={isDialogOpen}
register={methods.register}
onRevoke={handleRevokeApiKey}
onOpenChange={setIsDialogOpen}
handleSubmit={methods.handleSubmit}
isToolAuthenticated={isAuthenticated}
isUserProvided={authType === AuthType.USER_PROVIDED}
/>
</>
);
});
export default RunCode;

View file

@ -129,16 +129,17 @@ const ContentRender = memo(
<div className="flex-col gap-1 md:gap-3">
<div className="flex max-w-full flex-grow flex-col gap-0">
<ContentParts
content={msg.content as Array<TMessageContentParts | undefined>}
messageId={msg.messageId}
isCreatedByUser={msg.isCreatedByUser}
isLast={isLast}
isSubmitting={isSubmitting}
edit={edit}
isLast={isLast}
enterEdit={enterEdit}
siblingIdx={siblingIdx}
messageId={msg.messageId}
isSubmitting={isSubmitting}
setSiblingIdx={setSiblingIdx}
attachments={msg.attachments}
isCreatedByUser={msg.isCreatedByUser}
conversationId={conversation?.conversationId}
content={msg.content as Array<TMessageContentParts | undefined>}
/>
</div>
</div>

View file

@ -29,17 +29,19 @@ const LabelController: React.FC<LabelControllerProps> = ({
setValue,
}) => (
<div className="mb-4 flex items-center justify-between gap-2">
<label
<button
className="cursor-pointer select-none"
htmlFor={promptPerm}
type="button"
// htmlFor={promptPerm}
onClick={() =>
setValue(promptPerm, !getValues(promptPerm), {
shouldDirty: true,
})
}
tabIndex={0}
>
{label}
</label>
</button>
<Controller
name={promptPerm}
control={control}
@ -48,7 +50,7 @@ const LabelController: React.FC<LabelControllerProps> = ({
{...field}
checked={field.value}
onCheckedChange={field.onChange}
value={field?.value?.toString()}
value={field.value.toString()}
/>
)}
/>
@ -61,7 +63,7 @@ const AdminSettings = () => {
const { showToast } = useToastContext();
const { mutate, isLoading } = useUpdatePromptPermissionsMutation({
onSuccess: () => {
showToast({ status: 'success', message: localize('com_endpoint_preset_saved') });
showToast({ status: 'success', message: localize('com_ui_saved') });
},
onError: () => {
showToast({ status: 'error', message: localize('com_ui_error_save_admin_settings') });

View file

@ -14,9 +14,9 @@ import {
replaceSpecialVars,
extractVariableInfo,
} from '~/utils';
import { codeNoExecution } from '~/components/Chat/Messages/Content/Markdown';
import { useAuthContext, useLocalize, useSubmitMessage } from '~/hooks';
import { TextareaAutosize, InputCombobox } from '~/components/ui';
import { code } from '~/components/Chat/Messages/Content/Markdown';
type FieldType = 'text' | 'select';
@ -143,12 +143,16 @@ export default function VariableForm({
<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">
<ReactMarkdown
/** @ts-ignore */
remarkPlugins={[supersub, remarkGfm, [remarkMath, { singleDollarTextMath: true }]]}
rehypePlugins={[
/** @ts-ignore */
[rehypeKatex, { output: 'mathml' }],
/** @ts-ignore */
[rehypeHighlight, { ignoreMissing: true }],
]}
components={{ code }}
/** @ts-ignore */
components={{ code: codeNoExecution }}
className="prose dark:prose-invert light dark:text-gray-70 my-1 max-h-[50vh] break-words"
>
{generateHighlightedMarkdown()}

View file

@ -6,7 +6,7 @@ import remarkMath from 'remark-math';
import supersub from 'remark-supersub';
import rehypeHighlight from 'rehype-highlight';
import type { TPromptGroup } from 'librechat-data-provider';
import { code } from '~/components/Chat/Messages/Content/Markdown';
import { codeNoExecution } from '~/components/Chat/Messages/Content/Markdown';
import { useLocalize, useAuthContext } from '~/hooks';
import CategoryIcon from './Groups/CategoryIcon';
import PromptVariables from './PromptVariables';
@ -50,12 +50,20 @@ const PromptDetails = ({ group }: { group?: TPromptGroup }) => {
</h2>
<div className="group relative min-h-32 rounded-b-lg border border-gray-300 p-4 transition-all duration-150 dark:border-gray-600 sm:max-w-full">
<ReactMarkdown
remarkPlugins={[supersub, remarkGfm, [remarkMath, { singleDollarTextMath: true }]]}
remarkPlugins={[
/** @ts-ignore */
supersub,
remarkGfm,
[remarkMath, { singleDollarTextMath: true }],
]}
rehypePlugins={[
/** @ts-ignore */
[rehypeKatex, { output: 'mathml' }],
/** @ts-ignore */
[rehypeHighlight, { ignoreMissing: true }],
]}
components={{ p: PromptVariableGfm, code }}
/** @ts-ignore */
components={{ p: PromptVariableGfm, code: codeNoExecution }}
className="prose dark:prose-invert light dark:text-gray-70 my-1"
>
{mainText}

View file

@ -9,8 +9,8 @@ import rehypeKatex from 'rehype-katex';
import remarkMath from 'remark-math';
import supersub from 'remark-supersub';
import ReactMarkdown from 'react-markdown';
import { codeNoExecution } from '~/components/Chat/Messages/Content/Markdown';
import AlwaysMakeProd from '~/components/Prompts/Groups/AlwaysMakeProd';
import { code } from '~/components/Chat/Messages/Content/Markdown';
import { SaveIcon, CrossIcon } from '~/components/svg';
import { TextareaAutosize } from '~/components/ui';
import { PromptVariableGfm } from './Markdown';
@ -75,7 +75,7 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
role="button"
className={cn(
'min-h-[8rem] w-full rounded-b-lg border border-border-medium p-4 transition-all duration-150',
{ 'bg-surface-secondary-alt cursor-pointer hover:bg-surface-tertiary': !isEditing },
{ 'cursor-pointer bg-surface-secondary-alt hover:bg-surface-tertiary': !isEditing },
)}
onClick={() => !isEditing && setIsEditing(true)}
onKeyDown={(e) => {
@ -107,9 +107,12 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
/>
) : (
<ReactMarkdown
/** @ts-ignore */
remarkPlugins={[supersub, remarkGfm, [remarkMath, { singleDollarTextMath: true }]]}
/** @ts-ignore */
rehypePlugins={rehypePlugins}
components={{ p: PromptVariableGfm, code }}
/** @ts-ignore */
components={{ p: PromptVariableGfm, code: codeNoExecution }}
className="markdown prose dark:prose-invert light my-1 w-full break-words text-text-primary"
>
{field.value}

View file

@ -53,6 +53,7 @@ const PromptVariables = ({
) : (
<div className="flex h-7 items-center">
<span className="text-xs text-text-secondary md:text-sm">
{/** @ts-ignore */}
<ReactMarkdown components={{ code: CodeVariableGfm }}>
{localize('com_ui_variables_info')}
</ReactMarkdown>
@ -68,6 +69,7 @@ const PromptVariables = ({
</span>
{'\u00A0'}
<span className="text-xs text-text-secondary md:text-sm">
{/** @ts-ignore */}
<ReactMarkdown components={{ code: CodeVariableGfm }}>
{localize('com_ui_special_variables_info')}
</ReactMarkdown>
@ -79,6 +81,7 @@ const PromptVariables = ({
</span>
{'\u00A0'}
<span className="text-xs text-text-secondary md:text-sm">
{/** @ts-ignore */}
<ReactMarkdown components={{ code: CodeVariableGfm }}>
{localize('com_ui_dropdown_variables_info')}
</ReactMarkdown>

View file

@ -6,6 +6,7 @@ import SearchContent from '~/components/Chat/Messages/Content/SearchContent';
import SiblingSwitch from '~/components/Chat/Messages/SiblingSwitch';
import { Plugin } from '~/components/Messages/Content';
import SubRow from '~/components/Chat/Messages/SubRow';
import { MessageContext } from '~/Providers';
// eslint-disable-next-line import/no-cycle
import MultiMessage from './MultiMessage';
import { cn } from '~/utils';
@ -31,10 +32,10 @@ export default function Message(props: TMessageProps) {
const {
text = '',
children,
messageId = null,
isCreatedByUser = true,
error = false,
messageId = '',
unfinished = false,
isCreatedByUser = true,
} = message;
let messageLabel = '';
@ -64,26 +65,33 @@ export default function Message(props: TMessageProps) {
<div className={cn('select-none font-semibold', fontSize)}>{messageLabel}</div>
<div className="flex-col gap-1 md:gap-3">
<div className="flex max-w-full flex-grow flex-col gap-0">
{/* Legacy Plugins */}
{message.plugin && <Plugin plugin={message.plugin} />}
{message.content ? (
<SearchContent message={message} />
) : (
<MessageContent
edit={false}
error={error}
isLast={false}
ask={() => ({})}
text={text}
message={message}
isSubmitting={false}
enterEdit={() => ({})}
unfinished={!!unfinished}
isCreatedByUser={isCreatedByUser}
siblingIdx={siblingIdx ?? 0}
setSiblingIdx={setSiblingIdx ?? (() => ({}))}
/>
)}
<MessageContext.Provider
value={{
messageId,
conversationId: conversation?.conversationId,
}}
>
{/* Legacy Plugins */}
{message.plugin && <Plugin plugin={message.plugin} />}
{message.content ? (
<SearchContent message={message} />
) : (
<MessageContent
edit={false}
error={error}
isLast={false}
ask={() => ({})}
text={text || ''}
message={message}
isSubmitting={false}
enterEdit={() => ({})}
unfinished={unfinished}
siblingIdx={siblingIdx ?? 0}
isCreatedByUser={isCreatedByUser}
setSiblingIdx={setSiblingIdx ?? (() => ({}))}
/>
)}
</MessageContext.Provider>
</div>
</div>
<SubRow classes="text-xs">

View file

@ -0,0 +1,163 @@
import { useMemo, useEffect } from 'react';
import { ShieldEllipsis } from 'lucide-react';
import { useForm, Controller } from 'react-hook-form';
import { Permissions, SystemRoles, roleDefaults, PermissionTypes } from 'librechat-data-provider';
import type { Control, UseFormSetValue, UseFormGetValues } from 'react-hook-form';
import { OGDialog, OGDialogTitle, OGDialogContent, OGDialogTrigger } from '~/components/ui';
import { useUpdateAgentPermissionsMutation } from '~/data-provider';
import { useLocalize, useAuthContext } from '~/hooks';
import { Button, Switch } from '~/components/ui';
import { useToastContext } from '~/Providers';
type FormValues = Record<Permissions, boolean>;
type LabelControllerProps = {
label: string;
agentPerm: Permissions;
control: Control<FormValues, unknown, FormValues>;
setValue: UseFormSetValue<FormValues>;
getValues: UseFormGetValues<FormValues>;
};
const defaultValues = roleDefaults[SystemRoles.USER];
const LabelController: React.FC<LabelControllerProps> = ({
control,
agentPerm,
label,
getValues,
setValue,
}) => (
<div className="mb-4 flex items-center justify-between gap-2">
<button
className="cursor-pointer select-none"
type="button"
onClick={() =>
setValue(agentPerm, !getValues(agentPerm), {
shouldDirty: true,
})
}
tabIndex={0}
>
{label}
</button>
<Controller
name={agentPerm}
control={control}
render={({ field }) => (
<Switch
{...field}
checked={field.value}
onCheckedChange={field.onChange}
value={field.value.toString()}
/>
)}
/>
</div>
);
const AdminSettings = () => {
const localize = useLocalize();
const { user, roles } = useAuthContext();
const { showToast } = useToastContext();
const { mutate, isLoading } = useUpdateAgentPermissionsMutation({
onSuccess: () => {
showToast({ status: 'success', message: localize('com_ui_saved') });
},
onError: () => {
showToast({ status: 'error', message: localize('com_ui_error_save_admin_settings') });
},
});
const {
reset,
control,
setValue,
getValues,
handleSubmit,
formState: { isSubmitting },
} = useForm<FormValues>({
mode: 'onChange',
defaultValues: useMemo(() => {
if (roles?.[SystemRoles.USER]) {
return roles[SystemRoles.USER][PermissionTypes.AGENTS];
}
return defaultValues[PermissionTypes.AGENTS];
}, [roles]),
});
useEffect(() => {
if (roles?.[SystemRoles.USER]?.[PermissionTypes.AGENTS]) {
reset(roles[SystemRoles.USER][PermissionTypes.AGENTS]);
}
}, [roles, reset]);
if (user?.role !== SystemRoles.ADMIN) {
return null;
}
const labelControllerData = [
{
agentPerm: Permissions.SHARED_GLOBAL,
label: localize('com_ui_agents_allow_share_global'),
},
{
agentPerm: Permissions.USE,
label: localize('com_ui_agents_allow_use'),
},
{
agentPerm: Permissions.CREATE,
label: localize('com_ui_agents_allow_create'),
},
];
const onSubmit = (data: FormValues) => {
mutate({ roleName: SystemRoles.USER, updates: data });
};
return (
<OGDialog>
<OGDialogTrigger asChild>
<Button
size={'sm'}
variant={'outline'}
className="btn btn-neutral border-token-border-light relative my-1 h-9 w-full rounded-lg font-medium"
>
<ShieldEllipsis className="cursor-pointer" />
{localize('com_ui_admin_settings')}
</Button>
</OGDialogTrigger>
<OGDialogContent className="w-1/4 bg-white dark:border-gray-700 dark:bg-gray-850 dark:text-gray-300">
<OGDialogTitle>{`${localize('com_ui_admin_settings')} - ${localize(
'com_ui_agents',
)}`}</OGDialogTitle>
<form className="p-2" onSubmit={handleSubmit(onSubmit)}>
<div className="py-5">
{labelControllerData.map(({ agentPerm, label }) => (
<LabelController
key={agentPerm}
control={control}
agentPerm={agentPerm}
label={label}
getValues={getValues}
setValue={setValue}
/>
))}
</div>
<div className="flex justify-end">
<button
type="submit"
disabled={isSubmitting || isLoading}
className="btn rounded bg-green-500 font-bold text-white transition-all hover:bg-green-600"
>
{localize('com_ui_save')}
</button>
</div>
</form>
</OGDialogContent>
</OGDialog>
);
};
export default AdminSettings;

View file

@ -1,24 +1,32 @@
import React, { useState, useMemo, useCallback } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { Controller, useWatch, useFormContext } from 'react-hook-form';
import { QueryKeys, AgentCapabilities, EModelEndpoint, SystemRoles } from 'librechat-data-provider';
import {
QueryKeys,
SystemRoles,
Permissions,
EModelEndpoint,
PermissionTypes,
AgentCapabilities,
} from 'librechat-data-provider';
import type { TConfig, TPlugin } from 'librechat-data-provider';
import type { AgentForm, AgentPanelProps } from '~/common';
import { cn, defaultTextProps, removeFocusOutlines, getEndpointField, getIconKey } from '~/utils';
import { useCreateAgentMutation, useUpdateAgentMutation } from '~/data-provider';
import { useLocalize, useAuthContext, useHasAccess } from '~/hooks';
import { useToastContext, useFileMapContext } from '~/Providers';
import { icons } from '~/components/Chat/Menus/Endpoints/Icons';
import Action from '~/components/SidePanel/Builder/Action';
import { ToolSelectDialog } from '~/components/Tools';
import { useLocalize, useAuthContext } from '~/hooks';
import { processAgentOption } from '~/utils';
import AdminSettings from './AdminSettings';
import { Spinner } from '~/components/svg';
import DeleteButton from './DeleteButton';
import AgentAvatar from './AgentAvatar';
import FileSearch from './FileSearch';
import ShareAgent from './ShareAgent';
import AgentTool from './AgentTool';
// import CodeForm from './Code/Form';
import CodeForm from './Code/Form';
import { Panel } from '~/common';
const labelClass = 'mb-2 text-token-text-primary block font-medium';
@ -55,6 +63,11 @@ export default function AgentConfig({
const tools = useWatch({ control, name: 'tools' });
const agent_id = useWatch({ control, name: 'id' });
const hasAccessToShareAgents = useHasAccess({
permissionType: PermissionTypes.AGENTS,
permission: Permissions.SHARED_GLOBAL,
});
const toolsEnabled = useMemo(
() => agentsConfig?.capabilities?.includes(AgentCapabilities.tools),
[agentsConfig],
@ -263,7 +276,7 @@ export default function AgentConfig({
/>
</div>
{/* Instructions */}
<div className="mb-6">
<div className="mb-4">
<label className={labelClass} htmlFor="instructions">
{localize('com_ui_instructions')}
</label>
@ -275,7 +288,7 @@ export default function AgentConfig({
<textarea
{...field}
value={field.value ?? ''}
maxLength={32768}
// maxLength={32768}
className={cn(inputClass, 'min-h-[100px] resize-y')}
id="instructions"
placeholder={localize('com_agents_instructions_placeholder')}
@ -297,7 +310,7 @@ export default function AgentConfig({
/>
</div>
{/* Model and Provider */}
<div className="mb-6">
<div className="mb-4">
<label className={labelClass} htmlFor="provider">
{localize('com_ui_model')} <span className="text-red-500">*</span>
</label>
@ -319,16 +332,23 @@ export default function AgentConfig({
/>
</div>
)}
<span>{model != null ? model : localize('com_ui_select_model')}</span>
<span>{model != null && model ? model : localize('com_ui_select_model')}</span>
</div>
</button>
</div>
{/* Code Execution */}
{/* {codeEnabled && <CodeForm agent_id={agent_id} files={code_files} />} */}
{/* File Search */}
{fileSearchEnabled && <FileSearch agent_id={agent_id} files={knowledge_files} />}
{(codeEnabled || fileSearchEnabled) && (
<div className="mb-4 flex w-full flex-col items-start gap-3">
<label className="text-token-text-primary block font-medium">
{localize('com_assistants_capabilities')}
</label>
{/* Code Execution */}
{codeEnabled && <CodeForm agent_id={agent_id} files={code_files} />}
{/* File Search */}
{fileSearchEnabled && <FileSearch agent_id={agent_id} files={knowledge_files} />}
</div>
)}
{/* Agent Tools & Actions */}
<div className="mb-6">
<div className="mb-4">
<label className={labelClass}>
{`${toolsEnabled === true ? localize('com_ui_tools') : ''}
${toolsEnabled === true && actionsEnabled === true ? ' + ' : ''}
@ -360,7 +380,7 @@ export default function AgentConfig({
<button
type="button"
onClick={() => setShowToolDialog(true)}
className="btn btn-neutral border-token-border-light relative h-8 w-full rounded-lg font-medium"
className="btn btn-neutral border-token-border-light relative h-9 w-full rounded-lg font-medium"
aria-haspopup="dialog"
>
<div className="flex w-full items-center justify-center gap-2">
@ -373,7 +393,7 @@ export default function AgentConfig({
type="button"
disabled={!agent_id}
onClick={handleAddActions}
className="btn btn-neutral border-token-border-light relative h-8 w-full rounded-lg font-medium"
className="btn btn-neutral border-token-border-light relative h-9 w-full rounded-lg font-medium"
aria-haspopup="dialog"
>
<div className="flex w-full items-center justify-center gap-2">
@ -384,6 +404,7 @@ export default function AgentConfig({
</div>
</div>
</div>
{user?.role === SystemRoles.ADMIN && <AdminSettings />}
{/* Context Button */}
<div className="flex items-center justify-end gap-2">
<DeleteButton
@ -391,7 +412,8 @@ export default function AgentConfig({
setCurrentAgentId={setCurrentAgentId}
createMutation={create}
/>
{(agent?.author === user?.id || user?.role === SystemRoles.ADMIN) && (
{(agent?.author === user?.id || user?.role === SystemRoles.ADMIN) &&
hasAccessToShareAgents && (
<ShareAgent
agent_id={agent_id}
agentName={agent?.name ?? ''}
@ -401,7 +423,7 @@ export default function AgentConfig({
)}
{/* Submit Button */}
<button
className="btn btn-primary focus:shadow-outline flex w-full items-center justify-center px-4 py-2 font-semibold text-white hover:bg-green-600 focus:border-green-500"
className="btn btn-primary focus:shadow-outline flex h-9 w-full items-center justify-center px-4 py-2 font-semibold text-white hover:bg-green-600 focus:border-green-500"
type="submit"
disabled={create.isLoading || update.isLoading}
aria-busy={create.isLoading || update.isLoading}

View file

@ -126,6 +126,9 @@ export default function AgentPanel({
model: _model,
model_parameters,
provider: _provider,
agent_ids,
end_after_tools,
hide_sequential_outputs,
} = data;
const model = _model ?? '';
@ -143,6 +146,9 @@ export default function AgentPanel({
tools,
provider,
model_parameters,
agent_ids,
end_after_tools,
hide_sequential_outputs,
},
});
return;
@ -163,6 +169,9 @@ export default function AgentPanel({
tools,
provider,
model_parameters,
agent_ids,
end_after_tools,
hide_sequential_outputs,
});
},
[agent_id, create, update, showToast, localize],

View file

@ -58,6 +58,8 @@ export default function AgentSelect({
const capabilities: TAgentCapabilities = {
[AgentCapabilities.execute_code]: false,
[AgentCapabilities.file_search]: false,
[AgentCapabilities.end_after_tools]: false,
[AgentCapabilities.hide_sequential_outputs]: false,
};
const agentTools: string[] = [];

View file

@ -1,38 +1,39 @@
import { useState } from 'react';
import { KeyRoundIcon } from 'lucide-react';
import { AuthType, AgentCapabilities } from 'librechat-data-provider';
import { useFormContext, Controller, useForm, useWatch } from 'react-hook-form';
import { useFormContext, Controller, useWatch } from 'react-hook-form';
import type { AgentForm } from '~/common';
import {
Input,
OGDialog,
Checkbox,
HoverCard,
HoverCardContent,
HoverCardPortal,
HoverCardTrigger,
Button,
} from '~/components/ui';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { useLocalize, useAuthCodeTool } from '~/hooks';
import { useLocalize, useCodeApiKeyForm } from '~/hooks';
import { CircleHelpIcon } from '~/components/svg';
import ApiKeyDialog from './ApiKeyDialog';
import { ESide } from '~/common';
type ApiKeyFormData = {
apiKey: string;
authType?: string | AuthType;
};
export default function Action({ authType = '', isToolAuthenticated = false }) {
const localize = useLocalize();
const methods = useFormContext<AgentForm>();
const { control, setValue, getValues } = methods;
const [isDialogOpen, setIsDialogOpen] = useState(false);
const {
onSubmit,
isDialogOpen,
setIsDialogOpen,
handleRevokeApiKey,
methods: keyFormMethods,
} = useCodeApiKeyForm({
onSubmit: () => {
setValue(AgentCapabilities.execute_code, true, { shouldDirty: true });
},
onRevoke: () => {
setValue(AgentCapabilities.execute_code, false, { shouldDirty: true });
},
});
const runCodeIsEnabled = useWatch({ control, name: AgentCapabilities.execute_code });
const { installTool, removeTool } = useAuthCodeTool({ isEntityTool: true });
const { reset, register, handleSubmit } = useForm<ApiKeyFormData>();
const isUserProvided = authType === AuthType.USER_PROVIDED;
const handleCheckboxChange = (checked: boolean) => {
@ -45,18 +46,6 @@ export default function Action({ authType = '', isToolAuthenticated = false }) {
}
};
const onSubmit = (data: { apiKey: string }) => {
reset();
installTool(data.apiKey);
setIsDialogOpen(false);
};
const handleRevokeApiKey = () => {
reset();
removeTool();
setIsDialogOpen(false);
};
return (
<>
<HoverCard openDelay={50}>
@ -87,7 +76,7 @@ export default function Action({ authType = '', isToolAuthenticated = false }) {
className="form-check-label text-token-text-primary w-full cursor-pointer"
htmlFor={AgentCapabilities.execute_code}
>
{localize('com_agents_execute_code')}
{localize('com_ui_run_code')}
</label>
</button>
<div className="ml-2 flex gap-2">
@ -104,48 +93,23 @@ export default function Action({ authType = '', isToolAuthenticated = false }) {
<HoverCardContent side={ESide.Top} className="w-80">
<div className="space-y-2">
<p className="text-sm text-text-secondary">
{/* // TODO: add a Code Interpreter description */}
{localize('com_agents_code_interpreter')}
</p>
</div>
</HoverCardContent>
</HoverCardPortal>
</div>
</HoverCard>
<OGDialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<OGDialogTemplate
className="w-11/12 sm:w-1/4"
title={localize('com_agents_tool_not_authenticated')}
main={
<form onSubmit={handleSubmit(onSubmit)}>
<Input
type="password"
placeholder="Enter API Key"
autoComplete="one-time-code"
readOnly={true}
onFocus={(e) => (e.target.readOnly = false)}
{...register('apiKey', { required: true })}
/>
</form>
}
selection={{
selectHandler: handleSubmit(onSubmit),
selectClasses: 'bg-green-500 hover:bg-green-600 text-white',
selectText: localize('com_ui_save'),
}}
buttons={
isUserProvided &&
isToolAuthenticated && (
<Button
onClick={handleRevokeApiKey}
className="bg-destructive text-white transition-all duration-200 hover:bg-destructive/80"
>
{localize('com_ui_revoke')}
</Button>
)
}
showCancelButton={true}
/>
</OGDialog>
<ApiKeyDialog
isOpen={isDialogOpen}
onSubmit={onSubmit}
onRevoke={handleRevokeApiKey}
onOpenChange={setIsDialogOpen}
register={keyFormMethods.register}
isToolAuthenticated={isToolAuthenticated}
handleSubmit={keyFormMethods.handleSubmit}
isUserProvided={authType === AuthType.USER_PROVIDED}
/>
</>
);
}

View file

@ -0,0 +1,106 @@
import type { UseFormRegister, UseFormHandleSubmit } from 'react-hook-form';
import type { ApiKeyFormData } from '~/common';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { Input, Button, OGDialog } from '~/components/ui';
import { useLocalize } from '~/hooks';
export default function ApiKeyDialog({
isOpen,
onSubmit,
onRevoke,
onOpenChange,
isUserProvided,
isToolAuthenticated,
register,
handleSubmit,
}: {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
onSubmit: (data: { apiKey: string }) => void;
onRevoke: () => void;
isUserProvided: boolean;
isToolAuthenticated: boolean;
register: UseFormRegister<ApiKeyFormData>;
handleSubmit: UseFormHandleSubmit<ApiKeyFormData>;
}) {
const localize = useLocalize();
const languageIcons = [
'python.svg',
'nodedotjs.svg',
'tsnode.svg',
'rust.svg',
'go.svg',
'c.svg',
'cplusplus.svg',
'php.svg',
'fortran.svg',
];
return (
<OGDialog open={isOpen} onOpenChange={onOpenChange}>
<OGDialogTemplate
className="w-11/12 sm:w-[450px]"
title=""
main={
<>
<div className="mb-4 text-center font-medium">
{localize('com_ui_librechat_code_api_title')}
</div>
<div className="mb-4 text-center text-sm">
{localize('com_ui_librechat_code_api_subtitle')}
</div>
{/* Language Icons Stack */}
<div className="mb-6">
<div className="mx-auto mb-4 flex max-w-[400px] flex-wrap justify-center gap-3">
{languageIcons.map((icon) => (
<div key={icon} className="h-6 w-6">
<img
src={`/assets/${icon}`}
alt=""
className="h-full w-full object-contain opacity-[0.85] dark:invert"
/>
</div>
))}
</div>
<a
href="https://code.librechat.ai/pricing"
target="_blank"
rel="noopener noreferrer"
className="block text-center text-[15px] font-medium text-blue-500 underline decoration-1 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300"
>
{localize('com_ui_librechat_code_api_key')}
</a>
</div>
<form onSubmit={handleSubmit(onSubmit)}>
<Input
type="password"
placeholder={localize('com_ui_enter_api_key')}
autoComplete="one-time-code"
readOnly={true}
onFocus={(e) => (e.target.readOnly = false)}
{...register('apiKey', { required: true })}
/>
</form>
</>
}
selection={{
selectHandler: handleSubmit(onSubmit),
selectClasses: 'bg-green-500 hover:bg-green-600 text-white',
selectText: localize('com_ui_save'),
}}
buttons={
isUserProvided &&
isToolAuthenticated && (
<Button
onClick={onRevoke}
className="bg-destructive text-white transition-all duration-200 hover:bg-destructive/80"
>
{localize('com_ui_revoke')}
</Button>
)
}
showCancelButton={true}
/>
</OGDialog>
);
}

View file

@ -12,6 +12,7 @@ import type { ExtendedFile, AgentForm } from '~/common';
import { useFileHandling, useLocalize, useLazyEffect } from '~/hooks';
import FileRow from '~/components/Chat/Input/Files/FileRow';
import { useGetFileConfig } from '~/data-provider';
import { AttachmentIcon } from '~/components/svg';
import { useChatContext } from '~/Providers';
const tool_resource = EToolResources.execute_code;
@ -68,8 +69,8 @@ export default function Files({
return (
<div className="mb-2 w-full">
<div className="flex flex-col gap-4">
<div className="text-token-text-tertiary rounded-lg text-xs">
<div className="flex flex-col gap-3">
<div className="rounded-lg text-xs text-text-secondary">
{localize('com_assistants_code_interpreter_files')}
</div>
<FileRow
@ -85,10 +86,10 @@ export default function Files({
<button
type="button"
disabled={!agent_id || codeChecked === false}
className="btn btn-neutral border-token-border-light relative h-8 w-full rounded-lg font-medium"
className="btn btn-neutral border-token-border-light relative h-9 w-full rounded-lg font-medium"
onClick={handleButtonClick}
>
<div className="flex w-full items-center justify-center gap-2">
<div className="flex w-full items-center justify-center gap-1">
<input
multiple={true}
type="file"
@ -98,7 +99,8 @@ export default function Files({
disabled={!agent_id || codeChecked === false}
onChange={handleFileChange}
/>
{localize('com_ui_upload_files')}
<AttachmentIcon className="text-token-text-primary h-4 w-4" />
{localize('com_ui_upload_code_files')}
</div>
</button>
</div>

View file

@ -16,13 +16,18 @@ export default function CodeForm({
const { data } = useVerifyAgentToolAuth({ toolId: Tools.execute_code });
return (
<div className="mb-4">
<div className="mb-1.5 flex items-center">
<span>
<label className="text-token-text-primary block font-medium">
{localize('com_assistants_capabilities')}
</label>
</span>
<div className="w-full">
<div className="mb-1.5 flex items-center gap-2">
<div className="flex flex-row items-center gap-1">
<div className="flex items-center gap-1">
<span className="text-token-text-primary block font-medium">
{localize('com_agents_code_interpreter_title')}
</span>
<span className="text-xs text-text-secondary">
{localize('com_agents_by_librechat')}
</span>
</div>
</div>
</div>
<div className="flex flex-col items-start gap-2">
<Action authType={data?.message} isToolAuthenticated={data?.authenticated} />

View file

@ -67,7 +67,7 @@ export default function FileSearch({
};
return (
<div className="mb-6">
<div className="w-full">
<div className="mb-1.5 flex items-center gap-2">
<span>
<label className="text-token-text-primary block font-medium">
@ -76,12 +76,12 @@ export default function FileSearch({
</span>
</div>
<FileSearchCheckbox />
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-3">
<div>
<button
type="button"
disabled={!agent_id || fileSearchChecked === false}
className="btn btn-neutral border-token-border-light relative h-8 w-full rounded-lg font-medium"
className="btn btn-neutral border-token-border-light relative h-9 w-full rounded-lg font-medium"
onClick={handleButtonClick}
>
<div className="flex w-full items-center justify-center gap-1">
@ -95,13 +95,13 @@ export default function FileSearch({
disabled={!agent_id || fileSearchChecked === false}
onChange={handleFileChange}
/>
{localize('com_ui_upload_files')}
{localize('com_ui_upload_file_search')}
</div>
</button>
</div>
{/* Disabled Message */}
{agent_id ? null : (
<div className="text-sm text-text-secondary">
<div className="text-xs text-text-secondary">
{localize('com_agents_file_search_disabled')}
</div>
)}

View file

@ -31,14 +31,17 @@ export default function Parameters({
: (providerOption as StringOption | undefined)?.value;
return value ?? '';
}, [providerOption]);
const models = useMemo(() => (provider ? modelsData[provider] : []), [modelsData, provider]);
const models = useMemo(
() => (provider ? modelsData[provider] ?? [] : []),
[modelsData, provider],
);
useEffect(() => {
const _model = model ?? '';
if (provider && _model) {
const modelExists = models.includes(_model);
if (!modelExists) {
const newModels = modelsData[provider];
const newModels = modelsData[provider] ?? [];
setValue('model', newModels[0] ?? '');
}
}
@ -105,14 +108,16 @@ export default function Parameters({
<SelectDropDown
emptyTitle={true}
value={field.value ?? ''}
title={localize('com_ui_provider')}
placeholder={localize('com_ui_select_provider')}
searchPlaceholder={localize('com_ui_select_search_provider')}
setValue={field.onChange}
availableValues={providers}
showAbove={false}
showLabel={false}
className={cn(
cardStyle,
'flex h-[40px] w-full flex-none items-center justify-center border-none px-4 hover:cursor-pointer',
'flex h-9 w-full flex-none items-center justify-center border-none px-4 hover:cursor-pointer',
(field.value === undefined || field.value === '') &&
'border-2 border-yellow-400',
)}

View file

@ -0,0 +1,74 @@
import { AgentCapabilities } from 'librechat-data-provider';
import { useFormContext, Controller } from 'react-hook-form';
import type { AgentForm } from '~/common';
import {
Checkbox,
HoverCard,
// HoverCardContent,
// HoverCardPortal,
// HoverCardTrigger,
} from '~/components/ui';
// import { CircleHelpIcon } from '~/components/svg';
// import { useLocalize } from '~/hooks';
// import { ESide } from '~/common';
export default function HideSequential() {
// const localize = useLocalize();
const methods = useFormContext<AgentForm>();
const { control, setValue, getValues } = methods;
return (
<>
<HoverCard openDelay={50}>
<div className="my-2 flex items-center">
<Controller
name={AgentCapabilities.hide_sequential_outputs}
control={control}
render={({ field }) => (
<Checkbox
{...field}
checked={field.value}
onCheckedChange={field.onChange}
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
value={field.value?.toString()}
/>
)}
/>
<button
type="button"
className="flex items-center space-x-2"
onClick={() =>
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
setValue(
AgentCapabilities.hide_sequential_outputs,
!getValues(AgentCapabilities.hide_sequential_outputs),
{
shouldDirty: true,
},
)
}
>
<label
className="form-check-label text-token-text-primary w-full cursor-pointer"
htmlFor={AgentCapabilities.hide_sequential_outputs}
>
Hide Sequential Agent Outputs except the last agent&apos;s
</label>
{/* <HoverCardTrigger>
<CircleHelpIcon className="h-5 w-5 text-gray-500" />
</HoverCardTrigger> */}
</button>
{/* <HoverCardPortal>
<HoverCardContent side={ESide.Top} className="w-80">
<div className="space-y-2">
<p className="text-sm text-text-secondary">
{localize('com_agents_ttg_info')}
</p>
</div>
</HoverCardContent>
</HoverCardPortal> */}
</div>
</HoverCard>
</>
);
}

View file

@ -0,0 +1,153 @@
import { Plus, X } from 'lucide-react';
import React, { useRef, useState } from 'react';
import { Transition } from 'react-transition-group';
import { Constants } from 'librechat-data-provider';
import { cn, defaultTextProps, removeFocusOutlines } from '~/utils';
import { TooltipAnchor } from '~/components/ui';
import HideSequential from './HideSequential';
interface SequentialAgentsProps {
field: {
value: string[];
onChange: (value: string[]) => void;
};
}
const labelClass = 'mb-2 text-token-text-primary block font-medium';
const inputClass = cn(
defaultTextProps,
'flex w-full px-3 py-2 dark:border-gray-800 dark:bg-gray-800 rounded-xl mb-2',
removeFocusOutlines,
);
const maxAgents = 5;
const SequentialAgents: React.FC<SequentialAgentsProps> = ({ field }) => {
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
const nodeRef = useRef(null);
const [newAgentId, setNewAgentId] = useState('');
const handleAddAgentId = () => {
if (newAgentId.trim() && field.value.length < maxAgents) {
const newValues = [...field.value, newAgentId];
field.onChange(newValues);
setNewAgentId('');
}
};
const handleDeleteAgentId = (index: number) => {
const newValues = field.value.filter((_, i) => i !== index);
field.onChange(newValues);
};
const defaultStyle = {
transition: 'opacity 200ms ease-in-out',
opacity: 0,
};
const triggerShake = (element: HTMLElement) => {
element.classList.remove('shake');
void element.offsetWidth;
element.classList.add('shake');
setTimeout(() => {
element.classList.remove('shake');
}, 200);
};
const transitionStyles = {
entering: { opacity: 1 },
entered: { opacity: 1 },
exiting: { opacity: 0 },
exited: { opacity: 0 },
};
const hasReachedMax = field.value.length >= Constants.MAX_CONVO_STARTERS;
return (
<div className="relative">
<label className={labelClass} htmlFor="agent_ids">
Sequential Agents
</label>
<div className="mt-4 space-y-2">
<HideSequential />
{/* Display existing agents first */}
{field.value.map((agentId, index) => (
<div key={index} className="relative">
<input
ref={(el) => (inputRefs.current[index] = el)}
value={agentId}
onChange={(e) => {
const newValue = [...field.value];
newValue[index] = e.target.value;
field.onChange(newValue);
}}
className={`${inputClass} pr-10`}
type="text"
maxLength={64}
/>
<TooltipAnchor
side="top"
description={'Remove agent ID'}
className="absolute right-1 top-1 flex size-7 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover"
onClick={() => handleDeleteAgentId(index)}
>
<X className="size-4" />
</TooltipAnchor>
</div>
))}
{/* Input for new agent at the bottom */}
<div className="relative">
<input
ref={(el) => (inputRefs.current[field.value.length] = el)}
value={newAgentId}
maxLength={64}
className={`${inputClass} pr-10`}
type="text"
placeholder={hasReachedMax ? 'Max agents reached' : 'Enter agent ID (e.g. agent_1234)'}
onChange={(e) => setNewAgentId(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
if (hasReachedMax) {
triggerShake(e.currentTarget);
} else {
handleAddAgentId();
}
}
}}
/>
<Transition
nodeRef={nodeRef}
in={field.value.length < Constants.MAX_CONVO_STARTERS}
timeout={200}
unmountOnExit
>
{(state: string) => (
<div
ref={nodeRef}
style={{
...defaultStyle,
...transitionStyles[state as keyof typeof transitionStyles],
transition: state === 'entering' ? 'none' : defaultStyle.transition,
}}
className="absolute right-1 top-1"
>
<TooltipAnchor
side="top"
description={hasReachedMax ? 'Max agents reached' : 'Add agent ID'}
className="flex size-7 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover"
onClick={handleAddAgentId}
disabled={hasReachedMax}
>
<Plus className="size-4" />
</TooltipAnchor>
</div>
)}
</Transition>
</div>
</div>
</div>
);
};
export default SequentialAgents;

View file

@ -76,7 +76,7 @@ export default function CodeFiles({
<button
type="button"
disabled={!assistant_id}
className="btn btn-neutral border-token-border-light relative h-8 w-full rounded-lg font-medium"
className="btn btn-neutral border-token-border-light relative h-9 w-full rounded-lg font-medium"
onClick={handleButtonClick}
>
<div className="flex w-full items-center justify-center gap-2">

View file

@ -32,6 +32,7 @@ export default function Nav({ links, isCollapsed, resize, defaultActive }: NavPr
<TooltipAnchor
description={localize(link.title)}
side="left"
key={`nav-link-${index}`}
render={
<Button
variant="ghost"

View file

@ -6,8 +6,8 @@ import {
useGetStartupConfig,
useUserKeyQuery,
} from 'librechat-data-provider/react-query';
import type { TEndpointsConfig, TInterfaceConfig } from 'librechat-data-provider';
import type { ImperativePanelHandle } from 'react-resizable-panels';
import type { TEndpointsConfig } from 'librechat-data-provider';
import { ResizableHandleAlt, ResizablePanel, ResizablePanelGroup } from '~/components/ui/Resizable';
import { useMediaQuery, useLocalStorage, useLocalize } from '~/hooks';
import useSideNavLinks from '~/hooks/Nav/useSideNavLinks';
@ -65,7 +65,7 @@ const SidePanel = ({
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
const { data: startupConfig } = useGetStartupConfig();
const interfaceConfig = useMemo(
() => startupConfig?.interface ?? defaultInterface,
() => (startupConfig?.interface ?? defaultInterface) as Partial<TInterfaceConfig>,
[startupConfig],
);
@ -117,17 +117,17 @@ const SidePanel = ({
});
const calculateLayout = useCallback(() => {
if (!artifacts) {
if (artifacts == null) {
const navSize = defaultLayout.length === 2 ? defaultLayout[1] : defaultLayout[2];
return [100 - navSize, navSize];
} else {
const navSize = Math.max(minSize, navCollapsedSize);
const navSize = 0;
const remainingSpace = 100 - navSize;
const newMainSize = Math.floor(remainingSpace / 2);
const artifactsSize = remainingSpace - newMainSize;
return [newMainSize, artifactsSize, navSize];
}
}, [artifacts, defaultLayout, minSize, navCollapsedSize]);
}, [artifacts, defaultLayout]);
const currentLayout = useMemo(() => normalizeLayout(calculateLayout()), [calculateLayout]);
@ -261,7 +261,7 @@ const SidePanel = ({
: 'opacity-100',
)}
>
{interfaceConfig.modelSelect && (
{interfaceConfig.modelSelect === true && (
<div
className={cn(
'sticky left-0 right-0 top-0 z-[100] flex h-[52px] flex-wrap items-center justify-center bg-background',

View file

@ -1,5 +1,6 @@
import React from 'react';
import * as Ariakit from '@ariakit/react';
import { cn } from '~/utils';
interface DropdownProps {
trigger: React.ReactNode;
@ -15,11 +16,21 @@ interface DropdownProps {
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
className?: string;
iconClassName?: string;
anchor?: { x: string; y: string };
modal?: boolean;
menuId: string;
}
const DropdownPopup: React.FC<DropdownProps> = ({ trigger, items, isOpen, setIsOpen, menuId }) => {
const DropdownPopup: React.FC<DropdownProps> = ({
trigger,
items,
isOpen,
setIsOpen,
menuId,
modal,
iconClassName,
}) => {
const menu = Ariakit.useMenuStore({ open: isOpen, setOpen: setIsOpen });
return (
@ -27,8 +38,9 @@ const DropdownPopup: React.FC<DropdownProps> = ({ trigger, items, isOpen, setIsO
{trigger}
<Ariakit.Menu
id={menuId}
className="z-50 mt-2 overflow-hidden rounded-lg bg-header-primary p-1.5 shadow-lg outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
className="absolute z-50 mt-2 overflow-hidden rounded-lg bg-header-primary p-1.5 shadow-lg outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
gutter={8}
modal={modal}
>
{items
.filter((item) => item.show !== false)
@ -49,7 +61,7 @@ const DropdownPopup: React.FC<DropdownProps> = ({ trigger, items, isOpen, setIsO
}}
>
{item.icon != null && (
<span className="mr-2 h-5 w-5" aria-hidden="true">
<span className={cn('mr-2 h-5 w-5', iconClassName)} aria-hidden="true">
{item.icon}
</span>
)}

View file

@ -81,13 +81,13 @@ function defaultGetStringKey(node: unknown): string {
* @returns
*/
export function useMultiSearch<OptionsType extends unknown[]>({
availableOptions,
availableOptions = [] as unknown as OptionsType,
placeholder,
getTextKeyOverride,
className,
disabled = false,
}: {
availableOptions: OptionsType;
availableOptions?: OptionsType;
placeholder?: string;
getTextKeyOverride?: (node: OptionsType[0]) => string;
className?: string;

View file

@ -20,7 +20,7 @@ type SelectDropDownProps = {
value: string | null | Option | OptionWithIcon;
setValue: DropdownValueSetter | ((value: string) => void);
tabIndex?: number;
availableValues: string[] | Option[] | OptionWithIcon[];
availableValues?: string[] | Option[] | OptionWithIcon[];
emptyTitle?: boolean;
showAbove?: boolean;
showLabel?: boolean;
@ -89,18 +89,20 @@ function SelectDropDown({
title = localize('com_ui_model');
}
const values = availableValues ?? [];
// Detemine if we should to convert this component into a searchable select. If we have enough elements, a search
// input will appear near the top of the menu, allowing correct filtering of different model menu items. This will
// reset once the component is unmounted (as per a normal search)
const [filteredValues, searchRender] = useMultiSearch<string[] | Option[]>({
availableOptions: availableValues,
availableOptions: values,
placeholder: searchPlaceholder,
getTextKeyOverride: (option) => getOptionText(option).toUpperCase(),
className: searchClassName,
disabled,
});
const hasSearchRender = searchRender != null;
const options = hasSearchRender ? filteredValues : availableValues;
const options = hasSearchRender ? filteredValues : values;
const renderIcon = showOptionIcon && value != null && (value as OptionWithIcon).icon != null;

View file

@ -7,6 +7,7 @@ interface TooltipAnchorProps extends Ariakit.TooltipAnchorProps {
description: string;
side?: 'top' | 'bottom' | 'left' | 'right';
className?: string;
focusable?: boolean;
role?: string;
}

View file

@ -0,0 +1 @@
export * from './queries';

View file

@ -0,0 +1,76 @@
import { QueryKeys, dataService, EModelEndpoint, defaultOrderQuery } from 'librechat-data-provider';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import type { QueryObserverResult, UseQueryOptions } from '@tanstack/react-query';
import type t from 'librechat-data-provider';
/**
* AGENTS
*/
/**
* Hook for getting all available tools for A
*/
export const useAvailableAgentToolsQuery = (): QueryObserverResult<t.TPlugin[]> => {
const queryClient = useQueryClient();
const endpointsConfig = queryClient.getQueryData<t.TEndpointsConfig>([QueryKeys.endpoints]);
const enabled = !!endpointsConfig?.[EModelEndpoint.agents];
return useQuery<t.TPlugin[]>([QueryKeys.tools], () => dataService.getAvailableAgentTools(), {
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
enabled,
});
};
/**
* Hook for listing all Agents, with optional parameters provided for pagination and sorting
*/
export const useListAgentsQuery = <TData = t.AgentListResponse>(
params: t.AgentListParams = defaultOrderQuery,
config?: UseQueryOptions<t.AgentListResponse, unknown, TData>,
): QueryObserverResult<TData> => {
const queryClient = useQueryClient();
const endpointsConfig = queryClient.getQueryData<t.TEndpointsConfig>([QueryKeys.endpoints]);
const enabled = !!endpointsConfig?.[EModelEndpoint.agents];
return useQuery<t.AgentListResponse, unknown, TData>(
[QueryKeys.agents, params],
() => dataService.listAgents(params),
{
// Example selector to sort them by created_at
// select: (res) => {
// return res.data.sort((a, b) => a.created_at - b.created_at);
// },
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
retry: false,
...config,
enabled: config?.enabled !== undefined ? config.enabled && enabled : enabled,
},
);
};
/**
* Hook for retrieving details about a single agent
*/
export const useGetAgentByIdQuery = (
agent_id: string,
config?: UseQueryOptions<t.Agent>,
): QueryObserverResult<t.Agent> => {
return useQuery<t.Agent>(
[QueryKeys.agent, agent_id],
() =>
dataService.getAgentById({
agent_id,
}),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
retry: false,
...config,
},
);
};

View file

@ -1,2 +1,2 @@
export * from './queries';
// export * from './mutations';
export * from './mutations';

View file

@ -0,0 +1,42 @@
import { dataService, QueryKeys, Tools } from 'librechat-data-provider';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import type { UseMutationResult } from '@tanstack/react-query';
import type * as t from 'librechat-data-provider';
export const useToolCallMutation = <T extends t.ToolId>(
toolId: T,
options?: t.ToolCallMutationOptions<T>,
): UseMutationResult<t.ToolCallResponse, Error, t.ToolParams<T>> => {
const queryClient = useQueryClient();
return useMutation(
(toolParams: t.ToolParams<T>) => {
return dataService.callTool({
toolId,
toolParams,
});
},
{
onMutate: (variables) => options?.onMutate?.(variables),
onError: (error, variables, context) => options?.onError?.(error, variables, context),
onSuccess: (response, variables, context) => {
queryClient.setQueryData<t.ToolCallResults>(
[QueryKeys.toolCalls, variables.conversationId],
(prev) => [
...(prev ?? []),
{
user: '',
toolId: Tools.execute_code,
partIndex: variables.partIndex,
messageId: variables.messageId,
blockIndex: variables.blockIndex,
conversationId: variables.conversationId,
result: response.result,
attachments: response.attachments,
},
],
);
return options?.onSuccess?.(response, variables, context);
},
},
);
};

View file

@ -1,5 +1,5 @@
import { QueryKeys, dataService } from 'librechat-data-provider';
import { useQuery } from '@tanstack/react-query';
import { Constants, QueryKeys, dataService } from 'librechat-data-provider';
import type { QueryObserverResult, UseQueryOptions } from '@tanstack/react-query';
import type t from 'librechat-data-provider';
@ -18,3 +18,24 @@ export const useVerifyAgentToolAuth = (
},
);
};
export const useGetToolCalls = <TData = t.ToolCallResults>(
params: t.GetToolCallParams,
config?: UseQueryOptions<t.ToolCallResults, unknown, TData>,
): QueryObserverResult<TData, unknown> => {
const { conversationId = '' } = params;
return useQuery<t.ToolCallResults, unknown, TData>(
[QueryKeys.toolCalls, conversationId],
() => dataService.getToolCalls(params),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
enabled:
conversationId.length > 0 &&
conversationId !== Constants.NEW_CONVO &&
conversationId !== Constants.SEARCH,
...config,
},
);
};

View file

@ -1,3 +1,4 @@
export * from './Agents';
export * from './Files';
export * from './Tools';
export * from './connection';

View file

@ -22,9 +22,6 @@ import type {
AssistantListParams,
AssistantListResponse,
AssistantDocument,
Agent,
AgentListParams,
AgentListResponse,
TEndpointsConfig,
TCheckUserKeyResponse,
SharedLinkListParams,
@ -370,78 +367,6 @@ export const useGetAssistantDocsQuery = <TData = AssistantDocument[]>(
);
};
/**
* AGENTS
*/
/**
* Hook for getting all available tools for A
*/
export const useAvailableAgentToolsQuery = (): QueryObserverResult<TPlugin[]> => {
const queryClient = useQueryClient();
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
const enabled = !!endpointsConfig?.[EModelEndpoint.agents];
return useQuery<TPlugin[]>([QueryKeys.tools], () => dataService.getAvailableAgentTools(), {
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
enabled,
});
};
/**
* Hook for listing all Agents, with optional parameters provided for pagination and sorting
*/
export const useListAgentsQuery = <TData = AgentListResponse>(
params: AgentListParams = defaultOrderQuery,
config?: UseQueryOptions<AgentListResponse, unknown, TData>,
): QueryObserverResult<TData> => {
const queryClient = useQueryClient();
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
const enabled = !!endpointsConfig?.[EModelEndpoint.agents];
return useQuery<AgentListResponse, unknown, TData>(
[QueryKeys.agents, params],
() => dataService.listAgents(params),
{
// Example selector to sort them by created_at
// select: (res) => {
// return res.data.sort((a, b) => a.created_at - b.created_at);
// },
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
retry: false,
...config,
enabled: config?.enabled !== undefined ? config.enabled && enabled : enabled,
},
);
};
/**
* Hook for retrieving details about a single agent
*/
export const useGetAgentByIdQuery = (
agent_id: string,
config?: UseQueryOptions<Agent>,
): QueryObserverResult<Agent> => {
return useQuery<Agent>(
[QueryKeys.agent, agent_id],
() =>
dataService.getAgentById({
agent_id,
}),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
retry: false,
...config,
},
);
};
/** STT/TTS */
/* Text to speech voices */

View file

@ -22,7 +22,12 @@ export const useGetRole = (
export const useUpdatePromptPermissionsMutation = (
options?: t.UpdatePromptPermOptions,
): UseMutationResult<t.UpdatePromptPermResponse, t.TError, t.UpdatePromptPermVars, unknown> => {
): UseMutationResult<
t.UpdatePermResponse,
t.TError | undefined,
t.UpdatePromptPermVars,
unknown
> => {
const queryClient = useQueryClient();
const { onMutate, onSuccess, onError } = options ?? {};
return useMutation(
@ -38,7 +43,10 @@ export const useUpdatePromptPermissionsMutation = (
}
},
onError: (...args) => {
args[0] && console.error('Failed to update prompt permissions:', args[0]);
const error = args[0];
if (error != null) {
console.error('Failed to update prompt permissions:', error);
}
if (onError) {
onError(...args);
}
@ -47,3 +55,39 @@ export const useUpdatePromptPermissionsMutation = (
},
);
};
export const useUpdateAgentPermissionsMutation = (
options?: t.UpdateAgentPermOptions,
): UseMutationResult<
t.UpdatePermResponse,
t.TError | undefined,
t.UpdateAgentPermVars,
unknown
> => {
const queryClient = useQueryClient();
const { onMutate, onSuccess, onError } = options ?? {};
return useMutation(
(variables) => {
promptPermissionsSchema.partial().parse(variables.updates);
return dataService.updateAgentPermissions(variables);
},
{
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries([QueryKeys.roles, variables.roleName]);
if (onSuccess != null) {
onSuccess(data, variables, context);
}
},
onError: (...args) => {
const error = args[0];
if (error != null) {
console.error('Failed to update prompt permissions:', error);
}
if (onError != null) {
onError(...args);
}
},
onMutate,
},
);
};

View file

@ -16,7 +16,7 @@ export default function useAssistantListMap<T = AssistantListItem[] | null>(
selector: (res: AssistantListResponse) => T = selectAssistantsResponse as (
res: AssistantListResponse,
) => T,
): Record<AssistantsEndpoint, T> {
): Record<AssistantsEndpoint, T | null> {
const { data: assistantsList = null } = useListAssistantsQuery(
EModelEndpoint.assistants,
undefined,

View file

@ -46,7 +46,7 @@ export default function usePresets() {
return;
}
if (presets && presets.length > 0 && user && presets[0].user !== user?.id) {
if (presets && presets.length > 0 && user && presets[0].user !== user.id) {
presetsQuery.refetch();
return;
}
@ -80,7 +80,7 @@ export default function usePresets() {
}
const previousPresets = presetsQuery.data ?? [];
if (previousPresets) {
setPresets(previousPresets.filter((p) => p.presetId !== preset?.presetId));
setPresets(previousPresets.filter((p) => p.presetId !== preset.presetId));
}
},
onSuccess: () => {
@ -99,12 +99,12 @@ export default function usePresets() {
const updatePreset = useUpdatePresetMutation({
onSuccess: (data, preset) => {
const toastTitle = data.title ? `"${data.title}"` : localize('com_endpoint_preset_title');
let message = `${toastTitle} ${localize('com_endpoint_preset_saved')}`;
let message = `${toastTitle} ${localize('com_ui_saved')}`;
if (data.defaultPreset && data.presetId !== _defaultPreset?.presetId) {
message = `${toastTitle} ${localize('com_endpoint_preset_default')}`;
setDefaultPreset(data);
newConversation({ preset: data });
} else if (preset?.defaultPreset === false) {
} else if (preset.defaultPreset === false) {
setDefaultPreset(null);
message = `${toastTitle} ${localize('com_endpoint_preset_default_removed')}`;
}
@ -233,7 +233,7 @@ export default function usePresets() {
if (!preset) {
return;
}
const fileName = filenamify(preset?.title || 'preset');
const fileName = filenamify(preset.title || 'preset');
exportFromJSON({
data: cleanupPreset({ preset }),
fileName,

View file

@ -25,7 +25,7 @@ export const useDelayedUploadToast = () => {
showToast({
message,
status: 'warning',
duration: 7000,
duration: 10000,
});
}, delay);

View file

@ -8,6 +8,7 @@ import {
EModelEndpoint,
codeTypeMapping,
mergeFileConfig,
isAgentsEndpoint,
isAssistantsEndpoint,
defaultAssistantsVersion,
fileConfig as defaultFileConfig,
@ -38,6 +39,7 @@ const useFileHandling = (params?: UseFileHandling) => {
const [errors, setErrors] = useState<string[]>([]);
const abortControllerRef = useRef<AbortController | null>(null);
const { startUploadTimer, clearUploadTimer } = useDelayedUploadToast();
const [toolResource, setToolResource] = useState<string | undefined>();
const { files, setFiles, setFilesLoading, conversation } = useChatContext();
const setError = (error: string) => setErrors((prevErrors) => [...prevErrors, error]);
const { addFile, replaceFile, updateFileById, deleteFileById } = useUpdateFiles(
@ -147,6 +149,9 @@ const useFileHandling = (params?: UseFileHandling) => {
: error?.response?.data?.message ?? 'com_error_files_upload';
setError(errorMessage);
},
onMutate: () => {
setToolResource(undefined);
},
},
abortControllerRef.current?.signal,
);
@ -178,6 +183,18 @@ const useFileHandling = (params?: UseFileHandling) => {
}
}
if (isAgentsEndpoint(endpoint)) {
if (!agent_id) {
formData.append('message_file', 'true');
}
if (toolResource != null) {
formData.append('tool_resource', toolResource);
}
if (conversation?.agent_id != null && formData.get('agent_id') == null) {
formData.append('agent_id', conversation.agent_id);
}
}
if (!isAssistantsEndpoint(endpoint)) {
uploadFile.mutate(formData);
return;
@ -377,6 +394,7 @@ const useFileHandling = (params?: UseFileHandling) => {
return {
handleFileChange,
setToolResource,
handleFiles,
abortUpload,
setFiles,

View file

@ -5,17 +5,17 @@ import {
useGetEndpointsQuery,
} from 'librechat-data-provider/react-query';
import {
getConfigDefaults,
EModelEndpoint,
alternateName,
EModelEndpoint,
getConfigDefaults,
isAssistantsEndpoint,
} from 'librechat-data-provider';
import type { AssistantsEndpoint, TAssistantsMap, TEndpointsConfig } from 'librechat-data-provider';
import type { TAssistantsMap, TEndpointsConfig } from 'librechat-data-provider';
import type { MentionOption } from '~/common';
import useAssistantListMap from '~/hooks/Assistants/useAssistantListMap';
import { useGetPresetsQuery, useListAgentsQuery } from '~/data-provider';
import { mapEndpoints, getPresetTitle } from '~/utils';
import { EndpointIcon } from '~/components/Endpoints';
import { useGetPresetsQuery } from '~/data-provider';
const defaultInterface = getConfigDefaults().interface;
@ -25,7 +25,7 @@ const assistantMapFn =
assistantMap,
endpointsConfig,
}: {
endpoint: AssistantsEndpoint;
endpoint: EModelEndpoint | string;
assistantMap: TAssistantsMap;
endpointsConfig: TEndpointsConfig;
}) =>
@ -65,6 +65,27 @@ export default function useMentions({
description,
})),
);
const { data: agentsList = null } = useListAgentsQuery(undefined, {
select: (res) => {
const { data } = res;
return data.map(({ id, name, avatar }) => ({
value: id,
label: name ?? '',
type: EModelEndpoint.agents,
icon: EndpointIcon({
conversation: {
agent_id: id,
endpoint: EModelEndpoint.agents,
iconURL: avatar?.filepath,
},
containerClassName: 'shadow-stroke overflow-hidden rounded-full',
endpointsConfig: endpointsConfig,
context: 'menu-item',
size: 20,
}),
}));
},
});
const assistantListMap = useMemo(
() => ({
[EModelEndpoint.assistants]: listMap[EModelEndpoint.assistants]
@ -101,7 +122,7 @@ export default function useMentions({
validEndpoints = endpoints.filter((endpoint) => !isAssistantsEndpoint(endpoint));
}
const mentions = [
...(modelSpecs?.length > 0 ? modelSpecs : []).map((modelSpec) => ({
...(modelSpecs.length > 0 ? modelSpecs : []).map((modelSpec) => ({
value: modelSpec.name,
label: modelSpec.label,
description: modelSpec.description,
@ -116,9 +137,9 @@ export default function useMentions({
}),
type: 'modelSpec' as const,
})),
...(interfaceConfig.endpointsMenu ? validEndpoints : []).map((endpoint) => ({
...(interfaceConfig.endpointsMenu === true ? validEndpoints : []).map((endpoint) => ({
value: endpoint,
label: alternateName[endpoint] ?? endpoint ?? '',
label: alternateName[endpoint as string] ?? endpoint ?? '',
type: 'endpoint' as const,
icon: EndpointIcon({
conversation: { endpoint },
@ -127,13 +148,14 @@ export default function useMentions({
size: 20,
}),
})),
...(agentsList ?? []),
...(endpointsConfig?.[EModelEndpoint.assistants] && includeAssistants
? assistantListMap[EModelEndpoint.assistants] || []
: []),
...(endpointsConfig?.[EModelEndpoint.azureAssistants] && includeAssistants
? assistantListMap[EModelEndpoint.azureAssistants] || []
: []),
...((interfaceConfig.presets ? presets : [])?.map((preset, index) => ({
...((interfaceConfig.presets === true ? presets : [])?.map((preset, index) => ({
value: preset.presetId ?? `preset-${index}`,
label: preset.title ?? preset.modelLabel ?? preset.chatGptLabel ?? '',
description: getPresetTitle(preset, true),
@ -154,6 +176,7 @@ export default function useMentions({
presets,
endpoints,
modelSpecs,
agentsList,
assistantMap,
endpointsConfig,
assistantListMap,
@ -166,6 +189,7 @@ export default function useMentions({
options,
presets,
modelSpecs,
agentsList,
modelsConfig,
endpointsConfig,
assistantListMap,

View file

@ -1,6 +1,6 @@
import { useCallback } from 'react';
import { useRecoilValue } from 'recoil';
import { EModelEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
import { EModelEndpoint, isAgentsEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
import type {
TPreset,
TModelSpec,
@ -64,7 +64,11 @@ export default function useSelectMention({
preset.endpointType = newEndpointType;
}
if (isAssistantsEndpoint(newEndpoint) && preset.assistant_id != null && !(preset.model ?? '')) {
if (
isAssistantsEndpoint(newEndpoint) &&
preset.assistant_id != null &&
!(preset.model ?? '')
) {
preset.model = assistantMap?.[newEndpoint]?.[preset.assistant_id]?.model;
}
@ -94,11 +98,19 @@ export default function useSelectMention({
keepAddedConvos: isModular,
});
},
[conversation, getDefaultConversation, modularChat, newConversation, endpointsConfig, assistantMap],
[
conversation,
getDefaultConversation,
modularChat,
newConversation,
endpointsConfig,
assistantMap,
],
);
type Kwargs = {
model?: string;
agent_id?: string;
assistant_id?: string;
};
@ -228,6 +240,10 @@ export default function useSelectMention({
assistant_id: key,
model: assistantMap?.[option.type]?.[key]?.model ?? '',
});
} else if (isAgentsEndpoint(option.type)) {
onSelectEndpoint(option.type, {
agent_id: key,
});
}
},
[modelSpecs, onSelectEndpoint, onSelectPreset, onSelectSpec, presets, assistantMap],

View file

@ -1,11 +1,12 @@
import debounce from 'lodash/debounce';
import { useEffect, useRef, useCallback } from 'react';
import { useRecoilValue, useRecoilState } from 'recoil';
import { isAssistantsEndpoint } from 'librechat-data-provider';
import { Constants } from 'librechat-data-provider';
import type { TEndpointOption } from 'librechat-data-provider';
import type { KeyboardEvent } from 'react';
import { forceResize, insertTextAtCursor, getAssistantName } from '~/utils';
import { forceResize, insertTextAtCursor, getEntityName, getEntity } from '~/utils';
import { useAssistantsMapContext } from '~/Providers/AssistantsMapContext';
import { useAgentsMapContext } from '~/Providers/AgentsMapContext';
import useGetSender from '~/hooks/Conversations/useGetSender';
import useFileHandling from '~/hooks/Files/useFileHandling';
import { useInteractionHealthCheck } from '~/data-provider';
@ -28,6 +29,7 @@ export default function useTextarea({
const localize = useLocalize();
const getSender = useGetSender();
const isComposing = useRef(false);
const agentsMap = useAgentsMapContext();
const { handleFiles } = useFileHandling();
const assistantMap = useAssistantsMapContext();
const checkHealth = useInteractionHealthCheck();
@ -44,19 +46,25 @@ export default function useTextarea({
} = useChatContext();
const [activePrompt, setActivePrompt] = useRecoilState(store.activePromptByIndex(index));
const { conversationId, jailbreak, endpoint = '', assistant_id } = conversation || {};
const { conversationId, jailbreak = false, endpoint = '' } = conversation || {};
const { entity, isAgent, isAssistant } = getEntity({
endpoint,
agentsMap,
assistantMap,
agent_id: conversation?.agent_id,
assistant_id: conversation?.assistant_id,
});
const entityName = entity?.name ?? '';
const isNotAppendable =
((latestMessage?.unfinished && !isSubmitting) || latestMessage?.error) &&
!isAssistantsEndpoint(endpoint);
(((latestMessage?.unfinished ?? false) && !isSubmitting) || (latestMessage?.error ?? false)) &&
!isAssistant;
// && (conversationId?.length ?? 0) > 6; // also ensures that we don't show the wrong placeholder
const assistant =
isAssistantsEndpoint(endpoint) && assistantMap?.[endpoint ?? '']?.[assistant_id ?? ''];
const assistantName = (assistant && assistant.name) || '';
useEffect(() => {
if (activePrompt && textAreaRef.current) {
insertTextAtCursor(textAreaRef.current, activePrompt);
const prompt = activePrompt ?? '';
if (prompt && textAreaRef.current) {
insertTextAtCursor(textAreaRef.current, prompt);
forceResize(textAreaRef.current);
setActivePrompt(undefined);
}
@ -64,16 +72,17 @@ export default function useTextarea({
// auto focus to input, when enter a conversation.
useEffect(() => {
if (!conversationId) {
const convoId = conversationId ?? '';
if (!convoId) {
return;
}
// Prevents Settings from not showing on new conversation, also prevents showing toneStyle change without jailbreak
if (conversationId === 'new' || !jailbreak) {
if (convoId === Constants.NEW_CONVO || !jailbreak) {
setShowBingToneSetting(false);
}
if (conversationId !== 'search') {
if (convoId !== Constants.SEARCH) {
textAreaRef.current?.focus();
}
// setShowBingToneSetting is a recoil setter, so it doesn't need to be in the dependency array
@ -89,7 +98,8 @@ export default function useTextarea({
}, [isSubmitting, textAreaRef]);
useEffect(() => {
if (textAreaRef.current?.value) {
const currentValue = textAreaRef.current?.value ?? '';
if (currentValue) {
return;
}
@ -98,10 +108,13 @@ export default function useTextarea({
return localize('com_endpoint_config_placeholder');
}
const currentEndpoint = conversation?.endpoint ?? '';
const currentAgentId = conversation?.agent_id ?? '';
const currentAssistantId = conversation?.assistant_id ?? '';
if (
isAssistantsEndpoint(currentEndpoint) &&
(!currentAssistantId || !assistantMap?.[currentEndpoint]?.[currentAssistantId ?? ''])
if (isAgent && (!currentAgentId || !agentsMap?.[currentAgentId])) {
return localize('com_endpoint_agent_placeholder');
} else if (
isAssistant &&
(!currentAssistantId || !assistantMap?.[currentEndpoint]?.[currentAssistantId])
) {
return localize('com_endpoint_assistant_placeholder');
}
@ -110,9 +123,10 @@ export default function useTextarea({
return localize('com_endpoint_message_not_appendable');
}
const sender = isAssistantsEndpoint(currentEndpoint)
? getAssistantName({ name: assistantName, localize })
: getSender(conversation as TEndpointOption);
const sender =
isAssistant || isAgent
? getEntityName({ name: entityName, isAgent, localize })
: getSender(conversation as TEndpointOption);
return `${localize('com_endpoint_message')} ${sender ? sender : 'AI'}`;
};
@ -137,15 +151,18 @@ export default function useTextarea({
return () => debouncedSetPlaceholder.cancel();
}, [
conversation,
isAgent,
localize,
disabled,
getSender,
agentsMap,
entityName,
textAreaRef,
isAssistant,
assistantMap,
conversation,
latestMessage,
isNotAppendable,
localize,
getSender,
assistantName,
textAreaRef,
assistantMap,
]);
const handleKeyDown = useCallback(
@ -181,7 +198,7 @@ export default function useTextarea({
}
if ((isNonShiftEnter || isCtrlEnter) && !isComposing.current) {
const globalAudio = document.getElementById(globalAudioId) as HTMLAudioElement;
const globalAudio = document.getElementById(globalAudioId) as HTMLAudioElement | undefined;
if (globalAudio) {
console.log('Unmuting global audio');
globalAudio.muted = false;
@ -207,14 +224,15 @@ export default function useTextarea({
return;
}
if (!e.clipboardData) {
const clipboardData = e.clipboardData as DataTransfer | undefined;
if (!clipboardData) {
return;
}
if (e.clipboardData.files.length > 0) {
if (clipboardData.files.length > 0) {
setFilesLoading(true);
const timestampedFiles: File[] = [];
for (const file of e.clipboardData.files) {
for (const file of clipboardData.files) {
const newFile = new File([file], `clipboard_${+new Date()}_${file.name}`, {
type: file.type,
});

View file

@ -41,7 +41,7 @@ export default function useMessageActions(props: TMessageActions) {
[isMultiMessage, addedConvo, rootConvo],
);
const agentMap = useAgentsMapContext();
const agentsMap = useAgentsMapContext();
const assistantMap = useAssistantsMapContext();
const { text, content, messageId = null, isCreatedByUser } = message ?? {};
@ -68,20 +68,20 @@ export default function useMessageActions(props: TMessageActions) {
return undefined;
}
if (!agentMap) {
if (!agentsMap) {
return undefined;
}
const modelKey = message?.model ?? '';
if (modelKey) {
return agentMap[modelKey];
return agentsMap[modelKey];
}
const agentId = conversation?.agent_id ?? '';
if (agentId) {
return agentMap[agentId];
return agentsMap[agentId];
}
}, [agentMap, conversation?.agent_id, conversation?.endpoint, message?.model]);
}, [agentsMap, conversation?.agent_id, conversation?.endpoint, message?.model]);
const isSubmitting = useMemo(
() => (isMultiMessage === true ? isSubmittingAdditional : isSubmittingRoot),

View file

@ -22,7 +22,7 @@ export default function useMessageHelpers(props: TMessageProps) {
setLatestMessage,
} = useChatContext();
const assistantMap = useAssistantsMapContext();
const agentMap = useAgentsMapContext();
const agentsMap = useAgentsMapContext();
const { text, content, children, messageId = null, isCreatedByUser } = message ?? {};
const edit = messageId === currentEditId;
@ -102,8 +102,8 @@ export default function useMessageHelpers(props: TMessageProps) {
const modelKey = message?.model ?? '';
return agentMap ? agentMap[modelKey] : undefined;
}, [agentMap, conversation?.endpoint, message?.model]);
return agentsMap ? agentsMap[modelKey] : undefined;
}, [agentsMap, conversation?.endpoint, message?.model]);
const regenerateMessage = () => {
if ((isSubmitting && isCreatedByUser === true) || !message) {

View file

@ -10,9 +10,9 @@ import {
} from 'librechat-data-provider';
import type { TConfig, TInterfaceConfig } from 'librechat-data-provider';
import type { NavLink } from '~/common';
import AgentPanelSwitch from '~/components/SidePanel/Agents/AgentPanelSwitch';
import BookmarkPanel from '~/components/SidePanel/Bookmarks/BookmarkPanel';
import PanelSwitch from '~/components/SidePanel/Builder/PanelSwitch';
import AgentPanelSwitch from '~/components/SidePanel/Agents/AgentPanelSwitch';
import PromptsAccordion from '~/components/Prompts/PromptsAccordion';
import Parameters from '~/components/SidePanel/Parameters/Panel';
import FilesPanel from '~/components/SidePanel/Files/Panel';
@ -44,6 +44,14 @@ export default function useSideNavLinks({
permissionType: PermissionTypes.BOOKMARKS,
permission: Permissions.USE,
});
const hasAccessToAgents = useHasAccess({
permissionType: PermissionTypes.AGENTS,
permission: Permissions.USE,
});
const hasAccessToCreateAgents = useHasAccess({
permissionType: PermissionTypes.AGENTS,
permission: Permissions.CREATE,
});
const Links = useMemo(() => {
const links: NavLink[] = [];
@ -64,6 +72,8 @@ export default function useSideNavLinks({
}
if (
hasAccessToAgents &&
hasAccessToCreateAgents &&
isAgentsEndpoint(endpoint) &&
agents &&
// agents.disableBuilder !== true &&
@ -137,8 +147,10 @@ export default function useSideNavLinks({
endpointType,
endpoint,
agents,
hasAccessToAgents,
hasAccessToPrompts,
hasAccessToBookmarks,
hasAccessToCreateAgents,
hidePanel,
]);

View file

@ -1,3 +1,4 @@
export { default as useAuthCodeTool } from './useAuthCodeTool';
export { default as usePluginInstall } from './usePluginInstall';
export { default as useCodeApiKeyForm } from './useCodeApiKeyForm';
export { default as usePluginDialogHelpers } from './usePluginDialogHelpers';

View file

@ -0,0 +1,43 @@
// client/src/hooks/Plugins/useCodeApiKeyForm.ts
import { useState, useCallback } from 'react';
import { useForm } from 'react-hook-form';
import type { ApiKeyFormData } from '~/common';
import useAuthCodeTool from '~/hooks/Plugins/useAuthCodeTool';
export default function useCodeApiKeyForm({
onSubmit,
onRevoke,
}: {
onSubmit?: () => void;
onRevoke?: () => void;
}) {
const methods = useForm<ApiKeyFormData>();
const [isDialogOpen, setIsDialogOpen] = useState(false);
const { installTool, removeTool } = useAuthCodeTool({ isEntityTool: true });
const { reset } = methods;
const onSubmitHandler = useCallback(
(data: { apiKey: string }) => {
reset();
installTool(data.apiKey);
setIsDialogOpen(false);
onSubmit?.();
},
[onSubmit, reset, installTool],
);
const handleRevokeApiKey = useCallback(() => {
reset();
removeTool();
setIsDialogOpen(false);
onRevoke?.();
}, [reset, onRevoke, removeTool]);
return {
methods,
isDialogOpen,
setIsDialogOpen,
handleRevokeApiKey,
onSubmit: onSubmitHandler,
};
}

View file

@ -0,0 +1,28 @@
import { ToolCallResult } from 'librechat-data-provider';
import { useMemo } from 'react';
import { useGetToolCalls } from '~/data-provider';
import { mapToolCalls, logger } from '~/utils';
type ToolCallsMap = {
[x: string]: ToolCallResult[] | undefined;
};
export default function useToolCallsMap({
conversationId,
}: {
conversationId: string;
}): ToolCallsMap | undefined {
const { data: toolCallsMap = null } = useGetToolCalls(
{ conversationId },
{
select: (res) => mapToolCalls(res),
},
);
const result = useMemo<ToolCallsMap | undefined>(() => {
return toolCallsMap !== null ? toolCallsMap : undefined;
}, [toolCallsMap]);
logger.log('tools', 'tool calls map:', result);
return result;
}

View file

@ -21,7 +21,14 @@ type TUseStepHandler = {
type TStepEvent = {
event: string;
data: Agents.MessageDeltaEvent | Agents.RunStep | Agents.ToolEndEvent;
data:
| Agents.MessageDeltaEvent
| Agents.RunStep
| Agents.ToolEndEvent
| {
runId?: string;
message: string;
};
};
type MessageDeltaUpdate = { type: ContentTypes.TEXT; text: string; tool_call_ids?: string[] };
@ -166,6 +173,30 @@ export default function useStepHandler({
}
});
}
} else if (event === 'on_agent_update') {
const { runId, message } = data as { runId?: string; message: string };
const responseMessageId = runId ?? '';
if (!responseMessageId) {
console.warn('No message id found in agent update event');
return;
}
const responseMessage = messages[messages.length - 1] as TMessage;
const response = {
...responseMessage,
parentMessageId: userMessage.messageId,
conversationId: userMessage.conversationId,
messageId: responseMessageId,
content: [
{
type: ContentTypes.TEXT,
text: message,
},
],
} as TMessage;
setMessages([...messages.slice(0, -1), response]);
} else if (event === 'on_message_delta') {
const messageDelta = data as Agents.MessageDeltaEvent;
const runStep = stepMap.current.get(messageDelta.id);

View file

@ -528,7 +528,7 @@ export default {
com_endpoint_preset_default_item: 'الافتراضي:',
com_endpoint_preset_default_none: 'لا يوجد إعداد مسبق افتراضي نشط.',
com_endpoint_preset_title: 'إعداد مسبق',
com_endpoint_preset_saved: 'تم الحفظ!',
com_ui_saved: 'تم الحفظ!',
com_endpoint_preset_default: 'أصبح الإعداد المسبق الافتراضي الآن.',
com_endpoint_preset_selected: 'الإعداد المسبق نشط!',
com_endpoint_preset_selected_title: 'مُحدَّد!',
@ -644,7 +644,7 @@ export default {
com_agents_file_search_info:
'عند التمكين، سيتم إعلام الوكيل بأسماء الملفات المدرجة أدناه بالضبط، مما يتيح له استرجاع السياق ذي الصلة من هذه الملفات.',
com_ui_agent_already_shared_to_all: 'هذا المساعد مشارك بالفعل مع جميع المستخدمين',
com_agents_execute_code: 'تنفيذ الشفرة',
com_ui_run_code: 'تنفيذ الشفرة',
com_ui_no_changes: 'لا توجد تغييرات للتحديث',
com_ui_agent_editing_allowed: 'يمكن للمستخدمين الآخرين تعديل هذا الوكيل بالفعل',
com_ui_error_connection: 'خطأ في الاتصال بالخادم، حاول تحديث الصفحة.',

View file

@ -548,7 +548,7 @@ export default {
com_endpoint_preset_default_item: 'Padrão:',
com_endpoint_preset_default_none: 'Nenhum preset padrão ativo.',
com_endpoint_preset_title: 'Preset',
com_endpoint_preset_saved: 'Salvo!',
com_ui_saved: 'Salvo!',
com_endpoint_preset_default: 'é agora o preset padrão.',
com_endpoint_preset: 'preset',
com_endpoint_presets: 'presets',

View file

@ -509,7 +509,7 @@ export default {
com_endpoint_preset_default_item: 'Standard:',
com_endpoint_preset_default_none: 'Keine Standardvoreinstellung aktiv.',
com_endpoint_preset_title: 'Voreinstellung',
com_endpoint_preset_saved: 'Gespeichert!',
com_ui_saved: 'Gespeichert!',
com_endpoint_preset_default: 'ist jetzt die Standardvoreinstellung.',
com_endpoint_preset: 'Voreinstellung',
com_endpoint_presets: 'Voreinstellungen',
@ -801,7 +801,7 @@ export default {
com_ui_endpoint: 'Endpunkt',
com_ui_region: 'Region',
com_ui_model_parameters: 'Modell-Parameter',
com_agents_execute_code: 'Code ausführen',
com_ui_run_code: 'Code ausführen',
com_ui_provider: 'Anbieter',
com_ui_model_save_success: 'Modellparameter erfolgreich gespeichert',
com_ui_select_region: 'Wähle eine Region',

View file

@ -3,6 +3,10 @@
// file deepcode ignore HardcodedNonCryptoSecret: No hardcoded secrets present in this file
export default {
com_ui_enter_api_key: 'Enter API Key',
com_ui_librechat_code_api_title: 'Run AI Code',
com_ui_librechat_code_api_subtitle: 'Secure. Multi-language. Input/Output Files.',
com_ui_librechat_code_api_key: 'Get your LibreChat Code Interpreter API key',
com_nav_convo_menu_options: 'Conversation Menu Options',
com_ui_artifacts: 'Artifacts',
com_ui_artifacts_toggle: 'Toggle Artifacts UI',
@ -102,6 +106,7 @@ export default {
com_agents_description_placeholder: 'Optional: Describe your Agent here',
com_agents_instructions_placeholder: 'The system instructions that the agent uses',
com_agents_search_name: 'Search agents by name',
com_sidepanel_select_agent: 'Select an Agent',
com_agents_update_error: 'There was an error updating your agent.',
com_agents_create_error: 'There was an error creating your agent.',
com_agents_missing_provider_model: 'Please select a provider and model before creating an agent.',
@ -111,8 +116,11 @@ export default {
com_agents_enable_file_search: 'Enable File Search',
com_agents_file_search_info:
'When enabled, the agent will be informed of the exact filenames listed below, allowing it to retrieve relevant context from these files.',
com_agents_code_interpreter_title: 'Code Interpreter',
com_agents_by_librechat: 'by LibreChat',
com_agents_code_interpreter:
'When enabled, allows your agent to leverage the LibreChat Code Interpreter API to run generated code, including file processing, securely. Requires a valid API key.',
com_agents_file_search_disabled: 'Agent must be created before uploading files for File Search.',
com_agents_execute_code: 'Run Code',
com_ui_agent_already_shared_to_all: 'This agent is already shared to all users',
com_ui_agent_editing_allowed: 'Other users can already edit this agent',
com_ui_no_changes: 'No changes to update',
@ -177,6 +185,7 @@ export default {
com_ui_select_provider: 'Select a provider',
com_ui_select_provider_first: 'Select a provider first',
com_ui_select_search_model: 'Search model by name',
com_ui_select_search_provider: 'Search provider by name',
com_ui_select_search_region: 'Search region by name',
com_ui_select_search_plugin: 'Search plugin by name',
com_ui_use_prompt: 'Use prompt',
@ -184,6 +193,9 @@ export default {
com_ui_next: 'Next',
com_ui_stop: 'Stop',
com_ui_upload_files: 'Upload files',
com_ui_upload_image_input: 'Upload Image',
com_ui_upload_file_search: 'Upload for File Search',
com_ui_upload_code_files: 'Upload for Code Interpreter',
com_ui_prompt: 'Prompt',
com_ui_prompts: 'Prompts',
com_ui_prompt_name: 'Prompt Name',
@ -236,6 +248,8 @@ export default {
com_ui_read_aloud: 'Read aloud',
com_ui_copied: 'Copied!',
com_ui_copy_code: 'Copy code',
com_ui_run_code: 'Run Code',
com_ui_run_code_error: 'There was an error running the code',
com_ui_copy_to_clipboard: 'Copy to clipboard',
com_ui_copied_to_clipboard: 'Copied to clipboard',
com_ui_fork: 'Fork',
@ -331,6 +345,9 @@ export default {
com_ui_prompts_allow_share_global: 'Allow sharing Prompts to all users',
com_ui_prompt_shared_to_all: 'This prompt is shared to all users',
com_ui_prompt_update_error: 'There was an error updating the prompt',
com_ui_agents_allow_share_global: 'Allow sharing Agents to all users',
com_ui_agents_allow_use: 'Allow using Agents',
com_ui_agents_allow_create: 'Allow creating Agents',
com_ui_prompt_already_shared_to_all: 'This prompt is already shared to all users',
com_ui_description_placeholder: 'Optional: Enter a description to display for the prompt',
com_ui_command_placeholder: 'Optional: Enter a command for the prompt or name will be used.',
@ -574,7 +591,7 @@ export default {
com_endpoint_preset_default_item: 'Default:',
com_endpoint_preset_default_none: 'No default preset active.',
com_endpoint_preset_title: 'Preset',
com_endpoint_preset_saved: 'Saved!',
com_ui_saved: 'Saved!',
com_endpoint_preset_default: 'is now the default preset.',
com_endpoint_preset: 'preset',
com_endpoint_presets: 'presets',
@ -610,6 +627,7 @@ export default {
com_endpoint_skip_hover:
'Enable skipping the completion step, which reviews the final answer and generated steps',
com_endpoint_config_key: 'Set API Key',
com_endpoint_agent_placeholder: 'Please select an Agent',
com_endpoint_assistant_placeholder: 'Please select an Assistant from the right-hand Side Panel',
com_endpoint_config_placeholder: 'Set your Key in the Header menu to chat.',
com_endpoint_config_key_for: 'Set API Key for',
@ -651,6 +669,7 @@ export default {
com_nav_font_size_lg: 'Large',
com_nav_font_size_xl: 'Extra Large',
com_nav_welcome_assistant: 'Please Select an Assistant',
com_nav_welcome_agent: 'Please Select an Agent',
com_nav_welcome_message: 'How can I help you today?',
com_nav_auto_scroll: 'Auto-Scroll to latest message on chat open',
com_nav_user_msg_markdown: 'Render user messages as markdown',

View file

@ -350,7 +350,7 @@ export default {
com_endpoint_preset_default_item: 'Predeterminado:',
com_endpoint_preset_default_none: 'No hay configuración preestablecida predeterminada activa.',
com_endpoint_preset_title: 'Configuración preestablecida',
com_endpoint_preset_saved: '¡Guardado!',
com_ui_saved: '¡Guardado!',
com_endpoint_preset_default: 'es ahora la configuración preestablecida predeterminada.',
com_endpoint_preset: 'configuración preestablecida',
com_endpoint_presets: 'configuraciones preestablecidas',
@ -716,7 +716,7 @@ export default {
com_agents_file_search_disabled:
'Es necesario crear el Agente antes de subir archivos para la Búsqueda de Archivos.',
com_agents_execute_code: 'Ejecutar código',
com_ui_run_code: 'Ejecutar código',
com_ui_agent_already_shared_to_all: 'Este asistente ya está compartido con todos los usuarios',

View file

@ -489,7 +489,7 @@ export default {
com_endpoint_preset_default_item: 'Oletus:',
com_endpoint_preset_default_none: 'Oletus-esiasetusta ei ole käytössä',
com_endpoint_preset_title: 'Esiasetus',
com_endpoint_preset_saved: 'Tallennettu!',
com_ui_saved: 'Tallennettu!',
com_endpoint_preset_default: 'on nyt oletus-esiasetus.',
com_endpoint_preset: 'esiasetus',
com_endpoint_presets: 'esiasetukset',

View file

@ -257,7 +257,7 @@ export default {
com_endpoint_preset_default_item: 'Par défaut :',
com_endpoint_preset_default_none: 'Aucun préréglage par défaut actif.',
com_endpoint_preset_title: 'Préréglage',
com_endpoint_preset_saved: 'Enregistré!',
com_ui_saved: 'Enregistré!',
com_endpoint_preset_default: 'est maintenant le préréglage par défaut.',
com_endpoint_preset: 'préréglage',
com_endpoint_presets: 'préréglages',
@ -794,7 +794,7 @@ export default {
com_agents_enable_file_search: 'Activer la recherche de fichiers',
com_agents_file_search_info:
'Lorsque cette option est activée, l\'agent sera informé des noms exacts des fichiers listés ci-dessous, lui permettant d\'extraire le contexte pertinent de ces fichiers.',
com_agents_execute_code: 'Exécuter le code',
com_ui_run_code: 'Exécuter le code',
com_agents_file_search_disabled:
'L\'agent doit être créé avant de pouvoir télécharger des fichiers pour la Recherche de Fichiers.',
com_ui_agent_already_shared_to_all: 'Cet agent est déjà partagé avec tous les utilisateurs',

View file

@ -283,7 +283,7 @@ export default {
com_endpoint_preset_default_item: 'ברירת מחדל:',
com_endpoint_preset_default_none: 'אין ברירת מחדל פעילה.',
com_endpoint_preset_title: 'הגדרה מראש',
com_endpoint_preset_saved: 'שמור!',
com_ui_saved: 'שמור!',
com_endpoint_preset_default: 'הוא כעת ברירת המחדל המוגדרת מראש.',
com_endpoint_preset: 'preset',
com_endpoint_presets: 'presets',

View file

@ -248,7 +248,7 @@ export default {
com_endpoint_preset_default_item: 'Default:',
com_endpoint_preset_default_none: 'Tidak ada preset default yang aktif.',
com_endpoint_preset_title: 'Preset',
com_endpoint_preset_saved: 'Tersimpan!',
com_ui_saved: 'Tersimpan!',
com_endpoint_preset_default: 'sekarang menjadi preset default.',
com_endpoint_preset: 'preset',
com_endpoint_presets: 'presets',

View file

@ -402,7 +402,7 @@ export default {
com_endpoint_preset_default_item: 'Predefinita:',
com_endpoint_preset_default_none: 'Nessuna preimpostazione predefinita attiva.',
com_endpoint_preset_title: 'Preimpostazione',
com_endpoint_preset_saved: 'Salvata!',
com_ui_saved: 'Salvata!',
com_endpoint_preset_default: 'è ora la preimpostazione predefinita.',
com_endpoint_preset: 'preimpostazione',
com_endpoint_presets: 'preimpostazioni',
@ -676,7 +676,7 @@ export default {
com_agents_enable_file_search: 'Abilita Ricerca File',
com_agents_file_search_info:
'Quando abilitato, l\'agente verrà informato dei nomi esatti dei file elencati di seguito, permettendogli di recuperare il contesto pertinente da questi file.',
com_agents_execute_code: 'Esegui Codice',
com_ui_run_code: 'Esegui Codice',
com_agents_file_search_disabled:
'L\'Agente deve essere creato prima di caricare file per la Ricerca File.',
com_ui_agent_already_shared_to_all: 'Questo assistente è già condiviso con tutti gli utenti',

View file

@ -541,7 +541,7 @@ export default {
com_endpoint_preset_default_item: 'デフォルト:',
com_endpoint_preset_default_none: '現在有効なプリセットはありません。',
com_endpoint_preset_title: 'プリセット',
com_endpoint_preset_saved: '保存しました!',
com_ui_saved: '保存しました!',
com_endpoint_preset_default: 'が有効化されました。',
com_endpoint_preset: 'プリセット',
com_endpoint_presets: 'プリセット',
@ -834,7 +834,7 @@ export default {
'ファイル検索用のファイルをアップロードする前に、エージェントを作成する必要があります。',
com_agents_file_search_info:
'有効にすると、エージェントは以下に表示されているファイル名を正確に認識し、それらのファイルから関連する情報を取得することができます。',
com_agents_execute_code: 'コードを実行',
com_ui_run_code: 'コードを実行',
com_ui_agent_editing_allowed: 'このエージェントは他のユーザーが既に編集可能です',
com_ui_agent_already_shared_to_all: 'このアシスタントは既に全ユーザーに共有されています',
com_ui_no_changes: '更新する変更はありません',

View file

@ -859,7 +859,7 @@ export default {
com_endpoint_preset_default_item: '기본값:',
com_endpoint_preset_default_none: '기본 프리셋이 설정되지 않았습니다.',
com_endpoint_preset_title: '프리셋',
com_endpoint_preset_saved: '저장되었습니다!',
com_ui_saved: '저장되었습니다!',
com_endpoint_preset_default: '이제 기본 프리셋입니다.',
com_endpoint_preset_selected: '프리셋 활성화됨',
com_endpoint_preset_selected_title: '활성화됨',
@ -1020,7 +1020,7 @@ export default {
com_agents_file_search_disabled:
'파일 검색을 위해 파일을 업로드하기 전에 에이전트를 먼저 생성해야 합니다',
com_agents_execute_code: '코드 실행',
com_ui_run_code: '코드 실행',
com_ui_agent_already_shared_to_all: '이 에이전트는 이미 모든 사용자와 공유되어 있습니다',

View file

@ -251,7 +251,7 @@ export default {
com_endpoint_preset_default_item: 'По умолчанию:',
com_endpoint_preset_default_none: 'Активных пресетов по умолчанию нет.',
com_endpoint_preset_title: 'Пресет',
com_endpoint_preset_saved: 'Сохранено!',
com_ui_saved: 'Сохранено!',
com_endpoint_preset_default: 'теперь пресет "По умолчаанию".',
com_endpoint_preset: 'пресет',
com_endpoint_presets: 'пресеты',
@ -699,7 +699,7 @@ export default {
com_agents_file_search_disabled: 'Для загрузки файлов в Поиск необходимо сначала создать агента',
com_agents_execute_code: 'Выполнить код',
com_ui_run_code: 'Выполнить код',
com_ui_agent_editing_allowed: 'Другие пользователи уже могут редактировать этого ассистента',

View file

@ -450,7 +450,7 @@ export default {
com_endpoint_preset_default_item: 'Varsayılan:',
com_endpoint_preset_default_none: 'Aktif varsayılan hazır ayar yok.',
com_endpoint_preset_title: 'Hazır Ayar',
com_endpoint_preset_saved: 'Kaydedildi!',
com_ui_saved: 'Kaydedildi!',
com_endpoint_preset_default: 'şu anda varsayılan hazır ayar.',
com_endpoint_preset: 'hazır ayar',
com_endpoint_presets: 'hazır ayarlar',

View file

@ -514,7 +514,7 @@ export default {
com_endpoint_preset_default_item: '默认:',
com_endpoint_preset_default_none: '无默认预设可用。',
com_endpoint_preset_title: '预设',
com_endpoint_preset_saved: '保存成功!',
com_ui_saved: '保存成功!',
com_endpoint_preset_default: '现在是默认预设。',
com_endpoint_preset: '预设',
com_endpoint_presets: '预设',
@ -806,7 +806,7 @@ export default {
com_agents_file_search_info:
'启用后系统会告知Agent以下列出的具体文件名使其能够从这些文件中检索相关内容。',
com_agents_execute_code: '运行代码',
com_ui_run_code: '运行代码',
com_agents_file_search_disabled: '必须先创建Agent才能上传文件用于文件搜索。',

View file

@ -502,7 +502,7 @@ export default {
com_endpoint_preset_default_item: '預設值',
com_endpoint_preset_default_none: '無啟用的預設設定。',
com_endpoint_preset_title: '預設項目',
com_endpoint_preset_saved: '已儲存!',
com_ui_saved: '已儲存!',
com_endpoint_preset_default: '現在是預設的預設設定。',
com_endpoint_preset_selected: '已選擇預設設定!',
com_endpoint_preset_selected_title: '已選取!',
@ -612,7 +612,7 @@ export default {
com_agents_missing_provider_model: '請在建立代理前選擇供應商和模型。',
com_agents_enable_file_search: '啟用檔案搜尋',
com_agents_file_search_disabled: '必須先建立代理才能上傳檔案進行檔案搜尋。',
com_agents_execute_code: '執行程式碼',
com_ui_run_code: '執行程式碼',
com_agents_no_access: '您沒有權限編輯此助理',
com_ui_no_changes: '沒有需要更新的變更',
com_ui_agent_already_shared_to_all: '此助理已與所有使用者共享',

View file

@ -1063,7 +1063,7 @@ Write a prompt that is mindful of the nuances in the language with respect to it
- **english**: Preset
- **translated**: Predefinição
- **com_endpoint_preset_saved**:
- **com_ui_saved**:
- **english**: Saved!
- **translated**: Salvo!

View file

@ -1091,7 +1091,7 @@ Write a prompt that is mindful of the nuances in the language with respect to it
- **english**: Preset
- **translated**: Voreinstellung
- **com_endpoint_preset_saved**:
- **com_ui_saved**:
- **english**: Saved!
- **translated**: Gespeichert!

View file

@ -1063,7 +1063,7 @@ Write a prompt that is mindful of the nuances in the language with respect to it
- **english**: Preset
- **translated**: Configuración preestablecida
- **com_endpoint_preset_saved**:
- **com_ui_saved**:
- **english**: Saved!
- **translated**: ¡Guardado!

View file

@ -731,7 +731,7 @@ Write a prompt that is mindful of the nuances in the language with respect to it
- **english**: Preset
- **translated**: Préréglage
- **com_endpoint_preset_saved**:
- **com_ui_saved**:
- **english**: Saved!
- **translated**: Enregistré!

View file

@ -875,7 +875,7 @@ Write a prompt that is mindful of the nuances in the language with respect to it
- **english**: Preset
- **translated**: הגדרה מראש
- **com_endpoint_preset_saved**:
- **com_ui_saved**:
- **english**: Saved!
- **translated**: שמור!

View file

@ -739,7 +739,7 @@ Write a prompt that is mindful of the nuances in the language with respect to it
- **english**: Preset
- **translated**: Preset
- **com_endpoint_preset_saved**:
- **com_ui_saved**:
- **english**: Saved!
- **translated**: Tersimpan!

View file

@ -1219,7 +1219,7 @@ Write a prompt that is mindful of the nuances in the language with respect to it
- **english**: Preset
- **translated**: Preimpostazione
- **com_endpoint_preset_saved**:
- **com_ui_saved**:
- **english**: Saved!
- **translated**: Salvata!

Some files were not shown because too many files have changed in this diff Show more