mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 00:40:14 +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 { createContext, useContext } from 'react';
|
||||
import { defaultAgentFormValues } from 'librechat-data-provider';
|
||||
import type { UseFormReturn } from 'react-hook-form';
|
||||
import type { AgentForm } from '~/common';
|
||||
import { getDefaultAgentFormValues } from '~/utils';
|
||||
|
||||
type AgentsContextType = UseFormReturn<AgentForm>;
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ export function useAgentsContext() {
|
|||
|
||||
export default function AgentsProvider({ children }) {
|
||||
const methods = useForm<AgentForm>({
|
||||
defaultValues: defaultAgentFormValues,
|
||||
defaultValues: getDefaultAgentFormValues(),
|
||||
});
|
||||
|
||||
return <FormProvider {...methods}>{children}</FormProvider>;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
export { default as ToastProvider } from './ToastContext';
|
||||
export { default as AssistantsProvider } from './AssistantsContext';
|
||||
export { default as AgentsProvider } from './AgentsContext';
|
||||
export { default as ToastProvider } from './ToastContext';
|
||||
export * from './AgentPanelContext';
|
||||
export * from './ChatContext';
|
||||
export * from './ShareContext';
|
||||
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',
|
||||
model = 'model',
|
||||
version = 'version',
|
||||
mcp = 'mcp',
|
||||
}
|
||||
|
||||
export type FileSetter =
|
||||
|
|
@ -166,6 +167,15 @@ export type ActionAuthForm = {
|
|||
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'> & {
|
||||
metadata: t.ActionMetadata | null;
|
||||
};
|
||||
|
|
@ -188,16 +198,33 @@ export type AgentPanelProps = {
|
|||
index?: number;
|
||||
agent_id?: string;
|
||||
activePanel?: string;
|
||||
mcp?: t.MCP;
|
||||
mcps?: t.MCP[];
|
||||
action?: t.Action;
|
||||
actions?: t.Action[];
|
||||
createMutation: UseMutationResult<t.Agent, Error, t.AgentCreateParams>;
|
||||
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
|
||||
setMcp: React.Dispatch<React.SetStateAction<t.MCP | undefined>>;
|
||||
setAction: React.Dispatch<React.SetStateAction<t.Action | undefined>>;
|
||||
endpointsConfig?: t.TEndpointsConfig;
|
||||
setCurrentAgentId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
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 = {
|
||||
agent_id?: string;
|
||||
providers: Option[];
|
||||
|
|
|
|||
|
|
@ -1,31 +1,27 @@
|
|||
import { useEffect } from 'react';
|
||||
import { ChevronLeft } from 'lucide-react';
|
||||
import { useForm, FormProvider } from 'react-hook-form';
|
||||
import {
|
||||
AuthTypeEnum,
|
||||
AuthorizationTypeEnum,
|
||||
TokenExchangeMethodEnum,
|
||||
} from 'librechat-data-provider';
|
||||
import { ChevronLeft } from 'lucide-react';
|
||||
import type { AgentPanelProps, ActionAuthForm } from '~/common';
|
||||
import ActionsAuth from '~/components/SidePanel/Builder/ActionsAuth';
|
||||
import { useAgentPanelContext } from '~/Providers/AgentPanelContext';
|
||||
import { OGDialog, OGDialogTrigger, Label } from '~/components/ui';
|
||||
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||
import { useDeleteAgentAction } from '~/data-provider';
|
||||
import type { ActionAuthForm } from '~/common';
|
||||
import useLocalize from '~/hooks/useLocalize';
|
||||
import { useToastContext } from '~/Providers';
|
||||
import { TrashIcon } from '~/components/svg';
|
||||
import ActionsInput from './ActionsInput';
|
||||
import { Panel } from '~/common';
|
||||
|
||||
export default function ActionsPanel({
|
||||
// activePanel,
|
||||
action,
|
||||
setAction,
|
||||
agent_id,
|
||||
setActivePanel,
|
||||
}: AgentPanelProps) {
|
||||
export default function ActionsPanel() {
|
||||
const localize = useLocalize();
|
||||
const { showToast } = useToastContext();
|
||||
const { setActivePanel, action, setAction, agent_id } = useAgentPanelContext();
|
||||
const deleteAgentAction = useDeleteAgentAction({
|
||||
onSuccess: () => {
|
||||
showToast({
|
||||
|
|
@ -62,7 +58,7 @@ export default function ActionsPanel({
|
|||
},
|
||||
});
|
||||
|
||||
const { reset, watch } = methods;
|
||||
const { reset } = methods;
|
||||
|
||||
useEffect(() => {
|
||||
if (action?.metadata.auth) {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import React, { useState, useMemo, useCallback } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import React, { useState, useMemo, useCallback } from 'react';
|
||||
import { Controller, useWatch, useFormContext } from 'react-hook-form';
|
||||
import { QueryKeys, EModelEndpoint, AgentCapabilities } from 'librechat-data-provider';
|
||||
import type { TPlugin } from 'librechat-data-provider';
|
||||
import type { AgentForm, AgentPanelProps, IconComponentTypes } from '~/common';
|
||||
import { cn, defaultTextProps, removeFocusOutlines, getEndpointField, getIconKey } from '~/utils';
|
||||
import { 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 { ToolSelectDialog } from '~/components/Tools';
|
||||
import { icons } from '~/hooks/Endpoint/Icons';
|
||||
|
|
@ -15,6 +15,7 @@ import AgentAvatar from './AgentAvatar';
|
|||
import FileContext from './FileContext';
|
||||
import SearchForm from './Search/Form';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import MCPSection from './MCPSection';
|
||||
import FileSearch from './FileSearch';
|
||||
import Artifacts from './Artifacts';
|
||||
import AgentTool from './AgentTool';
|
||||
|
|
@ -29,23 +30,19 @@ const inputClass = cn(
|
|||
);
|
||||
|
||||
export default function AgentConfig({
|
||||
setAction,
|
||||
actions = [],
|
||||
agentsConfig,
|
||||
createMutation,
|
||||
setActivePanel,
|
||||
endpointsConfig,
|
||||
}: AgentPanelProps) {
|
||||
}: Pick<AgentPanelProps, 'agentsConfig' | 'createMutation' | 'endpointsConfig'>) {
|
||||
const localize = useLocalize();
|
||||
const fileMap = useFileMapContext();
|
||||
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 { showToast } = useToastContext();
|
||||
const localize = useLocalize();
|
||||
|
||||
const [showToolDialog, setShowToolDialog] = useState(false);
|
||||
|
||||
const methods = useFormContext<AgentForm>();
|
||||
|
||||
const { control } = methods;
|
||||
const provider = useWatch({ control, name: 'provider' });
|
||||
|
|
@ -299,7 +296,7 @@ export default function AgentConfig({
|
|||
agent_id={agent_id}
|
||||
/>
|
||||
))}
|
||||
{actions
|
||||
{(actions ?? [])
|
||||
.filter((action) => action.agent_id === agent_id)
|
||||
.map((action, i) => (
|
||||
<Action
|
||||
|
|
@ -340,6 +337,8 @@ export default function AgentConfig({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* MCP Section */}
|
||||
{/* <MCPSection /> */}
|
||||
</div>
|
||||
<ToolSelectDialog
|
||||
isOpen={showToolDialog}
|
||||
|
|
|
|||
|
|
@ -7,20 +7,22 @@ import {
|
|||
Constants,
|
||||
SystemRoles,
|
||||
EModelEndpoint,
|
||||
TAgentsEndpoint,
|
||||
TEndpointsConfig,
|
||||
isAssistantsEndpoint,
|
||||
defaultAgentFormValues,
|
||||
} from 'librechat-data-provider';
|
||||
import type { AgentForm, AgentPanelProps, StringOption } from '~/common';
|
||||
import type { AgentForm, StringOption } from '~/common';
|
||||
import {
|
||||
useCreateAgentMutation,
|
||||
useUpdateAgentMutation,
|
||||
useGetAgentByIdQuery,
|
||||
} from '~/data-provider';
|
||||
import { createProviderOption, getDefaultAgentFormValues } from '~/utils';
|
||||
import { useSelectAgent, useLocalize, useAuthContext } from '~/hooks';
|
||||
import { useAgentPanelContext } from '~/Providers/AgentPanelContext';
|
||||
import AgentPanelSkeleton from './AgentPanelSkeleton';
|
||||
import { createProviderOption } from '~/utils';
|
||||
import { useToastContext } from '~/Providers';
|
||||
import AdvancedPanel from './Advanced/AdvancedPanel';
|
||||
import { useToastContext } from '~/Providers';
|
||||
import AgentConfig from './AgentConfig';
|
||||
import AgentSelect from './AgentSelect';
|
||||
import AgentFooter from './AgentFooter';
|
||||
|
|
@ -29,18 +31,21 @@ import ModelPanel from './ModelPanel';
|
|||
import { Panel } from '~/common';
|
||||
|
||||
export default function AgentPanel({
|
||||
setAction,
|
||||
activePanel,
|
||||
actions = [],
|
||||
setActivePanel,
|
||||
agent_id: current_agent_id,
|
||||
setCurrentAgentId,
|
||||
agentsConfig,
|
||||
endpointsConfig,
|
||||
}: AgentPanelProps) {
|
||||
}: {
|
||||
agentsConfig: TAgentsEndpoint | null;
|
||||
endpointsConfig: TEndpointsConfig;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const { user } = useAuthContext();
|
||||
const { showToast } = useToastContext();
|
||||
const {
|
||||
activePanel,
|
||||
setActivePanel,
|
||||
setCurrentAgentId,
|
||||
agent_id: current_agent_id,
|
||||
} = useAgentPanelContext();
|
||||
|
||||
const { onSelect: onSelectAgent } = useSelectAgent();
|
||||
|
||||
|
|
@ -51,7 +56,7 @@ export default function AgentPanel({
|
|||
|
||||
const models = useMemo(() => modelsQuery.data ?? {}, [modelsQuery.data]);
|
||||
const methods = useForm<AgentForm>({
|
||||
defaultValues: defaultAgentFormValues,
|
||||
defaultValues: getDefaultAgentFormValues(),
|
||||
});
|
||||
|
||||
const { control, handleSubmit, reset } = methods;
|
||||
|
|
@ -277,7 +282,7 @@ export default function AgentPanel({
|
|||
variant="outline"
|
||||
className="w-full justify-center"
|
||||
onClick={() => {
|
||||
reset(defaultAgentFormValues);
|
||||
reset(getDefaultAgentFormValues());
|
||||
setCurrentAgentId(undefined);
|
||||
}}
|
||||
disabled={agentQuery.isInitialLoading}
|
||||
|
|
@ -315,22 +320,13 @@ export default function AgentPanel({
|
|||
</div>
|
||||
)}
|
||||
{canEditAgent && !agentQuery.isInitialLoading && activePanel === Panel.model && (
|
||||
<ModelPanel
|
||||
setActivePanel={setActivePanel}
|
||||
agent_id={agent_id}
|
||||
providers={providers}
|
||||
models={models}
|
||||
/>
|
||||
<ModelPanel models={models} providers={providers} setActivePanel={setActivePanel} />
|
||||
)}
|
||||
{canEditAgent && !agentQuery.isInitialLoading && activePanel === Panel.builder && (
|
||||
<AgentConfig
|
||||
actions={actions}
|
||||
setAction={setAction}
|
||||
createMutation={create}
|
||||
agentsConfig={agentsConfig}
|
||||
setActivePanel={setActivePanel}
|
||||
endpointsConfig={endpointsConfig}
|
||||
setCurrentAgentId={setCurrentAgentId}
|
||||
/>
|
||||
)}
|
||||
{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 type { ActionsEndpoint } from '~/common';
|
||||
import type { Action, TConfig, TEndpointsConfig, TAgentsEndpoint } from 'librechat-data-provider';
|
||||
import { useGetActionsQuery, useGetEndpointsQuery, useCreateAgentMutation } from '~/data-provider';
|
||||
import type { TConfig, TEndpointsConfig, TAgentsEndpoint } from 'librechat-data-provider';
|
||||
import { AgentPanelProvider, useAgentPanelContext } from '~/Providers/AgentPanelContext';
|
||||
import { useGetEndpointsQuery } from '~/data-provider';
|
||||
import VersionPanel from './Version/VersionPanel';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import ActionsPanel from './ActionsPanel';
|
||||
import AgentPanel from './AgentPanel';
|
||||
import VersionPanel from './Version/VersionPanel';
|
||||
import MCPPanel from './MCPPanel';
|
||||
import { Panel } from '~/common';
|
||||
|
||||
export default function AgentPanelSwitch() {
|
||||
const { conversation, index } = useChatContext();
|
||||
const [activePanel, setActivePanel] = useState(Panel.builder);
|
||||
const [action, setAction] = useState<Action | undefined>(undefined);
|
||||
const [currentAgentId, setCurrentAgentId] = useState<string | undefined>(conversation?.agent_id);
|
||||
const { data: actions = [] } = useGetActionsQuery(conversation?.endpoint as ActionsEndpoint);
|
||||
return (
|
||||
<AgentPanelProvider>
|
||||
<AgentPanelSwitchWithContext />
|
||||
</AgentPanelProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentPanelSwitchWithContext() {
|
||||
const { conversation } = useChatContext();
|
||||
const { activePanel, setCurrentAgentId } = useAgentPanelContext();
|
||||
|
||||
// TODO: Implement MCP endpoint
|
||||
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
|
||||
const createMutation = useCreateAgentMutation();
|
||||
|
||||
const agentsConfig = useMemo<TAgentsEndpoint | null>(() => {
|
||||
const config = endpointsConfig?.[EModelEndpoint.agents] ?? null;
|
||||
|
|
@ -35,39 +42,20 @@ export default function AgentPanelSwitch() {
|
|||
if (agent_id) {
|
||||
setCurrentAgentId(agent_id);
|
||||
}
|
||||
}, [conversation?.agent_id]);
|
||||
}, [setCurrentAgentId, conversation?.agent_id]);
|
||||
|
||||
if (!conversation?.endpoint) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const commonProps = {
|
||||
index,
|
||||
action,
|
||||
actions,
|
||||
setAction,
|
||||
activePanel,
|
||||
setActivePanel,
|
||||
setCurrentAgentId,
|
||||
agent_id: currentAgentId,
|
||||
createMutation,
|
||||
};
|
||||
|
||||
if (activePanel === Panel.actions) {
|
||||
return <ActionsPanel {...commonProps} />;
|
||||
return <ActionsPanel />;
|
||||
}
|
||||
|
||||
if (activePanel === Panel.version) {
|
||||
return (
|
||||
<VersionPanel
|
||||
setActivePanel={setActivePanel}
|
||||
agentsConfig={agentsConfig}
|
||||
selectedAgentId={currentAgentId}
|
||||
/>
|
||||
);
|
||||
return <VersionPanel />;
|
||||
}
|
||||
|
||||
return (
|
||||
<AgentPanel {...commonProps} agentsConfig={agentsConfig} endpointsConfig={endpointsConfig} />
|
||||
);
|
||||
if (activePanel === Panel.mcp) {
|
||||
return <MCPPanel />;
|
||||
}
|
||||
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 { Agent, AgentCreateParams } from 'librechat-data-provider';
|
||||
import type { TAgentCapabilities, AgentForm } from '~/common';
|
||||
import { cn, createProviderOption, processAgentOption, getDefaultAgentFormValues } from '~/utils';
|
||||
import { useListAgentsQuery, useGetStartupConfig } from '~/data-provider';
|
||||
import { cn, createProviderOption, processAgentOption } from '~/utils';
|
||||
import ControlCombobox from '~/components/ui/ControlCombobox';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
|
|
@ -32,7 +32,10 @@ export default function AgentSelect({
|
|||
select: (res) =>
|
||||
res.data.map((agent) =>
|
||||
processAgentOption({
|
||||
agent,
|
||||
agent: {
|
||||
...agent,
|
||||
name: agent.name || agent.id,
|
||||
},
|
||||
instanceProjectId: startupConfig?.instanceProjectId,
|
||||
}),
|
||||
),
|
||||
|
|
@ -124,9 +127,7 @@ export default function AgentSelect({
|
|||
createMutation.reset();
|
||||
if (!agentExists) {
|
||||
setCurrentAgentId(undefined);
|
||||
return reset({
|
||||
...defaultAgentFormValues,
|
||||
});
|
||||
return reset(getDefaultAgentFormValues());
|
||||
}
|
||||
|
||||
setCurrentAgentId(selectedId);
|
||||
|
|
@ -179,7 +180,7 @@ export default function AgentSelect({
|
|||
containerClassName="px-0"
|
||||
selectedValue={(field?.value?.value ?? '') + ''}
|
||||
displayValue={field?.value?.label ?? ''}
|
||||
selectPlaceholder={createAgent}
|
||||
selectPlaceholder={field?.value?.value ?? createAgent}
|
||||
iconSide="right"
|
||||
searchPlaceholder={localize('com_agents_search_name')}
|
||||
SelectIcon={field?.value?.icon}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import { useFormContext } from 'react-hook-form';
|
||||
import { defaultAgentFormValues } from 'librechat-data-provider';
|
||||
import type { Agent, AgentCreateParams } from 'librechat-data-provider';
|
||||
import type { UseMutationResult } from '@tanstack/react-query';
|
||||
import { cn, logger, removeFocusOutlines, getDefaultAgentFormValues } from '~/utils';
|
||||
import { OGDialog, OGDialogTrigger, Label } from '~/components/ui';
|
||||
import { useChatContext, useToastContext } from '~/Providers';
|
||||
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||
import { useChatContext, useToastContext } from '~/Providers';
|
||||
import { useLocalize, useSetIndexOptions } from '~/hooks';
|
||||
import { cn, removeFocusOutlines, logger } from '~/utils';
|
||||
import { useDeleteAgentMutation } from '~/data-provider';
|
||||
import { TrashIcon } from '~/components/svg';
|
||||
|
||||
|
|
@ -45,9 +44,7 @@ export default function DeleteButton({
|
|||
const firstAgent = updatedList[0] as Agent | undefined;
|
||||
if (!firstAgent) {
|
||||
setCurrentAgentId(undefined);
|
||||
reset({
|
||||
...defaultAgentFormValues,
|
||||
});
|
||||
reset(getDefaultAgentFormValues());
|
||||
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 { ChevronLeft, RotateCcw } from 'lucide-react';
|
||||
import { useFormContext, useWatch, Controller } from 'react-hook-form';
|
||||
import { componentMapping } from '~/components/SidePanel/Parameters/components';
|
||||
import {
|
||||
alternateName,
|
||||
getSettingsKeys,
|
||||
LocalStorageKeys,
|
||||
SettingDefinition,
|
||||
agentParamSettings,
|
||||
} from 'librechat-data-provider';
|
||||
import type * as t from 'librechat-data-provider';
|
||||
import type { AgentForm, AgentModelPanelProps, StringOption } from '~/common';
|
||||
import { componentMapping } from '~/components/SidePanel/Parameters/components';
|
||||
import ControlCombobox from '~/components/ui/ControlCombobox';
|
||||
import { useGetEndpointsQuery } from '~/data-provider';
|
||||
import { getEndpointField, cn } from '~/utils';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { Panel } from '~/common';
|
||||
import keyBy from 'lodash/keyBy';
|
||||
|
||||
export default function ModelPanel({
|
||||
setActivePanel,
|
||||
providers,
|
||||
setActivePanel,
|
||||
models: modelsData,
|
||||
}: AgentModelPanelProps) {
|
||||
}: Pick<AgentModelPanelProps, 'models' | 'providers' | 'setActivePanel'>) {
|
||||
const localize = useLocalize();
|
||||
|
||||
const { control, setValue } = useFormContext<AgentForm>();
|
||||
|
|
@ -50,6 +51,8 @@ export default function ModelPanel({
|
|||
const newModels = modelsData[provider] ?? [];
|
||||
setValue('model', newModels[0] ?? '');
|
||||
}
|
||||
localStorage.setItem(LocalStorageKeys.LAST_AGENT_MODEL, _model);
|
||||
localStorage.setItem(LocalStorageKeys.LAST_AGENT_PROVIDER, provider);
|
||||
}
|
||||
|
||||
if (provider && !_model) {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import type { Agent, TAgentsEndpoint } from 'librechat-data-provider';
|
||||
import { ChevronLeft } from 'lucide-react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import type { AgentPanelProps } from '~/common';
|
||||
import { Panel } from '~/common';
|
||||
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 VersionContent from './VersionContent';
|
||||
import { isActiveVersion } from './isActiveVersion';
|
||||
import { Panel } from '~/common';
|
||||
|
||||
export type VersionRecord = Record<string, any>;
|
||||
|
||||
|
|
@ -39,15 +39,13 @@ export interface AgentWithVersions extends Agent {
|
|||
versions?: Array<VersionRecord>;
|
||||
}
|
||||
|
||||
export type VersionPanelProps = {
|
||||
agentsConfig: TAgentsEndpoint | null;
|
||||
setActivePanel: AgentPanelProps['setActivePanel'];
|
||||
selectedAgentId?: string;
|
||||
};
|
||||
|
||||
export default function VersionPanel({ setActivePanel, selectedAgentId = '' }: VersionPanelProps) {
|
||||
export default function VersionPanel() {
|
||||
const localize = useLocalize();
|
||||
const { showToast } = useToast();
|
||||
const { agent_id, setActivePanel } = useAgentPanelContext();
|
||||
|
||||
const selectedAgentId = agent_id ?? '';
|
||||
|
||||
const {
|
||||
data: agent,
|
||||
isLoading,
|
||||
|
|
|
|||
|
|
@ -55,13 +55,18 @@ jest.mock('~/hooks', () => ({
|
|||
useToast: jest.fn(() => ({ showToast: jest.fn() })),
|
||||
}));
|
||||
|
||||
// Mock the AgentPanelContext
|
||||
jest.mock('~/Providers/AgentPanelContext', () => ({
|
||||
...jest.requireActual('~/Providers/AgentPanelContext'),
|
||||
useAgentPanelContext: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('VersionPanel', () => {
|
||||
const mockSetActivePanel = jest.fn();
|
||||
const defaultProps = {
|
||||
agentsConfig: null,
|
||||
setActivePanel: mockSetActivePanel,
|
||||
selectedAgentId: 'agent-123',
|
||||
};
|
||||
const mockUseAgentPanelContext = jest.requireMock(
|
||||
'~/Providers/AgentPanelContext',
|
||||
).useAgentPanelContext;
|
||||
|
||||
const mockUseGetAgentByIdQuery = jest.requireMock('~/data-provider').useGetAgentByIdQuery;
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -72,10 +77,17 @@ describe('VersionPanel', () => {
|
|||
error: null,
|
||||
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', () => {
|
||||
render(<VersionPanel {...defaultProps} />);
|
||||
render(<VersionPanel />);
|
||||
expect(screen.getByText('com_ui_agent_version_history')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('version-content')).toBeInTheDocument();
|
||||
|
||||
|
|
@ -84,7 +96,7 @@ describe('VersionPanel', () => {
|
|||
});
|
||||
|
||||
test('VersionContent receives correct props', () => {
|
||||
render(<VersionPanel {...defaultProps} />);
|
||||
render(<VersionPanel />);
|
||||
expect(VersionContent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
selectedAgentId: 'agent-123',
|
||||
|
|
@ -101,19 +113,31 @@ describe('VersionPanel', () => {
|
|||
});
|
||||
|
||||
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.objectContaining({ selectedAgentId: '' }),
|
||||
expect.anything(),
|
||||
);
|
||||
|
||||
// Test with null data
|
||||
mockUseGetAgentByIdQuery.mockReturnValueOnce({
|
||||
data: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
render(<VersionPanel {...defaultProps} />);
|
||||
mockUseAgentPanelContext.mockReturnValueOnce({
|
||||
setActivePanel: mockSetActivePanel,
|
||||
agent_id: 'agent-123',
|
||||
activePanel: Panel.version,
|
||||
});
|
||||
render(<VersionPanel />);
|
||||
expect(VersionContent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
versionContext: expect.objectContaining({
|
||||
|
|
@ -125,13 +149,14 @@ describe('VersionPanel', () => {
|
|||
expect.anything(),
|
||||
);
|
||||
|
||||
// 3. versions is undefined
|
||||
mockUseGetAgentByIdQuery.mockReturnValueOnce({
|
||||
data: { ...mockAgentData, versions: undefined },
|
||||
isLoading: false,
|
||||
error: null,
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
render(<VersionPanel {...defaultProps} />);
|
||||
render(<VersionPanel />);
|
||||
expect(VersionContent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
versionContext: expect.objectContaining({ versions: [] }),
|
||||
|
|
@ -139,18 +164,20 @@ describe('VersionPanel', () => {
|
|||
expect.anything(),
|
||||
);
|
||||
|
||||
// 4. loading state
|
||||
mockUseGetAgentByIdQuery.mockReturnValueOnce({
|
||||
data: null,
|
||||
isLoading: true,
|
||||
error: null,
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
render(<VersionPanel {...defaultProps} />);
|
||||
render(<VersionPanel />);
|
||||
expect(VersionContent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ isLoading: true }),
|
||||
expect.anything(),
|
||||
);
|
||||
|
||||
// 5. error state
|
||||
const testError = new Error('Test error');
|
||||
mockUseGetAgentByIdQuery.mockReturnValueOnce({
|
||||
data: null,
|
||||
|
|
@ -158,7 +185,7 @@ describe('VersionPanel', () => {
|
|||
error: testError,
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
render(<VersionPanel {...defaultProps} />);
|
||||
render(<VersionPanel />);
|
||||
expect(VersionContent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ error: testError }),
|
||||
expect.anything(),
|
||||
|
|
@ -173,7 +200,7 @@ describe('VersionPanel', () => {
|
|||
refetch: jest.fn(),
|
||||
});
|
||||
|
||||
render(<VersionPanel {...defaultProps} />);
|
||||
render(<VersionPanel />);
|
||||
expect(VersionContent).toHaveBeenCalledWith(
|
||||
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) => {
|
||||
return prev
|
||||
?.map((action) => {
|
||||
if (!prev) {
|
||||
return [updateAgentActionResponse[1]];
|
||||
}
|
||||
|
||||
if (variables.action_id) {
|
||||
return prev.map((action) => {
|
||||
if (action.action_id === variables.action_id) {
|
||||
return updateAgentActionResponse[1];
|
||||
}
|
||||
return action;
|
||||
})
|
||||
.concat(
|
||||
variables.action_id != null && variables.action_id
|
||||
? []
|
||||
: [updateAgentActionResponse[1]],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return [...prev, updateAgentActionResponse[1]];
|
||||
});
|
||||
|
||||
queryClient.setQueryData<t.Agent>([QueryKeys.agent, variables.agent_id], updatedAgent);
|
||||
|
|
|
|||
|
|
@ -1005,5 +1005,27 @@
|
|||
"com_ui_yes": "Yes",
|
||||
"com_ui_zoom": "Zoom",
|
||||
"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,
|
||||
EModelEndpoint,
|
||||
EToolResources,
|
||||
LocalStorageKeys,
|
||||
defaultAgentFormValues,
|
||||
} from 'librechat-data-provider';
|
||||
import type { Agent, TFile } from 'librechat-data-provider';
|
||||
import type { DropdownValueSetter, TAgentOption, ExtendedFile } from '~/common';
|
||||
|
|
@ -42,6 +44,16 @@ export const createProviderOption = (provider: string) => ({
|
|||
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 = ({
|
||||
agent: _agent,
|
||||
fileMap,
|
||||
|
|
|
|||
|
|
@ -1434,6 +1434,10 @@ export enum LocalStorageKeys {
|
|||
LAST_CODE_TOGGLE_ = 'LAST_CODE_TOGGLE_',
|
||||
/** Last checked toggle for Web Search per conversation ID */
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -515,6 +515,8 @@ export type ActionAuth = {
|
|||
token_exchange_method?: TokenExchangeMethodEnum;
|
||||
};
|
||||
|
||||
export type MCPAuth = ActionAuth;
|
||||
|
||||
export type ActionMetadata = {
|
||||
api_key?: string;
|
||||
auth?: ActionAuth;
|
||||
|
|
@ -525,6 +527,16 @@ export type ActionMetadata = {
|
|||
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 & {
|
||||
oauth_access_token?: string;
|
||||
oauth_refresh_token?: string;
|
||||
|
|
@ -541,6 +553,11 @@ export type Action = {
|
|||
version: number | 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 = {
|
||||
filepath: string;
|
||||
source: string;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue