[AI] agent node prompt tab new design + refactor (#19012)

This commit is contained in:
nitin
2026-04-02 13:54:58 +05:30
committed by GitHub
parent 19c710b87f
commit ade6ed9c32
28 changed files with 786 additions and 475 deletions
@@ -1,17 +1,23 @@
import { type VariablePickerComponent } from '@/object-record/record-field/ui/form-types/types/VariablePickerComponent';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { WorkflowVariablesDropdown } from '@/workflow/workflow-variables/components/WorkflowVariablesDropdown';
import { SEARCH_VARIABLES_DROPDOWN_ID } from '@/workflow/workflow-variables/constants/SearchVariablesDropdownId';
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledSearchVariablesDropdownContainer = styled.div<{
multiline?: boolean;
isReadonly?: boolean;
isUnfolded?: boolean;
multiline?: boolean;
}>`
align-items: center;
background-color: ${({ multiline }) =>
multiline
? 'transparent'
: themeCssVariables.background.transparent.lighter};
background-color: ${({ isUnfolded, multiline }) =>
isUnfolded
? themeCssVariables.background.transparent.light
: multiline
? 'transparent'
: themeCssVariables.background.transparent.lighter};
border: ${({ multiline }) =>
multiline ? 'none' : `1px solid ${themeCssVariables.border.color.medium}`};
@@ -21,28 +27,31 @@ const StyledSearchVariablesDropdownContainer = styled.div<{
: `0 ${themeCssVariables.border.radius.sm} ${themeCssVariables.border.radius.sm} 0`};
display: flex;
height: ${({ multiline }) =>
multiline ? themeCssVariables.spacing[7] : 'auto'};
justify-content: center;
padding: ${({ multiline }) =>
multiline
? `${themeCssVariables.spacing[0.5]} ${themeCssVariables.spacing[0]}`
: '0'};
margin: ${({ multiline }) =>
multiline ? `${themeCssVariables.spacing[1]}` : '0'};
position: ${({ multiline }) => (multiline ? 'absolute' : 'static')};
right: ${({ multiline }) =>
multiline ? themeCssVariables.spacing[0] : 'auto'};
top: ${({ multiline }) =>
multiline ? themeCssVariables.spacing[0] : 'auto'};
width: ${({ multiline }) =>
multiline ? themeCssVariables.spacing[7] : 'auto'};
&:hover {
background-color: ${({ isReadonly, multiline }) => {
background-color: ${({ isReadonly, isUnfolded, multiline }) => {
if (isReadonly === true) {
return multiline
? 'transparent'
: themeCssVariables.background.transparent.lighter;
}
return themeCssVariables.background.transparent.light;
return isUnfolded
? themeCssVariables.background.transparent.medium
: themeCssVariables.background.transparent.light;
}};
}
`;
@@ -55,10 +64,17 @@ export const WorkflowVariablePicker: VariablePickerComponent = ({
shouldDisplayRecordObjects = false,
shouldDisplayRecordFields = true,
}) => {
const dropdownId = `${SEARCH_VARIABLES_DROPDOWN_ID}-${instanceId}`;
const isDropdownOpen = useAtomComponentStateValue(
isDropdownOpenComponentState,
dropdownId,
);
return (
<StyledSearchVariablesDropdownContainer
multiline={multiline}
isReadonly={disabled}
isUnfolded={isDropdownOpen}
multiline={multiline}
>
<WorkflowVariablesDropdown
instanceId={instanceId}
@@ -66,7 +82,6 @@ export const WorkflowVariablePicker: VariablePickerComponent = ({
disabled={disabled}
shouldDisplayRecordObjects={shouldDisplayRecordObjects}
shouldDisplayRecordFields={shouldDisplayRecordFields}
multiline={multiline}
/>
</StyledSearchVariablesDropdownContainer>
);
@@ -1,11 +1,9 @@
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { type InputSchemaPropertyType } from 'twenty-shared/workflow';
import { WorkflowVariablesDropdownStepItems } from '@/workflow/workflow-variables/components/WorkflowVariablesDropdownStepItems';
import { WorkflowVariablesDropdownSteps } from '@/workflow/workflow-variables/components/WorkflowVariablesDropdownSteps';
import { SEARCH_VARIABLES_DROPDOWN_ID } from '@/workflow/workflow-variables/constants/SearchVariablesDropdownId';
import { type InputSchemaPropertyType } from 'twenty-shared/workflow';
import { useAvailableVariablesInWorkflowStep } from '@/workflow/workflow-variables/hooks/useAvailableVariablesInWorkflowStep';
import { type StepOutputSchemaV2 } from '@/workflow/workflow-variables/types/StepOutputSchemaV2';
@@ -13,59 +11,42 @@ import { styled } from '@linaria/react';
import { useContext, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { IconVariablePlus } from 'twenty-ui/display';
import { themeCssVariables, ThemeContext } from 'twenty-ui/theme-constants';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
const StyledDropdownVariableButtonContainer = styled.div<{
isUnfolded?: boolean;
transparentBackground?: boolean;
disabled?: boolean;
}>`
align-items: center;
background-color: ${({ transparentBackground }) =>
transparentBackground
? 'transparent'
: themeCssVariables.background.transparent.lighter};
border-radius: ${themeCssVariables.border.radius.sm};
background-color: transparent;
border-bottom-right-radius: ${themeCssVariables.border.radius.sm};
border-top-right-radius: ${themeCssVariables.border.radius.sm};
color: ${themeCssVariables.font.color.tertiary};
cursor: pointer;
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: flex;
justify-content: center;
padding: ${themeCssVariables.spacing[2]};
user-select: none;
&:hover {
background: ${({ isUnfolded, transparentBackground }) =>
transparentBackground
? 'transparent'
: isUnfolded
? themeCssVariables.background.transparent.medium
: themeCssVariables.background.transparent.light};
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
}
`;
export const WorkflowVariablesDropdown = ({
clickableComponent,
disabled,
fieldTypesToExclude,
instanceId,
onVariableSelect,
disabled,
shouldDisplayRecordFields,
shouldDisplayRecordObjects,
fieldTypesToExclude,
multiline,
clickableComponent,
}: {
clickableComponent?: React.ReactNode;
disabled?: boolean;
fieldTypesToExclude?: InputSchemaPropertyType[];
instanceId: string;
onVariableSelect: (variableName: string) => void;
shouldDisplayRecordFields: boolean;
shouldDisplayRecordObjects: boolean;
fieldTypesToExclude?: InputSchemaPropertyType[];
disabled?: boolean;
multiline?: boolean;
clickableComponent?: React.ReactNode;
}) => {
const { theme } = useContext(ThemeContext);
const dropdownId = `${SEARCH_VARIABLES_DROPDOWN_ID}-${instanceId}`;
const isDropdownOpen = useAtomComponentStateValue(
isDropdownOpenComponentState,
dropdownId,
);
const { closeDropdown } = useCloseDropdown();
const availableVariablesInWorkflowStep = useAvailableVariablesInWorkflowStep({
shouldDisplayRecordFields,
@@ -102,13 +83,9 @@ export const WorkflowVariablesDropdown = ({
if (disabled === true || noAvailableVariables) {
return (
<StyledDropdownVariableButtonContainer
isUnfolded={isDropdownOpen}
disabled={true}
transparentBackground
>
<StyledDropdownVariableButtonContainer disabled={true}>
<IconVariablePlus
size={theme.icon.size.sm}
size={theme.icon.size.md}
color={theme.font.color.light}
/>
</StyledDropdownVariableButtonContainer>
@@ -121,11 +98,8 @@ export const WorkflowVariablesDropdown = ({
isDropdownInModal={true}
clickableComponent={
clickableComponent ?? (
<StyledDropdownVariableButtonContainer
isUnfolded={isDropdownOpen}
transparentBackground
>
<IconVariablePlus size={theme.icon.size.sm} />
<StyledDropdownVariableButtonContainer>
<IconVariablePlus size={theme.icon.size.md} />
</StyledDropdownVariableButtonContainer>
)
}
@@ -147,8 +121,7 @@ export const WorkflowVariablesDropdown = ({
}
dropdownPlacement="bottom-end"
dropdownOffset={{
x: 2,
y: parseInt(theme.spacing[multiline ? 11 : 1], 10),
y: parseInt(theme.spacing[1], 10),
}}
/>
);
@@ -44,12 +44,29 @@ export const useStepsOutputSchema = () => {
return;
}
// TODO: Remove this fallback after upgrade command
// `upgrade:1-21:migrate-ai-agent-text-to-json-response-format`
// has run on all workspaces.
const persistedOutputSchema =
step.type === 'AI_AGENT' &&
(!isDefined(step.settings?.outputSchema) ||
Object.keys(step.settings.outputSchema).length === 0)
? {
response: {
isLeaf: true,
type: 'string',
label: 'Response',
value: null,
},
}
: step.settings?.outputSchema;
const outputSchema = shouldComputeOnFrontend
? computeStepOutputSchema({
step,
objectMetadataItems,
})
: step.settings?.outputSchema;
: persistedOutputSchema;
const stepOutputSchema: StepOutputSchemaV2 = {
id: step.id,
@@ -409,20 +409,13 @@ describe('computeStepOutputSchema', () => {
});
describe('AI_AGENT step', () => {
it('should return response schema', () => {
it('should return undefined for AI_AGENT step type', () => {
const result = computeStepOutputSchema({
step: { type: 'AI_AGENT', settings: {} } as any,
objectMetadataItems: [],
});
expect(result).toEqual({
response: {
isLeaf: true,
type: FieldMetadataType.TEXT,
label: 'Response',
value: null,
},
});
expect(result).toBeUndefined();
});
});
@@ -461,8 +454,8 @@ describe('shouldComputeOutputSchemaOnFrontend', () => {
expect(shouldComputeOutputSchemaOnFrontend('HTTP_REQUEST')).toBe(false);
});
it('should return true for AI_AGENT', () => {
expect(shouldComputeOutputSchemaOnFrontend('AI_AGENT')).toBe(true);
it('should return false for AI_AGENT', () => {
expect(shouldComputeOutputSchemaOnFrontend('AI_AGENT')).toBe(false);
});
it('should return false for WEBHOOK', () => {
@@ -14,6 +14,7 @@ import { isDefined } from 'twenty-shared/utils';
import { DatabaseEventAction } from '~/generated-metadata/graphql';
const PERSISTED_OUTPUT_SCHEMA_TYPES = [
'AI_AGENT',
'CODE',
'HTTP_REQUEST',
'WEBHOOK',
@@ -190,17 +191,6 @@ export const computeStepOutputSchema = ({
return generateFormOutputSchema(formFields, objectMetadataItems);
}
case 'AI_AGENT': {
return {
response: {
isLeaf: true,
type: FieldMetadataType.TEXT,
label: 'Response',
value: null,
},
};
}
case 'SEND_EMAIL':
case 'DRAFT_EMAIL': {
return {