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:
@ -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';
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
@ -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';
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -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,
|
||||
);
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -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} />;
|
||||
};
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
@ -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';
|
||||
@ -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,
|
||||
});
|
||||
@ -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[]
|
||||
@ -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<
|
||||
@ -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,
|
||||
});
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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} />],
|
||||
},
|
||||
};
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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);
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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}
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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,
|
||||
}) ?? ''
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
),
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -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`
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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';
|
||||
|
||||
Reference in New Issue
Block a user