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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user