Merge branch 'main' into feat/multi-lang-Terms-of-service

This commit is contained in:
Ruben Talstra 2025-03-19 13:30:26 +01:00 committed by GitHub
commit b170a57482
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
107 changed files with 3274 additions and 765 deletions

View file

@ -5,6 +5,7 @@ import type { OptionWithIcon, ExtendedFile } from './types';
export type TAgentOption = OptionWithIcon &
Agent & {
knowledge_files?: Array<[string, ExtendedFile]>;
context_files?: Array<[string, ExtendedFile]>;
code_files?: Array<[string, ExtendedFile]>;
};
@ -27,4 +28,5 @@ export type AgentForm = {
provider?: AgentProvider | OptionWithIcon;
agent_ids?: string[];
[AgentCapabilities.artifacts]?: ArtifactModes | string;
recursion_limit?: number;
} & TAgentCapabilities;

View file

@ -131,6 +131,7 @@ export interface DataColumnMeta {
}
export enum Panel {
advanced = 'advanced',
builder = 'builder',
actions = 'actions',
model = 'model',
@ -181,6 +182,7 @@ export type AgentPanelProps = {
activePanel?: string;
action?: t.Action;
actions?: t.Action[];
createMutation: UseMutationResult<t.Agent, Error, t.AgentCreateParams>;
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
setAction: React.Dispatch<React.SetStateAction<t.Action | undefined>>;
endpointsConfig?: t.TEndpointsConfig;
@ -483,6 +485,7 @@ export interface ExtendedFile {
attached?: boolean;
embedded?: boolean;
tool_resource?: string;
metadata?: t.TFile['metadata'];
}
export type ContextType = { navVisible: boolean; setNavVisible: (visible: boolean) => void };

View file

@ -1,7 +1,7 @@
import * as Ariakit from '@ariakit/react';
import React, { useRef, useState, useMemo } from 'react';
import { FileSearch, ImageUpIcon, TerminalSquareIcon } from 'lucide-react';
import { EToolResources, EModelEndpoint } from 'librechat-data-provider';
import { FileSearch, ImageUpIcon, TerminalSquareIcon, FileType2Icon } from 'lucide-react';
import { FileUpload, TooltipAnchor, DropdownPopup } from '~/components/ui';
import { useGetEndpointsQuery } from '~/data-provider';
import { AttachmentIcon } from '~/components/svg';
@ -49,6 +49,17 @@ const AttachFile = ({ isRTL, disabled, handleFileChange }: AttachFileProps) => {
},
];
if (capabilities.includes(EToolResources.ocr)) {
items.push({
label: localize('com_ui_upload_ocr_text'),
onClick: () => {
setToolResource(EToolResources.ocr);
handleUploadClick();
},
icon: <FileType2Icon className="icon-md" />,
});
}
if (capabilities.includes(EToolResources.file_search)) {
items.push({
label: localize('com_ui_upload_file_search'),

View file

@ -1,6 +1,6 @@
import React, { useMemo } from 'react';
import { EModelEndpoint, EToolResources } from 'librechat-data-provider';
import { FileSearch, ImageUpIcon, TerminalSquareIcon } from 'lucide-react';
import { FileSearch, ImageUpIcon, FileType2Icon, TerminalSquareIcon } from 'lucide-react';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { useGetEndpointsQuery } from '~/data-provider';
import useLocalize from '~/hooks/useLocalize';
@ -50,6 +50,12 @@ const DragDropModal = ({ onOptionSelect, setShowModal, files, isVisible }: DragD
value: EToolResources.execute_code,
icon: <TerminalSquareIcon className="icon-md" />,
});
} else if (capability === EToolResources.ocr) {
_options.push({
label: localize('com_ui_upload_ocr_text'),
value: EToolResources.ocr,
icon: <FileType2Icon className="icon-md" />,
});
}
}

View file

@ -19,7 +19,7 @@ const FilePreview = ({
};
className?: string;
}) => {
const radius = 55; // Radius of the SVG circle
const radius = 55;
const circumference = 2 * Math.PI * radius;
const progress = useProgress(
file?.['progress'] ?? 1,
@ -27,16 +27,15 @@ const FilePreview = ({
(file as ExtendedFile | undefined)?.size ?? 1,
);
// Calculate the offset based on the loading progress
const offset = circumference - progress * circumference;
const circleCSSProperties = {
transition: 'stroke-dashoffset 0.5s linear',
};
return (
<div className={cn('size-10 shrink-0 overflow-hidden rounded-xl', className)}>
<div className={cn('relative size-10 shrink-0 overflow-hidden rounded-xl', className)}>
<FileIcon file={file} fileType={fileType} />
<SourceIcon source={file?.source} />
<SourceIcon source={file?.source} isCodeFile={!!file?.['metadata']?.fileIdentifier} />
{progress < 1 && (
<ProgressCircle
circumference={circumference}

View file

@ -1,3 +1,4 @@
import { Terminal, Type, Database } from 'lucide-react';
import { EModelEndpoint, FileSources } from 'librechat-data-provider';
import { MinimalIcon } from '~/components/Endpoints';
import { cn } from '~/utils';
@ -6,9 +7,13 @@ const sourceToEndpoint = {
[FileSources.openai]: EModelEndpoint.openAI,
[FileSources.azure]: EModelEndpoint.azureOpenAI,
};
const sourceToClassname = {
[FileSources.openai]: 'bg-white/75 dark:bg-black/65',
[FileSources.azure]: 'azure-bg-color opacity-85',
[FileSources.execute_code]: 'bg-black text-white opacity-85',
[FileSources.text]: 'bg-blue-500 dark:bg-blue-900 opacity-85 text-white',
[FileSources.vectordb]: 'bg-yellow-700 dark:bg-yellow-900 opacity-85 text-white',
};
const defaultClassName =
@ -16,13 +21,41 @@ const defaultClassName =
export default function SourceIcon({
source,
isCodeFile,
className = defaultClassName,
}: {
source?: FileSources;
isCodeFile?: boolean;
className?: string;
}) {
if (source === FileSources.local || source === FileSources.firebase) {
return null;
if (isCodeFile === true) {
return (
<div className={cn(className, sourceToClassname[FileSources.execute_code] ?? '')}>
<span className="flex items-center justify-center">
<Terminal className="h-3 w-3" />
</span>
</div>
);
}
if (source === FileSources.text) {
return (
<div className={cn(className, sourceToClassname[source] ?? '')}>
<span className="flex items-center justify-center">
<Type className="h-3 w-3" />
</span>
</div>
);
}
if (source === FileSources.vectordb) {
return (
<div className={cn(className, sourceToClassname[source] ?? '')}>
<span className="flex items-center justify-center">
<Database className="h-3 w-3" />
</span>
</div>
);
}
const endpoint = sourceToEndpoint[source ?? ''];
@ -31,7 +64,7 @@ export default function SourceIcon({
return null;
}
return (
<button type="button" className={cn(className, sourceToClassname[source ?? ''] ?? '')}>
<div className={cn(className, sourceToClassname[source ?? ''] ?? '')}>
<span className="flex items-center justify-center">
<MinimalIcon
endpoint={endpoint}
@ -40,6 +73,6 @@ export default function SourceIcon({
iconClassName="h-3 w-3"
/>
</span>
</button>
</div>
);
}

View file

@ -75,7 +75,7 @@ const MenuItem: FC<MenuItemProps> = ({
{showIconInMenu && <SpecIcon currentSpec={spec} endpointsConfig={endpointsConfig} />}
<div>
{title}
<div className="text-token-text-tertiary">{description}</div>
<div className="text-text-secondary">{description}</div>
</div>
</div>
</div>

View file

@ -139,6 +139,7 @@ const ContentParts = memo(
isSubmitting={isSubmitting}
key={`part-${messageId}-${idx}`}
isCreatedByUser={isCreatedByUser}
isLast={idx === content.length - 1}
showCursor={idx === content.length - 1 && isLast}
/>
</MessageContext.Provider>

View file

@ -166,15 +166,12 @@ export const p: React.ElementType = memo(({ children }: TParagraphProps) => {
return <p className="mb-2 whitespace-pre-wrap">{children}</p>;
});
const cursor = ' ';
type TContentProps = {
content: string;
showCursor?: boolean;
isLatestMessage: boolean;
};
const Markdown = memo(({ content = '', showCursor, isLatestMessage }: TContentProps) => {
const Markdown = memo(({ content = '', isLatestMessage }: TContentProps) => {
const LaTeXParsing = useRecoilValue<boolean>(store.LaTeXParsing);
const isInitializing = content === '';
@ -240,7 +237,7 @@ const Markdown = memo(({ content = '', showCursor, isLatestMessage }: TContentPr
}
}
>
{isLatestMessage && (showCursor ?? false) ? currentContent + cursor : currentContent}
{currentContent}
</ReactMarkdown>
</CodeBlockProvider>
</ArtifactProvider>

View file

@ -83,9 +83,7 @@ const DisplayMessage = ({ text, isCreatedByUser, message, showCursor }: TDisplay
let content: React.ReactElement;
if (!isCreatedByUser) {
content = (
<Markdown content={text} showCursor={showCursorState} isLatestMessage={isLatestMessage} />
);
content = <Markdown content={text} isLatestMessage={isLatestMessage} />;
} else if (enableUserMsgMarkdown) {
content = <MarkdownLite content={text} />;
} else {

View file

@ -8,9 +8,11 @@ import {
import { memo } from 'react';
import type { TMessageContentParts, TAttachment } from 'librechat-data-provider';
import { ErrorMessage } from './MessageContent';
import AgentUpdate from './Parts/AgentUpdate';
import ExecuteCode from './Parts/ExecuteCode';
import RetrievalCall from './RetrievalCall';
import Reasoning from './Parts/Reasoning';
import EmptyText from './Parts/EmptyText';
import CodeAnalyze from './CodeAnalyze';
import Container from './Container';
import ToolCall from './ToolCall';
@ -20,145 +22,159 @@ import Image from './Image';
type PartProps = {
part?: TMessageContentParts;
isLast?: boolean;
isSubmitting: boolean;
showCursor: boolean;
isCreatedByUser: boolean;
attachments?: TAttachment[];
};
const Part = memo(({ part, isSubmitting, attachments, showCursor, isCreatedByUser }: PartProps) => {
if (!part) {
return null;
}
if (part.type === ContentTypes.ERROR) {
return (
<ErrorMessage
text={part[ContentTypes.ERROR] ?? 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.THINK) {
const reasoning = typeof part.think === 'string' ? part.think : part.think.value;
if (typeof reasoning !== 'string') {
return null;
}
return <Reasoning reasoning={reasoning} />;
} else if (part.type === ContentTypes.TOOL_CALL) {
const toolCall = part[ContentTypes.TOOL_CALL];
if (!toolCall) {
const Part = memo(
({ part, isSubmitting, attachments, isLast, showCursor, isCreatedByUser }: PartProps) => {
if (!part) {
return null;
}
const isToolCall =
'args' in toolCall && (!toolCall.type || toolCall.type === ToolCallTypes.TOOL_CALL);
if (isToolCall && toolCall.name === Tools.execute_code) {
if (part.type === ContentTypes.ERROR) {
return (
<ExecuteCode
args={typeof toolCall.args === 'string' ? toolCall.args : ''}
output={toolCall.output ?? ''}
initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting}
attachments={attachments}
<ErrorMessage
text={part[ContentTypes.ERROR] ?? part[ContentTypes.TEXT]?.value}
className="my-2"
/>
);
} else if (isToolCall) {
} else if (part.type === ContentTypes.AGENT_UPDATE) {
return (
<ToolCall
args={toolCall.args ?? ''}
name={toolCall.name || ''}
output={toolCall.output ?? ''}
initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting}
attachments={attachments}
auth={toolCall.auth}
expires_at={toolCall.expires_at}
/>
);
} 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 (
<>
<AgentUpdate currentAgentId={part[ContentTypes.AGENT_UPDATE]?.agentId} />
{isLast && showCursor && (
<Container>
<Text text={''} isCreatedByUser={isCreatedByUser} showCursor={showCursor} />
<EmptyText />
</Container>
);
}
)}
</>
);
} 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.THINK) {
const reasoning = typeof part.think === 'string' ? part.think : part.think.value;
if (typeof reasoning !== 'string') {
return null;
}
return <Reasoning reasoning={reasoning} />;
} else if (part.type === ContentTypes.TOOL_CALL) {
const toolCall = part[ContentTypes.TOOL_CALL];
if (!toolCall) {
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}
attachments={attachments}
auth={toolCall.auth}
expires_at={toolCall.expires_at}
/>
);
} 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;
}
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 (
<ToolCall
initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting}
args={toolCall.function.arguments as string}
name={toolCall.function.name}
output={toolCall.function.output}
<Image
imagePath={imageFile.filepath}
height={height}
width={width}
altText={imageFile.filename ?? 'Uploaded Image'}
placeholderDimensions={{
height: height + 'px',
width: width + 'px',
}}
/>
);
}
} 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,39 @@
import React, { useMemo } from 'react';
import { EModelEndpoint } from 'librechat-data-provider';
import { useAgentsMapContext } from '~/Providers';
import Icon from '~/components/Endpoints/Icon';
interface AgentUpdateProps {
currentAgentId: string;
}
const AgentUpdate: React.FC<AgentUpdateProps> = ({ currentAgentId }) => {
const agentsMap = useAgentsMapContext() || {};
const currentAgent = useMemo(() => agentsMap[currentAgentId], [agentsMap, currentAgentId]);
if (!currentAgentId) {
return null;
}
return (
<div className="relative">
<div className="absolute -left-6 flex h-full w-4 items-center justify-center">
<div className="relative h-full w-4">
<div className="absolute left-0 top-0 h-1/2 w-px border border-border-medium"></div>
<div className="absolute left-0 top-1/2 h-px w-3 border border-border-medium"></div>
</div>
</div>
<div className="my-4 flex items-center gap-2">
<div className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full">
<Icon
endpoint={EModelEndpoint.agents}
agentName={currentAgent?.name ?? ''}
iconURL={currentAgent?.avatar?.filepath}
isCreatedByUser={false}
/>
</div>
<div className="font-medium text-text-primary">{currentAgent?.name}</div>
</div>
</div>
);
};
export default AgentUpdate;

View file

@ -0,0 +1,17 @@
import { memo } from 'react';
const EmptyTextPart = memo(() => {
return (
<div className="text-message mb-[0.625rem] flex min-h-[20px] flex-col items-start gap-3 overflow-visible">
<div className="markdown prose dark:prose-invert light w-full break-words dark:text-gray-100">
<div className="absolute">
<p className="submitting relative">
<span className="result-thinking" />
</p>
</div>
</div>
</div>
);
});
export default EmptyTextPart;

View file

@ -29,9 +29,7 @@ const TextPart = memo(({ text, isCreatedByUser, showCursor }: TextPartProps) =>
const content: ContentType = useMemo(() => {
if (!isCreatedByUser) {
return (
<Markdown content={text} showCursor={showCursorState} isLatestMessage={isLatestMessage} />
);
return <Markdown content={text} isLatestMessage={isLatestMessage} />;
} else if (enableUserMsgMarkdown) {
return <MarkdownLite content={text} />;
} else {

View file

@ -142,7 +142,7 @@ const AdminSettings = () => {
<Button
size={'sm'}
variant={'outline'}
className="btn btn-neutral border-token-border-light relative mb-4 h-9 w-full gap-1 rounded-lg font-medium"
className="btn btn-neutral border-token-border-light relative h-9 w-full gap-1 rounded-lg font-medium"
>
<ShieldEllipsis className="cursor-pointer" aria-hidden="true" />
{localize('com_ui_admin_settings')}

View file

@ -0,0 +1,27 @@
import React from 'react';
import { Settings2 } from 'lucide-react';
import { Button } from '~/components/ui';
import { useLocalize } from '~/hooks';
import { Panel } from '~/common';
interface AdvancedButtonProps {
setActivePanel: (panel: Panel) => void;
}
const AdvancedButton: React.FC<AdvancedButtonProps> = ({ setActivePanel }) => {
const localize = useLocalize();
return (
<Button
size={'sm'}
variant={'outline'}
className="btn btn-neutral border-token-border-light relative h-9 w-full gap-1 rounded-lg font-medium"
onClick={() => setActivePanel(Panel.advanced)}
>
<Settings2 className="h-4 w-4 cursor-pointer" aria-hidden="true" />
{localize('com_ui_advanced')}
</Button>
);
};
export default AdvancedButton;

View file

@ -0,0 +1,55 @@
import { useMemo } from 'react';
import { ChevronLeft } from 'lucide-react';
import { AgentCapabilities } from 'librechat-data-provider';
import { useFormContext, Controller } from 'react-hook-form';
import type { AgentForm, AgentPanelProps } from '~/common';
import MaxAgentSteps from './MaxAgentSteps';
import AgentChain from './AgentChain';
import { useLocalize } from '~/hooks';
import { Panel } from '~/common';
export default function AdvancedPanel({
agentsConfig,
setActivePanel,
}: Pick<AgentPanelProps, 'setActivePanel' | 'agentsConfig'>) {
const localize = useLocalize();
const methods = useFormContext<AgentForm>();
const { control, watch } = methods;
const currentAgentId = watch('id');
const chainEnabled = useMemo(
() => agentsConfig?.capabilities.includes(AgentCapabilities.chain) ?? false,
[agentsConfig],
);
return (
<div className="scrollbar-gutter-stable h-full min-h-[40vh] overflow-auto pb-12 text-sm">
<div className="advanced-panel relative flex flex-col items-center px-16 py-4 text-center">
<div className="absolute left-0 top-4">
<button
type="button"
className="btn btn-neutral relative"
onClick={() => {
setActivePanel(Panel.builder);
}}
>
<div className="advanced-panel-content flex w-full items-center justify-center gap-2">
<ChevronLeft />
</div>
</button>
</div>
<div className="mb-2 mt-2 text-xl font-medium">{localize('com_ui_advanced_settings')}</div>
</div>
<div className="flex flex-col gap-4 px-2">
<MaxAgentSteps />
{chainEnabled && (
<Controller
name="agent_ids"
control={control}
defaultValue={[]}
render={({ field }) => <AgentChain field={field} currentAgentId={currentAgentId} />}
/>
)}
</div>
</div>
);
}

View file

@ -0,0 +1,179 @@
import { X, Link2, PlusCircle } from 'lucide-react';
import { EModelEndpoint } from 'librechat-data-provider';
import React, { useState, useMemo, useCallback, useEffect } from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import type { AgentForm, OptionWithIcon } from '~/common';
import ControlCombobox from '~/components/ui/ControlCombobox';
import { HoverCard, HoverCardPortal, HoverCardContent, HoverCardTrigger } from '~/components/ui';
import { CircleHelpIcon } from '~/components/svg';
import { useAgentsMapContext } from '~/Providers';
import Icon from '~/components/Endpoints/Icon';
import { useLocalize } from '~/hooks';
import { ESide } from '~/common';
interface AgentChainProps {
field: ControllerRenderProps<AgentForm, 'agent_ids'>;
currentAgentId: string;
}
/** TODO: make configurable */
const MAX_AGENTS = 10;
const AgentChain: React.FC<AgentChainProps> = ({ field, currentAgentId }) => {
const localize = useLocalize();
const [newAgentId, setNewAgentId] = useState('');
const agentsMap = useAgentsMapContext() || {};
const agentIds = field.value || [];
const agents = useMemo(() => Object.values(agentsMap), [agentsMap]);
const selectableAgents = useMemo(
() =>
agents
.filter((agent) => agent?.id !== currentAgentId)
.map(
(agent) =>
({
label: agent?.name || '',
value: agent?.id,
icon: (
<Icon
endpoint={EModelEndpoint.agents}
agentName={agent?.name ?? ''}
iconURL={agent?.avatar?.filepath}
isCreatedByUser={false}
/>
),
}) as OptionWithIcon,
),
[agents, currentAgentId],
);
const getAgentDetails = useCallback((id: string) => agentsMap[id], [agentsMap]);
useEffect(() => {
if (newAgentId && agentIds.length < MAX_AGENTS) {
field.onChange([...agentIds, newAgentId]);
setNewAgentId('');
}
}, [newAgentId, agentIds, field]);
const removeAgentAt = (index: number) => {
field.onChange(agentIds.filter((_, i) => i !== index));
};
const updateAgentAt = (index: number, id: string) => {
const updated = [...agentIds];
updated[index] = id;
field.onChange(updated);
};
return (
<HoverCard openDelay={50}>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<label className="font-semibold text-text-primary">
{localize('com_ui_agent_chain')}
</label>
<HoverCardTrigger>
<CircleHelpIcon className="h-4 w-4 text-text-tertiary" />
</HoverCardTrigger>
</div>
<div className="text-xs text-text-secondary">
{agentIds.length} / {MAX_AGENTS}
</div>
</div>
<div className="space-y-1">
{/* Current fixed agent */}
<div className="flex h-10 items-center justify-between rounded-md border border-border-medium bg-surface-primary-contrast px-3 py-2">
<div className="flex items-center gap-2">
<div className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full">
<Icon
endpoint={EModelEndpoint.agents}
agentName={getAgentDetails(currentAgentId)?.name ?? ''}
iconURL={getAgentDetails(currentAgentId)?.avatar?.filepath}
isCreatedByUser={false}
/>
</div>
<div className="font-medium text-text-primary">
{getAgentDetails(currentAgentId)?.name}
</div>
</div>
</div>
{<Link2 className="mx-auto text-text-secondary" size={14} />}
{agentIds.map((agentId, idx) => (
<React.Fragment key={agentId}>
<div className="flex h-10 items-center gap-2 rounded-md border border-border-medium bg-surface-tertiary pr-2">
<ControlCombobox
isCollapsed={false}
ariaLabel={localize('com_ui_agent_var', { 0: localize('com_ui_select') })}
selectedValue={agentId}
setValue={(id) => updateAgentAt(idx, id)}
selectPlaceholder={localize('com_ui_agent_var', { 0: localize('com_ui_select') })}
searchPlaceholder={localize('com_ui_agent_var', { 0: localize('com_ui_search') })}
items={selectableAgents}
displayValue={getAgentDetails(agentId)?.name ?? ''}
SelectIcon={
<Icon
endpoint={EModelEndpoint.agents}
isCreatedByUser={false}
agentName={getAgentDetails(agentId)?.name ?? ''}
iconURL={getAgentDetails(agentId)?.avatar?.filepath}
/>
}
className="flex-1 border-border-heavy"
containerClassName="px-0"
/>
{/* Future Settings button? */}
{/* <button className="hover:bg-surface-hover p-1 rounded transition">
<Settings size={16} className="text-text-secondary" />
</button> */}
<button
className="rounded-xl p-1 transition hover:bg-surface-hover"
onClick={() => removeAgentAt(idx)}
>
<X size={18} className="text-text-secondary" />
</button>
</div>
{idx < agentIds.length - 1 && (
<Link2 className="mx-auto text-text-secondary" size={14} />
)}
</React.Fragment>
))}
{agentIds.length < MAX_AGENTS && (
<>
{agentIds.length > 0 && <Link2 className="mx-auto text-text-secondary" size={14} />}
<ControlCombobox
isCollapsed={false}
ariaLabel={localize('com_ui_agent_var', { 0: localize('com_ui_add') })}
selectedValue=""
setValue={setNewAgentId}
selectPlaceholder={localize('com_ui_agent_var', { 0: localize('com_ui_add') })}
searchPlaceholder={localize('com_ui_agent_var', { 0: localize('com_ui_search') })}
items={selectableAgents}
className="h-10 w-full border-dashed border-border-heavy text-center text-text-secondary hover:text-text-primary"
containerClassName="px-0"
SelectIcon={<PlusCircle size={16} className="text-text-secondary" />}
/>
</>
)}
{agentIds.length >= MAX_AGENTS && (
<p className="pt-1 text-center text-xs italic text-text-tertiary">
{localize('com_ui_agent_chain_max', { 0: MAX_AGENTS })}
</p>
)}
</div>
<HoverCardPortal>
<HoverCardContent side={ESide.Top} className="w-80">
<div className="space-y-2">
<p className="text-sm text-text-secondary">{localize('com_ui_agent_chain_info')}</p>
</div>
</HoverCardContent>
</HoverCardPortal>
</HoverCard>
);
};
export default AgentChain;

View file

@ -0,0 +1,52 @@
import { useFormContext, Controller } from 'react-hook-form';
import type { AgentForm } from '~/common';
import {
HoverCard,
FormInput,
HoverCardPortal,
HoverCardContent,
HoverCardTrigger,
} from '~/components/ui';
import { CircleHelpIcon } from '~/components/svg';
import { useLocalize } from '~/hooks';
import { ESide } from '~/common';
export default function AdvancedPanel() {
const localize = useLocalize();
const methods = useFormContext<AgentForm>();
const { control } = methods;
return (
<HoverCard openDelay={50}>
<Controller
name="recursion_limit"
control={control}
render={({ field }) => (
<FormInput
field={field}
containerClass="w-1/2"
inputClass="w-full"
label={localize('com_ui_agent_recursion_limit')}
placeholder={localize('com_nav_theme_system')}
type="number"
labelClass="w-fit"
labelAdjacent={
<HoverCardTrigger>
<CircleHelpIcon className="h-4 w-4 text-text-tertiary" />
</HoverCardTrigger>
}
/>
)}
/>
<HoverCardPortal>
<HoverCardContent side={ESide.Top} className="w-80">
<div className="space-y-2">
<p className="text-sm text-text-secondary">
{localize('com_ui_agent_recursion_limit_info')}
</p>
</div>
</HoverCardContent>
</HoverCardPortal>
</HoverCard>
);
}

View file

@ -1,31 +1,19 @@
import React, { useState, useMemo, useCallback } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { Controller, useWatch, useFormContext } from 'react-hook-form';
import {
QueryKeys,
SystemRoles,
Permissions,
EModelEndpoint,
PermissionTypes,
AgentCapabilities,
} from 'librechat-data-provider';
import { QueryKeys, EModelEndpoint, AgentCapabilities } from 'librechat-data-provider';
import type { TPlugin } from 'librechat-data-provider';
import type { AgentForm, AgentPanelProps, IconComponentTypes } 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 DuplicateAgent from './DuplicateAgent';
import { processAgentOption } from '~/utils';
import AdminSettings from './AdminSettings';
import DeleteButton from './DeleteButton';
import AgentAvatar from './AgentAvatar';
import { Spinner } from '~/components';
import FileContext from './FileContext';
import { useLocalize } from '~/hooks';
import FileSearch from './FileSearch';
import ShareAgent from './ShareAgent';
import Artifacts from './Artifacts';
import AgentTool from './AgentTool';
import CodeForm from './Code/Form';
@ -42,11 +30,10 @@ export default function AgentConfig({
setAction,
actions = [],
agentsConfig,
endpointsConfig,
createMutation,
setActivePanel,
setCurrentAgentId,
endpointsConfig,
}: AgentPanelProps) {
const { user } = useAuthContext();
const fileMap = useFileMapContext();
const queryClient = useQueryClient();
@ -65,11 +52,6 @@ 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],
@ -82,6 +64,10 @@ export default function AgentConfig({
() => agentsConfig?.capabilities.includes(AgentCapabilities.artifacts) ?? false,
[agentsConfig],
);
const ocrEnabled = useMemo(
() => agentsConfig?.capabilities.includes(AgentCapabilities.ocr) ?? false,
[agentsConfig],
);
const fileSearchEnabled = useMemo(
() => agentsConfig?.capabilities.includes(AgentCapabilities.file_search) ?? false,
[agentsConfig],
@ -91,6 +77,26 @@ export default function AgentConfig({
[agentsConfig],
);
const context_files = useMemo(() => {
if (typeof agent === 'string') {
return [];
}
if (agent?.id !== agent_id) {
return [];
}
if (agent.context_files) {
return agent.context_files;
}
const _agent = processAgentOption({
agent,
fileMap,
});
return _agent.context_files ?? [];
}, [agent, agent_id, fileMap]);
const knowledge_files = useMemo(() => {
if (typeof agent === 'string') {
return [];
@ -131,46 +137,6 @@ export default function AgentConfig({
return _agent.code_files ?? [];
}, [agent, agent_id, fileMap]);
/* Mutations */
const update = useUpdateAgentMutation({
onSuccess: (data) => {
showToast({
message: `${localize('com_assistants_update_success')} ${
data.name ?? localize('com_ui_agent')
}`,
});
},
onError: (err) => {
const error = err as Error;
showToast({
message: `${localize('com_agents_update_error')}${
error.message ? ` ${localize('com_ui_error')}: ${error.message}` : ''
}`,
status: 'error',
});
},
});
const create = useCreateAgentMutation({
onSuccess: (data) => {
setCurrentAgentId(data.id);
showToast({
message: `${localize('com_assistants_create_success')} ${
data.name ?? localize('com_ui_agent')
}`,
});
},
onError: (err) => {
const error = err as Error;
showToast({
message: `${localize('com_agents_create_error')}${
error.message ? ` ${localize('com_ui_error')}: ${error.message}` : ''
}`,
status: 'error',
});
},
});
const handleAddActions = useCallback(() => {
if (!agent_id) {
showToast({
@ -200,26 +166,14 @@ export default function AgentConfig({
Icon = icons[iconKey];
}
const renderSaveButton = () => {
if (create.isLoading || update.isLoading) {
return <Spinner className="icon-md" aria-hidden="true" />;
}
if (agent_id) {
return localize('com_ui_save');
}
return localize('com_ui_create');
};
return (
<>
<div className="h-auto bg-white px-4 pb-8 pt-3 dark:bg-transparent">
<div className="h-auto bg-white px-4 pt-3 dark:bg-transparent">
{/* Avatar & Name */}
<div className="mb-4">
<AgentAvatar
createMutation={create}
agent_id={agent_id}
createMutation={createMutation}
avatar={agent?.['avatar'] ?? null}
/>
<label className={labelClass} htmlFor="name">
@ -334,17 +288,19 @@ export default function AgentConfig({
</div>
</button>
</div>
{(codeEnabled || fileSearchEnabled || artifactsEnabled) && (
{(codeEnabled || fileSearchEnabled || artifactsEnabled || ocrEnabled) && (
<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} />}
{/* File Context (OCR) */}
{ocrEnabled && <FileContext agent_id={agent_id} files={context_files} />}
{/* Artifacts */}
{artifactsEnabled && <Artifacts />}
{/* File Search */}
{fileSearchEnabled && <FileSearch agent_id={agent_id} files={knowledge_files} />}
</div>
)}
{/* Agent Tools & Actions */}
@ -404,34 +360,6 @@ export default function AgentConfig({
</div>
</div>
</div>
{user?.role === SystemRoles.ADMIN && <AdminSettings />}
{/* Context Button */}
<div className="flex items-center justify-end gap-2">
<DeleteButton
agent_id={agent_id}
setCurrentAgentId={setCurrentAgentId}
createMutation={create}
/>
{(agent?.author === user?.id || user?.role === SystemRoles.ADMIN) &&
hasAccessToShareAgents && (
<ShareAgent
agent_id={agent_id}
agentName={agent?.name ?? ''}
projectIds={agent?.projectIds ?? []}
isCollaborative={agent?.isCollaborative}
/>
)}
{agent && agent.author === user?.id && <DuplicateAgent agent_id={agent_id} />}
{/* Submit Button */}
<button
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}
>
{renderSaveButton()}
</button>
</div>
</div>
<ToolSelectDialog
isOpen={showToolDialog}

View file

@ -0,0 +1,86 @@
import React from 'react';
import { useWatch, useFormContext } from 'react-hook-form';
import { SystemRoles, Permissions, PermissionTypes } from 'librechat-data-provider';
import type { AgentForm, AgentPanelProps } from '~/common';
import { useLocalize, useAuthContext, useHasAccess } from '~/hooks';
import { useUpdateAgentMutation } from '~/data-provider';
import AdvancedButton from './Advanced/AdvancedButton';
import DuplicateAgent from './DuplicateAgent';
import AdminSettings from './AdminSettings';
import DeleteButton from './DeleteButton';
import { Spinner } from '~/components';
import ShareAgent from './ShareAgent';
import { Panel } from '~/common';
export default function AgentFooter({
activePanel,
createMutation,
updateMutation,
setActivePanel,
setCurrentAgentId,
}: Pick<
AgentPanelProps,
'setCurrentAgentId' | 'createMutation' | 'activePanel' | 'setActivePanel'
> & {
updateMutation: ReturnType<typeof useUpdateAgentMutation>;
}) {
const localize = useLocalize();
const { user } = useAuthContext();
const methods = useFormContext<AgentForm>();
const { control } = methods;
const agent = useWatch({ control, name: 'agent' });
const agent_id = useWatch({ control, name: 'id' });
const hasAccessToShareAgents = useHasAccess({
permissionType: PermissionTypes.AGENTS,
permission: Permissions.SHARED_GLOBAL,
});
const renderSaveButton = () => {
if (createMutation.isLoading || updateMutation.isLoading) {
return <Spinner className="icon-md" aria-hidden="true" />;
}
if (agent_id) {
return localize('com_ui_save');
}
return localize('com_ui_create');
};
return (
<div className="mx-1 mb-1 flex w-full flex-col gap-2">
{activePanel !== Panel.advanced && <AdvancedButton setActivePanel={setActivePanel} />}
{user?.role === SystemRoles.ADMIN && <AdminSettings />}
{/* Context Button */}
<div className="flex items-center justify-end gap-2">
<DeleteButton
agent_id={agent_id}
setCurrentAgentId={setCurrentAgentId}
createMutation={createMutation}
/>
{(agent?.author === user?.id || user?.role === SystemRoles.ADMIN) &&
hasAccessToShareAgents && (
<ShareAgent
agent_id={agent_id}
agentName={agent?.name ?? ''}
projectIds={agent?.projectIds ?? []}
isCollaborative={agent?.isCollaborative}
/>
)}
{agent && agent.author === user?.id && <DuplicateAgent agent_id={agent_id} />}
{/* Submit Button */}
<button
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={createMutation.isLoading || updateMutation.isLoading}
aria-busy={createMutation.isLoading || updateMutation.isLoading}
>
{renderSaveButton()}
</button>
</div>
</div>
);
}

View file

@ -19,8 +19,10 @@ import { useSelectAgent, useLocalize, useAuthContext } from '~/hooks';
import AgentPanelSkeleton from './AgentPanelSkeleton';
import { createProviderOption } from '~/utils';
import { useToastContext } from '~/Providers';
import AdvancedPanel from './Advanced/AdvancedPanel';
import AgentConfig from './AgentConfig';
import AgentSelect from './AgentSelect';
import AgentFooter from './AgentFooter';
import { Button } from '~/components';
import ModelPanel from './ModelPanel';
import { Panel } from '~/common';
@ -130,6 +132,7 @@ export default function AgentPanel({
agent_ids,
end_after_tools,
hide_sequential_outputs,
recursion_limit,
} = data;
const model = _model ?? '';
@ -151,6 +154,7 @@ export default function AgentPanel({
agent_ids,
end_after_tools,
hide_sequential_outputs,
recursion_limit,
},
});
return;
@ -175,6 +179,7 @@ export default function AgentPanel({
agent_ids,
end_after_tools,
hide_sequential_outputs,
recursion_limit,
});
},
[agent_id, create, update, showToast, localize],
@ -276,12 +281,25 @@ export default function AgentPanel({
<AgentConfig
actions={actions}
setAction={setAction}
createMutation={create}
agentsConfig={agentsConfig}
setActivePanel={setActivePanel}
endpointsConfig={endpointsConfig}
setCurrentAgentId={setCurrentAgentId}
/>
)}
{canEditAgent && !agentQuery.isInitialLoading && activePanel === Panel.advanced && (
<AdvancedPanel setActivePanel={setActivePanel} agentsConfig={agentsConfig} />
)}
{canEditAgent && !agentQuery.isInitialLoading && (
<AgentFooter
createMutation={create}
updateMutation={update}
activePanel={activePanel}
setActivePanel={setActivePanel}
setCurrentAgentId={setCurrentAgentId}
/>
)}
</form>
</FormProvider>
);

View file

@ -3,7 +3,7 @@ import { Skeleton } from '~/components/ui';
export default function AgentPanelSkeleton() {
return (
<div className="h-auto bg-white px-4 pb-8 pt-3 dark:bg-transparent">
<div className="h-auto bg-white dark:bg-transparent">
{/* Avatar */}
<div className="mb-4">
<div className="flex w-full items-center justify-center gap-4">

View file

@ -81,10 +81,29 @@ export default function AgentSelect({
return;
}
if (capabilities[name] !== undefined) {
formValues[name] = value;
return;
}
if (
name === 'agent_ids' &&
Array.isArray(value) &&
value.every((item) => typeof item === 'string')
) {
formValues[name] = value;
return;
}
if (!keys.has(name)) {
return;
}
if (name === 'recursion_limit' && typeof value === 'number') {
formValues[name] = value;
return;
}
if (typeof value !== 'number' && typeof value !== 'object') {
formValues[name] = value;
}

View file

@ -0,0 +1,128 @@
import { useState, useRef } from 'react';
import {
EModelEndpoint,
EToolResources,
mergeFileConfig,
fileConfig as defaultFileConfig,
} from 'librechat-data-provider';
import type { ExtendedFile } from '~/common';
import { useFileHandling, useLocalize, useLazyEffect } from '~/hooks';
import FileRow from '~/components/Chat/Input/Files/FileRow';
import { useGetFileConfig } from '~/data-provider';
import { HoverCard, HoverCardContent, HoverCardPortal, HoverCardTrigger } from '~/components/ui';
import { AttachmentIcon, CircleHelpIcon } from '~/components/svg';
import { useChatContext } from '~/Providers';
import { ESide } from '~/common';
export default function FileContext({
agent_id,
files: _files,
}: {
agent_id: string;
files?: [string, ExtendedFile][];
}) {
const localize = useLocalize();
const { setFilesLoading } = useChatContext();
const fileInputRef = useRef<HTMLInputElement>(null);
const [files, setFiles] = useState<Map<string, ExtendedFile>>(new Map());
const { data: fileConfig = defaultFileConfig } = useGetFileConfig({
select: (data) => mergeFileConfig(data),
});
const { handleFileChange } = useFileHandling({
overrideEndpoint: EModelEndpoint.agents,
additionalMetadata: { agent_id, tool_resource: EToolResources.ocr },
fileSetter: setFiles,
});
useLazyEffect(
() => {
if (_files) {
setFiles(new Map(_files));
}
},
[_files],
750,
);
const endpointFileConfig = fileConfig.endpoints[EModelEndpoint.agents];
const isUploadDisabled = endpointFileConfig.disabled ?? false;
if (isUploadDisabled) {
return null;
}
const handleButtonClick = () => {
// necessary to reset the input
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
fileInputRef.current?.click();
};
return (
<div className="w-full">
<HoverCard openDelay={50}>
<div className="mb-2 flex items-center gap-2">
<HoverCardTrigger asChild>
<span className="flex items-center gap-2">
<label className="text-token-text-primary block font-medium">
{localize('com_agents_file_context')}
</label>
<CircleHelpIcon className="h-4 w-4 text-text-tertiary" />
</span>
</HoverCardTrigger>
<HoverCardPortal>
<HoverCardContent side={ESide.Top} className="w-80">
<div className="space-y-2">
<p className="text-sm text-text-secondary">
{localize('com_agents_file_context_info')}
</p>
</div>
</HoverCardContent>
</HoverCardPortal>
</div>
</HoverCard>
<div className="flex flex-col gap-3">
{/* File Context (OCR) Files */}
<FileRow
files={files}
setFiles={setFiles}
setFilesLoading={setFilesLoading}
agent_id={agent_id}
tool_resource={EToolResources.ocr}
Wrapper={({ children }) => <div className="flex flex-wrap gap-2">{children}</div>}
/>
<div>
<button
type="button"
disabled={!agent_id}
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">
<AttachmentIcon className="text-token-text-primary h-4 w-4" />
<input
multiple={true}
type="file"
style={{ display: 'none' }}
tabIndex={-1}
ref={fileInputRef}
disabled={!agent_id}
onChange={handleFileChange}
/>
{localize('com_ui_upload_file_context')}
</div>
</button>
</div>
{/* Disabled Message */}
{agent_id ? null : (
<div className="text-xs text-text-secondary">
{localize('com_agents_file_context_disabled')}
</div>
)}
</div>
</div>
);
}

View file

@ -77,7 +77,7 @@ export default function Parameters({
};
return (
<div className="scrollbar-gutter-stable h-full min-h-[50vh] overflow-auto pb-12 text-sm">
<div className="mx-1 mb-1 flex h-full min-h-[50vh] w-full flex-col gap-2 text-sm">
<div className="model-panel relative flex flex-col items-center px-16 py-4 text-center">
<div className="absolute left-0 top-4">
<button
@ -224,19 +224,17 @@ export default function Parameters({
);
})}
</div>
{/* Reset Parameters Button */}
<div className="mt-6 flex justify-center">
<button
type="button"
onClick={handleResetParameters}
className="btn btn-neutral flex w-full items-center justify-center gap-2 px-4 py-2 text-sm"
>
<RotateCcw className="h-4 w-4" aria-hidden="true" />
{localize('com_ui_reset_var', { 0: localize('com_ui_model_parameters') })}
</button>
</div>
</div>
)}
{/* Reset Parameters Button */}
<button
type="button"
onClick={handleResetParameters}
className="btn btn-neutral my-1 flex w-full items-center justify-center gap-2 px-4 py-2 text-sm"
>
<RotateCcw className="h-4 w-4" aria-hidden="true" />
{localize('com_ui_reset_var', { 0: localize('com_ui_model_parameters') })}
</button>
</div>
);
}

View file

@ -1,74 +0,0 @@
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={() =>
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

@ -1,153 +0,0 @@
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

@ -1,21 +1,23 @@
import { ArrowUpDown } from 'lucide-react';
import type { ColumnDef } from '@tanstack/react-table';
import type { TFile } from 'librechat-data-provider';
import useLocalize from '~/hooks/useLocalize';
import PanelFileCell from './PanelFileCell';
import { Button } from '~/components/ui';
import { formatDate } from '~/utils';
export const columns: ColumnDef<TFile>[] = [
export const columns: ColumnDef<TFile | undefined>[] = [
{
accessorKey: 'filename',
header: ({ column }) => {
const localize = useLocalize();
return (
<Button
variant="ghost"
className="hover:bg-surface-hover"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
>
Name
{localize('com_ui_name')}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@ -31,20 +33,21 @@ export const columns: ColumnDef<TFile>[] = [
size: '10%',
},
header: ({ column }) => {
const localize = useLocalize();
return (
<Button
variant="ghost"
className="hover:bg-surface-hover"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
>
Date
{localize('com_ui_date')}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
},
cell: ({ row }) => (
<span className="flex justify-end text-xs">
{formatDate(row.original.updatedAt?.toString() ?? '')}
{formatDate(row.original?.updatedAt?.toString() ?? '')}
</span>
),
},

View file

@ -6,7 +6,6 @@ import { getFileType } from '~/utils';
export default function PanelFileCell({ row }: { row: Row<TFile | undefined> }) {
const file = row.original;
return (
<div className="flex w-full items-center gap-2">
{file?.type.startsWith('image') === true ? (

View file

@ -159,6 +159,7 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
filename: fileData.filename,
source: fileData.source,
size: fileData.bytes,
metadata: fileData.metadata,
});
},
[addFile, fileMap, conversation, localize, showToast, fileConfig.endpoints],

View file

@ -665,6 +665,7 @@ export const settings: Record<string, SettingsConfiguration | undefined> = {
[`${EModelEndpoint.bedrock}-${BedrockProviders.Meta}`]: bedrockGeneral,
[`${EModelEndpoint.bedrock}-${BedrockProviders.AI21}`]: bedrockGeneral,
[`${EModelEndpoint.bedrock}-${BedrockProviders.Amazon}`]: bedrockGeneral,
[`${EModelEndpoint.bedrock}-${BedrockProviders.DeepSeek}`]: bedrockGeneral,
[EModelEndpoint.google]: googleConfig,
};
@ -708,6 +709,7 @@ export const presetSettings: Record<
[`${EModelEndpoint.bedrock}-${BedrockProviders.Meta}`]: bedrockGeneralColumns,
[`${EModelEndpoint.bedrock}-${BedrockProviders.AI21}`]: bedrockGeneralColumns,
[`${EModelEndpoint.bedrock}-${BedrockProviders.Amazon}`]: bedrockGeneralColumns,
[`${EModelEndpoint.bedrock}-${BedrockProviders.DeepSeek}`]: bedrockGeneralColumns,
[EModelEndpoint.google]: {
col1: googleCol1,
col2: googleCol2,

View file

@ -0,0 +1,62 @@
import React from 'react';
import { Label, Input } from '~/components/ui';
import { cn } from '~/utils';
export default function FormInput({
field,
label,
labelClass,
inputClass,
containerClass,
labelAdjacent,
placeholder = '',
type = 'string',
}: {
field: any;
label: string;
labelClass?: string;
inputClass?: string;
placeholder?: string;
containerClass?: string;
type?: 'string' | 'number';
labelAdjacent?: React.ReactNode;
}) {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
if (type !== 'number') {
field.onChange(value);
return;
}
if (value === '') {
field.onChange(value);
} else if (!isNaN(Number(value))) {
field.onChange(Number(value));
}
};
return (
<div className={cn('flex w-full flex-col items-center gap-2', containerClass)}>
<div className="flex w-full items-center justify-start gap-2">
<Label
htmlFor={`${field.name}-input`}
className={cn('text-left text-sm font-semibold text-text-primary', labelClass)}
>
{label}
</Label>
{labelAdjacent}
</div>
<Input
id={`${field.name}-input`}
value={field.value ?? ''}
onChange={handleChange}
placeholder={placeholder}
className={cn(
'flex h-10 max-h-10 w-full resize-none border-none bg-surface-secondary px-3 py-2',
inputClass,
)}
/>
</div>
);
}

View file

@ -29,6 +29,7 @@ export * from './InputOTP';
export { default as Combobox } from './Combobox';
export { default as Dropdown } from './Dropdown';
export { default as FileUpload } from './FileUpload';
export { default as FormInput } from './FormInput';
export { default as DropdownPopup } from './DropdownPopup';
export { default as DelayedRender } from './DelayedRender';
export { default as ThemeSelector } from './ThemeSelector';

View file

@ -63,8 +63,9 @@ export const useUploadFileMutation = (
const update = {};
const prevResources = agent.tool_resources ?? {};
const prevResource: t.ExecuteCodeResource | t.AgentFileSearchResource = agent
.tool_resources?.[tool_resource] ?? {
const prevResource: t.ExecuteCodeResource | t.AgentFileResource = agent.tool_resources?.[
tool_resource
] ?? {
file_ids: [],
};
if (!prevResource.file_ids) {

View file

@ -23,6 +23,7 @@ type TStepEvent = {
event: string;
data:
| Agents.MessageDeltaEvent
| Agents.AgentUpdate
| Agents.RunStep
| Agents.ToolEndEvent
| {
@ -87,6 +88,17 @@ export default function useStepHandler({
if (contentPart.tool_call_ids != null) {
update.tool_call_ids = contentPart.tool_call_ids;
}
updatedContent[index] = update;
} else if (
contentType.startsWith(ContentTypes.AGENT_UPDATE) &&
ContentTypes.AGENT_UPDATE in contentPart &&
contentPart.agent_update
) {
const update: Agents.AgentUpdate = {
type: ContentTypes.AGENT_UPDATE,
agent_update: contentPart.agent_update,
};
updatedContent[index] = update;
} else if (
contentType.startsWith(ContentTypes.THINK) &&
@ -191,29 +203,20 @@ export default function useStepHandler({
});
}
} else if (event === 'on_agent_update') {
const { runId, message } = data as { runId?: string; message: string };
const responseMessageId = runId ?? '';
const { agent_update } = data as Agents.AgentUpdate;
const responseMessageId = agent_update.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]);
const response = messageMap.current.get(responseMessageId);
if (response) {
const updatedResponse = updateContent(response, agent_update.index, data);
messageMap.current.set(responseMessageId, updatedResponse);
const currentMessages = getMessages() || [];
setMessages([...currentMessages.slice(0, -1), updatedResponse]);
}
} else if (event === 'on_message_delta') {
const messageDelta = data as Agents.MessageDeltaEvent;
const runStep = stepMap.current.get(messageDelta.id);

View file

@ -11,6 +11,9 @@
"com_agents_create_error": "There was an error creating your agent.",
"com_agents_description_placeholder": "Optional: Describe your Agent here",
"com_agents_enable_file_search": "Enable File Search",
"com_agents_file_context": "File Context (OCR)",
"com_agents_file_context_disabled": "Agent must be created before uploading files for File Context.",
"com_agents_file_context_info": "Files uploaded as \"Context\" are processed using OCR to extract text, which is then added to the Agent's instructions. Ideal for documents, images with text, or PDFs where you need the full text content of a file",
"com_agents_file_search_disabled": "Agent must be created before uploading files for 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_instructions_placeholder": "The system instructions that the agent uses",
@ -462,13 +465,20 @@
"com_ui_admin_access_warning": "Disabling Admin access to this feature may cause unexpected UI issues requiring refresh. If saved, the only way to revert is via the interface setting in librechat.yaml config which affects all roles.",
"com_ui_admin_settings": "Admin Settings",
"com_ui_advanced": "Advanced",
"com_ui_advanced_settings": "Advanced Settings",
"com_ui_agent": "Agent",
"com_ui_agent_chain": "Agent Chain (Mixture-of-Agents)",
"com_ui_agent_chain_info": "Enables creating sequences of agents. Each agent can access outputs from previous agents in the chain. Based on the \"Mixture-of-Agents\" architecture where agents use previous outputs as auxiliary information.",
"com_ui_agent_chain_max": "You have reached the maximum of {{0}} agents.",
"com_ui_agent_delete_error": "There was an error deleting the agent",
"com_ui_agent_deleted": "Successfully deleted agent",
"com_ui_agent_duplicate_error": "There was an error duplicating the agent",
"com_ui_agent_duplicated": "Agent duplicated successfully",
"com_ui_agent_editing_allowed": "Other users can already edit this agent",
"com_ui_agent_recursion_limit": "Max Agent Steps",
"com_ui_agent_recursion_limit_info": "Limits how many steps the agent can take in a run before giving a final response. Default is 25 steps. A step is either an AI API request or a tool usage round. For example, a basic tool interaction takes 3 steps: initial request, tool usage, and follow-up request.",
"com_ui_agent_shared_to_all": "something needs to go here. was empty",
"com_ui_agent_var": "{{0}} agent",
"com_ui_agents": "Agents",
"com_ui_agents_allow_create": "Allow creating Agents",
"com_ui_agents_allow_share_global": "Allow sharing Agents to all users",
@ -811,12 +821,14 @@
"com_ui_upload_code_files": "Upload for Code Interpreter",
"com_ui_upload_delay": "Uploading \"{{0}}\" is taking more time than anticipated. Please wait while the file finishes indexing for retrieval.",
"com_ui_upload_error": "There was an error uploading your file",
"com_ui_upload_file_context": "Upload File Context",
"com_ui_upload_file_search": "Upload for File Search",
"com_ui_upload_files": "Upload files",
"com_ui_upload_image": "Upload an image",
"com_ui_upload_image_input": "Upload Image",
"com_ui_upload_invalid": "Invalid file for upload. Must be an image not exceeding the limit",
"com_ui_upload_invalid_var": "Invalid file for upload. Must be an image not exceeding {{0}} MB",
"com_ui_upload_ocr_text": "Upload as Text",
"com_ui_upload_success": "Successfully uploaded file",
"com_ui_upload_type": "Select Upload Type",
"com_ui_use_2fa_code": "Use 2FA Code Instead",

View file

@ -11,6 +11,9 @@
"com_agents_create_error": "Houve um erro ao criar seu agente.",
"com_agents_description_placeholder": "Opcional: Descreva seu Agente aqui",
"com_agents_enable_file_search": "Habilitar pesquisa de arquivos",
"com_agents_file_context": "Contexto de arquivo (OCR)",
"com_agents_file_context_disabled": "O agente deve ser criado antes de carregar arquivos para o Contexto de Arquivo.",
"com_agents_file_context_info": "Os arquivos carregados como \"Contexto\" são processados usando OCR para extrair texto, que é então adicionado às instruções do Agente. Ideal para documentos, imagens com texto ou PDFs onde você precisa do conteúdo de texto completo de um arquivo",
"com_agents_file_search_disabled": "O agente deve ser criado antes de carregar arquivos para Pesquisa de Arquivos.",
"com_agents_file_search_info": "Quando ativado, o agente será informado dos nomes exatos dos arquivos listados abaixo, permitindo que ele recupere o contexto relevante desses arquivos.",
"com_agents_instructions_placeholder": "As instruções do sistema que o agente usa",
@ -811,12 +814,14 @@
"com_ui_upload_code_files": "Carregar para o interpretador de código",
"com_ui_upload_delay": "O upload de \"{{0}}\" está demorando mais do que o esperado. Por favor, aguarde enquanto o arquivo termina de ser indexado para recuperação.",
"com_ui_upload_error": "Houve um erro ao carregar seu arquivo",
"com_ui_upload_file_context": "Contexto de upload de arquivo",
"com_ui_upload_file_search": "Upload para pesquisa de arquivos",
"com_ui_upload_files": "Carregar arquivos",
"com_ui_upload_image": "Carregar uma imagem",
"com_ui_upload_image_input": "Upload de imagem",
"com_ui_upload_invalid": "Arquivo inválido para upload. Deve ser uma imagem não excedendo o limite",
"com_ui_upload_invalid_var": "Arquivo inválido para upload. Deve ser uma imagem não excedendo {{0}} MB",
"com_ui_upload_ocr_text": "Carregar como texto",
"com_ui_upload_success": "Arquivo carregado com sucesso",
"com_ui_upload_type": "Selecione o tipo de upload",
"com_ui_use_2fa_code": "Use o código 2FA em vez disso",

View file

@ -361,4 +361,14 @@ div[role="tabpanel"][data-state="active"][data-orientation="horizontal"][aria-la
.cm-content:focus {
outline: none !important;
}
p.whitespace-pre-wrap a, li a {
color: #0066cc;
text-decoration: underline;
font-weight: bold;
}
.dark p.whitespace-pre-wrap a, .dark li a {
color: #52a0ff;
}

View file

@ -58,6 +58,9 @@ export const processAgentOption = ({
label: _agent?.name ?? '',
value: _agent?.id ?? '',
icon: isGlobal ? <EarthIcon className="icon-md text-green-400" /> : null,
context_files: _agent?.tool_resources?.ocr?.file_ids
? ([] as Array<[string, ExtendedFile]>)
: undefined,
knowledge_files: _agent?.tool_resources?.file_search?.file_ids
? ([] as Array<[string, ExtendedFile]>)
: undefined,
@ -83,7 +86,7 @@ export const processAgentOption = ({
const source =
tool_resource === EToolResources.file_search
? FileSources.vectordb
: file?.source ?? FileSources.local;
: (file?.source ?? FileSources.local);
if (file) {
list?.push([
@ -97,6 +100,7 @@ export const processAgentOption = ({
height: file.height,
size: file.bytes,
preview: file.filepath,
metadata: file.metadata,
progress: 1,
source,
},
@ -117,6 +121,16 @@ export const processAgentOption = ({
}
};
if (agent.context_files && _agent?.tool_resources?.ocr?.file_ids) {
_agent.tool_resources.ocr.file_ids.forEach((file_id) =>
handleFile({
file_id,
list: agent.context_files,
tool_resource: EToolResources.ocr,
}),
);
}
if (agent.knowledge_files && _agent?.tool_resources?.file_search?.file_ids) {
_agent.tool_resources.file_search.file_ids.forEach((file_id) =>
handleFile({