[AI] new model tab design (#19384)
closes https://discord.com/channels/1130383047699738754/1480979892610007121 <img width="1839" height="1316" alt="CleanShot 2026-04-07 at 15 22 38" src="https://github.com/user-attachments/assets/d8f6048c-4e0f-425f-b7ea-4913d116020e" />
This commit is contained in:
@@ -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!
|
||||
|
||||
@@ -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']} }
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -867,16 +867,19 @@ export type CheckUserExist = {
|
||||
|
||||
export type ClientAiModelConfig = {
|
||||
__typename?: 'ClientAIModelConfig';
|
||||
contextWindowTokens?: Maybe<Scalars['Float']>;
|
||||
dataResidency?: Maybe<Scalars['String']>;
|
||||
inputCostPerMillionTokensInCredits: Scalars['Float'];
|
||||
inputCostPerMillionTokens?: Maybe<Scalars['Float']>;
|
||||
isDeprecated?: Maybe<Scalars['Boolean']>;
|
||||
isRecommended?: Maybe<Scalars['Boolean']>;
|
||||
label: Scalars['String'];
|
||||
maxOutputTokens?: Maybe<Scalars['Float']>;
|
||||
modelFamily?: Maybe<ModelFamily>;
|
||||
modelFamilyLabel?: Maybe<Scalars['String']>;
|
||||
modelId: Scalars['String'];
|
||||
nativeCapabilities?: Maybe<NativeModelCapabilities>;
|
||||
outputCostPerMillionTokensInCredits: Scalars['Float'];
|
||||
outputCostPerMillionTokens?: Maybe<Scalars['Float']>;
|
||||
providerLabel?: Maybe<Scalars['String']>;
|
||||
providerName?: Maybe<Scalars['String']>;
|
||||
sdkPackage?: Maybe<Scalars['String']>;
|
||||
};
|
||||
@@ -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<Scalars['String']>;
|
||||
};
|
||||
|
||||
|
||||
export type MutationSetAdminAiModelsRecommendedArgs = {
|
||||
modelIds: Array<Scalars['String']>;
|
||||
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']> | Scalars['String'];
|
||||
enabled: Scalars['Boolean'];
|
||||
}>;
|
||||
|
||||
|
||||
export type SetAdminAiModelsEnabledMutation = { __typename?: 'Mutation', setAdminAiModelsEnabled: boolean };
|
||||
|
||||
export type SetAdminAiModelsRecommendedMutationVariables = Exact<{
|
||||
modelIds: Array<Scalars['String']> | 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<RemoveModelFromProviderMutation, RemoveModelFromProviderMutationVariables>;
|
||||
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<SetAdminAiModelEnabledMutation, SetAdminAiModelEnabledMutationVariables>;
|
||||
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<SetAdminAiModelRecommendedMutation, SetAdminAiModelRecommendedMutationVariables>;
|
||||
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<SetAdminAiModelsEnabledMutation, SetAdminAiModelsEnabledMutationVariables>;
|
||||
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<SetAdminAiModelsRecommendedMutation, SetAdminAiModelsRecommendedMutationVariables>;
|
||||
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<SetAdminDefaultAiModelMutation, SetAdminDefaultAiModelMutationVariables>;
|
||||
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<GetAdminAiModelsQuery, GetAdminAiModelsQueryVariables>;
|
||||
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<GetAdminAiUsageByWorkspaceQuery, GetAdminAiUsageByWorkspaceQueryVariables>;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
+36
-5
@@ -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`}
|
||||
/>
|
||||
|
||||
<SettingsAdminAiModelsTable
|
||||
<SettingsAiModelsTable
|
||||
models={enabledModels}
|
||||
isChecked={(model) => 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"
|
||||
/>
|
||||
</Section>
|
||||
|
||||
-149
@@ -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: (
|
||||
<StyledNameValue>
|
||||
<ModelIcon size={14} />
|
||||
{label}
|
||||
</StyledNameValue>
|
||||
),
|
||||
},
|
||||
{
|
||||
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 (
|
||||
<StyledHoverCardWrapper>
|
||||
<SettingsAdminTableCard
|
||||
rounded
|
||||
items={items}
|
||||
gridAutoColumns="120px 1fr"
|
||||
/>
|
||||
</StyledHoverCardWrapper>
|
||||
);
|
||||
};
|
||||
-245
@@ -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<AdminAiModelConfig, 'providerLabel' | 'providerName'>,
|
||||
): 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<SecondaryColumn, string> = {
|
||||
provider: '1fr 120px 40px',
|
||||
cost: '1fr 140px 40px',
|
||||
};
|
||||
|
||||
const GRID_TEMPLATE_COLUMNS_WITH_REMOVE: Record<SecondaryColumn, string> = {
|
||||
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<string | null>(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 (
|
||||
<>
|
||||
<Table>
|
||||
<TableRow gridTemplateColumns={gridColumns}>
|
||||
<TableHeader>
|
||||
<Trans>Name</Trans>
|
||||
</TableHeader>
|
||||
<TableHeader align="right">
|
||||
{secondaryColumn === 'provider' ? (
|
||||
<Trans>Provider</Trans>
|
||||
) : (
|
||||
<Trans>Cost / 1M tokens</Trans>
|
||||
)}
|
||||
</TableHeader>
|
||||
<TableHeader />
|
||||
{hasRemove && <TableHeader />}
|
||||
</TableRow>
|
||||
<TableBody>
|
||||
{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 (
|
||||
<div
|
||||
key={model.modelId}
|
||||
id={`${anchorPrefix}-${safeId}`}
|
||||
onMouseEnter={() => setHoveredModelId(model.modelId)}
|
||||
onMouseLeave={() => setHoveredModelId(null)}
|
||||
>
|
||||
<TableRow
|
||||
gridTemplateColumns={gridColumns}
|
||||
onClick={
|
||||
isDisabled
|
||||
? undefined
|
||||
: () => onToggle(model.modelId, isChecked)
|
||||
}
|
||||
>
|
||||
<TableCell
|
||||
color={
|
||||
isDisabled
|
||||
? themeCssVariables.font.color.light
|
||||
: themeCssVariables.font.color.primary
|
||||
}
|
||||
>
|
||||
<StyledModelNameCell>
|
||||
<ModelIcon
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
color={
|
||||
isDisabled
|
||||
? theme.font.color.light
|
||||
: theme.font.color.secondary
|
||||
}
|
||||
/>
|
||||
<StyledModelLabel>{model.label}</StyledModelLabel>
|
||||
{showDisabledState && model.isDeprecated && (
|
||||
<StyledDeprecatedSuffix>
|
||||
· Deprecated
|
||||
</StyledDeprecatedSuffix>
|
||||
)}
|
||||
</StyledModelNameCell>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
color={themeCssVariables.font.color.tertiary}
|
||||
>
|
||||
{secondaryColumn === 'provider'
|
||||
? displayLabel
|
||||
: formatCost(model)}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<Checkbox
|
||||
checked={isChecked}
|
||||
disabled={isDisabled}
|
||||
onChange={() => onToggle(model.modelId, isChecked)}
|
||||
/>
|
||||
</TableCell>
|
||||
{hasRemove && (
|
||||
<TableCell align="right">
|
||||
<IconButton
|
||||
Icon={IconTrash}
|
||||
accent="danger"
|
||||
variant="tertiary"
|
||||
size="small"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onRemove(model);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
{hoveredModel && (
|
||||
<AppTooltip
|
||||
anchorSelect={`#${anchorPrefix}-${sanitizeIdForSelector(hoveredModel.modelId)}`}
|
||||
place="left"
|
||||
noArrow
|
||||
offset={8}
|
||||
delay={TooltipDelay.noDelay}
|
||||
className={hoverCardTooltipClass}
|
||||
width="320px"
|
||||
>
|
||||
<SettingsAdminAiModelHoverCard
|
||||
label={hoveredModel.label}
|
||||
modelFamily={hoveredModel.modelFamily}
|
||||
providerName={hoveredModel.providerName}
|
||||
providerLabel={getProviderDisplayLabel(hoveredModel)}
|
||||
contextWindowTokens={hoveredModel.contextWindowTokens}
|
||||
maxOutputTokens={hoveredModel.maxOutputTokens}
|
||||
inputCostPerMillionTokens={hoveredModel.inputCostPerMillionTokens}
|
||||
outputCostPerMillionTokens={hoveredModel.outputCostPerMillionTokens}
|
||||
dataResidency={hoveredModel.dataResidency}
|
||||
/>
|
||||
</AppTooltip>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+7
@@ -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)
|
||||
}
|
||||
`;
|
||||
+10
@@ -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)
|
||||
}
|
||||
`;
|
||||
+2
-2
@@ -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 = () => {
|
||||
<>
|
||||
<Section>
|
||||
<H2Title title={t`User Info`} description={t`About this user`} />
|
||||
<SettingsAdminTableCard
|
||||
<SettingsTableCard
|
||||
items={userInfoItems}
|
||||
rounded
|
||||
gridAutoColumns="1fr 4fr"
|
||||
|
||||
+2
-6
@@ -1,4 +1,4 @@
|
||||
import { SettingsAdminTableCard } from '@/settings/admin-panel/components/SettingsAdminTableCard';
|
||||
import { SettingsTableCard } from '@/settings/components/SettingsTableCard';
|
||||
import { SettingsAdminVersionDisplay } from '@/settings/admin-panel/components/SettingsAdminVersionDisplay';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { IconCircleDot, IconStatusChange } from 'twenty-ui/display';
|
||||
@@ -35,10 +35,6 @@ export const SettingsAdminVersionContainer = () => {
|
||||
];
|
||||
|
||||
return (
|
||||
<SettingsAdminTableCard
|
||||
rounded
|
||||
items={versionItems}
|
||||
gridAutoColumns="3fr 8fr"
|
||||
/>
|
||||
<SettingsTableCard rounded items={versionItems} gridAutoColumns="3fr 8fr" />
|
||||
);
|
||||
};
|
||||
|
||||
+5
-5
@@ -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`}
|
||||
/>
|
||||
<SettingsAdminTableCard
|
||||
<SettingsTableCard
|
||||
items={workspaceInfoItems}
|
||||
gridAutoColumns="1fr 4fr"
|
||||
/>
|
||||
|
||||
+5
-5
@@ -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 (
|
||||
<Section>
|
||||
<H2Title title={title} description={description} />
|
||||
<StyledSettingsAdminTableCardContainer>
|
||||
<SettingsAdminTableCard
|
||||
<StyledSettingsTableCardContainer>
|
||||
<SettingsTableCard
|
||||
items={items}
|
||||
rounded
|
||||
gridAutoColumns="1fr 1fr"
|
||||
labelAlign="left"
|
||||
valueAlign="right"
|
||||
/>
|
||||
</StyledSettingsAdminTableCardContainer>
|
||||
</StyledSettingsTableCardContainer>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
+5
-5
@@ -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 = ({
|
||||
)}
|
||||
</StyledGraphContainer>
|
||||
{isDefined(metricsDetails) && (
|
||||
<StyledSettingsAdminTableCardContainer>
|
||||
<SettingsAdminTableCard
|
||||
<StyledSettingsTableCardContainer>
|
||||
<SettingsTableCard
|
||||
rounded
|
||||
items={Object.entries(metricsDetails)
|
||||
.filter(([key]) => key !== '__typename')
|
||||
@@ -217,7 +217,7 @@ export const SettingsAdminWorkerMetricsGraph = ({
|
||||
labelAlign="left"
|
||||
valueAlign="right"
|
||||
/>
|
||||
</StyledSettingsAdminTableCardContainer>
|
||||
</StyledSettingsTableCardContainer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
+182
@@ -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: (
|
||||
<StyledChipContainer>
|
||||
<Chip
|
||||
size={ChipSize.Small}
|
||||
accent={ChipAccent.TextPrimary}
|
||||
variant={ChipVariant.Static}
|
||||
clickable={false}
|
||||
label={model.label}
|
||||
leftComponent={
|
||||
<ModelIcon
|
||||
size={theme.icon.size.sm}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
}
|
||||
rightComponent={null}
|
||||
/>
|
||||
</StyledChipContainer>
|
||||
),
|
||||
},
|
||||
{
|
||||
Icon: IconBuildingSkyscraper,
|
||||
label: t`Provider`,
|
||||
value: <StyledValueText>{providerLabel}</StyledValueText>,
|
||||
},
|
||||
...(isDefined(model.inputCostPerMillionTokens) ||
|
||||
isDefined(model.outputCostPerMillionTokens)
|
||||
? [
|
||||
{
|
||||
Icon: IconUsers,
|
||||
label: t`Cost per 1M tokens`,
|
||||
value: (
|
||||
<StyledValueText>
|
||||
{formatCost(
|
||||
model.inputCostPerMillionTokens,
|
||||
model.outputCostPerMillionTokens,
|
||||
)}
|
||||
</StyledValueText>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(isDefined(model.contextWindowTokens)
|
||||
? [
|
||||
{
|
||||
Icon: IconWindow,
|
||||
label: t`Context`,
|
||||
value: (
|
||||
<StyledValueText>
|
||||
{`${formatNumber(model.contextWindowTokens, {
|
||||
abbreviate: true,
|
||||
decimals: 1,
|
||||
})} tokens`}
|
||||
</StyledValueText>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(isDefined(model.maxOutputTokens)
|
||||
? [
|
||||
{
|
||||
Icon: IconBolt,
|
||||
label: t`Max output`,
|
||||
value: (
|
||||
<StyledValueText>
|
||||
{`${formatNumber(model.maxOutputTokens, {
|
||||
abbreviate: true,
|
||||
decimals: 1,
|
||||
})} tokens`}
|
||||
</StyledValueText>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(isDefined(model.dataResidency)
|
||||
? [
|
||||
{
|
||||
Icon: IconFlag,
|
||||
label: t`Data residency`,
|
||||
value: (
|
||||
<StyledValueText>
|
||||
{getDataResidencyDisplay(model.dataResidency)}
|
||||
</StyledValueText>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
return (
|
||||
<StyledHoverCardWrapper>
|
||||
<SettingsTableCard
|
||||
rounded
|
||||
items={items}
|
||||
gridAutoColumns={`${themeCssVariables.spacing[30]} 1fr`}
|
||||
/>
|
||||
</StyledHoverCardWrapper>
|
||||
);
|
||||
};
|
||||
@@ -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<TModel extends AiModelSummary> = {
|
||||
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 = <TModel extends AiModelSummary>({
|
||||
models,
|
||||
isChecked,
|
||||
isDisabled,
|
||||
onToggle,
|
||||
onToggleAll,
|
||||
onRemove,
|
||||
showProviderColumn = true,
|
||||
anchorPrefix,
|
||||
}: SettingsAiModelsTableProps<TModel>) => {
|
||||
const [hoveredModelId, setHoveredModelId] = useState<string | null>(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 (
|
||||
<>
|
||||
<Table>
|
||||
<TableRow gridTemplateColumns={gridColumns}>
|
||||
<TableHeader>
|
||||
<Trans>Name</Trans>
|
||||
</TableHeader>
|
||||
{showProviderColumn && (
|
||||
<TableHeader align="right">
|
||||
<Trans>Provider</Trans>
|
||||
</TableHeader>
|
||||
)}
|
||||
<TableHeader align="right">
|
||||
{isDefined(onToggleAll) && (
|
||||
<Checkbox
|
||||
checked={allChecked}
|
||||
indeterminate={!allChecked && !noneChecked}
|
||||
onChange={() => onToggleAll(!allChecked)}
|
||||
/>
|
||||
)}
|
||||
</TableHeader>
|
||||
{hasRemove && <TableHeader />}
|
||||
</TableRow>
|
||||
<TableBody>
|
||||
{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 (
|
||||
<TableRow
|
||||
key={model.modelId}
|
||||
id={anchorPrefix ? `${anchorPrefix}-${safeId}` : undefined}
|
||||
gridTemplateColumns={gridColumns}
|
||||
onMouseEnter={
|
||||
anchorPrefix
|
||||
? () => setHoveredModelId(model.modelId)
|
||||
: undefined
|
||||
}
|
||||
onMouseLeave={
|
||||
anchorPrefix ? () => setHoveredModelId(null) : undefined
|
||||
}
|
||||
onClick={
|
||||
disabled ? undefined : () => onToggle(model.modelId, checked)
|
||||
}
|
||||
>
|
||||
<TableCell
|
||||
color={
|
||||
disabled
|
||||
? themeCssVariables.font.color.light
|
||||
: themeCssVariables.font.color.primary
|
||||
}
|
||||
>
|
||||
<StyledModelNameCell>
|
||||
<ModelIcon
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
color={
|
||||
disabled
|
||||
? theme.font.color.light
|
||||
: theme.font.color.secondary
|
||||
}
|
||||
/>
|
||||
<StyledModelLabel>{model.label}</StyledModelLabel>
|
||||
{disabled && model.isDeprecated && (
|
||||
<StyledDeprecatedSuffix>
|
||||
· <Trans>Deprecated</Trans>
|
||||
</StyledDeprecatedSuffix>
|
||||
)}
|
||||
</StyledModelNameCell>
|
||||
</TableCell>
|
||||
{showProviderColumn && (
|
||||
<TableCell
|
||||
align="right"
|
||||
color={themeCssVariables.font.color.tertiary}
|
||||
>
|
||||
{getProviderDisplayLabel(model)}
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell
|
||||
align="right"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
onChange={() => onToggle(model.modelId, checked)}
|
||||
/>
|
||||
</TableCell>
|
||||
{hasRemove && (
|
||||
<TableCell align="right">
|
||||
<IconButton
|
||||
Icon={IconTrash}
|
||||
accent="danger"
|
||||
variant="tertiary"
|
||||
size="small"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onRemove(model);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
{anchorPrefix && hoveredModel && (
|
||||
<AppTooltip
|
||||
anchorSelect={`#${anchorPrefix}-${sanitizeIdForSelector(hoveredModel.modelId)}`}
|
||||
place="top-end"
|
||||
noArrow
|
||||
offset={8}
|
||||
delay={TooltipDelay.noDelay}
|
||||
className={hoverCardTooltipClass}
|
||||
width="320px"
|
||||
isOpen={true}
|
||||
>
|
||||
<SettingsAiModelHoverCard model={hoveredModel} />
|
||||
</AppTooltip>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
+1
-1
@@ -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';
|
||||
|
||||
+22
-12
@@ -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 (
|
||||
<Card
|
||||
@@ -40,21 +50,22 @@ export const SettingsAdminTableCard = ({
|
||||
backgroundColor={themeCssVariables.background.secondary}
|
||||
>
|
||||
<Table>
|
||||
<TableBody>
|
||||
<StyledTableBody>
|
||||
{items.map((item, index) => (
|
||||
<TableRow
|
||||
key={index + item.label}
|
||||
gridAutoColumns={gridAutoColumns}
|
||||
height={`${themeCssVariables.spacing[7]}px`}
|
||||
>
|
||||
<TableCell
|
||||
align={labelAlign}
|
||||
color={themeCssVariables.font.color.tertiary}
|
||||
height="auto"
|
||||
gap={themeCssVariables.spacing[2]}
|
||||
padding={`${themeCssVariables.spacing[2]} ${themeCssVariables.spacing[2]}`}
|
||||
overflow="hidden"
|
||||
>
|
||||
{item.Icon && <item.Icon size={theme.icon.size.md} />}
|
||||
<span>{item.label}</span>
|
||||
<OverflowingTextWithTooltip text={item.label} />
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align={valueAlign}
|
||||
@@ -62,13 +73,12 @@ export const SettingsAdminTableCard = ({
|
||||
height="auto"
|
||||
onClick={item.onClick}
|
||||
clickable={isDefined(item.onClick)}
|
||||
padding={`${themeCssVariables.spacing[2]} ${themeCssVariables.spacing[2]}`}
|
||||
>
|
||||
{item.value}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</StyledTableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
);
|
||||
@@ -55,10 +55,13 @@ const StyledTableRow = styled.div<{
|
||||
`;
|
||||
|
||||
type TableRowProps = {
|
||||
id?: string;
|
||||
isSelected?: boolean;
|
||||
isExpanded?: boolean;
|
||||
isClickable?: boolean;
|
||||
onClick?: () => void;
|
||||
onMouseEnter?: React.MouseEventHandler<HTMLDivElement>;
|
||||
onMouseLeave?: React.MouseEventHandler<HTMLDivElement>;
|
||||
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<TableRowProps>) => (
|
||||
<StyledTableRow
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
isExpanded={isExpanded}
|
||||
onClick={onClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
gridAutoColumns={gridAutoColumns}
|
||||
gridTemplateColumns={gridTemplateColumns}
|
||||
className={className}
|
||||
|
||||
+45
-9
@@ -26,14 +26,15 @@ import { RoundedLink, UndecoratedLink } from 'twenty-ui/navigation';
|
||||
|
||||
import { useClientConfig } from '@/client-config/hooks/useClientConfig';
|
||||
import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
|
||||
import { SettingsAdminAiModelsTable } from '@/settings/admin-panel/ai/components/SettingsAdminAiModelsTable';
|
||||
import { SettingsAiModelsTable } from '@/settings/ai/components/SettingsAiModelsTable';
|
||||
import { REMOVE_AI_PROVIDER } from '@/settings/admin-panel/ai/graphql/mutations/removeAiProvider';
|
||||
import { SET_ADMIN_AI_MODELS_ENABLED } from '@/settings/admin-panel/ai/graphql/mutations/setAdminAiModelsEnabled';
|
||||
import { REMOVE_MODEL_FROM_PROVIDER } from '@/settings/admin-panel/ai/graphql/mutations/removeModelFromProvider';
|
||||
import { GET_ADMIN_AI_MODELS } from '@/settings/admin-panel/ai/graphql/queries/getAdminAiModels';
|
||||
import { GET_AI_PROVIDERS } from '@/settings/admin-panel/ai/graphql/queries/getAiProviders';
|
||||
import { type GetAiProvidersResult } from '@/settings/admin-panel/ai/types/GetAiProvidersResult';
|
||||
import { getDataResidencyDisplay } from '@/settings/admin-panel/ai/utils/getDataResidencyDisplay';
|
||||
import { SettingsAdminTableCard } from '@/settings/admin-panel/components/SettingsAdminTableCard';
|
||||
import { getDataResidencyDisplay } from '@/settings/ai/utils/getDataResidencyDisplay';
|
||||
import { SettingsTableCard } from '@/settings/components/SettingsTableCard';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
@@ -63,13 +64,18 @@ export const SettingsAdminAiProviderDetail = () => {
|
||||
const { data: providersData, loading: isLoadingProviders } =
|
||||
useQuery<GetAiProvidersResult>(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 && (
|
||||
<SettingsAdminTableCard
|
||||
<SettingsTableCard
|
||||
rounded
|
||||
items={providerInfoItems}
|
||||
gridAutoColumns="120px 1fr"
|
||||
@@ -339,13 +345,43 @@ export const SettingsAdminAiProviderDetail = () => {
|
||||
)}
|
||||
|
||||
{filteredModels.length > 0 && (
|
||||
<SettingsAdminAiModelsTable
|
||||
<SettingsAiModelsTable
|
||||
models={filteredModels}
|
||||
isChecked={(model) => 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}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Models`}
|
||||
description={t`Configure default AI models and availability`}
|
||||
title={t`Default`}
|
||||
description={t`Configure your default AI model`}
|
||||
/>
|
||||
|
||||
<Card rounded>
|
||||
<SettingsOptionCardContentSelect
|
||||
Icon={IconBolt}
|
||||
Icon={IconBrain}
|
||||
title={t`Smart Model`}
|
||||
description={t`Used for chats, agents, and complex reasoning`}
|
||||
>
|
||||
@@ -257,8 +259,17 @@ export const SettingsAIModelsTab = () => {
|
||||
dropdownWidth={GenericDropdownContentWidth.ExtraLarge}
|
||||
/>
|
||||
</SettingsOptionCardContentSelect>
|
||||
</Card>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Available`}
|
||||
description={t`Models available in the chat model picker`}
|
||||
/>
|
||||
<Card rounded>
|
||||
<SettingsOptionCardContentToggle
|
||||
Icon={IconTwentyStar}
|
||||
Icon={IconStar}
|
||||
title={t`Use best models only`}
|
||||
description={t`Restrict available models to a curated list`}
|
||||
checked={useRecommendedModels}
|
||||
@@ -266,50 +277,52 @@ export const SettingsAIModelsTab = () => {
|
||||
divider={!useRecommendedModels}
|
||||
/>
|
||||
</Card>
|
||||
</Section>
|
||||
|
||||
{!useRecommendedModels && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Available`}
|
||||
description={t`Models available in the chat model picker`}
|
||||
/>
|
||||
|
||||
<StyledSearchContainer>
|
||||
{!useRecommendedModels && (
|
||||
<StyledCustomModelsContainer>
|
||||
<SearchInput
|
||||
placeholder={t`Search a model...`}
|
||||
value={searchQuery}
|
||||
onChange={setSearchQuery}
|
||||
/>
|
||||
</StyledSearchContainer>
|
||||
|
||||
<Card rounded>
|
||||
{filteredModels.map((model, index) => {
|
||||
const familyLabel = model.modelFamilyLabel ?? '';
|
||||
const residency = model.dataResidency
|
||||
? getDataResidencyDisplay(model.dataResidency)
|
||||
: undefined;
|
||||
const description = residency
|
||||
? `${familyLabel} · ${residency}`
|
||||
: familyLabel;
|
||||
<SettingsAiModelsTable
|
||||
models={filteredModels}
|
||||
isChecked={(model) => enabledModelIdSet.has(model.modelId)}
|
||||
onToggle={handleModelToggle}
|
||||
onToggleAll={async (shouldCheckAll) => {
|
||||
const previousIds = currentWorkspace?.enabledAiModelIds ?? [];
|
||||
const visibleModelIds = new Set(
|
||||
filteredModels.map((m) => m.modelId),
|
||||
);
|
||||
|
||||
return (
|
||||
<SettingsOptionCardContentToggle
|
||||
key={model.modelId}
|
||||
Icon={getModelIcon(model.modelFamily, model.providerName)}
|
||||
title={model.label}
|
||||
description={description}
|
||||
checked={model.isEnabled}
|
||||
onChange={() =>
|
||||
handleModelToggle(model.modelId, model.isEnabled)
|
||||
}
|
||||
divider={index < filteredModels.length - 1}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Card>
|
||||
</Section>
|
||||
)}
|
||||
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"
|
||||
/>
|
||||
</StyledCustomModelsContainer>
|
||||
)}
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+2
-2
@@ -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 (
|
||||
<StyledContainer>
|
||||
<SettingsAdminTableCard
|
||||
<SettingsTableCard
|
||||
rounded
|
||||
items={versionItems}
|
||||
gridAutoColumns="3fr 8fr"
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { SettingsAdminTableCard } from '@/settings/admin-panel/components/SettingsAdminTableCard';
|
||||
import { SettingsTableCard } from '@/settings/components/SettingsTableCard';
|
||||
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
import { styled } from '@linaria/react';
|
||||
@@ -164,7 +164,7 @@ export const SettingsApplicationRegistrationDistributionTab = ({
|
||||
title={t`Install Stats`}
|
||||
description={t`Usage across all workspaces on this server`}
|
||||
/>
|
||||
<SettingsAdminTableCard
|
||||
<SettingsTableCard
|
||||
rounded
|
||||
items={statsItems}
|
||||
gridAutoColumns="3fr 8fr"
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { SettingsAdminTableCard } from '@/settings/admin-panel/components/SettingsAdminTableCard';
|
||||
import { SettingsTableCard } from '@/settings/components/SettingsTableCard';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
@@ -387,7 +387,7 @@ export const SettingsApplicationRegistrationGeneralTab = ({
|
||||
title={t`General`}
|
||||
description={t`Name and description are managed via your app manifest (CLI)`}
|
||||
/>
|
||||
<SettingsAdminTableCard
|
||||
<SettingsTableCard
|
||||
rounded
|
||||
items={generalItems}
|
||||
gridAutoColumns="3fr 8fr"
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { SettingsAdminTableCard } from '@/settings/admin-panel/components/SettingsAdminTableCard';
|
||||
import { SettingsTableCard } from '@/settings/components/SettingsTableCard';
|
||||
import { ApiKeyInput } from '@/settings/developers/components/ApiKeyInput';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
||||
@@ -208,7 +208,7 @@ export const SettingsApplicationRegistrationOAuthTab = ({
|
||||
title={t`OAuth Credentials`}
|
||||
description={t`Credentials and scopes for OAuth authorization flows`}
|
||||
/>
|
||||
<SettingsAdminTableCard
|
||||
<SettingsTableCard
|
||||
rounded
|
||||
items={credentialItems}
|
||||
gridAutoColumns="3fr 8fr"
|
||||
|
||||
@@ -228,6 +228,17 @@ export class AdminPanelResolver {
|
||||
return true;
|
||||
}
|
||||
|
||||
@UseGuards(AdminPanelGuard)
|
||||
@Mutation(() => Boolean)
|
||||
async setAdminAiModelsEnabled(
|
||||
@Args('modelIds', { type: () => [String] }) modelIds: string[],
|
||||
@Args('enabled', { type: () => Boolean }) enabled: boolean,
|
||||
): Promise<boolean> {
|
||||
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<boolean> {
|
||||
await this.aiModelRegistryService.setModelsRecommended(
|
||||
modelIds,
|
||||
recommended,
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@UseGuards(AdminPanelGuard)
|
||||
@Mutation(() => Boolean)
|
||||
async setAdminDefaultAiModel(
|
||||
|
||||
+13
-4
@@ -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;
|
||||
}
|
||||
|
||||
+3
@@ -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),
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
+31
-16
@@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+32
-4
@@ -29,6 +29,24 @@ export class AiModelPreferencesService {
|
||||
await this.togglePreferenceList(modelId, 'recommendedModels', recommended);
|
||||
}
|
||||
|
||||
async setModelsAdminEnabled(
|
||||
modelIds: string[],
|
||||
enabled: boolean,
|
||||
): Promise<void> {
|
||||
await this.togglePreferenceListBulk(modelIds, 'disabledModels', !enabled);
|
||||
}
|
||||
|
||||
async setModelsRecommended(
|
||||
modelIds: string[],
|
||||
recommended: boolean,
|
||||
): Promise<void> {
|
||||
await this.togglePreferenceListBulk(
|
||||
modelIds,
|
||||
'recommendedModels',
|
||||
recommended,
|
||||
);
|
||||
}
|
||||
|
||||
async setDefaultModel(role: AiModelRole, modelId: string): Promise<void> {
|
||||
const prefs = { ...this.getPreferences() };
|
||||
const key =
|
||||
@@ -46,16 +64,26 @@ export class AiModelPreferencesService {
|
||||
modelId: string,
|
||||
key: 'disabledModels' | 'recommendedModels',
|
||||
add: boolean,
|
||||
): Promise<void> {
|
||||
await this.togglePreferenceListBulk([modelId], key, add);
|
||||
}
|
||||
|
||||
private async togglePreferenceListBulk(
|
||||
modelIds: string[],
|
||||
key: 'disabledModels' | 'recommendedModels',
|
||||
add: boolean,
|
||||
): Promise<void> {
|
||||
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);
|
||||
|
||||
+16
@@ -361,6 +361,22 @@ export class AiModelRegistryService {
|
||||
await this.preferencesService.setModelRecommended(modelId, recommended);
|
||||
}
|
||||
|
||||
async setModelsAdminEnabled(
|
||||
modelIds: string[],
|
||||
enabled: boolean,
|
||||
): Promise<void> {
|
||||
modelIds.forEach((id) => this.validateModelInRegistry(id));
|
||||
await this.preferencesService.setModelsAdminEnabled(modelIds, enabled);
|
||||
}
|
||||
|
||||
async setModelsRecommended(
|
||||
modelIds: string[],
|
||||
recommended: boolean,
|
||||
): Promise<void> {
|
||||
modelIds.forEach((id) => this.validateModelInRegistry(id));
|
||||
await this.preferencesService.setModelsRecommended(modelIds, recommended);
|
||||
}
|
||||
|
||||
async setDefaultModel(role: AiModelRole, modelId: string): Promise<void> {
|
||||
this.validateModelInRegistry(modelId);
|
||||
await this.preferencesService.setDefaultModel(role, modelId);
|
||||
|
||||
@@ -10,6 +10,7 @@ export {
|
||||
IconApi,
|
||||
IconApps,
|
||||
IconAppWindow,
|
||||
IconWindow,
|
||||
IconArchive,
|
||||
IconArchiveOff,
|
||||
IconArrowBackUp,
|
||||
@@ -374,6 +375,7 @@ export {
|
||||
IconSquareRoundedX,
|
||||
IconSquareX,
|
||||
IconStack2,
|
||||
IconStar,
|
||||
IconStatusChange,
|
||||
IconStepInto,
|
||||
IconStrikethrough,
|
||||
|
||||
@@ -88,6 +88,7 @@ export {
|
||||
IconApi,
|
||||
IconApps,
|
||||
IconAppWindow,
|
||||
IconWindow,
|
||||
IconArchive,
|
||||
IconArchiveOff,
|
||||
IconArrowBackUp,
|
||||
@@ -452,6 +453,7 @@ export {
|
||||
IconSquareRoundedX,
|
||||
IconSquareX,
|
||||
IconStack2,
|
||||
IconStar,
|
||||
IconStatusChange,
|
||||
IconStepInto,
|
||||
IconStrikethrough,
|
||||
|
||||
Reference in New Issue
Block a user