diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql index 85c4adb0c1..7d15b96cc4 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql @@ -1634,12 +1634,15 @@ type ClientAIModelConfig { modelFamily: ModelFamily modelFamilyLabel: String sdkPackage: String - inputCostPerMillionTokensInCredits: Float! - outputCostPerMillionTokensInCredits: Float! + inputCostPerMillionTokens: Float + outputCostPerMillionTokens: Float nativeCapabilities: NativeModelCapabilities isDeprecated: Boolean isRecommended: Boolean providerName: String + providerLabel: String + contextWindowTokens: Float + maxOutputTokens: Float dataResidency: String } @@ -3608,7 +3611,9 @@ type Mutation { userLookupAdminPanel(userIdentifier: String!): UserLookup! updateWorkspaceFeatureFlag(workspaceId: UUID!, featureFlag: String!, value: Boolean!): Boolean! setAdminAiModelEnabled(modelId: String!, enabled: Boolean!): Boolean! + setAdminAiModelsEnabled(modelIds: [String!]!, enabled: Boolean!): Boolean! setAdminAiModelRecommended(modelId: String!, recommended: Boolean!): Boolean! + setAdminAiModelsRecommended(modelIds: [String!]!, recommended: Boolean!): Boolean! setAdminDefaultAiModel(role: AiModelRole!, modelId: String!): Boolean! createDatabaseConfigVariable(key: String!, value: JSON!): Boolean! updateDatabaseConfigVariable(key: String!, value: JSON!): Boolean! diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts index 84ebe1aca0..e47a002be0 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts @@ -1345,12 +1345,15 @@ export interface ClientAIModelConfig { modelFamily?: ModelFamily modelFamilyLabel?: Scalars['String'] sdkPackage?: Scalars['String'] - inputCostPerMillionTokensInCredits: Scalars['Float'] - outputCostPerMillionTokensInCredits: Scalars['Float'] + inputCostPerMillionTokens?: Scalars['Float'] + outputCostPerMillionTokens?: Scalars['Float'] nativeCapabilities?: NativeModelCapabilities isDeprecated?: Scalars['Boolean'] isRecommended?: Scalars['Boolean'] providerName?: Scalars['String'] + providerLabel?: Scalars['String'] + contextWindowTokens?: Scalars['Float'] + maxOutputTokens?: Scalars['Float'] dataResidency?: Scalars['String'] __typename: 'ClientAIModelConfig' } @@ -3063,7 +3066,9 @@ export interface Mutation { userLookupAdminPanel: UserLookup updateWorkspaceFeatureFlag: Scalars['Boolean'] setAdminAiModelEnabled: Scalars['Boolean'] + setAdminAiModelsEnabled: Scalars['Boolean'] setAdminAiModelRecommended: Scalars['Boolean'] + setAdminAiModelsRecommended: Scalars['Boolean'] setAdminDefaultAiModel: Scalars['Boolean'] createDatabaseConfigVariable: Scalars['Boolean'] updateDatabaseConfigVariable: Scalars['Boolean'] @@ -4514,12 +4519,15 @@ export interface ClientAIModelConfigGenqlSelection{ modelFamily?: boolean | number modelFamilyLabel?: boolean | number sdkPackage?: boolean | number - inputCostPerMillionTokensInCredits?: boolean | number - outputCostPerMillionTokensInCredits?: boolean | number + inputCostPerMillionTokens?: boolean | number + outputCostPerMillionTokens?: boolean | number nativeCapabilities?: NativeModelCapabilitiesGenqlSelection isDeprecated?: boolean | number isRecommended?: boolean | number providerName?: boolean | number + providerLabel?: boolean | number + contextWindowTokens?: boolean | number + maxOutputTokens?: boolean | number dataResidency?: boolean | number __typename?: boolean | number __scalar?: boolean | number @@ -6371,7 +6379,9 @@ export interface MutationGenqlSelection{ userLookupAdminPanel?: (UserLookupGenqlSelection & { __args: {userIdentifier: Scalars['String']} }) updateWorkspaceFeatureFlag?: { __args: {workspaceId: Scalars['UUID'], featureFlag: Scalars['String'], value: Scalars['Boolean']} } setAdminAiModelEnabled?: { __args: {modelId: Scalars['String'], enabled: Scalars['Boolean']} } + setAdminAiModelsEnabled?: { __args: {modelIds: Scalars['String'][], enabled: Scalars['Boolean']} } setAdminAiModelRecommended?: { __args: {modelId: Scalars['String'], recommended: Scalars['Boolean']} } + setAdminAiModelsRecommended?: { __args: {modelIds: Scalars['String'][], recommended: Scalars['Boolean']} } setAdminDefaultAiModel?: { __args: {role: AiModelRole, modelId: Scalars['String']} } createDatabaseConfigVariable?: { __args: {key: Scalars['String'], value: Scalars['JSON']} } updateDatabaseConfigVariable?: { __args: {key: Scalars['String'], value: Scalars['JSON']} } diff --git a/packages/twenty-client-sdk/src/metadata/generated/types.ts b/packages/twenty-client-sdk/src/metadata/generated/types.ts index a262b4737d..6951e0017b 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/types.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/types.ts @@ -3299,10 +3299,10 @@ export default { "sdkPackage": [ 1 ], - "inputCostPerMillionTokensInCredits": [ + "inputCostPerMillionTokens": [ 11 ], - "outputCostPerMillionTokensInCredits": [ + "outputCostPerMillionTokens": [ 11 ], "nativeCapabilities": [ @@ -3317,6 +3317,15 @@ export default { "providerName": [ 1 ], + "providerLabel": [ + 1 + ], + "contextWindowTokens": [ + 11 + ], + "maxOutputTokens": [ + 11 + ], "dataResidency": [ 1 ], @@ -9020,6 +9029,19 @@ export default { ] } ], + "setAdminAiModelsEnabled": [ + 6, + { + "modelIds": [ + 1, + "[String!]!" + ], + "enabled": [ + 6, + "Boolean!" + ] + } + ], "setAdminAiModelRecommended": [ 6, { @@ -9033,6 +9055,19 @@ export default { ] } ], + "setAdminAiModelsRecommended": [ + 6, + { + "modelIds": [ + 1, + "[String!]!" + ], + "recommended": [ + 6, + "Boolean!" + ] + } + ], "setAdminDefaultAiModel": [ 6, { diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index b86e180ac6..b2f6c9d5ce 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -867,16 +867,19 @@ export type CheckUserExist = { export type ClientAiModelConfig = { __typename?: 'ClientAIModelConfig'; + contextWindowTokens?: Maybe; dataResidency?: Maybe; - inputCostPerMillionTokensInCredits: Scalars['Float']; + inputCostPerMillionTokens?: Maybe; isDeprecated?: Maybe; isRecommended?: Maybe; label: Scalars['String']; + maxOutputTokens?: Maybe; modelFamily?: Maybe; modelFamilyLabel?: Maybe; modelId: Scalars['String']; nativeCapabilities?: Maybe; - outputCostPerMillionTokensInCredits: Scalars['Float']; + outputCostPerMillionTokens?: Maybe; + providerLabel?: Maybe; providerName?: Maybe; sdkPackage?: Maybe; }; @@ -2665,6 +2668,8 @@ export type Mutation = { sendInvitations: SendInvitations; setAdminAiModelEnabled: Scalars['Boolean']; setAdminAiModelRecommended: Scalars['Boolean']; + setAdminAiModelsEnabled: Scalars['Boolean']; + setAdminAiModelsRecommended: Scalars['Boolean']; setAdminDefaultAiModel: Scalars['Boolean']; setEnterpriseKey: EnterpriseLicenseInfoDto; setMaintenanceMode: Scalars['Boolean']; @@ -3412,6 +3417,18 @@ export type MutationSetAdminAiModelRecommendedArgs = { }; +export type MutationSetAdminAiModelsEnabledArgs = { + enabled: Scalars['Boolean']; + modelIds: Array; +}; + + +export type MutationSetAdminAiModelsRecommendedArgs = { + modelIds: Array; + recommended: Scalars['Boolean']; +}; + + export type MutationSetAdminDefaultAiModelArgs = { modelId: Scalars['String']; role: AiModelRole; @@ -7223,6 +7240,22 @@ export type SetAdminAiModelRecommendedMutationVariables = Exact<{ export type SetAdminAiModelRecommendedMutation = { __typename?: 'Mutation', setAdminAiModelRecommended: boolean }; +export type SetAdminAiModelsEnabledMutationVariables = Exact<{ + modelIds: Array | Scalars['String']; + enabled: Scalars['Boolean']; +}>; + + +export type SetAdminAiModelsEnabledMutation = { __typename?: 'Mutation', setAdminAiModelsEnabled: boolean }; + +export type SetAdminAiModelsRecommendedMutationVariables = Exact<{ + modelIds: Array | Scalars['String']; + recommended: Scalars['Boolean']; +}>; + + +export type SetAdminAiModelsRecommendedMutation = { __typename?: 'Mutation', setAdminAiModelsRecommended: boolean }; + export type SetAdminDefaultAiModelMutationVariables = Exact<{ role: AiModelRole; modelId: Scalars['String']; @@ -8479,6 +8512,8 @@ export const RemoveAiProviderDocument = {"kind":"Document","definitions":[{"kind export const RemoveModelFromProviderDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RemoveModelFromProvider"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"providerName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"modelName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"removeModelFromProvider"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"providerName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"providerName"}}},{"kind":"Argument","name":{"kind":"Name","value":"modelName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"modelName"}}}]}]}}]} as unknown as DocumentNode; export const SetAdminAiModelEnabledDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SetAdminAiModelEnabled"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"enabled"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setAdminAiModelEnabled"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"modelId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}}},{"kind":"Argument","name":{"kind":"Name","value":"enabled"},"value":{"kind":"Variable","name":{"kind":"Name","value":"enabled"}}}]}]}}]} as unknown as DocumentNode; export const SetAdminAiModelRecommendedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SetAdminAiModelRecommended"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recommended"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setAdminAiModelRecommended"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"modelId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}}},{"kind":"Argument","name":{"kind":"Name","value":"recommended"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recommended"}}}]}]}}]} as unknown as DocumentNode; +export const SetAdminAiModelsEnabledDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SetAdminAiModelsEnabled"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"modelIds"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"enabled"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setAdminAiModelsEnabled"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"modelIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"modelIds"}}},{"kind":"Argument","name":{"kind":"Name","value":"enabled"},"value":{"kind":"Variable","name":{"kind":"Name","value":"enabled"}}}]}]}}]} as unknown as DocumentNode; +export const SetAdminAiModelsRecommendedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SetAdminAiModelsRecommended"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"modelIds"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recommended"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setAdminAiModelsRecommended"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"modelIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"modelIds"}}},{"kind":"Argument","name":{"kind":"Name","value":"recommended"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recommended"}}}]}]}}]} as unknown as DocumentNode; export const SetAdminDefaultAiModelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SetAdminDefaultAiModel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"role"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"AiModelRole"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setAdminDefaultAiModel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"role"},"value":{"kind":"Variable","name":{"kind":"Name","value":"role"}}},{"kind":"Argument","name":{"kind":"Name","value":"modelId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}}}]}]}}]} as unknown as DocumentNode; export const GetAdminAiModelsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAdminAiModels"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getAdminAiModels"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"defaultSmartModelId"}},{"kind":"Field","name":{"kind":"Name","value":"defaultFastModelId"}},{"kind":"Field","name":{"kind":"Name","value":"models"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"modelId"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"modelFamily"}},{"kind":"Field","name":{"kind":"Name","value":"sdkPackage"}},{"kind":"Field","name":{"kind":"Name","value":"isAvailable"}},{"kind":"Field","name":{"kind":"Name","value":"isAdminEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"isDeprecated"}},{"kind":"Field","name":{"kind":"Name","value":"isRecommended"}},{"kind":"Field","name":{"kind":"Name","value":"contextWindowTokens"}},{"kind":"Field","name":{"kind":"Name","value":"maxOutputTokens"}},{"kind":"Field","name":{"kind":"Name","value":"inputCostPerMillionTokens"}},{"kind":"Field","name":{"kind":"Name","value":"outputCostPerMillionTokens"}},{"kind":"Field","name":{"kind":"Name","value":"providerName"}},{"kind":"Field","name":{"kind":"Name","value":"providerLabel"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"dataResidency"}}]}}]}}]}}]} as unknown as DocumentNode; export const GetAdminAiUsageByWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAdminAiUsageByWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"periodStart"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"DateTime"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"periodEnd"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"DateTime"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getAdminAiUsageByWorkspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"periodStart"},"value":{"kind":"Variable","name":{"kind":"Name","value":"periodStart"}}},{"kind":"Argument","name":{"kind":"Name","value":"periodEnd"},"value":{"kind":"Variable","name":{"kind":"Name","value":"periodEnd"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"creditsUsed"}}]}}]}}]} as unknown as DocumentNode; diff --git a/packages/twenty-front/src/modules/ai/hooks/useAiModelOptions.ts b/packages/twenty-front/src/modules/ai/hooks/useAiModelOptions.ts index d65dc5d90c..5ce1c5aff4 100644 --- a/packages/twenty-front/src/modules/ai/hooks/useAiModelOptions.ts +++ b/packages/twenty-front/src/modules/ai/hooks/useAiModelOptions.ts @@ -5,7 +5,7 @@ import { type SelectOption } from 'twenty-ui/input'; import { useWorkspaceAiModelAvailability } from '@/ai/hooks/useWorkspaceAiModelAvailability'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { aiModelsState } from '@/client-config/states/aiModelsState'; -import { getModelIcon } from '@/settings/admin-panel/ai/utils/getModelIcon'; +import { getModelIcon } from '@/settings/ai/utils/getModelIcon'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; type UseAiModelOptionsVariant = 'all' | 'pinned-default'; diff --git a/packages/twenty-front/src/modules/ai/hooks/useWorkspaceAiModelAvailability.ts b/packages/twenty-front/src/modules/ai/hooks/useWorkspaceAiModelAvailability.ts index 7e844289d8..b871eec49b 100644 --- a/packages/twenty-front/src/modules/ai/hooks/useWorkspaceAiModelAvailability.ts +++ b/packages/twenty-front/src/modules/ai/hooks/useWorkspaceAiModelAvailability.ts @@ -3,49 +3,25 @@ import { isAutoSelectModelId } from 'twenty-shared/utils'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { aiModelsState } from '@/client-config/states/aiModelsState'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; -import { type ClientAiModelConfig } from '~/generated-metadata/graphql'; export const useWorkspaceAiModelAvailability = () => { const aiModels = useAtomStateValue(aiModelsState); const currentWorkspace = useAtomStateValue(currentWorkspaceState); const useRecommendedModels = currentWorkspace?.useRecommendedModels ?? true; - const enabledAiModelIds = currentWorkspace?.enabledAiModelIds ?? []; - - const isModelEnabled = ( - modelId: string, - model?: ClientAiModelConfig, - ): boolean => { - if (isAutoSelectModelId(modelId)) { - return true; - } - - if (useRecommendedModels) { - return model?.isRecommended === true; - } - - return enabledAiModelIds.includes(modelId); - }; + const enabledAiModelIds = new Set(currentWorkspace?.enabledAiModelIds ?? []); const realModels = aiModels.filter( (model) => !isAutoSelectModelId(model.modelId) && !model.isDeprecated, ); - const enabledModels = realModels.filter((model) => - isModelEnabled(model.modelId, model), - ); - - const allModelsWithAvailability = realModels.map((model) => ({ - ...model, - isEnabled: isModelEnabled(model.modelId, model), - })); + const enabledModels = useRecommendedModels + ? realModels.filter((model) => model.isRecommended === true) + : realModels.filter((model) => enabledAiModelIds.has(model.modelId)); return { - isModelEnabled, enabledModels, realModels, - allModelsWithAvailability, useRecommendedModels, - enabledAiModelIds, }; }; 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 index 1ff84371d8..7eaf502052 100644 --- 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 @@ -10,17 +10,18 @@ import { themeCssVariables } from 'twenty-ui/theme-constants'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { billingState } from '@/client-config/states/billingState'; import { useClientConfig } from '@/client-config/hooks/useClientConfig'; -import { SettingsAdminAiModelsTable } from '@/settings/admin-panel/ai/components/SettingsAdminAiModelsTable'; +import { SettingsAiModelsTable } from '@/settings/ai/components/SettingsAiModelsTable'; import { SettingsAdminAiProviderListCard } from '@/settings/admin-panel/ai/components/SettingsAdminAiProviderListCard'; import { AI_PROVIDER_SOURCE } from '@/settings/admin-panel/ai/constants/AiProviderSource'; import { SET_ADMIN_AI_MODEL_RECOMMENDED } from '@/settings/admin-panel/ai/graphql/mutations/setAdminAiModelRecommended'; +import { SET_ADMIN_AI_MODELS_RECOMMENDED } from '@/settings/admin-panel/ai/graphql/mutations/setAdminAiModelsRecommended'; import { SET_ADMIN_DEFAULT_AI_MODEL } from '@/settings/admin-panel/ai/graphql/mutations/setAdminDefaultAiModel'; import { GET_ADMIN_AI_MODELS } from '@/settings/admin-panel/ai/graphql/queries/getAdminAiModels'; import { GET_ADMIN_AI_USAGE_BY_WORKSPACE } from '@/settings/admin-panel/ai/graphql/queries/getAdminAiUsageByWorkspace'; import { GET_AI_PROVIDERS } from '@/settings/admin-panel/ai/graphql/queries/getAiProviders'; import { type GetAiProvidersResult } from '@/settings/admin-panel/ai/types/GetAiProvidersResult'; -import { getModelIcon } from '@/settings/admin-panel/ai/utils/getModelIcon'; import { parseProviderItems } from '@/settings/admin-panel/ai/utils/parseProviderItems'; +import { getModelIcon } from '@/settings/ai/utils/getModelIcon'; import { SettingsAdminTabSkeletonLoader } from '@/settings/admin-panel/components/SettingsAdminTabSkeletonLoader'; import { SettingsEnterpriseFeatureGateCard } from '@/settings/components/SettingsEnterpriseFeatureGateCard'; import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect'; @@ -62,7 +63,11 @@ export const SettingsAdminAI = () => { const periodOptions = getPeriodOptions(); const usageDates = getPeriodDates(usagePeriod); - const { data, loading: isLoadingModels } = useQuery<{ + const { + data, + loading: isLoadingModels, + refetch: refetchModels, + } = useQuery<{ getAdminAiModels: { defaultSmartModelId?: string | null; defaultFastModelId?: string | null; @@ -71,6 +76,7 @@ export const SettingsAdminAI = () => { }>(GET_ADMIN_AI_MODELS); const [setModelRecommended] = useMutation(SET_ADMIN_AI_MODEL_RECOMMENDED); + const [setModelsRecommended] = useMutation(SET_ADMIN_AI_MODELS_RECOMMENDED); const [setDefaultModel] = useMutation(SET_ADMIN_DEFAULT_AI_MODEL); const { data: providersData, loading: isLoadingProviders } = @@ -244,10 +250,35 @@ export const SettingsAdminAI = () => { description={t`Select which models appear as recommended in the workspace model picker`} /> - model.isRecommended === true} onToggle={handleRecommendedToggle} - checkedField="isRecommended" + onToggleAll={async (shouldCheckAll) => { + const modelIds = enabledModels + .filter( + (model) => (model.isRecommended === true) !== shouldCheckAll, + ) + .map((model) => model.modelId); + + if (modelIds.length === 0) return; + + try { + await setModelsRecommended({ + variables: { + modelIds, + recommended: shouldCheckAll, + }, + }); + } catch { + enqueueErrorSnackBar({ + message: t`Failed to update model recommendations`, + }); + } finally { + await refetchModels(); + await refetchClientConfig(); + } + }} anchorPrefix="recommended-model-row" /> diff --git a/packages/twenty-front/src/modules/settings/admin-panel/ai/components/SettingsAdminAiModelHoverCard.tsx b/packages/twenty-front/src/modules/settings/admin-panel/ai/components/SettingsAdminAiModelHoverCard.tsx deleted file mode 100644 index bde2dfe050..0000000000 --- a/packages/twenty-front/src/modules/settings/admin-panel/ai/components/SettingsAdminAiModelHoverCard.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import { styled } from '@linaria/react'; -import { t } from '@lingui/core/macro'; -import { - IconBolt, - IconCoins, - IconFileText, - IconFlag, - IconServer, - IconTag, -} from 'twenty-ui/display'; -import { themeCssVariables } from 'twenty-ui/theme-constants'; - -import { SettingsAdminTableCard } from '@/settings/admin-panel/components/SettingsAdminTableCard'; -import { getDataResidencyDisplay } from '@/settings/admin-panel/ai/utils/getDataResidencyDisplay'; -import { getModelIcon } from '@/settings/admin-panel/ai/utils/getModelIcon'; -import { type ModelFamily } from '~/generated-metadata/graphql'; -import { formatNumber } from '~/utils/format/formatNumber'; - -const StyledNameValue = styled.span` - align-items: center; - display: flex; - gap: 4px; -`; - -const StyledHoverCardWrapper = styled.div` - border-radius: ${themeCssVariables.border.radius.md}; - box-shadow: ${themeCssVariables.boxShadow.strong}; - overflow: hidden; -`; - -type SettingsAdminAiModelHoverCardProps = { - label: string; - modelFamily?: ModelFamily | null; - providerName?: string | null; - providerLabel: string; - contextWindowTokens?: number | null; - maxOutputTokens?: number | null; - inputCostPerMillionTokens?: number | null; - outputCostPerMillionTokens?: number | null; - dataResidency?: string | null; -}; - -const formatCost = ( - inputCost?: number | null, - outputCost?: number | null, -): string => { - if (inputCost == null && outputCost == null) { - return '—'; - } - - const parts: string[] = []; - - if (inputCost != null) { - parts.push(`$${inputCost} in`); - } - - if (outputCost != null) { - parts.push(`$${outputCost} out`); - } - - return parts.join(' / '); -}; - -export const SettingsAdminAiModelHoverCard = ({ - label, - modelFamily, - providerName, - providerLabel, - contextWindowTokens, - maxOutputTokens, - inputCostPerMillionTokens, - outputCostPerMillionTokens, - dataResidency, -}: SettingsAdminAiModelHoverCardProps) => { - const ModelIcon = getModelIcon(modelFamily, providerName); - - const items = [ - { - Icon: IconTag, - label: t`Name`, - value: ( - - - {label} - - ), - }, - { - Icon: IconServer, - label: t`Provider`, - value: providerLabel || '—', - }, - ...(inputCostPerMillionTokens != null || outputCostPerMillionTokens != null - ? [ - { - Icon: IconCoins, - label: t`Cost / 1M`, - value: formatCost( - inputCostPerMillionTokens, - outputCostPerMillionTokens, - ), - }, - ] - : []), - ...(contextWindowTokens != null - ? [ - { - Icon: IconFileText, - label: t`Context`, - value: `${formatNumber(contextWindowTokens, { - abbreviate: true, - decimals: 1, - })} tokens`, - }, - ] - : []), - ...(maxOutputTokens != null - ? [ - { - Icon: IconBolt, - label: t`Max output`, - value: `${formatNumber(maxOutputTokens, { - abbreviate: true, - decimals: 1, - })} tokens`, - }, - ] - : []), - ...(dataResidency - ? [ - { - Icon: IconFlag, - label: t`Data residency`, - value: getDataResidencyDisplay(dataResidency), - }, - ] - : []), - ]; - - return ( - - - - ); -}; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/ai/components/SettingsAdminAiModelsTable.tsx b/packages/twenty-front/src/modules/settings/admin-panel/ai/components/SettingsAdminAiModelsTable.tsx deleted file mode 100644 index 6d904851e4..0000000000 --- a/packages/twenty-front/src/modules/settings/admin-panel/ai/components/SettingsAdminAiModelsTable.tsx +++ /dev/null @@ -1,245 +0,0 @@ -import { useContext, useState } from 'react'; - -import { css } from '@linaria/core'; -import { styled } from '@linaria/react'; -import { Trans } from '@lingui/react/macro'; -import { isDefined } from 'twenty-shared/utils'; -import { AppTooltip, IconTrash, TooltipDelay } from 'twenty-ui/display'; -import { Checkbox, IconButton } from 'twenty-ui/input'; -import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants'; - -import { SettingsAdminAiModelHoverCard } from '@/settings/admin-panel/ai/components/SettingsAdminAiModelHoverCard'; -import { type AdminAiModelConfig } from '~/generated-metadata/graphql'; -import { Table } from '@/ui/layout/table/components/Table'; -import { TableBody } from '@/ui/layout/table/components/TableBody'; -import { TableCell } from '@/ui/layout/table/components/TableCell'; -import { TableHeader } from '@/ui/layout/table/components/TableHeader'; -import { TableRow } from '@/ui/layout/table/components/TableRow'; -import { getModelIcon } from '@/settings/admin-panel/ai/utils/getModelIcon'; - -const getProviderDisplayLabel = ( - model: Pick, -): string => model.providerLabel ?? model.providerName ?? ''; - -const formatCost = ( - model: Pick< - AdminAiModelConfig, - 'inputCostPerMillionTokens' | 'outputCostPerMillionTokens' - >, -): string => { - const input = model.inputCostPerMillionTokens; - const output = model.outputCostPerMillionTokens; - - if (!isDefined(input) && !isDefined(output)) { - return '—'; - } - - const formatValue = (value: number | null | undefined) => - isDefined(value) ? `$${value}` : '—'; - - return `${formatValue(input)} / ${formatValue(output)}`; -}; - -type SecondaryColumn = 'provider' | 'cost'; - -const GRID_TEMPLATE_COLUMNS: Record = { - provider: '1fr 120px 40px', - cost: '1fr 140px 40px', -}; - -const GRID_TEMPLATE_COLUMNS_WITH_REMOVE: Record = { - provider: '1fr 120px 40px 32px', - cost: '1fr 140px 40px 32px', -}; - -const StyledModelNameCell = styled.div` - align-items: center; - display: flex; - gap: ${themeCssVariables.spacing[2]}; - overflow: hidden; -`; - -const StyledModelLabel = styled.span` - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -`; - -const StyledDeprecatedSuffix = styled.span` - color: ${themeCssVariables.font.color.light}; -`; - -const hoverCardTooltipClass = css` - background-color: transparent !important; - border: none !important; - border-radius: 0 !important; - box-shadow: none !important; - padding: 0 !important; -`; - -const sanitizeIdForSelector = (id: string): string => - id.replace(/[^a-zA-Z0-9-_]/g, '_'); - -type SettingsAdminAiModelsTableProps = { - models: AdminAiModelConfig[]; - onToggle: (modelId: string, currentValue: boolean) => void; - checkedField: 'isAdminEnabled' | 'isRecommended'; - anchorPrefix: string; - showDisabledState?: boolean; - onRemove?: (model: AdminAiModelConfig) => void; - secondaryColumn?: SecondaryColumn; -}; - -export const SettingsAdminAiModelsTable = ({ - models, - onToggle, - checkedField, - anchorPrefix, - showDisabledState = false, - onRemove, - secondaryColumn = 'provider', -}: SettingsAdminAiModelsTableProps) => { - const [hoveredModelId, setHoveredModelId] = useState(null); - const { theme } = useContext(ThemeContext); - - const hoveredModel = models.find((model) => model.modelId === hoveredModelId); - const hasRemove = isDefined(onRemove); - const gridColumns = hasRemove - ? GRID_TEMPLATE_COLUMNS_WITH_REMOVE[secondaryColumn] - : GRID_TEMPLATE_COLUMNS[secondaryColumn]; - - return ( - <> - - - - Name - - - {secondaryColumn === 'provider' ? ( - Provider - ) : ( - Cost / 1M tokens - )} - - - {hasRemove && } - - - {models.map((model) => { - const ModelIcon = getModelIcon( - model.modelFamily, - model.providerName, - ); - const displayLabel = getProviderDisplayLabel(model); - const safeId = sanitizeIdForSelector(model.modelId); - const isChecked = model[checkedField] === true; - const isDisabled = - showDisabledState && - (!model.isAvailable || model.isDeprecated === true); - - return ( -
setHoveredModelId(model.modelId)} - onMouseLeave={() => setHoveredModelId(null)} - > - onToggle(model.modelId, isChecked) - } - > - - - - {model.label} - {showDisabledState && model.isDeprecated && ( - - · Deprecated - - )} - - - - {secondaryColumn === 'provider' - ? displayLabel - : formatCost(model)} - - event.stopPropagation()} - > - onToggle(model.modelId, isChecked)} - /> - - {hasRemove && ( - - { - event.stopPropagation(); - onRemove(model); - }} - /> - - )} - -
- ); - })} -
-
- - {hoveredModel && ( - - - - )} - - ); -}; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/ai/graphql/mutations/setAdminAiModelsEnabled.ts b/packages/twenty-front/src/modules/settings/admin-panel/ai/graphql/mutations/setAdminAiModelsEnabled.ts new file mode 100644 index 0000000000..0fdc4cc76d --- /dev/null +++ b/packages/twenty-front/src/modules/settings/admin-panel/ai/graphql/mutations/setAdminAiModelsEnabled.ts @@ -0,0 +1,7 @@ +import { gql } from '@apollo/client'; + +export const SET_ADMIN_AI_MODELS_ENABLED = gql` + mutation SetAdminAiModelsEnabled($modelIds: [String!]!, $enabled: Boolean!) { + setAdminAiModelsEnabled(modelIds: $modelIds, enabled: $enabled) + } +`; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/ai/graphql/mutations/setAdminAiModelsRecommended.ts b/packages/twenty-front/src/modules/settings/admin-panel/ai/graphql/mutations/setAdminAiModelsRecommended.ts new file mode 100644 index 0000000000..be62f6b43f --- /dev/null +++ b/packages/twenty-front/src/modules/settings/admin-panel/ai/graphql/mutations/setAdminAiModelsRecommended.ts @@ -0,0 +1,10 @@ +import { gql } from '@apollo/client'; + +export const SET_ADMIN_AI_MODELS_RECOMMENDED = gql` + mutation SetAdminAiModelsRecommended( + $modelIds: [String!]! + $recommended: Boolean! + ) { + setAdminAiModelsRecommended(modelIds: $modelIds, recommended: $recommended) + } +`; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminGeneral.tsx b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminGeneral.tsx index e9afc5a285..3cc7721be4 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminGeneral.tsx +++ b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminGeneral.tsx @@ -16,7 +16,7 @@ import { UserLookupAdminPanelDocument } from '~/generated-metadata/graphql'; import { currentUserState } from '@/auth/states/currentUserState'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; -import { SettingsAdminTableCard } from '@/settings/admin-panel/components/SettingsAdminTableCard'; +import { SettingsTableCard } from '@/settings/components/SettingsTableCard'; import { SettingsAdminVersionContainer } from '@/settings/admin-panel/components/SettingsAdminVersionContainer'; import { SETTINGS_ADMIN_USER_LOOKUP_WORKSPACE_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminUserLookupWorkspaceTabsId'; import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; @@ -183,7 +183,7 @@ export const SettingsAdminGeneral = () => { <>
- { ]; return ( - + ); }; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminWorkspaceContent.tsx b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminWorkspaceContent.tsx index b46f6745c6..f68fb195ea 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminWorkspaceContent.tsx +++ b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminWorkspaceContent.tsx @@ -1,7 +1,7 @@ import { currentUserState } from '@/auth/states/currentUserState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeatureFlagsState'; -import { SettingsAdminTableCard } from '@/settings/admin-panel/components/SettingsAdminTableCard'; +import { SettingsTableCard } from '@/settings/components/SettingsTableCard'; import { useFeatureFlagState } from '@/settings/admin-panel/hooks/useFeatureFlagState'; import { useImpersonationAuth } from '@/settings/admin-panel/hooks/useImpersonationAuth'; import { useImpersonationRedirect } from '@/settings/admin-panel/hooks/useImpersonationRedirect'; @@ -15,13 +15,14 @@ import { TableCell } from '@/ui/layout/table/components/TableCell'; import { TableHeader } from '@/ui/layout/table/components/TableHeader'; import { TableRow } from '@/ui/layout/table/components/TableRow'; import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo'; +import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { useMutation } from '@apollo/client/react'; import { styled } from '@linaria/react'; import { useLingui } from '@lingui/react/macro'; import { isNonEmptyString } from '@sniptt/guards'; -import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { useState } from 'react'; import { getImageAbsoluteURI, isDefined } from 'twenty-shared/utils'; -import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; import { AvatarOrIcon, Chip } from 'twenty-ui/components'; import { H2Title, @@ -35,7 +36,6 @@ import { Button, Toggle } from 'twenty-ui/input'; import { Section } from 'twenty-ui/layout'; import { themeCssVariables } from 'twenty-ui/theme-constants'; import { REACT_APP_SERVER_BASE_URL } from '~/config'; -import { useMutation } from '@apollo/client/react'; import { type FeatureFlagKey, ImpersonateDocument, @@ -202,7 +202,7 @@ export const SettingsAdminWorkspaceContent = ({ title={t`Workspace Info`} description={t`About this workspace`} /> - diff --git a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/SettingsAdminHealthAccountSyncCountersTable.tsx b/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/SettingsAdminHealthAccountSyncCountersTable.tsx index dba2921fdd..faf9491cf3 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/SettingsAdminHealthAccountSyncCountersTable.tsx +++ b/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/SettingsAdminHealthAccountSyncCountersTable.tsx @@ -1,11 +1,11 @@ -import { SettingsAdminTableCard } from '@/settings/admin-panel/components/SettingsAdminTableCard'; +import { SettingsTableCard } from '@/settings/components/SettingsTableCard'; import { styled } from '@linaria/react'; import { useLingui } from '@lingui/react/macro'; import { H2Title } from 'twenty-ui/display'; import { Section } from 'twenty-ui/layout'; import { themeCssVariables } from 'twenty-ui/theme-constants'; -const StyledSettingsAdminTableCardContainer = styled.div` +const StyledSettingsTableCardContainer = styled.div` > * { padding-left: ${themeCssVariables.spacing[2]}; padding-right: ${themeCssVariables.spacing[2]}; @@ -49,15 +49,15 @@ export const SettingsAdminHealthAccountSyncCountersTable = ({ return (
- - + - +
); }; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/SettingsAdminWorkerMetricsGraph.tsx b/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/SettingsAdminWorkerMetricsGraph.tsx index 2eac44a3a8..1bfff29d11 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/SettingsAdminWorkerMetricsGraph.tsx +++ b/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/SettingsAdminWorkerMetricsGraph.tsx @@ -1,5 +1,5 @@ import { isDefined } from 'twenty-shared/utils'; -import { SettingsAdminTableCard } from '@/settings/admin-panel/components/SettingsAdminTableCard'; +import { SettingsTableCard } from '@/settings/components/SettingsTableCard'; import { SettingsAdminWorkerMetricsTooltip } from '@/settings/admin-panel/health-status/components/SettingsAdminWorkerMetricsTooltip'; import { useSnackBarOnQueryError } from '@/apollo/hooks/useSnackBarOnQueryError'; import { styled } from '@linaria/react'; @@ -31,7 +31,7 @@ const StyledNoDataMessage = styled.div` justify-content: center; `; -const StyledSettingsAdminTableCardContainer = styled.div` +const StyledSettingsTableCardContainer = styled.div` > * { padding-left: ${themeCssVariables.spacing[2]}; padding-right: ${themeCssVariables.spacing[2]}; @@ -199,8 +199,8 @@ export const SettingsAdminWorkerMetricsGraph = ({ )} {isDefined(metricsDetails) && ( - - + key !== '__typename') @@ -217,7 +217,7 @@ export const SettingsAdminWorkerMetricsGraph = ({ labelAlign="left" valueAlign="right" /> - + )} ); diff --git a/packages/twenty-front/src/modules/settings/ai/components/SettingsAiModelHoverCard.tsx b/packages/twenty-front/src/modules/settings/ai/components/SettingsAiModelHoverCard.tsx new file mode 100644 index 0000000000..c25efa45b5 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/ai/components/SettingsAiModelHoverCard.tsx @@ -0,0 +1,182 @@ +import { styled } from '@linaria/react'; +import { t } from '@lingui/core/macro'; +import { useContext } from 'react'; +import { Chip, ChipAccent, ChipSize, ChipVariant } from 'twenty-ui/components'; +import { + IconBolt, + IconBuildingSkyscraper, + IconFlag, + IconTag, + IconUsers, + IconWindow, + type IconComponent, +} from 'twenty-ui/display'; +import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants'; + +import { type AiModelSummary } from '@/settings/ai/types/AiModelSummary'; +import { getDataResidencyDisplay } from '@/settings/ai/utils/getDataResidencyDisplay'; +import { getModelIcon } from '@/settings/ai/utils/getModelIcon'; +import { SettingsTableCard } from '@/settings/components/SettingsTableCard'; +import { isDefined } from 'twenty-shared/utils'; +import { formatNumber } from '~/utils/format/formatNumber'; + +const StyledHoverCardWrapper = styled.div` + border-radius: ${themeCssVariables.border.radius.md}; + box-shadow: ${themeCssVariables.boxShadow.strong}; + overflow: hidden; + width: 320px; +`; + +const StyledChipContainer = styled.div` + max-width: 100%; + overflow: hidden; +`; + +const StyledValueText = styled.span` + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const formatCost = ( + inputCost?: number | null, + outputCost?: number | null, +): string => { + if (!isDefined(inputCost) && !isDefined(outputCost)) { + return '—'; + } + + const parts: string[] = []; + + if (isDefined(inputCost)) { + parts.push(`$${inputCost} in`); + } + + if (isDefined(outputCost)) { + parts.push(`$${outputCost} out`); + } + + return parts.join(' / '); +}; + +type SettingsAiModelHoverCardProps = { + model: AiModelSummary; +}; + +type HoverCardItem = { + Icon: IconComponent; + label: string; + value: React.ReactNode; +}; + +export const SettingsAiModelHoverCard = ({ + model, +}: SettingsAiModelHoverCardProps) => { + const { theme } = useContext(ThemeContext); + + const ModelIcon = getModelIcon(model.modelFamily, model.providerName); + const providerLabel = model.providerLabel ?? model.providerName ?? '—'; + + const items: HoverCardItem[] = [ + { + Icon: IconTag, + label: t`Name`, + value: ( + + + } + rightComponent={null} + /> + + ), + }, + { + Icon: IconBuildingSkyscraper, + label: t`Provider`, + value: {providerLabel}, + }, + ...(isDefined(model.inputCostPerMillionTokens) || + isDefined(model.outputCostPerMillionTokens) + ? [ + { + Icon: IconUsers, + label: t`Cost per 1M tokens`, + value: ( + + {formatCost( + model.inputCostPerMillionTokens, + model.outputCostPerMillionTokens, + )} + + ), + }, + ] + : []), + ...(isDefined(model.contextWindowTokens) + ? [ + { + Icon: IconWindow, + label: t`Context`, + value: ( + + {`${formatNumber(model.contextWindowTokens, { + abbreviate: true, + decimals: 1, + })} tokens`} + + ), + }, + ] + : []), + ...(isDefined(model.maxOutputTokens) + ? [ + { + Icon: IconBolt, + label: t`Max output`, + value: ( + + {`${formatNumber(model.maxOutputTokens, { + abbreviate: true, + decimals: 1, + })} tokens`} + + ), + }, + ] + : []), + ...(isDefined(model.dataResidency) + ? [ + { + Icon: IconFlag, + label: t`Data residency`, + value: ( + + {getDataResidencyDisplay(model.dataResidency)} + + ), + }, + ] + : []), + ]; + + return ( + + + + ); +}; diff --git a/packages/twenty-front/src/modules/settings/ai/components/SettingsAiModelsTable.tsx b/packages/twenty-front/src/modules/settings/ai/components/SettingsAiModelsTable.tsx new file mode 100644 index 0000000000..272b0a9fde --- /dev/null +++ b/packages/twenty-front/src/modules/settings/ai/components/SettingsAiModelsTable.tsx @@ -0,0 +1,227 @@ +import { useContext, useState } from 'react'; + +import { css } from '@linaria/core'; +import { styled } from '@linaria/react'; +import { Trans } from '@lingui/react/macro'; +import { isDefined } from 'twenty-shared/utils'; +import { AppTooltip, IconTrash, TooltipDelay } from 'twenty-ui/display'; +import { Checkbox, IconButton } from 'twenty-ui/input'; +import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants'; + +import { SettingsAiModelHoverCard } from '@/settings/ai/components/SettingsAiModelHoverCard'; +import { type AiModelSummary } from '@/settings/ai/types/AiModelSummary'; +import { getModelIcon } from '@/settings/ai/utils/getModelIcon'; +import { Table } from '@/ui/layout/table/components/Table'; +import { TableBody } from '@/ui/layout/table/components/TableBody'; +import { TableCell } from '@/ui/layout/table/components/TableCell'; +import { TableHeader } from '@/ui/layout/table/components/TableHeader'; +import { TableRow } from '@/ui/layout/table/components/TableRow'; + +const GRID_COLUMNS = '1fr 120px 40px'; +const GRID_COLUMNS_WITHOUT_PROVIDER = '1fr 40px'; +const GRID_COLUMNS_WITH_REMOVE = '1fr 120px 40px 32px'; +const GRID_COLUMNS_WITH_REMOVE_WITHOUT_PROVIDER = '1fr 40px 32px'; + +const StyledModelNameCell = styled.div` + align-items: center; + display: flex; + gap: ${themeCssVariables.spacing[2]}; + overflow: hidden; +`; + +const StyledModelLabel = styled.span` + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const StyledDeprecatedSuffix = styled.span` + color: ${themeCssVariables.font.color.light}; +`; + +const hoverCardTooltipClass = css` + border-radius: ${themeCssVariables.border.radius.rounded} !important; + + padding: 0 !important; +`; + +const sanitizeIdForSelector = (id: string): string => + id.replace(/[^a-zA-Z0-9-_]/g, '_'); + +const getProviderDisplayLabel = (model: AiModelSummary): string => + model.providerLabel ?? model.providerName ?? ''; + +type SettingsAiModelsTableProps = { + models: TModel[]; + isChecked: (model: TModel) => boolean; + isDisabled?: (model: TModel) => boolean; + onToggle: (modelId: string, isCurrentlyChecked: boolean) => void; + onToggleAll?: (shouldCheckAll: boolean) => void; + onRemove?: (model: TModel) => void; + showProviderColumn?: boolean; + anchorPrefix?: string; +}; + +export const SettingsAiModelsTable = ({ + models, + isChecked, + isDisabled, + onToggle, + onToggleAll, + onRemove, + showProviderColumn = true, + anchorPrefix, +}: SettingsAiModelsTableProps) => { + const [hoveredModelId, setHoveredModelId] = useState(null); + const { theme } = useContext(ThemeContext); + + const hoveredModel = models.find((model) => model.modelId === hoveredModelId); + const hasRemove = isDefined(onRemove); + const gridColumns = hasRemove + ? showProviderColumn + ? GRID_COLUMNS_WITH_REMOVE + : GRID_COLUMNS_WITH_REMOVE_WITHOUT_PROVIDER + : showProviderColumn + ? GRID_COLUMNS + : GRID_COLUMNS_WITHOUT_PROVIDER; + + const toggleableModels = models.filter( + (model) => !(isDisabled?.(model) ?? false), + ); + const checkedCount = toggleableModels.filter((model) => + isChecked(model), + ).length; + const allChecked = + toggleableModels.length > 0 && checkedCount === toggleableModels.length; + const noneChecked = checkedCount === 0; + + return ( + <> + + + + Name + + {showProviderColumn && ( + + Provider + + )} + + {isDefined(onToggleAll) && ( + onToggleAll(!allChecked)} + /> + )} + + {hasRemove && } + + + {models.map((model) => { + const ModelIcon = getModelIcon( + model.modelFamily, + model.providerName, + ); + const safeId = sanitizeIdForSelector(model.modelId); + const checked = isChecked(model); + const disabled = isDisabled?.(model) ?? false; + + return ( + setHoveredModelId(model.modelId) + : undefined + } + onMouseLeave={ + anchorPrefix ? () => setHoveredModelId(null) : undefined + } + onClick={ + disabled ? undefined : () => onToggle(model.modelId, checked) + } + > + + + + {model.label} + {disabled && model.isDeprecated && ( + + · Deprecated + + )} + + + {showProviderColumn && ( + + {getProviderDisplayLabel(model)} + + )} + event.stopPropagation()} + > + onToggle(model.modelId, checked)} + /> + + {hasRemove && ( + + { + event.stopPropagation(); + onRemove(model); + }} + /> + + )} + + ); + })} + +
+ + {anchorPrefix && hoveredModel && ( + + + + )} + + ); +}; diff --git a/packages/twenty-front/src/modules/settings/ai/types/AiModelSummary.ts b/packages/twenty-front/src/modules/settings/ai/types/AiModelSummary.ts new file mode 100644 index 0000000000..c0b0bc7836 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/ai/types/AiModelSummary.ts @@ -0,0 +1,15 @@ +import { type ModelFamily } from '~/generated-metadata/graphql'; + +export type AiModelSummary = { + modelId: string; + label: string; + modelFamily?: ModelFamily | null; + providerName?: string | null; + isDeprecated?: boolean | null; + dataResidency?: string | null; + providerLabel?: string | null; + contextWindowTokens?: number | null; + maxOutputTokens?: number | null; + inputCostPerMillionTokens?: number | null; + outputCostPerMillionTokens?: number | null; +}; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/ai/utils/getDataResidencyDisplay.ts b/packages/twenty-front/src/modules/settings/ai/utils/getDataResidencyDisplay.ts similarity index 100% rename from packages/twenty-front/src/modules/settings/admin-panel/ai/utils/getDataResidencyDisplay.ts rename to packages/twenty-front/src/modules/settings/ai/utils/getDataResidencyDisplay.ts diff --git a/packages/twenty-front/src/modules/settings/admin-panel/ai/utils/getModelIcon.ts b/packages/twenty-front/src/modules/settings/ai/utils/getModelIcon.ts similarity index 100% rename from packages/twenty-front/src/modules/settings/admin-panel/ai/utils/getModelIcon.ts rename to packages/twenty-front/src/modules/settings/ai/utils/getModelIcon.ts index 74a340dc81..988b441162 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/ai/utils/getModelIcon.ts +++ b/packages/twenty-front/src/modules/settings/ai/utils/getModelIcon.ts @@ -1,8 +1,8 @@ import { type IconComponent } from 'twenty-ui/display'; import { MODEL_ICON_CONFIG } from '@/settings/admin-panel/ai/constants/ModelIconConfig'; -import { isModelIconKey } from '@/settings/admin-panel/ai/utils/isModelIconKey'; import { getProviderIcon } from '@/settings/admin-panel/ai/utils/getProviderIcon'; +import { isModelIconKey } from '@/settings/admin-panel/ai/utils/isModelIconKey'; import { type ModelFamily } from '~/generated-metadata/graphql'; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminTableCard.tsx b/packages/twenty-front/src/modules/settings/components/SettingsTableCard.tsx similarity index 75% rename from packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminTableCard.tsx rename to packages/twenty-front/src/modules/settings/components/SettingsTableCard.tsx index 22479fc44a..ce8fc99be6 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminTableCard.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsTableCard.tsx @@ -1,11 +1,14 @@ import { Table } from '@/ui/layout/table/components/Table'; -import { TableBody } from '@/ui/layout/table/components/TableBody'; import { TableCell } from '@/ui/layout/table/components/TableCell'; import { TableRow } from '@/ui/layout/table/components/TableRow'; -import { isDefined } from 'twenty-shared/utils'; -import { type IconComponent } from 'twenty-ui/display'; -import { Card } from 'twenty-ui/layout'; +import { styled } from '@linaria/react'; import { useContext } from 'react'; +import { isDefined } from 'twenty-shared/utils'; +import { + OverflowingTextWithTooltip, + type IconComponent, +} from 'twenty-ui/display'; +import { Card } from 'twenty-ui/layout'; import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants'; type TableItem = { @@ -15,7 +18,14 @@ type TableItem = { onClick?: () => void; }; -type SettingsAdminTableCardProps = { +const StyledTableBody = styled.div` + display: flex; + flex-direction: column; + gap: ${themeCssVariables.spacing[2]}; + padding: ${themeCssVariables.spacing[2]} 0; +`; + +type SettingsTableCardProps = { items: TableItem[]; rounded?: boolean; gridAutoColumns?: string; @@ -24,14 +34,14 @@ type SettingsAdminTableCardProps = { className?: string; }; -export const SettingsAdminTableCard = ({ +export const SettingsTableCard = ({ items, rounded = false, gridAutoColumns, labelAlign = 'left', valueAlign = 'left', className, -}: SettingsAdminTableCardProps) => { +}: SettingsTableCardProps) => { const { theme } = useContext(ThemeContext); return ( - + {items.map((item, index) => ( {item.Icon && } - {item.label} + {item.value} ))} - +
); diff --git a/packages/twenty-front/src/modules/ui/layout/table/components/TableRow.tsx b/packages/twenty-front/src/modules/ui/layout/table/components/TableRow.tsx index cbb9b3261a..3c22466a31 100644 --- a/packages/twenty-front/src/modules/ui/layout/table/components/TableRow.tsx +++ b/packages/twenty-front/src/modules/ui/layout/table/components/TableRow.tsx @@ -55,10 +55,13 @@ const StyledTableRow = styled.div<{ `; type TableRowProps = { + id?: string; isSelected?: boolean; isExpanded?: boolean; isClickable?: boolean; onClick?: () => void; + onMouseEnter?: React.MouseEventHandler; + onMouseLeave?: React.MouseEventHandler; to?: string; className?: string; style?: React.CSSProperties; @@ -71,10 +74,13 @@ type TableRowProps = { }; export const TableRow = ({ + id, isSelected, isExpanded, isClickable, onClick, + onMouseEnter, + onMouseLeave, to, className, style, @@ -87,9 +93,12 @@ export const TableRow = ({ hoverBackgroundColor, }: React.PropsWithChildren) => ( { const { data: providersData, loading: isLoadingProviders } = useQuery(GET_AI_PROVIDERS); - const { data: modelsData, loading: isLoadingModels } = useQuery<{ + const { + data: modelsData, + loading: isLoadingModels, + refetch: refetchModels, + } = useQuery<{ getAdminAiModels: { models: AdminAiModelConfig[]; }; }>(GET_ADMIN_AI_MODELS); const [setModelEnabled] = useMutation(SetAdminAiModelEnabledDocument); + const [setModelsEnabled] = useMutation(SET_ADMIN_AI_MODELS_ENABLED); const [removeAiProvider] = useMutation(REMOVE_AI_PROVIDER); const [removeModelFromProvider] = useMutation(REMOVE_MODEL_FROM_PROVIDER); @@ -312,7 +318,7 @@ export const SettingsAdminAiProviderDetail = () => { /> {provider && ( - { )} {filteredModels.length > 0 && ( - model.isAdminEnabled} + isDisabled={(model) => + !model.isAvailable || model.isDeprecated === true + } onToggle={handleModelToggle} - checkedField="isAdminEnabled" + showProviderColumn={false} + onToggleAll={async (shouldCheckAll) => { + const modelIds = filteredModels + .filter( + (model) => + model.isAvailable && + model.isDeprecated !== true && + model.isAdminEnabled !== shouldCheckAll, + ) + .map((model) => model.modelId); + + if (modelIds.length === 0) return; + + try { + await setModelsEnabled({ + variables: { + modelIds, + enabled: shouldCheckAll, + }, + }); + } catch { + enqueueErrorSnackBar({ + message: t`Failed to update model availability`, + }); + } finally { + await refetchModels(); + await refetchClientConfig(); + } + }} anchorPrefix="provider-model-row" - showDisabledState - secondaryColumn="cost" onRemove={isCustomProvider ? handleModelRemoveClick : undefined} /> )} 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 11c60c683e..35898f7c3e 100644 --- a/packages/twenty-front/src/pages/settings/ai/components/SettingsAIModelsTab.tsx +++ b/packages/twenty-front/src/pages/settings/ai/components/SettingsAIModelsTab.tsx @@ -1,5 +1,5 @@ -import { useState } from 'react'; import { styled } from '@linaria/react'; +import { useState } from 'react'; import { AUTO_SELECT_FAST_MODEL_ID, @@ -9,6 +9,9 @@ import { import { useWorkspaceAiModelAvailability } from '@/ai/hooks/useWorkspaceAiModelAvailability'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { aiModelsState } from '@/client-config/states/aiModelsState'; +import { SettingsAiModelsTable } from '@/settings/ai/components/SettingsAiModelsTable'; +import { getDataResidencyDisplay } from '@/settings/ai/utils/getDataResidencyDisplay'; +import { getModelIcon } from '@/settings/ai/utils/getModelIcon'; import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect'; import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; @@ -16,18 +19,19 @@ import { Select } from '@/ui/input/components/Select'; import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { useMutation } from '@apollo/client/react'; import { t } from '@lingui/core/macro'; -import { H2Title, IconBolt, IconTwentyStar } from 'twenty-ui/display'; +import { H2Title, IconBolt, IconBrain, IconStar } from 'twenty-ui/display'; import { SearchInput } from 'twenty-ui/input'; import { Card, Section } from 'twenty-ui/layout'; import { themeCssVariables } from 'twenty-ui/theme-constants'; -import { useMutation } from '@apollo/client/react'; import { UpdateWorkspaceDocument } from '~/generated-metadata/graphql'; -import { getDataResidencyDisplay } from '@/settings/admin-panel/ai/utils/getDataResidencyDisplay'; -import { getModelIcon } from '@/settings/admin-panel/ai/utils/getModelIcon'; -const StyledSearchContainer = styled.div` - padding-bottom: ${themeCssVariables.spacing[2]}; +const StyledCustomModelsContainer = styled.div` + display: flex; + flex-direction: column; + gap: ${themeCssVariables.spacing[4]}; + padding-top: ${themeCssVariables.spacing[4]}; `; export const SettingsAIModelsTab = () => { @@ -39,12 +43,10 @@ export const SettingsAIModelsTab = () => { const [searchQuery, setSearchQuery] = useState(''); const aiModels = useAtomStateValue(aiModelsState); - const { - allModelsWithAvailability, - enabledModels, - useRecommendedModels, - realModels, - } = useWorkspaceAiModelAvailability(); + const { enabledModels, useRecommendedModels, realModels } = + useWorkspaceAiModelAvailability(); + + const enabledModelIdSet = new Set(currentWorkspace?.enabledAiModelIds ?? []); const currentSmartModel = currentWorkspace?.smartModel; const currentFastModel = currentWorkspace?.fastModel; @@ -207,7 +209,7 @@ export const SettingsAIModelsTab = () => { }; const filteredModels = searchQuery.trim() - ? allModelsWithAvailability.filter((model) => { + ? realModels.filter((model) => { const query = searchQuery.toLowerCase(); return ( @@ -216,19 +218,19 @@ export const SettingsAIModelsTab = () => { (model.sdkPackage?.toLowerCase().includes(query) ?? false) ); }) - : allModelsWithAvailability; + : realModels; return ( <>
@@ -257,8 +259,17 @@ export const SettingsAIModelsTab = () => { dropdownWidth={GenericDropdownContentWidth.ExtraLarge} /> + +
+ +
+ + { divider={!useRecommendedModels} /> -
- {!useRecommendedModels && ( -
- - - + {!useRecommendedModels && ( + - - - {filteredModels.map((model, index) => { - const familyLabel = model.modelFamilyLabel ?? ''; - const residency = model.dataResidency - ? getDataResidencyDisplay(model.dataResidency) - : undefined; - const description = residency - ? `${familyLabel} · ${residency}` - : familyLabel; + enabledModelIdSet.has(model.modelId)} + onToggle={handleModelToggle} + onToggleAll={async (shouldCheckAll) => { + const previousIds = currentWorkspace?.enabledAiModelIds ?? []; + const visibleModelIds = new Set( + filteredModels.map((m) => m.modelId), + ); - return ( - - handleModelToggle(model.modelId, model.isEnabled) - } - divider={index < filteredModels.length - 1} - /> - ); - })} - -
- )} + const newEnabledIds = shouldCheckAll + ? [...new Set([...previousIds, ...visibleModelIds])] + : previousIds.filter((id) => !visibleModelIds.has(id)); + + try { + setCurrentWorkspace({ + ...currentWorkspace!, + enabledAiModelIds: newEnabledIds, + }); + await updateWorkspace({ + variables: { input: { enabledAiModelIds: newEnabledIds } }, + }); + } catch { + setCurrentWorkspace({ + ...currentWorkspace!, + enabledAiModelIds: previousIds, + }); + enqueueErrorSnackBar({ + message: t`Failed to update model availability`, + }); + } + }} + anchorPrefix="workspace-model-row" + /> + + )} +
); }; diff --git a/packages/twenty-front/src/pages/settings/applications/components/SettingsApplicationVersionContainer.tsx b/packages/twenty-front/src/pages/settings/applications/components/SettingsApplicationVersionContainer.tsx index a3d93a5fdf..93b3783e64 100644 --- a/packages/twenty-front/src/pages/settings/applications/components/SettingsApplicationVersionContainer.tsx +++ b/packages/twenty-front/src/pages/settings/applications/components/SettingsApplicationVersionContainer.tsx @@ -1,6 +1,6 @@ import { styled } from '@linaria/react'; import { themeCssVariables } from 'twenty-ui/theme-constants'; -import { SettingsAdminTableCard } from '@/settings/admin-panel/components/SettingsAdminTableCard'; +import { SettingsTableCard } from '@/settings/components/SettingsTableCard'; import { SettingsAdminVersionDisplay } from '@/settings/admin-panel/components/SettingsAdminVersionDisplay'; import { useUpgradeApplication } from '@/marketplace/hooks/useUpgradeApplication'; import { t } from '@lingui/core/macro'; @@ -90,7 +90,7 @@ export const SettingsApplicationVersionContainer = ({ return ( - - - - Boolean) + async setAdminAiModelsEnabled( + @Args('modelIds', { type: () => [String] }) modelIds: string[], + @Args('enabled', { type: () => Boolean }) enabled: boolean, + ): Promise { + await this.aiModelRegistryService.setModelsAdminEnabled(modelIds, enabled); + + return true; + } + @UseGuards(AdminPanelGuard) @Mutation(() => Boolean) async setAdminAiModelRecommended( @@ -239,6 +250,20 @@ export class AdminPanelResolver { return true; } + @UseGuards(AdminPanelGuard) + @Mutation(() => Boolean) + async setAdminAiModelsRecommended( + @Args('modelIds', { type: () => [String] }) modelIds: string[], + @Args('recommended', { type: () => Boolean }) recommended: boolean, + ): Promise { + await this.aiModelRegistryService.setModelsRecommended( + modelIds, + recommended, + ); + + return true; + } + @UseGuards(AdminPanelGuard) @Mutation(() => Boolean) async setAdminDefaultAiModel( diff --git a/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts b/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts index eeab3dcc4d..e0a39123e3 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts @@ -56,11 +56,11 @@ export class ClientAIModelConfig { @Field(() => String, { nullable: true }) sdkPackage: AiSdkPackage | null; - @Field(() => Number) - inputCostPerMillionTokensInCredits: number; + @Field(() => Number, { nullable: true }) + inputCostPerMillionTokens?: number; - @Field(() => Number) - outputCostPerMillionTokensInCredits: number; + @Field(() => Number, { nullable: true }) + outputCostPerMillionTokens?: number; @Field(() => NativeModelCapabilities, { nullable: true }) nativeCapabilities?: NativeModelCapabilities; @@ -74,6 +74,15 @@ export class ClientAIModelConfig { @Field(() => String, { nullable: true }) providerName?: string; + @Field(() => String, { nullable: true }) + providerLabel?: string; + + @Field(() => Number, { nullable: true }) + contextWindowTokens?: number; + + @Field(() => Number, { nullable: true }) + maxOutputTokens?: number; + @Field(() => String, { nullable: true }) dataResidency?: string; } diff --git a/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.spec.ts b/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.spec.ts index 0e89625ffc..0dd06442b5 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.spec.ts @@ -38,6 +38,9 @@ describe('ClientConfigService', () => { getAdminFilteredModels: jest.fn().mockReturnValue([]), getRecommendedModelIds: jest.fn().mockReturnValue(new Set()), getModelConfig: jest.fn().mockReturnValue(undefined), + getResolvedProvidersForAdmin: jest.fn().mockReturnValue({}), + getDefaultSpeedModel: jest.fn().mockReturnValue(undefined), + getDefaultPerformanceModel: jest.fn().mockReturnValue(undefined), }, }, { diff --git a/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.ts b/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.ts index a44fbf561a..19379b4c81 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.ts @@ -21,7 +21,6 @@ import { import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service'; import { PUBLIC_FEATURE_FLAGS } from 'src/engine/core-modules/feature-flag/constants/public-feature-flag.const'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; -import { convertDollarsToBillingCredits } from 'src/engine/metadata-modules/ai/ai-billing/utils/convert-dollars-to-billing-credits.util'; import { AUTO_SELECT_FAST_MODEL_ID, AUTO_SELECT_SMART_MODEL_ID, @@ -69,6 +68,13 @@ export class ClientConfigService { this.aiModelRegistryService.getAdminFilteredModels(); const recommendedModelIds = this.aiModelRegistryService.getRecommendedModelIds(); + const resolvedProviders = + this.aiModelRegistryService.getResolvedProvidersForAdmin(); + + const getProviderLabel = (providerName?: string | null) => + providerName + ? (resolvedProviders[providerName]?.label ?? providerName) + : undefined; const aiModels: ClientAIModelConfig[] = availableModels.map( (registeredModel) => { @@ -77,6 +83,7 @@ export class ClientConfigService { ); const modelFamily = modelConfig?.modelFamily; + const providerName = registeredModel.providerName; return { modelId: registeredModel.modelId, @@ -86,20 +93,15 @@ export class ClientConfigService { ? MODEL_FAMILY_LABELS[modelFamily] : undefined, sdkPackage: registeredModel.sdkPackage, - providerName: registeredModel.providerName, + providerName, + providerLabel: getProviderLabel(providerName), nativeCapabilities: this.deriveNativeCapabilities( registeredModel.sdkPackage, ), - inputCostPerMillionTokensInCredits: modelConfig - ? convertDollarsToBillingCredits( - modelConfig.inputCostPerMillionTokens, - ) - : 0, - outputCostPerMillionTokensInCredits: modelConfig - ? convertDollarsToBillingCredits( - modelConfig.outputCostPerMillionTokens, - ) - : 0, + inputCostPerMillionTokens: modelConfig?.inputCostPerMillionTokens, + outputCostPerMillionTokens: modelConfig?.outputCostPerMillionTokens, + contextWindowTokens: modelConfig?.contextWindowTokens, + maxOutputTokens: modelConfig?.maxOutputTokens, isDeprecated: modelConfig?.isDeprecated, isRecommended: recommendedModelIds.has(registeredModel.modelId), dataResidency: modelConfig?.dataResidency, @@ -129,9 +131,17 @@ export class ClientConfigService { 'Default', modelFamily: defaultPerformanceModelConfig?.modelFamily, providerName: defaultPerformanceModel?.providerName, + providerLabel: getProviderLabel( + defaultPerformanceModel?.providerName, + ), sdkPackage: defaultPerformanceModel?.sdkPackage ?? null, - inputCostPerMillionTokensInCredits: 0, - outputCostPerMillionTokensInCredits: 0, + inputCostPerMillionTokens: + defaultPerformanceModelConfig?.inputCostPerMillionTokens, + outputCostPerMillionTokens: + defaultPerformanceModelConfig?.outputCostPerMillionTokens, + contextWindowTokens: + defaultPerformanceModelConfig?.contextWindowTokens, + maxOutputTokens: defaultPerformanceModelConfig?.maxOutputTokens, }, { modelId: AUTO_SELECT_FAST_MODEL_ID, @@ -141,9 +151,14 @@ export class ClientConfigService { 'Default', modelFamily: defaultSpeedModelConfig?.modelFamily, providerName: defaultSpeedModel?.providerName, + providerLabel: getProviderLabel(defaultSpeedModel?.providerName), sdkPackage: defaultSpeedModel?.sdkPackage ?? null, - inputCostPerMillionTokensInCredits: 0, - outputCostPerMillionTokensInCredits: 0, + inputCostPerMillionTokens: + defaultSpeedModelConfig?.inputCostPerMillionTokens, + outputCostPerMillionTokens: + defaultSpeedModelConfig?.outputCostPerMillionTokens, + contextWindowTokens: defaultSpeedModelConfig?.contextWindowTokens, + maxOutputTokens: defaultSpeedModelConfig?.maxOutputTokens, }, ); } diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/services/ai-model-preferences.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/services/ai-model-preferences.service.ts index 15092c8256..ff8c0f2a68 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/services/ai-model-preferences.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/services/ai-model-preferences.service.ts @@ -29,6 +29,24 @@ export class AiModelPreferencesService { await this.togglePreferenceList(modelId, 'recommendedModels', recommended); } + async setModelsAdminEnabled( + modelIds: string[], + enabled: boolean, + ): Promise { + await this.togglePreferenceListBulk(modelIds, 'disabledModels', !enabled); + } + + async setModelsRecommended( + modelIds: string[], + recommended: boolean, + ): Promise { + await this.togglePreferenceListBulk( + modelIds, + 'recommendedModels', + recommended, + ); + } + async setDefaultModel(role: AiModelRole, modelId: string): Promise { const prefs = { ...this.getPreferences() }; const key = @@ -46,16 +64,26 @@ export class AiModelPreferencesService { modelId: string, key: 'disabledModels' | 'recommendedModels', add: boolean, + ): Promise { + await this.togglePreferenceListBulk([modelId], key, add); + } + + private async togglePreferenceListBulk( + modelIds: string[], + key: 'disabledModels' | 'recommendedModels', + add: boolean, ): Promise { const prefs = { ...this.getPreferences() }; const current = prefs[key] ?? []; + const idSet = new Set(modelIds); if (add) { - if (!current.includes(modelId)) { - prefs[key] = [...current, modelId]; - } + const existing = new Set(current); + const toAdd = modelIds.filter((id) => !existing.has(id)); + + prefs[key] = [...current, ...toAdd]; } else { - prefs[key] = current.filter((id) => id !== modelId); + prefs[key] = current.filter((id) => !idSet.has(id)); } await this.twentyConfigService.set('AI_MODEL_PREFERENCES', prefs); diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service.ts index 1ee47997b3..1fc72f79b4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service.ts @@ -361,6 +361,22 @@ export class AiModelRegistryService { await this.preferencesService.setModelRecommended(modelId, recommended); } + async setModelsAdminEnabled( + modelIds: string[], + enabled: boolean, + ): Promise { + modelIds.forEach((id) => this.validateModelInRegistry(id)); + await this.preferencesService.setModelsAdminEnabled(modelIds, enabled); + } + + async setModelsRecommended( + modelIds: string[], + recommended: boolean, + ): Promise { + modelIds.forEach((id) => this.validateModelInRegistry(id)); + await this.preferencesService.setModelsRecommended(modelIds, recommended); + } + async setDefaultModel(role: AiModelRole, modelId: string): Promise { this.validateModelInRegistry(modelId); await this.preferencesService.setDefaultModel(role, modelId); diff --git a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts index 122f50f7da..3c943f9532 100644 --- a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts +++ b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts @@ -10,6 +10,7 @@ export { IconApi, IconApps, IconAppWindow, + IconWindow, IconArchive, IconArchiveOff, IconArrowBackUp, @@ -374,6 +375,7 @@ export { IconSquareRoundedX, IconSquareX, IconStack2, + IconStar, IconStatusChange, IconStepInto, IconStrikethrough, diff --git a/packages/twenty-ui/src/display/index.ts b/packages/twenty-ui/src/display/index.ts index 8102ccbe98..27398aee1e 100644 --- a/packages/twenty-ui/src/display/index.ts +++ b/packages/twenty-ui/src/display/index.ts @@ -88,6 +88,7 @@ export { IconApi, IconApps, IconAppWindow, + IconWindow, IconArchive, IconArchiveOff, IconArrowBackUp, @@ -452,6 +453,7 @@ export { IconSquareRoundedX, IconSquareX, IconStack2, + IconStar, IconStatusChange, IconStepInto, IconStrikethrough,