mirror of
https://github.com/twentyhq/twenty
synced 2026-04-21 13:37:22 +00:00
- 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" />
253 lines
8 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
};
|