[AI] Improve tools tab (#19221)
This commit is contained in:
@@ -2653,6 +2653,7 @@ type ToolIndexEntry {
|
||||
description: String!
|
||||
category: String!
|
||||
objectName: String
|
||||
icon: String
|
||||
inputSchema: JSON
|
||||
}
|
||||
|
||||
|
||||
@@ -2297,6 +2297,7 @@ export interface ToolIndexEntry {
|
||||
description: Scalars['String']
|
||||
category: Scalars['String']
|
||||
objectName?: Scalars['String']
|
||||
icon?: Scalars['String']
|
||||
inputSchema?: Scalars['JSON']
|
||||
__typename: 'ToolIndexEntry'
|
||||
}
|
||||
@@ -5475,6 +5476,7 @@ export interface ToolIndexEntryGenqlSelection{
|
||||
description?: boolean | number
|
||||
category?: boolean | number
|
||||
objectName?: boolean | number
|
||||
icon?: boolean | number
|
||||
inputSchema?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
|
||||
@@ -5179,6 +5179,9 @@ export default {
|
||||
"objectName": [
|
||||
1
|
||||
],
|
||||
"icon": [
|
||||
1
|
||||
],
|
||||
"inputSchema": [
|
||||
15
|
||||
],
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
@@ -5141,6 +5141,7 @@ export type ToolIndexEntry = {
|
||||
__typename?: 'ToolIndexEntry';
|
||||
category: Scalars['String'];
|
||||
description: Scalars['String'];
|
||||
icon?: Maybe<Scalars['String']>;
|
||||
inputSchema?: Maybe<Scalars['JSON']>;
|
||||
name: Scalars['String'];
|
||||
objectName?: Maybe<Scalars['String']>;
|
||||
@@ -6404,7 +6405,7 @@ export type GetChatThreadsQuery = { __typename?: 'Query', chatThreads: { __typen
|
||||
export type GetToolIndexQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type GetToolIndexQuery = { __typename?: 'Query', getToolIndex: Array<{ __typename?: 'ToolIndexEntry', name: string, description: string, category: string, objectName?: string | null }> };
|
||||
export type GetToolIndexQuery = { __typename?: 'Query', getToolIndex: Array<{ __typename?: 'ToolIndexEntry', name: string, description: string, category: string, objectName?: string | null, icon?: string | null }> };
|
||||
|
||||
export type GetToolInputSchemaQueryVariables = Exact<{
|
||||
toolName: Scalars['String'];
|
||||
@@ -8261,7 +8262,7 @@ export const FindOneSkillDocument = {"kind":"Document","definitions":[{"kind":"O
|
||||
export const GetAgentTurnsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAgentTurns"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"agentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"agentTurns"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"agentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"agentId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"threadId"}},{"kind":"Field","name":{"kind":"Name","value":"agentId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"evaluations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"score"}},{"kind":"Field","name":{"kind":"Name","value":"comment"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"messages"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"parts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"textContent"}},{"kind":"Field","name":{"kind":"Name","value":"reasoningContent"}},{"kind":"Field","name":{"kind":"Name","value":"toolName"}},{"kind":"Field","name":{"kind":"Name","value":"toolCallId"}},{"kind":"Field","name":{"kind":"Name","value":"toolInput"}},{"kind":"Field","name":{"kind":"Name","value":"toolOutput"}},{"kind":"Field","name":{"kind":"Name","value":"errorMessage"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"errorDetails"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrlSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrlUrl"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrlTitle"}},{"kind":"Field","name":{"kind":"Name","value":"sourceDocumentSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"sourceDocumentMediaType"}},{"kind":"Field","name":{"kind":"Name","value":"sourceDocumentTitle"}},{"kind":"Field","name":{"kind":"Name","value":"sourceDocumentFilename"}},{"kind":"Field","name":{"kind":"Name","value":"fileMediaType"}},{"kind":"Field","name":{"kind":"Name","value":"fileFilename"}},{"kind":"Field","name":{"kind":"Name","value":"fileUrl"}},{"kind":"Field","name":{"kind":"Name","value":"providerMetadata"}}]}}]}}]}}]}}]} as unknown as DocumentNode<GetAgentTurnsQuery, GetAgentTurnsQueryVariables>;
|
||||
export const GetChatMessagesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetChatMessages"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"chatMessages"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"threadId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"threadId"}},{"kind":"Field","name":{"kind":"Name","value":"turnId"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"parts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"messageId"}},{"kind":"Field","name":{"kind":"Name","value":"orderIndex"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"textContent"}},{"kind":"Field","name":{"kind":"Name","value":"reasoningContent"}},{"kind":"Field","name":{"kind":"Name","value":"toolName"}},{"kind":"Field","name":{"kind":"Name","value":"toolCallId"}},{"kind":"Field","name":{"kind":"Name","value":"toolInput"}},{"kind":"Field","name":{"kind":"Name","value":"toolOutput"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"errorMessage"}},{"kind":"Field","name":{"kind":"Name","value":"errorDetails"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrlSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrlUrl"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrlTitle"}},{"kind":"Field","name":{"kind":"Name","value":"sourceDocumentSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"sourceDocumentMediaType"}},{"kind":"Field","name":{"kind":"Name","value":"sourceDocumentTitle"}},{"kind":"Field","name":{"kind":"Name","value":"sourceDocumentFilename"}},{"kind":"Field","name":{"kind":"Name","value":"fileMediaType"}},{"kind":"Field","name":{"kind":"Name","value":"fileFilename"}},{"kind":"Field","name":{"kind":"Name","value":"fileUrl"}},{"kind":"Field","name":{"kind":"Name","value":"fileId"}},{"kind":"Field","name":{"kind":"Name","value":"providerMetadata"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"chatStreamCatchupChunks"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"threadId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"chunks"}},{"kind":"Field","name":{"kind":"Name","value":"maxSeq"}}]}}]}}]} as unknown as DocumentNode<GetChatMessagesQuery, GetChatMessagesQueryVariables>;
|
||||
export const GetChatThreadsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetChatThreads"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"paging"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"CursorPaging"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"chatThreads"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"Variable","name":{"kind":"Name","value":"paging"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"totalInputTokens"}},{"kind":"Field","name":{"kind":"Name","value":"totalOutputTokens"}},{"kind":"Field","name":{"kind":"Name","value":"contextWindowTokens"}},{"kind":"Field","name":{"kind":"Name","value":"conversationSize"}},{"kind":"Field","name":{"kind":"Name","value":"totalInputCredits"}},{"kind":"Field","name":{"kind":"Name","value":"totalOutputCredits"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"endCursor"}},{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}}]}}]}}]}}]} as unknown as DocumentNode<GetChatThreadsQuery, GetChatThreadsQueryVariables>;
|
||||
export const GetToolIndexDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetToolIndex"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getToolIndex"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"objectName"}}]}}]}}]} as unknown as DocumentNode<GetToolIndexQuery, GetToolIndexQueryVariables>;
|
||||
export const GetToolIndexDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetToolIndex"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getToolIndex"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"objectName"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}}]}}]}}]} as unknown as DocumentNode<GetToolIndexQuery, GetToolIndexQueryVariables>;
|
||||
export const GetToolInputSchemaDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetToolInputSchema"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"toolName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getToolInputSchema"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"toolName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"toolName"}}}]}]}}]} as unknown as DocumentNode<GetToolInputSchemaQuery, GetToolInputSchemaQueryVariables>;
|
||||
export const OnAgentChatEventDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"OnAgentChatEvent"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"onAgentChatEvent"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"threadId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"threadId"}},{"kind":"Field","name":{"kind":"Name","value":"event"}}]}}]}}]} as unknown as DocumentNode<OnAgentChatEventSubscription, OnAgentChatEventSubscriptionVariables>;
|
||||
export const TrackAnalyticsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"TrackAnalytics"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"type"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"AnalyticsType"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"event"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"name"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"properties"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"JSON"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"trackAnalytics"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"type"},"value":{"kind":"Variable","name":{"kind":"Name","value":"type"}}},{"kind":"Argument","name":{"kind":"Name","value":"event"},"value":{"kind":"Variable","name":{"kind":"Name","value":"event"}}},{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}},{"kind":"Argument","name":{"kind":"Name","value":"properties"},"value":{"kind":"Variable","name":{"kind":"Name","value":"properties"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}}]}}]}}]} as unknown as DocumentNode<TrackAnalyticsMutation, TrackAnalyticsMutationVariables>;
|
||||
|
||||
@@ -7,6 +7,7 @@ export const GET_TOOL_INDEX = gql`
|
||||
description
|
||||
category
|
||||
objectName
|
||||
icon
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
import { GET_TOOL_INDEX } from '@/ai/graphql/queries/getToolIndex';
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
|
||||
type ToolIndexEntry = {
|
||||
name: string;
|
||||
description: string;
|
||||
category: string;
|
||||
objectName?: string;
|
||||
};
|
||||
|
||||
type GetToolIndexQuery = {
|
||||
getToolIndex: ToolIndexEntry[];
|
||||
};
|
||||
import { type GetToolIndexQuery } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useGetToolIndex = () => {
|
||||
const { data, loading, error } = useQuery<GetToolIndexQuery>(GET_TOOL_INDEX);
|
||||
|
||||
@@ -161,6 +161,12 @@ const SettingsAIUsageUserDetail = lazy(() =>
|
||||
})),
|
||||
);
|
||||
|
||||
const SettingsToolDetail = lazy(() =>
|
||||
import('~/pages/settings/ai/SettingsToolDetail').then((module) => ({
|
||||
default: module.SettingsToolDetail,
|
||||
})),
|
||||
);
|
||||
|
||||
const SettingsApplications = lazy(() =>
|
||||
import('~/pages/settings/applications/SettingsApplications').then(
|
||||
(module) => ({
|
||||
@@ -546,6 +552,10 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => (
|
||||
path={SettingsPath.AIUsageUserDetail}
|
||||
element={<SettingsAIUsageUserDetail />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.AIToolDetail}
|
||||
element={<SettingsToolDetail />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.LogicFunctionDetail}
|
||||
element={<SettingsLogicFunctionDetail />}
|
||||
|
||||
@@ -6,7 +6,10 @@ import {
|
||||
type LogicFunctionIdInput,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const useGetOneLogicFunction = ({ id }: LogicFunctionIdInput) => {
|
||||
export const useGetOneLogicFunction = ({
|
||||
id,
|
||||
skip,
|
||||
}: LogicFunctionIdInput & { skip?: boolean }) => {
|
||||
const { data, loading } = useQuery<
|
||||
FindOneLogicFunctionQuery,
|
||||
FindOneLogicFunctionQueryVariables
|
||||
@@ -14,6 +17,7 @@ export const useGetOneLogicFunction = ({ id }: LogicFunctionIdInput) => {
|
||||
variables: {
|
||||
input: { id },
|
||||
},
|
||||
skip,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
|
||||
import { type LogicFunction } from '@/logic-functions/types/LogicFunction';
|
||||
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
|
||||
|
||||
export const logicFunctionsSelector = createAtomSelector<LogicFunction[]>({
|
||||
key: 'logicFunctionsSelector',
|
||||
get: ({ get }) => {
|
||||
const storeItem = get(metadataStoreState, 'logicFunctions');
|
||||
|
||||
return storeItem.current as LogicFunction[];
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
import { type FindManyLogicFunctionsQuery } from '~/generated-metadata/graphql';
|
||||
|
||||
export type LogicFunction =
|
||||
FindManyLogicFunctionsQuery['findManyLogicFunctions'][number];
|
||||
+1
-8
@@ -5,10 +5,8 @@ import { splitPageLayoutWithRelated } from '@/metadata-store/utils/splitPageLayo
|
||||
import { splitViewWithRelated } from '@/metadata-store/utils/splitViewWithRelated';
|
||||
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries';
|
||||
import { transformPageLayout } from '@/page-layout/utils/transformPageLayout';
|
||||
import { logicFunctionsState } from '@/settings/logic-functions/states/logicFunctionsState';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useApolloClient } from '@apollo/client/react';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
@@ -56,7 +54,6 @@ const hasOverlap = (
|
||||
|
||||
export const useLoadStaleMetadataEntities = () => {
|
||||
const client = useApolloClient();
|
||||
const store = useStore();
|
||||
const { replaceDraft, applyChanges } = useUpdateMetadataStoreDraft();
|
||||
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
|
||||
|
||||
@@ -166,10 +163,6 @@ export const useLoadStaleMetadataEntities = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
store.set(
|
||||
logicFunctionsState.atom,
|
||||
result.data.findManyLogicFunctions,
|
||||
);
|
||||
replaceDraft(
|
||||
'logicFunctions',
|
||||
result.data.findManyLogicFunctions,
|
||||
@@ -240,7 +233,7 @@ export const useLoadStaleMetadataEntities = () => {
|
||||
await Promise.all(fetchPromises);
|
||||
applyChanges();
|
||||
},
|
||||
[client, store, replaceDraft, applyChanges, isAiEnabled],
|
||||
[client, replaceDraft, applyChanges, isAiEnabled],
|
||||
);
|
||||
|
||||
return { loadStaleMetadataEntities };
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import { type LogicFunction } from '@/settings/logic-functions/states/logicFunctionsState';
|
||||
import { type LogicFunction } from '@/logic-functions/types/LogicFunction';
|
||||
|
||||
export type FlatLogicFunction = LogicFunction;
|
||||
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
|
||||
import { type FindManyLogicFunctionsQuery } from '~/generated-metadata/graphql';
|
||||
|
||||
export type LogicFunction =
|
||||
FindManyLogicFunctionsQuery['findManyLogicFunctions'][number];
|
||||
|
||||
export const logicFunctionsState = createAtomState<LogicFunction[]>({
|
||||
key: 'logicFunctionsState',
|
||||
defaultValue: [],
|
||||
});
|
||||
+3
-3
@@ -1,5 +1,6 @@
|
||||
import { WorkflowActionMenuItems } from '@/side-panel/pages/workflow/action/components/WorkflowActionMenuItems';
|
||||
import { logicFunctionsState } from '@/settings/logic-functions/states/logicFunctionsState';
|
||||
import { logicFunctionsSelector } from '@/logic-functions/states/logicFunctionsSelector';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { type WorkflowActionType } from '@/workflow/types/Workflow';
|
||||
import { SidePanelStepListContainer } from '@/workflow/workflow-steps/components/SidePanelWorkflowSelectStepContainer';
|
||||
import { SidePanelWorkflowSelectStepTitle } from '@/workflow/workflow-steps/components/SidePanelWorkflowSelectStepTitle';
|
||||
@@ -10,7 +11,6 @@ import { HUMAN_INPUT_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/
|
||||
import { RECORD_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/RecordActions';
|
||||
import { getActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIconColorOrThrow';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { IconFunction } from 'twenty-ui/display';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
@@ -33,7 +33,7 @@ export const SidePanelWorkflowSelectAction = ({
|
||||
|
||||
const { t } = useLingui();
|
||||
|
||||
const logicFunctions = useAtomStateValue(logicFunctionsState);
|
||||
const logicFunctions = useAtomStateValue(logicFunctionsSelector);
|
||||
|
||||
const toolFunctions = logicFunctions.filter((fn) => fn.isTool === true);
|
||||
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { usePersistLogicFunction } from '@/logic-functions/hooks/usePersistLogicFunction';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { TabList } from '@/ui/layout/tab-list/components/TabList';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
@@ -12,18 +17,26 @@ import { t } from '@lingui/core/macro';
|
||||
import {
|
||||
IconChartBar,
|
||||
IconCpu,
|
||||
IconPlus,
|
||||
IconSettingsBolt,
|
||||
IconSparkles,
|
||||
IconTool,
|
||||
} from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { UndecoratedLink } from 'twenty-ui/navigation';
|
||||
import { SettingsAIMoreTab } from '~/pages/settings/ai/components/SettingsAIMoreTab';
|
||||
import { SettingsAgentToolsTab } from '~/pages/settings/ai/components/SettingsAgentToolsTab';
|
||||
import { SettingsAIModelsTab } from './components/SettingsAIModelsTab';
|
||||
import { SettingsAIUsageTab } from './components/SettingsAIUsageTab';
|
||||
import { SettingsAgentSkills } from './components/SettingsAgentSkills';
|
||||
import { SettingsToolsTable } from './components/SettingsToolsTable';
|
||||
import { SETTINGS_AI_TABS } from './constants/SettingsAiTabs';
|
||||
|
||||
export const SettingsAI = () => {
|
||||
const navigate = useNavigate();
|
||||
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
|
||||
const { createLogicFunction } = usePersistLogicFunction();
|
||||
const [isCreatingTool, setIsCreatingTool] = useState(false);
|
||||
|
||||
const activeTabId = useAtomComponentStateValue(
|
||||
activeTabIdComponentState,
|
||||
SETTINGS_AI_TABS.COMPONENT_INSTANCE_ID,
|
||||
@@ -33,6 +46,44 @@ export const SettingsAI = () => {
|
||||
FeatureFlagKey.IS_USAGE_ANALYTICS_ENABLED,
|
||||
);
|
||||
|
||||
const handleCreateTool = async () => {
|
||||
setIsCreatingTool(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` });
|
||||
|
||||
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,
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
enqueueErrorSnackBar({ message: t`Failed to create tool` });
|
||||
}
|
||||
} finally {
|
||||
setIsCreatingTool(false);
|
||||
}
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
id: SETTINGS_AI_TABS.TABS_IDS.MODELS,
|
||||
@@ -74,6 +125,27 @@ export const SettingsAI = () => {
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
title={t`AI`}
|
||||
actionButton={
|
||||
isSkillsTab ? (
|
||||
<UndecoratedLink to={getSettingsPath(SettingsPath.AINewSkill)}>
|
||||
<Button
|
||||
Icon={IconPlus}
|
||||
title={t`New Skill`}
|
||||
accent="blue"
|
||||
size="small"
|
||||
/>
|
||||
</UndecoratedLink>
|
||||
) : isToolsTab ? (
|
||||
<Button
|
||||
Icon={IconPlus}
|
||||
title={t`New Tool`}
|
||||
accent="blue"
|
||||
size="small"
|
||||
onClick={handleCreateTool}
|
||||
disabled={isCreatingTool}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
links={[
|
||||
{
|
||||
children: t`Workspace`,
|
||||
@@ -89,7 +161,7 @@ export const SettingsAI = () => {
|
||||
/>
|
||||
{isModelsTab && <SettingsAIModelsTab />}
|
||||
{isSkillsTab && <SettingsAgentSkills />}
|
||||
{isToolsTab && <SettingsToolsTable />}
|
||||
{isToolsTab && <SettingsAgentToolsTab />}
|
||||
{isUsageTab && <SettingsAIUsageTab />}
|
||||
{isMoreTab && <SettingsAIMoreTab />}
|
||||
</SettingsPageContainer>
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useContext, useState } from 'react';
|
||||
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { useGetOneLogicFunction } from '@/logic-functions/hooks/useGetOneLogicFunction';
|
||||
import { usePersistLogicFunction } from '@/logic-functions/hooks/usePersistLogicFunction';
|
||||
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { SettingsLogicFunctionLabelContainer } from '@/settings/logic-functions/components/SettingsLogicFunctionLabelContainer';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { TextArea } from '@/ui/input/components/TextArea';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath, isDefined, isValidUuid } from 'twenty-shared/utils';
|
||||
import { H2Title, IconTrash } from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
|
||||
import { ThemeContext } from 'twenty-ui/theme-constants';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import {
|
||||
GetToolIndexDocument,
|
||||
GetToolInputSchemaDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { SettingsToolParameterTable } from '~/pages/settings/ai/components/SettingsToolParameterTable';
|
||||
|
||||
const DELETE_TOOL_MODAL_ID = 'delete-tool-modal';
|
||||
|
||||
export const SettingsToolDetail = () => {
|
||||
const { toolIdentifier } = useParams();
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const navigate = useNavigate();
|
||||
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
|
||||
const { updateLogicFunction, deleteLogicFunction } =
|
||||
usePersistLogicFunction();
|
||||
const { openModal } = useModal();
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [editedName, setEditedName] = useState<string | null>(null);
|
||||
const [editedDescription, setEditedDescription] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const isCustomTool = isDefined(toolIdentifier) && isValidUuid(toolIdentifier);
|
||||
|
||||
const { logicFunction, loading: logicFunctionLoading } =
|
||||
useGetOneLogicFunction({
|
||||
id: toolIdentifier ?? '',
|
||||
skip: !isCustomTool,
|
||||
});
|
||||
|
||||
const { data: toolIndexData, loading: toolIndexLoading } = useQuery(
|
||||
GetToolIndexDocument,
|
||||
{ skip: isCustomTool },
|
||||
);
|
||||
|
||||
const systemTool = toolIndexData?.getToolIndex.find(
|
||||
(entry) => entry.name === toolIdentifier,
|
||||
);
|
||||
|
||||
const { data: schemaData, loading: schemaLoading } = useQuery(
|
||||
GetToolInputSchemaDocument,
|
||||
{
|
||||
variables: { toolName: toolIdentifier ?? '' },
|
||||
skip: isCustomTool || !isDefined(systemTool),
|
||||
},
|
||||
);
|
||||
|
||||
const loading = isCustomTool
|
||||
? logicFunctionLoading
|
||||
: toolIndexLoading || schemaLoading;
|
||||
|
||||
const workspaceCustomApplicationId =
|
||||
currentWorkspace?.workspaceCustomApplication?.id;
|
||||
|
||||
const isManaged =
|
||||
isCustomTool &&
|
||||
isDefined(logicFunction?.applicationId) &&
|
||||
logicFunction.applicationId !== workspaceCustomApplicationId;
|
||||
|
||||
const isReadOnly = !isCustomTool || isManaged;
|
||||
|
||||
const name = isCustomTool ? logicFunction?.name : toolIdentifier;
|
||||
const description = isCustomTool
|
||||
? logicFunction?.description
|
||||
: systemTool?.description;
|
||||
|
||||
const inputSchema = isCustomTool
|
||||
? logicFunction?.toolInputSchema
|
||||
: schemaData?.getToolInputSchema;
|
||||
|
||||
const functionLink = isCustomTool
|
||||
? isDefined(logicFunction?.applicationId)
|
||||
? getSettingsPath(SettingsPath.ApplicationLogicFunctionDetail, {
|
||||
applicationId: logicFunction.applicationId,
|
||||
logicFunctionId: logicFunction?.id ?? '',
|
||||
})
|
||||
: getSettingsPath(SettingsPath.LogicFunctionDetail, {
|
||||
logicFunctionId: logicFunction?.id ?? '',
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const debouncedSaveName = useDebouncedCallback(async (value: string) => {
|
||||
if (!isCustomTool || !isDefined(logicFunction)) return;
|
||||
|
||||
const result = await updateLogicFunction({
|
||||
input: {
|
||||
id: logicFunction.id,
|
||||
update: { name: value },
|
||||
},
|
||||
});
|
||||
|
||||
if (result.status === 'failed') {
|
||||
setEditedName(logicFunction.name);
|
||||
}
|
||||
}, 1_000);
|
||||
|
||||
const handleNameChange = (value: string) => {
|
||||
setEditedName(value);
|
||||
debouncedSaveName(value);
|
||||
};
|
||||
|
||||
const debouncedSaveDescription = useDebouncedCallback(
|
||||
async (value: string) => {
|
||||
if (!isCustomTool || !isDefined(logicFunction)) return;
|
||||
|
||||
const result = await updateLogicFunction({
|
||||
input: {
|
||||
id: logicFunction.id,
|
||||
update: { description: value },
|
||||
},
|
||||
});
|
||||
|
||||
if (result.status === 'failed') {
|
||||
setEditedDescription(logicFunction.description ?? null);
|
||||
}
|
||||
},
|
||||
1_000,
|
||||
);
|
||||
|
||||
const handleDescriptionChange = (value: string) => {
|
||||
setEditedDescription(value);
|
||||
debouncedSaveDescription(value);
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!isCustomTool || !isDefined(logicFunction)) return;
|
||||
|
||||
setIsDeleting(true);
|
||||
|
||||
const result = await deleteLogicFunction({
|
||||
input: { id: logicFunction.id },
|
||||
});
|
||||
|
||||
if (result.status === 'successful') {
|
||||
enqueueSuccessSnackBar({ message: t`Tool deleted` });
|
||||
navigate(getSettingsPath(SettingsPath.AI, undefined, undefined, 'tools'));
|
||||
} else {
|
||||
enqueueErrorSnackBar({ message: t`Failed to delete tool` });
|
||||
}
|
||||
|
||||
setIsDeleting(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
title={
|
||||
isCustomTool ? (
|
||||
<SettingsLogicFunctionLabelContainer
|
||||
value={editedName ?? name ?? ''}
|
||||
onChange={handleNameChange}
|
||||
/>
|
||||
) : (
|
||||
(name ?? '')
|
||||
)
|
||||
}
|
||||
links={[
|
||||
{
|
||||
children: t`Workspace`,
|
||||
href: getSettingsPath(SettingsPath.Workspace),
|
||||
},
|
||||
{
|
||||
children: t`AI`,
|
||||
href: getSettingsPath(SettingsPath.AI, undefined, undefined, 'tools'),
|
||||
},
|
||||
{ children: editedName ?? name ?? '' },
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
{loading ? (
|
||||
<SkeletonTheme
|
||||
baseColor={theme.background.tertiary}
|
||||
highlightColor={theme.background.transparent.lighter}
|
||||
borderRadius={4}
|
||||
>
|
||||
<Section>
|
||||
<Skeleton height={20} width={200} />
|
||||
<Skeleton height={20} width={400} />
|
||||
|
||||
<Skeleton height={80} />
|
||||
</Section>
|
||||
</SkeletonTheme>
|
||||
) : (
|
||||
<>
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Parameters`}
|
||||
description={t`Input parameters accepted by this tool`}
|
||||
/>
|
||||
<SettingsToolParameterTable
|
||||
schemaProperties={inputSchema?.properties ?? {}}
|
||||
requiredFields={inputSchema?.required}
|
||||
functionLink={functionLink}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Description`}
|
||||
description={t`Define what this tool does`}
|
||||
/>
|
||||
<TextArea
|
||||
textAreaId="tool-description-textarea"
|
||||
placeholder={t`Write a description`}
|
||||
minRows={3}
|
||||
value={editedDescription ?? description ?? ''}
|
||||
onChange={handleDescriptionChange}
|
||||
disabled={isReadOnly}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
{isCustomTool && !isManaged && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Danger zone`}
|
||||
description={t`Delete this tool`}
|
||||
/>
|
||||
<Button
|
||||
Icon={IconTrash}
|
||||
title={t`Delete`}
|
||||
accent="danger"
|
||||
size="small"
|
||||
variant="secondary"
|
||||
onClick={() => openModal(DELETE_TOOL_MODAL_ID)}
|
||||
/>
|
||||
</Section>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</SettingsPageContainer>
|
||||
<ConfirmationModal
|
||||
modalInstanceId={DELETE_TOOL_MODAL_ID}
|
||||
title={t`Delete Tool`}
|
||||
subtitle={t`Are you sure you want to delete this tool? This action cannot be undone.`}
|
||||
onConfirmClick={handleDelete}
|
||||
confirmButtonText={t`Delete`}
|
||||
loading={isDeleting}
|
||||
/>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
};
|
||||
@@ -6,11 +6,9 @@ import { useSortedArray } from '@/ui/layout/table/hooks/useSortedArray';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useContext, useMemo, useState } from 'react';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import { H2Title, IconArchive, IconPlus } from 'twenty-ui/display';
|
||||
import { Button, SearchInput } from 'twenty-ui/input';
|
||||
import { MenuItemToggle, UndecoratedLink } from 'twenty-ui/navigation';
|
||||
import { H2Title, IconArchive } from 'twenty-ui/display';
|
||||
import { SearchInput } from 'twenty-ui/input';
|
||||
import { MenuItemToggle } from 'twenty-ui/navigation';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { useMutation, useQuery } from '@apollo/client/react';
|
||||
@@ -31,13 +29,6 @@ const StyledCoverImage = styled.div`
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledFooterContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: ${themeCssVariables.spacing[4]};
|
||||
`;
|
||||
|
||||
const StyledSearchInput = styled(SearchInput)`
|
||||
margin-bottom: ${themeCssVariables.spacing[4]};
|
||||
`;
|
||||
@@ -147,16 +138,6 @@ export const SettingsAgentSkills = () => {
|
||||
onActivate={handleActivate}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
<StyledFooterContainer>
|
||||
<UndecoratedLink to={getSettingsPath(SettingsPath.AINewSkill)}>
|
||||
<Button
|
||||
Icon={IconPlus}
|
||||
title={t`New Skill`}
|
||||
size="small"
|
||||
variant="secondary"
|
||||
/>
|
||||
</UndecoratedLink>
|
||||
</StyledFooterContainer>
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useContext } from 'react';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { ThemeContext } from 'twenty-ui/theme-constants';
|
||||
import { SettingsToolsTable } from '~/pages/settings/ai/components/SettingsToolsTable';
|
||||
|
||||
const StyledCoverImage = styled.div`
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
height: 160px;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
export const SettingsAgentToolsTab = () => {
|
||||
const { colorScheme } = useContext(ThemeContext);
|
||||
const coverImage =
|
||||
colorScheme === 'light'
|
||||
? '/images/ai/ai-tools-cover-light.png'
|
||||
: '/images/ai/ai-tools-cover-dark.png';
|
||||
return (
|
||||
<>
|
||||
<Section>
|
||||
<StyledCoverImage style={{ backgroundImage: `url('${coverImage}')` }} />
|
||||
</Section>
|
||||
<SettingsToolsTable />
|
||||
</>
|
||||
);
|
||||
};
|
||||
-195
@@ -1,195 +0,0 @@
|
||||
import { useLazyQuery } from '@apollo/client/react';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { GET_TOOL_INPUT_SCHEMA } from '@/ai/graphql/queries/getToolInputSchemas';
|
||||
import { SettingsItemTypeTag } from '@/settings/components/SettingsItemTypeTag';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconChevronRight,
|
||||
IconCode,
|
||||
IconDatabase,
|
||||
IconPlayerPlay,
|
||||
IconSettings,
|
||||
IconTable,
|
||||
OverflowingTextWithTooltip,
|
||||
} from 'twenty-ui/display';
|
||||
import { JsonTree } from 'twenty-ui/json-visualizer';
|
||||
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { type JsonValue } from 'type-fest';
|
||||
|
||||
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
|
||||
|
||||
type GetToolInputSchemaQuery = {
|
||||
getToolInputSchema: object | null;
|
||||
};
|
||||
|
||||
export type SystemTool = {
|
||||
name: string;
|
||||
description: string;
|
||||
category: string;
|
||||
objectName?: string;
|
||||
};
|
||||
|
||||
export type SettingsSystemToolTableRowProps = {
|
||||
tool: SystemTool;
|
||||
};
|
||||
|
||||
const StyledSystemToolTableRowContainer = styled.div`
|
||||
> div,
|
||||
> a {
|
||||
opacity: 0.7;
|
||||
|
||||
&[data-clickable='true']:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledSystemToolTableRow = (
|
||||
props: React.ComponentProps<typeof TableRow>,
|
||||
) => (
|
||||
<StyledSystemToolTableRowContainer>
|
||||
{/* oxlint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<TableRow gridTemplateColumns="1fr 100px 36px" {...props} />
|
||||
</StyledSystemToolTableRowContainer>
|
||||
);
|
||||
|
||||
const StyledIconContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
const StyledExpandableContent = styled.div`
|
||||
background-color: ${themeCssVariables.background.secondary};
|
||||
border-top: 1px solid ${themeCssVariables.border.color.light};
|
||||
padding: ${themeCssVariables.spacing[4]};
|
||||
`;
|
||||
|
||||
const StyledSectionTitle = styled.div`
|
||||
color: ${themeCssVariables.font.color.secondary};
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
font-weight: ${themeCssVariables.font.weight.medium};
|
||||
margin-bottom: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
const StyledDescription = styled.div`
|
||||
color: ${themeCssVariables.font.color.tertiary};
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
margin-bottom: ${themeCssVariables.spacing[4]};
|
||||
`;
|
||||
|
||||
const getCategoryIcon = (category: string) => {
|
||||
switch (category) {
|
||||
case 'database':
|
||||
return IconDatabase;
|
||||
case 'workflow':
|
||||
return IconPlayerPlay;
|
||||
case 'metadata':
|
||||
return IconSettings;
|
||||
case 'view':
|
||||
return IconTable;
|
||||
default:
|
||||
return IconCode;
|
||||
}
|
||||
};
|
||||
|
||||
export const SettingsSystemToolTableRow = ({
|
||||
tool,
|
||||
}: SettingsSystemToolTableRowProps) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const { copyToClipboard } = useCopyToClipboard();
|
||||
|
||||
const [fetchSchemaRaw, { data: schemaData, loading: schemaLoading }] =
|
||||
useLazyQuery<GetToolInputSchemaQuery>(GET_TOOL_INPUT_SCHEMA);
|
||||
|
||||
const fetchSchema = useCallback(
|
||||
() => fetchSchemaRaw({ variables: { toolName: tool.name } }),
|
||||
[fetchSchemaRaw, tool.name],
|
||||
);
|
||||
|
||||
const inputSchema = schemaData?.getToolInputSchema;
|
||||
|
||||
const hasInputSchema =
|
||||
isDefined(inputSchema) && Object.keys(inputSchema).length > 0;
|
||||
|
||||
const Icon = getCategoryIcon(tool.category);
|
||||
|
||||
const handleRowClick = () => {
|
||||
if (!schemaData && !schemaLoading) {
|
||||
fetchSchema();
|
||||
}
|
||||
setIsExpanded(!isExpanded);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledSystemToolTableRow
|
||||
key={tool.name}
|
||||
onClick={handleRowClick}
|
||||
isClickable
|
||||
>
|
||||
<TableCell
|
||||
color={themeCssVariables.font.color.primary}
|
||||
gap={themeCssVariables.spacing[2]}
|
||||
minWidth="0"
|
||||
overflow="hidden"
|
||||
>
|
||||
<StyledIconContainer>
|
||||
<Icon size={16} />
|
||||
</StyledIconContainer>
|
||||
<OverflowingTextWithTooltip text={tool.name} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<SettingsItemTypeTag item={{ isCustom: false }} />
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
padding={`0 ${themeCssVariables.spacing[2]} 0 0`}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<IconChevronDown size={16} />
|
||||
) : (
|
||||
<IconChevronRight size={16} />
|
||||
)}
|
||||
</TableCell>
|
||||
</StyledSystemToolTableRow>
|
||||
|
||||
<AnimatedExpandableContainer isExpanded={isExpanded} mode="fit-content">
|
||||
<StyledExpandableContent>
|
||||
{tool.description && (
|
||||
<StyledDescription>{tool.description}</StyledDescription>
|
||||
)}
|
||||
{schemaLoading && (
|
||||
<StyledSectionTitle>{t`Loading schema...`}</StyledSectionTitle>
|
||||
)}
|
||||
{hasInputSchema && (
|
||||
<>
|
||||
<StyledSectionTitle>{t`Input Schema`}</StyledSectionTitle>
|
||||
<JsonTree
|
||||
value={inputSchema as JsonValue}
|
||||
shouldExpandNodeInitially={() => true}
|
||||
emptyArrayLabel={t`Empty Array`}
|
||||
emptyObjectLabel={t`No parameters`}
|
||||
emptyStringLabel={t`[empty string]`}
|
||||
arrowButtonCollapsedLabel={t`Expand`}
|
||||
arrowButtonExpandedLabel={t`Collapse`}
|
||||
onNodeValueClick={copyToClipboard}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!schemaLoading && !hasInputSchema && (
|
||||
<StyledSectionTitle>{t`No parameters`}</StyledSectionTitle>
|
||||
)}
|
||||
</StyledExpandableContent>
|
||||
</AnimatedExpandableContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,184 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { getNavigationMenuItemIconStyleFromColor } from '@/navigation-menu-item/common/utils/getNavigationMenuItemIconStyleFromColor';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { getObjectColorWithFallback } from '@/object-metadata/utils/getObjectColorWithFallback';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
Avatar,
|
||||
type IconComponent,
|
||||
IconCode,
|
||||
IconEdit,
|
||||
IconPlus,
|
||||
IconSearch,
|
||||
IconTrash,
|
||||
useIcons,
|
||||
} from 'twenty-ui/display';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
type ApplicationInfo = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
type MarketplaceAppInfo = {
|
||||
icon: string;
|
||||
logo?: string | null;
|
||||
};
|
||||
|
||||
type SettingsToolIconProps = {
|
||||
icon?: string | null;
|
||||
toolName?: string;
|
||||
objectName?: string;
|
||||
application?: ApplicationInfo;
|
||||
marketplaceApp?: MarketplaceAppInfo;
|
||||
};
|
||||
|
||||
const getOperationIcon = (toolName: string): IconComponent | null => {
|
||||
if (toolName.startsWith('create_')) return IconPlus;
|
||||
if (toolName.startsWith('update_')) return IconEdit;
|
||||
if (toolName.startsWith('delete_')) return IconTrash;
|
||||
if (toolName.startsWith('find_') || toolName.startsWith('get_'))
|
||||
return IconSearch;
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const StyledCompositeContainer = styled.div`
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
height: 16px;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
width: 16px;
|
||||
`;
|
||||
|
||||
const StyledMainIconWrapper = styled.div<{
|
||||
$backgroundColor: string;
|
||||
$borderColor?: string;
|
||||
}>`
|
||||
align-items: center;
|
||||
background-color: ${({ $backgroundColor }) => $backgroundColor};
|
||||
border: ${({ $borderColor }) =>
|
||||
$borderColor ? `1px solid ${$borderColor}` : 'none'};
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
inset: 0;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
`;
|
||||
|
||||
const StyledOperationOverlay = styled.div`
|
||||
align-items: center;
|
||||
background-color: ${themeCssVariables.grayScale.gray4};
|
||||
border-radius: 4px;
|
||||
bottom: -5px;
|
||||
display: flex;
|
||||
height: 14px;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
right: -6px;
|
||||
width: 12px;
|
||||
`;
|
||||
|
||||
export const SettingsToolIcon = ({
|
||||
icon,
|
||||
toolName,
|
||||
objectName,
|
||||
application,
|
||||
marketplaceApp,
|
||||
}: SettingsToolIconProps) => {
|
||||
const { getIcon } = useIcons();
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
// Custom tools: application/marketplace icons
|
||||
if (isDefined(application) && isDefined(marketplaceApp?.logo)) {
|
||||
return (
|
||||
<Avatar
|
||||
avatarUrl={marketplaceApp?.logo ?? null}
|
||||
placeholder={application.name}
|
||||
placeholderColorSeed={application.name}
|
||||
type="squared"
|
||||
size="xs"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isDefined(marketplaceApp)) {
|
||||
const MarketplaceIcon = getIcon(marketplaceApp.icon);
|
||||
return <MarketplaceIcon size={16} />;
|
||||
}
|
||||
|
||||
if (isDefined(application)) {
|
||||
return (
|
||||
<Avatar
|
||||
placeholder={application.name}
|
||||
placeholderColorSeed={application.name}
|
||||
type="squared"
|
||||
size="xs"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// System tools: icon from server, color derived from object metadata
|
||||
const MainIcon = isDefined(icon) ? getIcon(icon) : IconCode;
|
||||
const OperationIcon = isDefined(toolName) ? getOperationIcon(toolName) : null;
|
||||
|
||||
const objectMetadata = isDefined(objectName)
|
||||
? objectMetadataItems.find((item) => item.nameSingular === objectName)
|
||||
: undefined;
|
||||
|
||||
const objectStyle = isDefined(objectMetadata)
|
||||
? getNavigationMenuItemIconStyleFromColor(
|
||||
getObjectColorWithFallback(objectMetadata),
|
||||
)
|
||||
: null;
|
||||
|
||||
if (isDefined(OperationIcon)) {
|
||||
return (
|
||||
<StyledCompositeContainer>
|
||||
<StyledMainIconWrapper
|
||||
$backgroundColor={objectStyle?.backgroundColor ?? 'transparent'}
|
||||
$borderColor={objectStyle?.borderColor}
|
||||
>
|
||||
<MainIcon
|
||||
size="14px"
|
||||
stroke={theme.icon.stroke.md}
|
||||
color={objectStyle?.iconColor ?? theme.font.color.secondary}
|
||||
/>
|
||||
</StyledMainIconWrapper>
|
||||
<StyledOperationOverlay>
|
||||
<OperationIcon
|
||||
size="12px"
|
||||
stroke={theme.icon.stroke.md}
|
||||
color={themeCssVariables.grayScale.gray10}
|
||||
/>
|
||||
</StyledOperationOverlay>
|
||||
</StyledCompositeContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (isDefined(objectStyle)) {
|
||||
return (
|
||||
<StyledCompositeContainer>
|
||||
<StyledMainIconWrapper
|
||||
$backgroundColor={objectStyle.backgroundColor}
|
||||
$borderColor={objectStyle.borderColor}
|
||||
>
|
||||
<MainIcon
|
||||
size="14px"
|
||||
stroke={theme.icon.stroke.md}
|
||||
color={objectStyle.iconColor}
|
||||
/>
|
||||
</StyledMainIconWrapper>
|
||||
</StyledCompositeContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return <MainIcon size={16} />;
|
||||
};
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { type ComponentType, useContext } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import {
|
||||
AppTooltip,
|
||||
IconArrowUpRight,
|
||||
IconInfoCircle,
|
||||
IllustrationIconArray,
|
||||
IllustrationIconJson,
|
||||
IllustrationIconNumbers,
|
||||
IllustrationIconText,
|
||||
IllustrationIconToggle,
|
||||
OverflowingTextWithTooltip,
|
||||
TooltipDelay,
|
||||
} from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { UndecoratedLink } from 'twenty-ui/navigation';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
type SchemaProperty = {
|
||||
type?: string;
|
||||
description?: string;
|
||||
format?: string;
|
||||
items?: { type?: string };
|
||||
};
|
||||
|
||||
export type SettingsToolParameterTableProps = {
|
||||
schemaProperties: Record<string, SchemaProperty>;
|
||||
requiredFields?: string[];
|
||||
functionLink?: string;
|
||||
};
|
||||
|
||||
const PARAMETER_TABLE_GRID = '140px 1fr 80px 24px';
|
||||
|
||||
const TYPE_ICON_MAP: Record<string, ComponentType<{ size?: number }>> = {
|
||||
string: IllustrationIconText,
|
||||
number: IllustrationIconNumbers,
|
||||
integer: IllustrationIconNumbers,
|
||||
boolean: IllustrationIconToggle,
|
||||
array: IllustrationIconArray,
|
||||
object: IllustrationIconJson,
|
||||
};
|
||||
|
||||
const StyledTableHeaderRow = styled.div`
|
||||
margin-bottom: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
const StyledFieldsContainer = styled.div`
|
||||
border-bottom: 1px solid ${themeCssVariables.border.color.light};
|
||||
padding-bottom: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
const StyledFooter = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: ${themeCssVariables.spacing[2]} 0;
|
||||
`;
|
||||
|
||||
const StyledInfoIconContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const getDisplayType = (property: SchemaProperty): string => {
|
||||
if (property.format) {
|
||||
return property.format;
|
||||
}
|
||||
|
||||
if (property.type === 'array' && property.items?.type) {
|
||||
return `${property.items.type}[]`;
|
||||
}
|
||||
|
||||
return property.type ?? '';
|
||||
};
|
||||
|
||||
export const SettingsToolParameterTable = ({
|
||||
schemaProperties,
|
||||
requiredFields,
|
||||
functionLink,
|
||||
}: SettingsToolParameterTableProps) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const entries = Object.entries(schemaProperties);
|
||||
|
||||
if (entries.length === 0 && !functionLink) {
|
||||
return <div>{t`No parameters`}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Table>
|
||||
{entries.length > 0 && (
|
||||
<>
|
||||
<StyledTableHeaderRow>
|
||||
<TableRow gridTemplateColumns={PARAMETER_TABLE_GRID}>
|
||||
<TableHeader>{t`Name`}</TableHeader>
|
||||
<TableHeader>{t`Type`}</TableHeader>
|
||||
<TableHeader>{t`Required`}</TableHeader>
|
||||
<TableHeader />
|
||||
</TableRow>
|
||||
</StyledTableHeaderRow>
|
||||
<StyledFieldsContainer>
|
||||
{entries.map(([paramName, property], index) => {
|
||||
const infoIconId = `param-info-${index}`;
|
||||
const TypeIcon = TYPE_ICON_MAP[property.type ?? ''];
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={paramName}
|
||||
gridTemplateColumns={PARAMETER_TABLE_GRID}
|
||||
>
|
||||
<TableCell
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
<OverflowingTextWithTooltip text={paramName} />
|
||||
</TableCell>
|
||||
<TableCell gap={themeCssVariables.spacing[1]}>
|
||||
{isDefined(TypeIcon) && (
|
||||
<TypeIcon size={theme.icon.size.md} />
|
||||
)}
|
||||
{getDisplayType(property)}
|
||||
</TableCell>
|
||||
<TableCell color={themeCssVariables.font.color.tertiary}>
|
||||
{requiredFields?.includes(paramName) ? t`Yes` : ''}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{property.description && (
|
||||
<StyledInfoIconContainer>
|
||||
<IconInfoCircle
|
||||
id={infoIconId}
|
||||
size={theme.icon.size.md}
|
||||
color={theme.font.color.tertiary}
|
||||
style={{ outline: 'none', cursor: 'pointer' }}
|
||||
/>
|
||||
<AppTooltip
|
||||
anchorSelect={`#${infoIconId}`}
|
||||
content={property.description}
|
||||
offset={5}
|
||||
noArrow
|
||||
place="bottom"
|
||||
positionStrategy="fixed"
|
||||
delay={TooltipDelay.shortDelay}
|
||||
/>
|
||||
</StyledInfoIconContainer>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</StyledFieldsContainer>
|
||||
</>
|
||||
)}
|
||||
{functionLink && (
|
||||
<StyledFooter>
|
||||
<UndecoratedLink to={functionLink}>
|
||||
<Button
|
||||
Icon={IconArrowUpRight}
|
||||
title={t`See function`}
|
||||
size="small"
|
||||
variant="secondary"
|
||||
/>
|
||||
</UndecoratedLink>
|
||||
</StyledFooter>
|
||||
)}
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
@@ -1,20 +1,15 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import { SettingsItemTypeTag } from '@/settings/components/SettingsItemTypeTag';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { IconCode, OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { type LogicFunction } from '~/generated-metadata/graphql';
|
||||
|
||||
type ToolWithApplicationId = LogicFunction & {
|
||||
applicationId?: string | null;
|
||||
};
|
||||
|
||||
export type SettingsToolTableRowProps = {
|
||||
tool: ToolWithApplicationId;
|
||||
leftIcon: ReactNode;
|
||||
name: string;
|
||||
appLabel: string;
|
||||
action?: ReactNode;
|
||||
link?: string;
|
||||
};
|
||||
@@ -28,7 +23,9 @@ const StyledIconContainer = styled.div`
|
||||
`;
|
||||
|
||||
export const SettingsToolTableRow = ({
|
||||
tool,
|
||||
leftIcon,
|
||||
name,
|
||||
appLabel,
|
||||
action,
|
||||
link,
|
||||
}: SettingsToolTableRowProps) => {
|
||||
@@ -43,15 +40,11 @@ export const SettingsToolTableRow = ({
|
||||
minWidth="0"
|
||||
overflow="hidden"
|
||||
>
|
||||
<StyledIconContainer>
|
||||
<IconCode size={16} />
|
||||
</StyledIconContainer>
|
||||
<OverflowingTextWithTooltip text={tool.name} />
|
||||
<StyledIconContainer>{leftIcon}</StyledIconContainer>
|
||||
<OverflowingTextWithTooltip text={name} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<SettingsItemTypeTag
|
||||
item={{ isCustom: true, applicationId: tool.applicationId }}
|
||||
/>
|
||||
<TableCell minWidth="0" overflow="hidden">
|
||||
<OverflowingTextWithTooltip text={appLabel} />
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
|
||||
@@ -1,31 +1,73 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { gql } from '@apollo/client';
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useContext, useMemo, useState } from 'react';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
import { type ReactNode, useContext, useMemo, useState } from 'react';
|
||||
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
||||
|
||||
import { useGetToolIndex } from '@/ai/hooks/useGetToolIndex';
|
||||
import { usePersistLogicFunction } from '@/logic-functions/hooks/usePersistLogicFunction';
|
||||
import { logicFunctionsState } from '@/settings/logic-functions/states/logicFunctionsState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { logicFunctionsSelector } from '@/logic-functions/states/logicFunctionsSelector';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
|
||||
import { H2Title, IconChevronRight, IconPlus } from 'twenty-ui/display';
|
||||
import { Button, SearchInput } from 'twenty-ui/input';
|
||||
import {
|
||||
H2Title,
|
||||
IconChevronRight,
|
||||
IconLock,
|
||||
IconPuzzle,
|
||||
IconTool,
|
||||
} from 'twenty-ui/display';
|
||||
import { SearchInput } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { MenuItemToggle } from 'twenty-ui/navigation';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { ToolCategory } from 'twenty-shared/ai';
|
||||
import { normalizeSearchText } from '~/utils/normalizeSearchText';
|
||||
import { SettingsSystemToolTableRow } from './SettingsSystemToolTableRow';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { SettingsToolIcon } from './SettingsToolIcon';
|
||||
import {
|
||||
TOOL_TABLE_ROW_GRID_TEMPLATE_COLUMNS,
|
||||
SettingsToolTableRow,
|
||||
TOOL_TABLE_ROW_GRID_TEMPLATE_COLUMNS,
|
||||
} from './SettingsToolTableRow';
|
||||
|
||||
type ToolItem = {
|
||||
identifier: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
category?: string;
|
||||
objectName?: string | null;
|
||||
icon?: string | null;
|
||||
applicationId?: string | null;
|
||||
};
|
||||
|
||||
const FIND_MANY_APPLICATIONS_FOR_TOOL_TABLE = gql`
|
||||
query FindManyApplicationsForToolTable {
|
||||
findManyApplications {
|
||||
id
|
||||
name
|
||||
universalIdentifier
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const FIND_MANY_MARKETPLACE_APPS_FOR_TOOL_TABLE = gql`
|
||||
query FindManyMarketplaceAppsForToolTable {
|
||||
findManyMarketplaceApps {
|
||||
id
|
||||
universalIdentifier
|
||||
icon
|
||||
logo
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledSearchContainer = styled.div`
|
||||
padding-bottom: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
@@ -34,207 +76,223 @@ const StyledTableHeaderRowContainer = styled.div`
|
||||
margin-bottom: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
const StyledFooterContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: ${themeCssVariables.spacing[4]};
|
||||
`;
|
||||
|
||||
export const SettingsToolsTable = () => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const logicFunctions = useAtomStateValue(logicFunctionsState);
|
||||
const { toolIndex, loading: toolIndexLoading } = useGetToolIndex();
|
||||
const { createLogicFunction } = usePersistLogicFunction();
|
||||
|
||||
const logicFunctions = useAtomStateValue(logicFunctionsSelector);
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
const {
|
||||
toolIndex,
|
||||
loading: toolIndexLoading,
|
||||
error: toolIndexError,
|
||||
} = useGetToolIndex();
|
||||
const { data: applicationsData } = useQuery<{
|
||||
findManyApplications: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
universalIdentifier: string;
|
||||
}>;
|
||||
}>(FIND_MANY_APPLICATIONS_FOR_TOOL_TABLE);
|
||||
const { data: marketplaceAppsData } = useQuery<{
|
||||
findManyMarketplaceApps: Array<{
|
||||
id: string;
|
||||
universalIdentifier: string;
|
||||
icon: string;
|
||||
logo?: string | null;
|
||||
}>;
|
||||
}>(FIND_MANY_MARKETPLACE_APPS_FOR_TOOL_TABLE);
|
||||
const { t } = useLingui();
|
||||
const navigate = useNavigate();
|
||||
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
|
||||
const [customSearchTerm, setCustomSearchTerm] = useState('');
|
||||
const [builtInSearchTerm, setBuiltInSearchTerm] = useState('');
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [showCustomTools, setShowCustomTools] = useState(true);
|
||||
const [showManagedTools, setShowManagedTools] = useState(true);
|
||||
const [showStandardTools, setShowStandardTools] = useState(true);
|
||||
|
||||
const tools = useMemo(
|
||||
() => logicFunctions.filter((fn) => fn.isTool === true),
|
||||
[logicFunctions],
|
||||
);
|
||||
const workspaceCustomApplicationId =
|
||||
currentWorkspace?.workspaceCustomApplication?.id;
|
||||
|
||||
const filteredTools = useMemo(
|
||||
() =>
|
||||
tools.filter((tool) => {
|
||||
const searchNormalized = normalizeSearchText(customSearchTerm);
|
||||
const matchesSearch =
|
||||
normalizeSearchText(tool.name).includes(searchNormalized) ||
|
||||
normalizeSearchText(tool.description ?? '').includes(
|
||||
searchNormalized,
|
||||
);
|
||||
const isManaged = (applicationId?: string | null) =>
|
||||
isDefined(applicationId) && applicationId !== workspaceCustomApplicationId;
|
||||
|
||||
return matchesSearch;
|
||||
}),
|
||||
[tools, customSearchTerm],
|
||||
);
|
||||
const isCustom = (item: ToolItem) => isDefined(item.applicationId);
|
||||
|
||||
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` });
|
||||
|
||||
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,
|
||||
const getToolLink = (item: ToolItem) =>
|
||||
getSettingsPath(SettingsPath.AIToolDetail, {
|
||||
toolIdentifier: item.identifier,
|
||||
});
|
||||
};
|
||||
|
||||
const allTools: ToolItem[] = useMemo(
|
||||
() => [
|
||||
...logicFunctions
|
||||
.filter((fn) => fn.isTool === true)
|
||||
.map((fn) => ({
|
||||
identifier: fn.id,
|
||||
name: fn.name,
|
||||
description: fn.description,
|
||||
applicationId: fn.applicationId,
|
||||
})),
|
||||
...toolIndex
|
||||
.filter((tool) => tool.category !== ToolCategory.LOGIC_FUNCTION)
|
||||
.map((tool) => ({
|
||||
identifier: tool.name,
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
category: tool.category,
|
||||
objectName: tool.objectName,
|
||||
icon: tool.icon,
|
||||
})),
|
||||
],
|
||||
[logicFunctions, toolIndex],
|
||||
);
|
||||
|
||||
const applicationById = new Map(
|
||||
(applicationsData?.findManyApplications ?? []).map((application) => [
|
||||
application.id,
|
||||
application,
|
||||
]),
|
||||
);
|
||||
const marketplaceAppByUniversalIdentifier = new Map(
|
||||
(marketplaceAppsData?.findManyMarketplaceApps ?? []).map(
|
||||
(marketplaceApp) => [marketplaceApp.universalIdentifier, marketplaceApp],
|
||||
),
|
||||
);
|
||||
|
||||
const filteredTools = allTools
|
||||
.filter((item) => {
|
||||
const searchNormalized = normalizeSearchText(searchTerm);
|
||||
|
||||
const matchesSearch =
|
||||
normalizeSearchText(item.name).includes(searchNormalized) ||
|
||||
normalizeSearchText(item.description ?? '').includes(searchNormalized);
|
||||
|
||||
if (!matchesSearch) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isCustom(item)) {
|
||||
return showStandardTools;
|
||||
}
|
||||
|
||||
if (isManaged(item.applicationId)) {
|
||||
return showManagedTools;
|
||||
}
|
||||
|
||||
return showCustomTools;
|
||||
})
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
const showSkeleton = toolIndexLoading && !toolIndexError;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Custom`}
|
||||
description={t`Custom tools created in your workspace`}
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Tools`}
|
||||
description={t`Use filter to see existing tools or create your own`}
|
||||
/>
|
||||
<StyledSearchContainer>
|
||||
<SearchInput
|
||||
placeholder={t`Search a tool...`}
|
||||
value={searchTerm}
|
||||
onChange={setSearchTerm}
|
||||
filterDropdown={(filterButton: ReactNode) => (
|
||||
<Dropdown
|
||||
dropdownId="settings-tools-filter-dropdown"
|
||||
dropdownPlacement="bottom-end"
|
||||
dropdownOffset={{ x: 0, y: 8 }}
|
||||
clickableComponent={filterButton}
|
||||
dropdownComponents={
|
||||
<DropdownContent>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItemToggle
|
||||
LeftIcon={IconTool}
|
||||
onToggleChange={() =>
|
||||
setShowCustomTools(!showCustomTools)
|
||||
}
|
||||
toggled={showCustomTools}
|
||||
text={t`Custom`}
|
||||
toggleSize="small"
|
||||
/>
|
||||
<MenuItemToggle
|
||||
LeftIcon={IconLock}
|
||||
onToggleChange={() =>
|
||||
setShowManagedTools(!showManagedTools)
|
||||
}
|
||||
toggled={showManagedTools}
|
||||
text={t`Managed`}
|
||||
toggleSize="small"
|
||||
/>
|
||||
<MenuItemToggle
|
||||
LeftIcon={IconPuzzle}
|
||||
onToggleChange={() =>
|
||||
setShowStandardTools(!showStandardTools)
|
||||
}
|
||||
toggled={showStandardTools}
|
||||
text={t`Standard`}
|
||||
toggleSize="small"
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<StyledSearchContainer>
|
||||
<SearchInput
|
||||
placeholder={t`Search a custom tool...`}
|
||||
value={customSearchTerm}
|
||||
onChange={setCustomSearchTerm}
|
||||
/>
|
||||
</StyledSearchContainer>
|
||||
<Table>
|
||||
<StyledTableHeaderRowContainer>
|
||||
<TableRow
|
||||
gridTemplateColumns={TOOL_TABLE_ROW_GRID_TEMPLATE_COLUMNS}
|
||||
>
|
||||
<TableHeader>{t`Name`}</TableHeader>
|
||||
<TableHeader align="right">{t`Type`}</TableHeader>
|
||||
<TableHeader />
|
||||
</TableRow>
|
||||
</StyledTableHeaderRowContainer>
|
||||
{showSkeleton
|
||||
? Array.from({ length: 3 }).map((_, index) => (
|
||||
<Skeleton height={32} borderRadius={4} key={index} />
|
||||
))
|
||||
: filteredTools.map((tool) => (
|
||||
</StyledSearchContainer>
|
||||
<Table>
|
||||
<StyledTableHeaderRowContainer>
|
||||
<TableRow gridTemplateColumns={TOOL_TABLE_ROW_GRID_TEMPLATE_COLUMNS}>
|
||||
<TableHeader>{t`Name`}</TableHeader>
|
||||
<TableHeader>{t`App`}</TableHeader>
|
||||
<TableHeader />
|
||||
</TableRow>
|
||||
</StyledTableHeaderRowContainer>
|
||||
{showSkeleton
|
||||
? Array.from({ length: 3 }).map((_, index) => (
|
||||
<SkeletonTheme
|
||||
key={index}
|
||||
baseColor={theme.background.tertiary}
|
||||
highlightColor={theme.background.transparent.lighter}
|
||||
borderRadius={4}
|
||||
>
|
||||
<Skeleton height={32} borderRadius={4} />
|
||||
</SkeletonTheme>
|
||||
))
|
||||
: filteredTools.map((item) => {
|
||||
const application = isDefined(item.applicationId)
|
||||
? applicationById.get(item.applicationId)
|
||||
: undefined;
|
||||
const marketplaceApp = isDefined(application)
|
||||
? marketplaceAppByUniversalIdentifier.get(
|
||||
application.universalIdentifier,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const appLabel = isCustom(item)
|
||||
? (application?.name ?? t`Custom`)
|
||||
: t`Standard`;
|
||||
|
||||
return (
|
||||
<SettingsToolTableRow
|
||||
key={tool.id}
|
||||
tool={tool}
|
||||
key={item.identifier}
|
||||
leftIcon={
|
||||
<SettingsToolIcon
|
||||
icon={item.icon}
|
||||
toolName={item.name}
|
||||
objectName={item.objectName ?? undefined}
|
||||
application={application}
|
||||
marketplaceApp={marketplaceApp}
|
||||
/>
|
||||
}
|
||||
name={item.name}
|
||||
appLabel={appLabel}
|
||||
action={
|
||||
<IconChevronRight
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
}
|
||||
link={getToolLink(tool)}
|
||||
link={getToolLink(item)}
|
||||
/>
|
||||
))}
|
||||
</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`}
|
||||
/>
|
||||
<StyledSearchContainer>
|
||||
<SearchInput
|
||||
placeholder={t`Search a built-in tool...`}
|
||||
value={builtInSearchTerm}
|
||||
onChange={setBuiltInSearchTerm}
|
||||
/>
|
||||
</StyledSearchContainer>
|
||||
<Table>
|
||||
<StyledTableHeaderRowContainer>
|
||||
<TableRow
|
||||
gridTemplateColumns={TOOL_TABLE_ROW_GRID_TEMPLATE_COLUMNS}
|
||||
>
|
||||
<TableHeader>{t`Name`}</TableHeader>
|
||||
<TableHeader align="right">{t`Type`}</TableHeader>
|
||||
<TableHeader />
|
||||
</TableRow>
|
||||
</StyledTableHeaderRowContainer>
|
||||
{filteredSystemTools.map((systemTool) => (
|
||||
<SettingsSystemToolTableRow
|
||||
key={systemTool.name}
|
||||
tool={systemTool}
|
||||
/>
|
||||
))}
|
||||
</Table>
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</Table>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-2
@@ -1,9 +1,8 @@
|
||||
import { type ToolSet } from 'ai';
|
||||
import { type CodeExecutionData } from 'twenty-shared/ai';
|
||||
import { type CodeExecutionData, type ToolCategory } from 'twenty-shared/ai';
|
||||
import { type ActorMetadata } from 'twenty-shared/types';
|
||||
|
||||
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
|
||||
import { type ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import {
|
||||
type ToolDescriptor,
|
||||
type ToolIndexEntry,
|
||||
|
||||
+2
-1
@@ -9,7 +9,7 @@ import {
|
||||
type ToolProviderContext,
|
||||
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
|
||||
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { ToolCategory } from 'twenty-shared/ai';
|
||||
import {
|
||||
type StaticToolHandler,
|
||||
ToolExecutorService,
|
||||
@@ -159,6 +159,7 @@ export class ActionToolProvider implements ToolProvider {
|
||||
name: toolId,
|
||||
description: tool.description,
|
||||
category: ToolCategory.ACTION,
|
||||
icon: 'IconPlayerPlay',
|
||||
...(includeSchemas && {
|
||||
inputSchema: z.toJSONSchema(tool.inputSchema as z.ZodType),
|
||||
}),
|
||||
|
||||
+29
-1
@@ -9,7 +9,8 @@ import {
|
||||
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
|
||||
import { DASHBOARD_TOOL_SERVICE_TOKEN } from 'src/engine/core-modules/tool-provider/constants/dashboard-tool-service.token';
|
||||
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { ToolCategory } from 'twenty-shared/ai';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service';
|
||||
import {
|
||||
type ToolDescriptor,
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
} from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
import { toolSetToDescriptors } from 'src/engine/core-modules/tool-provider/utils/tool-set-to-descriptors.util';
|
||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import type { DashboardToolWorkspaceService } from 'src/modules/dashboard/tools/services/dashboard-tool.workspace-service';
|
||||
|
||||
@Injectable()
|
||||
@@ -29,6 +31,7 @@ export class DashboardToolProvider implements ToolProvider, OnModuleInit {
|
||||
private readonly dashboardToolService: DashboardToolWorkspaceService | null,
|
||||
private readonly permissionsService: PermissionsService,
|
||||
private readonly toolExecutorService: ToolExecutorService,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
) {}
|
||||
|
||||
onModuleInit(): void {
|
||||
@@ -71,8 +74,33 @@ export class DashboardToolProvider implements ToolProvider, OnModuleInit {
|
||||
context.rolePermissionConfig,
|
||||
);
|
||||
|
||||
const icon = await this.resolveObjectIcon(
|
||||
context.workspaceId,
|
||||
CoreObjectNameSingular.Dashboard,
|
||||
);
|
||||
|
||||
return toolSetToDescriptors(toolSet, ToolCategory.DASHBOARD, {
|
||||
includeSchemas: options?.includeSchemas ?? true,
|
||||
icon,
|
||||
});
|
||||
}
|
||||
|
||||
private async resolveObjectIcon(
|
||||
workspaceId: string,
|
||||
nameSingular: string,
|
||||
): Promise<string | undefined> {
|
||||
const { flatObjectMetadataMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatObjectMetadataMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const flatObject = Object.values(
|
||||
flatObjectMetadataMaps.byUniversalIdentifier,
|
||||
).find((obj) => obj?.nameSingular === nameSingular);
|
||||
|
||||
return flatObject?.icon ?? undefined;
|
||||
}
|
||||
}
|
||||
|
||||
+8
-1
@@ -21,7 +21,7 @@ import { generateUpdateRecordInputSchema } from 'src/engine/core-modules/record-
|
||||
import { DeleteToolInputSchema } from 'src/engine/core-modules/record-crud/zod-schemas/delete-tool.zod-schema';
|
||||
import { FindOneToolInputSchema } from 'src/engine/core-modules/record-crud/zod-schemas/find-one-tool.zod-schema';
|
||||
import { generateFindToolInputSchema } from 'src/engine/core-modules/record-crud/zod-schemas/find-tool.zod-schema';
|
||||
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { ToolCategory } from 'twenty-shared/ai';
|
||||
import {
|
||||
type ToolDescriptor,
|
||||
type ToolIndexEntry,
|
||||
@@ -122,6 +122,7 @@ export class DatabaseToolProvider implements ToolProvider {
|
||||
operation: 'find',
|
||||
},
|
||||
objectName: objectMetadata.nameSingular,
|
||||
icon: flatObject.icon ?? undefined,
|
||||
operation: 'find',
|
||||
});
|
||||
|
||||
@@ -138,6 +139,7 @@ export class DatabaseToolProvider implements ToolProvider {
|
||||
operation: 'find_one',
|
||||
},
|
||||
objectName: objectMetadata.nameSingular,
|
||||
icon: flatObject.icon ?? undefined,
|
||||
operation: 'find_one',
|
||||
});
|
||||
}
|
||||
@@ -158,6 +160,7 @@ export class DatabaseToolProvider implements ToolProvider {
|
||||
operation: 'create',
|
||||
},
|
||||
objectName: objectMetadata.nameSingular,
|
||||
icon: flatObject.icon ?? undefined,
|
||||
operation: 'create',
|
||||
});
|
||||
|
||||
@@ -179,6 +182,7 @@ export class DatabaseToolProvider implements ToolProvider {
|
||||
operation: 'create_many',
|
||||
},
|
||||
objectName: objectMetadata.nameSingular,
|
||||
icon: flatObject.icon ?? undefined,
|
||||
operation: 'create_many',
|
||||
});
|
||||
|
||||
@@ -197,6 +201,7 @@ export class DatabaseToolProvider implements ToolProvider {
|
||||
operation: 'update',
|
||||
},
|
||||
objectName: objectMetadata.nameSingular,
|
||||
icon: flatObject.icon ?? undefined,
|
||||
operation: 'update',
|
||||
});
|
||||
|
||||
@@ -218,6 +223,7 @@ export class DatabaseToolProvider implements ToolProvider {
|
||||
operation: 'update_many',
|
||||
},
|
||||
objectName: objectMetadata.nameSingular,
|
||||
icon: flatObject.icon ?? undefined,
|
||||
operation: 'update_many',
|
||||
});
|
||||
}
|
||||
@@ -236,6 +242,7 @@ export class DatabaseToolProvider implements ToolProvider {
|
||||
operation: 'delete',
|
||||
},
|
||||
objectName: objectMetadata.nameSingular,
|
||||
icon: flatObject.icon ?? undefined,
|
||||
operation: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import {
|
||||
type ToolProviderContext,
|
||||
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
|
||||
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { ToolCategory } from 'twenty-shared/ai';
|
||||
import {
|
||||
type ToolDescriptor,
|
||||
type ToolIndexEntry,
|
||||
|
||||
+2
-1
@@ -8,7 +8,7 @@ import {
|
||||
type ToolProviderContext,
|
||||
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
|
||||
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { ToolCategory } from 'twenty-shared/ai';
|
||||
import { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service';
|
||||
import {
|
||||
type ToolDescriptor,
|
||||
@@ -62,6 +62,7 @@ export class MetadataToolProvider implements ToolProvider, OnModuleInit {
|
||||
|
||||
return toolSetToDescriptors(toolSet, ToolCategory.METADATA, {
|
||||
includeSchemas: options?.includeSchemas ?? true,
|
||||
icon: 'IconSettings',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ import {
|
||||
type ToolProviderContext,
|
||||
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
|
||||
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { ToolCategory } from 'twenty-shared/ai';
|
||||
import { AgentModelConfigService } from 'src/engine/metadata-modules/ai/ai-models/services/agent-model-config.service';
|
||||
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
|
||||
|
||||
|
||||
+2
-1
@@ -8,7 +8,7 @@ import {
|
||||
type ToolProviderContext,
|
||||
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
|
||||
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { ToolCategory } from 'twenty-shared/ai';
|
||||
import { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service';
|
||||
import {
|
||||
type ToolDescriptor,
|
||||
@@ -64,6 +64,7 @@ export class ViewFieldToolProvider implements ToolProvider, OnModuleInit {
|
||||
): Promise<(ToolIndexEntry | ToolDescriptor)[]> {
|
||||
const schemaOptions = {
|
||||
includeSchemas: options?.includeSchemas ?? true,
|
||||
icon: 'IconTable',
|
||||
};
|
||||
|
||||
const readTools = this.viewFieldToolsFactory.generateReadTools(
|
||||
|
||||
+2
-1
@@ -8,7 +8,7 @@ import {
|
||||
type ToolProviderContext,
|
||||
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
|
||||
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { ToolCategory } from 'twenty-shared/ai';
|
||||
import { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service';
|
||||
import {
|
||||
type ToolDescriptor,
|
||||
@@ -74,6 +74,7 @@ export class ViewToolProvider implements ToolProvider, OnModuleInit {
|
||||
const workspaceMemberId = context.actorContext?.workspaceMemberId;
|
||||
const schemaOptions = {
|
||||
includeSchemas: options?.includeSchemas ?? true,
|
||||
icon: 'IconTable',
|
||||
};
|
||||
|
||||
const readTools = this.viewToolsFactory.generateReadTools(
|
||||
|
||||
+29
-1
@@ -9,7 +9,8 @@ import {
|
||||
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
|
||||
import { WORKFLOW_TOOL_SERVICE_TOKEN } from 'src/engine/core-modules/tool-provider/constants/workflow-tool-service.token';
|
||||
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { ToolCategory } from 'twenty-shared/ai';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service';
|
||||
import {
|
||||
type ToolDescriptor,
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
} from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
import { toolSetToDescriptors } from 'src/engine/core-modules/tool-provider/utils/tool-set-to-descriptors.util';
|
||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import type { WorkflowToolWorkspaceService } from 'src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service';
|
||||
|
||||
@Injectable()
|
||||
@@ -29,6 +31,7 @@ export class WorkflowToolProvider implements ToolProvider, OnModuleInit {
|
||||
private readonly workflowToolService: WorkflowToolWorkspaceService | null,
|
||||
private readonly permissionsService: PermissionsService,
|
||||
private readonly toolExecutorService: ToolExecutorService,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
) {}
|
||||
|
||||
onModuleInit(): void {
|
||||
@@ -71,8 +74,33 @@ export class WorkflowToolProvider implements ToolProvider, OnModuleInit {
|
||||
context.rolePermissionConfig,
|
||||
);
|
||||
|
||||
const icon = await this.resolveObjectIcon(
|
||||
context.workspaceId,
|
||||
CoreObjectNameSingular.Workflow,
|
||||
);
|
||||
|
||||
return toolSetToDescriptors(toolSet, ToolCategory.WORKFLOW, {
|
||||
includeSchemas: options?.includeSchemas ?? true,
|
||||
icon,
|
||||
});
|
||||
}
|
||||
|
||||
private async resolveObjectIcon(
|
||||
workspaceId: string,
|
||||
nameSingular: string,
|
||||
): Promise<string | undefined> {
|
||||
const { flatObjectMetadataMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatObjectMetadataMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const flatObject = Object.values(
|
||||
flatObjectMetadataMaps.byUniversalIdentifier,
|
||||
).find((obj) => obj?.nameSingular === nameSingular);
|
||||
|
||||
return flatObject?.icon ?? undefined;
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -28,6 +28,9 @@ export class ToolIndexEntryDTO {
|
||||
@Field({ nullable: true })
|
||||
objectName?: string;
|
||||
|
||||
@Field({ nullable: true })
|
||||
icon?: string;
|
||||
|
||||
@Field(() => graphqlTypeJson, { nullable: true })
|
||||
inputSchema?: object;
|
||||
}
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ import { DeleteRecordService } from 'src/engine/core-modules/record-crud/service
|
||||
import { FindRecordsService } from 'src/engine/core-modules/record-crud/services/find-records.service';
|
||||
import { UpdateManyRecordsService } from 'src/engine/core-modules/record-crud/services/update-many-records.service';
|
||||
import { UpdateRecordService } from 'src/engine/core-modules/record-crud/services/update-record.service';
|
||||
import { type ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { type ToolCategory } from 'twenty-shared/ai';
|
||||
import {
|
||||
type ToolDescriptor,
|
||||
type ToolIndexEntry,
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import {
|
||||
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
|
||||
import { TOOL_PROVIDERS } from 'src/engine/core-modules/tool-provider/constants/tool-providers.token';
|
||||
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { ToolCategory } from 'twenty-shared/ai';
|
||||
import { compactToolOutput } from 'src/engine/core-modules/tool-provider/output-serialization/compact-tool-output.util';
|
||||
import { NativeModelToolProvider } from 'src/engine/core-modules/tool-provider/providers/native-model-tool.provider';
|
||||
import { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service';
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { ToolCategory } from 'twenty-shared/ai';
|
||||
import { type ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
|
||||
import { type ToolIndexEntry } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
|
||||
|
||||
+2
-1
@@ -1,4 +1,4 @@
|
||||
import { type ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { type ToolCategory } from 'twenty-shared/ai';
|
||||
|
||||
export type DatabaseCrudOperation =
|
||||
| 'find'
|
||||
@@ -26,6 +26,7 @@ export type ToolIndexEntry = {
|
||||
executionRef: ToolExecutionRef;
|
||||
objectName?: string;
|
||||
operation?: string;
|
||||
icon?: string;
|
||||
};
|
||||
|
||||
// Full descriptor with schema (on-demand)
|
||||
|
||||
+3
-1
@@ -1,7 +1,7 @@
|
||||
import { type ToolSet } from 'ai';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { type ToolCategory } from 'twenty-shared/ai';
|
||||
import {
|
||||
type ToolDescriptor,
|
||||
type ToolIndexEntry,
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
|
||||
export type ToolSetToDescriptorsOptions = {
|
||||
includeSchemas?: boolean;
|
||||
icon?: string;
|
||||
};
|
||||
|
||||
// Converts a ToolSet (with Zod schemas and closures) into an array of
|
||||
@@ -27,6 +28,7 @@ export const toolSetToDescriptors = (
|
||||
description: tool.description ?? '',
|
||||
category,
|
||||
executionRef: { kind: 'static' as const, toolId: name },
|
||||
...(options?.icon && { icon: options.icon }),
|
||||
};
|
||||
|
||||
if (!includeSchemas) {
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/
|
||||
|
||||
import { isUserAuthContext } from 'src/engine/core-modules/auth/guards/is-user-auth-context.guard';
|
||||
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
|
||||
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { ToolCategory } from 'twenty-shared/ai';
|
||||
import { ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
|
||||
import { type AgentExecutionResult } from 'src/engine/metadata-modules/ai/ai-agent-execution/types/agent-execution-result.type';
|
||||
import { extractCacheCreationTokensFromSteps } from 'src/engine/metadata-modules/ai/ai-billing/utils/extract-cache-creation-tokens.util';
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
import { COMMON_PRELOAD_TOOLS } from 'src/engine/core-modules/tool-provider/constants/common-preload-tools.const';
|
||||
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { ToolCategory } from 'twenty-shared/ai';
|
||||
import { ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
|
||||
import {
|
||||
EXECUTE_TOOL_TOOL_NAME,
|
||||
|
||||
@@ -14,6 +14,7 @@ export type { DataResidency } from './constants/data-residency.const';
|
||||
export { DATA_RESIDENCY_KEYS } from './constants/data-residency.const';
|
||||
export type { NativeAiSdkProviderId } from './constants/native-ai-sdk-provider-ids.const';
|
||||
export { NATIVE_AI_SDK_PROVIDER_IDS } from './constants/native-ai-sdk-provider-ids.const';
|
||||
export { ToolCategory } from './constants/tool-category.const';
|
||||
export type {
|
||||
AgentResponseFieldType,
|
||||
AgentResponseSchema,
|
||||
|
||||
@@ -39,6 +39,7 @@ export enum SettingsPath {
|
||||
AIAgentTurnDetail = 'ai/agents/:agentId/turns/:turnId',
|
||||
AINewSkill = 'ai/new-skill',
|
||||
AISkillDetail = 'ai/skills/:skillId',
|
||||
AIToolDetail = 'ai/tools/:toolIdentifier',
|
||||
Applications = 'applications',
|
||||
ApplicationDetail = 'applications/:applicationId',
|
||||
ApplicationLogicFunctionDetail = 'applications/:applicationId/logicFunctions/:logicFunctionId',
|
||||
|
||||
Reference in New Issue
Block a user