diff --git a/packages/twenty-front/public/images/integrations/integration-mcp-cover-dark.svg b/packages/twenty-front/public/images/ai/ai-mcp-cover-dark.svg similarity index 100% rename from packages/twenty-front/public/images/integrations/integration-mcp-cover-dark.svg rename to packages/twenty-front/public/images/ai/ai-mcp-cover-dark.svg diff --git a/packages/twenty-front/public/images/integrations/integration-mcp-cover-light.svg b/packages/twenty-front/public/images/ai/ai-mcp-cover-light.svg similarity index 100% rename from packages/twenty-front/public/images/integrations/integration-mcp-cover-light.svg rename to packages/twenty-front/public/images/ai/ai-mcp-cover-light.svg diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormAdvancedTextFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormAdvancedTextFieldInput.tsx index 5a38d6195d..38e6385fb5 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormAdvancedTextFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormAdvancedTextFieldInput.tsx @@ -8,7 +8,6 @@ import { type VariablePickerComponent } from '@/object-record/record-field/ui/fo import { InputErrorHelper } from '@/ui/input/components/InputErrorHelper'; import { InputHint } from '@/ui/input/components/InputHint'; import { InputLabel } from '@/ui/input/components/InputLabel'; -import { StyledDropdownButtonContainer } from '@/ui/layout/dropdown/components/StyledDropdownButtonContainer'; import { useFullScreenModal } from '@/ui/layout/fullscreen/hooks/useFullScreenModal'; import { type BreadcrumbProps } from '@/ui/navigation/bread-crumb/components/Breadcrumb'; import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack'; @@ -16,10 +15,11 @@ import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; import { styled } from '@linaria/react'; import { useLingui } from '@lingui/react/macro'; -import { useContext, useId, useState } from 'react'; +import { useId, useState } from 'react'; import { isDefined } from 'twenty-shared/utils'; import { IconMaximize } from 'twenty-ui/display'; -import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants'; +import { LightIconButton } from 'twenty-ui/input'; +import { themeCssVariables } from 'twenty-ui/theme-constants'; import { useIsMobile } from 'twenty-ui/utilities'; const StyledAdvancedTextFieldContainerWrapper = styled.div` @@ -47,8 +47,9 @@ const StyledAdvancedTextFieldInnerContainer = styled.div` `; const StyledEditorActionButtonContainer = styled.div` + margin-top: ${themeCssVariables.spacing[1]}; position: absolute; - right: 30px; + right: ${themeCssVariables.spacing[1]}; top: ${themeCssVariables.spacing[0]}; z-index: 1; `; @@ -63,19 +64,6 @@ const StyledFullScreenEditorContainer = styled.div` padding: ${themeCssVariables.spacing[2]}; `; -const StyledFullScreenButtonContainerWrapper = styled.div` - > * { - background-color: transparent; - color: ${themeCssVariables.font.color.tertiary}; - padding: ${themeCssVariables.spacing[2]}; - - :hover { - background-color: ${themeCssVariables.background.transparent.light}; - cursor: pointer; - } - } -`; - type FormAdvancedTextFieldInputProps = { label?: string; error?: string; @@ -111,7 +99,6 @@ export const FormAdvancedTextFieldInput = ({ maxWidth, contentType = 'json', }: FormAdvancedTextFieldInputProps) => { - const { theme } = useContext(ThemeContext); const instanceId = useId(); const isMobile = useIsMobile(); const [isFullScreen, setIsFullScreen] = useState(false); @@ -229,15 +216,12 @@ export const FormAdvancedTextFieldInput = ({ {enableFullScreen && ( {!readonly && !isFullScreen && ( - - - - - + )} )} diff --git a/packages/twenty-front/src/pages/settings/ai/SettingsAI.tsx b/packages/twenty-front/src/pages/settings/ai/SettingsAI.tsx index cf23c2aa49..75ffd4ef10 100644 --- a/packages/twenty-front/src/pages/settings/ai/SettingsAI.tsx +++ b/packages/twenty-front/src/pages/settings/ai/SettingsAI.tsx @@ -1,7 +1,3 @@ -import { styled } from '@linaria/react'; -import { Link } from 'react-router-dom'; - -import { SettingsOptionCardContentButton } from '@/settings/components/SettingsOptions/SettingsOptionCardContentButton'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { TabList } from '@/ui/layout/tab-list/components/TabList'; @@ -13,29 +9,19 @@ import { getSettingsPath } from 'twenty-shared/utils'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { t } from '@lingui/core/macro'; import { - H2Title, IconChartBar, IconCpu, - IconFileText, 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 { SettingsAIMoreTab } from '~/pages/settings/ai/components/SettingsAIMoreTab'; import { SettingsAIModelsTab } from './components/SettingsAIModelsTab'; import { SettingsAIUsageTab } from './components/SettingsAIUsageTab'; import { SettingsAgentSkills } from './components/SettingsAgentSkills'; import { SettingsToolsTable } from './components/SettingsToolsTable'; import { SETTINGS_AI_TABS } from './constants/SettingsAiTabs'; -const StyledLinkContainer = styled.div` - > a { - text-decoration: none; - } -`; - export const SettingsAI = () => { const activeTabId = useAtomComponentStateValue( activeTabIdComponentState, @@ -104,35 +90,7 @@ export const SettingsAI = () => { {isSkillsTab && } {isToolsTab && } {isUsageTab && } - {isMoreTab && ( - <> -
- - - - -
- - - )} + {isMoreTab && } ); diff --git a/packages/twenty-front/src/pages/settings/ai/SettingsAIPrompts.tsx b/packages/twenty-front/src/pages/settings/ai/SettingsAIPrompts.tsx index 05eaace252..4a9144d270 100644 --- a/packages/twenty-front/src/pages/settings/ai/SettingsAIPrompts.tsx +++ b/packages/twenty-front/src/pages/settings/ai/SettingsAIPrompts.tsx @@ -1,28 +1,19 @@ -import { CombinedGraphQLErrors } from '@apollo/client/errors'; import { styled } from '@linaria/react'; -import { useDebouncedCallback } from 'use-debounce'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; -import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { FormAdvancedTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormAdvancedTextFieldInput'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; -import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; -import { t } from '@lingui/core/macro'; -import { useState } from 'react'; -import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { useQuery } from '@apollo/client/react'; +import { t } from '@lingui/core/macro'; import { SettingsPath } from 'twenty-shared/types'; import { getSettingsPath, isDefined } from 'twenty-shared/utils'; -import { formatNumber } from '~/utils/format/formatNumber'; import { H2Title } from 'twenty-ui/display'; import { Section } from 'twenty-ui/layout'; import { themeCssVariables } from 'twenty-ui/theme-constants'; -import { useQuery, useMutation } from '@apollo/client/react'; -import { - GetAiSystemPromptPreviewDocument, - UpdateWorkspaceDocument, -} from '~/generated-metadata/graphql'; +import { GetAiSystemPromptPreviewDocument } from '~/generated-metadata/graphql'; +import { formatNumber } from '~/utils/format/formatNumber'; const StyledFormContainer = styled.div` display: flex; @@ -41,83 +32,12 @@ const StyledTokenBadge = styled.span` `; export const SettingsAIPrompts = () => { - const { enqueueErrorSnackBar } = useSnackBar(); - const [currentWorkspace, setCurrentWorkspace] = useAtomState( - currentWorkspaceState, - ); const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState); - const [updateWorkspace] = useMutation(UpdateWorkspaceDocument); const { data: previewData, loading: previewLoading } = useQuery( GetAiSystemPromptPreviewDocument, ); - const [workspaceInstructions, setWorkspaceInstructions] = useState( - currentWorkspace?.aiAdditionalInstructions ?? '', - ); - const [originalInstructions, setOriginalInstructions] = useState( - currentWorkspace?.aiAdditionalInstructions ?? '', - ); - - const handleWorkspaceInstructionsInit = () => { - if (currentWorkspace?.aiAdditionalInstructions !== undefined) { - setWorkspaceInstructions(currentWorkspace.aiAdditionalInstructions ?? ''); - setOriginalInstructions(currentWorkspace.aiAdditionalInstructions ?? ''); - } - }; - - if ( - currentWorkspace?.aiAdditionalInstructions !== undefined && - originalInstructions === '' && - currentWorkspace.aiAdditionalInstructions !== null && - currentWorkspace.aiAdditionalInstructions !== originalInstructions - ) { - handleWorkspaceInstructionsInit(); - } - - const autoSave = useDebouncedCallback(async (newValue: string) => { - if (!currentWorkspace?.id || newValue === originalInstructions) { - return; - } - - try { - setCurrentWorkspace({ - ...currentWorkspace, - aiAdditionalInstructions: newValue || null, - }); - - await updateWorkspace({ - variables: { - input: { - aiAdditionalInstructions: newValue || null, - }, - }, - }); - - setOriginalInstructions(newValue); - } catch (error) { - setCurrentWorkspace({ - ...currentWorkspace, - aiAdditionalInstructions: originalInstructions || null, - }); - - if (CombinedGraphQLErrors.is(error)) { - enqueueErrorSnackBar({ - apolloError: error, - }); - } else { - enqueueErrorSnackBar({ - message: t`Failed to save workspace instructions`, - }); - } - } - }, 1000); - - const handleWorkspaceInstructionsChange = (value: string) => { - setWorkspaceInstructions(value); - autoSave(value); - }; - const preview = previewData?.getAISystemPromptPreview; const sections = preview?.sections ?? []; @@ -210,36 +130,6 @@ export const SettingsAIPrompts = () => { ))} -
- - - - -
-
{ const { t } = useLingui(); const { copyToClipboard } = useCopyToClipboard(); const [authMethod, setAuthMethod] = useState('oauth'); + const { colorScheme } = useContext(ThemeContext); + const coverImage = + colorScheme === 'light' + ? '/images/ai/ai-mcp-cover-light.svg' + : '/images/ai/ai-mcp-cover-dark.svg'; const oauthConfig = JSON.stringify( { @@ -87,6 +103,7 @@ export const SettingsAIMCP = () => { folding: false, selectionHighlight: false, occurrencesHighlight: 'off' as const, + scrollBeyondLastLine: false, hover: { enabled: false, }, @@ -104,30 +121,30 @@ export const SettingsAIMCP = () => {
- - setAuthMethod(value as McpAuthMethod)} + options={[ + { label: t`OAuth`, value: 'oauth' }, + { label: t`API Key`, value: 'api-key' }, + ]} + selectSizeVariant="small" + dropdownWidth={GenericDropdownContentWidth.Medium} + dropdownOffset={{ x: 0, y: 4 }} + /> + { copyToClipboard( @@ -135,17 +152,17 @@ export const SettingsAIMCP = () => { t`MCP Configuration copied to clipboard`, ); }} - type="button" + size="small" /> - + - +
); diff --git a/packages/twenty-front/src/pages/settings/ai/components/SettingsAIMoreTab.tsx b/packages/twenty-front/src/pages/settings/ai/components/SettingsAIMoreTab.tsx new file mode 100644 index 0000000000..52e36701cf --- /dev/null +++ b/packages/twenty-front/src/pages/settings/ai/components/SettingsAIMoreTab.tsx @@ -0,0 +1,148 @@ +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; +import { FormAdvancedTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormAdvancedTextFieldInput'; +import { SettingsCard } from '@/settings/components/SettingsCard'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; +import { CombinedGraphQLErrors } from '@apollo/client'; +import { useMutation, useQuery } from '@apollo/client/react'; +import { styled } from '@linaria/react'; +import { t } from '@lingui/core/macro'; +import { useContext, useState } from 'react'; +import { SettingsPath } from 'twenty-shared/types'; +import { getSettingsPath, isDefined } from 'twenty-shared/utils'; +import { H2Title, IconPrompt } from 'twenty-ui/display'; +import { Section } from 'twenty-ui/layout'; +import { UndecoratedLink } from 'twenty-ui/navigation'; +import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants'; +import { useDebouncedCallback } from 'use-debounce'; +import { + GetAiSystemPromptPreviewDocument, + UpdateWorkspaceDocument, +} from '~/generated-metadata/graphql'; +import { SettingsAIMCP } from '~/pages/settings/ai/components/SettingsAIMCP'; +import { formatNumber } from '~/utils/format/formatNumber'; + +const StyledFormContainer = styled.div` + display: flex; + flex-direction: column; + gap: ${themeCssVariables.spacing[4]}; +`; + +export const SettingsAIMoreTab = () => { + const { theme } = useContext(ThemeContext); + const { enqueueErrorSnackBar } = useSnackBar(); + const [currentWorkspace, setCurrentWorkspace] = useAtomState( + currentWorkspaceState, + ); + const [updateWorkspace] = useMutation(UpdateWorkspaceDocument); + const { data: previewData } = useQuery(GetAiSystemPromptPreviewDocument); + + const initialInstructions = currentWorkspace?.aiAdditionalInstructions ?? ''; + const [workspaceInstructions, setWorkspaceInstructions] = + useState(initialInstructions); + const [originalInstructions, setOriginalInstructions] = + useState(initialInstructions); + + const autoSave = useDebouncedCallback(async (newValue: string) => { + if (!currentWorkspace?.id || newValue === originalInstructions) { + return; + } + + try { + setCurrentWorkspace({ + ...currentWorkspace, + aiAdditionalInstructions: newValue || null, + }); + + await updateWorkspace({ + variables: { + input: { + aiAdditionalInstructions: newValue || null, + }, + }, + }); + + setOriginalInstructions(newValue); + } catch (error) { + setCurrentWorkspace({ + ...currentWorkspace, + aiAdditionalInstructions: originalInstructions || null, + }); + + if (CombinedGraphQLErrors.is(error)) { + enqueueErrorSnackBar({ + apolloError: error, + }); + } else { + enqueueErrorSnackBar({ + message: t`Failed to save workspace instructions`, + }); + } + } + }, 1000); + + const handleWorkspaceInstructionsChange = (value: string) => { + setWorkspaceInstructions(value); + autoSave(value); + }; + + const systemPromptTokenCount = + previewData?.getAISystemPromptPreview.estimatedTokenCount; + const systemPromptDescription = isDefined(systemPromptTokenCount) + ? t`Read the system prompts to understand how the AI works (~${formatNumber( + systemPromptTokenCount, + { + abbreviate: true, + decimals: 1, + }, + )} tokens)` + : t`Read the system prompts to understand how the AI works`; + + return ( + <> +
+ + + + +
+ + +
+ + + + } + title={t`Read system prompts`} + /> + +
+ + ); +}; diff --git a/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentSkills.tsx b/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentSkills.tsx index c913f5ed84..0ecb6df1c8 100644 --- a/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentSkills.tsx +++ b/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentSkills.tsx @@ -25,6 +25,7 @@ import { normalizeSearchText } from '~/utils/normalizeSearchText'; import { SettingsAgentSkillsTable } from './SettingsAgentSkillsTable'; const StyledCoverImage = styled.div` + background-position: center; background-size: cover; height: 160px; overflow: hidden; diff --git a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts index 78a3801326..122f50f7da 100644 --- a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts +++ b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts @@ -8,8 +8,8 @@ export { IconAlignLeft, IconAlignRight, IconApi, - IconAppWindow, IconApps, + IconAppWindow, IconArchive, IconArchiveOff, IconArrowBackUp, @@ -17,12 +17,12 @@ export { IconArrowLeft, IconArrowMerge, IconArrowRight, - IconArrowUp, - IconArrowUpRight, IconArrowsDiagonal, IconArrowsSort, IconArrowsSplit2, IconArrowsVertical, + IconArrowUp, + IconArrowUpRight, IconAt, IconAxisX, IconAxisY, @@ -47,8 +47,8 @@ export { IconBrandGoogle, IconBrandGraphql, IconBrandLinkedin, - IconBrandOpenai, IconBrandNpm, + IconBrandOpenai, IconBrandX, IconBriefcase, IconBroadcast, @@ -77,8 +77,8 @@ export { IconChevronLeft, IconChevronRight, IconChevronRightPipe, - IconChevronUp, IconChevronsRight, + IconChevronUp, IconCircle, IconCircleDashed, IconCircleDot, @@ -183,8 +183,8 @@ export { IconFilterCog, IconFilterOff, IconFilterPlus, - IconFilterX, IconFilters, + IconFilterX, IconFlag, IconFlask, IconFocusCentered, @@ -310,6 +310,7 @@ export { IconPresentation, IconPrinter, IconProgressCheck, + IconPrompt, IconPuzzle, IconPuzzle2, IconQuestionMark, @@ -345,10 +346,10 @@ export { IconShield, IconSitemap, IconSlash, - IconSortAZ, IconSortAscending, IconSortAscendingLetters, IconSortAscendingNumbers, + IconSortAZ, IconSortDescending, IconSortDescendingLetters, IconSortDescendingNumbers, diff --git a/packages/twenty-ui/src/display/index.ts b/packages/twenty-ui/src/display/index.ts index f44130b551..a6cc282de1 100644 --- a/packages/twenty-ui/src/display/index.ts +++ b/packages/twenty-ui/src/display/index.ts @@ -86,8 +86,8 @@ export { IconAlignLeft, IconAlignRight, IconApi, - IconAppWindow, IconApps, + IconAppWindow, IconArchive, IconArchiveOff, IconArrowBackUp, @@ -95,12 +95,12 @@ export { IconArrowLeft, IconArrowMerge, IconArrowRight, - IconArrowUp, - IconArrowUpRight, IconArrowsDiagonal, IconArrowsSort, IconArrowsSplit2, IconArrowsVertical, + IconArrowUp, + IconArrowUpRight, IconAt, IconAxisX, IconAxisY, @@ -125,8 +125,8 @@ export { IconBrandGoogle, IconBrandGraphql, IconBrandLinkedin, - IconBrandOpenai, IconBrandNpm, + IconBrandOpenai, IconBrandX, IconBriefcase, IconBroadcast, @@ -155,8 +155,8 @@ export { IconChevronLeft, IconChevronRight, IconChevronRightPipe, - IconChevronUp, IconChevronsRight, + IconChevronUp, IconCircle, IconCircleDashed, IconCircleDot, @@ -261,8 +261,8 @@ export { IconFilterCog, IconFilterOff, IconFilterPlus, - IconFilterX, IconFilters, + IconFilterX, IconFlag, IconFlask, IconFocusCentered, @@ -388,6 +388,7 @@ export { IconPresentation, IconPrinter, IconProgressCheck, + IconPrompt, IconPuzzle, IconPuzzle2, IconQuestionMark, @@ -423,10 +424,10 @@ export { IconShield, IconSitemap, IconSlash, - IconSortAZ, IconSortAscending, IconSortAscendingLetters, IconSortAscendingNumbers, + IconSortAZ, IconSortDescending, IconSortDescendingLetters, IconSortDescendingNumbers,