diff --git a/packages/twenty-front/public/images/ai/ai-skills-cover-dark.png b/packages/twenty-front/public/images/ai/ai-skills-cover-dark.png new file mode 100644 index 0000000000000..8e805a8c134cb Binary files /dev/null and b/packages/twenty-front/public/images/ai/ai-skills-cover-dark.png differ diff --git a/packages/twenty-front/public/images/ai/ai-skills-cover-light.png b/packages/twenty-front/public/images/ai/ai-skills-cover-light.png new file mode 100644 index 0000000000000..bdceaca5c0c52 Binary files /dev/null and b/packages/twenty-front/public/images/ai/ai-skills-cover-light.png differ diff --git a/packages/twenty-front/src/pages/settings/ai/SettingsAI.tsx b/packages/twenty-front/src/pages/settings/ai/SettingsAI.tsx index 80aa6199e5bff..ee7cfece904ee 100644 --- a/packages/twenty-front/src/pages/settings/ai/SettingsAI.tsx +++ b/packages/twenty-front/src/pages/settings/ai/SettingsAI.tsx @@ -23,7 +23,7 @@ import { Button } from 'twenty-ui/input'; import { Card, Section } from 'twenty-ui/layout'; import { SettingsAIMCP } from './components/SettingsAIMCP'; import { SettingsAIModelsTab } from './components/SettingsAIModelsTab'; -import { SettingsSkillsTable } from './components/SettingsSkillsTable'; +import { SettingsAgentSkills } from './components/SettingsAgentSkills'; import { SettingsToolsTable } from './components/SettingsToolsTable'; import { SETTINGS_AI_TABS } from './constants/SettingsAiTabs'; @@ -84,7 +84,7 @@ export const SettingsAI = () => { componentInstanceId={SETTINGS_AI_TABS.COMPONENT_INSTANCE_ID} /> {isModelsTab && } - {isSkillsTab && } + {isSkillsTab && } {isToolsTab && } {isMoreTab && ( <> diff --git a/packages/twenty-front/src/pages/settings/ai/components/SettingsAIAgentsTable.tsx b/packages/twenty-front/src/pages/settings/ai/components/SettingsAIAgentsTable.tsx index 49dd834c2cd26..76a56d180d104 100644 --- a/packages/twenty-front/src/pages/settings/ai/components/SettingsAIAgentsTable.tsx +++ b/packages/twenty-front/src/pages/settings/ai/components/SettingsAIAgentsTable.tsx @@ -2,7 +2,6 @@ import { styled } from '@linaria/react'; import { useLingui } from '@lingui/react/macro'; import { useContext, useState } from 'react'; -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'; @@ -12,13 +11,8 @@ import { TableHeader } from '@/ui/layout/table/components/TableHeader'; import { useSortedArray } from '@/ui/layout/table/hooks/useSortedArray'; import { SettingsPath } from 'twenty-shared/types'; import { getSettingsPath } from 'twenty-shared/utils'; -import { - IconChevronRight, - IconFilter, - IconSearch, - IconSettingsAutomation, -} from 'twenty-ui/display'; -import { Button } from 'twenty-ui/input'; +import { IconChevronRight, IconSettingsAutomation } from 'twenty-ui/display'; +import { SearchInput } from 'twenty-ui/input'; import { MenuItemToggle } from 'twenty-ui/navigation'; import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants'; import { SETTINGS_AI_AGENT_TABLE_METADATA } from '~/pages/settings/ai/constants/SettingsAiAgentTableMetadata'; @@ -33,15 +27,8 @@ import { SettingsAIAgentTableRow, } from './SettingsAIAgentTableRow'; -const StyledSearchAndFilterContainer = styled.div` - display: flex; - gap: ${themeCssVariables.spacing[2]}; +const StyledSearchContainer = styled.div` margin-bottom: ${themeCssVariables.spacing[2]}; - width: 100%; -`; - -const StyledSearchInputContainer = styled.div` - flex: 1; `; const StyledTableContainer = styled.div` @@ -79,47 +66,36 @@ export const SettingsAIAgentsTable = () => { return ( <> - - - - - + ( + + + + setShowWorkflowAgents(!showWorkflowAgents) + } + toggled={showWorkflowAgents} + text={t`Workflow agents`} + toggleSize="small" + /> + + + } /> - } - dropdownComponents={ - - - - setShowWorkflowAgents(!showWorkflowAgents) - } - toggled={showWorkflowAgents} - text={t`Workflow agents`} - toggleSize="small" - /> - - - } + )} /> - + diff --git a/packages/twenty-front/src/pages/settings/ai/components/SettingsAIModelsTab.tsx b/packages/twenty-front/src/pages/settings/ai/components/SettingsAIModelsTab.tsx index f79ff8e176b45..b47057c3bab11 100644 --- a/packages/twenty-front/src/pages/settings/ai/components/SettingsAIModelsTab.tsx +++ b/packages/twenty-front/src/pages/settings/ai/components/SettingsAIModelsTab.tsx @@ -11,16 +11,11 @@ import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsO import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { Select } from '@/ui/input/components/Select'; import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; -import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput'; import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { t } from '@lingui/core/macro'; -import { - H2Title, - IconBolt, - IconSearch, - IconTwentyStar, -} from 'twenty-ui/display'; +import { H2Title, IconBolt, IconTwentyStar } 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'; @@ -278,13 +273,10 @@ export const SettingsAIModelsTab = () => { /> - diff --git a/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentSkills.tsx b/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentSkills.tsx new file mode 100644 index 0000000000000..c913f5ed8438a --- /dev/null +++ b/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentSkills.tsx @@ -0,0 +1,162 @@ +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +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 { useSortedArray } from '@/ui/layout/table/hooks/useSortedArray'; +import { styled } from '@linaria/react'; +import { useLingui } from '@lingui/react/macro'; +import { useContext, useMemo, useState } from 'react'; +import { SettingsPath } from 'twenty-shared/types'; +import { getSettingsPath } from 'twenty-shared/utils'; +import { H2Title, IconArchive, IconPlus } from 'twenty-ui/display'; +import { Button, SearchInput } from 'twenty-ui/input'; +import { MenuItemToggle, UndecoratedLink } from 'twenty-ui/navigation'; +import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants'; + +import { useMutation, useQuery } from '@apollo/client/react'; +import { Section } from 'twenty-ui/layout'; +import { + ActivateSkillDocument, + DeleteSkillDocument, + FindManySkillsDocument, +} from '~/generated-metadata/graphql'; +import { SETTINGS_SKILL_TABLE_METADATA } from '~/pages/settings/ai/constants/SettingsSkillTableMetadata'; +import { normalizeSearchText } from '~/utils/normalizeSearchText'; +import { SettingsAgentSkillsTable } from './SettingsAgentSkillsTable'; + +const StyledCoverImage = styled.div` + background-size: cover; + height: 160px; + overflow: hidden; +`; + +const StyledFooterContainer = styled.div` + align-items: center; + display: flex; + justify-content: flex-end; + margin-top: ${themeCssVariables.spacing[4]}; +`; + +const StyledSearchInput = styled(SearchInput)` + margin-bottom: ${themeCssVariables.spacing[4]}; +`; + +export const SettingsAgentSkills = () => { + const { t } = useLingui(); + const { colorScheme } = useContext(ThemeContext); + const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar(); + + const { data, loading, refetch } = useQuery(FindManySkillsDocument); + const [activateSkill] = useMutation(ActivateSkillDocument); + const [deleteSkill] = useMutation(DeleteSkillDocument); + + const [searchTerm, setSearchTerm] = useState(''); + const [showDeactivated, setShowDeactivated] = useState(true); + + const skills = data?.skills ?? []; + + const sortedSkills = useSortedArray(skills, SETTINGS_SKILL_TABLE_METADATA); + + const filteredSkills = useMemo( + () => + sortedSkills.filter((skill) => { + const searchNormalized = normalizeSearchText(searchTerm); + const matchesSearch = + normalizeSearchText(skill.name).includes(searchNormalized) || + normalizeSearchText(skill.label).includes(searchNormalized); + + if (!matchesSearch) { + return false; + } + + if (!skill.isActive && !showDeactivated) { + return false; + } + + return true; + }), + [sortedSkills, searchTerm, showDeactivated], + ); + + const handleActivate = async (skillId: string) => { + try { + await activateSkill({ variables: { id: skillId } }); + enqueueSuccessSnackBar({ message: t`Skill activated` }); + refetch(); + } catch { + enqueueErrorSnackBar({ message: t`Failed to activate skill` }); + } + }; + + const handleDelete = async (skillId: string) => { + try { + await deleteSkill({ variables: { id: skillId } }); + enqueueSuccessSnackBar({ message: t`Skill deleted` }); + refetch(); + } catch { + enqueueErrorSnackBar({ message: t`Failed to delete skill` }); + } + }; + + const coverImage = + colorScheme === 'light' + ? '/images/ai/ai-skills-cover-light.png' + : '/images/ai/ai-skills-cover-dark.png'; + + return ( + <> + +
+ + + ( + + + + setShowDeactivated(!showDeactivated) + } + toggled={showDeactivated} + text={t`Deactivated`} + toggleSize="small" + /> + + + } + /> + )} + /> + + + +
+ + ); +}; diff --git a/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentSkillsTable.tsx b/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentSkillsTable.tsx new file mode 100644 index 0000000000000..0878758dd3128 --- /dev/null +++ b/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentSkillsTable.tsx @@ -0,0 +1,97 @@ +import { styled } from '@linaria/react'; +import { useLingui } from '@lingui/react/macro'; +import { useContext } from 'react'; +import Skeleton from 'react-loading-skeleton'; + +import { SortableTableHeader } from '@/ui/layout/table/components/SortableTableHeader'; +import { Table } from '@/ui/layout/table/components/Table'; +import { TableHeader } from '@/ui/layout/table/components/TableHeader'; +import { TableRow } from '@/ui/layout/table/components/TableRow'; +import { SettingsPath } from 'twenty-shared/types'; +import { getSettingsPath } from 'twenty-shared/utils'; +import { IconChevronRight } from 'twenty-ui/display'; +import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants'; + +import { type FindManySkillsQuery } from '~/generated-metadata/graphql'; +import { SettingsSkillInactiveMenuDropDown } from '~/pages/settings/ai/components/SettingsSkillInactiveMenuDropDown'; +import { SETTINGS_SKILL_TABLE_METADATA } from '~/pages/settings/ai/constants/SettingsSkillTableMetadata'; +import { SettingsSkillTableRow } from './SettingsSkillTableRow'; + +type Skill = FindManySkillsQuery['skills'][number]; + +type SettingsAgentSkillsTableProps = { + skills: Skill[]; + loading: boolean; + onActivate: (skillId: string) => void; + onDelete: (skillId: string) => void; +}; + +const StyledTableHeaderRowContainer = styled.div` + margin-bottom: ${themeCssVariables.spacing[2]}; +`; + +export const SettingsAgentSkillsTable = ({ + skills, + loading, + onActivate, + onDelete, +}: SettingsAgentSkillsTableProps) => { + const { theme } = useContext(ThemeContext); + const { t } = useLingui(); + + const showSkeleton = loading && skills.length === 0; + + return ( +
+ + + {SETTINGS_SKILL_TABLE_METADATA.fields.map( + (settingsSkillTableMetadataField) => ( + + ), + )} + + + + {showSkeleton + ? Array.from({ length: 3 }).map((_, index) => ( + + )) + : skills.map((skill) => ( + + ) : ( + onActivate(skill.id)} + onDelete={() => onDelete(skill.id)} + /> + ) + } + link={ + skill.isActive + ? getSettingsPath(SettingsPath.AISkillDetail, { + skillId: skill.id, + }) + : undefined + } + /> + ))} +
+ ); +}; diff --git a/packages/twenty-front/src/pages/settings/ai/components/SettingsSkillTableRow.tsx b/packages/twenty-front/src/pages/settings/ai/components/SettingsSkillTableRow.tsx index fdbccdac459b0..8c855f7036903 100644 --- a/packages/twenty-front/src/pages/settings/ai/components/SettingsSkillTableRow.tsx +++ b/packages/twenty-front/src/pages/settings/ai/components/SettingsSkillTableRow.tsx @@ -4,7 +4,7 @@ import { type ReactNode, useContext } from 'react'; import { SettingsItemTypeTag } from '@/settings/components/SettingsItemTypeTag'; import { TableCell } from '@/ui/layout/table/components/TableCell'; import { TableRow } from '@/ui/layout/table/components/TableRow'; -import { useIcons, OverflowingTextWithTooltip } from 'twenty-ui/display'; +import { OverflowingTextWithTooltip, useIcons } from 'twenty-ui/display'; import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants'; import { type Skill } from '~/generated-metadata/graphql'; @@ -44,7 +44,7 @@ export const SettingsSkillTableRow = ({ overflow="hidden" > - + diff --git a/packages/twenty-front/src/pages/settings/ai/components/SettingsSkillsTable.tsx b/packages/twenty-front/src/pages/settings/ai/components/SettingsSkillsTable.tsx deleted file mode 100644 index 68de84234a423..0000000000000 --- a/packages/twenty-front/src/pages/settings/ai/components/SettingsSkillsTable.tsx +++ /dev/null @@ -1,226 +0,0 @@ -import { styled } from '@linaria/react'; -import { useLingui } from '@lingui/react/macro'; -import { useContext, useMemo, useState } from 'react'; -import Skeleton from 'react-loading-skeleton'; - -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 { SortableTableHeader } from '@/ui/layout/table/components/SortableTableHeader'; -import { Table } from '@/ui/layout/table/components/Table'; -import { TableHeader } from '@/ui/layout/table/components/TableHeader'; -import { TableRow } from '@/ui/layout/table/components/TableRow'; -import { useSortedArray } from '@/ui/layout/table/hooks/useSortedArray'; -import { SettingsPath } from 'twenty-shared/types'; -import { getSettingsPath } from 'twenty-shared/utils'; -import { - IconArchive, - IconChevronRight, - IconFilter, - IconPlus, - IconSearch, -} from 'twenty-ui/display'; -import { Button } from 'twenty-ui/input'; -import { MenuItemToggle, UndecoratedLink } from 'twenty-ui/navigation'; -import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants'; - -import { useMutation, useQuery } from '@apollo/client/react'; -import { - ActivateSkillDocument, - DeleteSkillDocument, - FindManySkillsDocument, -} from '~/generated-metadata/graphql'; -import { SettingsSkillInactiveMenuDropDown } from '~/pages/settings/ai/components/SettingsSkillInactiveMenuDropDown'; -import { SETTINGS_SKILL_TABLE_METADATA } from '~/pages/settings/ai/constants/SettingsSkillTableMetadata'; -import { normalizeSearchText } from '~/utils/normalizeSearchText'; -import { SettingsSkillTableRow } from './SettingsSkillTableRow'; - -const StyledSearchAndFilterContainer = styled.div` - align-items: center; - display: flex; - gap: ${themeCssVariables.spacing[2]}; - padding-bottom: ${themeCssVariables.spacing[2]}; -`; - -const StyledSearchInputWrapper = styled.div` - flex: 1; -`; - -const StyledTableHeaderRowContainer = styled.div` - margin-bottom: ${themeCssVariables.spacing[2]}; -`; - -const StyledFooterContainer = styled.div` - align-items: center; - display: flex; - justify-content: flex-end; - margin-top: ${themeCssVariables.spacing[4]}; -`; - -export const SettingsSkillsTable = () => { - const { theme } = useContext(ThemeContext); - const { data, loading, refetch } = useQuery(FindManySkillsDocument); - const [activateSkill] = useMutation(ActivateSkillDocument); - const [deleteSkill] = useMutation(DeleteSkillDocument); - - const { t } = useLingui(); - const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar(); - const [searchTerm, setSearchTerm] = useState(''); - const [showDeactivated, setShowDeactivated] = useState(true); - - const skills = data?.skills ?? []; - - const sortedSkills = useSortedArray(skills, SETTINGS_SKILL_TABLE_METADATA); - - const filteredSkills = useMemo( - () => - sortedSkills.filter((skill) => { - const searchNormalized = normalizeSearchText(searchTerm); - const matchesSearch = - normalizeSearchText(skill.name).includes(searchNormalized) || - normalizeSearchText(skill.label).includes(searchNormalized); - - if (!matchesSearch) { - return false; - } - - if (!skill.isActive && !showDeactivated) { - return false; - } - - return true; - }), - [sortedSkills, searchTerm, showDeactivated], - ); - - const showSkeleton = loading && skills.length === 0; - - const handleActivate = async (skillId: string) => { - try { - await activateSkill({ variables: { id: skillId } }); - enqueueSuccessSnackBar({ message: t`Skill activated` }); - refetch(); - } catch { - enqueueErrorSnackBar({ message: t`Failed to activate skill` }); - } - }; - - const handleDelete = async (skillId: string) => { - try { - await deleteSkill({ variables: { id: skillId } }); - enqueueSuccessSnackBar({ message: t`Skill deleted` }); - refetch(); - } catch { - enqueueErrorSnackBar({ message: t`Failed to delete skill` }); - } - }; - - return ( - <> - - - - - - } - dropdownComponents={ - - - setShowDeactivated(!showDeactivated)} - toggled={showDeactivated} - text={t`Deactivated`} - toggleSize="small" - /> - - - } - /> - - - - - - {SETTINGS_SKILL_TABLE_METADATA.fields.map( - (settingsSkillTableMetadataField) => ( - - ), - )} - - - - {showSkeleton - ? Array.from({ length: 3 }).map((_, index) => ( - - )) - : filteredSkills.map((skill) => ( - - ) : ( - handleActivate(skill.id)} - onDelete={() => handleDelete(skill.id)} - /> - ) - } - link={ - skill.isActive - ? getSettingsPath(SettingsPath.AISkillDetail, { - skillId: skill.id, - }) - : undefined - } - /> - ))} -
- - - -