mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-18 09:20:15 +01:00
✨ feat: Agent Panel UI Enhancements (#7800)
* feat: add MCP Panel to Agent Builder - Add MCP server panel and configuration UI - Implement MCP input forms and tool lists - Add MCP icon and metadata support - Integrate MCP with agent configuration - Add localization support for MCP features - Refactor components for better reusability - Update types and add MCP-related mutations - Fix small issues with Actions and AgentSelect - Refactor AgentPanelSwitch and related components to use new AgentPanelContext to reduce prop drilling * chore: import order * chore: clean up import statements and unused var in ActionsPanel component * refactor: AgentPanelContext with actions query, remove unnecessary `actions` state - Added actions query using `useGetActionsQuery` to fetch actions based on the current agent ID. - Removed now unused `setActions` state and related logic from `AgentPanelContext` and `AgentPanelSwitch` components. - Updated `AgentPanelContextType` to reflect the removal of `setActions`. * chore: re-order import statements in AgentConfig component * chore: re-order import statements in ModelPanel component * chore: update ModelPanel props to consolidated props to avoid passing unnecessary props * chore: update import statements in Providers index file to include ToastProvider and AgentPanelContext exports * chore: clean up import statements in VersionPanel component * refactor: streamline AgentConfig and AgentPanel components - Consolidated props in AgentConfig to only include necessary fields. - Updated AgentPanel to remove unused state and props, enhancing clarity and maintainability. - Reorganized import statements for better structure and readability. * refactor: replace default agent form values with utility function - Updated AgentsProvider, AgentPanel, AgentSelect, and DeleteButton components to use getDefaultAgentFormValues utility function instead of directly importing defaultAgentFormValues. - Enhanced the initialization of agent forms by incorporating localStorage values for model and provider in the new utility function. * chore: comment out rendering MCPSection --------- Co-authored-by: Dustin Healy <54083382+dustinhealy@users.noreply.github.com>
This commit is contained in:
parent
5f2d1c5dc9
commit
4419e2c294
27 changed files with 1027 additions and 136 deletions
45
client/src/Providers/AgentPanelContext.tsx
Normal file
45
client/src/Providers/AgentPanelContext.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import React, { createContext, useContext, useState } from 'react';
|
||||||
|
import { Action, MCP, EModelEndpoint } from 'librechat-data-provider';
|
||||||
|
import type { AgentPanelContextType } from '~/common';
|
||||||
|
import { useGetActionsQuery } from '~/data-provider';
|
||||||
|
import { Panel } from '~/common';
|
||||||
|
|
||||||
|
const AgentPanelContext = createContext<AgentPanelContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export function useAgentPanelContext() {
|
||||||
|
const context = useContext(AgentPanelContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useAgentPanelContext must be used within an AgentPanelProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Houses relevant state for the Agent Form Panels (formerly 'commonProps') */
|
||||||
|
export function AgentPanelProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const [mcp, setMcp] = useState<MCP | undefined>(undefined);
|
||||||
|
const [mcps, setMcps] = useState<MCP[] | undefined>(undefined);
|
||||||
|
const [action, setAction] = useState<Action | undefined>(undefined);
|
||||||
|
const [activePanel, setActivePanel] = useState<Panel>(Panel.builder);
|
||||||
|
const [agent_id, setCurrentAgentId] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
const { data: actions } = useGetActionsQuery(EModelEndpoint.agents, {
|
||||||
|
enabled: !!agent_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
action,
|
||||||
|
setAction,
|
||||||
|
mcp,
|
||||||
|
setMcp,
|
||||||
|
mcps,
|
||||||
|
setMcps,
|
||||||
|
activePanel,
|
||||||
|
setActivePanel,
|
||||||
|
setCurrentAgentId,
|
||||||
|
agent_id,
|
||||||
|
/** Query data for actions */
|
||||||
|
actions,
|
||||||
|
};
|
||||||
|
|
||||||
|
return <AgentPanelContext.Provider value={value}>{children}</AgentPanelContext.Provider>;
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { useForm, FormProvider } from 'react-hook-form';
|
import { useForm, FormProvider } from 'react-hook-form';
|
||||||
import { createContext, useContext } from 'react';
|
import { createContext, useContext } from 'react';
|
||||||
import { defaultAgentFormValues } from 'librechat-data-provider';
|
|
||||||
import type { UseFormReturn } from 'react-hook-form';
|
import type { UseFormReturn } from 'react-hook-form';
|
||||||
import type { AgentForm } from '~/common';
|
import type { AgentForm } from '~/common';
|
||||||
|
import { getDefaultAgentFormValues } from '~/utils';
|
||||||
|
|
||||||
type AgentsContextType = UseFormReturn<AgentForm>;
|
type AgentsContextType = UseFormReturn<AgentForm>;
|
||||||
|
|
||||||
|
|
@ -20,7 +20,7 @@ export function useAgentsContext() {
|
||||||
|
|
||||||
export default function AgentsProvider({ children }) {
|
export default function AgentsProvider({ children }) {
|
||||||
const methods = useForm<AgentForm>({
|
const methods = useForm<AgentForm>({
|
||||||
defaultValues: defaultAgentFormValues,
|
defaultValues: getDefaultAgentFormValues(),
|
||||||
});
|
});
|
||||||
|
|
||||||
return <FormProvider {...methods}>{children}</FormProvider>;
|
return <FormProvider {...methods}>{children}</FormProvider>;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
export { default as ToastProvider } from './ToastContext';
|
|
||||||
export { default as AssistantsProvider } from './AssistantsContext';
|
export { default as AssistantsProvider } from './AssistantsContext';
|
||||||
export { default as AgentsProvider } from './AgentsContext';
|
export { default as AgentsProvider } from './AgentsContext';
|
||||||
|
export { default as ToastProvider } from './ToastContext';
|
||||||
|
export * from './AgentPanelContext';
|
||||||
export * from './ChatContext';
|
export * from './ChatContext';
|
||||||
export * from './ShareContext';
|
export * from './ShareContext';
|
||||||
export * from './ToastContext';
|
export * from './ToastContext';
|
||||||
|
|
|
||||||
26
client/src/common/mcp.ts
Normal file
26
client/src/common/mcp.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import {
|
||||||
|
AuthorizationTypeEnum,
|
||||||
|
AuthTypeEnum,
|
||||||
|
TokenExchangeMethodEnum,
|
||||||
|
} from 'librechat-data-provider';
|
||||||
|
import { MCPForm } from '~/common/types';
|
||||||
|
|
||||||
|
export const defaultMCPFormValues: MCPForm = {
|
||||||
|
type: AuthTypeEnum.None,
|
||||||
|
saved_auth_fields: false,
|
||||||
|
api_key: '',
|
||||||
|
authorization_type: AuthorizationTypeEnum.Basic,
|
||||||
|
custom_auth_header: '',
|
||||||
|
oauth_client_id: '',
|
||||||
|
oauth_client_secret: '',
|
||||||
|
authorization_url: '',
|
||||||
|
client_url: '',
|
||||||
|
scope: '',
|
||||||
|
token_exchange_method: TokenExchangeMethodEnum.DefaultPost,
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
url: '',
|
||||||
|
tools: [],
|
||||||
|
icon: '',
|
||||||
|
trust: false,
|
||||||
|
};
|
||||||
|
|
@ -143,6 +143,7 @@ export enum Panel {
|
||||||
actions = 'actions',
|
actions = 'actions',
|
||||||
model = 'model',
|
model = 'model',
|
||||||
version = 'version',
|
version = 'version',
|
||||||
|
mcp = 'mcp',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FileSetter =
|
export type FileSetter =
|
||||||
|
|
@ -166,6 +167,15 @@ export type ActionAuthForm = {
|
||||||
token_exchange_method: t.TokenExchangeMethodEnum;
|
token_exchange_method: t.TokenExchangeMethodEnum;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MCPForm = ActionAuthForm & {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
url?: string;
|
||||||
|
tools?: string[];
|
||||||
|
icon?: string;
|
||||||
|
trust?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type ActionWithNullableMetadata = Omit<t.Action, 'metadata'> & {
|
export type ActionWithNullableMetadata = Omit<t.Action, 'metadata'> & {
|
||||||
metadata: t.ActionMetadata | null;
|
metadata: t.ActionMetadata | null;
|
||||||
};
|
};
|
||||||
|
|
@ -188,16 +198,33 @@ export type AgentPanelProps = {
|
||||||
index?: number;
|
index?: number;
|
||||||
agent_id?: string;
|
agent_id?: string;
|
||||||
activePanel?: string;
|
activePanel?: string;
|
||||||
|
mcp?: t.MCP;
|
||||||
|
mcps?: t.MCP[];
|
||||||
action?: t.Action;
|
action?: t.Action;
|
||||||
actions?: t.Action[];
|
actions?: t.Action[];
|
||||||
createMutation: UseMutationResult<t.Agent, Error, t.AgentCreateParams>;
|
createMutation: UseMutationResult<t.Agent, Error, t.AgentCreateParams>;
|
||||||
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
|
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
|
||||||
|
setMcp: React.Dispatch<React.SetStateAction<t.MCP | undefined>>;
|
||||||
setAction: React.Dispatch<React.SetStateAction<t.Action | undefined>>;
|
setAction: React.Dispatch<React.SetStateAction<t.Action | undefined>>;
|
||||||
endpointsConfig?: t.TEndpointsConfig;
|
endpointsConfig?: t.TEndpointsConfig;
|
||||||
setCurrentAgentId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
setCurrentAgentId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||||
agentsConfig?: t.TAgentsEndpoint | null;
|
agentsConfig?: t.TAgentsEndpoint | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AgentPanelContextType = {
|
||||||
|
action?: t.Action;
|
||||||
|
actions?: t.Action[];
|
||||||
|
setAction: React.Dispatch<React.SetStateAction<t.Action | undefined>>;
|
||||||
|
mcp?: t.MCP;
|
||||||
|
mcps?: t.MCP[];
|
||||||
|
setMcp: React.Dispatch<React.SetStateAction<t.MCP | undefined>>;
|
||||||
|
setMcps: React.Dispatch<React.SetStateAction<t.MCP[] | undefined>>;
|
||||||
|
activePanel?: string;
|
||||||
|
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
|
||||||
|
setCurrentAgentId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||||
|
agent_id?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type AgentModelPanelProps = {
|
export type AgentModelPanelProps = {
|
||||||
agent_id?: string;
|
agent_id?: string;
|
||||||
providers: Option[];
|
providers: Option[];
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,27 @@
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
import { ChevronLeft } from 'lucide-react';
|
||||||
import { useForm, FormProvider } from 'react-hook-form';
|
import { useForm, FormProvider } from 'react-hook-form';
|
||||||
import {
|
import {
|
||||||
AuthTypeEnum,
|
AuthTypeEnum,
|
||||||
AuthorizationTypeEnum,
|
AuthorizationTypeEnum,
|
||||||
TokenExchangeMethodEnum,
|
TokenExchangeMethodEnum,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import { ChevronLeft } from 'lucide-react';
|
|
||||||
import type { AgentPanelProps, ActionAuthForm } from '~/common';
|
|
||||||
import ActionsAuth from '~/components/SidePanel/Builder/ActionsAuth';
|
import ActionsAuth from '~/components/SidePanel/Builder/ActionsAuth';
|
||||||
|
import { useAgentPanelContext } from '~/Providers/AgentPanelContext';
|
||||||
import { OGDialog, OGDialogTrigger, Label } from '~/components/ui';
|
import { OGDialog, OGDialogTrigger, Label } from '~/components/ui';
|
||||||
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||||
import { useDeleteAgentAction } from '~/data-provider';
|
import { useDeleteAgentAction } from '~/data-provider';
|
||||||
|
import type { ActionAuthForm } from '~/common';
|
||||||
import useLocalize from '~/hooks/useLocalize';
|
import useLocalize from '~/hooks/useLocalize';
|
||||||
import { useToastContext } from '~/Providers';
|
import { useToastContext } from '~/Providers';
|
||||||
import { TrashIcon } from '~/components/svg';
|
import { TrashIcon } from '~/components/svg';
|
||||||
import ActionsInput from './ActionsInput';
|
import ActionsInput from './ActionsInput';
|
||||||
import { Panel } from '~/common';
|
import { Panel } from '~/common';
|
||||||
|
|
||||||
export default function ActionsPanel({
|
export default function ActionsPanel() {
|
||||||
// activePanel,
|
|
||||||
action,
|
|
||||||
setAction,
|
|
||||||
agent_id,
|
|
||||||
setActivePanel,
|
|
||||||
}: AgentPanelProps) {
|
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { showToast } = useToastContext();
|
const { showToast } = useToastContext();
|
||||||
|
const { setActivePanel, action, setAction, agent_id } = useAgentPanelContext();
|
||||||
const deleteAgentAction = useDeleteAgentAction({
|
const deleteAgentAction = useDeleteAgentAction({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
showToast({
|
showToast({
|
||||||
|
|
@ -62,7 +58,7 @@ export default function ActionsPanel({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { reset, watch } = methods;
|
const { reset } = methods;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (action?.metadata.auth) {
|
if (action?.metadata.auth) {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import React, { useState, useMemo, useCallback } from 'react';
|
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import React, { useState, useMemo, useCallback } from 'react';
|
||||||
import { Controller, useWatch, useFormContext } from 'react-hook-form';
|
import { Controller, useWatch, useFormContext } from 'react-hook-form';
|
||||||
import { QueryKeys, EModelEndpoint, AgentCapabilities } from 'librechat-data-provider';
|
import { QueryKeys, EModelEndpoint, AgentCapabilities } from 'librechat-data-provider';
|
||||||
import type { TPlugin } from 'librechat-data-provider';
|
import type { TPlugin } from 'librechat-data-provider';
|
||||||
import type { AgentForm, AgentPanelProps, IconComponentTypes } from '~/common';
|
|
||||||
import { cn, defaultTextProps, removeFocusOutlines, getEndpointField, getIconKey } from '~/utils';
|
import { cn, defaultTextProps, removeFocusOutlines, getEndpointField, getIconKey } from '~/utils';
|
||||||
import { useToastContext, useFileMapContext } from '~/Providers';
|
import { useToastContext, useFileMapContext, useAgentPanelContext } from '~/Providers';
|
||||||
|
import type { AgentForm, AgentPanelProps, IconComponentTypes } from '~/common';
|
||||||
import Action from '~/components/SidePanel/Builder/Action';
|
import Action from '~/components/SidePanel/Builder/Action';
|
||||||
import { ToolSelectDialog } from '~/components/Tools';
|
import { ToolSelectDialog } from '~/components/Tools';
|
||||||
import { icons } from '~/hooks/Endpoint/Icons';
|
import { icons } from '~/hooks/Endpoint/Icons';
|
||||||
|
|
@ -15,6 +15,7 @@ import AgentAvatar from './AgentAvatar';
|
||||||
import FileContext from './FileContext';
|
import FileContext from './FileContext';
|
||||||
import SearchForm from './Search/Form';
|
import SearchForm from './Search/Form';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
|
import MCPSection from './MCPSection';
|
||||||
import FileSearch from './FileSearch';
|
import FileSearch from './FileSearch';
|
||||||
import Artifacts from './Artifacts';
|
import Artifacts from './Artifacts';
|
||||||
import AgentTool from './AgentTool';
|
import AgentTool from './AgentTool';
|
||||||
|
|
@ -29,23 +30,19 @@ const inputClass = cn(
|
||||||
);
|
);
|
||||||
|
|
||||||
export default function AgentConfig({
|
export default function AgentConfig({
|
||||||
setAction,
|
|
||||||
actions = [],
|
|
||||||
agentsConfig,
|
agentsConfig,
|
||||||
createMutation,
|
createMutation,
|
||||||
setActivePanel,
|
|
||||||
endpointsConfig,
|
endpointsConfig,
|
||||||
}: AgentPanelProps) {
|
}: Pick<AgentPanelProps, 'agentsConfig' | 'createMutation' | 'endpointsConfig'>) {
|
||||||
|
const localize = useLocalize();
|
||||||
const fileMap = useFileMapContext();
|
const fileMap = useFileMapContext();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { showToast } = useToastContext();
|
||||||
|
const methods = useFormContext<AgentForm>();
|
||||||
|
const [showToolDialog, setShowToolDialog] = useState(false);
|
||||||
|
const { actions, setAction, setActivePanel } = useAgentPanelContext();
|
||||||
|
|
||||||
const allTools = queryClient.getQueryData<TPlugin[]>([QueryKeys.tools]) ?? [];
|
const allTools = queryClient.getQueryData<TPlugin[]>([QueryKeys.tools]) ?? [];
|
||||||
const { showToast } = useToastContext();
|
|
||||||
const localize = useLocalize();
|
|
||||||
|
|
||||||
const [showToolDialog, setShowToolDialog] = useState(false);
|
|
||||||
|
|
||||||
const methods = useFormContext<AgentForm>();
|
|
||||||
|
|
||||||
const { control } = methods;
|
const { control } = methods;
|
||||||
const provider = useWatch({ control, name: 'provider' });
|
const provider = useWatch({ control, name: 'provider' });
|
||||||
|
|
@ -299,7 +296,7 @@ export default function AgentConfig({
|
||||||
agent_id={agent_id}
|
agent_id={agent_id}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{actions
|
{(actions ?? [])
|
||||||
.filter((action) => action.agent_id === agent_id)
|
.filter((action) => action.agent_id === agent_id)
|
||||||
.map((action, i) => (
|
.map((action, i) => (
|
||||||
<Action
|
<Action
|
||||||
|
|
@ -340,6 +337,8 @@ export default function AgentConfig({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* MCP Section */}
|
||||||
|
{/* <MCPSection /> */}
|
||||||
</div>
|
</div>
|
||||||
<ToolSelectDialog
|
<ToolSelectDialog
|
||||||
isOpen={showToolDialog}
|
isOpen={showToolDialog}
|
||||||
|
|
|
||||||
|
|
@ -7,20 +7,22 @@ import {
|
||||||
Constants,
|
Constants,
|
||||||
SystemRoles,
|
SystemRoles,
|
||||||
EModelEndpoint,
|
EModelEndpoint,
|
||||||
|
TAgentsEndpoint,
|
||||||
|
TEndpointsConfig,
|
||||||
isAssistantsEndpoint,
|
isAssistantsEndpoint,
|
||||||
defaultAgentFormValues,
|
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import type { AgentForm, AgentPanelProps, StringOption } from '~/common';
|
import type { AgentForm, StringOption } from '~/common';
|
||||||
import {
|
import {
|
||||||
useCreateAgentMutation,
|
useCreateAgentMutation,
|
||||||
useUpdateAgentMutation,
|
useUpdateAgentMutation,
|
||||||
useGetAgentByIdQuery,
|
useGetAgentByIdQuery,
|
||||||
} from '~/data-provider';
|
} from '~/data-provider';
|
||||||
|
import { createProviderOption, getDefaultAgentFormValues } from '~/utils';
|
||||||
import { useSelectAgent, useLocalize, useAuthContext } from '~/hooks';
|
import { useSelectAgent, useLocalize, useAuthContext } from '~/hooks';
|
||||||
|
import { useAgentPanelContext } from '~/Providers/AgentPanelContext';
|
||||||
import AgentPanelSkeleton from './AgentPanelSkeleton';
|
import AgentPanelSkeleton from './AgentPanelSkeleton';
|
||||||
import { createProviderOption } from '~/utils';
|
|
||||||
import { useToastContext } from '~/Providers';
|
|
||||||
import AdvancedPanel from './Advanced/AdvancedPanel';
|
import AdvancedPanel from './Advanced/AdvancedPanel';
|
||||||
|
import { useToastContext } from '~/Providers';
|
||||||
import AgentConfig from './AgentConfig';
|
import AgentConfig from './AgentConfig';
|
||||||
import AgentSelect from './AgentSelect';
|
import AgentSelect from './AgentSelect';
|
||||||
import AgentFooter from './AgentFooter';
|
import AgentFooter from './AgentFooter';
|
||||||
|
|
@ -29,18 +31,21 @@ import ModelPanel from './ModelPanel';
|
||||||
import { Panel } from '~/common';
|
import { Panel } from '~/common';
|
||||||
|
|
||||||
export default function AgentPanel({
|
export default function AgentPanel({
|
||||||
setAction,
|
|
||||||
activePanel,
|
|
||||||
actions = [],
|
|
||||||
setActivePanel,
|
|
||||||
agent_id: current_agent_id,
|
|
||||||
setCurrentAgentId,
|
|
||||||
agentsConfig,
|
agentsConfig,
|
||||||
endpointsConfig,
|
endpointsConfig,
|
||||||
}: AgentPanelProps) {
|
}: {
|
||||||
|
agentsConfig: TAgentsEndpoint | null;
|
||||||
|
endpointsConfig: TEndpointsConfig;
|
||||||
|
}) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { user } = useAuthContext();
|
const { user } = useAuthContext();
|
||||||
const { showToast } = useToastContext();
|
const { showToast } = useToastContext();
|
||||||
|
const {
|
||||||
|
activePanel,
|
||||||
|
setActivePanel,
|
||||||
|
setCurrentAgentId,
|
||||||
|
agent_id: current_agent_id,
|
||||||
|
} = useAgentPanelContext();
|
||||||
|
|
||||||
const { onSelect: onSelectAgent } = useSelectAgent();
|
const { onSelect: onSelectAgent } = useSelectAgent();
|
||||||
|
|
||||||
|
|
@ -51,7 +56,7 @@ export default function AgentPanel({
|
||||||
|
|
||||||
const models = useMemo(() => modelsQuery.data ?? {}, [modelsQuery.data]);
|
const models = useMemo(() => modelsQuery.data ?? {}, [modelsQuery.data]);
|
||||||
const methods = useForm<AgentForm>({
|
const methods = useForm<AgentForm>({
|
||||||
defaultValues: defaultAgentFormValues,
|
defaultValues: getDefaultAgentFormValues(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { control, handleSubmit, reset } = methods;
|
const { control, handleSubmit, reset } = methods;
|
||||||
|
|
@ -277,7 +282,7 @@ export default function AgentPanel({
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full justify-center"
|
className="w-full justify-center"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
reset(defaultAgentFormValues);
|
reset(getDefaultAgentFormValues());
|
||||||
setCurrentAgentId(undefined);
|
setCurrentAgentId(undefined);
|
||||||
}}
|
}}
|
||||||
disabled={agentQuery.isInitialLoading}
|
disabled={agentQuery.isInitialLoading}
|
||||||
|
|
@ -315,22 +320,13 @@ export default function AgentPanel({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{canEditAgent && !agentQuery.isInitialLoading && activePanel === Panel.model && (
|
{canEditAgent && !agentQuery.isInitialLoading && activePanel === Panel.model && (
|
||||||
<ModelPanel
|
<ModelPanel models={models} providers={providers} setActivePanel={setActivePanel} />
|
||||||
setActivePanel={setActivePanel}
|
|
||||||
agent_id={agent_id}
|
|
||||||
providers={providers}
|
|
||||||
models={models}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{canEditAgent && !agentQuery.isInitialLoading && activePanel === Panel.builder && (
|
{canEditAgent && !agentQuery.isInitialLoading && activePanel === Panel.builder && (
|
||||||
<AgentConfig
|
<AgentConfig
|
||||||
actions={actions}
|
|
||||||
setAction={setAction}
|
|
||||||
createMutation={create}
|
createMutation={create}
|
||||||
agentsConfig={agentsConfig}
|
agentsConfig={agentsConfig}
|
||||||
setActivePanel={setActivePanel}
|
|
||||||
endpointsConfig={endpointsConfig}
|
endpointsConfig={endpointsConfig}
|
||||||
setCurrentAgentId={setCurrentAgentId}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{canEditAgent && !agentQuery.isInitialLoading && activePanel === Panel.advanced && (
|
{canEditAgent && !agentQuery.isInitialLoading && activePanel === Panel.advanced && (
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,29 @@
|
||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import { EModelEndpoint, AgentCapabilities } from 'librechat-data-provider';
|
import { EModelEndpoint, AgentCapabilities } from 'librechat-data-provider';
|
||||||
import type { ActionsEndpoint } from '~/common';
|
import type { TConfig, TEndpointsConfig, TAgentsEndpoint } from 'librechat-data-provider';
|
||||||
import type { Action, TConfig, TEndpointsConfig, TAgentsEndpoint } from 'librechat-data-provider';
|
import { AgentPanelProvider, useAgentPanelContext } from '~/Providers/AgentPanelContext';
|
||||||
import { useGetActionsQuery, useGetEndpointsQuery, useCreateAgentMutation } from '~/data-provider';
|
import { useGetEndpointsQuery } from '~/data-provider';
|
||||||
|
import VersionPanel from './Version/VersionPanel';
|
||||||
import { useChatContext } from '~/Providers';
|
import { useChatContext } from '~/Providers';
|
||||||
import ActionsPanel from './ActionsPanel';
|
import ActionsPanel from './ActionsPanel';
|
||||||
import AgentPanel from './AgentPanel';
|
import AgentPanel from './AgentPanel';
|
||||||
import VersionPanel from './Version/VersionPanel';
|
import MCPPanel from './MCPPanel';
|
||||||
import { Panel } from '~/common';
|
import { Panel } from '~/common';
|
||||||
|
|
||||||
export default function AgentPanelSwitch() {
|
export default function AgentPanelSwitch() {
|
||||||
const { conversation, index } = useChatContext();
|
return (
|
||||||
const [activePanel, setActivePanel] = useState(Panel.builder);
|
<AgentPanelProvider>
|
||||||
const [action, setAction] = useState<Action | undefined>(undefined);
|
<AgentPanelSwitchWithContext />
|
||||||
const [currentAgentId, setCurrentAgentId] = useState<string | undefined>(conversation?.agent_id);
|
</AgentPanelProvider>
|
||||||
const { data: actions = [] } = useGetActionsQuery(conversation?.endpoint as ActionsEndpoint);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AgentPanelSwitchWithContext() {
|
||||||
|
const { conversation } = useChatContext();
|
||||||
|
const { activePanel, setCurrentAgentId } = useAgentPanelContext();
|
||||||
|
|
||||||
|
// TODO: Implement MCP endpoint
|
||||||
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
|
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
|
||||||
const createMutation = useCreateAgentMutation();
|
|
||||||
|
|
||||||
const agentsConfig = useMemo<TAgentsEndpoint | null>(() => {
|
const agentsConfig = useMemo<TAgentsEndpoint | null>(() => {
|
||||||
const config = endpointsConfig?.[EModelEndpoint.agents] ?? null;
|
const config = endpointsConfig?.[EModelEndpoint.agents] ?? null;
|
||||||
|
|
@ -35,39 +42,20 @@ export default function AgentPanelSwitch() {
|
||||||
if (agent_id) {
|
if (agent_id) {
|
||||||
setCurrentAgentId(agent_id);
|
setCurrentAgentId(agent_id);
|
||||||
}
|
}
|
||||||
}, [conversation?.agent_id]);
|
}, [setCurrentAgentId, conversation?.agent_id]);
|
||||||
|
|
||||||
if (!conversation?.endpoint) {
|
if (!conversation?.endpoint) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const commonProps = {
|
|
||||||
index,
|
|
||||||
action,
|
|
||||||
actions,
|
|
||||||
setAction,
|
|
||||||
activePanel,
|
|
||||||
setActivePanel,
|
|
||||||
setCurrentAgentId,
|
|
||||||
agent_id: currentAgentId,
|
|
||||||
createMutation,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (activePanel === Panel.actions) {
|
if (activePanel === Panel.actions) {
|
||||||
return <ActionsPanel {...commonProps} />;
|
return <ActionsPanel />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activePanel === Panel.version) {
|
if (activePanel === Panel.version) {
|
||||||
return (
|
return <VersionPanel />;
|
||||||
<VersionPanel
|
|
||||||
setActivePanel={setActivePanel}
|
|
||||||
agentsConfig={agentsConfig}
|
|
||||||
selectedAgentId={currentAgentId}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
if (activePanel === Panel.mcp) {
|
||||||
return (
|
return <MCPPanel />;
|
||||||
<AgentPanel {...commonProps} agentsConfig={agentsConfig} endpointsConfig={endpointsConfig} />
|
}
|
||||||
);
|
return <AgentPanel agentsConfig={agentsConfig} endpointsConfig={endpointsConfig} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ import { AgentCapabilities, defaultAgentFormValues } from 'librechat-data-provid
|
||||||
import type { UseMutationResult, QueryObserverResult } from '@tanstack/react-query';
|
import type { UseMutationResult, QueryObserverResult } from '@tanstack/react-query';
|
||||||
import type { Agent, AgentCreateParams } from 'librechat-data-provider';
|
import type { Agent, AgentCreateParams } from 'librechat-data-provider';
|
||||||
import type { TAgentCapabilities, AgentForm } from '~/common';
|
import type { TAgentCapabilities, AgentForm } from '~/common';
|
||||||
|
import { cn, createProviderOption, processAgentOption, getDefaultAgentFormValues } from '~/utils';
|
||||||
import { useListAgentsQuery, useGetStartupConfig } from '~/data-provider';
|
import { useListAgentsQuery, useGetStartupConfig } from '~/data-provider';
|
||||||
import { cn, createProviderOption, processAgentOption } from '~/utils';
|
|
||||||
import ControlCombobox from '~/components/ui/ControlCombobox';
|
import ControlCombobox from '~/components/ui/ControlCombobox';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
|
|
@ -32,7 +32,10 @@ export default function AgentSelect({
|
||||||
select: (res) =>
|
select: (res) =>
|
||||||
res.data.map((agent) =>
|
res.data.map((agent) =>
|
||||||
processAgentOption({
|
processAgentOption({
|
||||||
agent,
|
agent: {
|
||||||
|
...agent,
|
||||||
|
name: agent.name || agent.id,
|
||||||
|
},
|
||||||
instanceProjectId: startupConfig?.instanceProjectId,
|
instanceProjectId: startupConfig?.instanceProjectId,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
@ -124,9 +127,7 @@ export default function AgentSelect({
|
||||||
createMutation.reset();
|
createMutation.reset();
|
||||||
if (!agentExists) {
|
if (!agentExists) {
|
||||||
setCurrentAgentId(undefined);
|
setCurrentAgentId(undefined);
|
||||||
return reset({
|
return reset(getDefaultAgentFormValues());
|
||||||
...defaultAgentFormValues,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentAgentId(selectedId);
|
setCurrentAgentId(selectedId);
|
||||||
|
|
@ -179,7 +180,7 @@ export default function AgentSelect({
|
||||||
containerClassName="px-0"
|
containerClassName="px-0"
|
||||||
selectedValue={(field?.value?.value ?? '') + ''}
|
selectedValue={(field?.value?.value ?? '') + ''}
|
||||||
displayValue={field?.value?.label ?? ''}
|
displayValue={field?.value?.label ?? ''}
|
||||||
selectPlaceholder={createAgent}
|
selectPlaceholder={field?.value?.value ?? createAgent}
|
||||||
iconSide="right"
|
iconSide="right"
|
||||||
searchPlaceholder={localize('com_agents_search_name')}
|
searchPlaceholder={localize('com_agents_search_name')}
|
||||||
SelectIcon={field?.value?.icon}
|
SelectIcon={field?.value?.icon}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
import { defaultAgentFormValues } from 'librechat-data-provider';
|
|
||||||
import type { Agent, AgentCreateParams } from 'librechat-data-provider';
|
import type { Agent, AgentCreateParams } from 'librechat-data-provider';
|
||||||
import type { UseMutationResult } from '@tanstack/react-query';
|
import type { UseMutationResult } from '@tanstack/react-query';
|
||||||
|
import { cn, logger, removeFocusOutlines, getDefaultAgentFormValues } from '~/utils';
|
||||||
import { OGDialog, OGDialogTrigger, Label } from '~/components/ui';
|
import { OGDialog, OGDialogTrigger, Label } from '~/components/ui';
|
||||||
import { useChatContext, useToastContext } from '~/Providers';
|
|
||||||
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||||
|
import { useChatContext, useToastContext } from '~/Providers';
|
||||||
import { useLocalize, useSetIndexOptions } from '~/hooks';
|
import { useLocalize, useSetIndexOptions } from '~/hooks';
|
||||||
import { cn, removeFocusOutlines, logger } from '~/utils';
|
|
||||||
import { useDeleteAgentMutation } from '~/data-provider';
|
import { useDeleteAgentMutation } from '~/data-provider';
|
||||||
import { TrashIcon } from '~/components/svg';
|
import { TrashIcon } from '~/components/svg';
|
||||||
|
|
||||||
|
|
@ -45,9 +44,7 @@ export default function DeleteButton({
|
||||||
const firstAgent = updatedList[0] as Agent | undefined;
|
const firstAgent = updatedList[0] as Agent | undefined;
|
||||||
if (!firstAgent) {
|
if (!firstAgent) {
|
||||||
setCurrentAgentId(undefined);
|
setCurrentAgentId(undefined);
|
||||||
reset({
|
reset(getDefaultAgentFormValues());
|
||||||
...defaultAgentFormValues,
|
|
||||||
});
|
|
||||||
return setOption('agent_id')('');
|
return setOption('agent_id')('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
64
client/src/components/SidePanel/Agents/MCPIcon.tsx
Normal file
64
client/src/components/SidePanel/Agents/MCPIcon.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
import SquirclePlusIcon from '~/components/svg/SquirclePlusIcon';
|
||||||
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
|
interface MCPIconProps {
|
||||||
|
icon?: string;
|
||||||
|
onIconChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MCPIcon({ icon, onIconChange }: MCPIconProps) {
|
||||||
|
const [previewUrl, setPreviewUrl] = useState('');
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const localize = useLocalize();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (icon) {
|
||||||
|
setPreviewUrl(icon);
|
||||||
|
} else {
|
||||||
|
setPreviewUrl('');
|
||||||
|
}
|
||||||
|
}, [icon]);
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = '';
|
||||||
|
fileInputRef.current.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div
|
||||||
|
onClick={handleClick}
|
||||||
|
className="bg-token-surface-secondary dark:bg-token-surface-tertiary border-token-border-medium flex h-16 w-16 shrink-0 cursor-pointer items-center justify-center rounded-[1.5rem] border-2 border-dashed"
|
||||||
|
>
|
||||||
|
{previewUrl ? (
|
||||||
|
<img
|
||||||
|
src={previewUrl}
|
||||||
|
className="h-full w-full rounded-[1.5rem] object-cover"
|
||||||
|
alt="MCP Icon"
|
||||||
|
width="64"
|
||||||
|
height="64"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<SquirclePlusIcon />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="token-text-secondary text-sm">
|
||||||
|
{localize('com_ui_icon')} {localize('com_ui_optional')}
|
||||||
|
</span>
|
||||||
|
<span className="token-text-tertiary text-xs">{localize('com_agents_mcp_icon_size')}</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
accept="image/png,.png,image/jpeg,.jpg,.jpeg,image/gif,.gif,image/webp,.webp"
|
||||||
|
multiple={false}
|
||||||
|
type="file"
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
onChange={onIconChange}
|
||||||
|
ref={fileInputRef}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
288
client/src/components/SidePanel/Agents/MCPInput.tsx
Normal file
288
client/src/components/SidePanel/Agents/MCPInput.tsx
Normal file
|
|
@ -0,0 +1,288 @@
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useFormContext, Controller } from 'react-hook-form';
|
||||||
|
import { MCP } from 'librechat-data-provider/dist/types/types/assistants';
|
||||||
|
import MCPAuth from '~/components/SidePanel/Builder/MCPAuth';
|
||||||
|
import MCPIcon from '~/components/SidePanel/Agents/MCPIcon';
|
||||||
|
import { Label, Checkbox } from '~/components/ui';
|
||||||
|
import useLocalize from '~/hooks/useLocalize';
|
||||||
|
import { useToastContext } from '~/Providers';
|
||||||
|
import { Spinner } from '~/components/svg';
|
||||||
|
import { MCPForm } from '~/common/types';
|
||||||
|
|
||||||
|
function useUpdateAgentMCP({
|
||||||
|
onSuccess,
|
||||||
|
onError,
|
||||||
|
}: {
|
||||||
|
onSuccess: (data: [string, MCP]) => void;
|
||||||
|
onError: (error: Error) => void;
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
mutate: async ({
|
||||||
|
mcp_id,
|
||||||
|
metadata,
|
||||||
|
agent_id,
|
||||||
|
}: {
|
||||||
|
mcp_id?: string;
|
||||||
|
metadata: MCP['metadata'];
|
||||||
|
agent_id: string;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
// TODO: Implement MCP endpoint
|
||||||
|
onSuccess(['success', { mcp_id, metadata, agent_id } as MCP]);
|
||||||
|
} catch (error) {
|
||||||
|
onError(error as Error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isLoading: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MCPInputProps {
|
||||||
|
mcp?: MCP;
|
||||||
|
agent_id?: string;
|
||||||
|
setMCP: React.Dispatch<React.SetStateAction<MCP | undefined>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MCPInput({ mcp, agent_id, setMCP }: MCPInputProps) {
|
||||||
|
const localize = useLocalize();
|
||||||
|
const { showToast } = useToastContext();
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
register,
|
||||||
|
formState: { errors },
|
||||||
|
control,
|
||||||
|
} = useFormContext<MCPForm>();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [showTools, setShowTools] = useState(false);
|
||||||
|
const [selectedTools, setSelectedTools] = useState<string[]>([]);
|
||||||
|
|
||||||
|
// Initialize tools list if editing existing MCP
|
||||||
|
useEffect(() => {
|
||||||
|
if (mcp?.mcp_id && mcp.metadata.tools) {
|
||||||
|
setShowTools(true);
|
||||||
|
setSelectedTools(mcp.metadata.tools);
|
||||||
|
}
|
||||||
|
}, [mcp]);
|
||||||
|
|
||||||
|
const updateAgentMCP = useUpdateAgentMCP({
|
||||||
|
onSuccess(data) {
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_update_mcp_success'),
|
||||||
|
status: 'success',
|
||||||
|
});
|
||||||
|
setMCP(data[1]);
|
||||||
|
setShowTools(true);
|
||||||
|
setSelectedTools(data[1].metadata.tools ?? []);
|
||||||
|
setIsLoading(false);
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
showToast({
|
||||||
|
message: (error as Error).message || localize('com_ui_update_mcp_error'),
|
||||||
|
status: 'error',
|
||||||
|
});
|
||||||
|
setIsLoading(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveMCP = handleSubmit(async (data: MCPForm) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await updateAgentMCP.mutate({
|
||||||
|
agent_id: agent_id ?? '',
|
||||||
|
mcp_id: mcp?.mcp_id,
|
||||||
|
metadata: {
|
||||||
|
...data,
|
||||||
|
tools: selectedTools,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setMCP(response[1]);
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_update_mcp_success'),
|
||||||
|
status: 'success',
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_update_mcp_error'),
|
||||||
|
status: 'error',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSelectAll = () => {
|
||||||
|
if (mcp?.metadata.tools) {
|
||||||
|
setSelectedTools(mcp.metadata.tools);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeselectAll = () => {
|
||||||
|
setSelectedTools([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToolToggle = (tool: string) => {
|
||||||
|
setSelectedTools((prev) =>
|
||||||
|
prev.includes(tool) ? prev.filter((t) => t !== tool) : [...prev, tool],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleAll = () => {
|
||||||
|
if (selectedTools.length === mcp?.metadata.tools?.length) {
|
||||||
|
handleDeselectAll();
|
||||||
|
} else {
|
||||||
|
handleSelectAll();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleIconChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onloadend = () => {
|
||||||
|
const base64String = reader.result as string;
|
||||||
|
setMCP({
|
||||||
|
mcp_id: mcp?.mcp_id ?? '',
|
||||||
|
agent_id: agent_id ?? '',
|
||||||
|
metadata: {
|
||||||
|
...mcp?.metadata,
|
||||||
|
icon: base64String,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{/* Icon Picker */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<MCPIcon icon={mcp?.metadata.icon} onIconChange={handleIconChange} />
|
||||||
|
</div>
|
||||||
|
{/* name, description, url */}
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="name">{localize('com_ui_name')}</Label>
|
||||||
|
<input
|
||||||
|
id="name"
|
||||||
|
{...register('name', { required: true })}
|
||||||
|
className="border-token-border-medium flex h-9 w-full rounded-lg border bg-transparent px-3 py-1.5 text-sm outline-none placeholder:text-text-secondary-alt focus:ring-1 focus:ring-border-light"
|
||||||
|
placeholder={localize('com_agents_mcp_name_placeholder')}
|
||||||
|
/>
|
||||||
|
{errors.name && (
|
||||||
|
<span className="text-xs text-red-500">{localize('com_ui_field_required')}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="description">
|
||||||
|
{localize('com_ui_description')}
|
||||||
|
<span className="ml-1 text-xs text-text-secondary-alt">
|
||||||
|
{localize('com_ui_optional')}
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
<input
|
||||||
|
id="description"
|
||||||
|
{...register('description')}
|
||||||
|
className="border-token-border-medium flex h-9 w-full rounded-lg border bg-transparent px-3 py-1.5 text-sm outline-none placeholder:text-text-secondary-alt focus:ring-1 focus:ring-border-light"
|
||||||
|
placeholder={localize('com_agents_mcp_description_placeholder')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="url">{localize('com_ui_mcp_url')}</Label>
|
||||||
|
<input
|
||||||
|
id="url"
|
||||||
|
{...register('url', {
|
||||||
|
required: true,
|
||||||
|
})}
|
||||||
|
className="border-token-border-medium flex h-9 w-full rounded-lg border bg-transparent px-3 py-1.5 text-sm outline-none placeholder:text-text-secondary-alt focus:ring-1 focus:ring-border-light"
|
||||||
|
placeholder={'https://mcp.example.com'}
|
||||||
|
/>
|
||||||
|
{errors.url && (
|
||||||
|
<span className="text-xs text-red-500">
|
||||||
|
{errors.url.type === 'required'
|
||||||
|
? localize('com_ui_field_required')
|
||||||
|
: errors.url.message}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<MCPAuth />
|
||||||
|
<div className="my-2 flex items-center gap-2">
|
||||||
|
<Controller
|
||||||
|
name="trust"
|
||||||
|
control={control}
|
||||||
|
rules={{ required: true }}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Checkbox id="trust" checked={field.value} onCheckedChange={field.onChange} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="trust" className="flex flex-col">
|
||||||
|
{localize('com_ui_trust_app')}
|
||||||
|
<span className="text-xs text-text-secondary">
|
||||||
|
{localize('com_agents_mcp_trust_subtext')}
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
{errors.trust && (
|
||||||
|
<span className="text-xs text-red-500">{localize('com_ui_field_required')}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
<button
|
||||||
|
onClick={saveMCP}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="focus:shadow-outline mt-1 flex min-w-[100px] items-center justify-center rounded bg-green-500 px-4 py-2 font-semibold text-white hover:bg-green-400 focus:border-green-500 focus:outline-none focus:ring-0 disabled:bg-green-400"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{(() => {
|
||||||
|
if (isLoading) {
|
||||||
|
return <Spinner className="icon-md" />;
|
||||||
|
}
|
||||||
|
return mcp?.mcp_id ? localize('com_ui_update') : localize('com_ui_create');
|
||||||
|
})()}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showTools && mcp?.metadata.tools && (
|
||||||
|
<div className="mt-4 flex flex-col gap-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-token-text-primary block font-medium">
|
||||||
|
{localize('com_ui_available_tools')}
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={handleToggleAll}
|
||||||
|
type="button"
|
||||||
|
className="btn btn-neutral border-token-border-light relative h-8 rounded-full px-4 font-medium"
|
||||||
|
>
|
||||||
|
{selectedTools.length === mcp.metadata.tools.length
|
||||||
|
? localize('com_ui_deselect_all')
|
||||||
|
: localize('com_ui_select_all')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{mcp.metadata.tools.map((tool) => (
|
||||||
|
<label
|
||||||
|
key={tool}
|
||||||
|
htmlFor={tool}
|
||||||
|
className="border-token-border-light hover:bg-token-surface-secondary flex cursor-pointer items-center rounded-lg border p-2"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
id={tool}
|
||||||
|
checked={selectedTools.includes(tool)}
|
||||||
|
onCheckedChange={() => handleToolToggle(tool)}
|
||||||
|
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
|
||||||
|
/>
|
||||||
|
<span className="text-token-text-primary">
|
||||||
|
{tool
|
||||||
|
.split('_')
|
||||||
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
|
.join(' ')}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
172
client/src/components/SidePanel/Agents/MCPPanel.tsx
Normal file
172
client/src/components/SidePanel/Agents/MCPPanel.tsx
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { ChevronLeft } from 'lucide-react';
|
||||||
|
import { useForm, FormProvider } from 'react-hook-form';
|
||||||
|
import { useAgentPanelContext } from '~/Providers/AgentPanelContext';
|
||||||
|
import { OGDialog, OGDialogTrigger, Label } from '~/components/ui';
|
||||||
|
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||||
|
import { defaultMCPFormValues } from '~/common/mcp';
|
||||||
|
import useLocalize from '~/hooks/useLocalize';
|
||||||
|
import { useToastContext } from '~/Providers';
|
||||||
|
import { TrashIcon } from '~/components/svg';
|
||||||
|
import type { MCPForm } from '~/common';
|
||||||
|
import MCPInput from './MCPInput';
|
||||||
|
import { Panel } from '~/common';
|
||||||
|
import {
|
||||||
|
AuthTypeEnum,
|
||||||
|
AuthorizationTypeEnum,
|
||||||
|
TokenExchangeMethodEnum,
|
||||||
|
} from 'librechat-data-provider';
|
||||||
|
// TODO: Add MCP delete (for now mocked for ui)
|
||||||
|
// import { useDeleteAgentMCP } from '~/data-provider';
|
||||||
|
|
||||||
|
function useDeleteAgentMCP({
|
||||||
|
onSuccess,
|
||||||
|
onError,
|
||||||
|
}: {
|
||||||
|
onSuccess: () => void;
|
||||||
|
onError: (error: Error) => void;
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
mutate: async ({ mcp_id, agent_id }: { mcp_id: string; agent_id: string }) => {
|
||||||
|
try {
|
||||||
|
console.log('Mock delete MCP:', { mcp_id, agent_id });
|
||||||
|
onSuccess();
|
||||||
|
} catch (error) {
|
||||||
|
onError(error as Error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MCPPanel() {
|
||||||
|
const localize = useLocalize();
|
||||||
|
const { showToast } = useToastContext();
|
||||||
|
const { mcp, setMcp, agent_id, setActivePanel } = useAgentPanelContext();
|
||||||
|
const deleteAgentMCP = useDeleteAgentMCP({
|
||||||
|
onSuccess: () => {
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_delete_mcp_success'),
|
||||||
|
status: 'success',
|
||||||
|
});
|
||||||
|
setActivePanel(Panel.builder);
|
||||||
|
setMcp(undefined);
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
showToast({
|
||||||
|
message: (error as Error).message ?? localize('com_ui_delete_mcp_error'),
|
||||||
|
status: 'error',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const methods = useForm<MCPForm>({
|
||||||
|
defaultValues: defaultMCPFormValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { reset } = methods;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (mcp) {
|
||||||
|
const formData = {
|
||||||
|
icon: mcp.metadata.icon ?? '',
|
||||||
|
name: mcp.metadata.name ?? '',
|
||||||
|
description: mcp.metadata.description ?? '',
|
||||||
|
url: mcp.metadata.url ?? '',
|
||||||
|
tools: mcp.metadata.tools ?? [],
|
||||||
|
trust: mcp.metadata.trust ?? false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (mcp.metadata.auth) {
|
||||||
|
Object.assign(formData, {
|
||||||
|
type: mcp.metadata.auth.type || AuthTypeEnum.None,
|
||||||
|
saved_auth_fields: false,
|
||||||
|
api_key: mcp.metadata.api_key ?? '',
|
||||||
|
authorization_type: mcp.metadata.auth.authorization_type || AuthorizationTypeEnum.Basic,
|
||||||
|
oauth_client_id: mcp.metadata.oauth_client_id ?? '',
|
||||||
|
oauth_client_secret: mcp.metadata.oauth_client_secret ?? '',
|
||||||
|
authorization_url: mcp.metadata.auth.authorization_url ?? '',
|
||||||
|
client_url: mcp.metadata.auth.client_url ?? '',
|
||||||
|
scope: mcp.metadata.auth.scope ?? '',
|
||||||
|
token_exchange_method:
|
||||||
|
mcp.metadata.auth.token_exchange_method ?? TokenExchangeMethodEnum.DefaultPost,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(formData);
|
||||||
|
}
|
||||||
|
}, [mcp, reset]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...methods}>
|
||||||
|
<form className="h-full grow overflow-hidden">
|
||||||
|
<div className="h-full overflow-auto px-2 pb-12 text-sm">
|
||||||
|
<div className="relative flex flex-col items-center px-16 py-6 text-center">
|
||||||
|
<div className="absolute left-0 top-6">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-neutral relative"
|
||||||
|
onClick={() => {
|
||||||
|
setActivePanel(Panel.builder);
|
||||||
|
setMcp(undefined);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex w-full items-center justify-center gap-2">
|
||||||
|
<ChevronLeft />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!!mcp && (
|
||||||
|
<OGDialog>
|
||||||
|
<OGDialogTrigger asChild>
|
||||||
|
<div className="absolute right-0 top-6">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={!agent_id || !mcp.mcp_id}
|
||||||
|
className="btn btn-neutral border-token-border-light relative h-9 rounded-lg font-medium"
|
||||||
|
>
|
||||||
|
<TrashIcon className="text-red-500" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</OGDialogTrigger>
|
||||||
|
<OGDialogTemplate
|
||||||
|
showCloseButton={false}
|
||||||
|
title={localize('com_ui_delete_mcp')}
|
||||||
|
className="max-w-[450px]"
|
||||||
|
main={
|
||||||
|
<Label className="text-left text-sm font-medium">
|
||||||
|
{localize('com_ui_delete_mcp_confirm')}
|
||||||
|
</Label>
|
||||||
|
}
|
||||||
|
selection={{
|
||||||
|
selectHandler: () => {
|
||||||
|
if (!agent_id) {
|
||||||
|
return showToast({
|
||||||
|
message: localize('com_agents_no_agent_id_error'),
|
||||||
|
status: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
deleteAgentMCP.mutate({
|
||||||
|
mcp_id: mcp.mcp_id,
|
||||||
|
agent_id,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
selectClasses:
|
||||||
|
'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 transition-color duration-200 text-white',
|
||||||
|
selectText: localize('com_ui_delete'),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</OGDialog>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="text-xl font-medium">
|
||||||
|
{mcp ? localize('com_ui_edit_mcp_server') : localize('com_ui_add_mcp_server')}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-text-secondary">{localize('com_agents_mcp_info')}</div>
|
||||||
|
</div>
|
||||||
|
<MCPInput mcp={mcp} agent_id={agent_id} setMCP={setMcp} />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
57
client/src/components/SidePanel/Agents/MCPSection.tsx
Normal file
57
client/src/components/SidePanel/Agents/MCPSection.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useLocalize } from '~/hooks';
|
||||||
|
import { useToastContext } from '~/Providers';
|
||||||
|
import { useAgentPanelContext } from '~/Providers/AgentPanelContext';
|
||||||
|
import MCP from '~/components/SidePanel/Builder/MCP';
|
||||||
|
import { Panel } from '~/common';
|
||||||
|
|
||||||
|
export default function MCPSection() {
|
||||||
|
const { showToast } = useToastContext();
|
||||||
|
const localize = useLocalize();
|
||||||
|
const { mcps = [], agent_id, setMcp, setActivePanel } = useAgentPanelContext();
|
||||||
|
|
||||||
|
const handleAddMCP = useCallback(() => {
|
||||||
|
if (!agent_id) {
|
||||||
|
showToast({
|
||||||
|
message: localize('com_agents_mcps_disabled'),
|
||||||
|
status: 'warning',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setActivePanel(Panel.mcp);
|
||||||
|
}, [agent_id, setActivePanel, showToast, localize]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="text-token-text-primary mb-2 block font-medium">
|
||||||
|
{localize('com_ui_mcp_servers')}
|
||||||
|
</label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{mcps
|
||||||
|
.filter((mcp) => mcp.agent_id === agent_id)
|
||||||
|
.map((mcp, i) => (
|
||||||
|
<MCP
|
||||||
|
key={i}
|
||||||
|
mcp={mcp}
|
||||||
|
onClick={() => {
|
||||||
|
setMcp(mcp);
|
||||||
|
setActivePanel(Panel.mcp);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleAddMCP}
|
||||||
|
className="btn btn-neutral border-token-border-light relative h-9 w-full rounded-lg font-medium"
|
||||||
|
aria-haspopup="dialog"
|
||||||
|
>
|
||||||
|
<div className="flex w-full items-center justify-center gap-2">
|
||||||
|
{localize('com_ui_add_mcp')}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,27 +1,28 @@
|
||||||
|
import keyBy from 'lodash/keyBy';
|
||||||
import React, { useMemo, useEffect } from 'react';
|
import React, { useMemo, useEffect } from 'react';
|
||||||
import { ChevronLeft, RotateCcw } from 'lucide-react';
|
import { ChevronLeft, RotateCcw } from 'lucide-react';
|
||||||
import { useFormContext, useWatch, Controller } from 'react-hook-form';
|
import { useFormContext, useWatch, Controller } from 'react-hook-form';
|
||||||
|
import { componentMapping } from '~/components/SidePanel/Parameters/components';
|
||||||
import {
|
import {
|
||||||
alternateName,
|
alternateName,
|
||||||
getSettingsKeys,
|
getSettingsKeys,
|
||||||
|
LocalStorageKeys,
|
||||||
SettingDefinition,
|
SettingDefinition,
|
||||||
agentParamSettings,
|
agentParamSettings,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import type * as t from 'librechat-data-provider';
|
import type * as t from 'librechat-data-provider';
|
||||||
import type { AgentForm, AgentModelPanelProps, StringOption } from '~/common';
|
import type { AgentForm, AgentModelPanelProps, StringOption } from '~/common';
|
||||||
import { componentMapping } from '~/components/SidePanel/Parameters/components';
|
|
||||||
import ControlCombobox from '~/components/ui/ControlCombobox';
|
import ControlCombobox from '~/components/ui/ControlCombobox';
|
||||||
import { useGetEndpointsQuery } from '~/data-provider';
|
import { useGetEndpointsQuery } from '~/data-provider';
|
||||||
import { getEndpointField, cn } from '~/utils';
|
import { getEndpointField, cn } from '~/utils';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import { Panel } from '~/common';
|
import { Panel } from '~/common';
|
||||||
import keyBy from 'lodash/keyBy';
|
|
||||||
|
|
||||||
export default function ModelPanel({
|
export default function ModelPanel({
|
||||||
setActivePanel,
|
|
||||||
providers,
|
providers,
|
||||||
|
setActivePanel,
|
||||||
models: modelsData,
|
models: modelsData,
|
||||||
}: AgentModelPanelProps) {
|
}: Pick<AgentModelPanelProps, 'models' | 'providers' | 'setActivePanel'>) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
|
||||||
const { control, setValue } = useFormContext<AgentForm>();
|
const { control, setValue } = useFormContext<AgentForm>();
|
||||||
|
|
@ -50,6 +51,8 @@ export default function ModelPanel({
|
||||||
const newModels = modelsData[provider] ?? [];
|
const newModels = modelsData[provider] ?? [];
|
||||||
setValue('model', newModels[0] ?? '');
|
setValue('model', newModels[0] ?? '');
|
||||||
}
|
}
|
||||||
|
localStorage.setItem(LocalStorageKeys.LAST_AGENT_MODEL, _model);
|
||||||
|
localStorage.setItem(LocalStorageKeys.LAST_AGENT_PROVIDER, provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (provider && !_model) {
|
if (provider && !_model) {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import type { Agent, TAgentsEndpoint } from 'librechat-data-provider';
|
|
||||||
import { ChevronLeft } from 'lucide-react';
|
import { ChevronLeft } from 'lucide-react';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import type { AgentPanelProps } from '~/common';
|
|
||||||
import { Panel } from '~/common';
|
|
||||||
import { useGetAgentByIdQuery, useRevertAgentVersionMutation } from '~/data-provider';
|
import { useGetAgentByIdQuery, useRevertAgentVersionMutation } from '~/data-provider';
|
||||||
|
import type { Agent } from 'librechat-data-provider';
|
||||||
|
import { isActiveVersion } from './isActiveVersion';
|
||||||
|
import { useAgentPanelContext } from '~/Providers';
|
||||||
import { useLocalize, useToast } from '~/hooks';
|
import { useLocalize, useToast } from '~/hooks';
|
||||||
import VersionContent from './VersionContent';
|
import VersionContent from './VersionContent';
|
||||||
import { isActiveVersion } from './isActiveVersion';
|
import { Panel } from '~/common';
|
||||||
|
|
||||||
export type VersionRecord = Record<string, any>;
|
export type VersionRecord = Record<string, any>;
|
||||||
|
|
||||||
|
|
@ -39,15 +39,13 @@ export interface AgentWithVersions extends Agent {
|
||||||
versions?: Array<VersionRecord>;
|
versions?: Array<VersionRecord>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type VersionPanelProps = {
|
export default function VersionPanel() {
|
||||||
agentsConfig: TAgentsEndpoint | null;
|
|
||||||
setActivePanel: AgentPanelProps['setActivePanel'];
|
|
||||||
selectedAgentId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function VersionPanel({ setActivePanel, selectedAgentId = '' }: VersionPanelProps) {
|
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
|
const { agent_id, setActivePanel } = useAgentPanelContext();
|
||||||
|
|
||||||
|
const selectedAgentId = agent_id ?? '';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: agent,
|
data: agent,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
|
|
||||||
|
|
@ -55,13 +55,18 @@ jest.mock('~/hooks', () => ({
|
||||||
useToast: jest.fn(() => ({ showToast: jest.fn() })),
|
useToast: jest.fn(() => ({ showToast: jest.fn() })),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Mock the AgentPanelContext
|
||||||
|
jest.mock('~/Providers/AgentPanelContext', () => ({
|
||||||
|
...jest.requireActual('~/Providers/AgentPanelContext'),
|
||||||
|
useAgentPanelContext: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('VersionPanel', () => {
|
describe('VersionPanel', () => {
|
||||||
const mockSetActivePanel = jest.fn();
|
const mockSetActivePanel = jest.fn();
|
||||||
const defaultProps = {
|
const mockUseAgentPanelContext = jest.requireMock(
|
||||||
agentsConfig: null,
|
'~/Providers/AgentPanelContext',
|
||||||
setActivePanel: mockSetActivePanel,
|
).useAgentPanelContext;
|
||||||
selectedAgentId: 'agent-123',
|
|
||||||
};
|
|
||||||
const mockUseGetAgentByIdQuery = jest.requireMock('~/data-provider').useGetAgentByIdQuery;
|
const mockUseGetAgentByIdQuery = jest.requireMock('~/data-provider').useGetAgentByIdQuery;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
@ -72,10 +77,17 @@ describe('VersionPanel', () => {
|
||||||
error: null,
|
error: null,
|
||||||
refetch: jest.fn(),
|
refetch: jest.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set up the default context mock
|
||||||
|
mockUseAgentPanelContext.mockReturnValue({
|
||||||
|
setActivePanel: mockSetActivePanel,
|
||||||
|
agent_id: 'agent-123',
|
||||||
|
activePanel: Panel.version,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('renders panel UI and handles navigation', () => {
|
test('renders panel UI and handles navigation', () => {
|
||||||
render(<VersionPanel {...defaultProps} />);
|
render(<VersionPanel />);
|
||||||
expect(screen.getByText('com_ui_agent_version_history')).toBeInTheDocument();
|
expect(screen.getByText('com_ui_agent_version_history')).toBeInTheDocument();
|
||||||
expect(screen.getByTestId('version-content')).toBeInTheDocument();
|
expect(screen.getByTestId('version-content')).toBeInTheDocument();
|
||||||
|
|
||||||
|
|
@ -84,7 +96,7 @@ describe('VersionPanel', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('VersionContent receives correct props', () => {
|
test('VersionContent receives correct props', () => {
|
||||||
render(<VersionPanel {...defaultProps} />);
|
render(<VersionPanel />);
|
||||||
expect(VersionContent).toHaveBeenCalledWith(
|
expect(VersionContent).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
selectedAgentId: 'agent-123',
|
selectedAgentId: 'agent-123',
|
||||||
|
|
@ -101,19 +113,31 @@ describe('VersionPanel', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handles data state variations', () => {
|
test('handles data state variations', () => {
|
||||||
render(<VersionPanel {...defaultProps} selectedAgentId="" />);
|
// Test with empty agent_id
|
||||||
|
mockUseAgentPanelContext.mockReturnValueOnce({
|
||||||
|
setActivePanel: mockSetActivePanel,
|
||||||
|
agent_id: '',
|
||||||
|
activePanel: Panel.version,
|
||||||
|
});
|
||||||
|
render(<VersionPanel />);
|
||||||
expect(VersionContent).toHaveBeenCalledWith(
|
expect(VersionContent).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ selectedAgentId: '' }),
|
expect.objectContaining({ selectedAgentId: '' }),
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Test with null data
|
||||||
mockUseGetAgentByIdQuery.mockReturnValueOnce({
|
mockUseGetAgentByIdQuery.mockReturnValueOnce({
|
||||||
data: null,
|
data: null,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
refetch: jest.fn(),
|
refetch: jest.fn(),
|
||||||
});
|
});
|
||||||
render(<VersionPanel {...defaultProps} />);
|
mockUseAgentPanelContext.mockReturnValueOnce({
|
||||||
|
setActivePanel: mockSetActivePanel,
|
||||||
|
agent_id: 'agent-123',
|
||||||
|
activePanel: Panel.version,
|
||||||
|
});
|
||||||
|
render(<VersionPanel />);
|
||||||
expect(VersionContent).toHaveBeenCalledWith(
|
expect(VersionContent).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
versionContext: expect.objectContaining({
|
versionContext: expect.objectContaining({
|
||||||
|
|
@ -125,13 +149,14 @@ describe('VersionPanel', () => {
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 3. versions is undefined
|
||||||
mockUseGetAgentByIdQuery.mockReturnValueOnce({
|
mockUseGetAgentByIdQuery.mockReturnValueOnce({
|
||||||
data: { ...mockAgentData, versions: undefined },
|
data: { ...mockAgentData, versions: undefined },
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
refetch: jest.fn(),
|
refetch: jest.fn(),
|
||||||
});
|
});
|
||||||
render(<VersionPanel {...defaultProps} />);
|
render(<VersionPanel />);
|
||||||
expect(VersionContent).toHaveBeenCalledWith(
|
expect(VersionContent).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
versionContext: expect.objectContaining({ versions: [] }),
|
versionContext: expect.objectContaining({ versions: [] }),
|
||||||
|
|
@ -139,18 +164,20 @@ describe('VersionPanel', () => {
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 4. loading state
|
||||||
mockUseGetAgentByIdQuery.mockReturnValueOnce({
|
mockUseGetAgentByIdQuery.mockReturnValueOnce({
|
||||||
data: null,
|
data: null,
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
error: null,
|
error: null,
|
||||||
refetch: jest.fn(),
|
refetch: jest.fn(),
|
||||||
});
|
});
|
||||||
render(<VersionPanel {...defaultProps} />);
|
render(<VersionPanel />);
|
||||||
expect(VersionContent).toHaveBeenCalledWith(
|
expect(VersionContent).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ isLoading: true }),
|
expect.objectContaining({ isLoading: true }),
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 5. error state
|
||||||
const testError = new Error('Test error');
|
const testError = new Error('Test error');
|
||||||
mockUseGetAgentByIdQuery.mockReturnValueOnce({
|
mockUseGetAgentByIdQuery.mockReturnValueOnce({
|
||||||
data: null,
|
data: null,
|
||||||
|
|
@ -158,7 +185,7 @@ describe('VersionPanel', () => {
|
||||||
error: testError,
|
error: testError,
|
||||||
refetch: jest.fn(),
|
refetch: jest.fn(),
|
||||||
});
|
});
|
||||||
render(<VersionPanel {...defaultProps} />);
|
render(<VersionPanel />);
|
||||||
expect(VersionContent).toHaveBeenCalledWith(
|
expect(VersionContent).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ error: testError }),
|
expect.objectContaining({ error: testError }),
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
|
|
@ -173,7 +200,7 @@ describe('VersionPanel', () => {
|
||||||
refetch: jest.fn(),
|
refetch: jest.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
render(<VersionPanel {...defaultProps} />);
|
render(<VersionPanel />);
|
||||||
expect(VersionContent).toHaveBeenCalledWith(
|
expect(VersionContent).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
versionContext: expect.objectContaining({
|
versionContext: expect.objectContaining({
|
||||||
|
|
|
||||||
60
client/src/components/SidePanel/Builder/MCP.tsx
Normal file
60
client/src/components/SidePanel/Builder/MCP.tsx
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
import type { MCP } from 'librechat-data-provider';
|
||||||
|
import GearIcon from '~/components/svg/GearIcon';
|
||||||
|
import MCPIcon from '~/components/svg/MCPIcon';
|
||||||
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
|
type MCPProps = {
|
||||||
|
mcp: MCP;
|
||||||
|
onClick: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function MCP({ mcp, onClick }: MCPProps) {
|
||||||
|
const [isHovering, setIsHovering] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={onClick}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
onClick();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="group flex w-full rounded-lg border border-border-medium text-sm hover:cursor-pointer focus:outline-none focus:ring-2 focus:ring-text-primary"
|
||||||
|
onMouseEnter={() => setIsHovering(true)}
|
||||||
|
onMouseLeave={() => setIsHovering(false)}
|
||||||
|
aria-label={`MCP for ${mcp.metadata.name}`}
|
||||||
|
>
|
||||||
|
<div className="flex h-9 items-center gap-2 px-3">
|
||||||
|
{mcp.metadata.icon ? (
|
||||||
|
<img
|
||||||
|
src={mcp.metadata.icon}
|
||||||
|
alt={`${mcp.metadata.name} icon`}
|
||||||
|
className="h-6 w-6 rounded-md object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-6 w-6 items-center justify-center rounded-md bg-surface-secondary">
|
||||||
|
<MCPIcon />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className="grow overflow-hidden text-ellipsis whitespace-nowrap"
|
||||||
|
style={{ wordBreak: 'break-all' }}
|
||||||
|
>
|
||||||
|
{mcp.metadata.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'ml-auto h-9 w-9 min-w-9 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-tertiary focus:outline-none focus:ring-2 focus:ring-text-primary group-focus:flex',
|
||||||
|
isHovering ? 'flex' : 'hidden',
|
||||||
|
)}
|
||||||
|
aria-label="Settings"
|
||||||
|
>
|
||||||
|
<GearIcon className="icon-sm" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
55
client/src/components/SidePanel/Builder/MCPAuth.tsx
Normal file
55
client/src/components/SidePanel/Builder/MCPAuth.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
import ActionsAuth from '~/components/SidePanel/Builder/ActionsAuth';
|
||||||
|
import {
|
||||||
|
AuthorizationTypeEnum,
|
||||||
|
TokenExchangeMethodEnum,
|
||||||
|
AuthTypeEnum,
|
||||||
|
} from 'librechat-data-provider';
|
||||||
|
|
||||||
|
export default function MCPAuth() {
|
||||||
|
// Create a separate form for auth
|
||||||
|
const authMethods = useForm({
|
||||||
|
defaultValues: {
|
||||||
|
/* General */
|
||||||
|
type: AuthTypeEnum.None,
|
||||||
|
saved_auth_fields: false,
|
||||||
|
/* API key */
|
||||||
|
api_key: '',
|
||||||
|
authorization_type: AuthorizationTypeEnum.Basic,
|
||||||
|
custom_auth_header: '',
|
||||||
|
/* OAuth */
|
||||||
|
oauth_client_id: '',
|
||||||
|
oauth_client_secret: '',
|
||||||
|
authorization_url: '',
|
||||||
|
client_url: '',
|
||||||
|
scope: '',
|
||||||
|
token_exchange_method: TokenExchangeMethodEnum.DefaultPost,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { watch, setValue } = authMethods;
|
||||||
|
const type = watch('type');
|
||||||
|
|
||||||
|
// Sync form state when auth type changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (type === 'none') {
|
||||||
|
// Reset auth fields when type is none
|
||||||
|
setValue('api_key', '');
|
||||||
|
setValue('authorization_type', AuthorizationTypeEnum.Basic);
|
||||||
|
setValue('custom_auth_header', '');
|
||||||
|
setValue('oauth_client_id', '');
|
||||||
|
setValue('oauth_client_secret', '');
|
||||||
|
setValue('authorization_url', '');
|
||||||
|
setValue('client_url', '');
|
||||||
|
setValue('scope', '');
|
||||||
|
setValue('token_exchange_method', TokenExchangeMethodEnum.DefaultPost);
|
||||||
|
}
|
||||||
|
}, [type, setValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...authMethods}>
|
||||||
|
<ActionsAuth />
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
15
client/src/components/svg/MCPIcon.tsx
Normal file
15
client/src/components/svg/MCPIcon.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
export default function MCPIcon() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
className="h-4 w-4"
|
||||||
|
>
|
||||||
|
<path d="M11.016 2.099a3.998 3.998 0 0 1 5.58.072l.073.074a3.991 3.991 0 0 1 1.058 3.318 3.994 3.994 0 0 1 3.32 1.06l.073.071.048.047.071.075a3.998 3.998 0 0 1 0 5.506l-.071.074-8.183 8.182-.034.042a.267.267 0 0 0 .034.335l1.68 1.68a.8.8 0 0 1-1.131 1.13l-1.68-1.679a1.866 1.866 0 0 1-.034-2.604l8.26-8.261a2.4 2.4 0 0 0-.044-3.349l-.047-.047-.044-.043a2.4 2.4 0 0 0-3.349.043l-6.832 6.832-.03.029a.8.8 0 0 1-1.1-1.16l6.876-6.875a2.4 2.4 0 0 0-.044-3.35l-.179-.161a2.399 2.399 0 0 0-3.169.119l-.045.043-9.047 9.047-.03.028a.8.8 0 0 1-1.1-1.16l9.046-9.046.074-.072Z" />
|
||||||
|
<path d="M13.234 4.404a.8.8 0 0 1 1.1 1.16l-6.69 6.691a2.399 2.399 0 1 0 3.393 3.393l6.691-6.692a.8.8 0 0 1 1.131 1.131l-6.691 6.692a4 4 0 0 1-5.581.07l-.073-.07a3.998 3.998 0 0 1 0-5.655l6.69-6.691.03-.029Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
client/src/components/svg/SquirclePlusIcon.tsx
Normal file
19
client/src/components/svg/SquirclePlusIcon.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
export default function SquirclePlusIcon() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
strokeWidth="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="text-3xl"
|
||||||
|
height="1em"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<line x1="12" y1="5" x2="12" y2="19" />
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -224,18 +224,20 @@ export const useUpdateAgentAction = (
|
||||||
});
|
});
|
||||||
|
|
||||||
queryClient.setQueryData<t.Action[]>([QueryKeys.actions], (prev) => {
|
queryClient.setQueryData<t.Action[]>([QueryKeys.actions], (prev) => {
|
||||||
return prev
|
if (!prev) {
|
||||||
?.map((action) => {
|
return [updateAgentActionResponse[1]];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (variables.action_id) {
|
||||||
|
return prev.map((action) => {
|
||||||
if (action.action_id === variables.action_id) {
|
if (action.action_id === variables.action_id) {
|
||||||
return updateAgentActionResponse[1];
|
return updateAgentActionResponse[1];
|
||||||
}
|
}
|
||||||
return action;
|
return action;
|
||||||
})
|
});
|
||||||
.concat(
|
}
|
||||||
variables.action_id != null && variables.action_id
|
|
||||||
? []
|
return [...prev, updateAgentActionResponse[1]];
|
||||||
: [updateAgentActionResponse[1]],
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
queryClient.setQueryData<t.Agent>([QueryKeys.agent, variables.agent_id], updatedAgent);
|
queryClient.setQueryData<t.Agent>([QueryKeys.agent, variables.agent_id], updatedAgent);
|
||||||
|
|
|
||||||
|
|
@ -1005,5 +1005,27 @@
|
||||||
"com_ui_yes": "Yes",
|
"com_ui_yes": "Yes",
|
||||||
"com_ui_zoom": "Zoom",
|
"com_ui_zoom": "Zoom",
|
||||||
"com_user_message": "You",
|
"com_user_message": "You",
|
||||||
"com_warning_resubmit_unsupported": "Resubmitting the AI message is not supported for this endpoint."
|
"com_warning_resubmit_unsupported": "Resubmitting the AI message is not supported for this endpoint.",
|
||||||
|
"com_ui_add_mcp": "Add MCP",
|
||||||
|
"com_ui_add_mcp_server": "Add MCP Server",
|
||||||
|
"com_ui_edit_mcp_server": "Edit MCP Server",
|
||||||
|
"com_agents_mcps_disabled": "You need to create an agent before adding MCPs.",
|
||||||
|
"com_ui_delete_mcp": "Delete MCP",
|
||||||
|
"com_ui_delete_mcp_confirm": "Are you sure you want to delete this MCP server?",
|
||||||
|
"com_ui_delete_mcp_success": "MCP server deleted successfully",
|
||||||
|
"com_ui_delete_mcp_error": "Failed to delete MCP server",
|
||||||
|
"com_agents_mcp_info": "Add MCP servers to your agent to enable it to perform tasks and interact with external services",
|
||||||
|
"com_ui_update_mcp_error": "There was an error creating or updating the MCP.",
|
||||||
|
"com_ui_update_mcp_success": "Successfully created or updated MCP",
|
||||||
|
"com_ui_available_tools": "Available Tools",
|
||||||
|
"com_ui_select_all": "Select All",
|
||||||
|
"com_ui_deselect_all": "Deselect All",
|
||||||
|
"com_agents_mcp_name_placeholder": "Custom Tool",
|
||||||
|
"com_ui_optional": "(optional)",
|
||||||
|
"com_agents_mcp_description_placeholder": "Explain what it does in a few words",
|
||||||
|
"com_ui_mcp_url": "MCP Server URL",
|
||||||
|
"com_ui_trust_app": "I trust this application",
|
||||||
|
"com_agents_mcp_trust_subtext": "Custom connectors are not verified by LibreChat",
|
||||||
|
"com_ui_icon": "Icon",
|
||||||
|
"com_agents_mcp_icon_size": "Minimum size 128 x 128 px"
|
||||||
}
|
}
|
||||||
|
|
@ -4,6 +4,8 @@ import {
|
||||||
alternateName,
|
alternateName,
|
||||||
EModelEndpoint,
|
EModelEndpoint,
|
||||||
EToolResources,
|
EToolResources,
|
||||||
|
LocalStorageKeys,
|
||||||
|
defaultAgentFormValues,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import type { Agent, TFile } from 'librechat-data-provider';
|
import type { Agent, TFile } from 'librechat-data-provider';
|
||||||
import type { DropdownValueSetter, TAgentOption, ExtendedFile } from '~/common';
|
import type { DropdownValueSetter, TAgentOption, ExtendedFile } from '~/common';
|
||||||
|
|
@ -42,6 +44,16 @@ export const createProviderOption = (provider: string) => ({
|
||||||
value: provider,
|
value: provider,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets default agent form values with localStorage values for model and provider.
|
||||||
|
* This is used to initialize agent forms with the last used model and provider.
|
||||||
|
**/
|
||||||
|
export const getDefaultAgentFormValues = () => ({
|
||||||
|
...defaultAgentFormValues,
|
||||||
|
model: localStorage.getItem(LocalStorageKeys.LAST_AGENT_MODEL) ?? '',
|
||||||
|
provider: createProviderOption(localStorage.getItem(LocalStorageKeys.LAST_AGENT_PROVIDER) ?? ''),
|
||||||
|
});
|
||||||
|
|
||||||
export const processAgentOption = ({
|
export const processAgentOption = ({
|
||||||
agent: _agent,
|
agent: _agent,
|
||||||
fileMap,
|
fileMap,
|
||||||
|
|
|
||||||
|
|
@ -1434,6 +1434,10 @@ export enum LocalStorageKeys {
|
||||||
LAST_CODE_TOGGLE_ = 'LAST_CODE_TOGGLE_',
|
LAST_CODE_TOGGLE_ = 'LAST_CODE_TOGGLE_',
|
||||||
/** Last checked toggle for Web Search per conversation ID */
|
/** Last checked toggle for Web Search per conversation ID */
|
||||||
LAST_WEB_SEARCH_TOGGLE_ = 'LAST_WEB_SEARCH_TOGGLE_',
|
LAST_WEB_SEARCH_TOGGLE_ = 'LAST_WEB_SEARCH_TOGGLE_',
|
||||||
|
/** Key for the last selected agent provider */
|
||||||
|
LAST_AGENT_PROVIDER = 'lastAgentProvider',
|
||||||
|
/** Key for the last selected agent model */
|
||||||
|
LAST_AGENT_MODEL = 'lastAgentModel',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ForkOptions {
|
export enum ForkOptions {
|
||||||
|
|
|
||||||
|
|
@ -515,6 +515,8 @@ export type ActionAuth = {
|
||||||
token_exchange_method?: TokenExchangeMethodEnum;
|
token_exchange_method?: TokenExchangeMethodEnum;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MCPAuth = ActionAuth;
|
||||||
|
|
||||||
export type ActionMetadata = {
|
export type ActionMetadata = {
|
||||||
api_key?: string;
|
api_key?: string;
|
||||||
auth?: ActionAuth;
|
auth?: ActionAuth;
|
||||||
|
|
@ -525,6 +527,16 @@ export type ActionMetadata = {
|
||||||
oauth_client_secret?: string;
|
oauth_client_secret?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MCPMetadata = Omit<ActionMetadata, 'auth'> & {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
url?: string;
|
||||||
|
tools?: string[];
|
||||||
|
auth?: MCPAuth;
|
||||||
|
icon?: string;
|
||||||
|
trust?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type ActionMetadataRuntime = ActionMetadata & {
|
export type ActionMetadataRuntime = ActionMetadata & {
|
||||||
oauth_access_token?: string;
|
oauth_access_token?: string;
|
||||||
oauth_refresh_token?: string;
|
oauth_refresh_token?: string;
|
||||||
|
|
@ -541,6 +553,11 @@ export type Action = {
|
||||||
version: number | string;
|
version: number | string;
|
||||||
} & ({ assistant_id: string; agent_id?: never } | { assistant_id?: never; agent_id: string });
|
} & ({ assistant_id: string; agent_id?: never } | { assistant_id?: never; agent_id: string });
|
||||||
|
|
||||||
|
export type MCP = {
|
||||||
|
mcp_id: string;
|
||||||
|
metadata: MCPMetadata;
|
||||||
|
} & ({ assistant_id: string; agent_id?: never } | { assistant_id?: never; agent_id: string });
|
||||||
|
|
||||||
export type AssistantAvatar = {
|
export type AssistantAvatar = {
|
||||||
filepath: string;
|
filepath: string;
|
||||||
source: string;
|
source: string;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue