🚧 chore: merge latest dev build (#4288)

* fix: agent initialization, add `collectedUsage` handling

* style: improve side panel styling

* refactor(loadAgent): Optimize order agent project ID retrieval

* feat: code execution

* fix: typing issues

* feat: ExecuteCode content part

* refactor: use local state for default collapsed state of analysis content parts

* fix: code parsing in ExecuteCode component

* chore: bump agents package, export loadAuthValues

* refactor: Update handleTools.js to use EnvVar for code execution tool authentication

* WIP

* feat: download code outputs

* fix(useEventHandlers): type issues

* feat: backend handling for code outputs

* Refactor: Remove console.log statement in Part.tsx

* refactor: add attachments to TMessage/messageSchema

* WIP: prelim handling for code outputs

* feat: attachments rendering

* refactor: improve attachments rendering

* fix: attachments, nullish edge case, handle attachments from event stream, bump agents package

* fix filename download

* fix: tool assignment for 'run code' on agent creation

* fix: image handling by adding attachments

* refactor: prevent agent creation without provider/model

* refactor: remove unnecessary space in agent creation success message

* refactor: select first model if selecting provider from empty on form

* fix: Agent avatar bug

* fix: `defaultAgentFormValues` causing boolean typing issue and typeerror

* fix: capabilities counting as tools, causing duplication of them

* fix: formatted messages edge case where consecutive content text type parts with the latter having tool_call_ids would cause consecutive AI messages to be created. furthermore, content could not be an array for tool_use messages (anthropic limitation)

* chore: bump @librechat/agents dependency to version 1.6.9

* feat: bedrock agents

* feat: new Agents icon

* feat: agent titling

* feat: agent landing

* refactor: allow sharing agent globally only if user is admin or author

* feat: initial AgentPanelSkeleton

* feat: AgentPanelSkeleton

* feat: collaborative agents

* chore: add potential authorName as part of schema

* chore: Remove unnecessary console.log statement

* WIP: agent model parameters

* chore: ToolsDialog typing and tool related localization chnages

* refactor: update tool instance type (latest langchain class), and rename google tool to 'google' proper

* chore: add back tools

* feat: Agent knowledge files upload

* refactor: better verbiage for disabled knowledge

* chore: debug logs for file deletions

* chore: debug logs for file deletions

* feat: upload/delete agent knowledge/file-search files

* feat: file search UI for agents

* feat: first pass, file search tool

* chore: update default agent capabilities and info
This commit is contained in:
Danny Avila 2024-09-30 17:17:57 -04:00 committed by GitHub
parent f33e75e2ee
commit ad74350036
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
123 changed files with 3611 additions and 1541 deletions

View file

@ -11,14 +11,13 @@ export default function FileRow({
setFiles,
setFilesLoading,
assistant_id,
// TODO: Agent file handling
agent_id,
tool_resource,
fileFilter,
isRTL,
isRTL = false,
Wrapper,
}: {
files: Map<string, ExtendedFile>;
files: Map<string, ExtendedFile> | undefined;
setFiles: React.Dispatch<React.SetStateAction<Map<string, ExtendedFile>>>;
setFilesLoading: React.Dispatch<React.SetStateAction<boolean>>;
fileFilter?: (file: ExtendedFile) => boolean;
@ -28,13 +27,18 @@ export default function FileRow({
isRTL?: boolean;
Wrapper?: React.FC<{ children: React.ReactNode }>;
}) {
const files = Array.from(_files.values()).filter((file) =>
const files = Array.from(_files?.values() ?? []).filter((file) =>
fileFilter ? fileFilter(file) : true,
);
const { mutateAsync } = useDeleteFilesMutation({
onMutate: async () =>
console.log('Deleting files: assistant_id, tool_resource', assistant_id, tool_resource),
console.log(
'Deleting files: agent_id, assistant_id, tool_resource',
agent_id,
assistant_id,
tool_resource,
),
onSuccess: () => {
console.log('Files deleted');
},
@ -43,13 +47,9 @@ export default function FileRow({
},
});
const { deleteFile } = useFileDeletion({ mutateAsync, assistant_id, tool_resource });
const { deleteFile } = useFileDeletion({ mutateAsync, agent_id, assistant_id, tool_resource });
useEffect(() => {
if (!files) {
return;
}
if (files.length === 0) {
return;
}
@ -87,11 +87,12 @@ export default function FileRow({
)
.uniqueFiles.map((file: ExtendedFile, index: number) => {
const handleDelete = () => deleteFile({ file, setFiles });
if (file.type?.startsWith('image')) {
const isImage = file.type?.startsWith('image') ?? false;
if (isImage) {
return (
<Image
key={index}
url={file.preview || file.filepath}
url={file.preview ?? file.filepath}
onDelete={handleDelete}
progress={file.progress}
source={file.source}

View file

@ -1,18 +1,20 @@
import { useMemo } from 'react';
import { EModelEndpoint, isAssistantsEndpoint, Constants } from 'librechat-data-provider';
import { EModelEndpoint, Constants } from 'librechat-data-provider';
import { useGetEndpointsQuery, useGetStartupConfig } from 'librechat-data-provider/react-query';
import type * as t from 'librechat-data-provider';
import type { ReactNode } from 'react';
import { useChatContext, useAssistantsMapContext } from '~/Providers';
import { useChatContext, useAgentsMapContext, useAssistantsMapContext } from '~/Providers';
import { useGetAssistantDocsQuery } from '~/data-provider';
import ConvoIcon from '~/components/Endpoints/ConvoIcon';
import { getIconEndpoint, getEntity, cn } from '~/utils';
import { useLocalize, useSubmitMessage } from '~/hooks';
import { TooltipAnchor } from '~/components/ui';
import { BirthdayIcon } from '~/components/svg';
import { getIconEndpoint, cn } from '~/utils';
import ConvoStarter from './ConvoStarter';
export default function Landing({ Header }: { Header?: ReactNode }) {
const { conversation } = useChatContext();
const agentsMap = useAgentsMapContext();
const assistantMap = useAssistantsMapContext();
const { data: startupConfig } = useGetStartupConfig();
const { data: endpointsConfig } = useGetEndpointsQuery();
@ -20,7 +22,6 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
const localize = useLocalize();
let { endpoint = '' } = conversation ?? {};
const { assistant_id = null } = conversation ?? {};
if (
endpoint === EModelEndpoint.chatGPTBrowser ||
@ -36,20 +37,32 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
select: (data) => new Map(data.map((dbA) => [dbA.assistant_id, dbA])),
});
const isAssistant = isAssistantsEndpoint(endpoint);
const assistant = isAssistant ? assistantMap?.[endpoint][assistant_id ?? ''] : undefined;
const assistantName = assistant?.name ?? '';
const assistantDesc = assistant?.description ?? '';
const avatar = assistant?.metadata?.avatar ?? '';
const { entity, isAgent, isAssistant } = getEntity({
endpoint,
agentsMap,
assistantMap,
agent_id: conversation?.agent_id,
assistant_id: conversation?.assistant_id,
});
const name = entity?.name ?? '';
const description = entity?.description ?? '';
const avatar = isAgent
? (entity as t.Agent | undefined)?.avatar?.filepath ?? ''
: ((entity as t.Assistant | undefined)?.metadata?.avatar as string | undefined) ?? '';
const conversation_starters = useMemo(() => {
/* The user made updates, use client-side cache, */
if (assistant?.conversation_starters) {
return assistant.conversation_starters;
/* The user made updates, use client-side cache, or they exist in an Agent */
if (entity && (entity.conversation_starters?.length ?? 0) > 0) {
return entity.conversation_starters;
}
if (isAgent) {
return entity?.conversation_starters ?? [];
}
/* If none in cache, we use the latest assistant docs */
const assistantDocs = documentsMap.get(assistant_id ?? '');
return assistantDocs?.conversation_starters ?? [];
}, [documentsMap, assistant_id, assistant?.conversation_starters]);
const entityDocs = documentsMap.get(entity?.id ?? '');
return entityDocs?.conversation_starters ?? [];
}, [documentsMap, isAgent, entity]);
const containerClassName =
'shadow-stroke relative flex h-full items-center justify-center rounded-full bg-white text-black';
@ -57,14 +70,32 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
const { submitMessage } = useSubmitMessage();
const sendConversationStarter = (text: string) => submitMessage({ text });
const getWelcomeMessage = () => {
const greeting = conversation?.greeting ?? '';
if (greeting) {
return greeting;
}
if (isAssistant) {
return localize('com_nav_welcome_assistant');
}
if (isAgent) {
return localize('com_nav_welcome_agent');
}
return localize('com_nav_welcome_message');
};
return (
<div className="relative h-full">
<div className="absolute left-0 right-0">{Header != null ? Header : null}</div>
<div className="flex h-full flex-col items-center justify-center">
<div className={cn('relative h-12 w-12', assistantName && avatar ? 'mb-0' : 'mb-3')}>
<div className={cn('relative h-12 w-12', name && avatar ? 'mb-0' : 'mb-3')}>
<ConvoIcon
conversation={conversation}
agentsMap={agentsMap}
assistantMap={assistantMap}
conversation={conversation}
endpointsConfig={endpointsConfig}
containerClassName={containerClassName}
context="landing"
@ -80,11 +111,11 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
</TooltipAnchor>
) : null}
</div>
{assistantName ? (
{name ? (
<div className="flex flex-col items-center gap-0 p-2">
<div className="text-center text-2xl font-medium dark:text-white">{assistantName}</div>
<div className="text-center text-2xl font-medium dark:text-white">{name}</div>
<div className="max-w-md text-center text-sm font-normal text-text-primary ">
{assistantDesc ? assistantDesc : localize('com_nav_welcome_message')}
{description ? description : localize('com_nav_welcome_message')}
</div>
{/* <div className="mt-1 flex items-center gap-1 text-token-text-tertiary">
<div className="text-sm text-token-text-tertiary">By Daniel Avila</div>
@ -92,16 +123,14 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
</div>
) : (
<h2 className="mb-5 max-w-[75vh] px-12 text-center text-lg font-medium dark:text-white md:px-0 md:text-2xl">
{isAssistant
? conversation?.greeting ?? localize('com_nav_welcome_assistant')
: conversation?.greeting ?? localize('com_nav_welcome_message')}
{getWelcomeMessage()}
</h2>
)}
<div className="mt-8 flex flex-wrap justify-center gap-3 px-4">
{conversation_starters.length > 0 &&
conversation_starters
.slice(0, Constants.MAX_CONVO_STARTERS)
.map((text, index) => (
.map((text: string, index: number) => (
<ConvoStarter
key={index}
text={text}

View file

@ -1,6 +1,6 @@
import { EModelEndpoint } from 'librechat-data-provider';
import type { IconMapProps, AgentIconMapProps } from '~/common';
import { BrainCircuit } from 'lucide-react';
import { Feather } from 'lucide-react';
import {
MinimalPlugin,
GPTIcon,
@ -17,7 +17,13 @@ import {
import UnknownIcon from './UnknownIcon';
import { cn } from '~/utils';
const AssistantAvatar = ({ className = '', assistantName, avatar, size }: IconMapProps) => {
const AssistantAvatar = ({
className = '',
assistantName = '',
avatar = '',
context,
size,
}: IconMapProps) => {
if (assistantName && avatar) {
return (
<img
@ -32,10 +38,10 @@ const AssistantAvatar = ({ className = '', assistantName, avatar, size }: IconMa
return <AssistantIcon className={cn('text-token-secondary', className)} size={size} />;
}
return <Sparkles className={cn(assistantName === '' ? 'icon-2xl' : '', className)} />;
return <Sparkles className={cn(context === 'landing' ? 'icon-2xl' : '', className)} />;
};
const AgentAvatar = ({ className = '', agentName, avatar, size }: AgentIconMapProps) => {
const AgentAvatar = ({ className = '', avatar = '', agentName, size }: AgentIconMapProps) => {
if (agentName && avatar) {
return (
<img
@ -46,11 +52,9 @@ const AgentAvatar = ({ className = '', agentName, avatar, size }: AgentIconMapPr
height="80"
/>
);
} else if (agentName) {
return <AssistantIcon className={cn('text-token-secondary', className)} size={size} />;
}
return <BrainCircuit className={cn(agentName === '' ? 'icon-2xl' : '', className)} />;
return <Feather className={cn(agentName === '' ? 'icon-2xl' : '', className)} size={size} />;
};
const Bedrock = ({ className = '' }: IconMapProps) => {

View file

@ -113,7 +113,7 @@ const MenuItem: FC<MenuItemProps> = ({
<div className="flex grow items-center justify-between gap-2">
<div>
<div className="flex items-center gap-2">
{Icon && (
{Icon != null && (
<Icon
size={18}
endpoint={endpoint}

View file

@ -1,31 +1,38 @@
import { alternateName } from 'librechat-data-provider';
import { Content, Portal, Root } from '@radix-ui/react-popover';
import { alternateName, isAssistantsEndpoint } from 'librechat-data-provider';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type { FC } from 'react';
import { useChatContext, useAssistantsMapContext } from '~/Providers';
import { useChatContext, useAgentsMapContext, useAssistantsMapContext } from '~/Providers';
import { mapEndpoints, getEntity } from '~/utils';
import EndpointItems from './Endpoints/MenuItems';
import TitleButton from './UI/TitleButton';
import { mapEndpoints } from '~/utils';
const EndpointsMenu: FC = () => {
const { data: endpoints = [] } = useGetEndpointsQuery({
select: mapEndpoints,
});
const { conversation } = useChatContext();
const { endpoint = '', assistant_id = null } = conversation ?? {};
const agentsMap = useAgentsMapContext();
const assistantMap = useAssistantsMapContext();
const assistant =
isAssistantsEndpoint(endpoint) && assistantMap?.[endpoint ?? '']?.[assistant_id ?? ''];
const assistantName = (assistant && assistant?.name) || 'Assistant';
const { conversation } = useChatContext();
const { endpoint = '' } = conversation ?? {};
if (!endpoint) {
console.warn('No endpoint selected');
return null;
}
const primaryText = assistant ? assistantName : (alternateName[endpoint] ?? endpoint ?? '') + ' ';
const { entity } = getEntity({
endpoint,
agentsMap,
assistantMap,
agent_id: conversation?.agent_id,
assistant_id: conversation?.assistant_id,
});
const primaryText = entity
? entity.name
: (alternateName[endpoint] as string | undefined) ?? endpoint;
return (
<Root>
@ -44,7 +51,7 @@ const EndpointsMenu: FC = () => {
<Content
side="bottom"
align="start"
className="mt-2 max-h-[65vh] min-w-[340px] overflow-y-auto rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-700 dark:text-white lg:max-h-[75vh]"
className="mt-2 max-h-[65vh] min-w-[340px] overflow-y-auto rounded-lg border border-border-light bg-header-primary text-text-primary shadow-lg lg:max-h-[75vh]"
>
<EndpointItems endpoints={endpoints} selected={endpoint} />
</Content>

View file

@ -1,11 +1,10 @@
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import ProgressCircle from './ProgressCircle';
import CancelledIcon from './CancelledIcon';
import { CodeInProgress } from './Parts/CodeProgress';
import { useProgress, useLocalize } from '~/hooks';
import ProgressText from './ProgressText';
import FinishedIcon from './FinishedIcon';
import MarkdownLite from './MarkdownLite';
import { useProgress } from '~/hooks';
import store from '~/store';
export default function CodeAnalyze({
@ -19,9 +18,11 @@ export default function CodeAnalyze({
outputs: Record<string, unknown>[];
isSubmitting: boolean;
}) {
const showCodeDefault = useRecoilValue(store.showCode);
const [showCode, setShowCode] = useState(showCodeDefault);
const localize = useLocalize();
const progress = useProgress(initialProgress);
const showAnalysisCode = useRecoilValue(store.showCode);
const [showCode, setShowCode] = useState(showAnalysisCode);
const radius = 56.08695652173913;
const circumference = 2 * Math.PI * radius;
const offset = circumference - progress * circumference;
@ -62,7 +63,7 @@ export default function CodeAnalyze({
<MarkdownLite content={code ? `\`\`\`python\n${code}\n\`\`\`` : ''} />
{logs && (
<div className="bg-gray-700 p-4 text-xs">
<div className="mb-1 text-gray-400">Result</div>
<div className="mb-1 text-gray-400">{localize('com_ui_result')}</div>
<div
className="prose flex flex-col-reverse text-white"
style={{
@ -78,91 +79,3 @@ export default function CodeAnalyze({
</>
);
}
const CodeInProgress = ({
offset,
circumference,
radius,
isSubmitting,
progress,
}: {
progress: number;
offset: number;
circumference: number;
radius: number;
isSubmitting: boolean;
}) => {
if (progress < 1 && !isSubmitting) {
return <CancelledIcon />;
}
return (
<div
className="absolute left-0 top-0 flex h-full w-full items-center justify-center rounded-full bg-transparent text-white"
style={{ opacity: 1, transform: 'none' }}
data-projection-id="77"
>
<div className="absolute bottom-[1.5px] right-[1.5px]">
<svg
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
viewBox="0 0 20 20"
width="20"
height="20"
style={{ transform: 'translate3d(0px, 0px, 0px)' }}
preserveAspectRatio="xMidYMid meet"
>
<defs>
<clipPath id="__lottie_element_11">
<rect width="20" height="20" x="0" y="0" />
</clipPath>
</defs>
<g clipPath="url(#__lottie_element_11)">
<g
style={{ display: 'block', transform: 'matrix(1,0,0,1,-2,-2)', opacity: 1 }}
className="slide-from-left"
>
<g opacity="1" transform="matrix(1,0,0,1,7.026679992675781,8.834091186523438)">
<path
fill="rgb(177,98,253)"
fillOpacity="1"
d=" M1.2870399951934814,0.2207774966955185 C0.992609977722168,-0.07359249889850616 0.5152599811553955,-0.07359249889850616 0.22082999348640442,0.2207774966955185 C-0.07361000031232834,0.5151575207710266 -0.07361000031232834,0.992437481880188 0.22082999348640442,1.2868175506591797 C0.8473266959190369,1.9131841659545898 1.4738233089447021,2.53955078125 2.1003201007843018,3.16591739654541 C1.4738233089447021,3.7922842502593994 0.8473266959190369,4.4186506271362305 0.22082999348640442,5.045017719268799 C-0.07361000031232834,5.339417457580566 -0.07361000031232834,5.816617488861084 0.22082999348640442,6.11101770401001 C0.5152599811553955,6.405417442321777 0.992609977722168,6.405417442321777 1.2870399951934814,6.11101770401001 C2.091266632080078,5.306983947753906 2.895493268966675,4.502950668334961 3.6997199058532715,3.6989173889160156 C3.994119882583618,3.404517412185669 3.994119882583618,2.927217483520508 3.6997199058532715,2.6329174041748047 C2.895493268966675,1.8288708925247192 2.091266632080078,1.0248241424560547 1.2870399951934814,0.2207774966955185 C1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185 C1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
fillOpacity="0"
stroke="rgb(177,98,253)"
strokeOpacity="1"
strokeWidth="0.201031"
d=" M1.2870399951934814,0.2207774966955185 C0.992609977722168,-0.07359249889850616 0.5152599811553955,-0.07359249889850616 0.22082999348640442,0.2207774966955185 C-0.07361000031232834,0.5151575207710266 -0.07361000031232834,0.992437481880188 0.22082999348640442,1.2868175506591797 C0.8473266959190369,1.9131841659545898 1.4738233089447021,2.53955078125 2.1003201007843018,3.16591739654541 C1.4738233089447021,3.7922842502593994 0.8473266959190369,4.4186506271362305 0.22082999348640442,5.045017719268799 C-0.07361000031232834,5.339417457580566 -0.07361000031232834,5.816617488861084 0.22082999348640442,6.11101770401001 C0.5152599811553955,6.405417442321777 0.992609977722168,6.405417442321777 1.2870399951934814,6.11101770401001 C2.091266632080078,5.306983947753906 2.895493268966675,4.502950668334961 3.6997199058532715,3.6989173889160156 C3.994119882583618,3.404517412185669 3.994119882583618,2.927217483520508 3.6997199058532715,2.6329174041748047 C2.895493268966675,1.8288708925247192 2.091266632080078,1.0248241424560547 1.2870399951934814,0.2207774966955185 C1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185 C1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185"
/>
</g>
</g>
<g
style={{ display: 'block', transform: 'matrix(1,0,0,1,-2,-2)', opacity: 1 }}
className="slide-to-down"
>
<g opacity="1" transform="matrix(1,0,0,1,11.79640007019043,13.512199401855469)">
<path
fill="rgb(177,98,253)"
fillOpacity="1"
d=" M4.3225998878479,0 C3.1498000621795654,0 1.9769999980926514,0 0.8041999936103821,0 C0.36010000109672546,0 0,0.36000001430511475 0,0.804099977016449 C0,1.2482000589370728 0.36010000109672546,1.6081000566482544 0.8041999936103821,1.6081000566482544 C1.9769999980926514,1.6081000566482544 3.1498000621795654,1.6081000566482544 4.3225998878479,1.6081000566482544 C4.7667999267578125,1.6081000566482544 5.126800060272217,1.2482000589370728 5.126800060272217,0.804099977016449 C5.126800060272217,0.36000001430511475 4.7667999267578125,0 4.3225998878479,0 C4.3225998878479,0 4.3225998878479,0 4.3225998878479,0 C4.3225998878479,0 4.3225998878479,0 4.3225998878479,0"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
fillOpacity="0"
stroke="rgb(177,98,253)"
strokeOpacity="1"
strokeWidth="0.100515"
d=" M4.3225998878479,0 C3.1498000621795654,0 1.9769999980926514,0 0.8041999936103821,0 C0.36010000109672546,0 0,0.36000001430511475 0,0.804099977016449 C0,1.2482000589370728 0.36010000109672546,1.6081000566482544 0.8041999936103821,1.6081000566482544 C1.9769999980926514,1.6081000566482544 3.1498000621795654,1.6081000566482544 4.3225998878479,1.6081000566482544 C4.7667999267578125,1.6081000566482544 5.126800060272217,1.2482000589370728 5.126800060272217,0.804099977016449 C5.126800060272217,0.36000001430511475 4.7667999267578125,0 4.3225998878479,0 C4.3225998878479,0 4.3225998878479,0 4.3225998878479,0 C4.3225998878479,0 4.3225998878479,0 4.3225998878479,0"
/>
</g>
</g>
</g>
</svg>
</div>
<ProgressCircle radius={radius} circumference={circumference} offset={offset} />
</div>
);
};

View file

@ -1,12 +1,16 @@
import { memo } from 'react';
import { memo, useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { ContentTypes } from 'librechat-data-provider';
import type { TMessageContentParts } from 'librechat-data-provider';
import type { TMessageContentParts, TAttachment, Agents } from 'librechat-data-provider';
import EditTextPart from './Parts/EditTextPart';
import { mapAttachments } from '~/utils/map';
import store from '~/store';
import Part from './Part';
type ContentPartsProps = {
content: Array<TMessageContentParts | undefined> | undefined;
messageId: string;
attachments?: TAttachment[];
isCreatedByUser: boolean;
isLast: boolean;
isSubmitting: boolean;
@ -23,6 +27,7 @@ const ContentParts = memo(
({
content,
messageId,
attachments,
isCreatedByUser,
isLast,
isSubmitting,
@ -31,6 +36,11 @@ const ContentParts = memo(
siblingIdx,
setSiblingIdx,
}: ContentPartsProps) => {
const messageAttachmentsMap = useRecoilValue(store.messageAttachmentsMap);
const attachmentMap = useMemo(
() => mapAttachments(attachments ?? messageAttachmentsMap[messageId] ?? []),
[attachments, messageAttachmentsMap, messageId],
);
if (!content) {
return null;
}
@ -58,20 +68,28 @@ const ContentParts = memo(
</>
);
}
return (
<>
{content
.filter((part) => part)
.map((part, idx) => (
<Part
key={`display-${messageId}-${idx}`}
part={part}
isSubmitting={isSubmitting}
showCursor={idx === content.length - 1 && isLast}
messageId={messageId}
isCreatedByUser={isCreatedByUser}
/>
))}
.map((part, idx) => {
const toolCallId =
(part?.[ContentTypes.TOOL_CALL] as Agents.ToolCall | undefined)?.id ?? '';
const attachments = attachmentMap[toolCallId];
return (
<Part
part={part}
isSubmitting={isSubmitting}
attachments={attachments}
key={`display-${messageId}-${idx}`}
showCursor={idx === content.length - 1 && isLast}
messageId={messageId}
isCreatedByUser={isCreatedByUser}
/>
);
})}
</>
);
},

View file

@ -1,12 +1,14 @@
import {
ToolCallTypes,
Tools,
ContentTypes,
ToolCallTypes,
imageGenTools,
isImageVisionTool,
} from 'librechat-data-provider';
import { memo } from 'react';
import type { TMessageContentParts } from 'librechat-data-provider';
import type { TMessageContentParts, TAttachment } from 'librechat-data-provider';
import { ErrorMessage } from './MessageContent';
import ExecuteCode from './Parts/ExecuteCode';
import RetrievalCall from './RetrievalCall';
import CodeAnalyze from './CodeAnalyze';
import Container from './Container';
@ -21,125 +23,141 @@ type PartProps = {
showCursor: boolean;
messageId: string;
isCreatedByUser: boolean;
attachments?: TAttachment[];
};
const Part = memo(({ part, isSubmitting, showCursor, messageId, isCreatedByUser }: PartProps) => {
if (!part) {
return null;
}
if (part.type === ContentTypes.ERROR) {
return <ErrorMessage text={part[ContentTypes.TEXT].value} className="my-2" />;
} else if (part.type === ContentTypes.TEXT) {
const text = typeof part.text === 'string' ? part.text : part.text.value;
if (typeof text !== 'string') {
return null;
}
if (part.tool_call_ids != null && !text) {
return null;
}
return (
<Container>
<Text
text={text}
isCreatedByUser={isCreatedByUser}
messageId={messageId}
showCursor={showCursor}
/>
</Container>
);
} else if (part.type === ContentTypes.TOOL_CALL) {
const toolCall = part[ContentTypes.TOOL_CALL];
if (!toolCall) {
const Part = memo(
({ part, isSubmitting, attachments, showCursor, messageId, isCreatedByUser }: PartProps) => {
attachments && console.log(attachments);
if (!part) {
return null;
}
if ('args' in toolCall && (!toolCall.type || toolCall.type === ToolCallTypes.TOOL_CALL)) {
if (part.type === ContentTypes.ERROR) {
return <ErrorMessage text={part[ContentTypes.TEXT].value} className="my-2" />;
} else if (part.type === ContentTypes.TEXT) {
const text = typeof part.text === 'string' ? part.text : part.text.value;
if (typeof text !== 'string') {
return null;
}
if (part.tool_call_ids != null && !text) {
return null;
}
return (
<ToolCall
args={toolCall.args ?? ''}
name={toolCall.name ?? ''}
output={toolCall.output ?? ''}
initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting}
/>
<Container>
<Text
text={text}
isCreatedByUser={isCreatedByUser}
messageId={messageId}
showCursor={showCursor}
/>
</Container>
);
} else if (toolCall.type === ToolCallTypes.CODE_INTERPRETER) {
const code_interpreter = toolCall[ToolCallTypes.CODE_INTERPRETER];
return (
<CodeAnalyze
initialProgress={toolCall.progress ?? 0.1}
code={code_interpreter.input}
outputs={code_interpreter.outputs ?? []}
isSubmitting={isSubmitting}
/>
);
} else if (
toolCall.type === ToolCallTypes.RETRIEVAL ||
toolCall.type === ToolCallTypes.FILE_SEARCH
) {
return (
<RetrievalCall initialProgress={toolCall.progress ?? 0.1} isSubmitting={isSubmitting} />
);
} else if (
toolCall.type === ToolCallTypes.FUNCTION &&
ToolCallTypes.FUNCTION in toolCall &&
imageGenTools.has(toolCall.function.name)
) {
return (
<ImageGen
initialProgress={toolCall.progress ?? 0.1}
args={toolCall.function.arguments as string}
/>
);
} else if (toolCall.type === ToolCallTypes.FUNCTION && ToolCallTypes.FUNCTION in toolCall) {
if (isImageVisionTool(toolCall)) {
if (isSubmitting && showCursor) {
return (
<Container>
<Text
text={''}
isCreatedByUser={isCreatedByUser}
messageId={messageId}
showCursor={showCursor}
/>
</Container>
);
}
} 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}
/>
);
} else if (toolCall.type === ToolCallTypes.CODE_INTERPRETER) {
const code_interpreter = toolCall[ToolCallTypes.CODE_INTERPRETER];
return (
<CodeAnalyze
initialProgress={toolCall.progress ?? 0.1}
code={code_interpreter.input}
outputs={code_interpreter.outputs ?? []}
isSubmitting={isSubmitting}
/>
);
} else if (
toolCall.type === ToolCallTypes.RETRIEVAL ||
toolCall.type === ToolCallTypes.FILE_SEARCH
) {
return (
<RetrievalCall initialProgress={toolCall.progress ?? 0.1} isSubmitting={isSubmitting} />
);
} else if (
toolCall.type === ToolCallTypes.FUNCTION &&
ToolCallTypes.FUNCTION in toolCall &&
imageGenTools.has(toolCall.function.name)
) {
return (
<ImageGen
initialProgress={toolCall.progress ?? 0.1}
args={toolCall.function.arguments as string}
/>
);
} else if (toolCall.type === ToolCallTypes.FUNCTION && ToolCallTypes.FUNCTION in toolCall) {
if (isImageVisionTool(toolCall)) {
if (isSubmitting && showCursor) {
return (
<Container>
<Text
text={''}
isCreatedByUser={isCreatedByUser}
messageId={messageId}
showCursor={showCursor}
/>
</Container>
);
}
return null;
}
return (
<ToolCall
initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting}
args={toolCall.function.arguments as string}
name={toolCall.function.name}
output={toolCall.function.output}
/>
);
}
} else if (part.type === ContentTypes.IMAGE_FILE) {
const imageFile = part[ContentTypes.IMAGE_FILE];
const height = imageFile.height ?? 1920;
const width = imageFile.width ?? 1080;
return (
<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,90 @@
import ProgressCircle from '~/components/Chat/Messages/Content/ProgressCircle';
import CancelledIcon from '~/components/Chat/Messages/Content/CancelledIcon';
export const CodeInProgress = ({
offset,
circumference,
radius,
isSubmitting,
progress,
}: {
progress: number;
offset: number;
circumference: number;
radius: number;
isSubmitting: boolean;
}) => {
if (progress < 1 && !isSubmitting) {
return <CancelledIcon />;
}
return (
<div
className="absolute left-0 top-0 flex h-full w-full items-center justify-center rounded-full bg-transparent text-white"
style={{ opacity: 1, transform: 'none' }}
data-projection-id="77"
>
<div className="absolute bottom-[1.5px] right-[1.5px]">
<svg
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
viewBox="0 0 20 20"
width="20"
height="20"
style={{ transform: 'translate3d(0px, 0px, 0px)' }}
preserveAspectRatio="xMidYMid meet"
>
<defs>
<clipPath id="__lottie_element_11">
<rect width="20" height="20" x="0" y="0" />
</clipPath>
</defs>
<g clipPath="url(#__lottie_element_11)">
<g
style={{ display: 'block', transform: 'matrix(1,0,0,1,-2,-2)', opacity: 1 }}
className="slide-from-left"
>
<g opacity="1" transform="matrix(1,0,0,1,7.026679992675781,8.834091186523438)">
<path
fill="rgb(177,98,253)"
fillOpacity="1"
d=" M1.2870399951934814,0.2207774966955185 C0.992609977722168,-0.07359249889850616 0.5152599811553955,-0.07359249889850616 0.22082999348640442,0.2207774966955185 C-0.07361000031232834,0.5151575207710266 -0.07361000031232834,0.992437481880188 0.22082999348640442,1.2868175506591797 C0.8473266959190369,1.9131841659545898 1.4738233089447021,2.53955078125 2.1003201007843018,3.16591739654541 C1.4738233089447021,3.7922842502593994 0.8473266959190369,4.4186506271362305 0.22082999348640442,5.045017719268799 C-0.07361000031232834,5.339417457580566 -0.07361000031232834,5.816617488861084 0.22082999348640442,6.11101770401001 C0.5152599811553955,6.405417442321777 0.992609977722168,6.405417442321777 1.2870399951934814,6.11101770401001 C2.091266632080078,5.306983947753906 2.895493268966675,4.502950668334961 3.6997199058532715,3.6989173889160156 C3.994119882583618,3.404517412185669 3.994119882583618,2.927217483520508 3.6997199058532715,2.6329174041748047 C2.895493268966675,1.8288708925247192 2.091266632080078,1.0248241424560547 1.2870399951934814,0.2207774966955185 C1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185 C1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
fillOpacity="0"
stroke="rgb(177,98,253)"
strokeOpacity="1"
strokeWidth="0.201031"
d=" M1.2870399951934814,0.2207774966955185 C0.992609977722168,-0.07359249889850616 0.5152599811553955,-0.07359249889850616 0.22082999348640442,0.2207774966955185 C-0.07361000031232834,0.5151575207710266 -0.07361000031232834,0.992437481880188 0.22082999348640442,1.2868175506591797 C0.8473266959190369,1.9131841659545898 1.4738233089447021,2.53955078125 2.1003201007843018,3.16591739654541 C1.4738233089447021,3.7922842502593994 0.8473266959190369,4.4186506271362305 0.22082999348640442,5.045017719268799 C-0.07361000031232834,5.339417457580566 -0.07361000031232834,5.816617488861084 0.22082999348640442,6.11101770401001 C0.5152599811553955,6.405417442321777 0.992609977722168,6.405417442321777 1.2870399951934814,6.11101770401001 C2.091266632080078,5.306983947753906 2.895493268966675,4.502950668334961 3.6997199058532715,3.6989173889160156 C3.994119882583618,3.404517412185669 3.994119882583618,2.927217483520508 3.6997199058532715,2.6329174041748047 C2.895493268966675,1.8288708925247192 2.091266632080078,1.0248241424560547 1.2870399951934814,0.2207774966955185 C1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185 C1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185 1.2870399951934814,0.2207774966955185"
/>
</g>
</g>
<g
style={{ display: 'block', transform: 'matrix(1,0,0,1,-2,-2)', opacity: 1 }}
className="slide-to-down"
>
<g opacity="1" transform="matrix(1,0,0,1,11.79640007019043,13.512199401855469)">
<path
fill="rgb(177,98,253)"
fillOpacity="1"
d=" M4.3225998878479,0 C3.1498000621795654,0 1.9769999980926514,0 0.8041999936103821,0 C0.36010000109672546,0 0,0.36000001430511475 0,0.804099977016449 C0,1.2482000589370728 0.36010000109672546,1.6081000566482544 0.8041999936103821,1.6081000566482544 C1.9769999980926514,1.6081000566482544 3.1498000621795654,1.6081000566482544 4.3225998878479,1.6081000566482544 C4.7667999267578125,1.6081000566482544 5.126800060272217,1.2482000589370728 5.126800060272217,0.804099977016449 C5.126800060272217,0.36000001430511475 4.7667999267578125,0 4.3225998878479,0 C4.3225998878479,0 4.3225998878479,0 4.3225998878479,0 C4.3225998878479,0 4.3225998878479,0 4.3225998878479,0"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
fillOpacity="0"
stroke="rgb(177,98,253)"
strokeOpacity="1"
strokeWidth="0.100515"
d=" M4.3225998878479,0 C3.1498000621795654,0 1.9769999980926514,0 0.8041999936103821,0 C0.36010000109672546,0 0,0.36000001430511475 0,0.804099977016449 C0,1.2482000589370728 0.36010000109672546,1.6081000566482544 0.8041999936103821,1.6081000566482544 C1.9769999980926514,1.6081000566482544 3.1498000621795654,1.6081000566482544 4.3225998878479,1.6081000566482544 C4.7667999267578125,1.6081000566482544 5.126800060272217,1.2482000589370728 5.126800060272217,0.804099977016449 C5.126800060272217,0.36000001430511475 4.7667999267578125,0 4.3225998878479,0 C4.3225998878479,0 4.3225998878479,0 4.3225998878479,0 C4.3225998878479,0 4.3225998878479,0 4.3225998878479,0"
/>
</g>
</g>
</g>
</svg>
</div>
<ProgressCircle radius={radius} circumference={circumference} offset={offset} />
</div>
);
};

View file

@ -0,0 +1,127 @@
import React, { useMemo, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { CodeInProgress } from './CodeProgress';
import { imageExtRegex } from 'librechat-data-provider';
import type { TFile, TAttachment, TAttachmentMetadata } from 'librechat-data-provider';
import ProgressText from '~/components/Chat/Messages/Content/ProgressText';
import FinishedIcon from '~/components/Chat/Messages/Content/FinishedIcon';
import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite';
import Image from '~/components/Chat/Messages/Content/Image';
import LogContent from './LogContent';
import { useProgress } from '~/hooks';
import store from '~/store';
interface ParsedArgs {
lang: string;
code: string;
}
export function useParseArgs(args: string): ParsedArgs {
return useMemo(() => {
const langMatch = args.match(/"lang"\s*:\s*"(\w+)"/);
const codeMatch = args.match(/"code"\s*:\s*"(.+?)(?="\s*,\s*"args"|$)/s);
let code = '';
if (codeMatch) {
code = codeMatch[1];
if (code.endsWith('"}')) {
code = code.slice(0, -2);
}
code = code.replace(/\\n/g, '\n').replace(/\\/g, '');
}
return {
lang: langMatch ? langMatch[1] : '',
code,
};
}, [args]);
}
export default function ExecuteCode({
initialProgress = 0.1,
args,
output = '',
isSubmitting,
attachments,
}: {
initialProgress: number;
args: string;
output?: string;
isSubmitting: boolean;
attachments?: TAttachment[];
}) {
const showAnalysisCode = useRecoilValue(store.showCode);
const [showCode, setShowCode] = useState(showAnalysisCode);
const { lang, code } = useParseArgs(args);
const progress = useProgress(initialProgress);
const radius = 56.08695652173913;
const circumference = 2 * Math.PI * radius;
const offset = circumference - progress * circumference;
return (
<>
<div className="my-2.5 flex items-center gap-2.5">
<div className="relative h-5 w-5 shrink-0">
{progress < 1 ? (
<CodeInProgress
offset={offset}
radius={radius}
progress={progress}
isSubmitting={isSubmitting}
circumference={circumference}
/>
) : (
<FinishedIcon />
)}
</div>
<ProgressText
progress={progress}
onClick={() => setShowCode((prev) => !prev)}
inProgressText="Analyzing"
finishedText="Finished analyzing"
hasInput={!!code.length}
/>
</div>
{showCode && (
<div className="code-analyze-block mb-3 mt-0.5 overflow-hidden rounded-xl bg-black">
<MarkdownLite content={code ? `\`\`\`${lang}\n${code}\n\`\`\`` : ''} />
{output.length > 0 && (
<div className="bg-gray-700 p-4 text-xs">
<div
className="prose flex flex-col-reverse text-white"
style={{
color: 'white',
}}
>
<pre className="shrink-0">
<LogContent output={output} attachments={attachments} />
</pre>
</div>
</div>
)}
</div>
)}
{attachments?.map((attachment, index) => {
const { width, height, filepath } = attachment as TFile & TAttachmentMetadata;
const isImage =
imageExtRegex.test(attachment.filename) &&
width != null &&
height != null &&
filepath != null;
if (isImage) {
return (
<Image
key={index}
altText={attachment.filename}
imagePath={filepath}
height={height}
width={width}
/>
);
}
})}
</>
);
}

View file

@ -0,0 +1,66 @@
import { isAfter } from 'date-fns';
import React, { useMemo } from 'react';
import { imageExtRegex } from 'librechat-data-provider';
import type { TAttachment } from 'librechat-data-provider';
import { useLocalize } from '~/hooks';
import LogLink from './LogLink';
interface LogContentProps {
output?: string;
attachments?: TAttachment[];
}
const LogContent: React.FC<LogContentProps> = ({ output = '', attachments }) => {
const localize = useLocalize();
const processedContent = useMemo(() => {
if (!output) {
return '';
}
const parts = output.split('Generated files:');
return parts[0].trim();
}, [output]);
const nonImageAttachments =
attachments?.filter((file) => !imageExtRegex.test(file.filename)) || [];
const renderAttachment = (file: TAttachment) => {
const now = new Date();
const expiresAt = typeof file.expiresAt === 'number' ? new Date(file.expiresAt) : null;
const isExpired = expiresAt ? isAfter(now, expiresAt) : false;
if (isExpired) {
return `${file.filename} ${localize('com_download_expired')}`;
}
// const expirationText = expiresAt
// ? ` ${localize('com_download_expires', format(expiresAt, 'MM/dd/yy HH:mm'))}`
// : ` ${localize('com_click_to_download')}`;
return (
<LogLink href={file.filepath} filename={file.filename}>
{'- '}
{file.filename} {localize('com_click_to_download')}
</LogLink>
);
};
return (
<>
{processedContent && <div>{processedContent}</div>}
{nonImageAttachments.length > 0 && (
<div>
<p>{localize('com_generated_files')}</p>
{nonImageAttachments.map((file, index) => (
<React.Fragment key={file.filepath}>
{renderAttachment(file)}
{index < nonImageAttachments.length - 1 && ', '}
</React.Fragment>
))}
</div>
)}
</>
);
};
export default LogContent;

View file

@ -0,0 +1,52 @@
import React from 'react';
import { useCodeOutputDownload } from '~/data-provider';
import { useToastContext } from '~/Providers';
interface LogLinkProps {
href: string;
filename: string;
children: React.ReactNode;
}
const LogLink: React.FC<LogLinkProps> = ({ href, filename, children }) => {
const { showToast } = useToastContext();
const { refetch: downloadFile } = useCodeOutputDownload(href);
const handleDownload = async (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
try {
const stream = await downloadFile();
if (stream.data == null || stream.data === '') {
console.error('Error downloading file: No data found');
showToast({
status: 'error',
message: 'Error downloading file',
});
return;
}
const link = document.createElement('a');
link.href = stream.data;
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(stream.data);
} catch (error) {
console.error('Error downloading file:', error);
}
};
return (
<a
href={href}
onClick={handleDownload}
target="_blank"
rel="noopener noreferrer"
className="!text-blue-400 visited:!text-purple-400 hover:underline"
>
{children}
</a>
);
};
export default LogLink;

View file

@ -8,7 +8,6 @@ import CancelledIcon from './CancelledIcon';
import ProgressText from './ProgressText';
import FinishedIcon from './FinishedIcon';
import ToolPopover from './ToolPopover';
// import ActionIcon from './ActionIcon';
import WrenchIcon from './WrenchIcon';
import { useProgress } from '~/hooks';
import { logger } from '~/utils';
@ -32,7 +31,7 @@ export default function ToolCall({
const circumference = 2 * Math.PI * radius;
const offset = circumference - progress * circumference;
const [function_name, _domain] = name.split(actionDelimiter);
const [function_name, _domain] = name.split(actionDelimiter) as [string, string | undefined];
const domain = _domain?.replaceAll(actionDomainSeparator, '.') ?? null;
const error = output?.toLowerCase()?.includes('error processing tool');
@ -50,50 +49,60 @@ export default function ToolCall({
);
return '';
}
}, [_args]);
}, [_args]) as string | undefined;
const hasInfo = useMemo(
() => (args?.length || 0) > 0 || (output?.length || 0) > 0,
() => (args?.length ?? 0) > 0 || (output?.length ?? 0) > 0,
[args, output],
);
const renderIcon = () => {
if (progress < 1) {
return (
<InProgressCall progress={progress} isSubmitting={isSubmitting} error={error}>
<div
className="absolute left-0 top-0 flex h-full w-full items-center justify-center rounded-full bg-transparent text-white"
style={{ opacity: 1, transform: 'none' }}
data-projection-id="849"
>
<div>
<WrenchIcon />
</div>
<ProgressCircle radius={radius} circumference={circumference} offset={offset} />
</div>
</InProgressCall>
);
}
return error === true ? <CancelledIcon /> : <FinishedIcon />;
};
const getFinishedText = () => {
if (domain != null && domain && domain.length !== Constants.ENCODED_DOMAIN_LENGTH) {
return localize('com_assistants_completed_action', domain);
}
return localize('com_assistants_completed_function', function_name);
};
return (
<Popover.Root>
<div className="my-2.5 flex items-center gap-2.5">
<div className="relative h-5 w-5 shrink-0">
{progress < 1 ? (
<InProgressCall progress={progress} isSubmitting={isSubmitting} error={error}>
<div
className="absolute left-0 top-0 flex h-full w-full items-center justify-center rounded-full bg-transparent text-white"
style={{ opacity: 1, transform: 'none' }}
data-projection-id="849"
>
<div>
<WrenchIcon />
</div>
<ProgressCircle radius={radius} circumference={circumference} offset={offset} />
</div>
</InProgressCall>
) : error ? (
<CancelledIcon />
) : (
<FinishedIcon />
)}
</div>
<div className="relative h-5 w-5 shrink-0">{renderIcon()}</div>
<ProgressText
progress={progress}
onClick={() => ({})}
inProgressText={localize('com_assistants_running_action')}
finishedText={
domain && domain.length !== Constants.ENCODED_DOMAIN_LENGTH
? localize('com_assistants_completed_action', domain)
: localize('com_assistants_completed_function', function_name)
}
finishedText={getFinishedText()}
hasInput={hasInfo}
popover={true}
/>
{hasInfo && (
<ToolPopover input={args} output={output} domain={domain} function_name={function_name} />
<ToolPopover
input={args ?? ''}
output={output}
domain={domain ?? ''}
function_name={function_name}
/>
)}
</div>
</Popover.Root>

View file

@ -1,12 +1,6 @@
import React, { useMemo } from 'react';
import { isAssistantsEndpoint } from 'librechat-data-provider';
import type {
TAssistantsMap,
TConversation,
TEndpointsConfig,
TPreset,
} from 'librechat-data-provider';
import { getEndpointField, getIconKey, getIconEndpoint } from '~/utils';
import type * as t from 'librechat-data-provider';
import { getEndpointField, getIconKey, getEntity, getIconEndpoint } from '~/utils';
import { icons } from '~/components/Chat/Menus/Endpoints/Icons';
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
@ -14,61 +8,72 @@ export default function ConvoIcon({
conversation,
endpointsConfig,
assistantMap,
agentsMap,
className = '',
containerClassName = '',
context,
size,
}: {
conversation: TConversation | TPreset | null;
endpointsConfig: TEndpointsConfig;
assistantMap: TAssistantsMap | undefined;
conversation: t.TConversation | t.TPreset | null;
endpointsConfig: t.TEndpointsConfig;
assistantMap: t.TAssistantsMap | undefined;
agentsMap: t.TAgentsMap | undefined;
containerClassName?: string;
context?: 'message' | 'nav' | 'landing' | 'menu-item';
className?: string;
size?: number;
}) {
const iconURL = conversation?.iconURL;
const iconURL = conversation?.iconURL ?? '';
let endpoint = conversation?.endpoint;
endpoint = getIconEndpoint({ endpointsConfig, iconURL, endpoint });
const assistant = useMemo(() => {
if (!isAssistantsEndpoint(conversation?.endpoint)) {
return undefined;
}
const endpointKey = conversation?.endpoint ?? '';
const assistantId = conversation?.assistant_id ?? '';
const { entity, isAgent } = useMemo(
() =>
getEntity({
endpoint,
agentsMap,
assistantMap,
agent_id: conversation?.agent_id,
assistant_id: conversation?.assistant_id,
}),
[endpoint, conversation?.agent_id, conversation?.assistant_id, agentsMap, assistantMap],
);
return assistantMap?.[endpointKey] ? assistantMap[endpointKey][assistantId] : undefined;
}, [conversation?.endpoint, conversation?.assistant_id, assistantMap]);
const assistantName = assistant && (assistant.name ?? '');
const name = entity?.name ?? '';
const avatar = isAgent
? (entity as t.Agent | undefined)?.avatar?.filepath
: ((entity as t.Assistant | undefined)?.metadata?.avatar as string);
const avatar = (assistant && (assistant.metadata?.avatar as string)) || '';
const endpointIconURL = getEndpointField(endpointsConfig, endpoint, 'iconURL');
const iconKey = getIconKey({ endpoint, endpointsConfig, endpointIconURL });
const Icon = icons[iconKey];
const Icon = icons[iconKey] ?? null;
return (
<>
{iconURL && iconURL.includes('http') ? (
<ConvoIconURL
preset={conversation}
endpointIconURL={endpointIconURL}
assistantName={assistantName}
assistantAvatar={avatar}
assistantName={name}
agentAvatar={avatar}
agentName={name}
context={context}
/>
) : (
<div className={containerClassName}>
{endpoint &&
Icon &&
Icon({
size,
context,
className,
iconURL: endpointIconURL,
assistantName,
endpoint,
avatar,
})}
{endpoint && Icon != null && (
<Icon
size={size}
context={context}
endpoint={endpoint}
className={className}
iconURL={endpointIconURL}
assistantName={name}
agentName={name}
avatar={avatar}
/>
)}
</div>
)}
</>

View file

@ -64,7 +64,7 @@ const ConvoIconURL: React.FC<ConvoIconURLProps> = ({
</div>
);
return <Icon />;
return <Icon context={context} />;
}
return (
@ -73,10 +73,10 @@ const ConvoIconURL: React.FC<ConvoIconURLProps> = ({
size={41}
context={context}
className="h-2/3 w-2/3"
agentName={agentName}
iconURL={endpointIconURL}
assistantName={assistantName}
avatar={assistantAvatar || agentAvatar}
agentName={agentName}
avatar={assistantAvatar ?? agentAvatar}
/>
</div>
);

View file

@ -1,6 +1,6 @@
import { EModelEndpoint, isAssistantsEndpoint, alternateName } from 'librechat-data-provider';
import UnknownIcon from '~/components/Chat/Menus/Endpoints/UnknownIcon';
import { BrainCircuit } from 'lucide-react';
import { Feather } from 'lucide-react';
import {
Plugin,
GPTIcon,
@ -109,7 +109,7 @@ const MessageEndpointIcon: React.FC<IconProps> = (props) => {
) : (
<div className="h-6 w-6">
<div className="shadow-stroke flex h-6 w-6 items-center justify-center overflow-hidden rounded-full">
<BrainCircuit className="h-2/3 w-2/3 text-gray-400" />
<Feather className="h-2/3 w-2/3 text-gray-400" />
</div>
</div>
),

View file

@ -1,6 +1,5 @@
import { Feather } from 'lucide-react';
import { EModelEndpoint, alternateName } from 'librechat-data-provider';
import { BrainCircuit } from 'lucide-react';
import UnknownIcon from '~/components/Chat/Menus/Endpoints/UnknownIcon';
import {
AzureMinimalIcon,
OpenAIMinimalIcon,
@ -13,11 +12,12 @@ import {
BedrockIcon,
Sparkles,
} from '~/components/svg';
import { cn } from '~/utils';
import UnknownIcon from '~/components/Chat/Menus/Endpoints/UnknownIcon';
import { IconProps } from '~/common';
import { cn } from '~/utils';
const MinimalIcon: React.FC<IconProps> = (props) => {
const { size = 30, iconClassName, error } = props;
const { size = 30, iconURL = '', iconClassName, error } = props;
let endpoint = 'default'; // Default value for endpoint
@ -49,7 +49,7 @@ const MinimalIcon: React.FC<IconProps> = (props) => {
[EModelEndpoint.assistants]: { icon: <Sparkles className="icon-sm" />, name: 'Assistant' },
[EModelEndpoint.azureAssistants]: { icon: <Sparkles className="icon-sm" />, name: 'Assistant' },
[EModelEndpoint.agents]: {
icon: <BrainCircuit className="icon-sm" />,
icon: <Feather className="icon-sm" />,
name: props.modelLabel ?? alternateName[EModelEndpoint.agents],
},
[EModelEndpoint.bedrock]: {
@ -57,21 +57,14 @@ const MinimalIcon: React.FC<IconProps> = (props) => {
name: props.modelLabel ?? alternateName[EModelEndpoint.bedrock],
},
default: {
icon: (
<UnknownIcon
iconURL={props.iconURL}
endpoint={endpoint}
className="icon-sm"
context="nav"
/>
),
icon: <UnknownIcon iconURL={iconURL} endpoint={endpoint} className="icon-sm" context="nav" />,
name: endpoint,
},
};
let { icon, name } = endpointIcons[endpoint] ?? endpointIcons.default;
if (props.iconURL && endpointIcons[props.iconURL]) {
({ icon, name } = endpointIcons[props.iconURL]);
if (iconURL && endpointIcons[iconURL] != null) {
({ icon, name } = endpointIcons[iconURL]);
}
return (

View file

@ -138,6 +138,7 @@ const ContentRender = memo(
enterEdit={enterEdit}
siblingIdx={siblingIdx}
setSiblingIdx={setSiblingIdx}
attachments={msg.attachments}
/>
</div>
</div>

View file

@ -21,7 +21,7 @@ import { useLocalize } from '~/hooks';
import { formatBytes } from '~/utils';
function Avatar({
agent_id,
agent_id = '',
avatar,
createMutation,
}: {
@ -31,9 +31,9 @@ function Avatar({
}) {
const queryClient = useQueryClient();
const [menuOpen, setMenuOpen] = useState(false);
const [previewUrl, setPreviewUrl] = useState('');
const [progress, setProgress] = useState<number>(1);
const [input, setInput] = useState<File | null>(null);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const lastSeenCreatedId = useRef<string | null>(null);
const { data: fileConfig = defaultFileConfig } = useGetFileConfig({
select: (data) => mergeFileConfig(data),
@ -54,7 +54,8 @@ function Avatar({
}
setInput(null);
setPreviewUrl(data.avatar?.filepath as string | null);
const newUrl = data.avatar?.filepath ?? '';
setPreviewUrl(newUrl);
const res = queryClient.getQueryData<AgentListResponse>([
QueryKeys.agents,
@ -65,16 +66,15 @@ function Avatar({
return;
}
const agents =
res.data.map((agent) => {
if (agent.id === agent_id) {
return {
...agent,
...data,
};
}
return agent;
}) ?? [];
const agents = res.data.map((agent) => {
if (agent.id === agent_id) {
return {
...agent,
...data,
};
}
return agent;
});
queryClient.setQueryData<AgentListResponse>([QueryKeys.agents, defaultOrderQuery], {
...res,
@ -86,7 +86,7 @@ function Avatar({
onError: (error) => {
console.error('Error:', error);
setInput(null);
setPreviewUrl(null);
setPreviewUrl('');
showToast({ message: localize('com_ui_upload_error'), status: 'error' });
setProgress(1);
},
@ -103,8 +103,10 @@ function Avatar({
}, [input]);
useEffect(() => {
if (avatar) {
setPreviewUrl((avatar.filepath as string | undefined) ?? null);
if (avatar && avatar.filepath) {
setPreviewUrl(avatar.filepath);
} else {
setPreviewUrl('');
}
}, [avatar]);
@ -147,29 +149,31 @@ function Avatar({
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
const file = event.target.files?.[0];
const sizeLimit = fileConfig.avatarSizeLimit ?? 0;
if (fileConfig.avatarSizeLimit && file && file.size <= fileConfig.avatarSizeLimit) {
if (sizeLimit && file && file.size <= sizeLimit) {
setInput(file);
setMenuOpen(false);
if (!agent_id) {
const currentId = agent_id ?? '';
if (!currentId) {
return;
}
const formData = new FormData();
formData.append('file', file, file.name);
formData.append('agent_id', agent_id);
formData.append('agent_id', currentId);
if (typeof avatar === 'object') {
formData.append('avatar', JSON.stringify(avatar));
}
uploadAvatar({
agent_id,
agent_id: currentId,
formData,
});
} else {
const megabytes = fileConfig.avatarSizeLimit ? formatBytes(fileConfig.avatarSizeLimit) : 2;
const megabytes = sizeLimit ? formatBytes(sizeLimit) : 2;
showToast({
message: localize('com_ui_upload_invalid_var', megabytes + ''),
status: 'error',

View file

@ -1,19 +1,22 @@
import React, { useState, useMemo, useCallback } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { Controller, useWatch, useFormContext } from 'react-hook-form';
import { QueryKeys, Capabilities, EModelEndpoint } from 'librechat-data-provider';
import { QueryKeys, AgentCapabilities, EModelEndpoint, SystemRoles } from 'librechat-data-provider';
import type { TConfig, TPlugin } from 'librechat-data-provider';
import type { AgentForm, AgentPanelProps } from '~/common';
import { cn, defaultTextProps, removeFocusOutlines, getEndpointField, getIconKey } from '~/utils';
import { useCreateAgentMutation, useUpdateAgentMutation } from '~/data-provider';
import { useToastContext, useFileMapContext } from '~/Providers';
import { icons } from '~/components/Chat/Menus/Endpoints/Icons';
import Action from '~/components/SidePanel/Builder/Action';
import { useLocalize } from '~/hooks';
import { ToolSelectDialog } from '~/components/Tools';
import { useToastContext } from '~/Providers';
import { useLocalize, useAuthContext } from '~/hooks';
import CapabilitiesForm from './CapabilitiesForm';
import { processAgentOption } from '~/utils';
import { Spinner } from '~/components/svg';
import DeleteButton from './DeleteButton';
import AgentAvatar from './AgentAvatar';
import FileSearch from './FileSearch';
import ShareAgent from './ShareAgent';
import AgentTool from './AgentTool';
import { Panel } from '~/common';
@ -33,6 +36,8 @@ export default function AgentConfig({
setActivePanel,
setCurrentAgentId,
}: AgentPanelProps & { agentsConfig?: TConfig | null }) {
const { user } = useAuthContext();
const fileMap = useFileMapContext();
const queryClient = useQueryClient();
const allTools = queryClient.getQueryData<TPlugin[]>([QueryKeys.tools]) ?? [];
@ -51,21 +56,41 @@ export default function AgentConfig({
const agent_id = useWatch({ control, name: 'id' });
const toolsEnabled = useMemo(
() => agentsConfig?.capabilities?.includes(Capabilities.tools),
() => agentsConfig?.capabilities?.includes(AgentCapabilities.tools),
[agentsConfig],
);
const actionsEnabled = useMemo(
() => agentsConfig?.capabilities?.includes(Capabilities.actions),
() => agentsConfig?.capabilities?.includes(AgentCapabilities.actions),
[agentsConfig],
);
// const retrievalEnabled = useMemo(
// () => agentsConfig?.capabilities?.includes(Capabilities.retrieval),
// [agentsConfig],
// );
// const codeEnabled = useMemo(
// () => agentsConfig?.capabilities?.includes(Capabilities.code_interpreter),
// [agentsConfig],
// );
const fileSearchEnabled = useMemo(
() => agentsConfig?.capabilities?.includes(AgentCapabilities.file_search) ?? false,
[agentsConfig],
);
const codeEnabled = useMemo(
() => agentsConfig?.capabilities?.includes(AgentCapabilities.execute_code) ?? false,
[agentsConfig],
);
const knowledge_files = useMemo(() => {
if (typeof agent === 'string') {
return [];
}
if (agent?.id !== agent_id) {
return [];
}
if (agent.knowledge_files) {
return agent.knowledge_files;
}
const _agent = processAgentOption({
agent,
fileMap,
});
return _agent.knowledge_files ?? [];
}, [agent, agent_id, fileMap]);
/* Mutations */
const update = useUpdateAgentMutation({
@ -118,8 +143,6 @@ export default function AgentConfig({
setActivePanel(Panel.actions);
}, [agent_id, setActivePanel, showToast, localize]);
// Provider Icon logic
const providerValue = typeof provider === 'string' ? provider : provider?.value;
let endpointType: EModelEndpoint | undefined;
let endpointIconURL: string | undefined;
@ -280,10 +303,17 @@ export default function AgentConfig({
</div>
</button>
</div>
<CapabilitiesForm
codeEnabled={codeEnabled}
agentsConfig={agentsConfig}
retrievalEnabled={false}
/>
{/* File Search */}
{fileSearchEnabled && <FileSearch agent_id={agent_id} files={knowledge_files} />}
{/* Agent Tools & Actions */}
<div className="mb-6">
<label className={labelClass}>
{`${toolsEnabled === true ? localize('com_assistants_tools') : ''}
{`${toolsEnabled === true ? localize('com_ui_tools') : ''}
${toolsEnabled === true && actionsEnabled === true ? ' + ' : ''}
${actionsEnabled === true ? localize('com_assistants_actions') : ''}`}
</label>
@ -344,11 +374,14 @@ export default function AgentConfig({
setCurrentAgentId={setCurrentAgentId}
createMutation={create}
/>
<ShareAgent
agent_id={agent_id}
agentName={agent?.name ?? ''}
projectIds={agent?.projectIds ?? []}
/>
{(agent?.author === user?.id || user?.role === SystemRoles.ADMIN) && (
<ShareAgent
agent_id={agent_id}
agentName={agent?.name ?? ''}
projectIds={agent?.projectIds ?? []}
isCollaborative={agent?.isCollaborative}
/>
)}
{/* Submit Button */}
<button
className="btn btn-primary focus:shadow-outline flex w-full items-center justify-center px-4 py-2 font-semibold text-white hover:bg-green-600 focus:border-green-500"

View file

@ -3,15 +3,20 @@ import { useGetModelsQuery } from 'librechat-data-provider/react-query';
import { Controller, useWatch, useForm, FormProvider } from 'react-hook-form';
import {
Tools,
SystemRoles,
EModelEndpoint,
isAssistantsEndpoint,
defaultAgentFormValues,
} from 'librechat-data-provider';
import type { TConfig } from 'librechat-data-provider';
import type { AgentForm, AgentPanelProps, Option } from '~/common';
import { useCreateAgentMutation, useUpdateAgentMutation } from '~/data-provider';
import { useSelectAgent, useLocalize } from '~/hooks';
// import CapabilitiesForm from './CapabilitiesForm';
import type { AgentForm, AgentPanelProps, StringOption } from '~/common';
import {
useCreateAgentMutation,
useUpdateAgentMutation,
useGetAgentByIdQuery,
} from '~/data-provider';
import { useSelectAgent, useLocalize, useAuthContext } from '~/hooks';
import AgentPanelSkeleton from './AgentPanelSkeleton';
import { createProviderOption } from '~/utils';
import { useToastContext } from '~/Providers';
import AgentConfig from './AgentConfig';
@ -29,11 +34,17 @@ export default function AgentPanel({
agentsConfig,
endpointsConfig,
}: AgentPanelProps & { agentsConfig?: TConfig | null }) {
const { onSelect: onSelectAgent } = useSelectAgent();
const { showToast } = useToastContext();
const localize = useLocalize();
const { user } = useAuthContext();
const { showToast } = useToastContext();
const { onSelect: onSelectAgent } = useSelectAgent();
const modelsQuery = useGetModelsQuery();
const agentQuery = useGetAgentByIdQuery(current_agent_id ?? '', {
enabled: !!(current_agent_id ?? ''),
});
const models = useMemo(() => modelsQuery.data ?? {}, [modelsQuery.data]);
const methods = useForm<AgentForm>({
defaultValues: defaultAgentFormValues,
@ -81,7 +92,7 @@ export default function AgentPanel({
onSuccess: (data) => {
setCurrentAgentId(data.id);
showToast({
message: `${localize('com_assistants_create_success ')} ${
message: `${localize('com_assistants_create_success')} ${
data.name ?? localize('com_ui_agent')
}`,
});
@ -101,23 +112,25 @@ export default function AgentPanel({
(data: AgentForm) => {
const tools = data.tools ?? [];
if (data.code_interpreter) {
tools.push(Tools.code_interpreter);
if (data.execute_code === true) {
tools.push(Tools.execute_code);
}
if (data.retrieval) {
if (data.file_search === true) {
tools.push(Tools.file_search);
}
const {
name,
model,
model_parameters,
provider: _provider,
description,
instructions,
model: _model,
model_parameters,
provider: _provider,
} = data;
const provider = typeof _provider === 'string' ? _provider : (_provider as Option).value;
const model = _model ?? '';
const provider =
(typeof _provider === 'string' ? _provider : (_provider as StringOption).value) ?? '';
if (agent_id) {
update.mutate({
@ -135,6 +148,13 @@ export default function AgentPanel({
return;
}
if (!provider || !model) {
return showToast({
message: localize('com_agents_missing_provider_model'),
status: 'error',
});
}
create.mutate({
name,
description,
@ -145,7 +165,7 @@ export default function AgentPanel({
model_parameters,
});
},
[agent_id, create, update],
[agent_id, create, update, showToast, localize],
);
const handleSelectAgent = useCallback(() => {
@ -154,6 +174,15 @@ export default function AgentPanel({
}
}, [agent_id, onSelectAgent]);
if (agentQuery.isInitialLoading) {
return <AgentPanelSkeleton />;
}
const canEditAgent =
agentQuery.data?.isCollaborative ?? false
? true
: agentQuery.data?.author === user?.id || user?.role === SystemRoles.ADMIN;
return (
<FormProvider {...methods}>
<form
@ -169,6 +198,7 @@ export default function AgentPanel({
<AgentSelect
reset={reset}
value={field.value}
agentQuery={agentQuery}
setCurrentAgentId={setCurrentAgentId}
selectedAgentId={current_agent_id ?? null}
createMutation={create}
@ -188,10 +218,25 @@ export default function AgentPanel({
</button>
)}
</div>
{activePanel === Panel.model ? (
<ModelPanel setActivePanel={setActivePanel} providers={providers} models={models} />
) : null}
{activePanel === Panel.builder ? (
{!canEditAgent && (
<div className="flex h-[30vh] w-full items-center justify-center">
<div className="text-center">
<h2 className="text-token-text-primary m-2 text-xl font-semibold">
{localize('com_agents_not_available')}
</h2>
<p className="text-token-text-secondary">{localize('com_agents_no_access')}</p>
</div>
</div>
)}
{canEditAgent && activePanel === Panel.model && (
<ModelPanel
setActivePanel={setActivePanel}
agent_id={agent_id}
providers={providers}
models={models}
/>
)}
{canEditAgent && activePanel === Panel.builder && (
<AgentConfig
actions={actions}
setAction={setAction}
@ -200,7 +245,7 @@ export default function AgentPanel({
endpointsConfig={endpointsConfig}
setCurrentAgentId={setCurrentAgentId}
/>
) : null}
)}
</form>
</FormProvider>
);

View file

@ -0,0 +1,70 @@
import React from 'react';
import { Skeleton } from '~/components/ui';
export default function AgentPanelSkeleton() {
return (
<div className="scrollbar-gutter-stable h-auto w-full flex-shrink-0 overflow-x-hidden">
{/* Agent Select and Button */}
<div className="mt-1 flex w-full gap-2">
<Skeleton className="h-[40px] w-3/4 rounded" />
<Skeleton className="h-[40px] w-1/4 rounded" />
</div>
<div className="h-auto bg-white px-4 pb-8 pt-3 dark:bg-transparent">
{/* Avatar */}
<div className="mb-4">
<div className="flex w-full items-center justify-center gap-4">
<Skeleton className="relative h-20 w-20 rounded-full" />
</div>
{/* Name */}
<Skeleton className="mb-2 h-5 w-1/5 rounded" />
<Skeleton className="mb-1 h-[40px] w-full rounded" />
<Skeleton className="h-3 w-1/4 rounded" />
</div>
{/* Description */}
<div className="mb-4">
<Skeleton className="mb-2 h-5 w-1/4 rounded" />
<Skeleton className="h-[40px] w-full rounded" />
</div>
{/* Instructions */}
<div className="mb-6">
<Skeleton className="mb-2 h-5 w-1/4 rounded" />
<Skeleton className="h-[100px] w-full rounded" />
</div>
{/* Model and Provider */}
<div className="mb-6">
<Skeleton className="mb-2 h-5 w-1/4 rounded" />
<Skeleton className="h-[40px] w-full rounded" />
</div>
{/* Capabilities */}
<div className="mb-6">
<Skeleton className="mb-2 h-5 w-1/4 rounded" />
<Skeleton className="mb-2 h-[40px] w-full rounded" />
<Skeleton className="h-[40px] w-full rounded" />
</div>
{/* Tools & Actions */}
<div className="mb-6">
<Skeleton className="mb-2 h-5 w-1/4 rounded" />
<Skeleton className="mb-2 h-[40px] w-full rounded" />
<Skeleton className="mb-2 h-[40px] w-full rounded" />
<div className="flex space-x-2">
<Skeleton className="h-8 w-1/2 rounded" />
<Skeleton className="h-8 w-1/2 rounded" />
</div>
</div>
{/* Bottom Buttons */}
<div className="flex items-center justify-end gap-2">
<Skeleton className="h-[40px] w-[100px] rounded" />
<Skeleton className="h-[40px] w-[100px] rounded" />
<Skeleton className="h-[40px] w-[100px] rounded" />
</div>
</div>
</div>
);
}

View file

@ -1,5 +1,5 @@
import { useState, useEffect, useMemo } from 'react';
import { Capabilities } from 'librechat-data-provider';
import { EModelEndpoint } from 'librechat-data-provider';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type { ActionsEndpoint } from '~/common';
import type { Action, TConfig, TEndpointsConfig } from 'librechat-data-provider';
@ -18,19 +18,14 @@ export default function AgentPanelSwitch() {
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
const agentsConfig = useMemo(
() =>
// endpointsConfig?.[EModelEndpoint.agents] ??
({
// for testing purposes
capabilities: [Capabilities.tools, Capabilities.actions],
} as TConfig),
// [endpointsConfig]);
[],
() => endpointsConfig?.[EModelEndpoint.agents] ?? ({} as TConfig | null),
[endpointsConfig],
);
useEffect(() => {
if (conversation?.agent_id) {
setCurrentAgentId(conversation?.agent_id);
const agent_id = conversation?.agent_id ?? '';
if (agent_id) {
setCurrentAgentId(agent_id);
}
}, [conversation?.agent_id]);

View file

@ -1,21 +1,21 @@
import { Plus, EarthIcon } from 'lucide-react';
import { useCallback, useEffect, useRef } from 'react';
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
import { Capabilities, defaultAgentFormValues } from 'librechat-data-provider';
import { AgentCapabilities, defaultAgentFormValues } from 'librechat-data-provider';
import type { UseMutationResult, QueryObserverResult } from '@tanstack/react-query';
import type { Agent, AgentCreateParams } from 'librechat-data-provider';
import type { UseMutationResult } from '@tanstack/react-query';
import type { UseFormReset } from 'react-hook-form';
import type { AgentCapabilities, AgentForm, TAgentOption } from '~/common';
import type { TAgentCapabilities, AgentForm, TAgentOption } from '~/common';
import { cn, createDropdownSetter, createProviderOption, processAgentOption } from '~/utils';
import { useListAgentsQuery, useGetAgentByIdQuery } from '~/data-provider';
import SelectDropDown from '~/components/ui/SelectDropDown';
// import { useFileMapContext } from '~/Providers';
import { useListAgentsQuery } from '~/data-provider';
import { useLocalize } from '~/hooks';
const keys = new Set(Object.keys(defaultAgentFormValues));
export default function AgentSelect({
reset,
agentQuery,
value: currentAgentValue,
selectedAgentId = null,
setCurrentAgentId,
@ -24,12 +24,11 @@ export default function AgentSelect({
reset: UseFormReset<AgentForm>;
value?: TAgentOption;
selectedAgentId: string | null;
agentQuery: QueryObserverResult<Agent>;
setCurrentAgentId: React.Dispatch<React.SetStateAction<string | undefined>>;
createMutation: UseMutationResult<Agent, Error, AgentCreateParams>;
}) {
const localize = useLocalize();
// TODO: file handling for agents
// const fileMap = useFileMapContext();
const lastSelectedAgent = useRef<string | null>(null);
const { data: startupConfig } = useGetStartupConfig();
@ -39,15 +38,10 @@ export default function AgentSelect({
processAgentOption({
agent,
instanceProjectId: startupConfig?.instanceProjectId,
/* fileMap */
}),
),
});
const agentQuery = useGetAgentByIdQuery(selectedAgentId ?? '', {
enabled: !!(selectedAgentId ?? ''),
});
const resetAgentForm = useCallback(
(fullAgent: Agent) => {
const { instanceProjectId } = startupConfig ?? {};
@ -61,17 +55,26 @@ export default function AgentSelect({
icon: isGlobal ? <EarthIcon className={'icon-lg text-green-400'} /> : null,
};
const actions: AgentCapabilities = {
[Capabilities.code_interpreter]: false,
[Capabilities.image_vision]: false,
[Capabilities.retrieval]: false,
const capabilities: TAgentCapabilities = {
[AgentCapabilities.execute_code]: false,
[AgentCapabilities.file_search]: false,
};
const formValues: Partial<AgentForm & AgentCapabilities> = {
...actions,
const agentTools: string[] = [];
(fullAgent.tools ?? []).forEach((tool) => {
if (capabilities[tool] !== undefined) {
capabilities[tool] = true;
return;
}
agentTools.push(tool);
});
const formValues: Partial<AgentForm & TAgentCapabilities> = {
...capabilities,
agent: update,
model: update.model,
tools: update.tools ?? [],
tools: agentTools,
};
Object.entries(fullAgent).forEach(([name, value]) => {
@ -91,7 +94,7 @@ export default function AgentSelect({
reset(formValues);
},
[reset],
[reset, startupConfig],
);
const onSelect = useCallback(

View file

@ -1,12 +1,12 @@
import { useMemo } from 'react';
// import { Capabilities } from 'librechat-data-provider';
import { useFormContext, useWatch } from 'react-hook-form';
// import { useFormContext, useWatch } from 'react-hook-form';
import type { TConfig } from 'librechat-data-provider';
import type { AgentForm } from '~/common';
// import type { AgentForm } from '~/common';
// import ImageVision from './ImageVision';
import { useLocalize } from '~/hooks';
import Retrieval from './Retrieval';
import CodeFiles from './CodeFiles';
// import CodeFiles from './CodeFiles';
import Code from './Code';
export default function CapabilitiesForm({
@ -20,25 +20,21 @@ export default function CapabilitiesForm({
}) {
const localize = useLocalize();
const methods = useFormContext<AgentForm>();
const { control } = methods;
const agent = useWatch({ control, name: 'agent' });
const agent_id = useWatch({ control, name: 'id' });
const files = useMemo(() => {
if (typeof agent === 'string') {
return [];
}
return agent?.code_files;
}, [agent]);
// const methods = useFormContext<AgentForm>();
// const { control } = methods;
// const agent = useWatch({ control, name: 'agent' });
// const agent_id = useWatch({ control, name: 'id' });
// const files = useMemo(() => {
// if (typeof agent === 'string') {
// return [];
// }
// return agent?.code_files;
// }, [agent]);
const retrievalModels = useMemo(
() => new Set(agentsConfig?.retrievalModels ?? []),
[agentsConfig],
);
// const imageVisionEnabled = useMemo(
// () => agentsConfig?.capabilities?.includes(Capabilities.image_vision),
// [agentsConfig],
// );
return (
<div className="mb-4">
@ -50,10 +46,10 @@ export default function CapabilitiesForm({
</span>
</div>
<div className="flex flex-col items-start gap-2">
{codeEnabled && <Code />}
{retrievalEnabled && <Retrieval retrievalModels={retrievalModels} />}
{codeEnabled === true && <Code />}
{retrievalEnabled === true && <Retrieval retrievalModels={retrievalModels} />}
{/* {imageVisionEnabled && version == 1 && <ImageVision />} */}
{codeEnabled && <CodeFiles agent_id={agent_id} files={files} />}
{/* {codeEnabled && <CodeFiles agent_id={agent_id} files={files} />} */}
</div>
</div>
);

View file

@ -1,4 +1,4 @@
import { Capabilities } from 'librechat-data-provider';
import { AgentCapabilities } from 'librechat-data-provider';
import { useFormContext, Controller } from 'react-hook-form';
import type { AgentForm } from '~/common';
import {
@ -22,7 +22,7 @@ export default function Code() {
<HoverCard openDelay={50}>
<div className="flex items-center">
<Controller
name={Capabilities.code_interpreter}
name={AgentCapabilities.execute_code}
control={control}
render={({ field }) => (
<Checkbox
@ -30,30 +30,34 @@ export default function Code() {
checked={field.value}
onCheckedChange={field.onChange}
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
value={field?.value?.toString()}
value={field.value.toString()}
/>
)}
/>
<div className="flex items-center space-x-2">
<button
type="button"
className="flex items-center space-x-2"
onClick={() =>
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
setValue(AgentCapabilities.execute_code, !getValues(AgentCapabilities.execute_code), {
shouldDirty: true,
})
}
>
<label
className="form-check-label text-token-text-primary w-full cursor-pointer"
htmlFor={Capabilities.code_interpreter}
onClick={() =>
setValue(Capabilities.code_interpreter, !getValues(Capabilities.code_interpreter), {
shouldDirty: true,
})
}
htmlFor={AgentCapabilities.execute_code}
>
{localize('com_assistants_code_interpreter')}
{localize('com_agents_execute_code')}
</label>
<HoverCardTrigger>
<CircleHelpIcon className="h-5 w-5 text-gray-500" />
</HoverCardTrigger>
</div>
</button>
<HoverCardPortal>
<HoverCardContent side={ESide.Top} className="w-80">
<div className="space-y-2">
<p className="text-sm text-gray-600 dark:text-gray-300">
<p className="text-sm text-text-secondary">
{/* // TODO: add a Code Interpreter description */}
</p>
</div>

View file

@ -0,0 +1,120 @@
import { useState, useRef, useEffect } from 'react';
import { useFormContext } from 'react-hook-form';
import {
EModelEndpoint,
EToolResources,
mergeFileConfig,
AgentCapabilities,
retrievalMimeTypes,
fileConfig as defaultFileConfig,
} from 'librechat-data-provider';
import type { ExtendedFile, AgentForm } from '~/common';
import FileRow from '~/components/Chat/Input/Files/FileRow';
import FileSearchCheckbox from './FileSearchCheckbox';
import { useGetFileConfig } from '~/data-provider';
import { AttachmentIcon } from '~/components/svg';
import { useFileHandling } from '~/hooks/Files';
import useLocalize from '~/hooks/useLocalize';
import { useChatContext } from '~/Providers';
export default function FileSearch({
agent_id,
files: _files,
}: {
agent_id: string;
files?: [string, ExtendedFile][];
}) {
const localize = useLocalize();
const { setFilesLoading } = useChatContext();
const { watch } = useFormContext<AgentForm>();
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.file_search },
fileSetter: setFiles,
});
useEffect(() => {
if (_files) {
setFiles(new Map(_files));
}
}, [_files]);
const fileSearchChecked = watch(AgentCapabilities.file_search);
const endpointFileConfig = fileConfig.endpoints[EModelEndpoint.agents];
const disabled = endpointFileConfig.disabled ?? false;
if (disabled === true) {
return null;
}
const handleButtonClick = () => {
// necessary to reset the input
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
fileInputRef.current?.click();
};
return (
<div className="mb-6">
<div className="mb-1.5 flex items-center gap-2">
<span>
<label className="text-token-text-primary block font-medium">
{localize('com_assistants_file_search')}
</label>
</span>
</div>
<FileSearchCheckbox />
<div className="flex flex-col gap-2">
<div>
<button
type="button"
disabled={!agent_id || fileSearchChecked === false}
className="btn btn-neutral border-token-border-light relative h-8 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 || fileSearchChecked === false}
onChange={handleFileChange}
/>
{localize('com_ui_upload_files')}
</div>
</button>
</div>
{/* Disabled Message */}
{agent_id ? null : (
<div className="text-sm text-text-secondary">
{localize('com_agents_file_search_disabled')}
</div>
)}
{/* Knowledge Files */}
<FileRow
files={files}
setFiles={setFiles}
setFilesLoading={setFilesLoading}
agent_id={agent_id}
tool_resource={EToolResources.file_search}
fileFilter={(file: ExtendedFile) =>
retrievalMimeTypes.some((regex) => regex.test(file.type ?? ''))
}
Wrapper={({ children }) => <div className="flex flex-wrap gap-2">{children}</div>}
/>
</div>
</div>
);
}

View file

@ -0,0 +1,70 @@
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 FileSearchCheckbox() {
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.file_search}
control={control}
render={({ field }) => (
<Checkbox
{...field}
checked={field.value}
onCheckedChange={field.onChange}
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
value={field.value.toString()}
/>
)}
/>
<button
type="button"
className="flex items-center space-x-2"
onClick={() =>
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
setValue(AgentCapabilities.file_search, !getValues(AgentCapabilities.file_search), {
shouldDirty: true,
})
}
>
<label
className="form-check-label text-token-text-primary w-full cursor-pointer"
htmlFor={AgentCapabilities.file_search}
>
{localize('com_agents_enable_file_search')}
</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_file_search_info')}
</p>
</div>
</HoverCardContent>
</HoverCardPortal>
</div>
</HoverCard>
</>
);
}

View file

@ -1,13 +1,18 @@
import { useEffect, useMemo } from 'react';
import React, { useMemo, useEffect } from 'react';
import { ChevronLeft } from 'lucide-react';
import { Controller, useFormContext } from 'react-hook-form';
import type { AgentForm, AgentModelPanelProps } from '~/common';
import { SelectDropDown, ModelParameters } from '~/components/ui';
import { cn, cardStyle } from '~/utils';
import { getSettingsKeys } from 'librechat-data-provider';
import { useFormContext, Controller } from 'react-hook-form';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type * as t from 'librechat-data-provider';
import type { AgentForm, AgentModelPanelProps, StringOption } from '~/common';
import { componentMapping } from '~/components/SidePanel/Parameters/components';
import { agentSettings } from '~/components/SidePanel/Parameters/settings';
import { getEndpointField, cn, cardStyle } from '~/utils';
import { SelectDropDown } from '~/components/ui';
import { useLocalize } from '~/hooks';
import { Panel } from '~/common';
export default function ModelPanel({
export default function Parameters({
setActivePanel,
providers,
models: modelsData,
@ -15,30 +20,56 @@ export default function ModelPanel({
const localize = useLocalize();
const { control, setValue, watch } = useFormContext<AgentForm>();
const model = watch('model');
const modelParameters = watch('model_parameters');
const providerOption = watch('provider');
const model = watch('model');
const provider = useMemo(() => {
if (!providerOption) {
return '';
}
return typeof providerOption === 'string' ? providerOption : providerOption.value;
const value =
typeof providerOption === 'string'
? providerOption
: (providerOption as StringOption | undefined)?.value;
return value ?? '';
}, [providerOption]);
const models = useMemo(() => (provider ? modelsData[provider] : []), [modelsData, provider]);
useEffect(() => {
if (provider && model) {
const modelExists = models.includes(model);
const _model = model ?? '';
if (provider && _model) {
const modelExists = models.includes(_model);
if (!modelExists) {
const newModels = modelsData[provider];
setValue('model', newModels[0] ?? '');
}
}
if (provider && !_model) {
setValue('model', models[0] ?? '');
}
}, [provider, models, modelsData, setValue, model]);
const { data: endpointsConfig } = useGetEndpointsQuery();
const bedrockRegions = useMemo(() => {
return endpointsConfig?.[provider]?.availableRegions ?? [];
}, [endpointsConfig, provider]);
const endpointType = useMemo(
() => getEndpointField(endpointsConfig, provider, 'type'),
[provider, endpointsConfig],
);
const parameters = useMemo(() => {
const [combinedKey, endpointKey] = getSettingsKeys(endpointType ?? provider, model ?? '');
return agentSettings[combinedKey] ?? agentSettings[endpointKey];
}, [endpointType, model, provider]);
const setOption = (optionKey: keyof t.AgentModelParameters) => (value: t.AgentParameterValue) => {
setValue(`model_parameters.${optionKey}`, value);
};
return (
<div className="h-full overflow-auto px-2 pb-12 text-sm">
<div className="scrollbar-gutter-stable h-full min-h-[50vh] overflow-auto pb-12 text-sm">
<div className="model-panel relative flex flex-col items-center px-16 py-6 text-center">
<div className="absolute left-0 top-6">
<button
@ -56,228 +87,125 @@ export default function ModelPanel({
<div className="mb-2 mt-2 text-xl font-medium">{localize('com_ui_model_parameters')}</div>
</div>
{/* Endpoint aka Provider for Agents */}
<div className="mb-4">
<label
className="text-token-text-primary model-panel-label mb-2 block font-medium"
htmlFor="provider"
>
{localize('com_ui_provider')} <span className="text-red-500">*</span>
</label>
<Controller
name="provider"
control={control}
rules={{ required: true, minLength: 1 }}
render={({ field, fieldState: { error } }) => (
<>
<SelectDropDown
emptyTitle={true}
value={field.value ?? ''}
placeholder={localize('com_ui_select_provider')}
setValue={field.onChange}
availableValues={providers}
showAbove={false}
showLabel={false}
className={cn(
cardStyle,
'flex h-[40px] w-full flex-none items-center justify-center border-none px-4 hover:cursor-pointer',
!field.value && 'border-2 border-yellow-400',
<div className="p-2">
{/* Endpoint aka Provider for Agents */}
<div className="mb-4">
<label
className="text-token-text-primary model-panel-label mb-2 block font-medium"
htmlFor="provider"
>
{localize('com_ui_provider')} <span className="text-red-500">*</span>
</label>
<Controller
name="provider"
control={control}
rules={{ required: true, minLength: 1 }}
render={({ field, fieldState: { error } }) => (
<>
<SelectDropDown
emptyTitle={true}
value={field.value ?? ''}
placeholder={localize('com_ui_select_provider')}
setValue={field.onChange}
availableValues={providers}
showAbove={false}
showLabel={false}
className={cn(
cardStyle,
'flex h-[40px] w-full flex-none items-center justify-center border-none px-4 hover:cursor-pointer',
(field.value === undefined || field.value === '') &&
'border-2 border-yellow-400',
)}
containerClassName={cn('rounded-md', error ? 'border-red-500 border-2' : '')}
/>
{error && (
<span className="model-panel-error text-sm text-red-500 transition duration-300 ease-in-out">
{localize('com_ui_field_required')}
</span>
)}
containerClassName={cn('rounded-md', error ? 'border-red-500 border-2' : '')}
/>
{error && (
<span className="model-panel-error text-sm text-red-500 transition duration-300 ease-in-out">
{localize('com_ui_field_required')}
</span>
)}
</>
)}
/>
</div>
{/* Model */}
<div className="model-panel-section mb-6">
<label
className={cn(
'text-token-text-primary model-panel-label mb-2 block font-medium',
!provider && 'text-gray-500 dark:text-gray-400',
)}
htmlFor="model"
>
{localize('com_ui_model')} <span className="text-red-500">*</span>
</label>
<Controller
name="model"
control={control}
rules={{ required: true, minLength: 1 }}
render={({ field, fieldState: { error } }) => (
<>
<SelectDropDown
emptyTitle={true}
placeholder={
provider
? localize('com_ui_select_model')
: localize('com_ui_select_provider_first')
}
value={field.value}
setValue={field.onChange}
availableValues={models}
showAbove={false}
showLabel={false}
disabled={!provider}
className={cn(
cardStyle,
'flex h-[40px] w-full flex-none items-center justify-center border-none px-4',
!provider ? 'cursor-not-allowed bg-gray-200' : 'hover:cursor-pointer',
</>
)}
/>
</div>
{/* Model */}
<div className="model-panel-section mb-4">
<label
className={cn(
'text-token-text-primary model-panel-label mb-2 block font-medium',
!provider && 'text-gray-500 dark:text-gray-400',
)}
htmlFor="model"
>
{localize('com_ui_model')} <span className="text-red-500">*</span>
</label>
<Controller
name="model"
control={control}
rules={{ required: true, minLength: 1 }}
render={({ field, fieldState: { error } }) => (
<>
<SelectDropDown
emptyTitle={true}
placeholder={
provider
? localize('com_ui_select_model')
: localize('com_ui_select_provider_first')
}
value={field.value}
setValue={field.onChange}
availableValues={models}
showAbove={false}
showLabel={false}
disabled={!provider}
className={cn(
cardStyle,
'flex h-[40px] w-full flex-none items-center justify-center border-none px-4',
!provider ? 'cursor-not-allowed bg-gray-200' : 'hover:cursor-pointer',
)}
containerClassName={cn('rounded-md', error ? 'border-red-500 border-2' : '')}
/>
{provider && error && (
<span className="text-sm text-red-500 transition duration-300 ease-in-out">
{localize('com_ui_field_required')}
</span>
)}
containerClassName={cn('rounded-md', error ? 'border-red-500 border-2' : '')}
/>
{provider && error && (
<span className="text-sm text-red-500 transition duration-300 ease-in-out">
{localize('com_ui_field_required')}
</span>
)}
</>
)}
/>
</div>
<div className="mb-4">
<Controller
name="model_parameters.temperature"
control={control}
rules={{ required: false }}
render={({ field }) => (
<>
<ModelParameters
label="com_endpoint_temperature"
ariaLabel="Temperature"
min={-2}
max={2}
step={0.01}
stepClick={0.01}
initialValue={field.value ?? 1}
onChange={field.onChange}
showButtons={true}
disabled={!provider}
/>
</>
)}
/>
</div>
<div className="mb-4">
<Controller
name="model_parameters.max_context_tokens"
control={control}
rules={{ required: false }}
render={({ field }) => (
<>
<ModelParameters
label="com_endpoint_max_output_tokens"
ariaLabel="Max Context Tokens"
min={0}
max={4096}
step={1}
stepClick={1}
initialValue={field.value ?? 0}
onChange={field.onChange}
showButtons={true}
disabled={!provider}
/>
</>
)}
/>
</div>
<div className="mb-4">
<Controller
name="model_parameters.max_output_tokens"
control={control}
rules={{ required: false }}
render={({ field }) => (
<>
<ModelParameters
label="com_endpoint_context_tokens"
ariaLabel="Max Context Tokens"
min={0}
max={4096}
step={1}
stepClick={1}
initialValue={field.value ?? 0}
onChange={field.onChange}
showButtons={true}
disabled={!provider}
/>
</>
)}
/>
</div>
<div className="mb-4">
<Controller
name="model_parameters.top_p"
control={control}
rules={{ required: false }}
render={({ field }) => (
<>
<ModelParameters
label="com_endpoint_top_p"
ariaLabel="Top P"
min={-2}
max={2}
step={0.01}
stepClick={0.01}
initialValue={field.value ?? 1}
onChange={field.onChange}
showButtons={true}
disabled={!provider}
/>
</>
)}
/>
</div>
<div className="mb-4">
<Controller
name="model_parameters.frequency_penalty"
control={control}
rules={{ required: false }}
render={({ field }) => (
<>
<ModelParameters
label="com_endpoint_frequency_penalty"
ariaLabel="Frequency Penalty"
min={-2}
max={2}
step={0.01}
stepClick={0.01}
initialValue={field.value ?? 0}
onChange={field.onChange}
showButtons={true}
disabled={!provider}
/>
</>
)}
/>
</div>
<div className="mb-4">
<Controller
name="model_parameters.presence_penalty"
control={control}
rules={{ required: false }}
render={({ field }) => (
<>
<ModelParameters
label="com_endpoint_presence_penalty"
ariaLabel="Presence Penalty"
min={-2}
max={2}
step={0.01}
stepClick={0.01}
initialValue={field.value ?? 0}
onChange={field.onChange}
showButtons={true}
disabled={!provider}
/>
</>
)}
/>
</>
)}
/>
</div>
</div>
{/* Model Parameters */}
{parameters && (
<div className="h-auto max-w-full overflow-x-hidden p-2">
<div className="grid grid-cols-4 gap-6">
{' '}
{/* This is the parent element containing all settings */}
{/* Below is an example of an applied dynamic setting, each be contained by a div with the column span specified */}
{parameters.map((setting) => {
const Component = componentMapping[setting.component];
if (!Component) {
return null;
}
const { key, default: defaultValue, ...rest } = setting;
if (key === 'region' && bedrockRegions.length) {
rest.options = bedrockRegions;
}
return (
<Component
key={key}
settingKey={key}
defaultValue={defaultValue}
{...rest}
setOption={setOption as t.TSetOption}
conversation={modelParameters as Partial<t.TConversation>}
/>
);
})}
</div>
</div>
)}
</div>
);
}

View file

@ -19,16 +19,19 @@ import { useLocalize } from '~/hooks';
type FormValues = {
[Permissions.SHARED_GLOBAL]: boolean;
[Permissions.UPDATE]: boolean;
};
export default function ShareAgent({
agent_id = '',
agentName,
projectIds = [],
isCollaborative = false,
}: {
agent_id?: string;
agentName?: string;
projectIds?: string[];
isCollaborative?: boolean;
}) {
const localize = useLocalize();
const { showToast } = useToastContext();
@ -40,6 +43,7 @@ export default function ShareAgent({
);
const {
watch,
control,
setValue,
getValues,
@ -49,12 +53,22 @@ export default function ShareAgent({
mode: 'onChange',
defaultValues: {
[Permissions.SHARED_GLOBAL]: agentIsGlobal,
[Permissions.UPDATE]: isCollaborative,
},
});
const sharedGlobalValue = watch(Permissions.SHARED_GLOBAL);
useEffect(() => {
if (!sharedGlobalValue) {
setValue(Permissions.UPDATE, false);
}
}, [sharedGlobalValue, setValue]);
useEffect(() => {
setValue(Permissions.SHARED_GLOBAL, agentIsGlobal);
}, [agentIsGlobal, setValue]);
setValue(Permissions.UPDATE, isCollaborative);
}, [agentIsGlobal, isCollaborative, setValue]);
const updateAgent = useUpdateAgentMutation({
onSuccess: (data) => {
@ -87,16 +101,30 @@ export default function ShareAgent({
const payload = {} as AgentUpdateParams;
if (data[Permissions.SHARED_GLOBAL]) {
payload.projectIds = [startupConfig.instanceProjectId];
} else {
payload.removeProjectIds = [startupConfig.instanceProjectId];
if (data[Permissions.UPDATE] !== isCollaborative) {
payload.isCollaborative = data[Permissions.UPDATE];
}
updateAgent.mutate({
agent_id,
data: payload,
});
if (data[Permissions.SHARED_GLOBAL] !== agentIsGlobal) {
if (data[Permissions.SHARED_GLOBAL]) {
payload.projectIds = [startupConfig.instanceProjectId];
} else {
payload.removeProjectIds = [startupConfig.instanceProjectId];
payload.isCollaborative = false;
}
}
if (Object.keys(payload).length > 0) {
updateAgent.mutate({
agent_id,
data: payload,
});
} else {
showToast({
message: localize('com_ui_no_changes'),
status: 'info',
});
}
};
return (
@ -113,12 +141,12 @@ export default function ShareAgent({
)}
type="button"
>
<div className="flex w-full items-center justify-center gap-2 text-blue-500">
<div className="flex items-center justify-center gap-2 text-blue-500">
<Share2Icon className="icon-md h-4 w-4" />
</div>
</button>
</OGDialogTrigger>
<OGDialogContent className="border-border-light bg-surface-primary-alt text-text-secondary">
<OGDialogContent className="w-1/4 border-border-light bg-surface-primary-alt text-text-secondary">
<OGDialogTitle>
{localize(
'com_ui_share_var',
@ -133,11 +161,12 @@ export default function ShareAgent({
handleSubmit(onSubmit)(e);
}}
>
<div className="mb-4 flex items-center justify-between gap-2 py-4">
<div className="flex items-center justify-between gap-2 py-2">
<div className="flex items-center">
<button
type="button"
className="mr-2 cursor-pointer"
disabled={isFetching || updateAgent.isLoading || !instanceProjectId}
onClick={() =>
setValue(Permissions.SHARED_GLOBAL, !getValues(Permissions.SHARED_GLOBAL), {
shouldDirty: true,
@ -166,18 +195,54 @@ export default function ShareAgent({
name={Permissions.SHARED_GLOBAL}
control={control}
disabled={isFetching || updateAgent.isLoading || !instanceProjectId}
rules={{
validate: (value) => {
const isValid = !(value && agentIsGlobal);
if (!isValid) {
showToast({
message: localize('com_ui_agent_already_shared_to_all'),
status: 'warning',
render={({ field }) => (
<Switch
{...field}
checked={field.value}
onCheckedChange={field.onChange}
value={field.value.toString()}
/>
)}
/>
</div>
<div className="mb-4 flex items-center justify-between gap-2 py-2">
<div className="flex items-center">
<button
type="button"
className="mr-2 cursor-pointer"
disabled={
isFetching || updateAgent.isLoading || !instanceProjectId || !sharedGlobalValue
}
onClick={() =>
setValue(Permissions.UPDATE, !getValues(Permissions.UPDATE), {
shouldDirty: true,
})
}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setValue(Permissions.UPDATE, !getValues(Permissions.UPDATE), {
shouldDirty: true,
});
}
return isValid;
},
}}
}}
aria-checked={getValues(Permissions.UPDATE)}
role="checkbox"
>
{localize('com_agents_allow_editing')}
</button>
{/* <label htmlFor={Permissions.UPDATE} className="select-none">
{agentIsGlobal && (
<span className="ml-2 text-xs">{localize('com_ui_agent_editing_allowed')}</span>
)}
</label> */}
</div>
<Controller
name={Permissions.UPDATE}
control={control}
disabled={
isFetching || updateAgent.isLoading || !instanceProjectId || !sharedGlobalValue
}
render={({ field }) => (
<Switch
{...field}

View file

@ -389,7 +389,7 @@ export default function AssistantPanel({
{/* Tools */}
<div className="mb-6">
<label className={labelClass}>
{`${toolsEnabled === true ? localize('com_assistants_tools') : ''}
{`${toolsEnabled === true ? localize('com_ui_tools') : ''}
${toolsEnabled === true && actionsEnabled === true ? ' + ' : ''}
${actionsEnabled === true ? localize('com_assistants_actions') : ''}`}
</label>

View file

@ -2,10 +2,9 @@ import { useState } from 'react';
import * as AccordionPrimitive from '@radix-ui/react-accordion';
import type { NavLink, NavProps } from '~/common';
import { Accordion, AccordionItem, AccordionContent } from '~/components/ui/Accordion';
import { buttonVariants } from '~/components/ui/Button';
import { TooltipAnchor, Button } from '~/components';
import { cn, removeFocusOutlines } from '~/utils';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
export default function Nav({ links, isCollapsed, resize, defaultActive }: NavProps) {
const localize = useLocalize();
@ -20,7 +19,7 @@ export default function Nav({ links, isCollapsed, resize, defaultActive }: NavPr
return (
<div
data-collapsed={isCollapsed}
className="bg-token-sidebar-surface-primary hide-scrollbar group flex-shrink-0 overflow-x-hidden py-2 data-[collapsed=true]:py-2"
className="bg-token-sidebar-surface-primary hide-scrollbar group flex-shrink-0 overflow-x-hidden"
>
<div className="h-full">
<div className="flex h-full min-h-0 flex-col">
@ -76,12 +75,11 @@ export default function Nav({ links, isCollapsed, resize, defaultActive }: NavPr
>
<link.icon className="mr-2 h-4 w-4" />
{localize(link.title)}
{link.label && (
{link.label != null && link.label && (
<span
className={cn(
'ml-auto transition-all duration-300 ease-in-out',
'ml-auto opacity-100 transition-all duration-300 ease-in-out',
variant === 'default' ? 'text-background dark:text-white' : '',
isCollapsed ? 'opacity-0' : 'opacity-100',
)}
>
{link.label}

View file

@ -572,3 +572,12 @@ export const presetSettings: Record<
[`${EModelEndpoint.bedrock}-${BedrockProviders.AI21}`]: bedrockGeneralColumns,
[`${EModelEndpoint.bedrock}-${BedrockProviders.Amazon}`]: bedrockGeneralColumns,
};
export const agentSettings: Record<string, SettingsConfiguration | undefined> = Object.entries(
presetSettings,
).reduce((acc, [key, value]) => {
if (value) {
acc[key] = value.col2;
}
return acc;
}, {});

View file

@ -1,33 +1,17 @@
import { isAssistantsEndpoint, isAgentsEndpoint } from 'librechat-data-provider';
import type { SwitcherProps } from '~/common';
import { Separator } from '~/components/ui/Separator';
import AssistantSwitcher from './AssistantSwitcher';
import AgentSwitcher from './AgentSwitcher';
import ModelSwitcher from './ModelSwitcher';
export default function Switcher(props: SwitcherProps) {
if (isAssistantsEndpoint(props.endpoint) && props.endpointKeyProvided) {
return (
<>
<AssistantSwitcher {...props} />
<Separator className="max-w-[98%] bg-surface-tertiary" />
</>
);
return <AssistantSwitcher {...props} />;
} else if (isAgentsEndpoint(props.endpoint) && props.endpointKeyProvided) {
return (
<>
<AgentSwitcher {...props} />
<Separator className="bg-gray-100/50 dark:bg-gray-600" />
</>
);
return <AgentSwitcher {...props} />;
} else if (isAssistantsEndpoint(props.endpoint)) {
return null;
}
return (
<>
<ModelSwitcher {...props} />
<Separator className="max-w-[98%] bg-surface-tertiary" />
</>
);
return <ModelSwitcher {...props} />;
}

View file

@ -2,12 +2,13 @@ import { useEffect } from 'react';
import { Search, X } from 'lucide-react';
import { Dialog, DialogPanel, DialogTitle, Description } from '@headlessui/react';
import { useFormContext } from 'react-hook-form';
import { isAgentsEndpoint } from 'librechat-data-provider';
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
import type {
AssistantsEndpoint,
EModelEndpoint,
TError,
TPluginAction,
TError,
} from 'librechat-data-provider';
import type { TPluginStoreDialogProps } from '~/common/types';
import { PluginPagination, PluginAuthForm } from '~/components/Plugins/Store';
@ -26,7 +27,8 @@ function ToolSelectDialog({
}) {
const localize = useLocalize();
const { getValues, setValue } = useFormContext();
const { data: tools = [] } = useAvailableToolsQuery(endpoint);
const { data: tools } = useAvailableToolsQuery(endpoint);
const isAgentTools = isAgentsEndpoint(endpoint);
const {
maxPage,
@ -54,8 +56,9 @@ function ToolSelectDialog({
const updateUserPlugins = useUpdateUserPluginsMutation();
const handleInstallError = (error: TError) => {
setError(true);
if (error.response?.data?.message) {
setErrorMessage(error.response?.data?.message);
const errorMessage = error.response?.data?.message ?? '';
if (errorMessage) {
setErrorMessage(errorMessage);
}
setTimeout(() => {
setError(false);
@ -105,7 +108,7 @@ function ToolSelectDialog({
const getAvailablePluginFromKey = tools?.find((p) => p.pluginKey === pluginKey);
setSelectedPlugin(getAvailablePluginFromKey);
const { authConfig, authenticated } = getAvailablePluginFromKey ?? {};
const { authConfig, authenticated = false } = getAvailablePluginFromKey ?? {};
if (authConfig && authConfig.length > 0 && !authenticated) {
setShowPluginAuthForm(true);
@ -159,7 +162,9 @@ function ToolSelectDialog({
<div className="flex items-center">
<div className="text-center sm:text-left">
<DialogTitle className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-200">
{localize('com_nav_tool_dialog')}
{isAgentTools
? localize('com_nav_tool_dialog_agents')
: localize('com_nav_tool_dialog')}
</DialogTitle>
<Description className="text-sm text-gray-500 dark:text-gray-300">
{localize('com_nav_tool_dialog_description')}

View file

@ -1,10 +1,10 @@
import { cn } from '~/utils';
export default function Sparkles({ className = '' }) {
export default function Sparkles({ className = '', size = 24 }) {
return (
<svg
width="24"
height="24"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"