mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-06 07:34:04 +08:00
Fix: The tool icon for the agent operator created from the template is not clickable. (#17794)
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user