feat(ai): add current context to ai chat (#13315)

## TODO

- [ ] add dropdown to use records from outside the context
- [x] add loader for files chip
- [x] add roleId where it's necessary
- [x] Split AvatarChip in two components. One with the icon that will
call the second with leftComponent.
- [ ] Fix tests
- [x] Fix UI regression on Search
This commit is contained in:
Antoine Moreaux
2025-07-22 17:27:19 +02:00
committed by GitHub
parent d46a076aa0
commit 153739b9c3
69 changed files with 1111 additions and 819 deletions

View File

@ -3,8 +3,8 @@ import styled from '@emotion/styled';
import { Avatar, IconDotsVertical, IconSparkles } from 'twenty-ui/display';
import { LightCopyIconButton } from '@/object-record/record-field/components/LightCopyIconButton';
import { AgentChatFilePreview } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/components/AgentChatFilePreview';
import { AgentChatMessageRole } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/constants/agent-chat-message-role';
import { AgentChatFilePreview } from '@/ai/components/internal/AgentChatFilePreview';
import { AgentChatMessageRole } from '@/ai/constants/agent-chat-message-role';
import { AgentChatMessage } from '~/generated/graphql';
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';

View File

@ -7,7 +7,7 @@ import { useCreateNewAIChatThread } from '@/ai/hooks/useCreateNewAIChatThread';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { AgentChatFileUpload } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/components/AgentChatFileUpload';
import { AgentChatFileUploadButton } from '@/ai/components/internal/AgentChatFileUploadButton';
import { AIChatEmptyState } from '@/ai/components/AIChatEmptyState';
import { AIChatMessage } from '@/ai/components/AIChatMessage';
@ -16,8 +16,12 @@ import { t } from '@lingui/core/macro';
import { useState } from 'react';
import { Button } from 'twenty-ui/input';
import { useAgentChat } from '../hooks/useAgentChat';
import { AIChatSkeletonLoader } from './AIChatSkeletonLoader';
import { AgentChatSelectedFilesPreview } from './AgentChatSelectedFilesPreview';
import { AIChatSkeletonLoader } from '@/ai/components/internal/AIChatSkeletonLoader';
import { AgentChatContextPreview } from '@/ai/components/internal/AgentChatContextPreview';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { SendMessageWithRecordsContextButton } from '@/ai/components/internal/SendMessageWithRecordsContextButton';
import { SendMessageButton } from '@/ai/components/internal/SendMessageButton';
const StyledContainer = styled.div<{ isDraggingFile: boolean }>`
background: ${({ theme }) => theme.background.primary};
@ -63,10 +67,13 @@ export const AIChatTab = ({
}) => {
const [isDraggingFile, setIsDraggingFile] = useState(false);
const contextStoreCurrentObjectMetadataItemId = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataItemIdComponentState,
);
const {
messages,
isLoading,
handleSendMessage,
input,
handleInputChange,
agentStreamingMessage,
@ -105,7 +112,7 @@ export const AIChatTab = ({
{isLoading && messages.length === 0 && <AIChatSkeletonLoader />}
<StyledInputArea>
<AgentChatSelectedFilesPreview agentId={agentId} />
<AgentChatContextPreview agentId={agentId} />
<TextArea
textAreaId={`${agentId}-chat-input`}
placeholder={t`Enter a question...`}
@ -135,16 +142,12 @@ export const AIChatTab = ({
/>
</>
)}
<AgentChatFileUpload agentId={agentId} />
<Button
variant="primary"
accent="blue"
size="small"
hotkeys={input && !isLoading ? ['⏎'] : undefined}
disabled={!input || isLoading}
title={t`Send`}
onClick={handleSendMessage}
/>
<AgentChatFileUploadButton agentId={agentId} />
{contextStoreCurrentObjectMetadataItemId ? (
<SendMessageWithRecordsContextButton agentId={agentId} />
) : (
<SendMessageButton agentId={agentId} />
)}
</StyledButtonsContainer>
</StyledInputArea>
</>

View File

@ -5,7 +5,7 @@ import { AIChatThreadsListEffect } from '@/ai/components/AIChatThreadsListEffect
import { useCreateNewAIChatThread } from '@/ai/hooks/useCreateNewAIChatThread';
import { groupThreadsByDate } from '@/ai/utils/groupThreadsByDate';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { AIChatSkeletonLoader } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/components/AIChatSkeletonLoader';
import { AIChatSkeletonLoader } from '@/ai/components/internal/AIChatSkeletonLoader';
import { Key } from 'ts-key-enum';
import { capitalize } from 'twenty-shared/utils';
import { Button } from 'twenty-ui/input';

View File

@ -0,0 +1,93 @@
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { agentChatUploadedFilesComponentState } from '@/ai/states/agentChatUploadedFilesComponentState';
import styled from '@emotion/styled';
import { AgentChatFilePreview } from './AgentChatFilePreview';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { useLingui } from '@lingui/react/macro';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { agentChatSelectedFilesComponentState } from '@/ai/states/agentChatSelectedFilesComponentState';
import { useDeleteFileMutation } from '~/generated-metadata/graphql';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { AgentChatContextRecordPreview } from '@/ai/components/internal/AgentChatContextRecordPreview';
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(1)};
width: 100%;
`;
const StyledPreviewsContainer = styled.div`
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: ${({ theme }) => theme.spacing(1)};
`;
export const AgentChatContextPreview = ({ agentId }: { agentId: string }) => {
const { t } = useLingui();
const [agentChatSelectedFiles, setAgentChatSelectedFiles] =
useRecoilComponentStateV2(agentChatSelectedFilesComponentState, agentId);
const [agentChatUploadedFiles, setAgentChatUploadedFiles] =
useRecoilComponentStateV2(agentChatUploadedFilesComponentState, agentId);
const { enqueueErrorSnackBar } = useSnackBar();
const [deleteFile] = useDeleteFileMutation();
const handleRemoveUploadedFile = async (fileId: string) => {
const originalFiles = agentChatUploadedFiles;
setAgentChatUploadedFiles(
agentChatUploadedFiles.filter((f) => f.id !== fileId),
);
try {
await deleteFile({ variables: { fileId } });
} catch (error) {
setAgentChatUploadedFiles(originalFiles);
enqueueErrorSnackBar({
message: t`Failed to remove file`,
});
}
};
const contextStoreCurrentObjectMetadataItemId = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataItemIdComponentState,
);
return (
<StyledContainer>
<StyledPreviewsContainer>
{agentChatSelectedFiles.map((file) => (
<AgentChatFilePreview
file={file}
key={file.name}
onRemove={() => {
setAgentChatSelectedFiles(
agentChatSelectedFiles.filter((f) => f.name !== file.name),
);
}}
isUploading
/>
))}
{agentChatUploadedFiles.map((file) => (
<AgentChatFilePreview
file={file}
key={file.id}
onRemove={() => handleRemoveUploadedFile(file.id)}
isUploading={false}
/>
))}
{contextStoreCurrentObjectMetadataItemId && (
<AgentChatContextRecordPreview
agentId={agentId}
contextStoreCurrentObjectMetadataItemId={
contextStoreCurrentObjectMetadataItemId
}
/>
)}
</StyledPreviewsContainer>
</StyledContainer>
);
};

