feat: add two-layer AI model availability filtering (#18170)
## Summary - **Admin-level filtering**: New AI tab in admin panel with server-wide model availability controls (whitelist/blacklist via `AI_AUTO_ENABLE_NEW_MODELS`, `AI_DISABLED_MODEL_IDS`, `AI_ENABLED_MODEL_IDS` config variables). Dedicated `setAdminAiModelEnabled` mutation replaces frontend config-variable manipulation. Filter dropdown to show/hide unconfigured and deprecated models. - **Workspace-level filtering**: Per-workspace controls with "Use best models only" mode (curated list backed by `isRecommended` flag), or custom whitelist/blacklist. Separate Smart/Fast model selectors with "Best (...)" virtual options. - **Security enforcement**: Both layers enforced at every backend execution point — workspace update, agent create/update, chat execution. Model ID validated against known models before config mutation. All admin endpoints protected by `AdminPanelGuard`. ## Changes ### Backend (`twenty-server`) - New config variables for admin-level model filtering - `AiModelRegistryService`: `getAllModelsWithStatus()`, `setModelAdminEnabled()` with model ID validation, `isModelAdminAllowed()` - `AdminPanelResolver`: `getAdminAiModels` query, `setAdminAiModelEnabled` mutation - `WorkspaceEntity`: new fields (`autoEnableNewAiModels`, `disabledAiModelIds`, `enabledAiModelIds`, `useRecommendedModels`) - `WorkspaceService`: model validation on `smartModel`/`fastModel` updates - `AgentResolver`: model availability checks on create/update - `isModelAllowedByWorkspace` centralized utility - `isRecommended` flag on model definitions - Two TypeORM migrations ### Frontend (`twenty-front`) - New `SettingsAdminAI` component with search, filter dropdown (unconfigured/deprecated), and model toggle cards - AI tab added to admin panel navigation - `useWorkspaceAiModelAvailability` hook for workspace-level filtering - `SettingsAIModelsTab` redesigned: merged sections, "Use best models only" toggle, conditional available models list - `getModelIcon`/`getModelProviderLabel` shared utilities with GraphQL enum casing normalization - Updated generated GraphQL types and mock data ## Test plan - [ ] Toggle models on/off in admin panel AI tab and verify they appear/disappear in workspace settings - [ ] Enable "Use best models only" in workspace settings and verify only recommended models are selectable - [ ] Disable recommended mode and verify whitelist/blacklist toggles work correctly - [ ] Verify deprecated models hidden by default, shown greyed out when filter enabled - [ ] Verify unconfigured models hidden by default, shown disabled when filter enabled - [ ] Try setting a disabled model as Smart/Fast model — should be rejected - [ ] Try creating an agent with a disabled model — should be rejected - [ ] Verify admin panel AI tab requires admin access Made with [Cursor](https://cursor.com) --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,39 +1,35 @@
|
||||
import { aiModelsState } from '@/client-config/states/aiModelsState';
|
||||
import { type SelectOption } from 'twenty-ui/input';
|
||||
|
||||
import { DEFAULT_FAST_MODEL } from '@/ai/constants/DefaultFastModel';
|
||||
import { DEFAULT_SMART_MODEL } from '@/ai/constants/DefaultSmartModel';
|
||||
import { useWorkspaceAiModelAvailability } from '@/ai/hooks/useWorkspaceAiModelAvailability';
|
||||
import { aiModelsState } from '@/client-config/states/aiModelsState';
|
||||
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
|
||||
import { MODEL_FAMILY_CONFIG } from '~/pages/settings/ai/constants/SettingsAiModelProviders';
|
||||
import { getModelProviderLabel } from '~/pages/settings/ai/utils/getModelProviderLabel';
|
||||
|
||||
export const useAiModelOptions = (
|
||||
includeDeprecated = false,
|
||||
): SelectOption<string>[] => {
|
||||
const aiModels = useRecoilValueV2(aiModelsState);
|
||||
const { isModelEnabled } = useWorkspaceAiModelAvailability();
|
||||
|
||||
return aiModels
|
||||
.filter((model) => includeDeprecated || !model.deprecated)
|
||||
.filter(
|
||||
(model) =>
|
||||
(includeDeprecated || !model.deprecated) &&
|
||||
isModelEnabled(model.modelId, model),
|
||||
)
|
||||
.map((model) => ({
|
||||
value: model.modelId,
|
||||
label:
|
||||
model.modelId === DEFAULT_FAST_MODEL ||
|
||||
model.modelId === DEFAULT_SMART_MODEL
|
||||
? model.label
|
||||
: `${model.label} (${getModelFamilyLabel(model.modelFamily) ?? model.inferenceProvider})`,
|
||||
: `${model.label} (${getModelProviderLabel(model.modelFamily) || model.inferenceProvider})`,
|
||||
}))
|
||||
.sort((a, b) => a.label.localeCompare(b.label));
|
||||
};
|
||||
|
||||
const getModelFamilyLabel = (
|
||||
modelFamily: string | null | undefined,
|
||||
): string | undefined => {
|
||||
if (!modelFamily) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return MODEL_FAMILY_CONFIG[modelFamily]?.label || modelFamily;
|
||||
};
|
||||
|
||||
export const useAiModelLabel = (
|
||||
modelId: string | undefined,
|
||||
includeProvider = true,
|
||||
@@ -58,5 +54,5 @@ export const useAiModelLabel = (
|
||||
return model.label;
|
||||
}
|
||||
|
||||
return `${model.label} (${getModelFamilyLabel(model.modelFamily) ?? model.inferenceProvider})`;
|
||||
return `${model.label} (${getModelProviderLabel(model.modelFamily) || model.inferenceProvider})`;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { DEFAULT_FAST_MODEL } from '@/ai/constants/DefaultFastModel';
|
||||
import { DEFAULT_SMART_MODEL } from '@/ai/constants/DefaultSmartModel';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { aiModelsState } from '@/client-config/states/aiModelsState';
|
||||
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
|
||||
import { type ClientAiModelConfig } from '~/generated-metadata/graphql';
|
||||
|
||||
const VIRTUAL_MODEL_IDS: Set<string> = new Set([
|
||||
DEFAULT_SMART_MODEL,
|
||||
DEFAULT_FAST_MODEL,
|
||||
]);
|
||||
|
||||
const isVirtualModel = (modelId: string) => VIRTUAL_MODEL_IDS.has(modelId);
|
||||
|
||||
export const useWorkspaceAiModelAvailability = () => {
|
||||
const aiModels = useRecoilValueV2(aiModelsState);
|
||||
const currentWorkspace = useRecoilValueV2(currentWorkspaceState);
|
||||
|
||||
const useRecommendedModels = currentWorkspace?.useRecommendedModels ?? true;
|
||||
const autoEnableNewAiModels = currentWorkspace?.autoEnableNewAiModels ?? true;
|
||||
const disabledAiModelIds = currentWorkspace?.disabledAiModelIds ?? [];
|
||||
const enabledAiModelIds = currentWorkspace?.enabledAiModelIds ?? [];
|
||||
|
||||
const isModelEnabled = (
|
||||
modelId: string,
|
||||
model?: ClientAiModelConfig,
|
||||
): boolean => {
|
||||
if (isVirtualModel(modelId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (useRecommendedModels) {
|
||||
return model?.isRecommended === true;
|
||||
}
|
||||
|
||||
return autoEnableNewAiModels
|
||||
? !disabledAiModelIds.includes(modelId)
|
||||
: enabledAiModelIds.includes(modelId);
|
||||
};
|
||||
|
||||
const realModels = aiModels.filter(
|
||||
(model) => !isVirtualModel(model.modelId) && !model.deprecated,
|
||||
);
|
||||
|
||||
const enabledModels = realModels.filter((model) =>
|
||||
isModelEnabled(model.modelId, model),
|
||||
);
|
||||
|
||||
const allModelsWithAvailability = realModels.map((model) => ({
|
||||
...model,
|
||||
isEnabled: isModelEnabled(model.modelId, model),
|
||||
}));
|
||||
|
||||
return {
|
||||
isModelEnabled,
|
||||
enabledModels,
|
||||
realModels,
|
||||
allModelsWithAvailability,
|
||||
useRecommendedModels,
|
||||
autoEnableNewAiModels,
|
||||
disabledAiModelIds,
|
||||
enabledAiModelIds,
|
||||
};
|
||||
};
|
||||
@@ -70,6 +70,10 @@ const mockWorkspace = {
|
||||
fastModel: DEFAULT_FAST_MODEL,
|
||||
smartModel: DEFAULT_SMART_MODEL,
|
||||
routerModel: 'auto',
|
||||
autoEnableNewAiModels: true,
|
||||
disabledAiModelIds: [],
|
||||
enabledAiModelIds: [],
|
||||
useRecommendedModels: true,
|
||||
workspaceCustomApplication: CUSTOM_WORKSPACE_APPLICATION_MOCK,
|
||||
workspaceCustomApplicationId: CUSTOM_WORKSPACE_APPLICATION_MOCK.id,
|
||||
};
|
||||
|
||||
@@ -38,6 +38,10 @@ export type CurrentWorkspace = Pick<
|
||||
| 'smartModel'
|
||||
| 'aiAdditionalInstructions'
|
||||
| 'editableProfileFields'
|
||||
| 'autoEnableNewAiModels'
|
||||
| 'disabledAiModelIds'
|
||||
| 'enabledAiModelIds'
|
||||
| 'useRecommendedModels'
|
||||
> & {
|
||||
defaultRole?: Omit<Role, 'workspaceMembers' | 'agents' | 'apiKeys'> | null;
|
||||
workspaceCustomApplication: Pick<Application, 'id'> | null;
|
||||
|
||||
-7
@@ -18,13 +18,6 @@ export const IsAppMetadataReadyEffect = () => {
|
||||
const viewsEntry = useFamilyRecoilValueV2(metadataStoreState, 'views');
|
||||
const setIsAppMetadataReady = useSetRecoilStateV2(isAppMetadataReadyState);
|
||||
|
||||
console.log('objectsEntry', objectsEntry);
|
||||
console.log('viewsEntry', viewsEntry);
|
||||
console.log('isLoggedIn', isLoggedIn);
|
||||
console.log('currentUser', currentUser);
|
||||
console.log('currentWorkspace', currentWorkspace);
|
||||
console.log('setIsAppMetadataReady', setIsAppMetadataReady);
|
||||
|
||||
useEffect(() => {
|
||||
const hasActiveWorkspace = isWorkspaceActiveOrSuspended(currentWorkspace);
|
||||
|
||||
|
||||
+4
@@ -71,6 +71,10 @@ describe('useColumnDefinitionsFromObjectMetadata', () => {
|
||||
eventLogRetentionDays: 365 * 3,
|
||||
fastModel: DEFAULT_FAST_MODEL,
|
||||
smartModel: DEFAULT_SMART_MODEL,
|
||||
autoEnableNewAiModels: true,
|
||||
disabledAiModelIds: [],
|
||||
enabledAiModelIds: [],
|
||||
useRecommendedModels: true,
|
||||
});
|
||||
|
||||
const companyObjectMetadata = generatedMockObjectMetadataItems.find(
|
||||
|
||||
+7
-8
@@ -6,8 +6,8 @@ import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailab
|
||||
|
||||
import { availableFieldMetadataItemsForFilterFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForFilterFamilySelector';
|
||||
import { availableFieldMetadataItemsForSortFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForSortFamilySelector';
|
||||
import { useFamilySelectorValueV2 } from '@/ui/utilities/state/jotai/hooks/useFamilySelectorValueV2';
|
||||
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
|
||||
import { useFamilySelectorValueV2 } from '@/ui/utilities/state/jotai/hooks/useFamilySelectorValueV2';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useColumnDefinitionsFromObjectMetadata = (
|
||||
@@ -28,13 +28,12 @@ export const useColumnDefinitionsFromObjectMetadata = (
|
||||
);
|
||||
|
||||
const columnDefinitions: ColumnDefinition<FieldMetadata>[] = useMemo(() => {
|
||||
const activeFieldMetadataItems =
|
||||
objectMetadataItem.readableFields.filter(
|
||||
(field) =>
|
||||
field.isActive &&
|
||||
(!isHiddenSystemField(field) ||
|
||||
field.id === objectMetadataItem.labelIdentifierFieldMetadataId),
|
||||
);
|
||||
const activeFieldMetadataItems = objectMetadataItem.readableFields.filter(
|
||||
(field) =>
|
||||
field.isActive &&
|
||||
(!isHiddenSystemField(field) ||
|
||||
field.id === objectMetadataItem.labelIdentifierFieldMetadataId),
|
||||
);
|
||||
|
||||
return activeFieldMetadataItems
|
||||
.map((field, index) =>
|
||||
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
import { useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useClientConfig } from '@/client-config/hooks/useClientConfig';
|
||||
import { GET_ADMIN_AI_MODELS } from '@/settings/admin-panel/ai/graphql/queries/getAdminAiModels';
|
||||
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import {
|
||||
H2Title,
|
||||
IconArchive,
|
||||
IconFilter,
|
||||
IconPlug,
|
||||
IconRobot,
|
||||
IconSearch,
|
||||
} from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { Card, Section } from 'twenty-ui/layout';
|
||||
import { MenuItemToggle } from 'twenty-ui/navigation';
|
||||
import {
|
||||
useCreateDatabaseConfigVariableMutation,
|
||||
useGetAdminAiModelsQuery,
|
||||
useSetAdminAiModelEnabledMutation,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { getModelIcon } from '~/pages/settings/ai/utils/getModelIcon';
|
||||
import { getModelProviderLabel } from '~/pages/settings/ai/utils/getModelProviderLabel';
|
||||
|
||||
const StyledSearchAndFilterContainer = styled.div`
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledSearchInput = styled(SettingsTextInput)`
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
export const SettingsAdminAI = () => {
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [showUnconfigured, setShowUnconfigured] = useState(false);
|
||||
const [showDeprecated, setShowDeprecated] = useState(false);
|
||||
const { refetch: refetchClientConfig } = useClientConfig();
|
||||
|
||||
const { data } = useGetAdminAiModelsQuery();
|
||||
const [createConfigVariable] = useCreateDatabaseConfigVariableMutation();
|
||||
const [setModelEnabled] = useSetAdminAiModelEnabledMutation();
|
||||
|
||||
const autoEnableNewModels =
|
||||
data?.getAdminAiModels?.autoEnableNewModels ?? true;
|
||||
|
||||
const models = data?.getAdminAiModels?.models ?? [];
|
||||
|
||||
const handleAutoEnableToggle = async (checked: boolean) => {
|
||||
try {
|
||||
await createConfigVariable({
|
||||
variables: {
|
||||
key: 'AI_AUTO_ENABLE_NEW_MODELS',
|
||||
value: checked,
|
||||
},
|
||||
refetchQueries: [{ query: GET_ADMIN_AI_MODELS }],
|
||||
});
|
||||
|
||||
await refetchClientConfig();
|
||||
} catch {
|
||||
enqueueErrorSnackBar({
|
||||
message: t`Failed to update auto-enable setting`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleModelToggle = async (
|
||||
modelId: string,
|
||||
isCurrentlyEnabled: boolean,
|
||||
) => {
|
||||
try {
|
||||
await setModelEnabled({
|
||||
variables: {
|
||||
modelId,
|
||||
enabled: !isCurrentlyEnabled,
|
||||
},
|
||||
refetchQueries: [{ query: GET_ADMIN_AI_MODELS }],
|
||||
});
|
||||
|
||||
await refetchClientConfig();
|
||||
} catch {
|
||||
enqueueErrorSnackBar({
|
||||
message: t`Failed to update model availability`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let filteredModels = models;
|
||||
|
||||
if (!showUnconfigured) {
|
||||
filteredModels = filteredModels.filter((model) => model.isAvailable);
|
||||
}
|
||||
|
||||
if (!showDeprecated) {
|
||||
filteredModels = filteredModels.filter((model) => !model.deprecated);
|
||||
}
|
||||
|
||||
if (searchQuery.trim().length > 0) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
|
||||
filteredModels = filteredModels.filter(
|
||||
(model) =>
|
||||
model.label.toLowerCase().includes(query) ||
|
||||
(model.modelFamily?.toLowerCase().includes(query) ?? false) ||
|
||||
model.inferenceProvider.toLowerCase().includes(query),
|
||||
);
|
||||
}
|
||||
|
||||
const getModelDescription = (
|
||||
modelFamily: string | null | undefined,
|
||||
isAvailable: boolean,
|
||||
isDeprecated: boolean | null | undefined,
|
||||
) => {
|
||||
const providerLabel = getModelProviderLabel(modelFamily);
|
||||
|
||||
if (isDeprecated === true) {
|
||||
return providerLabel ? t`${providerLabel} — Deprecated` : t`Deprecated`;
|
||||
}
|
||||
|
||||
if (!isAvailable) {
|
||||
return providerLabel
|
||||
? t`${providerLabel} — API key not configured`
|
||||
: t`API key not configured`;
|
||||
}
|
||||
|
||||
return providerLabel;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Admin Model Controls`}
|
||||
description={t`Server-wide AI model availability settings`}
|
||||
/>
|
||||
|
||||
<Card rounded>
|
||||
<SettingsOptionCardContentToggle
|
||||
Icon={IconRobot}
|
||||
title={t`Automatically enable new models`}
|
||||
description={t`When enabled, newly added models are available to all workspaces by default`}
|
||||
checked={autoEnableNewModels}
|
||||
onChange={handleAutoEnableToggle}
|
||||
/>
|
||||
</Card>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`All Models`}
|
||||
description={t`Toggle model availability across all workspaces`}
|
||||
/>
|
||||
|
||||
<StyledSearchAndFilterContainer>
|
||||
<StyledSearchInput
|
||||
instanceId="admin-model-search"
|
||||
LeftIcon={IconSearch}
|
||||
placeholder={t`Search a model...`}
|
||||
value={searchQuery}
|
||||
onChange={setSearchQuery}
|
||||
/>
|
||||
<Dropdown
|
||||
dropdownId="admin-ai-models-filter-dropdown"
|
||||
dropdownPlacement="bottom-end"
|
||||
dropdownOffset={{ x: 0, y: 8 }}
|
||||
clickableComponent={
|
||||
<Button
|
||||
Icon={IconFilter}
|
||||
size="medium"
|
||||
variant="secondary"
|
||||
accent="default"
|
||||
ariaLabel={t`Filter`}
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<DropdownContent>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItemToggle
|
||||
LeftIcon={IconPlug}
|
||||
onToggleChange={() =>
|
||||
setShowUnconfigured(!showUnconfigured)
|
||||
}
|
||||
toggled={showUnconfigured}
|
||||
text={t`Unconfigured models`}
|
||||
toggleSize="small"
|
||||
/>
|
||||
<MenuItemToggle
|
||||
LeftIcon={IconArchive}
|
||||
onToggleChange={() => setShowDeprecated(!showDeprecated)}
|
||||
toggled={showDeprecated}
|
||||
text={t`Deprecated models`}
|
||||
toggleSize="small"
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
}
|
||||
/>
|
||||
</StyledSearchAndFilterContainer>
|
||||
|
||||
<Card rounded>
|
||||
{filteredModels.map((model, index) => (
|
||||
<SettingsOptionCardContentToggle
|
||||
key={model.modelId}
|
||||
Icon={getModelIcon(model.modelFamily)}
|
||||
title={model.label}
|
||||
description={getModelDescription(
|
||||
model.modelFamily,
|
||||
model.isAvailable,
|
||||
model.deprecated,
|
||||
)}
|
||||
checked={model.isAdminEnabled}
|
||||
onChange={() =>
|
||||
handleModelToggle(model.modelId, model.isAdminEnabled)
|
||||
}
|
||||
disabled={!model.isAvailable || model.deprecated === true}
|
||||
divider={index < filteredModels.length - 1}
|
||||
/>
|
||||
))}
|
||||
</Card>
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const SET_ADMIN_AI_MODEL_ENABLED = gql`
|
||||
mutation SetAdminAiModelEnabled($modelId: String!, $enabled: Boolean!) {
|
||||
setAdminAiModelEnabled(modelId: $modelId, enabled: $enabled)
|
||||
}
|
||||
`;
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_ADMIN_AI_MODELS = gql`
|
||||
query GetAdminAiModels {
|
||||
getAdminAiModels {
|
||||
autoEnableNewModels
|
||||
models {
|
||||
modelId
|
||||
label
|
||||
modelFamily
|
||||
inferenceProvider
|
||||
isAvailable
|
||||
isAdminEnabled
|
||||
deprecated
|
||||
isRecommended
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
+14
-3
@@ -4,7 +4,12 @@ import { SETTINGS_ADMIN_TABS } from '@/settings/admin-panel/constants/SettingsAd
|
||||
import { SETTINGS_ADMIN_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminTabsId';
|
||||
import { TabList } from '@/ui/layout/tab-list/components/TabList';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { IconHeart, IconSettings2, IconVariable } from 'twenty-ui/display';
|
||||
import {
|
||||
IconHeart,
|
||||
IconSettings2,
|
||||
IconSparkles,
|
||||
IconVariable,
|
||||
} from 'twenty-ui/display';
|
||||
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
|
||||
|
||||
export const SettingsAdminContent = () => {
|
||||
@@ -19,15 +24,21 @@ export const SettingsAdminContent = () => {
|
||||
Icon: IconSettings2,
|
||||
disabled: !canAccessFullAdminPanel && !canImpersonate,
|
||||
},
|
||||
{
|
||||
id: SETTINGS_ADMIN_TABS.AI,
|
||||
title: t`AI`,
|
||||
Icon: IconSparkles,
|
||||
disabled: !canAccessFullAdminPanel,
|
||||
},
|
||||
{
|
||||
id: SETTINGS_ADMIN_TABS.CONFIG_VARIABLES,
|
||||
title: t`Config Variables`,
|
||||
title: t`Config`,
|
||||
Icon: IconVariable,
|
||||
disabled: !canAccessFullAdminPanel,
|
||||
},
|
||||
{
|
||||
id: SETTINGS_ADMIN_TABS.HEALTH_STATUS,
|
||||
title: t`Health Status`,
|
||||
title: t`Health`,
|
||||
Icon: IconHeart,
|
||||
disabled: !canAccessFullAdminPanel,
|
||||
},
|
||||
|
||||
+3
@@ -1,3 +1,4 @@
|
||||
import { SettingsAdminAI } from '@/settings/admin-panel/ai/components/SettingsAdminAI';
|
||||
import { SettingsAdminGeneral } from '@/settings/admin-panel/components/SettingsAdminGeneral';
|
||||
import { SettingsAdminConfigVariables } from '@/settings/admin-panel/config-variables/components/SettingsAdminConfigVariables';
|
||||
import { SETTINGS_ADMIN_TABS } from '@/settings/admin-panel/constants/SettingsAdminTabs';
|
||||
@@ -15,6 +16,8 @@ export const SettingsAdminTabContent = () => {
|
||||
switch (activeTabId) {
|
||||
case SETTINGS_ADMIN_TABS.GENERAL:
|
||||
return <SettingsAdminGeneral />;
|
||||
case SETTINGS_ADMIN_TABS.AI:
|
||||
return <SettingsAdminAI />;
|
||||
case SETTINGS_ADMIN_TABS.CONFIG_VARIABLES:
|
||||
return <SettingsAdminConfigVariables />;
|
||||
case SETTINGS_ADMIN_TABS.HEALTH_STATUS:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export const SETTINGS_ADMIN_TABS = {
|
||||
GENERAL: 'general',
|
||||
AI: 'ai',
|
||||
CONFIG_VARIABLES: 'config-variables',
|
||||
HEALTH_STATUS: 'health-status',
|
||||
};
|
||||
|
||||
@@ -90,6 +90,10 @@ export const USER_QUERY_FRAGMENT = gql`
|
||||
fastModel
|
||||
smartModel
|
||||
aiAdditionalInstructions
|
||||
autoEnableNewAiModels
|
||||
disabledAiModelIds
|
||||
enabledAiModelIds
|
||||
useRecommendedModels
|
||||
isTwoFactorAuthenticationEnforced
|
||||
trashRetentionDays
|
||||
eventLogRetentionDays
|
||||
|
||||
Reference in New Issue
Block a user