import { useMemo, useState } from 'react'; import { useMutation, useQuery } from '@apollo/client/react'; import { t } from '@lingui/core/macro'; import { SettingsPath } from 'twenty-shared/types'; import { getSettingsPath } from 'twenty-shared/utils'; import { IconBolt, IconRobot } from 'twenty-ui/icon'; import { H2Title } from 'twenty-ui/typography'; import { Section } from 'twenty-ui/layout'; import { Card } from 'twenty-ui/surfaces'; 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 { SettingsAiModelsTable } from '@/settings/ai/components/SettingsAiModelsTable'; import { useApolloAdminClient } from '@/settings/admin-panel/apollo/hooks/useApolloAdminClient'; 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 { parseProviderItems } from '@/settings/admin-panel/ai/utils/parseProviderItems'; import { getModelIcon } from '@/settings/ai/utils/getModelIcon'; import { SettingsSectionSkeletonLoader } from '@/settings/components/SettingsSectionSkeletonLoader'; import { SettingsEnterpriseFeatureGateCard } from '@/settings/components/SettingsEnterpriseFeatureGateCard'; import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect'; import { useUsageValueFormatter } from '@/settings/usage/hooks/useUsageValueFormatter'; import { getPeriodDates } from '@/settings/usage/utils/getPeriodDates'; import { getPeriodOptions } from '@/settings/usage/utils/getPeriodOptions'; import { type PeriodPreset } from '@/settings/usage/utils/periodPreset'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { Select } from '@/ui/input/components/Select'; import { Table } from '@/ui/layout/table/components/Table'; import { TableCell } from '@/ui/layout/table/components/TableCell'; import { TableHeader } from '@/ui/layout/table/components/TableHeader'; import { TableRow } from '@/ui/layout/table/components/TableRow'; import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; import { AiModelRole, type AdminAiModelConfig, } from '~/generated-admin/graphql'; import { OrganizationAdornment } from '~/pages/settings/enterprise/components/OrganizationAdornment'; const USAGE_TABLE_GRID_TEMPLATE_COLUMNS = '1fr 120px'; type UsageBreakdownItem = { key: string; label?: string | null; creditsUsed: number; }; export const SettingsAdminAI = () => { const apolloAdminClient = useApolloAdminClient(); const { enqueueErrorSnackBar } = useSnackBar(); const { refetch: refetchClientConfig } = useClientConfig(); const { formatUsageValue } = useUsageValueFormatter(); const currentWorkspace = useAtomStateValue(currentWorkspaceState); const billing = useAtomStateValue(billingState); const isBillingEnabled = billing?.isBillingEnabled ?? false; const hasEnterpriseAccess = isBillingEnabled || currentWorkspace?.hasValidEnterpriseValidityToken === true; const [usagePeriod, setUsagePeriod] = useState('30d'); const periodOptions = getPeriodOptions(); const usageDates = getPeriodDates(usagePeriod); const { data, loading: isLoadingModels, refetch: refetchModels, } = useQuery<{ getAdminAiModels: { defaultSmartModelId?: string | null; defaultFastModelId?: string | null; models: AdminAiModelConfig[]; }; }>(GET_ADMIN_AI_MODELS, { client: apolloAdminClient }); const [setModelRecommended] = useMutation(SET_ADMIN_AI_MODEL_RECOMMENDED, { client: apolloAdminClient, }); const [setModelsRecommended] = useMutation(SET_ADMIN_AI_MODELS_RECOMMENDED, { client: apolloAdminClient, }); const [setDefaultModel] = useMutation(SET_ADMIN_DEFAULT_AI_MODEL, { client: apolloAdminClient, }); const { data: providersData, loading: isLoadingProviders } = useQuery(GET_AI_PROVIDERS, { client: apolloAdminClient, }); const { data: usageData, previousData: previousUsageData } = useQuery<{ getAdminAiUsageByWorkspace: UsageBreakdownItem[]; }>(GET_ADMIN_AI_USAGE_BY_WORKSPACE, { client: apolloAdminClient, variables: { periodStart: usageDates.periodStart, periodEnd: usageDates.periodEnd, }, skip: !hasEnterpriseAccess, }); const effectiveUsageData = usageData ?? previousUsageData; const usageByWorkspace = effectiveUsageData?.getAdminAiUsageByWorkspace ?? []; const models = data?.getAdminAiModels?.models ?? []; const providerItems = useMemo( () => parseProviderItems(providersData?.getAiProviders ?? {}), [providersData], ); const catalogProviders = useMemo( () => providerItems .filter((provider) => provider.source === AI_PROVIDER_SOURCE.CATALOG) .sort((a, b) => (a.label ?? a.id).localeCompare(b.label ?? b.id)), [providerItems], ); const customProviders = providerItems.filter( (provider) => provider.source === AI_PROVIDER_SOURCE.CUSTOM, ); if (isLoadingProviders || isLoadingModels) { return ; } const handleRecommendedToggle = async ( modelId: string, isCurrentlyRecommended: boolean, ) => { try { await setModelRecommended({ variables: { modelId, recommended: !isCurrentlyRecommended }, refetchQueries: [{ query: GET_ADMIN_AI_MODELS }], }); await refetchClientConfig(); } catch { enqueueErrorSnackBar({ message: t`Failed to update model recommendation`, }); } }; const defaultSmartModelId = data?.getAdminAiModels?.defaultSmartModelId; const defaultFastModelId = data?.getAdminAiModels?.defaultFastModelId; const enabledModels = models.filter( (model) => model.isAvailable && model.isAdminEnabled && !model.isDeprecated, ); const availableModelOptions = enabledModels.map((model) => ({ value: model.modelId, label: model.label, Icon: getModelIcon(model.modelFamily, model.providerName), })); const handleDefaultModelChange = async ( role: AiModelRole, modelId: string, ) => { try { await setDefaultModel({ variables: { role, modelId }, refetchQueries: [{ query: GET_ADMIN_AI_MODELS }], }); await refetchClientConfig(); } catch { enqueueErrorSnackBar({ message: t`Failed to update default model`, }); } }; return ( <>
} />
{availableModelOptions.length > 0 && (
handleDefaultModelChange(AiModelRole.FAST, value) } options={availableModelOptions} selectSizeVariant="small" dropdownWidth={GenericDropdownContentWidth.ExtraLarge} />
)} {enabledModels.length > 0 && (
model.isRecommended === true} onToggle={handleRecommendedToggle} 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" />
)}
) : ( ) } /> {hasEnterpriseAccess ? ( usageByWorkspace.length > 0 ? ( {t`Workspace`} {t`Usage`} {usageByWorkspace.map((item) => ( {item.label ?? item.key} {formatUsageValue(item.creditsUsed)} ))}
) : ( {t`No AI usage data recorded yet.`} ) ) : ( )}
); };