View File

@ -0,0 +1,104 @@
import { useFindManyRecordsSelectedInContextStore } from '@/context-store/hooks/useFindManyRecordsSelectedInContextStore';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { isAgentChatCurrentContextActiveState } from '@/ai/states/isAgentChatCurrentContextActiveState';
import { MultipleAvatarChip } from 'twenty-ui/components';
import { t } from '@lingui/core/macro';
import { IconReload, IconX } from 'twenty-ui/display';
import { CommandMenuContextRecordChipAvatars } from '@/command-menu/components/CommandMenuContextRecordChipAvatars';
import { getSelectedRecordsContextText } from '@/command-menu/utils/getRecordContextText';
import styled from '@emotion/styled';
import { useTheme } from '@emotion/react';
const StyledRightIconContainer = styled.div`
display: flex;
border-left: 1px solid ${({ theme }) => theme.border.color.light};
svg {
cursor: pointer;
}
`;
const StyledChipWrapper = styled.div<{ isActive: boolean }>`
opacity: ${({ isActive }) => (isActive ? 1 : 0.7)};
`;
export const AgentChatContextRecordPreview = ({
agentId,
contextStoreCurrentObjectMetadataItemId,
}: {
agentId: string;
contextStoreCurrentObjectMetadataItemId: string;
}) => {
const theme = useTheme();
const { records, totalCount } = useFindManyRecordsSelectedInContextStore({
limit: 3,
});
const { objectMetadataItem } = useObjectMetadataItemById({
objectId: contextStoreCurrentObjectMetadataItemId,
});
const [isAgentChatCurrentContextActive, setIsAgentChatCurrentContextActive] =
useRecoilComponentStateV2(isAgentChatCurrentContextActiveState, agentId);
const Avatars = records.map((record) => (
// @todo move this components to be less specific. (Outside of CommandMenu
<CommandMenuContextRecordChipAvatars
objectMetadataItem={objectMetadataItem}
key={record.id}
record={record}
/>
));
const recordSelectionContextChip = {
// @todo move this utils outside of CommandMenu
text: getSelectedRecordsContextText(
objectMetadataItem,
records,
totalCount ?? 0,
),
Icons: Avatars,
withIconBackground: false,
};
const toggleIsAgentChatCurrentContextActive = () => {
setIsAgentChatCurrentContextActive(!isAgentChatCurrentContextActive);
};
return (
<>
{records.length !== 0 && (
<StyledChipWrapper isActive={isAgentChatCurrentContextActive}>
<MultipleAvatarChip
Icons={recordSelectionContextChip.Icons}
text={
isAgentChatCurrentContextActive
? recordSelectionContextChip.text
: t`Context`
}
maxWidth={180}
rightComponent={
<StyledRightIconContainer>
{isAgentChatCurrentContextActive ? (
<IconX
size={theme.icon.size.sm}
color={theme.font.color.secondary}
onClick={toggleIsAgentChatCurrentContextActive}
/>
) : (
<IconReload
size={theme.icon.size.sm}
color={theme.font.color.secondary}
onClick={toggleIsAgentChatCurrentContextActive}
/>
)}
</StyledRightIconContainer>
}
/>
</StyledChipWrapper>
)}
</>
);
};

View File

@ -0,0 +1,47 @@
import { getFileType } from '@/activities/files/utils/getFileType';
import { IconMapping, useFileTypeColors } from '@/file/utils/fileIconMappings';
import { useTheme } from '@emotion/react';
import { File as FileDocument } from '~/generated-metadata/graphql';
import { Chip, ChipVariant, AvatarChip } from 'twenty-ui/components';
import { IconX } from 'twenty-ui/display';
import { Loader } from 'twenty-ui/feedback';
export const AgentChatFilePreview = ({
file,
onRemove,
isUploading,
}: {
file: File | FileDocument;
onRemove?: () => void;
isUploading?: boolean;
}) => {
const theme = useTheme();
const iconColors = useFileTypeColors();
return (
<Chip
label={file.name}
variant={ChipVariant.Static}
leftComponent={
isUploading ? (
<Loader color="yellow" />
) : (
<AvatarChip
Icon={IconMapping[getFileType(file.name)]}
IconBackgroundColor={iconColors[getFileType(file.name)]}
/>
)
}
rightComponent={
onRemove ? (
<AvatarChip
Icon={IconX}
IconColor={theme.font.color.secondary}
onClick={onRemove}
divider={'left'}
/>
) : undefined
}
/>
);
};

