mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-10 12:38:52 +01:00
Merge branch 'main' into feat/multi-lang-Terms-of-service
This commit is contained in:
commit
b170a57482
107 changed files with 3274 additions and 765 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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" />,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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')}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
179
client/src/components/SidePanel/Agents/Advanced/AgentChain.tsx
Normal file
179
client/src/components/SidePanel/Agents/Advanced/AgentChain.tsx
Normal 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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
86
client/src/components/SidePanel/Agents/AgentFooter.tsx
Normal file
86
client/src/components/SidePanel/Agents/AgentFooter.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
128
client/src/components/SidePanel/Agents/FileContext.tsx
Normal file
128
client/src/components/SidePanel/Agents/FileContext.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
62
client/src/components/ui/FormInput.tsx
Normal file
62
client/src/components/ui/FormInput.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue