Fix: The tool icon for the agent operator created from the template is not clickable. (#17794)

This commit is contained in:
balibabu
2026-08-04 17:18:28 +08:00
committed by GitHub
parent 4c2398cb79
commit 4d9c62ab39
15 changed files with 5336 additions and 5263 deletions

View File

@@ -4,7 +4,7 @@ import {
} from '@/constants/knowledge';
import { useTranslate } from '@/hooks/common-hooks';
import { camelCase, isEqual } from 'lodash';
import React, { useCallback } from 'react';
import { useCallback } from 'react';
import { useFormContext } from 'react-hook-form';
import { z } from 'zod';
import { Collapse } from '../collapse';
@@ -92,13 +92,6 @@ export function LlmSettingFieldItems({
const form = useFormContext();
const { t } = useTranslate('chat');
const CollapseComponent = showCollapse ? Collapse : React.Fragment;
const collapseProps = showCollapse
? {
title: t('modelSetting'),
}
: {};
const getFieldWithPrefix = useCallback(
(name: string) => {
return prefix ? `${prefix}.${name}` : name;
@@ -149,6 +142,141 @@ export function LlmSettingFieldItems({
}
};
const settingFields = (
<section className="space-y-5">
<FormField
control={form.control}
name={getFieldWithPrefix('parameter')}
render={({ field }) => (
<FormItem className="flex justify-between items-center">
<FormLabel className="flex-1">{t('freedom')}</FormLabel>
<FormControl>
<Select
value={field.value}
onValueChange={(val) => {
handleChange(val);
field.onChange(val);
}}
>
<SelectTrigger className="flex-1 !m-0">
<SelectValue />
</SelectTrigger>
<SelectContent>
{parameterOptions.map((x) => (
<SelectItem value={x.value} key={x.value}>
{x.label}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{showFields.some((item) => item === 'temperature') && (
<SliderInputSwitchFormField
name={getFieldWithPrefix('temperature')}
checkName="temperatureEnabled"
label="temperature"
max={1}
step={0.01}
min={0}
onChange={() => {
checkParameterIsEqual();
}}
></SliderInputSwitchFormField>
)}
{showFields.some((item) => item === 'top_p') && (
<SliderInputSwitchFormField
name={getFieldWithPrefix('top_p')}
checkName="topPEnabled"
label="topP"
max={1}
step={0.01}
min={0}
onChange={() => {
checkParameterIsEqual();
}}
></SliderInputSwitchFormField>
)}
{showFields.some((item) => item === 'presence_penalty') && (
<SliderInputSwitchFormField
name={getFieldWithPrefix('presence_penalty')}
checkName="presencePenaltyEnabled"
label="presencePenalty"
max={1}
step={0.01}
min={0}
onChange={() => {
checkParameterIsEqual();
}}
></SliderInputSwitchFormField>
)}
{showFields.some((item) => item === 'frequency_penalty') && (
<SliderInputSwitchFormField
name={getFieldWithPrefix('frequency_penalty')}
checkName="frequencyPenaltyEnabled"
label="frequencyPenalty"
max={1}
step={0.01}
min={0}
onChange={() => {
checkParameterIsEqual();
}}
></SliderInputSwitchFormField>
)}
{showFields.some((item) => item === 'max_tokens') && (
<SliderInputSwitchFormField
name={getFieldWithPrefix('max_tokens')}
checkName="maxTokensEnabled"
numberInputClassName="w-20"
label="maxTokens"
max={128000}
min={0}
onChange={() => {
checkParameterIsEqual();
}}
></SliderInputSwitchFormField>
)}
{showFields.some((item) => item === 'thinking') && (
<FormField
control={form.control}
name={getFieldWithPrefix('thinking')}
render={({ field }) => (
<FormItem className="flex justify-between items-center">
<FormLabel className="flex-1" tooltip={t('thinkingTip')}>
{t('thinking')}
</FormLabel>
<FormControl>
<Select
value={field.value ?? 'default'}
onValueChange={field.onChange}
>
<SelectTrigger className="flex-1 !m-0">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">
{t('thinkingDefault')}
</SelectItem>
<SelectItem value="enabled">
{t('thinkingEnabled')}
</SelectItem>
<SelectItem value="disabled">
{t('thinkingDisabled')}
</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
</section>
);
return (
<div className="space-y-5">
<LLMFormField
@@ -158,140 +286,11 @@ export function LlmSettingFieldItems({
optionTestIdPrefix={llmOptionTestIdPrefix}
ownerTenantId={ownerTenantId}
></LLMFormField>
<CollapseComponent {...collapseProps}>
<section className="space-y-5">
<FormField
control={form.control}
name={getFieldWithPrefix('parameter')}
render={({ field }) => (
<FormItem className="flex justify-between items-center">
<FormLabel className="flex-1">{t('freedom')}</FormLabel>
<FormControl>
<Select
value={field.value}
onValueChange={(val) => {
handleChange(val);
field.onChange(val);
}}
>
<SelectTrigger className="flex-1 !m-0">
<SelectValue />
</SelectTrigger>
<SelectContent>
{parameterOptions.map((x) => (
<SelectItem value={x.value} key={x.value}>
{x.label}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{showFields.some((item) => item === 'temperature') && (
<SliderInputSwitchFormField
name={getFieldWithPrefix('temperature')}
checkName="temperatureEnabled"
label="temperature"
max={1}
step={0.01}
min={0}
onChange={() => {
checkParameterIsEqual();
}}
></SliderInputSwitchFormField>
)}
{showFields.some((item) => item === 'top_p') && (
<SliderInputSwitchFormField
name={getFieldWithPrefix('top_p')}
checkName="topPEnabled"
label="topP"
max={1}
step={0.01}
min={0}
onChange={() => {
checkParameterIsEqual();
}}
></SliderInputSwitchFormField>
)}
{showFields.some((item) => item === 'presence_penalty') && (
<SliderInputSwitchFormField
name={getFieldWithPrefix('presence_penalty')}
checkName="presencePenaltyEnabled"
label="presencePenalty"
max={1}
step={0.01}
min={0}
onChange={() => {
checkParameterIsEqual();
}}
></SliderInputSwitchFormField>
)}
{showFields.some((item) => item === 'frequency_penalty') && (
<SliderInputSwitchFormField
name={getFieldWithPrefix('frequency_penalty')}
checkName="frequencyPenaltyEnabled"
label="frequencyPenalty"
max={1}
step={0.01}
min={0}
onChange={() => {
checkParameterIsEqual();
}}
></SliderInputSwitchFormField>
)}
{showFields.some((item) => item === 'max_tokens') && (
<SliderInputSwitchFormField
name={getFieldWithPrefix('max_tokens')}
checkName="maxTokensEnabled"
numberInputClassName="w-20"
label="maxTokens"
max={128000}
min={0}
onChange={() => {
checkParameterIsEqual();
}}
></SliderInputSwitchFormField>
)}
{showFields.some((item) => item === 'thinking') && (
<FormField
control={form.control}
name={getFieldWithPrefix('thinking')}
render={({ field }) => (
<FormItem className="flex justify-between items-center">
<FormLabel className="flex-1" tooltip={t('thinkingTip')}>
{t('thinking')}
</FormLabel>
<FormControl>
<Select
value={field.value ?? 'default'}
onValueChange={field.onChange}
>
<SelectTrigger className="flex-1 !m-0">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">
{t('thinkingDefault')}
</SelectItem>
<SelectItem value="enabled">
{t('thinkingEnabled')}
</SelectItem>
<SelectItem value="disabled">
{t('thinkingDisabled')}
</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
</section>
</CollapseComponent>
{showCollapse ? (
<Collapse title={t('modelSetting')}>{settingFields}</Collapse>
) : (
settingFields
)}
</div>
);
}

View File

@@ -10,7 +10,7 @@ import {
import { useFetchAllAddedModels } from '@/hooks/use-llm-request';
import { IAddedModel } from '@/interfaces/database/llm';
import { buildModelValue, getRealModelName } from '@/utils/llm-util';
import { useCallback, useMemo } from 'react';
import { forwardRef, useCallback, useMemo } from 'react';
import { useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { TreeSelect, TreeSelectNode } from './tree-select';
@@ -116,19 +116,25 @@ export interface ModelTreeSelectProps {
ownerTenantId?: string;
}
export function ModelTreeSelect({
modelTypes = ModelTypeMap.llm_id,
value,
onChange,
disabled,
placeholder,
showSearch = true,
allowClear = false,
className,
renderSelected,
testId,
ownerTenantId,
}: ModelTreeSelectProps) {
export const ModelTreeSelect = forwardRef<
HTMLButtonElement,
ModelTreeSelectProps
>(function ModelTreeSelect(
{
modelTypes = ModelTypeMap.llm_id,
value,
onChange,
disabled,
placeholder,
showSearch = true,
allowClear = false,
className,
renderSelected,
testId,
ownerTenantId,
},
ref,
) {
const { data: allAddedModels } = useFetchAllAddedModels(
undefined,
ownerTenantId,
@@ -185,6 +191,7 @@ export function ModelTreeSelect({
return (
<TreeSelect
ref={ref}
data={treeData}
value={normalizedValue}
onChange={onChange}
@@ -198,7 +205,7 @@ export function ModelTreeSelect({
testId={testId}
/>
);
}
});
export interface ModelTreeSelectFormFieldProps extends ModelTreeSelectProps {
name?: string;

View File

@@ -5,7 +5,7 @@ import {
} from '@/components/ui/popover';
import { cn } from '@/lib/utils';
import { ChevronDown, ChevronRight, Search, X } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
export interface TreeSelectNode {
@@ -31,239 +31,247 @@ interface TreeSelectProps {
testId?: string;
}
export function TreeSelect({
data,
value,
onChange,
placeholder,
disabled,
allowClear,
showSearch,
className,
defaultExpandAll,
renderSelected,
testId,
}: TreeSelectProps) {
const [open, setOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
const { t } = useTranslation();
useEffect(() => {
if (!defaultExpandAll) return;
const ids = new Set<string>();
const walk = (nodes: TreeSelectNode[]) => {
for (const node of nodes) {
if (node.children?.length) {
ids.add(node.id);
walk(node.children);
}
}
};
walk(data);
setExpandedIds(ids);
}, [data, defaultExpandAll]);
const selectedNode = useMemo(() => {
const find = (nodes: TreeSelectNode[]): TreeSelectNode | undefined => {
for (const node of nodes) {
if (node.id === value && value) return node;
if (node.children) {
const found = find(node.children);
if (found) return found;
}
}
};
return find(data);
}, [data, value]);
const isLeaf = useCallback(
(node: TreeSelectNode) => !node.children?.length,
[],
);
const handleToggle = useCallback((id: string) => {
setExpandedIds((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
}, []);
const handleSelect = useCallback(
(node: TreeSelectNode) => {
if (node.disabled) return;
if (isLeaf(node)) {
onChange?.(node.id);
setOpen(false);
setSearchTerm('');
} else {
handleToggle(node.id);
}
export const TreeSelect = forwardRef<HTMLButtonElement, TreeSelectProps>(
function TreeSelect(
{
data,
value,
onChange,
placeholder,
disabled,
allowClear,
showSearch,
className,
defaultExpandAll,
renderSelected,
testId,
},
[isLeaf, onChange, handleToggle],
);
ref,
) {
const [open, setOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
const { t } = useTranslation();
const handleClear = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation();
onChange?.('');
},
[onChange],
);
const filterTree = useCallback(
(nodes: TreeSelectNode[], term: string): TreeSelectNode[] => {
if (!term) return nodes;
return nodes.reduce<TreeSelectNode[]>((acc, node) => {
const titleMatch = node.title
.toLowerCase()
.includes(term.toLowerCase());
const filteredChildren = node.children
? filterTree(node.children, term)
: undefined;
if (titleMatch || filteredChildren?.length) {
acc.push({ ...node, children: filteredChildren ?? node.children });
useEffect(() => {
if (!defaultExpandAll) return;
const ids = new Set<string>();
const walk = (nodes: TreeSelectNode[]) => {
for (const node of nodes) {
if (node.children?.length) {
ids.add(node.id);
walk(node.children);
}
}
return acc;
}, []);
},
[],
);
};
walk(data);
setExpandedIds(ids);
}, [data, defaultExpandAll]);
const filteredData = useMemo(
() => filterTree(data, searchTerm),
[data, searchTerm, filterTree],
);
const visibleExpandedIds = useMemo(() => {
if (!searchTerm) return expandedIds;
const ids = new Set<string>();
const walk = (nodes: TreeSelectNode[]) => {
for (const node of nodes) {
if (node.children?.length) {
ids.add(node.id);
walk(node.children);
const selectedNode = useMemo(() => {
const find = (nodes: TreeSelectNode[]): TreeSelectNode | undefined => {
for (const node of nodes) {
if (node.id === value && value) return node;
if (node.children) {
const found = find(node.children);
if (found) return found;
}
}
}
};
walk(filteredData);
return ids;
}, [searchTerm, expandedIds, filteredData]);
};
return find(data);
}, [data, value]);
const renderTree = useCallback(
(nodes: TreeSelectNode[], level = 0): React.ReactNode => {
return nodes.map((node) => {
const leaf = isLeaf(node);
const expanded = visibleExpandedIds.has(node.id);
const selected = value === node.id;
const isLeaf = useCallback(
(node: TreeSelectNode) => !node.children?.length,
[],
);
return (
<div key={node.id}>
<div
className={cn(
'flex items-center rounded-sm cursor-pointer text-sm',
'hover:bg-accent transition-colors',
!leaf && 'text-text-primary font-medium',
selected && 'bg-accent text-accent-foreground',
node.disabled && 'opacity-50 pointer-events-none',
)}
style={{
paddingLeft: `${level * 20 + 4}px`,
paddingRight: '8px',
paddingTop: '6px',
paddingBottom: '6px',
}}
onClick={() => handleSelect(node)}
>
<span className="w-4 h-4 mr-0.5 flex-shrink-0 flex items-center justify-center">
{!leaf && (
<>
{expanded ? (
<ChevronDown className="h-3.5 w-3.5 text-text-secondary" />
) : (
<ChevronRight className="h-3.5 w-3.5 text-text-secondary" />
)}
</>
)}
</span>
<span className="truncate">{node.label ?? node.title}</span>
</div>
{!leaf && expanded && node.children && (
<div>{renderTree(node.children, level + 1)}</div>
)}
</div>
);
const handleToggle = useCallback((id: string) => {
setExpandedIds((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
},
[isLeaf, visibleExpandedIds, value, handleSelect],
);
}, []);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild disabled={disabled}>
<button
type="button"
data-testid={testId}
className={cn(
'flex items-center justify-between border border-border-button rounded-md px-3 py-1.5 w-full',
'bg-bg-input text-sm',
'hover:bg-border-button transition-colors',
disabled && 'opacity-50 cursor-not-allowed',
className,
)}
>
<span className={cn('truncate', !selectedNode && 'text-slate-400')}>
{renderSelected
? renderSelected(selectedNode)
: selectedNode?.title || placeholder || t('common.pleaseSelect')}
</span>
<div className="flex items-center ml-2 flex-shrink-0">
{allowClear && value ? (
<X
className="h-4 w-4 opacity-50 hover:opacity-100"
onClick={handleClear}
/>
) : (
<ChevronDown className="h-4 w-4 opacity-50" />
const handleSelect = useCallback(
(node: TreeSelectNode) => {
if (node.disabled) return;
if (isLeaf(node)) {
onChange?.(node.id);
setOpen(false);
setSearchTerm('');
} else {
handleToggle(node.id);
}
},
[isLeaf, onChange, handleToggle],
);
const handleClear = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation();
onChange?.('');
},
[onChange],
);
const filterTree = useCallback(
(nodes: TreeSelectNode[], term: string): TreeSelectNode[] => {
if (!term) return nodes;
return nodes.reduce<TreeSelectNode[]>((acc, node) => {
const titleMatch = node.title
.toLowerCase()
.includes(term.toLowerCase());
const filteredChildren = node.children
? filterTree(node.children, term)
: undefined;
if (titleMatch || filteredChildren?.length) {
acc.push({ ...node, children: filteredChildren ?? node.children });
}
return acc;
}, []);
},
[],
);
const filteredData = useMemo(
() => filterTree(data, searchTerm),
[data, searchTerm, filterTree],
);
const visibleExpandedIds = useMemo(() => {
if (!searchTerm) return expandedIds;
const ids = new Set<string>();
const walk = (nodes: TreeSelectNode[]) => {
for (const node of nodes) {
if (node.children?.length) {
ids.add(node.id);
walk(node.children);
}
}
};
walk(filteredData);
return ids;
}, [searchTerm, expandedIds, filteredData]);
const renderTree = useCallback(
(nodes: TreeSelectNode[], level = 0): React.ReactNode => {
return nodes.map((node) => {
const leaf = isLeaf(node);
const expanded = visibleExpandedIds.has(node.id);
const selected = value === node.id;
return (
<div key={node.id}>
<div
className={cn(
'flex items-center rounded-sm cursor-pointer text-sm',
'hover:bg-accent transition-colors',
!leaf && 'text-text-primary font-medium',
selected && 'bg-accent text-accent-foreground',
node.disabled && 'opacity-50 pointer-events-none',
)}
style={{
paddingLeft: `${level * 20 + 4}px`,
paddingRight: '8px',
paddingTop: '6px',
paddingBottom: '6px',
}}
onClick={() => handleSelect(node)}
>
<span className="w-4 h-4 mr-0.5 flex-shrink-0 flex items-center justify-center">
{!leaf && (
<>
{expanded ? (
<ChevronDown className="h-3.5 w-3.5 text-text-secondary" />
) : (
<ChevronRight className="h-3.5 w-3.5 text-text-secondary" />
)}
</>
)}
</span>
<span className="truncate">{node.label ?? node.title}</span>
</div>
{!leaf && expanded && node.children && (
<div>{renderTree(node.children, level + 1)}</div>
)}
</div>
);
});
},
[isLeaf, visibleExpandedIds, value, handleSelect],
);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild disabled={disabled}>
<button
ref={ref}
type="button"
data-testid={testId}
className={cn(
'flex items-center justify-between border border-border-button rounded-md px-3 py-1.5 w-full',
'bg-bg-input text-sm',
'hover:bg-border-button transition-colors',
disabled && 'opacity-50 cursor-not-allowed',
className,
)}
</div>
</button>
</PopoverTrigger>
<PopoverContent
className="p-0 w-auto min-w-[var(--radix-popover-trigger-width)]"
align="start"
sideOffset={4}
>
{showSearch && (
<div className="flex items-center border-b px-3 py-2">
<Search className="h-4 w-4 text-slate-400 mr-2 flex-shrink-0" />
<input
type="text"
className="flex-1 bg-transparent text-sm outline-none placeholder:text-slate-400"
placeholder={t('common.search')}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
)}
<div
className="max-h-60 overflow-auto p-1"
onWheel={(e) => e.stopPropagation()}
>
<span className={cn('truncate', !selectedNode && 'text-slate-400')}>
{renderSelected
? renderSelected(selectedNode)
: selectedNode?.title ||
placeholder ||
t('common.pleaseSelect')}
</span>
<div className="flex items-center ml-2 flex-shrink-0">
{allowClear && value ? (
<X
className="h-4 w-4 opacity-50 hover:opacity-100"
onClick={handleClear}
/>
) : (
<ChevronDown className="h-4 w-4 opacity-50" />
)}
</div>
</button>
</PopoverTrigger>
<PopoverContent
className="p-0 w-auto min-w-[var(--radix-popover-trigger-width)]"
align="start"
sideOffset={4}
>
{filteredData.length > 0 ? (
renderTree(filteredData)
) : (
<div className="py-6 text-center text-sm text-slate-400">
{t('common.noData')}
{showSearch && (
<div className="flex items-center border-b px-3 py-2">
<Search className="h-4 w-4 text-slate-400 mr-2 flex-shrink-0" />
<input
type="text"
className="flex-1 bg-transparent text-sm outline-none placeholder:text-slate-400"
placeholder={t('common.search')}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
)}
</div>
</PopoverContent>
</Popover>
);
}
<div
className="max-h-60 overflow-auto p-1"
onWheel={(e) => e.stopPropagation()}
>
{filteredData.length > 0 ? (
renderTree(filteredData)
) : (
<div className="py-6 text-center text-sm text-slate-400">
{t('common.noData')}
</div>
)}
</div>
</PopoverContent>
</Popover>
);
},
);

View File

@@ -54,13 +54,13 @@ function InnerToolNode({
/>
<NodeCollapsible items={[tools, mcpList]}>
{(x) => {
{(x, idx) => {
if (Reflect.has(x, 'mcp_id')) {
const mcp = x as unknown as IAgentForm['mcp'][number];
return (
<ToolCard
key={mcp.mcp_id}
key={mcp.mcp_id || `mcp-${idx}`}
onClick={(e) => {
if (mcp.mcp_id === Operator.Code) {
e.preventDefault();
@@ -79,7 +79,7 @@ function InnerToolNode({
return (
<ToolCard
key={tool.id}
key={tool.id || `tool-${idx}`}
onClick={(e) => {
if (tool.component_name === Operator.Code) {
e.preventDefault();

View File

@@ -39,25 +39,21 @@ export function ToolCard({
isNodeTool = true,
...props
}: ToolCardProps) {
const element = useMemo(() => {
return (
<LabelCard
{...props}
className={cn(
'flex justify-between ',
{ 'p-2.5 text-text-primary text-sm': !isNodeTool },
className,
)}
>
{children}
</LabelCard>
);
}, [children, className, isNodeTool, props]);
if (children === Operator.Code) {
return (
<Tooltip>
<TooltipTrigger asChild>{element}</TooltipTrigger>
<TooltipTrigger asChild>
<LabelCard
{...props}
className={cn(
'flex justify-between ',
{ 'p-2.5 text-text-primary text-sm': !isNodeTool },
className,
)}
>
{children}
</LabelCard>
</TooltipTrigger>
<TooltipContent>
<p>It doesn't have any config.</p>
</TooltipContent>
@@ -65,7 +61,18 @@ export function ToolCard({
);
}
return element;
return (
<LabelCard
{...props}
className={cn(
'flex justify-between ',
{ 'p-2.5 text-text-primary text-sm': !isNodeTool },
className,
)}
>
{children}
</LabelCard>
);
}
type ActionButtonProps<T> = {
@@ -131,22 +138,22 @@ export function AgentTools() {
<section className="space-y-2.5">
<span className="text-text-secondary text-sm">{t('flow.tools')}</span>
<ul className="space-y-2.5">
{tools.map(({ id, component_name, name }) => (
<ToolCard key={id} isNodeTool={false}>
{tools.map(({ id, component_name, name }, idx) => (
<ToolCard key={id || `tool-${idx}`} isNodeTool={false}>
<div className="flex gap-2 items-center">
<OperatorIcon name={component_name as Operator}></OperatorIcon>
{component_name === Operator.Retrieval ? name : component_name}
</div>
<ActionButton
record={id}
record={id || component_name}
deleteRecord={deleteNodeTool(id)}
edit={handleEdit}
/>
</ToolCard>
))}
{mcpIds.map((id) => (
<ToolCard key={id} isNodeTool={false}>
{mcpIds.map((id, idx) => (
<ToolCard key={id || `mcp-${idx}`} isNodeTool={false}>
{findMcpById(id)?.name}
<ActionButton
record={id}