View File

@ -1,10 +1,10 @@
import { useAIChatFileUpload } from '@/ai/hooks/useAIChatFileUpload';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { agentChatSelectedFilesComponentState } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/states/agentChatSelectedFilesComponentState';
import { agentChatSelectedFilesComponentState } from '@/ai/states/agentChatSelectedFilesComponentState';
import styled from '@emotion/styled';
import React, { useRef } from 'react';
import { IconPaperclip } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
const StyledFileUploadContainer = styled.div`
display: flex;
@ -16,8 +16,8 @@ const StyledFileInput = styled.input`
display: none;
`;
export const AgentChatFileUpload = ({ agentId }: { agentId: string }) => {
const [, setAgentChatSelectedFiles] = useRecoilComponentStateV2(
export const AgentChatFileUploadButton = ({ agentId }: { agentId: string }) => {
const setAgentChatSelectedFiles = useSetRecoilComponentStateV2(
agentChatSelectedFilesComponentState,
agentId,
);

View File

@ -0,0 +1,29 @@
import { Button } from 'twenty-ui/input';
import { t } from '@lingui/core/macro';
import { useAgentChat } from '@/ai/hooks/useAgentChat';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
export const SendMessageButton = ({
records,
agentId,
}: {
agentId: string;
records?: ObjectRecord[];
}) => {
const { isLoading, handleSendMessage, input } = useAgentChat(
agentId,
records,
);
return (
<Button
variant="primary"
accent="blue"
size="small"
hotkeys={input && !isLoading ? ['⏎'] : undefined}
disabled={!input || isLoading}
title={t`Send`}
onClick={handleSendMessage}
/>
);
};

View File

@ -0,0 +1,14 @@
import { useFindManyRecordsSelectedInContextStore } from '@/context-store/hooks/useFindManyRecordsSelectedInContextStore';
import { SendMessageButton } from '@/ai/components/internal/SendMessageButton';
export const SendMessageWithRecordsContextButton = ({
agentId,
}: {
agentId: string;
}) => {
const { records } = useFindManyRecordsSelectedInContextStore({
limit: 10,
});
return <SendMessageButton records={records} agentId={agentId} />;
};

View File

@ -1,8 +1,8 @@
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { agentChatSelectedFilesComponentState } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/states/agentChatSelectedFilesComponentState';
import { agentChatUploadedFilesComponentState } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/states/agentChatUploadedFilesComponentState';
import { agentChatSelectedFilesComponentState } from '@/ai/states/agentChatSelectedFilesComponentState';
import { agentChatUploadedFilesComponentState } from '@/ai/states/agentChatUploadedFilesComponentState';
import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared/utils';
import {

View File

@ -8,10 +8,10 @@ import { currentAIChatThreadComponentState } from '@/ai/states/currentAIChatThre
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { STREAM_CHAT_QUERY } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/api/agent-chat-apollo.api';
import { AgentChatMessageRole } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/constants/agent-chat-message-role';
import { agentChatSelectedFilesComponentState } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/states/agentChatSelectedFilesComponentState';
import { agentChatUploadedFilesComponentState } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/states/agentChatUploadedFilesComponentState';
import { STREAM_CHAT_QUERY } from '@/ai/rest-api/agent-chat-apollo.api';
import { AgentChatMessageRole } from '@/ai/constants/agent-chat-message-role';
import { agentChatSelectedFilesComponentState } from '@/ai/states/agentChatSelectedFilesComponentState';
import { agentChatUploadedFilesComponentState } from '@/ai/states/agentChatUploadedFilesComponentState';
import { useApolloClient } from '@apollo/client';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
@ -24,20 +24,43 @@ import { agentChatInputState } from '../states/agentChatInputState';
import { agentChatMessagesComponentState } from '../states/agentChatMessagesComponentState';
import { agentStreamingMessageState } from '../states/agentStreamingMessageState';
import { parseAgentStreamingChunk } from '../utils/parseAgentStreamingChunk';
import {
agentChatObjectMetadataAndRecordContextState,
AIChatObjectMetadataAndRecordContext,
} from '@/ai/states/agentChatObjectMetadataAndRecordContextState';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { useGetObjectMetadataItemById } from '@/object-metadata/hooks/useGetObjectMetadataItemById';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isAgentChatCurrentContextActiveState } from '@/ai/states/isAgentChatCurrentContextActiveState';
type OptimisticMessage = AgentChatMessage & {
isPending: boolean;
};
export const useAgentChat = (agentId: string) => {
export const useAgentChat = (agentId: string, records?: ObjectRecord[]) => {
const apolloClient = useApolloClient();
const { enqueueErrorSnackBar } = useSnackBar();
const { getObjectMetadataItemById } = useGetObjectMetadataItemById();
const contextStoreCurrentObjectMetadataItemId = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataItemIdComponentState,
);
const isAgentChatCurrentContextActive = useRecoilComponentValueV2(
isAgentChatCurrentContextActiveState,
agentId,
);
const agentChatSelectedFiles = useRecoilComponentValueV2(
agentChatSelectedFilesComponentState,
agentId,
);
const [agentChatContext, setAgentChatContext] = useRecoilComponentStateV2(
agentChatObjectMetadataAndRecordContextState,
agentId,
);
const [currentThreadId, setCurrentThreadId] = useRecoilComponentStateV2(
currentAIChatThreadComponentState,
agentId,
@ -128,6 +151,21 @@ export const useAgentChat = (agentId: string) => {
setIsStreaming(true);
const recordIdsByObjectMetadataNameSingular = [];
if (
isAgentChatCurrentContextActive === true &&
isDefined(records) &&
isDefined(contextStoreCurrentObjectMetadataItemId)
) {
recordIdsByObjectMetadataNameSingular.push({
objectMetadataNameSingular: getObjectMetadataItemById(
contextStoreCurrentObjectMetadataItemId,
).nameSingular,
recordIds: records.map(({ id }) => id),
});
}
await apolloClient.query({
query: STREAM_CHAT_QUERY,
variables: {
@ -135,6 +173,8 @@ export const useAgentChat = (agentId: string) => {
threadId: currentThreadId,
userMessage: content,
fileIds: agentChatUploadedFiles.map((file) => file.id),
recordIdsByObjectMetadataNameSingular:
recordIdsByObjectMetadataNameSingular,
},
},
context: {
@ -200,6 +240,12 @@ export const useAgentChat = (agentId: string) => {
await sendChatMessage(content);
};
const handleSetContext = async (
items: Array<AIChatObjectMetadataAndRecordContext>,
) => {
setAgentChatContext(items);
};
useHotkeysOnFocusedElement({
keys: [Key.Enter],
callback: (event: KeyboardEvent) => {
@ -219,6 +265,8 @@ export const useAgentChat = (agentId: string) => {
handleInputChange: (value: string) => setAgentChatInput(value),
messages: agentChatMessages,
input: agentChatInput,
context: agentChatContext,
handleSetContext,
handleSendMessage,
isLoading,
agentStreamingMessage,

View File

@ -1,5 +1,5 @@
import { WorkflowAiAgentAction } from '@/workflow/types/Workflow';
import { OutputSchemaField } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/constants/output-field-type-options';
import { OutputSchemaField } from '@/ai/constants/output-field-type-options';
import { BaseOutputSchema } from '@/workflow/workflow-variables/types/StepOutputSchema';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';

View File

@ -0,0 +1,14 @@
import { AgentChatMessagesComponentInstanceContext } from '@/ai/states/agentChatMessagesComponentState';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export type AIChatObjectMetadataAndRecordContext = {
type: 'objectMetadataId' | 'recordId';
id: string;
};
export const agentChatObjectMetadataAndRecordContextState =
createComponentStateV2<Array<AIChatObjectMetadataAndRecordContext>>({
defaultValue: [],
key: 'agentChatObjectMetadataAndRecordContextState',
componentInstanceContext: AgentChatMessagesComponentInstanceContext,
});

View File

@ -1,5 +1,5 @@
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
import { AgentChatMessagesComponentInstanceContext } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/states/agentChatMessagesComponentState';
import { AgentChatMessagesComponentInstanceContext } from '@/ai/states/agentChatMessagesComponentState';
export const agentChatSelectedFilesComponentState = createComponentStateV2<
File[]

View File

@ -1,5 +1,5 @@
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
import { AgentChatMessagesComponentInstanceContext } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/states/agentChatMessagesComponentState';
import { AgentChatMessagesComponentInstanceContext } from '@/ai/states/agentChatMessagesComponentState';
import { File } from '~/generated-metadata/graphql';
export const agentChatUploadedFilesComponentState = createComponentStateV2<

View File

@ -0,0 +1,12 @@
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
export const IsAgentChatCurrentContextActiveInstanceContext =
createComponentInstanceContext();
export const isAgentChatCurrentContextActiveState =
createComponentStateV2<boolean>({
defaultValue: true,
key: 'isAgentChatCurrentContextActiveState',
componentInstanceContext: IsAgentChatCurrentContextActiveInstanceContext,
});

View File

@ -1,88 +0,0 @@
import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
import { Fragment } from 'react/jsx-runtime';
import { isDefined } from 'twenty-shared/utils';
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
const StyledChip = styled.button<{
withText: boolean;
maxWidth?: string;
onClick?: () => void;
}>`
all: unset;
align-items: center;
justify-content: center;
background: ${({ theme }) => theme.background.transparent.light};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.md};
box-sizing: border-box;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
height: ${({ theme }) => theme.spacing(6)};
/* If the chip has text, we add extra padding to have a more balanced design */
padding: 0
${({ theme, withText }) => (withText ? theme.spacing(2) : theme.spacing(1))};
font-family: inherit;
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.medium};
line-height: ${({ theme }) => theme.text.lineHeight.lg};
color: ${({ theme }) => theme.font.color.primary};
cursor: ${({ onClick }) => (isDefined(onClick) ? 'pointer' : 'default')};
&:hover {
background: ${({ onClick, theme }) =>
isDefined(onClick)
? theme.background.transparent.medium
: theme.background.transparent.light};
}
max-width: ${({ maxWidth }) => maxWidth};
`;
const StyledIconsContainer = styled.div`
align-items: center;
display: flex;
`;
const StyledEmptyText = styled.div`
color: ${({ theme }) => theme.font.color.tertiary};
`;
export type CommandMenuContextChipProps = {
Icons: React.ReactNode[];
text?: string;
onClick?: () => void;
testId?: string;
maxWidth?: string;
forceEmptyText?: boolean;
};
export const CommandMenuContextChip = ({
Icons,
text,
onClick,
testId,
maxWidth,
forceEmptyText = false,
}: CommandMenuContextChipProps) => {
return (
<StyledChip
withText={isNonEmptyString(text)}
onClick={onClick}
data-testid={testId}
maxWidth={maxWidth}
>
<StyledIconsContainer>
{Icons.map((Icon, index) => (
<Fragment key={index}>{Icon}</Fragment>
))}
</StyledIconsContainer>
{text?.trim?.() ? (
<OverflowingTextWithTooltip text={text} />
) : !forceEmptyText ? (
<StyledEmptyText>Untitled</StyledEmptyText>
) : (
''
)}
</StyledChip>
);
};

View File

@ -6,14 +6,14 @@ import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { isDefined } from 'twenty-shared/utils';
import { MenuItem } from 'twenty-ui/navigation';
import {
CommandMenuContextChip,
CommandMenuContextChipProps,
} from './CommandMenuContextChip';
MultipleAvatarChip,
MultipleAvatarChipProps,
} from 'twenty-ui/components';
export const CommandMenuContextChipGroups = ({
contextChips,
}: {
contextChips: CommandMenuContextChipProps[];
contextChips: MultipleAvatarChipProps[];
}) => {
const { closeDropdown } = useCloseDropdown();
@ -25,9 +25,9 @@ export const CommandMenuContextChipGroups = ({
return (
<>
{contextChips.map((chip, index) => (
<CommandMenuContextChip
<MultipleAvatarChip
key={index}
maxWidth={'180px'}
maxWidth={180}
Icons={chip.Icons}
text={chip.text}
onClick={chip.onClick}
@ -46,7 +46,7 @@ export const CommandMenuContextChipGroups = ({
{firstChips.length > 0 && (
<Dropdown
clickableComponent={
<CommandMenuContextChip
<MultipleAvatarChip
Icons={firstThreeChips.map((chip) => chip.Icons?.[0])}
onClick={() => {}}
text={`${firstChips.length}`}
@ -77,11 +77,11 @@ export const CommandMenuContextChipGroups = ({
)}
{isDefined(lastChip) && (
<CommandMenuContextChip
<MultipleAvatarChip
Icons={lastChip.Icons}
text={lastChip.text}
onClick={lastChip.onClick}
maxWidth={'180px'}
maxWidth={180}
/>
)}
</>

View File

@ -4,14 +4,14 @@ import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { getSelectedRecordsContextText } from '@/command-menu/utils/getRecordContextText';
import { useFindManyRecordsSelectedInContextStore } from '@/context-store/hooks/useFindManyRecordsSelectedInContextStore';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { CommandMenuContextChipProps } from './CommandMenuContextChip';
import { isDefined } from 'twenty-shared/utils';
import { MultipleAvatarChipProps } from 'twenty-ui/components';
export const CommandMenuContextChipGroupsWithRecordSelection = ({
contextChips,
objectMetadataItemId,
}: {
contextChips: CommandMenuContextChipProps[];
contextChips: MultipleAvatarChipProps[];
objectMetadataItemId: string;
}) => {
const { objectMetadataItem } = useObjectMetadataItemById({

View File

@ -1,8 +1,8 @@
import { CommandMenuContextChip } from '@/command-menu/components/CommandMenuContextChip';
import { CommandMenuContextRecordChipAvatars } from '@/command-menu/components/CommandMenuContextRecordChipAvatars';
import { getSelectedRecordsContextText } from '@/command-menu/utils/getRecordContextText';
import { useFindManyRecordsSelectedInContextStore } from '@/context-store/hooks/useFindManyRecordsSelectedInContextStore';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { MultipleAvatarChip } from 'twenty-ui/components';
export const CommandMenuContextRecordsChip = ({
objectMetadataItemId,
@ -34,7 +34,7 @@ export const CommandMenuContextRecordsChip = ({
));
return (
<CommandMenuContextChip
<MultipleAvatarChip
text={getSelectedRecordsContextText(
objectMetadataItem,
records,

View File

@ -1,4 +1,3 @@
import { CommandMenuContextChip } from '@/command-menu/components/CommandMenuContextChip';
import { CommandMenuContextChipGroups } from '@/command-menu/components/CommandMenuContextChipGroups';
import { CommandMenuContextChipGroupsWithRecordSelection } from '@/command-menu/components/CommandMenuContextChipGroupsWithRecordSelection';
import { CommandMenuTopBarInputFocusEffect } from '@/command-menu/components/CommandMenuTopBarInputFocusEffect';
@ -22,6 +21,7 @@ import { useLocation } from 'react-router-dom';
import { useRecoilState, useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { IconChevronLeft, IconX } from 'twenty-ui/display';
import { MultipleAvatarChip } from 'twenty-ui/components';
import { Button } from 'twenty-ui/input';
import { getOsControlSymbol, useIsMobile } from 'twenty-ui/utilities';
@ -122,7 +122,7 @@ export const CommandMenuTopBar = () => {
duration: backButtonAnimationDuration,
}}
>
<CommandMenuContextChip
<MultipleAvatarChip
Icons={[<IconChevronLeft size={theme.icon.size.sm} />]}
onClick={goBackFromCommandMenu}
testId="command-menu-go-back-button"

View File

@ -1,33 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import { CommandMenuContextChip } from '../CommandMenuContextChip';
import { ComponentDecorator } from 'twenty-ui/testing';
import { IconBuildingSkyscraper, IconUser } from 'twenty-ui/display';
const meta: Meta<typeof CommandMenuContextChip> = {
title: 'Modules/CommandMenu/CommandMenuContextChip',
component: CommandMenuContextChip,
decorators: [ComponentDecorator],
};
export default meta;
type Story = StoryObj<typeof CommandMenuContextChip>;
export const SingleIcon: Story = {
args: {
Icons: [<IconUser size={16} />],
text: 'Person',
},
};
export const MultipleIcons: Story = {
args: {
Icons: [<IconUser size={16} />, <IconBuildingSkyscraper size={16} />],
text: 'Person & Company',
},
};
export const IconsOnly: Story = {
args: {
Icons: [<IconUser size={16} />, <IconBuildingSkyscraper size={16} />],
},
};

View File

@ -1,5 +1,5 @@
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { AIChatTab } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/components/AIChatTab';
import { AIChatTab } from '@/ai/components/AIChatTab';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';

View File

@ -1,19 +1,8 @@
import { AttachmentType } from '@/activities/files/types/Attachment';
import { IconMapping, useFileTypeColors } from '@/file/utils/fileIconMappings';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import {
IconComponent,
IconFile,
IconFileText,
IconFileZip,
IconHeadphones,
IconPhoto,
IconPresentation,
IconTable,
IconVideo,
} from 'twenty-ui/display';
const StyledIconContainer = styled.div<{ background: string }>`
align-items: center;
background: ${({ background }) => background};
@ -25,35 +14,14 @@ const StyledIconContainer = styled.div<{ background: string }>`
padding: ${({ theme }) => theme.spacing(1.25)};
`;
const IconMapping: { [key in AttachmentType]: IconComponent } = {
Archive: IconFileZip,
Audio: IconHeadphones,
Image: IconPhoto,
Presentation: IconPresentation,
Spreadsheet: IconTable,
TextDocument: IconFileText,
Video: IconVideo,
Other: IconFile,
};
export const FileIcon = ({ fileType }: { fileType: AttachmentType }) => {
const theme = useTheme();
const IconColors: { [key in AttachmentType]: string } = {
Archive: theme.color.gray,
Audio: theme.color.pink,
Image: theme.color.yellow,
Presentation: theme.color.orange,
Spreadsheet: theme.color.turquoise,
TextDocument: theme.color.blue,
Video: theme.color.purple,
Other: theme.color.gray,
};
const iconColors = useFileTypeColors();
const Icon = IconMapping[fileType];
return (
<StyledIconContainer background={IconColors[fileType]}>
<StyledIconContainer background={iconColors[fileType]}>
{Icon && <Icon size={theme.icon.size.sm} />}
</StyledIconContainer>
);

View File

@ -0,0 +1,40 @@
import { AttachmentType } from '@/activities/files/types/Attachment';
import { useTheme } from '@emotion/react';
import {
IconComponent,
IconFile,
IconFileText,
IconFileZip,
IconHeadphones,
IconPhoto,
IconPresentation,
IconTable,
IconVideo,
} from 'twenty-ui/display';
export const IconMapping: { [key in AttachmentType]: IconComponent } = {
Archive: IconFileZip,
Audio: IconHeadphones,
Image: IconPhoto,
Presentation: IconPresentation,
Spreadsheet: IconTable,
TextDocument: IconFileText,
Video: IconVideo,
Other: IconFile,
};
const getIconColors = (theme: any): { [key in AttachmentType]: string } => ({
Archive: theme.color.gray,
Audio: theme.color.pink,
Image: theme.color.yellow,
Presentation: theme.color.orange,
Spreadsheet: theme.color.turquoise,
TextDocument: theme.color.blue,
Video: theme.color.purple,
Other: theme.color.gray,
});
export const useFileTypeColors = () => {
const theme = useTheme();
return getIconColors(theme);
};

View File

@ -0,0 +1,28 @@
import { useRecoilValue } from 'recoil';
import { CustomError } from '@/error-handler/CustomError';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { isDefined } from 'twenty-shared/utils';
export const useGetObjectMetadataItemById = () => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const getObjectMetadataItemById = (objectId: string) => {
const objectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) => objectMetadataItem.id === objectId,
);
if (!isDefined(objectMetadataItem)) {
throw new CustomError(
`Object metadata item not found for id ${objectId}`,
'OBJECT_METADATA_ITEM_NOT_FOUND',
);
}
return objectMetadataItem;
};
return {
getObjectMetadataItemById,
};
};

View File

@ -10,11 +10,11 @@ import { MouseEvent } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import {
Chip,
AvatarChip,
AvatarChipVariant,
ChipSize,
ChipVariant,
LinkAvatarChip,
LinkChip,
} from 'twenty-ui/components';
import { TriggerEventType } from 'twenty-ui/utilities';
@ -22,7 +22,7 @@ export type RecordChipProps = {
objectNameSingular: string;
record: ObjectRecord;
className?: string;
variant?: AvatarChipVariant;
variant?: ChipVariant.Highlighted | ChipVariant.Transparent;
forceDisableClick?: boolean;
maxWidth?: number;
to?: string | undefined;
@ -72,39 +72,42 @@ export const RecordChip = ({
// TODO temporary until we create a record show page for Workspaces members
const avatarChip = (
<AvatarChip
placeholder={recordChipData.name}
placeholderColorSeed={record.id}
avatarType={recordChipData.avatarType}
avatarUrl={recordChipData.avatarUrl ?? ''}
/>
);
if (
forceDisableClick ||
objectNameSingular === CoreObjectNameSingular.WorkspaceMember
) {
return (
<AvatarChip
<Chip
label={recordChipData.name}
size={size}
maxWidth={maxWidth}
placeholderColorSeed={record.id}
name={recordChipData.name}
avatarType={recordChipData.avatarType}
avatarUrl={recordChipData.avatarUrl ?? ''}
className={className}
variant={ChipVariant.Transparent}
leftComponent={avatarChip}
/>
);
}
return (
<LinkAvatarChip
<LinkChip
size={size}
maxWidth={maxWidth}
placeholderColorSeed={record.id}
name={recordChipData.name}
label={recordChipData.name}
isLabelHidden={isLabelHidden}
avatarType={recordChipData.avatarType}
avatarUrl={recordChipData.avatarUrl ?? ''}
leftComponent={avatarChip}
className={className}
variant={
variant ??
(!forceDisableClick
? AvatarChipVariant.Regular
: AvatarChipVariant.Transparent)
(!forceDisableClick ? ChipVariant.Highlighted : ChipVariant.Transparent)
}
to={to ?? getLinkToShowPage(objectNameSingular, record)}
onClick={handleCustomClick}

View File

@ -19,9 +19,9 @@ import styled from '@emotion/styled';
import { Dispatch, SetStateAction, useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { AvatarChipVariant } from 'twenty-ui/components';
import { IconEye, IconEyeOff } from 'twenty-ui/display';
import { Checkbox, CheckboxVariant, LightIconButton } from 'twenty-ui/input';
import { ChipVariant } from 'twenty-ui/components';
const StyledCompactIconContainer = styled.div`
align-items: center;
@ -80,7 +80,7 @@ export const RecordBoardCardHeader = ({
<RecordChip
objectNameSingular={objectMetadataItem.nameSingular}
record={record}
variant={AvatarChipVariant.Transparent}
variant={ChipVariant.Transparent}
maxWidth={150}
onClick={() => {
activateBoardCard({ rowIndex, columnIndex });

View File

@ -20,7 +20,7 @@ import { isNonEmptyString } from '@sniptt/guards';
import { useState } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { getImageAbsoluteURI, isDefined } from 'twenty-shared/utils';
import { AvatarChip } from 'twenty-ui/components';
import { Chip, AvatarChip } from 'twenty-ui/components';
import {
H2Title,
IconEyeShare,
@ -137,15 +137,19 @@ export const SettingsAdminWorkspaceContent = ({
Icon: IconHome,
label: t`Name`,
value: (
<AvatarChip
name={activeWorkspace?.name ?? ''}
avatarUrl={
getImageAbsoluteURI({
imageUrl: isNonEmptyString(activeWorkspace?.logo)
? activeWorkspace?.logo
: DEFAULT_WORKSPACE_LOGO,
baseUrl: REACT_APP_SERVER_BASE_URL,
}) ?? ''
<Chip
label={activeWorkspace?.name ?? ''}
leftComponent={
<AvatarChip
avatarUrl={
getImageAbsoluteURI({
imageUrl: isNonEmptyString(activeWorkspace?.logo)
? activeWorkspace?.logo
: DEFAULT_WORKSPACE_LOGO,
baseUrl: REACT_APP_SERVER_BASE_URL,
}) ?? ''
}
/>
}
/>
),

View File

@ -2,7 +2,7 @@ import { FieldActorValue } from '@/object-record/record-field/types/FieldMetadat
import { useMemo } from 'react';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import { AvatarChip } from 'twenty-ui/components';
import { Chip, AvatarChip } from 'twenty-ui/components';
import {
IconApi,
IconCalendar,
@ -71,13 +71,18 @@ export const ActorDisplay = ({
source === 'API' || source === 'IMPORT' || source === 'SYSTEM';
return (
<AvatarChip
placeholderColorSeed={workspaceMemberId ?? undefined}
name={name ?? ''}
avatarType={workspaceMemberId ? 'rounded' : 'squared'}
LeftIcon={LeftIcon}
avatarUrl={avatarUrl ?? undefined}
isIconInverted={isIconInverted}
<Chip
label={name ?? ''}
leftComponent={
<AvatarChip
placeholderColorSeed={workspaceMemberId ?? undefined}
avatarType={workspaceMemberId ? 'rounded' : 'squared'}
placeholder={name}
Icon={LeftIcon}
avatarUrl={avatarUrl ?? undefined}
isIconInverted={isIconInverted}
/>
}
/>
);
};

View File

@ -1,103 +0,0 @@
import { getFileType } from '@/activities/files/utils/getFileType';
import { FileIcon } from '@/file/components/FileIcon';
import { formatFileSize } from '@/file/utils/formatFileSize';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconX } from 'twenty-ui/display';
import { Loader } from 'twenty-ui/feedback';
import { File as FileDocument } from '~/generated-metadata/graphql';
const StyledFileChip = styled.div`
display: flex;
align-items: center;
background: ${({ theme }) => theme.background.transparent.lighter};
border-radius: ${({ theme }) => theme.border.radius.md};
height: 40px;
border: 1px solid ${({ theme }) => theme.border.color.light};
`;
const StyledFileIconContainer = styled.div`
align-items: center;
display: flex;
height: 100%;
justify-content: center;
padding-inline: ${({ theme }) => theme.spacing(2)};
overflow: hidden;
`;
const StyledFileInfo = styled.div`
height: 100%;
display: flex;
justify-content: center;
flex: 1;
min-width: 0;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(1)};
border-left: 1px solid ${({ theme }) => theme.border.color.light};
padding-inline: ${({ theme }) => theme.spacing(2)};
`;
const StyledFileName = styled.div`
color: ${({ theme }) => theme.font.color.secondary};
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 125px;
`;
const StyledFileSize = styled.div`
color: ${({ theme }) => theme.font.color.secondary};
font-size: ${({ theme }) => theme.font.size.sm};
`;
const StyledRemoveIconContainer = styled.div`
display: flex;
align-items: center;
justify-content: center;
height: 100%;
padding-inline: ${({ theme }) => theme.spacing(3)};
border-left: 1px solid ${({ theme }) => theme.border.color.light};
svg {
cursor: pointer;
}
`;
export const AgentChatFilePreview = ({
file,
onRemove,
isUploading,
}: {
file: File | FileDocument;
onRemove?: () => void;
isUploading?: boolean;
}) => {
const theme = useTheme();
return (
<StyledFileChip key={file.name}>
<StyledFileIconContainer>
{isUploading ? (
<Loader color="yellow" />
) : (
<FileIcon fileType={getFileType(file.name)} />
)}
</StyledFileIconContainer>
<StyledFileInfo>
<StyledFileName title={file.name}>{file.name}</StyledFileName>
<StyledFileSize>{formatFileSize(file.size)}</StyledFileSize>
</StyledFileInfo>
{onRemove && (
<StyledRemoveIconContainer>
<IconX
size={theme.icon.size.md}
color={theme.font.color.secondary}
onClick={onRemove}
/>
</StyledRemoveIconContainer>
)}
</StyledFileChip>
);
};

View File

@ -1,75 +0,0 @@
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { AgentChatFilePreview } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/components/AgentChatFilePreview';
import { agentChatSelectedFilesComponentState } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/states/agentChatSelectedFilesComponentState';
import { agentChatUploadedFilesComponentState } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/states/agentChatUploadedFilesComponentState';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useDeleteFileMutation } from '~/generated-metadata/graphql';
const StyledPreviewContainer = styled.div`
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: ${({ theme }) => theme.spacing(2)};
margin-bottom: ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
export const AgentChatSelectedFilesPreview = ({
agentId,
}: {
agentId: string;
}) => {
const { t } = useLingui();
const [agentChatSelectedFiles, setAgentChatSelectedFiles] =
useRecoilComponentStateV2(agentChatSelectedFilesComponentState, agentId);
const [agentChatUploadedFiles, setAgentChatUploadedFiles] =
useRecoilComponentStateV2(agentChatUploadedFilesComponentState, agentId);
const { enqueueErrorSnackBar } = useSnackBar();
const [deleteFile] = useDeleteFileMutation();
const handleRemoveUploadedFile = async (fileId: string) => {
const originalFiles = agentChatUploadedFiles;
setAgentChatUploadedFiles(
agentChatUploadedFiles.filter((f) => f.id !== fileId),
);
try {
await deleteFile({ variables: { fileId } });
} catch (error) {
setAgentChatUploadedFiles(originalFiles);
enqueueErrorSnackBar({
message: t`Failed to remove file`,
});
}
};
return [...agentChatSelectedFiles, ...agentChatUploadedFiles].length > 0 ? (
<StyledPreviewContainer>
{agentChatSelectedFiles.map((file) => (
<AgentChatFilePreview
file={file}
key={file.name}
onRemove={() => {
setAgentChatSelectedFiles(
agentChatSelectedFiles.filter((f) => f.name !== file.name),
);
}}
isUploading
/>
))}
{agentChatUploadedFiles.map((file) => (
<AgentChatFilePreview
file={file}
key={file.id}
onRemove={() => handleRemoveUploadedFile(file.id)}
isUploading={false}
/>
))}
</StyledPreviewContainer>
) : null;
};

View File

@ -6,7 +6,7 @@ import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/
import { WorkflowAiAgentAction } from '@/workflow/types/Workflow';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
import { AIChatTab } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/components/AIChatTab';
import { AIChatTab } from '@/ai/components/AIChatTab';
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
import { BaseOutputSchema } from '@/workflow/workflow-variables/types/StepOutputSchema';
@ -23,10 +23,10 @@ import {
WORKFLOW_AI_AGENT_TAB_LIST_COMPONENT_ID,
WorkflowAiAgentTabId,
} from '../constants/workflow-ai-agent-tabs';
import { useAgentRoleAssignment } from '../hooks/useAgentRoleAssignment';
import { useAgentUpdateFormState } from '../hooks/useAgentUpdateFormState';
import { useAiAgentOutputSchema } from '../hooks/useAiAgentOutputSchema';
import { useAiModelOptions } from '../hooks/useAiModelOptions';
import { useAgentRoleAssignment } from '@/ai/hooks/useAgentRoleAssignment';
import { useAgentUpdateFormState } from '@/ai/hooks/useAgentUpdateFormState';
import { useAiAgentOutputSchema } from '@/ai/hooks/useAiAgentOutputSchema';
import { useAiModelOptions } from '@/ai/hooks/useAiModelOptions';
import { WorkflowOutputSchemaBuilder } from './WorkflowOutputSchemaBuilder';
const StyledErrorMessage = styled.div`

View File

@ -1,7 +1,7 @@
import { Select } from '@/ui/input/components/Select';
import { InputSchemaPropertyType } from '@/workflow/types/InputSchema';
import { t } from '@lingui/core/macro';
import { OUTPUT_FIELD_TYPE_OPTIONS } from '../constants/output-field-type-options';
import { OUTPUT_FIELD_TYPE_OPTIONS } from '@/ai/constants/output-field-type-options';
type WorkflowOutputFieldTypeSelectorProps = {
value?: InputSchemaPropertyType;

View File

@ -3,7 +3,7 @@ import { FormTextFieldInput } from '@/object-record/record-field/form-types/comp
import { InputLabel } from '@/ui/input/components/InputLabel';
import { InputSchemaPropertyType } from '@/workflow/types/InputSchema';
import { OutputSchemaField } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/constants/output-field-type-options';
import { OutputSchemaField } from '@/ai/constants/output-field-type-options';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';