[AI] More tab new design (#19165)
closes https://discord.com/channels/1130383047699738754/1480981616666087687 <img width="1596" height="1318" alt="CleanShot 2026-03-31 at 18 06 37" src="https://github.com/user-attachments/assets/0d87610e-e4ce-4f3d-8047-97d242f230c5" />
This commit is contained in:
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
+11
-27
@@ -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 && (
|
||||
<StyledEditorActionButtonContainer>
|
||||
{!readonly && !isFullScreen && (
|
||||
<StyledFullScreenButtonContainerWrapper>
|
||||
<StyledDropdownButtonContainer
|
||||
isUnfolded={false}
|
||||
transparentBackground
|
||||
onClick={handleEnterFullScreen}
|
||||
>
|
||||
<IconMaximize size={theme.icon.size.md} />
|
||||
</StyledDropdownButtonContainer>
|
||||
</StyledFullScreenButtonContainerWrapper>
|
||||
<LightIconButton
|
||||
Icon={IconMaximize}
|
||||
size="small"
|
||||
onClick={handleEnterFullScreen}
|
||||
accent="tertiary"
|
||||
/>
|
||||
)}
|
||||
</StyledEditorActionButtonContainer>
|
||||
)}
|
||||
|
||||
@@ -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 && <SettingsAgentSkills />}
|
||||
{isToolsTab && <SettingsToolsTable />}
|
||||
{isUsageTab && <SettingsAIUsageTab />}
|
||||
{isMoreTab && (
|
||||
<>
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`System Prompt`}
|
||||
description={t`View and customize AI instructions`}
|
||||
/>
|
||||
<Card rounded>
|
||||
<SettingsOptionCardContentButton
|
||||
Icon={IconFileText}
|
||||
title={t`System Prompt`}
|
||||
description={t`View the AI system prompt and add custom instructions`}
|
||||
Button={
|
||||
<StyledLinkContainer>
|
||||
<Link to={getSettingsPath(SettingsPath.AIPrompts)}>
|
||||
<Button
|
||||
title={t`Configure`}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
/>
|
||||
</Link>
|
||||
</StyledLinkContainer>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
</Section>
|
||||
<SettingsAIMCP />
|
||||
</>
|
||||
)}
|
||||
{isMoreTab && <SettingsAIMoreTab />}
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
|
||||
@@ -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 = () => {
|
||||
</Section>
|
||||
))}
|
||||
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Workspace Instructions`}
|
||||
description={t`Add custom instructions specific to your workspace (appended to system prompt)`}
|
||||
/>
|
||||
<StyledFormContainer>
|
||||
<FormAdvancedTextFieldInput
|
||||
key={originalInstructions}
|
||||
label={t`Additional Instructions`}
|
||||
readonly={false}
|
||||
defaultValue={workspaceInstructions}
|
||||
contentType="markdown"
|
||||
onChange={handleWorkspaceInstructionsChange}
|
||||
enableFullScreen={true}
|
||||
fullScreenBreadcrumbs={[
|
||||
{
|
||||
children: t`System Prompt`,
|
||||
href: '#',
|
||||
},
|
||||
{
|
||||
children: t`Workspace Instructions`,
|
||||
},
|
||||
]}
|
||||
placeholder={t`E.g., "We are a B2B SaaS company. Always use formal language..."`}
|
||||
minHeight={150}
|
||||
maxWidth={700}
|
||||
/>
|
||||
</StyledFormContainer>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`User Context`}
|
||||
|
||||
@@ -1,31 +1,42 @@
|
||||
import { useState } from 'react';
|
||||
import { useContext, useState } from 'react';
|
||||
|
||||
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { H2Title, IconCopy, IconPlug } from 'twenty-ui/display';
|
||||
import { Button, CodeEditor } from 'twenty-ui/input';
|
||||
import { Card, Section } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { H2Title, IconCopy } from 'twenty-ui/display';
|
||||
import { CodeEditor, IconButton } from 'twenty-ui/input';
|
||||
import { Card, CardContent, Section } from 'twenty-ui/layout';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
|
||||
|
||||
const StyledConfigWrapper = styled.div`
|
||||
background-color: ${themeCssVariables.background.secondary};
|
||||
border: 1px solid ${themeCssVariables.border.color.light};
|
||||
border-radius: ${themeCssVariables.border.radius.md};
|
||||
margin: 0 ${themeCssVariables.spacing[4]} ${themeCssVariables.spacing[4]};
|
||||
const StyledCoverImage = styled.div`
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
height: 160px;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledCopyButton = styled.div`
|
||||
const StyledConfigButtonsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
position: absolute;
|
||||
right: ${themeCssVariables.spacing[3]};
|
||||
top: ${themeCssVariables.spacing[3]};
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
const StyledCoverCardContent = styled(CardContent)`
|
||||
padding: 0;
|
||||
`;
|
||||
|
||||
const StyledCopyButton = styled(IconButton)`
|
||||
background-color: ${themeCssVariables.background.transparent.lighter};
|
||||
border: 1px solid ${themeCssVariables.border.color.medium};
|
||||
`;
|
||||
|
||||
const StyledEditorContainer = styled.div`
|
||||
.monaco-editor,
|
||||
.monaco-editor .overflow-guard {
|
||||
@@ -44,6 +55,11 @@ export const SettingsAIMCP = () => {
|
||||
const { t } = useLingui();
|
||||
const { copyToClipboard } = useCopyToClipboard();
|
||||
const [authMethod, setAuthMethod] = useState<McpAuthMethod>('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 = () => {
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`MCP Server`}
|
||||
description={t`Access your workspace data from your favorite MCP client like Claude Desktop, Claude Code, Cursor, or ChatGPT. Once connected, try: "Show me the 5 most recently created companies" or "Create a new person named Jane Doe".`}
|
||||
description={t`Access your workspace data from your favorite MCP client like Claude Desktop, Windsurf or Cursor.`}
|
||||
/>
|
||||
<Card rounded>
|
||||
<SettingsOptionCardContentSelect
|
||||
Icon={IconPlug}
|
||||
title={t`Authentication method`}
|
||||
description={t`OAuth or API Key`}
|
||||
>
|
||||
<Select
|
||||
dropdownId="mcp-auth-method-select"
|
||||
value={authMethod}
|
||||
onChange={(value) => setAuthMethod(value as McpAuthMethod)}
|
||||
options={[
|
||||
{ label: t`OAuth`, value: 'oauth' },
|
||||
{ label: t`API Key`, value: 'api-key' },
|
||||
]}
|
||||
selectSizeVariant="small"
|
||||
dropdownWidth={GenericDropdownContentWidth.Medium}
|
||||
<StyledCoverCardContent divider>
|
||||
<StyledCoverImage
|
||||
style={{ backgroundImage: `url('${coverImage}')` }}
|
||||
/>
|
||||
</SettingsOptionCardContentSelect>
|
||||
<StyledConfigWrapper>
|
||||
</StyledCoverCardContent>
|
||||
<StyledCoverCardContent>
|
||||
<StyledEditorContainer style={{ position: 'relative' }}>
|
||||
<StyledCopyButton>
|
||||
<Button
|
||||
<StyledConfigButtonsContainer>
|
||||
<Select
|
||||
dropdownId="mcp-auth-method-select"
|
||||
value={authMethod}
|
||||
onChange={(value) => 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 }}
|
||||
/>
|
||||
<StyledCopyButton
|
||||
Icon={IconCopy}
|
||||
onClick={() => {
|
||||
copyToClipboard(
|
||||
@@ -135,17 +152,17 @@ export const SettingsAIMCP = () => {
|
||||
t`MCP Configuration copied to clipboard`,
|
||||
);
|
||||
}}
|
||||
type="button"
|
||||
size="small"
|
||||
/>
|
||||
</StyledCopyButton>
|
||||
</StyledConfigButtonsContainer>
|
||||
<CodeEditor
|
||||
value={activeConfig}
|
||||
language="application/json"
|
||||
language="json"
|
||||
options={codeEditorOptions}
|
||||
height={editorHeight}
|
||||
/>
|
||||
</StyledEditorContainer>
|
||||
</StyledConfigWrapper>
|
||||
</StyledCoverCardContent>
|
||||
</Card>
|
||||
</Section>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Workspace Instructions`}
|
||||
description={t`Add custom instructions specific to your workspace (appended to system prompt)`}
|
||||
/>
|
||||
<StyledFormContainer>
|
||||
<FormAdvancedTextFieldInput
|
||||
key={originalInstructions}
|
||||
readonly={false}
|
||||
defaultValue={workspaceInstructions}
|
||||
contentType="markdown"
|
||||
onChange={handleWorkspaceInstructionsChange}
|
||||
enableFullScreen={true}
|
||||
fullScreenBreadcrumbs={[
|
||||
{
|
||||
children: t`System Prompt`,
|
||||
href: '#',
|
||||
},
|
||||
{
|
||||
children: t`Workspace Instructions`,
|
||||
},
|
||||
]}
|
||||
placeholder={t`E.g., "We are a B2B SaaS company. Always use formal language..."`}
|
||||
minHeight={150}
|
||||
maxWidth={700}
|
||||
/>
|
||||
</StyledFormContainer>
|
||||
</Section>
|
||||
<SettingsAIMCP />
|
||||
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`System Prompt`}
|
||||
description={systemPromptDescription}
|
||||
/>
|
||||
|
||||
<UndecoratedLink to={getSettingsPath(SettingsPath.AIPrompts)}>
|
||||
<SettingsCard
|
||||
Icon={<IconPrompt size={theme.icon.size.md} />}
|
||||
title={t`Read system prompts`}
|
||||
/>
|
||||
</UndecoratedLink>
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user