Show tool execution messages in AI agent chat (#13117)
https://github.com/user-attachments/assets/c0a42726-50ac-496e-a993-9d6076a84a6a --------- Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
@ -1042,6 +1042,7 @@ export enum MessageChannelVisibility {
|
||||
|
||||
export enum ModelProvider {
|
||||
ANTHROPIC = 'ANTHROPIC',
|
||||
NONE = 'NONE',
|
||||
OPENAI = 'OPENAI'
|
||||
}
|
||||
|
||||
@ -2785,6 +2786,7 @@ export type Workspace = {
|
||||
customDomain?: Maybe<Scalars['String']>;
|
||||
databaseSchema: Scalars['String'];
|
||||
databaseUrl: Scalars['String'];
|
||||
defaultAgent?: Maybe<Agent>;
|
||||
defaultRole?: Maybe<Role>;
|
||||
deletedAt?: Maybe<Scalars['DateTime']>;
|
||||
displayName?: Maybe<Scalars['String']>;
|
||||
@ -3599,7 +3601,7 @@ export type FindOneServerlessFunctionSourceCodeQueryVariables = Exact<{
|
||||
|
||||
export type FindOneServerlessFunctionSourceCodeQuery = { __typename?: 'Query', getServerlessFunctionSourceCode?: any | null };
|
||||
|
||||
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array<SettingPermissionType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, metadata: any }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } };
|
||||
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array<SettingPermissionType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, metadata: any }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null, defaultAgent?: { __typename?: 'Agent', id: any } | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } };
|
||||
|
||||
export type WorkspaceUrlsFragmentFragment = { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null };
|
||||
|
||||
@ -3618,7 +3620,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf
|
||||
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array<SettingPermissionType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, metadata: any }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } } };
|
||||
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array<SettingPermissionType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, metadata: any }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null, defaultAgent?: { __typename?: 'Agent', id: any } | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } } };
|
||||
|
||||
export type ActivateWorkflowVersionMutationVariables = Exact<{
|
||||
workflowVersionId: Scalars['String'];
|
||||
@ -4042,6 +4044,9 @@ export const UserQueryFragmentFragmentDoc = gql`
|
||||
defaultRole {
|
||||
...RoleFragment
|
||||
}
|
||||
defaultAgent {
|
||||
id
|
||||
}
|
||||
}
|
||||
availableWorkspaces {
|
||||
...AvailableWorkspacesFragment
|
||||
|
||||
@ -999,6 +999,7 @@ export enum MessageChannelVisibility {
|
||||
|
||||
export enum ModelProvider {
|
||||
ANTHROPIC = 'ANTHROPIC',
|
||||
NONE = 'NONE',
|
||||
OPENAI = 'OPENAI'
|
||||
}
|
||||
|
||||
@ -2613,6 +2614,7 @@ export type Workspace = {
|
||||
customDomain?: Maybe<Scalars['String']>;
|
||||
databaseSchema: Scalars['String'];
|
||||
databaseUrl: Scalars['String'];
|
||||
defaultAgent?: Maybe<Agent>;
|
||||
defaultRole?: Maybe<Role>;
|
||||
deletedAt?: Maybe<Scalars['DateTime']>;
|
||||
displayName?: Maybe<Scalars['String']>;
|
||||
|
||||
@ -6,7 +6,7 @@ import { ActionType } from '@/action-menu/actions/types/ActionType';
|
||||
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { IconSearch } from 'twenty-ui/display';
|
||||
import { IconSearch, IconSparkles } from 'twenty-ui/display';
|
||||
|
||||
export const RECORD_AGNOSTIC_ACTIONS_CONFIG: Record<string, ActionConfig> = {
|
||||
[RecordAgnosticActionsKeys.SEARCH_RECORDS]: {
|
||||
@ -50,4 +50,24 @@ export const RECORD_AGNOSTIC_ACTIONS_CONFIG: Record<string, ActionConfig> = {
|
||||
hotKeys: ['/'],
|
||||
shouldBeRegistered: () => true,
|
||||
},
|
||||
[RecordAgnosticActionsKeys.ASK_AI]: {
|
||||
type: ActionType.Standard,
|
||||
scope: ActionScope.Global,
|
||||
key: RecordAgnosticActionsKeys.ASK_AI,
|
||||
label: msg`Ask AI`,
|
||||
shortLabel: msg`Ask AI`,
|
||||
position: 2,
|
||||
isPinned: false,
|
||||
Icon: IconSparkles,
|
||||
availableOn: [ActionViewType.GLOBAL],
|
||||
component: (
|
||||
<ActionOpenSidePanelPage
|
||||
page={CommandMenuPages.AskAI}
|
||||
pageTitle="Ask AI"
|
||||
pageIcon={IconSparkles}
|
||||
/>
|
||||
),
|
||||
hotKeys: ['@'],
|
||||
shouldBeRegistered: () => true,
|
||||
},
|
||||
};
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
import { RECORD_AGNOSTIC_ACTIONS_CONFIG } from '@/action-menu/actions/record-agnostic-actions/constants/RecordAgnosticActionsConfig';
|
||||
import { RecordAgnosticActionsKeys } from '@/action-menu/actions/record-agnostic-actions/types/RecordAgnosticActionsKeys';
|
||||
import { ActionConfig } from '@/action-menu/actions/types/ActionConfig';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
|
||||
export const useRecordAgnosticActions = () => {
|
||||
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
|
||||
|
||||
const actions: Record<string, ActionConfig> = {
|
||||
[RecordAgnosticActionsKeys.SEARCH_RECORDS]:
|
||||
RECORD_AGNOSTIC_ACTIONS_CONFIG[RecordAgnosticActionsKeys.SEARCH_RECORDS],
|
||||
[RecordAgnosticActionsKeys.SEARCH_RECORDS_FALLBACK]:
|
||||
RECORD_AGNOSTIC_ACTIONS_CONFIG[
|
||||
RecordAgnosticActionsKeys.SEARCH_RECORDS_FALLBACK
|
||||
],
|
||||
};
|
||||
|
||||
if (isAiEnabled) {
|
||||
actions[RecordAgnosticActionsKeys.ASK_AI] =
|
||||
RECORD_AGNOSTIC_ACTIONS_CONFIG[RecordAgnosticActionsKeys.ASK_AI];
|
||||
}
|
||||
|
||||
return actions;
|
||||
};
|
||||
@ -1,4 +1,5 @@
|
||||
export enum RecordAgnosticActionsKeys {
|
||||
SEARCH_RECORDS = 'search-records',
|
||||
SEARCH_RECORDS_FALLBACK = 'search-records-fallback',
|
||||
ASK_AI = 'ask-ai',
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { RECORD_AGNOSTIC_ACTIONS_CONFIG } from '@/action-menu/actions/record-agnostic-actions/constants/RecordAgnosticActionsConfig';
|
||||
import { useRecordAgnosticActions } from '@/action-menu/actions/record-agnostic-actions/hooks/useRecordAgnosticActions';
|
||||
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
|
||||
import { ShouldBeRegisteredFunctionParams } from '@/action-menu/actions/types/ShouldBeRegisteredFunctionParams';
|
||||
import { getActionConfig } from '@/action-menu/actions/utils/getActionConfig';
|
||||
@ -30,7 +30,7 @@ export const useRegisteredActions = (
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const recordAgnosticActionConfig = RECORD_AGNOSTIC_ACTIONS_CONFIG;
|
||||
const recordAgnosticActionConfig = useRecordAgnosticActions();
|
||||
|
||||
const actionsConfig = {
|
||||
...recordActionConfig,
|
||||
|
||||
@ -2,7 +2,10 @@ import {
|
||||
ApolloClient,
|
||||
ApolloClientOptions,
|
||||
ApolloLink,
|
||||
FetchResult,
|
||||
fromPromise,
|
||||
Observable,
|
||||
Operation,
|
||||
ServerError,
|
||||
ServerParseError,
|
||||
} from '@apollo/client';
|
||||
@ -19,7 +22,12 @@ import { AuthTokenPair } from '~/generated/graphql';
|
||||
import { logDebug } from '~/utils/logDebug';
|
||||
|
||||
import { i18n } from '@lingui/core';
|
||||
import { GraphQLFormattedError } from 'graphql';
|
||||
import {
|
||||
DefinitionNode,
|
||||
DirectiveNode,
|
||||
GraphQLFormattedError,
|
||||
SelectionNode,
|
||||
} from 'graphql';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { getGenericOperationName, isDefined } from 'twenty-shared/utils';
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
@ -115,45 +123,45 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
|
||||
},
|
||||
attempts: {
|
||||
max: 2,
|
||||
retryIf: (error) => !!error,
|
||||
retryIf: (error) => {
|
||||
if (this.isAuthenticationError(error)) {
|
||||
return false;
|
||||
}
|
||||
return Boolean(error);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const handleTokenRenewal = (
|
||||
operation: Operation,
|
||||
forward: (operation: Operation) => Observable<FetchResult>,
|
||||
) => {
|
||||
return fromPromise(
|
||||
renewToken(uri, getTokenPair())
|
||||
.then((tokens) => {
|
||||
if (isDefined(tokens)) {
|
||||
onTokenPairChange?.(tokens);
|
||||
cookieStorage.setItem('tokenPair', JSON.stringify(tokens));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
onUnauthenticatedError?.();
|
||||
}),
|
||||
).flatMap(() => forward(operation));
|
||||
};
|
||||
|
||||
const errorLink = onError(
|
||||
({ graphQLErrors, networkError, forward, operation }) => {
|
||||
if (isDefined(graphQLErrors)) {
|
||||
onErrorCb?.(graphQLErrors);
|
||||
for (const graphQLError of graphQLErrors) {
|
||||
if (graphQLError.message === 'Unauthorized') {
|
||||
return fromPromise(
|
||||
renewToken(uri, getTokenPair())
|
||||
.then((tokens) => {
|
||||
if (isDefined(tokens)) {
|
||||
onTokenPairChange?.(tokens);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
onUnauthenticatedError?.();
|
||||
}),
|
||||
).flatMap(() => forward(operation));
|
||||
return handleTokenRenewal(operation, forward);
|
||||
}
|
||||
|
||||
switch (graphQLError?.extensions?.code) {
|
||||
case 'UNAUTHENTICATED': {
|
||||
return fromPromise(
|
||||
renewToken(uri, getTokenPair())
|
||||
.then((tokens) => {
|
||||
if (isDefined(tokens)) {
|
||||
onTokenPairChange?.(tokens);
|
||||
cookieStorage.setItem(
|
||||
'tokenPair',
|
||||
JSON.stringify(tokens),
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
onUnauthenticatedError?.();
|
||||
}),
|
||||
).flatMap(() => forward(operation));
|
||||
return handleTokenRenewal(operation, forward);
|
||||
}
|
||||
case 'FORBIDDEN': {
|
||||
return;
|
||||
@ -220,6 +228,13 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
|
||||
}
|
||||
|
||||
if (isDefined(networkError)) {
|
||||
if (
|
||||
this.isRestOperation(operation) &&
|
||||
this.isAuthenticationError(networkError as ServerError)
|
||||
) {
|
||||
return handleTokenRenewal(operation, forward);
|
||||
}
|
||||
|
||||
if (isDebugMode === true) {
|
||||
logDebug(`[Network error]: ${networkError}`);
|
||||
}
|
||||
@ -248,6 +263,26 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
|
||||
});
|
||||
}
|
||||
|
||||
private isRestOperation(operation: Operation): boolean {
|
||||
return operation.query.definitions.some(
|
||||
(def: DefinitionNode) =>
|
||||
def.kind === 'OperationDefinition' &&
|
||||
def.selectionSet?.selections.some(
|
||||
(selection: SelectionNode) =>
|
||||
selection.kind === 'Field' &&
|
||||
selection.directives?.some(
|
||||
(directive: DirectiveNode) =>
|
||||
directive.name.value === 'rest' ||
|
||||
directive.name.value === 'stream',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private isAuthenticationError(error: ServerError): boolean {
|
||||
return error.statusCode === 401;
|
||||
}
|
||||
|
||||
updateWorkspaceMember(workspaceMember: CurrentWorkspaceMember | null) {
|
||||
this.currentWorkspaceMember = workspaceMember;
|
||||
}
|
||||
|
||||
@ -1,4 +1,9 @@
|
||||
import { ApolloLink, Observable, Operation } from '@apollo/client/core';
|
||||
import {
|
||||
ApolloLink,
|
||||
Observable,
|
||||
Operation,
|
||||
ServerError,
|
||||
} from '@apollo/client/core';
|
||||
import { FetchResult } from '@apollo/client/link/core';
|
||||
import { ArgumentNode, DirectiveNode } from 'graphql';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@ -57,7 +62,13 @@ export class StreamingRestLink extends ApolloLink {
|
||||
fetch(url, requestConfig)
|
||||
.then(async (response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
const networkError = new Error(
|
||||
`HTTP error! status: ${response.status}`,
|
||||
) as ServerError;
|
||||
|
||||
networkError.statusCode = response.status;
|
||||
|
||||
throw networkError;
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
@ -66,7 +77,6 @@ export class StreamingRestLink extends ApolloLink {
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let accumulatedData = '';
|
||||
|
||||
let isStreaming = true;
|
||||
while (isStreaming) {
|
||||
@ -79,19 +89,9 @@ export class StreamingRestLink extends ApolloLink {
|
||||
}
|
||||
|
||||
const decodedChunk = decoder.decode(value, { stream: true });
|
||||
accumulatedData += decodedChunk;
|
||||
|
||||
if (isDefined(onChunk) && typeof onChunk === 'function') {
|
||||
onChunk(accumulatedData);
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedData = JSON.parse(decodedChunk);
|
||||
observer.next({ data: parsedData });
|
||||
} catch {
|
||||
observer.next({
|
||||
data: { streamingData: decodedChunk },
|
||||
});
|
||||
onChunk(decodedChunk);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -25,6 +25,7 @@ export type CurrentWorkspace = Pick<
|
||||
| 'metadataVersion'
|
||||
> & {
|
||||
defaultRole?: Omit<Role, 'workspaceMembers'> | null;
|
||||
defaultAgent?: { id: string } | null;
|
||||
};
|
||||
|
||||
export const currentWorkspaceState = createState<CurrentWorkspace | null>({
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { CommandMenu } from '@/command-menu/components/CommandMenu';
|
||||
import { CommandMenuAskAIPage } from '@/command-menu/pages/ask-ai/components/CommandMenuAskAIPage';
|
||||
import { CommandMenuCalendarEventPage } from '@/command-menu/pages/calendar-event/components/CommandMenuCalendarEventPage';
|
||||
import { CommandMenuMessageThreadPage } from '@/command-menu/pages/message-thread/components/CommandMenuMessageThreadPage';
|
||||
import { CommandMenuRecordPage } from '@/command-menu/pages/record-page/components/CommandMenuRecordPage';
|
||||
@ -32,4 +33,5 @@ export const COMMAND_MENU_PAGES_CONFIG = new Map<
|
||||
[CommandMenuPages.WorkflowStepView, <CommandMenuWorkflowViewStep />],
|
||||
[CommandMenuPages.WorkflowRunStepView, <CommandMenuWorkflowRunViewStep />],
|
||||
[CommandMenuPages.SearchRecords, <CommandMenuSearchRecordsPage />],
|
||||
[CommandMenuPages.AskAI, <CommandMenuAskAIPage />],
|
||||
]);
|
||||
|
||||
@ -2,6 +2,7 @@ import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/Com
|
||||
import { SIDE_PANEL_FOCUS_ID } from '@/command-menu/constants/SidePanelFocusId';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { useCommandMenuHistory } from '@/command-menu/hooks/useCommandMenuHistory';
|
||||
import { useOpenAskAIPageInCommandMenu } from '@/command-menu/hooks/useOpenAskAIPageInCommandMenu';
|
||||
import { useOpenRecordsSearchPageInCommandMenu } from '@/command-menu/hooks/useOpenRecordsSearchPageInCommandMenu';
|
||||
import { useSetGlobalCommandMenuContext } from '@/command-menu/hooks/useSetGlobalCommandMenuContext';
|
||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||
@ -12,15 +13,19 @@ import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeybo
|
||||
import { useGlobalHotkeys } from '@/ui/utilities/hotkey/hooks/useGlobalHotkeys';
|
||||
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
|
||||
export const useCommandMenuHotKeys = () => {
|
||||
const { toggleCommandMenu } = useCommandMenu();
|
||||
|
||||
const { openRecordsSearchPage } = useOpenRecordsSearchPageInCommandMenu();
|
||||
|
||||
const { openAskAIPage } = useOpenAskAIPageInCommandMenu();
|
||||
|
||||
const { goBackFromCommandMenu } = useCommandMenuHistory();
|
||||
|
||||
const { setGlobalCommandMenuContext } = useSetGlobalCommandMenuContext();
|
||||
@ -31,6 +36,8 @@ export const useCommandMenuHotKeys = () => {
|
||||
|
||||
const commandMenuPage = useRecoilValue(commandMenuPageState);
|
||||
|
||||
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
|
||||
|
||||
const contextStoreTargetedRecordsRuleComponent = useRecoilComponentValueV2(
|
||||
contextStoreTargetedRecordsRuleComponentState,
|
||||
COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
||||
@ -58,6 +65,20 @@ export const useCommandMenuHotKeys = () => {
|
||||
},
|
||||
);
|
||||
|
||||
useGlobalHotkeys(
|
||||
['@'],
|
||||
() => {
|
||||
if (isAiEnabled) {
|
||||
openAskAIPage();
|
||||
}
|
||||
},
|
||||
false,
|
||||
[openAskAIPage, isAiEnabled],
|
||||
{
|
||||
ignoreModifiers: true,
|
||||
},
|
||||
);
|
||||
|
||||
useHotkeysOnFocusedElement({
|
||||
keys: [Key.Escape],
|
||||
callback: () => {
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { IconSparkles } from 'twenty-ui/display';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
export const useOpenAskAIPageInCommandMenu = () => {
|
||||
const { navigateCommandMenu } = useCommandMenu();
|
||||
|
||||
const openAskAIPage = () => {
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.AskAI,
|
||||
pageTitle: t`Ask AI`,
|
||||
pageIcon: IconSparkles,
|
||||
pageId: v4(),
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
openAskAIPage,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,37 @@
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { AIChatTab } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/components/AIChatTab';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledEmptyState = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
display: flex;
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
export const CommandMenuAskAIPage = () => {
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
const agentId = currentWorkspace?.defaultAgent?.id;
|
||||
|
||||
if (!agentId) {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledEmptyState>No AI Agent found.</StyledEmptyState>
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<AIChatTab agentId={agentId} />
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@ -11,4 +11,5 @@ export enum CommandMenuPages {
|
||||
WorkflowStepEdit = 'workflow-step-edit',
|
||||
WorkflowRunStepView = 'workflow-run-step-view',
|
||||
SearchRecords = 'search-records',
|
||||
AskAI = 'ask-ai',
|
||||
}
|
||||
|
||||
@ -1,14 +1,17 @@
|
||||
import { useOpenAskAIPageInCommandMenu } from '@/command-menu/hooks/useOpenAskAIPageInCommandMenu';
|
||||
import { useOpenRecordsSearchPageInCommandMenu } from '@/command-menu/hooks/useOpenRecordsSearchPageInCommandMenu';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
|
||||
import { navigationDrawerExpandedMemorizedState } from '@/ui/navigation/states/navigationDrawerExpandedMemorizedState';
|
||||
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { IconSearch, IconSettings } from 'twenty-ui/display';
|
||||
import { IconSearch, IconSettings, IconSparkles } from 'twenty-ui/display';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||
|
||||
export const MainNavigationDrawerFixedItems = () => {
|
||||
@ -29,6 +32,9 @@ export const MainNavigationDrawerFixedItems = () => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const { openRecordsSearchPage } = useOpenRecordsSearchPageInCommandMenu();
|
||||
const { openAskAIPage } = useOpenAskAIPageInCommandMenu();
|
||||
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
|
||||
|
||||
return (
|
||||
!isMobile && (
|
||||
<>
|
||||
@ -39,6 +45,15 @@ export const MainNavigationDrawerFixedItems = () => {
|
||||
keyboard={['/']}
|
||||
mouseUpNavigation={true}
|
||||
/>
|
||||
{isAiEnabled && (
|
||||
<NavigationDrawerItem
|
||||
label={t`Ask AI`}
|
||||
Icon={IconSparkles}
|
||||
onClick={openAskAIPage}
|
||||
keyboard={['@']}
|
||||
mouseUpNavigation={true}
|
||||
/>
|
||||
)}
|
||||
<NavigationDrawerItem
|
||||
label={t`Settings`}
|
||||
to={getSettingsPath(SettingsPath.ProfilePage)}
|
||||
|
||||
@ -93,6 +93,8 @@ export const UserProviderEffect = () => {
|
||||
setCurrentWorkspace({
|
||||
...queryData.currentUser.currentWorkspace,
|
||||
defaultRole: queryData.currentUser.currentWorkspace.defaultRole ?? null,
|
||||
defaultAgent:
|
||||
queryData.currentUser.currentWorkspace.defaultAgent ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { OBJECT_PERMISSION_FRAGMENT } from '@/settings/roles/graphql/fragments/objectPermissionFragment';
|
||||
import { ROLE_FRAGMENT } from '@/settings/roles/graphql/fragments/roleFragment';
|
||||
import { DELETED_WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/deletedWorkspaceMemberQueryFragment';
|
||||
import { WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/workspaceMemberQueryFragment';
|
||||
import { gql } from '@apollo/client';
|
||||
import {
|
||||
AVAILABLE_WORKSPACE_FOR_AUTH_FRAGMENT,
|
||||
AVAILABLE_WORKSPACES_FOR_AUTH_FRAGMENT,
|
||||
} from '@/auth/graphql/fragments/authFragments';
|
||||
import { OBJECT_PERMISSION_FRAGMENT } from '@/settings/roles/graphql/fragments/objectPermissionFragment';
|
||||
import { ROLE_FRAGMENT } from '@/settings/roles/graphql/fragments/roleFragment';
|
||||
import { WORKSPACE_URLS_FRAGMENT } from '@/users/graphql/fragments/workspaceUrlsFragment';
|
||||
import { DELETED_WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/deletedWorkspaceMemberQueryFragment';
|
||||
import { WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/workspaceMemberQueryFragment';
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const USER_QUERY_FRAGMENT = gql`
|
||||
${ROLE_FRAGMENT}
|
||||
@ -92,6 +92,9 @@ export const USER_QUERY_FRAGMENT = gql`
|
||||
defaultRole {
|
||||
...RoleFragment
|
||||
}
|
||||
defaultAgent {
|
||||
id
|
||||
}
|
||||
}
|
||||
availableWorkspaces {
|
||||
...AvailableWorkspacesFragment
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { TextArea } from '@/ui/input/components/TextArea';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { keyframes, useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import React from 'react';
|
||||
import { Avatar, IconDotsVertical, IconSparkles } from 'twenty-ui/display';
|
||||
@ -11,6 +11,7 @@ import { t } from '@lingui/core/macro';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
|
||||
import { useAgentChat } from '../hooks/useAgentChat';
|
||||
import { AgentChatMessage } from '../hooks/useAgentChatMessages';
|
||||
import { AIChatSkeletonLoader } from './AIChatSkeletonLoader';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
@ -86,9 +87,11 @@ const StyledMessageBubble = styled.div<{ isUser?: boolean }>`
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledMessageRow = styled.div`
|
||||
const StyledMessageRow = styled.div<{ isShowingToolCall?: boolean }>`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: ${({ isShowingToolCall }) =>
|
||||
isShowingToolCall ? 'center' : 'flex-start'};
|
||||
gap: ${({ theme }) => theme.spacing(3)};
|
||||
width: 100%;
|
||||
`;
|
||||
@ -152,6 +155,23 @@ const StyledMessageContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const dots = keyframes`
|
||||
0% { content: ''; }
|
||||
33% { content: '.'; }
|
||||
66% { content: '..'; }
|
||||
100% { content: '...'; }
|
||||
`;
|
||||
|
||||
const StyledToolCallContainer = styled.div`
|
||||
&::after {
|
||||
display: inline-block;
|
||||
content: '';
|
||||
animation: ${dots} 750ms steps(3, end) infinite;
|
||||
width: 2ch;
|
||||
text-align: left;
|
||||
}
|
||||
`;
|
||||
|
||||
type AIChatTabProps = {
|
||||
agentId: string;
|
||||
};
|
||||
@ -168,16 +188,49 @@ export const AIChatTab: React.FC<AIChatTabProps> = ({ agentId }) => {
|
||||
agentStreamingMessage,
|
||||
} = useAgentChat(agentId);
|
||||
|
||||
const getAssistantMessageContent = (message: AgentChatMessage) => {
|
||||
if (message.content !== '') {
|
||||
return message.content;
|
||||
}
|
||||
|
||||
if (agentStreamingMessage.streamingText !== '') {
|
||||
return agentStreamingMessage.streamingText;
|
||||
}
|
||||
|
||||
if (agentStreamingMessage.toolCall !== '') {
|
||||
return (
|
||||
<StyledToolCallContainer>
|
||||
{agentStreamingMessage.toolCall}
|
||||
</StyledToolCallContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledDotsIconContainer>
|
||||
<StyledDotsIcon size={theme.icon.size.xl} />
|
||||
</StyledDotsIconContainer>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
{messages.length !== 0 && (
|
||||
<StyledScrollWrapper componentInstanceId={agentId}>
|
||||
<StyledScrollWrapper
|
||||
componentInstanceId={`scroll-wrapper-ai-chat-${agentId}`}
|
||||
>
|
||||
{messages.map((msg) => (
|
||||
<StyledMessageBubble
|
||||
key={msg.id}
|
||||
isUser={msg.role === AgentChatMessageRole.USER}
|
||||
>
|
||||
<StyledMessageRow>
|
||||
<StyledMessageRow
|
||||
isShowingToolCall={
|
||||
msg.role === AgentChatMessageRole.ASSISTANT &&
|
||||
msg.content === '' &&
|
||||
agentStreamingMessage.streamingText === '' &&
|
||||
agentStreamingMessage.toolCall !== ''
|
||||
}
|
||||
>
|
||||
{msg.role === AgentChatMessageRole.ASSISTANT && (
|
||||
<StyledAvatarContainer>
|
||||
<Avatar
|
||||
@ -197,12 +250,8 @@ export const AIChatTab: React.FC<AIChatTabProps> = ({ agentId }) => {
|
||||
<StyledMessageText
|
||||
isUser={msg.role === AgentChatMessageRole.USER}
|
||||
>
|
||||
{msg.role === AgentChatMessageRole.ASSISTANT && !msg.content
|
||||
? agentStreamingMessage || (
|
||||
<StyledDotsIconContainer>
|
||||
<StyledDotsIcon size={theme.icon.size.xl} />
|
||||
</StyledDotsIconContainer>
|
||||
)
|
||||
{msg.role === AgentChatMessageRole.ASSISTANT
|
||||
? getAssistantMessageContent(msg)
|
||||
: msg.content}
|
||||
</StyledMessageText>
|
||||
{msg.content && (
|
||||
|
||||
@ -8,11 +8,11 @@ import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWr
|
||||
import { STREAM_CHAT_QUERY } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/api/agent-chat-apollo.api';
|
||||
import { AgentChatMessageRole } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/constants/agent-chat-message-role';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
import { agentChatInputState } from '../states/agentChatInputState';
|
||||
import { agentChatMessagesComponentState } from '../states/agentChatMessagesComponentState';
|
||||
import { agentStreamingMessageState } from '../states/agentStreamingMessageState';
|
||||
import { parseAgentStreamingChunk } from '../utils/parseAgentStreamingChunk';
|
||||
import { AgentChatMessage, useAgentChatMessages } from './useAgentChatMessages';
|
||||
import { useAgentChatThreads } from './useAgentChatThreads';
|
||||
|
||||
@ -50,21 +50,14 @@ export const useAgentChat = (agentId: string) => {
|
||||
useAgentChatThreads(agentId);
|
||||
const currentThreadId = threads[0]?.id;
|
||||
|
||||
const {
|
||||
data: messagesData,
|
||||
loading: messagesLoading,
|
||||
refetch: refetchMessages,
|
||||
} = useAgentChatMessages(currentThreadId);
|
||||
const { loading: messagesLoading, refetch: refetchMessages } =
|
||||
useAgentChatMessages(currentThreadId, ({ messages }) => {
|
||||
setAgentChatMessages(messages);
|
||||
scrollToBottom();
|
||||
});
|
||||
|
||||
const isLoading = messagesLoading || threadsLoading || isStreaming;
|
||||
|
||||
if (
|
||||
agentChatMessages.length === 0 &&
|
||||
isDefined(messagesData?.messages?.length)
|
||||
) {
|
||||
setAgentChatMessages(messagesData.messages);
|
||||
}
|
||||
|
||||
const createOptimisticMessages = (content: string): AgentChatMessage[] => {
|
||||
const optimisticUserMessage: OptimisticMessage = {
|
||||
id: v4(),
|
||||
@ -104,8 +97,22 @@ export const useAgentChat = (agentId: string) => {
|
||||
},
|
||||
context: {
|
||||
onChunk: (chunk: string) => {
|
||||
setAgentStreamingMessage(chunk);
|
||||
scrollToBottom();
|
||||
parseAgentStreamingChunk(chunk, {
|
||||
onTextDelta: (message: string) => {
|
||||
setAgentStreamingMessage((prev) => ({
|
||||
...prev,
|
||||
streamingText: prev.streamingText + message,
|
||||
}));
|
||||
scrollToBottom();
|
||||
},
|
||||
onToolCall: (message: string) => {
|
||||
setAgentStreamingMessage((prev) => ({
|
||||
...prev,
|
||||
toolCall: message,
|
||||
}));
|
||||
scrollToBottom();
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -128,7 +135,10 @@ export const useAgentChat = (agentId: string) => {
|
||||
const { data } = await refetchMessages();
|
||||
|
||||
setAgentChatMessages(data?.messages);
|
||||
setAgentStreamingMessage('');
|
||||
setAgentStreamingMessage({
|
||||
toolCall: '',
|
||||
streamingText: '',
|
||||
});
|
||||
scrollToBottom();
|
||||
};
|
||||
|
||||
|
||||
@ -11,9 +11,13 @@ export type AgentChatMessage = {
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export const useAgentChatMessages = (threadId: string) => {
|
||||
export const useAgentChatMessages = (
|
||||
threadId: string,
|
||||
onCompleted?: (data: { messages: AgentChatMessage[] }) => void,
|
||||
) => {
|
||||
return useQuery<{ messages: AgentChatMessage[] }>(GET_AGENT_CHAT_MESSAGES, {
|
||||
variables: { threadId },
|
||||
skip: !isDefined(threadId),
|
||||
onCompleted,
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const agentStreamingMessageState = atom<string>({
|
||||
export const agentStreamingMessageState = atom<{
|
||||
toolCall: string;
|
||||
streamingText: string;
|
||||
}>({
|
||||
key: 'agentStreamingMessageState',
|
||||
default: '',
|
||||
default: {
|
||||
toolCall: '',
|
||||
streamingText: '',
|
||||
},
|
||||
});
|
||||
|
||||
@ -0,0 +1,43 @@
|
||||
export type AgentStreamingEvent = {
|
||||
type: 'text-delta' | 'tool-call';
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type AgentStreamingParserCallbacks = {
|
||||
onTextDelta?: (message: string) => void;
|
||||
onToolCall?: (message: string) => void;
|
||||
onError?: (error: Error, rawLine: string) => void;
|
||||
};
|
||||
|
||||
export const parseAgentStreamingChunk = (
|
||||
chunk: string,
|
||||
callbacks: AgentStreamingParserCallbacks,
|
||||
): void => {
|
||||
const lines = chunk.split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.trim() !== '') {
|
||||
try {
|
||||
const event = JSON.parse(line) as AgentStreamingEvent;
|
||||
|
||||
switch (event.type) {
|
||||
case 'text-delta':
|
||||
callbacks.onTextDelta?.(event.message);
|
||||
break;
|
||||
case 'tool-call':
|
||||
callbacks.onToolCall?.(event.message);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to parse stream event:', error);
|
||||
|
||||
const errorMessage =
|
||||
error instanceof Error
|
||||
? error
|
||||
: new Error(`Unknown parsing error: ${String(error)}`);
|
||||
callbacks.onError?.(errorMessage, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user