diff --git a/packages/twenty-front/src/modules/ai/components/RoutingStatusDisplay.tsx b/packages/twenty-front/src/modules/ai/components/RoutingStatusDisplay.tsx index 468e55276a..4991c73eed 100644 --- a/packages/twenty-front/src/modules/ai/components/RoutingStatusDisplay.tsx +++ b/packages/twenty-front/src/modules/ai/components/RoutingStatusDisplay.tsx @@ -1,14 +1,8 @@ import { ShimmeringText } from '@/ai/components/ShimmeringText'; -import { keyframes } from '@emotion/react'; import styled from '@emotion/styled'; import { type DataMessagePart } from 'twenty-shared/ai'; import { IconCpu, IconSparkles } from 'twenty-ui/display'; -const pulseAnimation = keyframes` - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } -`; - const StyledRoutingContainer = styled.div` align-items: center; background: ${({ theme }) => theme.background.transparent.lighter}; @@ -25,9 +19,19 @@ const StyledRoutingContainer = styled.div` const StyledIconContainer = styled.div<{ isLoading: boolean }>` align-items: center; animation: ${({ isLoading }) => - isLoading ? `${pulseAnimation} 2s ease-in-out infinite` : 'none'}; + isLoading ? 'pulseAnimation 2s ease-in-out infinite' : 'none'}; color: ${({ theme }) => theme.color.blue}; display: flex; + + @keyframes pulseAnimation { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } + } `; const StyledText = styled.div` diff --git a/packages/twenty-front/src/pages/settings/ai/SettingsAgentForm.tsx b/packages/twenty-front/src/pages/settings/ai/SettingsAgentForm.tsx index 8484697751..57f9de8882 100644 --- a/packages/twenty-front/src/pages/settings/ai/SettingsAgentForm.tsx +++ b/packages/twenty-front/src/pages/settings/ai/SettingsAgentForm.tsx @@ -5,13 +5,14 @@ import { useParams } from 'react-router-dom'; import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; -import { useModal } from '@/ui/layout/modal/hooks/useModal'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; +import { TabList } from '@/ui/layout/tab-list/components/TabList'; +import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { t } from '@lingui/core/macro'; import { AppPath, SettingsPath } from 'twenty-shared/types'; import { getSettingsPath, isDefined } from 'twenty-shared/utils'; -import { H2Title, IconTrash } from 'twenty-ui/display'; -import { Button } from 'twenty-ui/input'; +import { H2Title, IconLock, IconSettings } from 'twenty-ui/display'; import { Section } from 'twenty-ui/layout'; import { type CreateAgentInput, @@ -23,9 +24,10 @@ import { useNavigateApp } from '~/hooks/useNavigateApp'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; import { useState } from 'react'; -import { SettingsAgentDeleteConfirmationModal } from './components/SettingsAgentDeleteConfirmationModal'; import { SettingsAgentDetailSkeletonLoader } from './components/SettingsAgentDetailSkeletonLoader'; -import { SettingsAIAgentForm } from './forms/components/SettingsAIAgentForm'; +import { SettingsAgentRoleTab } from './components/SettingsAgentRoleTab'; +import { SettingsAgentSettingsTab } from './components/SettingsAgentSettingsTab'; +import { SETTINGS_AGENT_DETAIL_TABS } from './constants/SettingsAgentDetailTabs'; import { useSettingsAgentFormState } from './hooks/useSettingsAgentFormState'; const StyledContentContainer = styled.div` @@ -36,20 +38,26 @@ const StyledContentContainer = styled.div` width: 100%; `; -const DELETE_AGENT_MODAL_ID = 'delete-agent-modal'; +const StyledTabList = styled(TabList)` + margin-bottom: ${({ theme }) => theme.spacing(8)}; +`; export const SettingsAgentForm = ({ mode }: { mode: 'create' | 'edit' }) => { const { agentId = '' } = useParams<{ agentId: string }>(); const navigate = useNavigateSettings(); const navigateApp = useNavigateApp(); const { enqueueErrorSnackBar } = useSnackBar(); - const { openModal } = useModal(); const [isReadonlyMode, setIsReadonlyMode] = useState(false); const isEditMode = mode === 'edit'; - const isCreateMode = mode === 'create'; + const tabListComponentId = `${SETTINGS_AGENT_DETAIL_TABS.COMPONENT_INSTANCE_ID}-${agentId}`; + const activeTabId = useRecoilComponentValue( + activeTabIdComponentState, + tabListComponentId, + ); + const { formValues, isSubmitting, @@ -105,6 +113,19 @@ export const SettingsAgentForm = ({ mode }: { mode: 'create' | 'edit' }) => { const canSave = !isReadonlyMode && validateForm() && !isSubmitting; + const tabs = [ + { + id: SETTINGS_AGENT_DETAIL_TABS.TABS_IDS.SETTINGS, + title: t`Settings`, + Icon: IconSettings, + }, + { + id: SETTINGS_AGENT_DETAIL_TABS.TABS_IDS.ROLE, + title: t`Role`, + Icon: IconLock, + }, + ]; + const handleSave = async () => { if (isReadonlyMode) { return; @@ -181,6 +202,31 @@ export const SettingsAgentForm = ({ mode }: { mode: 'create' | 'edit' }) => { : agent?.label : t`New Agent`; + const renderActiveTabContent = () => { + switch (activeTabId) { + case SETTINGS_AGENT_DETAIL_TABS.TABS_IDS.ROLE: + return ( + + ); + + case SETTINGS_AGENT_DETAIL_TABS.TABS_IDS.SETTINGS: + return ( + + ); + default: + return <>; + } + }; + return ( <> { {isEditMode && loading ? ( ) : ( - - + - {!isReadonlyMode && - isEditMode && - agent && - formValues.isCustom && ( -
- -
- )} -
+ + {renderActiveTabContent()} + + )}
- {isEditMode && agent && ( - - )} ); }; diff --git a/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentRoleTab.tsx b/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentRoleTab.tsx new file mode 100644 index 0000000000..3f2130dca5 --- /dev/null +++ b/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentRoleTab.tsx @@ -0,0 +1,118 @@ +import styled from '@emotion/styled'; +import { useLingui } from '@lingui/react/macro'; + +import { SettingsRolePermissions } from '@/settings/roles/role-permissions/components/SettingsRolePermissions'; +import { Select } from '@/ui/input/components/Select'; +import { SettingsPath } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { + H1Title, + H1TitleFontColor, + H2Title, + IconArrowUpRight, + IconUser, + useIcons, +} from 'twenty-ui/display'; +import { Button } from 'twenty-ui/input'; +import { Section } from 'twenty-ui/layout'; +import { useGetRolesQuery } from '~/generated-metadata/graphql'; +import { useNavigateSettings } from '~/hooks/useNavigateSettings'; +import { type SettingsAIAgentFormValues } from '../hooks/useSettingsAgentFormState'; + +const StyledRoleContainer = styled.div` + align-items: flex-end; + display: flex; + gap: ${({ theme }) => theme.spacing(2)}; + margin-bottom: ${({ theme }) => theme.spacing(8)}; +`; + +const StyledRoleSelector = styled.div` + flex: 1; +`; + +type SettingsAgentRoleTabProps = { + formValues: SettingsAIAgentFormValues; + onFieldChange: ( + field: keyof SettingsAIAgentFormValues, + value: SettingsAIAgentFormValues[keyof SettingsAIAgentFormValues], + ) => void; + disabled: boolean; +}; + +export const SettingsAgentRoleTab = ({ + formValues, + onFieldChange, + disabled, +}: SettingsAgentRoleTabProps) => { + const { t } = useLingui(); + const { getIcon } = useIcons(); + const navigateSettings = useNavigateSettings(); + + const { data: rolesData } = useGetRolesQuery(); + + const rolesOptions = [ + { + label: t`None`, + value: null, + Icon: IconUser, + }, + ...(rolesData?.getRoles + ?.filter((role) => role.canBeAssignedToAgents) + .map((role) => ({ + label: role.label, + value: role.id, + Icon: getIcon(role.icon) ?? IconUser, + })) || []), + ]; + + const selectedRole = rolesData?.getRoles?.find( + (role) => role.id === formValues.role, + ); + + const handleOpenRole = () => { + if (isDefined(selectedRole)) { + navigateSettings(SettingsPath.RoleDetail, { roleId: selectedRole.id }); + } + }; + + return ( +
+ + + +