feat: design the ai-models tab on settings ai page (#18147)

Remove AI model selectors from the settings tab on AI settings page,
rename that tab to "More" and create a separate "Models" tab as per the
design.


https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=91311-278313&m=dev

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
Abdullah.
2026-02-22 15:41:26 +05:00
committed by GitHub
parent 088be4c47b
commit 0bccdedb4f
15 changed files with 260 additions and 229 deletions
@@ -13,15 +13,16 @@ import { getSettingsPath } from 'twenty-shared/utils';
import { t } from '@lingui/core/macro';
import {
H2Title,
IconCpu,
IconFileText,
IconSettings,
IconSettingsBolt,
IconSparkles,
IconTool,
} from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { Card, Section } from 'twenty-ui/layout';
import { SettingsAIMCP } from './components/SettingsAIMCP';
import { SettingsAIRouterSettings } from './components/SettingsAIRouterSettings';
import { SettingsAIModelsTab } from './components/SettingsAIModelsTab';
import { SettingsSkillsTable } from './components/SettingsSkillsTable';
import { SettingsToolsTable } from './components/SettingsToolsTable';
import { SETTINGS_AI_TABS } from './constants/SettingsAiTabs';
@@ -37,6 +38,11 @@ export const SettingsAI = () => {
);
const tabs = [
{
id: SETTINGS_AI_TABS.TABS_IDS.MODELS,
title: t`Models`,
Icon: IconCpu,
},
{
id: SETTINGS_AI_TABS.TABS_IDS.SKILLS,
title: t`Skills`,
@@ -48,15 +54,16 @@ export const SettingsAI = () => {
Icon: IconTool,
},
{
id: SETTINGS_AI_TABS.TABS_IDS.SETTINGS,
title: t`Settings`,
Icon: IconSettings,
id: SETTINGS_AI_TABS.TABS_IDS.MORE,
title: t`More`,
Icon: IconSettingsBolt,
},
];
const isModelsTab = activeTabId === SETTINGS_AI_TABS.TABS_IDS.MODELS;
const isSkillsTab = activeTabId === SETTINGS_AI_TABS.TABS_IDS.SKILLS;
const isToolsTab = activeTabId === SETTINGS_AI_TABS.TABS_IDS.TOOLS;
const isSettingsTab = activeTabId === SETTINGS_AI_TABS.TABS_IDS.SETTINGS;
const isMoreTab = activeTabId === SETTINGS_AI_TABS.TABS_IDS.MORE;
return (
<SubMenuTopBarContainer
@@ -74,11 +81,11 @@ export const SettingsAI = () => {
tabs={tabs}
componentInstanceId={SETTINGS_AI_TABS.COMPONENT_INSTANCE_ID}
/>
{isModelsTab && <SettingsAIModelsTab />}
{isSkillsTab && <SettingsSkillsTable />}
{isToolsTab && <SettingsToolsTable />}
{isSettingsTab && (
{isMoreTab && (
<>
<SettingsAIRouterSettings />
<Section>
<H2Title
title={t`System Prompt`}
@@ -0,0 +1,105 @@
import { useRecoilState } from 'recoil';
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 { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { Select } from '@/ui/input/components/Select';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { t } from '@lingui/core/macro';
import { H2Title, IconBolt } from 'twenty-ui/display';
import { Card, Section } from 'twenty-ui/layout';
import { useUpdateWorkspaceMutation } from '~/generated-metadata/graphql';
import { PROVIDER_CONFIG } from '~/pages/settings/ai/constants/SettingsAiModelProviders';
const VIRTUAL_MODEL_IDS: Set<string> = new Set([
DEFAULT_SMART_MODEL,
DEFAULT_FAST_MODEL,
]);
export const SettingsAIModelsTab = () => {
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
currentWorkspaceState,
);
const [updateWorkspace] = useUpdateWorkspaceMutation();
const aiModels = useRecoilValueV2(aiModelsState);
const realModels = aiModels.filter(
(model) => !VIRTUAL_MODEL_IDS.has(model.modelId),
);
const currentSmartModel = currentWorkspace?.smartModel;
const defaultModelOptions = realModels
.filter((model) => !model.deprecated || model.modelId === currentSmartModel)
.map((model) => ({
value: model.modelId,
label: model.label,
Icon: PROVIDER_CONFIG[model.provider.toUpperCase()]?.Icon,
}));
const handleDefaultModelChange = async (value: string) => {
if (!currentWorkspace?.id) {
return;
}
const previousSmartModel = currentWorkspace.smartModel;
try {
setCurrentWorkspace({
...currentWorkspace,
smartModel: value,
});
await updateWorkspace({
variables: {
input: {
smartModel: value,
},
},
});
enqueueSuccessSnackBar({
message: t`Default model updated successfully`,
});
} catch {
setCurrentWorkspace({
...currentWorkspace,
smartModel: previousSmartModel,
});
enqueueErrorSnackBar({
message: t`Failed to update default model`,
});
}
};
return (
<Section>
<H2Title
title={t`Default`}
description={t`Configure your default AI model`}
/>
<Card rounded>
<SettingsOptionCardContentSelect
Icon={IconBolt}
title={t`Default Model`}
description={t`Default model for new chats and agents`}
>
<Select
dropdownId="default-model-select"
value={currentSmartModel}
onChange={handleDefaultModelChange}
options={defaultModelOptions}
selectSizeVariant="small"
/>
</SettingsOptionCardContentSelect>
</Card>
</Section>
);
};
@@ -1,220 +0,0 @@
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { DEFAULT_FAST_MODEL } from '@/ai/constants/DefaultFastModel';
import { DEFAULT_SMART_MODEL } from '@/ai/constants/DefaultSmartModel';
import {
useAiModelLabel,
useAiModelOptions,
} from '@/ai/hooks/useAiModelOptions';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { aiModelsState } from '@/client-config/states/aiModelsState';
import {
StyledSettingsCardContent,
StyledSettingsCardDescription,
StyledSettingsCardIcon,
StyledSettingsCardTitle,
} from '@/settings/components/SettingsOptions/SettingsCardContentBase';
import { SettingsOptionIconCustomizer } from '@/settings/components/SettingsOptions/SettingsOptionIconCustomizer';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { Select } from '@/ui/input/components/Select';
import { t } from '@lingui/core/macro';
import { H2Title, IconBolt, IconBrain } from 'twenty-ui/display';
import { Card, Section } from 'twenty-ui/layout';
import { useUpdateWorkspaceMutation } from '~/generated-metadata/graphql';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
const StyledSelectContainer = styled.div`
justify-content: flex-end;
margin-left: auto;
max-width: 120px;
`;
const StyledErrorMessage = styled.div`
color: ${({ theme }) => theme.color.red};
font-size: ${({ theme }) => theme.font.size.sm};
margin-top: ${({ theme }) => theme.spacing(1)};
`;
export const SettingsAIRouterSettings = () => {
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
currentWorkspaceState,
);
const [updateWorkspace] = useUpdateWorkspaceMutation();
const aiModels = useRecoilValueV2(aiModelsState);
const activeModelOptions = useAiModelOptions();
const fastModelLabel = useAiModelLabel(currentWorkspace?.fastModel);
const smartModelLabel = useAiModelLabel(currentWorkspace?.smartModel);
const currentFastModel = aiModels.find(
(m) => m.modelId === currentWorkspace?.fastModel,
);
const currentSmartModel = aiModels.find(
(m) => m.modelId === currentWorkspace?.smartModel,
);
const fastModelOptions =
currentFastModel?.deprecated === true
? [
{
value: currentWorkspace?.fastModel ?? '',
label: `${fastModelLabel} (deprecated)`,
},
...activeModelOptions,
]
: activeModelOptions;
const smartModelOptions =
currentSmartModel?.deprecated === true
? [
{
value: currentWorkspace?.smartModel ?? '',
label: `${smartModelLabel} (deprecated)`,
},
...activeModelOptions,
]
: activeModelOptions;
const noModelsAvailable = activeModelOptions.length === 0;
const handleFastModelChange = async (value: string) => {
if (!currentWorkspace?.id) {
return;
}
const newValue = value;
const previousValue = currentWorkspace?.fastModel || DEFAULT_FAST_MODEL;
try {
setCurrentWorkspace({
...currentWorkspace,
fastModel: newValue,
});
await updateWorkspace({
variables: {
input: {
fastModel: newValue,
},
},
});
enqueueSuccessSnackBar({
message: t`Fast model updated successfully`,
});
} catch {
setCurrentWorkspace({
...currentWorkspace,
fastModel: previousValue,
});
enqueueErrorSnackBar({
message: t`Failed to update fast model`,
});
}
};
const handleSmartModelChange = async (value: string) => {
if (!currentWorkspace?.id) {
return;
}
const newValue = value;
const previousValue = currentWorkspace?.smartModel || DEFAULT_SMART_MODEL;
try {
setCurrentWorkspace({
...currentWorkspace,
smartModel: newValue,
});
await updateWorkspace({
variables: {
input: {
smartModel: newValue,
},
},
});
enqueueSuccessSnackBar({
message: t`Smart model updated successfully`,
});
} catch {
setCurrentWorkspace({
...currentWorkspace,
smartModel: previousValue,
});
enqueueErrorSnackBar({
message: t`Failed to update smart model`,
});
}
};
return (
<Section>
<H2Title
title={t`AI Models`}
description={t`Configure AI models for routing and planning`}
/>
{noModelsAvailable ? (
<Card rounded>
<StyledSettingsCardContent>
<StyledErrorMessage>
{t`No models available. Please configure AI models in your workspace settings.`}
</StyledErrorMessage>
</StyledSettingsCardContent>
</Card>
) : (
<Card rounded>
<StyledSettingsCardContent>
<StyledSettingsCardIcon>
<SettingsOptionIconCustomizer Icon={IconBolt} />
</StyledSettingsCardIcon>
<div>
<StyledSettingsCardTitle>{t`Fast Model`}</StyledSettingsCardTitle>
<StyledSettingsCardDescription>
{t`Quick model for routing decisions`}
</StyledSettingsCardDescription>
</div>
<StyledSelectContainer>
<Select
dropdownId="fast-model-select"
value={currentWorkspace?.fastModel || DEFAULT_FAST_MODEL}
onChange={handleFastModelChange}
options={fastModelOptions}
selectSizeVariant="small"
/>
</StyledSelectContainer>
</StyledSettingsCardContent>
<StyledSettingsCardContent>
<StyledSettingsCardIcon>
<SettingsOptionIconCustomizer Icon={IconBrain} />
</StyledSettingsCardIcon>
<div>
<StyledSettingsCardTitle>
{t`Smart Model`}
</StyledSettingsCardTitle>
<StyledSettingsCardDescription>
{t`Advanced model for complex planning`}
</StyledSettingsCardDescription>
</div>
<StyledSelectContainer>
<Select
dropdownId="smart-model-select"
value={currentWorkspace?.smartModel || DEFAULT_SMART_MODEL}
onChange={handleSmartModelChange}
options={smartModelOptions}
selectSizeVariant="small"
/>
</StyledSelectContainer>
</StyledSettingsCardContent>
</Card>
)}
</Section>
);
};
@@ -0,0 +1,23 @@
import {
IconBrandAnthropic,
IconBrandGoogle,
IconBrandGroq,
IconBrandMistral,
IconBrandOpenai,
IconBrandXai,
IconRobot,
type IconComponent,
} from 'twenty-ui/display';
export const PROVIDER_CONFIG: Record<
string,
{ label: string; Icon: IconComponent }
> = {
OPENAI: { label: 'OpenAI', Icon: IconBrandOpenai },
ANTHROPIC: { label: 'Anthropic', Icon: IconBrandAnthropic },
XAI: { label: 'xAI', Icon: IconBrandXai },
GOOGLE: { label: 'Google', Icon: IconBrandGoogle },
GROQ: { label: 'Groq', Icon: IconBrandGroq },
MISTRAL: { label: 'Mistral', Icon: IconBrandMistral },
NONE: { label: '', Icon: IconRobot },
};
@@ -1,8 +1,9 @@
export const SETTINGS_AI_TABS = {
COMPONENT_INSTANCE_ID: 'settings-ai-tab-list',
TABS_IDS: {
MODELS: 'models',
SKILLS: 'skills',
TOOLS: 'tools',
SETTINGS: 'settings',
MORE: 'more',
},
} as const;