twenty/packages/twenty-front/src/pages/settings/ai/components/SettingsToolsTable.tsx
martmull da064d5e88
Support define is tool logic function (#17926)
- supports isTool and timeout settings in defineLogicFunction in apps
and in setting tabs definition
- compute for all toolInputSchema for logic funciton, in settings and in
code steps

<img width="991" height="802" alt="image"
src="https://github.com/user-attachments/assets/05dc1221-cac9-45a3-87b0-3b13161446fd"
/>
2026-02-16 10:43:29 +01:00

253 lines
8 KiB
TypeScript

import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useMemo, useState } from 'react';
import Skeleton from 'react-loading-skeleton';
import { useRecoilValue } from 'recoil';
import { useGetToolIndex } from '@/ai/hooks/useGetToolIndex';
import { usePersistLogicFunction } from '@/logic-functions/hooks/usePersistLogicFunction';
import { logicFunctionsState } from '@/settings/logic-functions/states/logicFunctionsState';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { Table } from '@/ui/layout/table/components/Table';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { useTheme } from '@emotion/react';
import { useNavigate } from 'react-router-dom';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import {
H2Title,
IconChevronRight,
IconPlus,
IconSearch,
} from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { normalizeSearchText } from '~/utils/normalizeSearchText';
import { SettingsSystemToolTableRow } from './SettingsSystemToolTableRow';
import {
SettingsToolTableRow,
StyledToolTableRow,
} from './SettingsToolTableRow';
const StyledSearchAndFilterContainer = styled.div`
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
align-items: center;
padding-bottom: ${({ theme }) => theme.spacing(2)};
`;
const StyledSearchInput = styled(SettingsTextInput)`
flex: 1;
width: 100%;
`;
const StyledTableHeaderRow = styled(StyledToolTableRow)`
margin-bottom: ${({ theme }) => theme.spacing(2)};
`;
const StyledFooterContainer = styled.div`
align-items: center;
display: flex;
justify-content: flex-end;
margin-top: ${({ theme }) => theme.spacing(4)};
`;
export const SettingsToolsTable = () => {
const logicFunctions = useRecoilValue(logicFunctionsState);
const { toolIndex, loading: toolIndexLoading } = useGetToolIndex();
const { createLogicFunction } = usePersistLogicFunction();
const { t } = useLingui();
const theme = useTheme();
const navigate = useNavigate();
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
const [customSearchTerm, setCustomSearchTerm] = useState('');
const [builtInSearchTerm, setBuiltInSearchTerm] = useState('');
const [isCreating, setIsCreating] = useState(false);
// Filter to only show logic functions that are marked as tools
const tools = useMemo(
() => logicFunctions.filter((fn) => fn.isTool === true),
[logicFunctions],
);
const filteredTools = useMemo(
() =>
tools.filter((tool) => {
const searchNormalized = normalizeSearchText(customSearchTerm);
const matchesSearch =
normalizeSearchText(tool.name).includes(searchNormalized) ||
normalizeSearchText(tool.description ?? '').includes(
searchNormalized,
);
return matchesSearch;
}),
[tools, customSearchTerm],
);
// System tools from the tool index (excluding logic function tools which are shown separately)
const systemTools = useMemo(
() =>
toolIndex.filter(
(systemTool) => systemTool.category !== 'LOGIC_FUNCTION',
),
[toolIndex],
);
const filteredSystemTools = useMemo(
() =>
systemTools.filter((systemTool) => {
const searchNormalized = normalizeSearchText(builtInSearchTerm);
const matchesSearch =
normalizeSearchText(systemTool.name).includes(searchNormalized) ||
normalizeSearchText(systemTool.description).includes(
searchNormalized,
);
return matchesSearch;
}),
[systemTools, builtInSearchTerm],
);
const showSkeleton = toolIndexLoading && tools.length === 0;
const handleCreateTool = async () => {
setIsCreating(true);
try {
const result = await createLogicFunction({
input: {
name: 'new-tool',
isTool: true,
},
});
if (result.status === 'successful' && isDefined(result.response?.data)) {
const newLogicFunction = result.response.data.createOneLogicFunction;
enqueueSuccessSnackBar({ message: t`Tool created` });
// Navigate to the logic function detail page
// The applicationId might be null for workspace-level functions
const applicationId = (newLogicFunction as { applicationId?: string })
.applicationId;
if (isDefined(applicationId)) {
navigate(
getSettingsPath(SettingsPath.ApplicationLogicFunctionDetail, {
applicationId,
logicFunctionId: newLogicFunction.id,
}),
);
} else {
navigate(
getSettingsPath(SettingsPath.LogicFunctionDetail, {
logicFunctionId: newLogicFunction.id,
}),
);
}
}
} catch {
enqueueErrorSnackBar({ message: t`Failed to create tool` });
} finally {
setIsCreating(false);
}
};
const getToolLink = (tool: (typeof tools)[0]) => {
const applicationId = (tool as { applicationId?: string }).applicationId;
if (isDefined(applicationId)) {
return getSettingsPath(SettingsPath.ApplicationLogicFunctionDetail, {
applicationId,
logicFunctionId: tool.id,
});
}
return getSettingsPath(SettingsPath.LogicFunctionDetail, {
logicFunctionId: tool.id,
});
};
return (
<>
<Section>
<H2Title
title={t`Custom`}
description={t`Custom tools created in your workspace`}
/>
<StyledSearchAndFilterContainer>
<StyledSearchInput
instanceId="custom-tool-table-search"
LeftIcon={IconSearch}
placeholder={t`Search a custom tool...`}
value={customSearchTerm}
onChange={setCustomSearchTerm}
/>
</StyledSearchAndFilterContainer>
<Table>
<StyledTableHeaderRow>
<TableHeader>{t`Name`}</TableHeader>
<TableHeader align="right">{t`Type`}</TableHeader>
<TableHeader />
</StyledTableHeaderRow>
{showSkeleton
? Array.from({ length: 3 }).map((_, index) => (
<Skeleton height={32} borderRadius={4} key={index} />
))
: filteredTools.map((tool) => (
<SettingsToolTableRow
key={tool.id}
tool={tool}
action={
<IconChevronRight
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
/>
}
link={getToolLink(tool)}
/>
))}
</Table>
<StyledFooterContainer>
<Button
Icon={IconPlus}
title={t`New Tool`}
size="small"
variant="secondary"
onClick={handleCreateTool}
disabled={isCreating}
/>
</StyledFooterContainer>
</Section>
<Section>
<H2Title
title={t`Built-in`}
description={t`Standard tools available to AI agents`}
/>
<StyledSearchAndFilterContainer>
<StyledSearchInput
instanceId="builtin-tool-table-search"
LeftIcon={IconSearch}
placeholder={t`Search a built-in tool...`}
value={builtInSearchTerm}
onChange={setBuiltInSearchTerm}
/>
</StyledSearchAndFilterContainer>
<Table>
<StyledTableHeaderRow>
<TableHeader>{t`Name`}</TableHeader>
<TableHeader align="right">{t`Type`}</TableHeader>
<TableHeader />
</StyledTableHeaderRow>
{filteredSystemTools.map((systemTool) => (
<SettingsSystemToolTableRow
key={systemTool.name}
tool={systemTool}
/>
))}
</Table>
</Section>
</>
);
};