From 9a3852bf04d8544fcd2f7756e8625010cce8861d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Tue, 24 Feb 2026 10:14:24 +0100 Subject: [PATCH] feat: add two-layer AI model availability filtering (#18170) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - **Admin-level filtering**: New AI tab in admin panel with server-wide model availability controls (whitelist/blacklist via `AI_AUTO_ENABLE_NEW_MODELS`, `AI_DISABLED_MODEL_IDS`, `AI_ENABLED_MODEL_IDS` config variables). Dedicated `setAdminAiModelEnabled` mutation replaces frontend config-variable manipulation. Filter dropdown to show/hide unconfigured and deprecated models. - **Workspace-level filtering**: Per-workspace controls with "Use best models only" mode (curated list backed by `isRecommended` flag), or custom whitelist/blacklist. Separate Smart/Fast model selectors with "Best (...)" virtual options. - **Security enforcement**: Both layers enforced at every backend execution point — workspace update, agent create/update, chat execution. Model ID validated against known models before config mutation. All admin endpoints protected by `AdminPanelGuard`. ## Changes ### Backend (`twenty-server`) - New config variables for admin-level model filtering - `AiModelRegistryService`: `getAllModelsWithStatus()`, `setModelAdminEnabled()` with model ID validation, `isModelAdminAllowed()` - `AdminPanelResolver`: `getAdminAiModels` query, `setAdminAiModelEnabled` mutation - `WorkspaceEntity`: new fields (`autoEnableNewAiModels`, `disabledAiModelIds`, `enabledAiModelIds`, `useRecommendedModels`) - `WorkspaceService`: model validation on `smartModel`/`fastModel` updates - `AgentResolver`: model availability checks on create/update - `isModelAllowedByWorkspace` centralized utility - `isRecommended` flag on model definitions - Two TypeORM migrations ### Frontend (`twenty-front`) - New `SettingsAdminAI` component with search, filter dropdown (unconfigured/deprecated), and model toggle cards - AI tab added to admin panel navigation - `useWorkspaceAiModelAvailability` hook for workspace-level filtering - `SettingsAIModelsTab` redesigned: merged sections, "Use best models only" toggle, conditional available models list - `getModelIcon`/`getModelProviderLabel` shared utilities with GraphQL enum casing normalization - Updated generated GraphQL types and mock data ## Test plan - [ ] Toggle models on/off in admin panel AI tab and verify they appear/disappear in workspace settings - [ ] Enable "Use best models only" in workspace settings and verify only recommended models are selectable - [ ] Disable recommended mode and verify whitelist/blacklist toggles work correctly - [ ] Verify deprecated models hidden by default, shown greyed out when filter enabled - [ ] Verify unconfigured models hidden by default, shown disabled when filter enabled - [ ] Try setting a disabled model as Smart/Fast model — should be rejected - [ ] Try creating an agent with a disabled model — should be rejected - [ ] Verify admin panel AI tab requires admin access Made with [Cursor](https://cursor.com) --------- Co-authored-by: Cursor --- .../src/generated-metadata/graphql.ts | 106 ++++- .../src/modules/ai/hooks/useAiModelOptions.ts | 26 +- .../hooks/useWorkspaceAiModelAvailability.ts | 64 +++ .../services/__tests__/apollo.factory.test.ts | 4 + .../auth/states/currentWorkspaceState.ts | 4 + .../IsAppMetadataReadyEffect.tsx | 7 - ...olumnDefinitionsFromObjectMetadata.test.ts | 4 + .../useColumnDefinitionsFromObjectMetadata.ts | 15 +- .../ai/components/SettingsAdminAI.tsx | 233 +++++++++++ .../mutations/setAdminAiModelEnabled.ts | 7 + .../ai/graphql/queries/getAdminAiModels.ts | 19 + .../components/SettingsAdminContent.tsx | 17 +- .../components/SettingsAdminTabContent.tsx | 3 + .../constants/SettingsAdminTabs.ts | 1 + .../graphql/fragments/userQueryFragment.ts | 4 + .../ai/components/SettingsAIModelsTab.tsx | 386 +++++++++++++++--- .../pages/settings/ai/utils/getModelIcon.ts | 15 + .../ai/utils/getModelProviderLabel.ts | 13 + .../src/testing/mock-data/users.ts | 4 + ...47449-add-ai-model-availability-columns.ts | 31 ++ ...771768847450-add-use-recommended-models.ts | 19 + .../admin-panel/admin-panel.resolver.ts | 38 ++ .../client-config/client-config.entity.ts | 39 ++ .../services/client-config.service.ts | 8 +- .../twenty-config/config-variables.ts | 27 ++ .../workspace/dtos/update-workspace-input.ts | 22 + .../workspace/services/workspace.service.ts | 49 +++ .../workspace/workspace.entity.ts | 26 ++ .../workspace/workspace.resolver.ts | 28 ++ .../ai-agent-execution.module.ts | 2 + .../services/agent-async-executor.service.ts | 16 + .../ai/ai-agent/agent.resolver.ts | 29 +- .../controllers/agent-chat.controller.ts | 11 +- .../services/chat-execution.service.ts | 7 +- .../constants/ai-models-types.const.ts | 1 + .../constants/anthropic-models.const.ts | 2 + .../constants/google-models.const.ts | 1 + .../constants/openai-models.const.ts | 2 + .../ai-models/constants/xai-models.const.ts | 1 + .../services/ai-model-registry.service.ts | 95 +++++ .../ai-models/utils/is-model-allowed.util.ts | 35 ++ 41 files changed, 1320 insertions(+), 101 deletions(-) create mode 100644 packages/twenty-front/src/modules/ai/hooks/useWorkspaceAiModelAvailability.ts create mode 100644 packages/twenty-front/src/modules/settings/admin-panel/ai/components/SettingsAdminAI.tsx create mode 100644 packages/twenty-front/src/modules/settings/admin-panel/ai/graphql/mutations/setAdminAiModelEnabled.ts create mode 100644 packages/twenty-front/src/modules/settings/admin-panel/ai/graphql/queries/getAdminAiModels.ts create mode 100644 packages/twenty-front/src/pages/settings/ai/utils/getModelIcon.ts create mode 100644 packages/twenty-front/src/pages/settings/ai/utils/getModelProviderLabel.ts create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/common/1771768847449-add-ai-model-availability-columns.ts create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/common/1771840510112-1771768847450-add-use-recommended-models.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/ai/ai-models/utils/is-model-allowed.util.ts diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 7865106f1e..86cd0776e1 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -44,6 +44,24 @@ export type AddQuerySubscriptionInput = { queryId: Scalars['String']; }; +export type AdminAiModelConfig = { + __typename?: 'AdminAIModelConfig'; + deprecated?: Maybe; + inferenceProvider: InferenceProvider; + isAdminEnabled: Scalars['Boolean']; + isAvailable: Scalars['Boolean']; + isRecommended?: Maybe; + label: Scalars['String']; + modelFamily?: Maybe; + modelId: Scalars['String']; +}; + +export type AdminAiModelsOutput = { + __typename?: 'AdminAIModelsOutput'; + autoEnableNewModels: Scalars['Boolean']; + models: Array; +}; + export type AdminPanelHealthServiceData = { __typename?: 'AdminPanelHealthServiceData'; description: Scalars['String']; @@ -681,6 +699,7 @@ export type ClientAiModelConfig = { deprecated?: Maybe; inferenceProvider: InferenceProvider; inputCostPerMillionTokensInCredits: Scalars['Float']; + isRecommended?: Maybe; label: Scalars['String']; modelFamily?: Maybe; modelId: Scalars['String']; @@ -3737,6 +3756,7 @@ export type Query = { frontComponents: Array; getAISystemPromptPreview: AiSystemPromptPreview; getAddressDetails: PlaceDetailsResult; + getAdminAiModels: AdminAiModelsOutput; getApprovedAccessDomains: Array; getAutoCompleteAddress: Array; getAvailablePackages: Scalars['JSON']; @@ -4837,10 +4857,13 @@ export type UpdateWebhookInputUpdates = { export type UpdateWorkspaceInput = { aiAdditionalInstructions?: InputMaybe; allowImpersonation?: InputMaybe; + autoEnableNewAiModels?: InputMaybe; customDomain?: InputMaybe; defaultRoleId?: InputMaybe; + disabledAiModelIds?: InputMaybe>; displayName?: InputMaybe; editableProfileFields?: InputMaybe>; + enabledAiModelIds?: InputMaybe>; eventLogRetentionDays?: InputMaybe; fastModel?: InputMaybe; inviteHash?: InputMaybe; @@ -4856,6 +4879,7 @@ export type UpdateWorkspaceInput = { smartModel?: InputMaybe; subdomain?: InputMaybe; trashRetentionDays?: InputMaybe; + useRecommendedModels?: InputMaybe; }; export type UpsertFieldPermissionsInput = { @@ -5158,6 +5182,7 @@ export type Workspace = { activationStatus: WorkspaceActivationStatus; aiAdditionalInstructions?: Maybe; allowImpersonation: Scalars['Boolean']; + autoEnableNewAiModels: Scalars['Boolean']; billingEntitlements: Array; billingSubscriptions: Array; createdAt: Scalars['DateTime']; @@ -5167,8 +5192,10 @@ export type Workspace = { databaseUrl: Scalars['String']; defaultRole?: Maybe; deletedAt?: Maybe; + disabledAiModelIds?: Maybe>; displayName?: Maybe; editableProfileFields?: Maybe>; + enabledAiModelIds?: Maybe>; eventLogRetentionDays: Scalars['Float']; fastModel: Scalars['String']; featureFlags?: Maybe>; @@ -5192,6 +5219,7 @@ export type Workspace = { subdomain: Scalars['String']; trashRetentionDays: Scalars['Float']; updatedAt: Scalars['DateTime']; + useRecommendedModels: Scalars['Boolean']; version?: Maybe; viewFields?: Maybe>; viewFilterGroups?: Maybe>; @@ -6094,6 +6122,19 @@ export type GetConnectedImapSmtpCaldavAccountQueryVariables = Exact<{ export type GetConnectedImapSmtpCaldavAccountQuery = { __typename?: 'Query', getConnectedImapSmtpCaldavAccount: { __typename?: 'ConnectedImapSmtpCaldavAccount', id: string, handle: string, provider: string, accountOwnerId: string, connectionParameters?: { __typename?: 'ImapSmtpCaldavConnectionParameters', IMAP?: { __typename?: 'ConnectionParametersOutput', host: string, port: number, secure?: boolean | null, username?: string | null, password: string } | null, SMTP?: { __typename?: 'ConnectionParametersOutput', host: string, username?: string | null, port: number, secure?: boolean | null, password: string } | null, CALDAV?: { __typename?: 'ConnectionParametersOutput', host: string, username?: string | null, password: string } | null } | null } }; +export type GetAdminAiModelsQueryVariables = Exact<{ [key: string]: never; }>; + + +export type GetAdminAiModelsQuery = { __typename?: 'Query', getAdminAiModels: { __typename?: 'AdminAIModelsOutput', autoEnableNewModels: boolean, models: Array<{ __typename?: 'AdminAIModelConfig', modelId: string, label: string, modelFamily?: ModelFamily | null, inferenceProvider: InferenceProvider, isAvailable: boolean, isAdminEnabled: boolean, deprecated?: boolean | null, isRecommended?: boolean | null }> } }; + +export type SetAdminAiModelEnabledMutationVariables = Exact<{ + modelId: Scalars['String']; + enabled: Scalars['Boolean']; +}>; + + +export type SetAdminAiModelEnabledMutation = { __typename?: 'Mutation', setAdminAiModelEnabled: boolean }; + export type CreateDatabaseConfigVariableMutationVariables = Exact<{ key: Scalars['String']; value: Scalars['JSON']; @@ -6509,7 +6550,7 @@ export type BillingSubscriptionFragmentFragment = { __typename?: 'BillingSubscri export type CurrentBillingSubscriptionFragmentFragment = { __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, currentPeriodEnd?: string | null, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }>, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItemDTO', id: string, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, stripePriceId: string, billingProduct: { __typename?: 'BillingLicensedProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } | { __typename?: 'BillingMeteredProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } }> | null }; -export type UserQueryFragmentFragment = { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, hasPassword: boolean, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, calendarStartDay?: number | null, numberFormat?: WorkspaceMemberNumberFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', id: string, permissionFlags?: Array | null, objectsPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null, rowLevelPermissionPredicates?: Array<{ __typename?: 'RowLevelPermissionPredicate', id: string, fieldMetadataId: string, objectMetadataId: string, operand: RowLevelPermissionPredicateOperand, subFieldName?: string | null, workspaceMemberFieldMetadataId?: string | null, workspaceMemberSubFieldName?: string | null, rowLevelPermissionPredicateGroupId?: string | null, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, value?: any | null }> | null, rowLevelPermissionPredicateGroups?: Array<{ __typename?: 'RowLevelPermissionPredicateGroup', id: string, parentRowLevelPermissionPredicateGroupId?: string | null, logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, objectMetadataId: string }> | null }> | null, twoFactorAuthenticationMethodSummary?: Array<{ __typename?: 'TwoFactorAuthenticationMethodDTO', twoFactorAuthenticationMethodId: string, status: string, strategy: string }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, isGoogleAuthBypassEnabled: boolean, isMicrosoftAuthBypassEnabled: boolean, isPasswordAuthBypassEnabled: boolean, subdomain: string, customDomain?: string | null, hasValidEnterpriseKey: boolean, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, fastModel: string, smartModel: string, aiAdditionalInstructions?: string | null, isTwoFactorAuthenticationEnforced: boolean, trashRetentionDays: number, eventLogRetentionDays: number, editableProfileFields?: Array | null, workspaceCustomApplication?: { __typename?: 'Application', id: string } | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, currentPeriodEnd?: string | null, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }>, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItemDTO', id: string, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, stripePriceId: string, billingProduct: { __typename?: 'BillingLicensedProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } | { __typename?: 'BillingMeteredProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, metadata: any, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }> }>, billingEntitlements: Array<{ __typename?: 'BillingEntitlement', key: BillingEntitlementKey, value: boolean }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canBeAssignedToUsers: boolean, canBeAssignedToAgents: boolean, canBeAssignedToApiKeys: 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: string, firstName: string, lastName: string, email: string, hasPassword: boolean, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, calendarStartDay?: number | null, numberFormat?: WorkspaceMemberNumberFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', id: string, permissionFlags?: Array | null, objectsPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null, rowLevelPermissionPredicates?: Array<{ __typename?: 'RowLevelPermissionPredicate', id: string, fieldMetadataId: string, objectMetadataId: string, operand: RowLevelPermissionPredicateOperand, subFieldName?: string | null, workspaceMemberFieldMetadataId?: string | null, workspaceMemberSubFieldName?: string | null, rowLevelPermissionPredicateGroupId?: string | null, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, value?: any | null }> | null, rowLevelPermissionPredicateGroups?: Array<{ __typename?: 'RowLevelPermissionPredicateGroup', id: string, parentRowLevelPermissionPredicateGroupId?: string | null, logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, objectMetadataId: string }> | null }> | null, twoFactorAuthenticationMethodSummary?: Array<{ __typename?: 'TwoFactorAuthenticationMethodDTO', twoFactorAuthenticationMethodId: string, status: string, strategy: string }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, isGoogleAuthBypassEnabled: boolean, isMicrosoftAuthBypassEnabled: boolean, isPasswordAuthBypassEnabled: boolean, subdomain: string, customDomain?: string | null, hasValidEnterpriseKey: boolean, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, fastModel: string, smartModel: string, aiAdditionalInstructions?: string | null, autoEnableNewAiModels: boolean, disabledAiModelIds?: Array | null, enabledAiModelIds?: Array | null, useRecommendedModels: boolean, isTwoFactorAuthenticationEnforced: boolean, trashRetentionDays: number, eventLogRetentionDays: number, editableProfileFields?: Array | null, workspaceCustomApplication?: { __typename?: 'Application', id: string } | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, currentPeriodEnd?: string | null, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }>, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItemDTO', id: string, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, stripePriceId: string, billingProduct: { __typename?: 'BillingLicensedProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } | { __typename?: 'BillingMeteredProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, metadata: any, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }> }>, billingEntitlements: Array<{ __typename?: 'BillingEntitlement', key: BillingEntitlementKey, value: boolean }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canBeAssignedToUsers: boolean, canBeAssignedToAgents: boolean, canBeAssignedToApiKeys: 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 WorkspaceUrlsFragmentFragment = { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }; @@ -6528,7 +6569,7 @@ export type DeleteUserWorkspaceMutation = { __typename?: 'Mutation', deleteUserF export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>; -export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, hasPassword: boolean, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, calendarStartDay?: number | null, numberFormat?: WorkspaceMemberNumberFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', id: string, permissionFlags?: Array | null, objectsPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null, rowLevelPermissionPredicates?: Array<{ __typename?: 'RowLevelPermissionPredicate', id: string, fieldMetadataId: string, objectMetadataId: string, operand: RowLevelPermissionPredicateOperand, subFieldName?: string | null, workspaceMemberFieldMetadataId?: string | null, workspaceMemberSubFieldName?: string | null, rowLevelPermissionPredicateGroupId?: string | null, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, value?: any | null }> | null, rowLevelPermissionPredicateGroups?: Array<{ __typename?: 'RowLevelPermissionPredicateGroup', id: string, parentRowLevelPermissionPredicateGroupId?: string | null, logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, objectMetadataId: string }> | null }> | null, twoFactorAuthenticationMethodSummary?: Array<{ __typename?: 'TwoFactorAuthenticationMethodDTO', twoFactorAuthenticationMethodId: string, status: string, strategy: string }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, isGoogleAuthBypassEnabled: boolean, isMicrosoftAuthBypassEnabled: boolean, isPasswordAuthBypassEnabled: boolean, subdomain: string, customDomain?: string | null, hasValidEnterpriseKey: boolean, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, fastModel: string, smartModel: string, aiAdditionalInstructions?: string | null, isTwoFactorAuthenticationEnforced: boolean, trashRetentionDays: number, eventLogRetentionDays: number, editableProfileFields?: Array | null, workspaceCustomApplication?: { __typename?: 'Application', id: string } | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, currentPeriodEnd?: string | null, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }>, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItemDTO', id: string, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, stripePriceId: string, billingProduct: { __typename?: 'BillingLicensedProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } | { __typename?: 'BillingMeteredProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, metadata: any, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }> }>, billingEntitlements: Array<{ __typename?: 'BillingEntitlement', key: BillingEntitlementKey, value: boolean }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canBeAssignedToUsers: boolean, canBeAssignedToAgents: boolean, canBeAssignedToApiKeys: 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: string, firstName: string, lastName: string, email: string, hasPassword: boolean, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, calendarStartDay?: number | null, numberFormat?: WorkspaceMemberNumberFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', id: string, permissionFlags?: Array | null, objectsPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null, rowLevelPermissionPredicates?: Array<{ __typename?: 'RowLevelPermissionPredicate', id: string, fieldMetadataId: string, objectMetadataId: string, operand: RowLevelPermissionPredicateOperand, subFieldName?: string | null, workspaceMemberFieldMetadataId?: string | null, workspaceMemberSubFieldName?: string | null, rowLevelPermissionPredicateGroupId?: string | null, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, value?: any | null }> | null, rowLevelPermissionPredicateGroups?: Array<{ __typename?: 'RowLevelPermissionPredicateGroup', id: string, parentRowLevelPermissionPredicateGroupId?: string | null, logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, objectMetadataId: string }> | null }> | null, twoFactorAuthenticationMethodSummary?: Array<{ __typename?: 'TwoFactorAuthenticationMethodDTO', twoFactorAuthenticationMethodId: string, status: string, strategy: string }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, isGoogleAuthBypassEnabled: boolean, isMicrosoftAuthBypassEnabled: boolean, isPasswordAuthBypassEnabled: boolean, subdomain: string, customDomain?: string | null, hasValidEnterpriseKey: boolean, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, fastModel: string, smartModel: string, aiAdditionalInstructions?: string | null, autoEnableNewAiModels: boolean, disabledAiModelIds?: Array | null, enabledAiModelIds?: Array | null, useRecommendedModels: boolean, isTwoFactorAuthenticationEnforced: boolean, trashRetentionDays: number, eventLogRetentionDays: number, editableProfileFields?: Array | null, workspaceCustomApplication?: { __typename?: 'Application', id: string } | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, currentPeriodEnd?: string | null, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }>, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItemDTO', id: string, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, stripePriceId: string, billingProduct: { __typename?: 'BillingLicensedProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } | { __typename?: 'BillingMeteredProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, metadata: any, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }> }>, billingEntitlements: Array<{ __typename?: 'BillingEntitlement', key: BillingEntitlementKey, value: boolean }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canBeAssignedToUsers: boolean, canBeAssignedToAgents: boolean, canBeAssignedToApiKeys: 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 ViewFieldFragmentFragment = { __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }; @@ -7835,6 +7876,10 @@ export const UserQueryFragmentFragmentDoc = gql` fastModel smartModel aiAdditionalInstructions + autoEnableNewAiModels + disabledAiModelIds + enabledAiModelIds + useRecommendedModels isTwoFactorAuthenticationEnforced trashRetentionDays eventLogRetentionDays @@ -12056,6 +12101,63 @@ export function useGetConnectedImapSmtpCaldavAccountLazyQuery(baseOptions?: Apol export type GetConnectedImapSmtpCaldavAccountQueryHookResult = ReturnType; export type GetConnectedImapSmtpCaldavAccountLazyQueryHookResult = ReturnType; export type GetConnectedImapSmtpCaldavAccountQueryResult = Apollo.QueryResult; +export const GetAdminAiModelsDocument = gql` + query GetAdminAiModels { + getAdminAiModels { + autoEnableNewModels + models { + modelId + label + modelFamily + inferenceProvider + isAvailable + isAdminEnabled + deprecated + isRecommended + } + } +} + `; + +/** + * __useGetAdminAiModelsQuery__ + * + * To run a query within a React component, call `useGetAdminAiModelsQuery` and pass it any options that fit your needs. + * When your component renders, `useGetAdminAiModelsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetAdminAiModelsQuery({ + * variables: { + * }, + * }); + */ +export function useGetAdminAiModelsQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetAdminAiModelsDocument, options); + } +export function useGetAdminAiModelsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetAdminAiModelsDocument, options); + } +export type GetAdminAiModelsQueryHookResult = ReturnType; +export type GetAdminAiModelsLazyQueryHookResult = ReturnType; +export type GetAdminAiModelsQueryResult = Apollo.QueryResult; +export const SetAdminAiModelEnabledDocument = gql` + mutation SetAdminAiModelEnabled($modelId: String!, $enabled: Boolean!) { + setAdminAiModelEnabled(modelId: $modelId, enabled: $enabled) +} + `; +export type SetAdminAiModelEnabledMutationFn = Apollo.MutationFunction; +export function useSetAdminAiModelEnabledMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(SetAdminAiModelEnabledDocument, options); + } +export type SetAdminAiModelEnabledMutationHookResult = ReturnType; +export type SetAdminAiModelEnabledMutationResult = Apollo.MutationResult; +export type SetAdminAiModelEnabledMutationOptions = Apollo.BaseMutationOptions; export const CreateDatabaseConfigVariableDocument = gql` mutation CreateDatabaseConfigVariable($key: String!, $value: JSON!) { createDatabaseConfigVariable(key: $key, value: $value) diff --git a/packages/twenty-front/src/modules/ai/hooks/useAiModelOptions.ts b/packages/twenty-front/src/modules/ai/hooks/useAiModelOptions.ts index 4e257a592b..e5a5eaf6c2 100644 --- a/packages/twenty-front/src/modules/ai/hooks/useAiModelOptions.ts +++ b/packages/twenty-front/src/modules/ai/hooks/useAiModelOptions.ts @@ -1,39 +1,35 @@ -import { aiModelsState } from '@/client-config/states/aiModelsState'; import { type SelectOption } from 'twenty-ui/input'; import { DEFAULT_FAST_MODEL } from '@/ai/constants/DefaultFastModel'; import { DEFAULT_SMART_MODEL } from '@/ai/constants/DefaultSmartModel'; +import { useWorkspaceAiModelAvailability } from '@/ai/hooks/useWorkspaceAiModelAvailability'; +import { aiModelsState } from '@/client-config/states/aiModelsState'; import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2'; -import { MODEL_FAMILY_CONFIG } from '~/pages/settings/ai/constants/SettingsAiModelProviders'; +import { getModelProviderLabel } from '~/pages/settings/ai/utils/getModelProviderLabel'; export const useAiModelOptions = ( includeDeprecated = false, ): SelectOption[] => { const aiModels = useRecoilValueV2(aiModelsState); + const { isModelEnabled } = useWorkspaceAiModelAvailability(); return aiModels - .filter((model) => includeDeprecated || !model.deprecated) + .filter( + (model) => + (includeDeprecated || !model.deprecated) && + isModelEnabled(model.modelId, model), + ) .map((model) => ({ value: model.modelId, label: model.modelId === DEFAULT_FAST_MODEL || model.modelId === DEFAULT_SMART_MODEL ? model.label - : `${model.label} (${getModelFamilyLabel(model.modelFamily) ?? model.inferenceProvider})`, + : `${model.label} (${getModelProviderLabel(model.modelFamily) || model.inferenceProvider})`, })) .sort((a, b) => a.label.localeCompare(b.label)); }; -const getModelFamilyLabel = ( - modelFamily: string | null | undefined, -): string | undefined => { - if (!modelFamily) { - return undefined; - } - - return MODEL_FAMILY_CONFIG[modelFamily]?.label || modelFamily; -}; - export const useAiModelLabel = ( modelId: string | undefined, includeProvider = true, @@ -58,5 +54,5 @@ export const useAiModelLabel = ( return model.label; } - return `${model.label} (${getModelFamilyLabel(model.modelFamily) ?? model.inferenceProvider})`; + return `${model.label} (${getModelProviderLabel(model.modelFamily) || model.inferenceProvider})`; }; diff --git a/packages/twenty-front/src/modules/ai/hooks/useWorkspaceAiModelAvailability.ts b/packages/twenty-front/src/modules/ai/hooks/useWorkspaceAiModelAvailability.ts new file mode 100644 index 0000000000..7240343989 --- /dev/null +++ b/packages/twenty-front/src/modules/ai/hooks/useWorkspaceAiModelAvailability.ts @@ -0,0 +1,64 @@ +import { DEFAULT_FAST_MODEL } from '@/ai/constants/DefaultFastModel'; +import { DEFAULT_SMART_MODEL } from '@/ai/constants/DefaultSmartModel'; +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; +import { aiModelsState } from '@/client-config/states/aiModelsState'; +import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2'; +import { type ClientAiModelConfig } from '~/generated-metadata/graphql'; + +const VIRTUAL_MODEL_IDS: Set = new Set([ + DEFAULT_SMART_MODEL, + DEFAULT_FAST_MODEL, +]); + +const isVirtualModel = (modelId: string) => VIRTUAL_MODEL_IDS.has(modelId); + +export const useWorkspaceAiModelAvailability = () => { + const aiModels = useRecoilValueV2(aiModelsState); + const currentWorkspace = useRecoilValueV2(currentWorkspaceState); + + const useRecommendedModels = currentWorkspace?.useRecommendedModels ?? true; + const autoEnableNewAiModels = currentWorkspace?.autoEnableNewAiModels ?? true; + const disabledAiModelIds = currentWorkspace?.disabledAiModelIds ?? []; + const enabledAiModelIds = currentWorkspace?.enabledAiModelIds ?? []; + + const isModelEnabled = ( + modelId: string, + model?: ClientAiModelConfig, + ): boolean => { + if (isVirtualModel(modelId)) { + return true; + } + + if (useRecommendedModels) { + return model?.isRecommended === true; + } + + return autoEnableNewAiModels + ? !disabledAiModelIds.includes(modelId) + : enabledAiModelIds.includes(modelId); + }; + + const realModels = aiModels.filter( + (model) => !isVirtualModel(model.modelId) && !model.deprecated, + ); + + const enabledModels = realModels.filter((model) => + isModelEnabled(model.modelId, model), + ); + + const allModelsWithAvailability = realModels.map((model) => ({ + ...model, + isEnabled: isModelEnabled(model.modelId, model), + })); + + return { + isModelEnabled, + enabledModels, + realModels, + allModelsWithAvailability, + useRecommendedModels, + autoEnableNewAiModels, + disabledAiModelIds, + enabledAiModelIds, + }; +}; diff --git a/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts b/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts index e60c23cfd1..2ab887ee2c 100644 --- a/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts +++ b/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts @@ -70,6 +70,10 @@ const mockWorkspace = { fastModel: DEFAULT_FAST_MODEL, smartModel: DEFAULT_SMART_MODEL, routerModel: 'auto', + autoEnableNewAiModels: true, + disabledAiModelIds: [], + enabledAiModelIds: [], + useRecommendedModels: true, workspaceCustomApplication: CUSTOM_WORKSPACE_APPLICATION_MOCK, workspaceCustomApplicationId: CUSTOM_WORKSPACE_APPLICATION_MOCK.id, }; diff --git a/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts b/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts index 379458a686..001bad094f 100644 --- a/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts +++ b/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts @@ -38,6 +38,10 @@ export type CurrentWorkspace = Pick< | 'smartModel' | 'aiAdditionalInstructions' | 'editableProfileFields' + | 'autoEnableNewAiModels' + | 'disabledAiModelIds' + | 'enabledAiModelIds' + | 'useRecommendedModels' > & { defaultRole?: Omit | null; workspaceCustomApplication: Pick | null; diff --git a/packages/twenty-front/src/modules/metadata-store/effect-components/IsAppMetadataReadyEffect.tsx b/packages/twenty-front/src/modules/metadata-store/effect-components/IsAppMetadataReadyEffect.tsx index 8716045110..c710bcb710 100644 --- a/packages/twenty-front/src/modules/metadata-store/effect-components/IsAppMetadataReadyEffect.tsx +++ b/packages/twenty-front/src/modules/metadata-store/effect-components/IsAppMetadataReadyEffect.tsx @@ -18,13 +18,6 @@ export const IsAppMetadataReadyEffect = () => { const viewsEntry = useFamilyRecoilValueV2(metadataStoreState, 'views'); const setIsAppMetadataReady = useSetRecoilStateV2(isAppMetadataReadyState); - console.log('objectsEntry', objectsEntry); - console.log('viewsEntry', viewsEntry); - console.log('isLoggedIn', isLoggedIn); - console.log('currentUser', currentUser); - console.log('currentWorkspace', currentWorkspace); - console.log('setIsAppMetadataReady', setIsAppMetadataReady); - useEffect(() => { const hasActiveWorkspace = isWorkspaceActiveOrSuspended(currentWorkspace); diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromObjectMetadata.test.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromObjectMetadata.test.ts index f7bf32e806..1ddddc63d3 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromObjectMetadata.test.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromObjectMetadata.test.ts @@ -71,6 +71,10 @@ describe('useColumnDefinitionsFromObjectMetadata', () => { eventLogRetentionDays: 365 * 3, fastModel: DEFAULT_FAST_MODEL, smartModel: DEFAULT_SMART_MODEL, + autoEnableNewAiModels: true, + disabledAiModelIds: [], + enabledAiModelIds: [], + useRecommendedModels: true, }); const companyObjectMetadata = generatedMockObjectMetadataItems.find( diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useColumnDefinitionsFromObjectMetadata.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useColumnDefinitionsFromObjectMetadata.ts index a3b5630997..eb0db541c1 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useColumnDefinitionsFromObjectMetadata.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useColumnDefinitionsFromObjectMetadata.ts @@ -6,8 +6,8 @@ import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailab import { availableFieldMetadataItemsForFilterFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForFilterFamilySelector'; import { availableFieldMetadataItemsForSortFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForSortFamilySelector'; -import { useFamilySelectorValueV2 } from '@/ui/utilities/state/jotai/hooks/useFamilySelectorValueV2'; import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition'; +import { useFamilySelectorValueV2 } from '@/ui/utilities/state/jotai/hooks/useFamilySelectorValueV2'; import { useMemo } from 'react'; export const useColumnDefinitionsFromObjectMetadata = ( @@ -28,13 +28,12 @@ export const useColumnDefinitionsFromObjectMetadata = ( ); const columnDefinitions: ColumnDefinition[] = useMemo(() => { - const activeFieldMetadataItems = - objectMetadataItem.readableFields.filter( - (field) => - field.isActive && - (!isHiddenSystemField(field) || - field.id === objectMetadataItem.labelIdentifierFieldMetadataId), - ); + const activeFieldMetadataItems = objectMetadataItem.readableFields.filter( + (field) => + field.isActive && + (!isHiddenSystemField(field) || + field.id === objectMetadataItem.labelIdentifierFieldMetadataId), + ); return activeFieldMetadataItems .map((field, index) => diff --git a/packages/twenty-front/src/modules/settings/admin-panel/ai/components/SettingsAdminAI.tsx b/packages/twenty-front/src/modules/settings/admin-panel/ai/components/SettingsAdminAI.tsx new file mode 100644 index 0000000000..00eb5e781c --- /dev/null +++ b/packages/twenty-front/src/modules/settings/admin-panel/ai/components/SettingsAdminAI.tsx @@ -0,0 +1,233 @@ +import { useState } from 'react'; +import styled from '@emotion/styled'; + +import { useClientConfig } from '@/client-config/hooks/useClientConfig'; +import { GET_ADMIN_AI_MODELS } from '@/settings/admin-panel/ai/graphql/queries/getAdminAiModels'; +import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput'; +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 { t } from '@lingui/core/macro'; +import { + H2Title, + IconArchive, + IconFilter, + IconPlug, + IconRobot, + IconSearch, +} from 'twenty-ui/display'; +import { Button } from 'twenty-ui/input'; +import { Card, Section } from 'twenty-ui/layout'; +import { MenuItemToggle } from 'twenty-ui/navigation'; +import { + useCreateDatabaseConfigVariableMutation, + useGetAdminAiModelsQuery, + useSetAdminAiModelEnabledMutation, +} from '~/generated-metadata/graphql'; +import { getModelIcon } from '~/pages/settings/ai/utils/getModelIcon'; +import { getModelProviderLabel } from '~/pages/settings/ai/utils/getModelProviderLabel'; + +const StyledSearchAndFilterContainer = styled.div` + display: flex; + gap: ${({ theme }) => theme.spacing(2)}; + margin-bottom: ${({ theme }) => theme.spacing(2)}; + width: 100%; +`; + +const StyledSearchInput = styled(SettingsTextInput)` + flex: 1; +`; + +export const SettingsAdminAI = () => { + const { enqueueErrorSnackBar } = useSnackBar(); + const [searchQuery, setSearchQuery] = useState(''); + const [showUnconfigured, setShowUnconfigured] = useState(false); + const [showDeprecated, setShowDeprecated] = useState(false); + const { refetch: refetchClientConfig } = useClientConfig(); + + const { data } = useGetAdminAiModelsQuery(); + const [createConfigVariable] = useCreateDatabaseConfigVariableMutation(); + const [setModelEnabled] = useSetAdminAiModelEnabledMutation(); + + const autoEnableNewModels = + data?.getAdminAiModels?.autoEnableNewModels ?? true; + + const models = data?.getAdminAiModels?.models ?? []; + + const handleAutoEnableToggle = async (checked: boolean) => { + try { + await createConfigVariable({ + variables: { + key: 'AI_AUTO_ENABLE_NEW_MODELS', + value: checked, + }, + refetchQueries: [{ query: GET_ADMIN_AI_MODELS }], + }); + + await refetchClientConfig(); + } catch { + enqueueErrorSnackBar({ + message: t`Failed to update auto-enable setting`, + }); + } + }; + + const handleModelToggle = async ( + modelId: string, + isCurrentlyEnabled: boolean, + ) => { + try { + await setModelEnabled({ + variables: { + modelId, + enabled: !isCurrentlyEnabled, + }, + refetchQueries: [{ query: GET_ADMIN_AI_MODELS }], + }); + + await refetchClientConfig(); + } catch { + enqueueErrorSnackBar({ + message: t`Failed to update model availability`, + }); + } + }; + + let filteredModels = models; + + if (!showUnconfigured) { + filteredModels = filteredModels.filter((model) => model.isAvailable); + } + + if (!showDeprecated) { + filteredModels = filteredModels.filter((model) => !model.deprecated); + } + + if (searchQuery.trim().length > 0) { + const query = searchQuery.toLowerCase(); + + filteredModels = filteredModels.filter( + (model) => + model.label.toLowerCase().includes(query) || + (model.modelFamily?.toLowerCase().includes(query) ?? false) || + model.inferenceProvider.toLowerCase().includes(query), + ); + } + + const getModelDescription = ( + modelFamily: string | null | undefined, + isAvailable: boolean, + isDeprecated: boolean | null | undefined, + ) => { + const providerLabel = getModelProviderLabel(modelFamily); + + if (isDeprecated === true) { + return providerLabel ? t`${providerLabel} — Deprecated` : t`Deprecated`; + } + + if (!isAvailable) { + return providerLabel + ? t`${providerLabel} — API key not configured` + : t`API key not configured`; + } + + return providerLabel; + }; + + return ( + <> +
+ + + + + +
+ +
+ + + + + + } + dropdownComponents={ + + + + setShowUnconfigured(!showUnconfigured) + } + toggled={showUnconfigured} + text={t`Unconfigured models`} + toggleSize="small" + /> + setShowDeprecated(!showDeprecated)} + toggled={showDeprecated} + text={t`Deprecated models`} + toggleSize="small" + /> + + + } + /> + + + + {filteredModels.map((model, index) => ( + + handleModelToggle(model.modelId, model.isAdminEnabled) + } + disabled={!model.isAvailable || model.deprecated === true} + divider={index < filteredModels.length - 1} + /> + ))} + +
+ + ); +}; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/ai/graphql/mutations/setAdminAiModelEnabled.ts b/packages/twenty-front/src/modules/settings/admin-panel/ai/graphql/mutations/setAdminAiModelEnabled.ts new file mode 100644 index 0000000000..0c1259b0ca --- /dev/null +++ b/packages/twenty-front/src/modules/settings/admin-panel/ai/graphql/mutations/setAdminAiModelEnabled.ts @@ -0,0 +1,7 @@ +import { gql } from '@apollo/client'; + +export const SET_ADMIN_AI_MODEL_ENABLED = gql` + mutation SetAdminAiModelEnabled($modelId: String!, $enabled: Boolean!) { + setAdminAiModelEnabled(modelId: $modelId, enabled: $enabled) + } +`; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/ai/graphql/queries/getAdminAiModels.ts b/packages/twenty-front/src/modules/settings/admin-panel/ai/graphql/queries/getAdminAiModels.ts new file mode 100644 index 0000000000..0a5845e971 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/admin-panel/ai/graphql/queries/getAdminAiModels.ts @@ -0,0 +1,19 @@ +import { gql } from '@apollo/client'; + +export const GET_ADMIN_AI_MODELS = gql` + query GetAdminAiModels { + getAdminAiModels { + autoEnableNewModels + models { + modelId + label + modelFamily + inferenceProvider + isAvailable + isAdminEnabled + deprecated + isRecommended + } + } + } +`; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx index 9fc97cf6ee..62e4dfb863 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx +++ b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx @@ -4,7 +4,12 @@ import { SETTINGS_ADMIN_TABS } from '@/settings/admin-panel/constants/SettingsAd import { SETTINGS_ADMIN_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminTabsId'; import { TabList } from '@/ui/layout/tab-list/components/TabList'; import { t } from '@lingui/core/macro'; -import { IconHeart, IconSettings2, IconVariable } from 'twenty-ui/display'; +import { + IconHeart, + IconSettings2, + IconSparkles, + IconVariable, +} from 'twenty-ui/display'; import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2'; export const SettingsAdminContent = () => { @@ -19,15 +24,21 @@ export const SettingsAdminContent = () => { Icon: IconSettings2, disabled: !canAccessFullAdminPanel && !canImpersonate, }, + { + id: SETTINGS_ADMIN_TABS.AI, + title: t`AI`, + Icon: IconSparkles, + disabled: !canAccessFullAdminPanel, + }, { id: SETTINGS_ADMIN_TABS.CONFIG_VARIABLES, - title: t`Config Variables`, + title: t`Config`, Icon: IconVariable, disabled: !canAccessFullAdminPanel, }, { id: SETTINGS_ADMIN_TABS.HEALTH_STATUS, - title: t`Health Status`, + title: t`Health`, Icon: IconHeart, disabled: !canAccessFullAdminPanel, }, diff --git a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminTabContent.tsx b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminTabContent.tsx index e0034b3ecc..bd6b7cec17 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminTabContent.tsx +++ b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminTabContent.tsx @@ -1,3 +1,4 @@ +import { SettingsAdminAI } from '@/settings/admin-panel/ai/components/SettingsAdminAI'; import { SettingsAdminGeneral } from '@/settings/admin-panel/components/SettingsAdminGeneral'; import { SettingsAdminConfigVariables } from '@/settings/admin-panel/config-variables/components/SettingsAdminConfigVariables'; import { SETTINGS_ADMIN_TABS } from '@/settings/admin-panel/constants/SettingsAdminTabs'; @@ -15,6 +16,8 @@ export const SettingsAdminTabContent = () => { switch (activeTabId) { case SETTINGS_ADMIN_TABS.GENERAL: return ; + case SETTINGS_ADMIN_TABS.AI: + return ; case SETTINGS_ADMIN_TABS.CONFIG_VARIABLES: return ; case SETTINGS_ADMIN_TABS.HEALTH_STATUS: diff --git a/packages/twenty-front/src/modules/settings/admin-panel/constants/SettingsAdminTabs.ts b/packages/twenty-front/src/modules/settings/admin-panel/constants/SettingsAdminTabs.ts index c2b3e91307..4aa7bd6e46 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/constants/SettingsAdminTabs.ts +++ b/packages/twenty-front/src/modules/settings/admin-panel/constants/SettingsAdminTabs.ts @@ -1,5 +1,6 @@ export const SETTINGS_ADMIN_TABS = { GENERAL: 'general', + AI: 'ai', CONFIG_VARIABLES: 'config-variables', HEALTH_STATUS: 'health-status', }; diff --git a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts index 0885af796e..1e112a1748 100644 --- a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts +++ b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts @@ -90,6 +90,10 @@ export const USER_QUERY_FRAGMENT = gql` fastModel smartModel aiAdditionalInstructions + autoEnableNewAiModels + disabledAiModelIds + enabledAiModelIds + useRecommendedModels isTwoFactorAuthenticationEnforced trashRetentionDays eventLogRetentionDays diff --git a/packages/twenty-front/src/pages/settings/ai/components/SettingsAIModelsTab.tsx b/packages/twenty-front/src/pages/settings/ai/components/SettingsAIModelsTab.tsx index c6e9cb5372..0cae768ec2 100644 --- a/packages/twenty-front/src/pages/settings/ai/components/SettingsAIModelsTab.tsx +++ b/packages/twenty-front/src/pages/settings/ai/components/SettingsAIModelsTab.tsx @@ -1,108 +1,388 @@ +import { useState } from 'react'; +import styled from '@emotion/styled'; + import { DEFAULT_FAST_MODEL } from '@/ai/constants/DefaultFastModel'; import { DEFAULT_SMART_MODEL } from '@/ai/constants/DefaultSmartModel'; +import { useWorkspaceAiModelAvailability } from '@/ai/hooks/useWorkspaceAiModelAvailability'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { aiModelsState } from '@/client-config/states/aiModelsState'; import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect'; +import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { Select } from '@/ui/input/components/Select'; +import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; +import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput'; import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2'; import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2'; import { t } from '@lingui/core/macro'; -import { H2Title, IconBolt } from 'twenty-ui/display'; +import { + H2Title, + IconBolt, + IconRobot, + IconSearch, + IconTwentyStar, +} from 'twenty-ui/display'; import { Card, Section } from 'twenty-ui/layout'; import { useUpdateWorkspaceMutation } from '~/generated-metadata/graphql'; -import { MODEL_FAMILY_CONFIG } from '~/pages/settings/ai/constants/SettingsAiModelProviders'; +import { getModelIcon } from '~/pages/settings/ai/utils/getModelIcon'; +import { getModelProviderLabel } from '~/pages/settings/ai/utils/getModelProviderLabel'; -const VIRTUAL_MODEL_IDS: Set = new Set([ - DEFAULT_SMART_MODEL, - DEFAULT_FAST_MODEL, -]); +const StyledSearchContainer = styled.div` + padding-bottom: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledSearchInput = styled(SettingsTextInput)` + width: 100%; +`; export const SettingsAIModelsTab = () => { - const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const [currentWorkspace, setCurrentWorkspace] = useRecoilStateV2( currentWorkspaceState, ); const [updateWorkspace] = useUpdateWorkspaceMutation(); - + const [searchQuery, setSearchQuery] = useState(''); const aiModels = useRecoilValueV2(aiModelsState); - const realModels = aiModels.filter( - (model) => !VIRTUAL_MODEL_IDS.has(model.modelId), - ); + const { + allModelsWithAvailability, + enabledModels, + useRecommendedModels, + autoEnableNewAiModels, + realModels, + } = useWorkspaceAiModelAvailability(); const currentSmartModel = currentWorkspace?.smartModel; + const currentFastModel = currentWorkspace?.fastModel; - const defaultModelOptions = realModels - .filter((model) => !model.deprecated || model.modelId === currentSmartModel) - .map((model) => ({ - value: model.modelId, - label: model.label, - Icon: (model.modelFamily - ? (MODEL_FAMILY_CONFIG[model.modelFamily] ?? - MODEL_FAMILY_CONFIG.FALLBACK) - : MODEL_FAMILY_CONFIG.FALLBACK - ).Icon, - })); + const buildVirtualModelOption = (virtualModelId: string) => { + const virtualModel = aiModels.find( + (model) => model.modelId === virtualModelId, + ); - const handleDefaultModelChange = async (value: string) => { + return virtualModel + ? { + value: virtualModelId, + label: virtualModel.label, + Icon: IconTwentyStar, + } + : null; + }; + + const smartAutoOption = buildVirtualModelOption(DEFAULT_SMART_MODEL); + const fastAutoOption = buildVirtualModelOption(DEFAULT_FAST_MODEL); + + const modelOptions = enabledModels.map((model) => ({ + value: model.modelId, + label: model.label, + Icon: getModelIcon(model.modelFamily), + })); + + const smartModelOptions = [...modelOptions]; + + if (smartAutoOption !== null) { + smartModelOptions.unshift(smartAutoOption); + } + + const fastModelOptions = [...modelOptions]; + + if (fastAutoOption !== null) { + fastModelOptions.unshift(fastAutoOption); + } + + const handleModelFieldChange = async ( + field: 'smartModel' | 'fastModel', + value: string, + ) => { if (!currentWorkspace?.id) { return; } - const previousSmartModel = currentWorkspace.smartModel; + const previousValue = currentWorkspace[field]; try { setCurrentWorkspace({ ...currentWorkspace, - smartModel: value, + [field]: value, }); await updateWorkspace({ variables: { input: { - smartModel: value, + [field]: value, }, }, }); - - enqueueSuccessSnackBar({ - message: t`Default model updated successfully`, - }); } catch { setCurrentWorkspace({ ...currentWorkspace, - smartModel: previousSmartModel, + [field]: previousValue, }); enqueueErrorSnackBar({ - message: t`Failed to update default model`, + message: t`Failed to update model`, }); } }; - return ( -
- + const handleUseRecommendedToggle = async (checked: boolean) => { + if (!currentWorkspace?.id) { + return; + } - - - handleModelFieldChange('smartModel', value)} + options={smartModelOptions} + selectSizeVariant="small" + dropdownWidth={GenericDropdownContentWidth.ExtraLarge} + /> + + +