Fix AI chat streaming persistence when command menu closes (#14854)
Closes [#1583](https://github.com/twentyhq/core-team-issues/issues/1583) - Removed `messageId` column from `File` table and its references in code (unrelated to this PR) - Updated AI chat to use React Context API with persistent provider in `CommandMenuContainer` - Converted all AI chat component states to regular Recoil atoms (no instance context needed) --------- Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
@@ -14,12 +14,15 @@ import { AIChatMessage } from '@/ai/components/AIChatMessage';
|
||||
import { AIChatSkeletonLoader } from '@/ai/components/internal/AIChatSkeletonLoader';
|
||||
import { AgentChatContextPreview } from '@/ai/components/internal/AgentChatContextPreview';
|
||||
import { SendMessageButton } from '@/ai/components/internal/SendMessageButton';
|
||||
import { SendMessageWithRecordsContextButton } from '@/ai/components/internal/SendMessageWithRecordsContextButton';
|
||||
import { AI_CHAT_INPUT_ID } from '@/ai/constants/AiChatInputId';
|
||||
import { useAIChatFileUpload } from '@/ai/hooks/useAIChatFileUpload';
|
||||
import { type UIMessageWithMetadata } from '@/ai/types/UIMessageWithMetadata';
|
||||
import { useAgentChatContextOrThrow } from '@/ai/hooks/useAgentChatContextOrThrow';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useState } from 'react';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { useAgentChat } from '../hooks/useAgentChat';
|
||||
|
||||
const StyledContainer = styled.div<{ isDraggingFile: boolean }>`
|
||||
background: ${({ theme }) => theme.background.primary};
|
||||
@@ -56,13 +59,7 @@ const StyledButtonsContainer = styled.div`
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const AIChatTab = ({
|
||||
agentId,
|
||||
uiMessages,
|
||||
}: {
|
||||
agentId: string;
|
||||
uiMessages: UIMessageWithMetadata[];
|
||||
}) => {
|
||||
export const AIChatTab = ({ agentId }: { agentId: string }) => {
|
||||
const [isDraggingFile, setIsDraggingFile] = useState(false);
|
||||
|
||||
const {
|
||||
@@ -71,11 +68,14 @@ export const AIChatTab = ({
|
||||
handleInputChange,
|
||||
scrollWrapperId,
|
||||
messages,
|
||||
handleSendMessage,
|
||||
isStreaming,
|
||||
} = useAgentChat(agentId, uiMessages);
|
||||
const { uploadFiles } = useAIChatFileUpload({ agentId });
|
||||
} = useAgentChatContextOrThrow();
|
||||
|
||||
const contextStoreCurrentObjectMetadataItemId = useRecoilComponentValue(
|
||||
contextStoreCurrentObjectMetadataItemIdComponentState,
|
||||
);
|
||||
|
||||
const { uploadFiles } = useAIChatFileUpload();
|
||||
const { createAgentChatThread } = useCreateNewAIChatThread({ agentId });
|
||||
const { navigateCommandMenu } = useCommandMenu();
|
||||
|
||||
@@ -110,9 +110,9 @@ export const AIChatTab = ({
|
||||
{isLoading && messages.length === 0 && <AIChatSkeletonLoader />}
|
||||
|
||||
<StyledInputArea>
|
||||
<AgentChatContextPreview agentId={agentId} />
|
||||
<AgentChatContextPreview />
|
||||
<TextArea
|
||||
textAreaId={`${agentId}-chat-input`}
|
||||
textAreaId={AI_CHAT_INPUT_ID}
|
||||
placeholder={t`Enter a question...`}
|
||||
value={input}
|
||||
onChange={handleInputChange}
|
||||
@@ -138,13 +138,12 @@ export const AIChatTab = ({
|
||||
Icon={IconMessageCirclePlus}
|
||||
onClick={() => createAgentChatThread()}
|
||||
/>
|
||||
<AgentChatFileUploadButton agentId={agentId} />
|
||||
<SendMessageButton
|
||||
agentId={agentId}
|
||||
handleSendMessage={handleSendMessage}
|
||||
input={input}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<AgentChatFileUploadButton />
|
||||
{contextStoreCurrentObjectMetadataItemId ? (
|
||||
<SendMessageWithRecordsContextButton />
|
||||
) : (
|
||||
<SendMessageButton />
|
||||
)}
|
||||
</StyledButtonsContainer>
|
||||
</StyledInputArea>
|
||||
</>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { currentAIChatThreadComponentState } from '@/ai/states/currentAIChatThreadComponentState';
|
||||
import { currentAIChatThreadState } from '@/ai/states/currentAIChatThreadState';
|
||||
import { useOpenAskAIPageInCommandMenu } from '@/command-menu/hooks/useOpenAskAIPageInCommandMenu';
|
||||
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { IconSparkles } from 'twenty-ui/display';
|
||||
import { type AgentChatThread } from '~/generated-metadata/graphql';
|
||||
|
||||
@@ -69,18 +69,13 @@ const StyledThreadTitle = styled.div`
|
||||
export const AIChatThreadGroup = ({
|
||||
threads,
|
||||
title,
|
||||
agentId,
|
||||
}: {
|
||||
threads: AgentChatThread[];
|
||||
agentId: string;
|
||||
title: string;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
const theme = useTheme();
|
||||
const [, setCurrentThreadId] = useRecoilComponentState(
|
||||
currentAIChatThreadComponentState,
|
||||
agentId,
|
||||
);
|
||||
const [, setCurrentAIChatThread] = useRecoilState(currentAIChatThreadState);
|
||||
const { openAskAIPage } = useOpenAskAIPageInCommandMenu();
|
||||
|
||||
if (threads.length === 0) {
|
||||
@@ -94,7 +89,7 @@ export const AIChatThreadGroup = ({
|
||||
{threads.map((thread) => (
|
||||
<StyledThreadItem
|
||||
onClick={() => {
|
||||
setCurrentThreadId(thread.id);
|
||||
setCurrentAIChatThread(thread.id);
|
||||
openAskAIPage(thread.title);
|
||||
}}
|
||||
key={thread.id}
|
||||
|
||||
@@ -2,10 +2,10 @@ import styled from '@emotion/styled';
|
||||
|
||||
import { AIChatThreadGroup } from '@/ai/components/AIChatThreadGroup';
|
||||
import { AIChatThreadsListEffect } from '@/ai/components/AIChatThreadsListEffect';
|
||||
import { AIChatSkeletonLoader } from '@/ai/components/internal/AIChatSkeletonLoader';
|
||||
import { useCreateNewAIChatThread } from '@/ai/hooks/useCreateNewAIChatThread';
|
||||
import { groupThreadsByDate } from '@/ai/utils/groupThreadsByDate';
|
||||
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
|
||||
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';
|
||||
@@ -65,7 +65,6 @@ export const AIChatThreadsList = ({ agentId }: { agentId: string }) => {
|
||||
<AIChatThreadGroup
|
||||
key={title}
|
||||
title={capitalize(title)}
|
||||
agentId={agentId}
|
||||
threads={threads}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { AgentChatContext } from '@/ai/contexts/AgentChatContext';
|
||||
import { useAgentChat } from '@/ai/hooks/useAgentChat';
|
||||
import { useAgentChatData } from '@/ai/hooks/useAgentChatData';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { Suspense } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
|
||||
const AgentChatProviderContent = ({
|
||||
agentId,
|
||||
children,
|
||||
}: {
|
||||
agentId: string;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const { uiMessages, isLoading } = useAgentChatData(agentId);
|
||||
const chatState = useAgentChat(agentId, uiMessages);
|
||||
const combinedIsLoading = chatState.isLoading || isLoading;
|
||||
|
||||
return (
|
||||
<AgentChatContext.Provider
|
||||
value={{
|
||||
...chatState,
|
||||
isLoading: combinedIsLoading,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AgentChatContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const AgentChatProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
const agentId = currentWorkspace?.defaultAgent?.id;
|
||||
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
|
||||
|
||||
if (!isAiEnabled || !agentId) {
|
||||
return (
|
||||
<AgentChatContext.Provider value={null}>
|
||||
{children}
|
||||
</AgentChatContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<AgentChatProviderContent agentId={agentId}>
|
||||
{children}
|
||||
</AgentChatProviderContent>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import { AgentChatContext } from '@/ai/contexts/AgentChatContext';
|
||||
import { useAgentChat } from '@/ai/hooks/useAgentChat';
|
||||
import { useAgentChatData } from '@/ai/hooks/useAgentChatData';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { Suspense } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
const AgentChatProviderContent = ({
|
||||
agentId,
|
||||
children,
|
||||
}: {
|
||||
agentId: string;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const { uiMessages, isLoading } = useAgentChatData(agentId);
|
||||
const chatState = useAgentChat(agentId, uiMessages);
|
||||
const combinedIsLoading = chatState.isLoading || isLoading;
|
||||
|
||||
return (
|
||||
<AgentChatContext.Provider
|
||||
value={{
|
||||
...chatState,
|
||||
isLoading: combinedIsLoading,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AgentChatContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const AgentChatProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
const agentId = currentWorkspace?.defaultAgent?.id;
|
||||
|
||||
if (!agentId) {
|
||||
return (
|
||||
<AgentChatContext.Provider value={null}>
|
||||
{children}
|
||||
</AgentChatContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<AgentChatProviderContent agentId={agentId}>
|
||||
{children}
|
||||
</AgentChatProviderContent>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
import { AIChatSkeletonLoader } from '@/ai/components/internal/AIChatSkeletonLoader';
|
||||
import { type UIMessageWithMetadata } from '@/ai/types/UIMessageWithMetadata';
|
||||
import { lazy, Suspense } from 'react';
|
||||
|
||||
const AIChatTab = lazy(() =>
|
||||
import('./AIChatTab').then((module) => ({
|
||||
default: module.AIChatTab,
|
||||
})),
|
||||
);
|
||||
|
||||
export const LazyAIChatTab = ({
|
||||
agentId,
|
||||
uiMessages,
|
||||
}: {
|
||||
agentId: string;
|
||||
uiMessages: UIMessageWithMetadata[];
|
||||
}) => {
|
||||
return (
|
||||
<Suspense fallback={<AIChatSkeletonLoader />}>
|
||||
<AIChatTab agentId={agentId} uiMessages={uiMessages} />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
+10
-9
@@ -1,10 +1,10 @@
|
||||
import { AgentChatContextRecordPreview } from '@/ai/components/internal/AgentChatContextRecordPreview';
|
||||
import { agentChatSelectedFilesComponentState } from '@/ai/states/agentChatSelectedFilesComponentState';
|
||||
import { agentChatUploadedFilesComponentState } from '@/ai/states/agentChatUploadedFilesComponentState';
|
||||
import { agentChatSelectedFilesState } from '@/ai/states/agentChatSelectedFilesState';
|
||||
import { agentChatUploadedFilesState } from '@/ai/states/agentChatUploadedFilesState';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { AgentChatFilePreview } from './AgentChatFilePreview';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
@@ -21,11 +21,13 @@ const StyledPreviewsContainer = styled.div`
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
export const AgentChatContextPreview = ({ agentId }: { agentId: string }) => {
|
||||
const [agentChatSelectedFiles, setAgentChatSelectedFiles] =
|
||||
useRecoilComponentState(agentChatSelectedFilesComponentState, agentId);
|
||||
const [agentChatUploadedFiles, setAgentChatUploadedFiles] =
|
||||
useRecoilComponentState(agentChatUploadedFilesComponentState, agentId);
|
||||
export const AgentChatContextPreview = () => {
|
||||
const [agentChatSelectedFiles, setAgentChatSelectedFiles] = useRecoilState(
|
||||
agentChatSelectedFilesState,
|
||||
);
|
||||
const [agentChatUploadedFiles, setAgentChatUploadedFiles] = useRecoilState(
|
||||
agentChatUploadedFilesState,
|
||||
);
|
||||
|
||||
const handleRemoveUploadedFile = async (fileIndex: number) => {
|
||||
setAgentChatUploadedFiles(
|
||||
@@ -62,7 +64,6 @@ export const AgentChatContextPreview = ({ agentId }: { agentId: string }) => {
|
||||
))}
|
||||
{contextStoreCurrentObjectMetadataItemId && (
|
||||
<AgentChatContextRecordPreview
|
||||
agentId={agentId}
|
||||
contextStoreCurrentObjectMetadataItemId={
|
||||
contextStoreCurrentObjectMetadataItemId
|
||||
}
|
||||
|
||||
+2
-4
@@ -3,10 +3,10 @@ import { CommandMenuContextRecordChipAvatars } from '@/command-menu/components/C
|
||||
import { getSelectedRecordsContextText } from '@/command-menu/utils/getRecordContextText';
|
||||
import { useFindManyRecordsSelectedInContextStore } from '@/context-store/hooks/useFindManyRecordsSelectedInContextStore';
|
||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { MultipleAvatarChip } from 'twenty-ui/components';
|
||||
import { IconReload, IconX } from 'twenty-ui/display';
|
||||
|
||||
@@ -24,10 +24,8 @@ const StyledChipWrapper = styled.div<{ isActive: boolean }>`
|
||||
`;
|
||||
|
||||
export const AgentChatContextRecordPreview = ({
|
||||
agentId,
|
||||
contextStoreCurrentObjectMetadataItemId,
|
||||
}: {
|
||||
agentId: string;
|
||||
contextStoreCurrentObjectMetadataItemId: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
@@ -41,7 +39,7 @@ export const AgentChatContextRecordPreview = ({
|
||||
});
|
||||
|
||||
const [isAgentChatCurrentContextActive, setIsAgentChatCurrentContextActive] =
|
||||
useRecoilComponentState(isAgentChatCurrentContextActiveState, agentId);
|
||||
useRecoilState(isAgentChatCurrentContextActiveState);
|
||||
|
||||
const Avatars = records.map((record) => (
|
||||
// @todo move this components to be less specific. (Outside of CommandMenu
|
||||
|
||||
+6
-7
@@ -1,8 +1,8 @@
|
||||
import { useAIChatFileUpload } from '@/ai/hooks/useAIChatFileUpload';
|
||||
import { agentChatSelectedFilesComponentState } from '@/ai/states/agentChatSelectedFilesComponentState';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import { agentChatSelectedFilesState } from '@/ai/states/agentChatSelectedFilesState';
|
||||
import styled from '@emotion/styled';
|
||||
import React, { useRef } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { IconPaperclip } from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
|
||||
@@ -16,13 +16,12 @@ const StyledFileInput = styled.input`
|
||||
display: none;
|
||||
`;
|
||||
|
||||
export const AgentChatFileUploadButton = ({ agentId }: { agentId: string }) => {
|
||||
const setAgentChatSelectedFiles = useSetRecoilComponentState(
|
||||
agentChatSelectedFilesComponentState,
|
||||
agentId,
|
||||
export const AgentChatFileUploadButton = () => {
|
||||
const setAgentChatSelectedFiles = useSetRecoilState(
|
||||
agentChatSelectedFilesState,
|
||||
);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const { uploadFiles } = useAIChatFileUpload({ agentId });
|
||||
const { uploadFiles } = useAIChatFileUpload();
|
||||
|
||||
const handleFileInputChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
|
||||
@@ -1,25 +1,42 @@
|
||||
import { AI_CHAT_INPUT_ID } from '@/ai/constants/AiChatInputId';
|
||||
import { useAgentChatContextOrThrow } from '@/ai/hooks/useAgentChatContextOrThrow';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
|
||||
export const SendMessageButton = ({
|
||||
handleSendMessage,
|
||||
input,
|
||||
isLoading,
|
||||
records,
|
||||
}: {
|
||||
agentId: string;
|
||||
handleSendMessage: () => void;
|
||||
input: string;
|
||||
isLoading: boolean;
|
||||
records?: ObjectRecord[];
|
||||
}) => {
|
||||
const { handleSendMessage, isLoading, input } = useAgentChatContextOrThrow();
|
||||
|
||||
useHotkeysOnFocusedElement({
|
||||
keys: [Key.Enter],
|
||||
callback: (event: KeyboardEvent) => {
|
||||
if (!event.ctrlKey && !event.metaKey) {
|
||||
event.preventDefault();
|
||||
handleSendMessage(records);
|
||||
}
|
||||
},
|
||||
focusId: AI_CHAT_INPUT_ID,
|
||||
dependencies: [input, isLoading],
|
||||
options: {
|
||||
enableOnFormTags: true,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Button
|
||||
hotkeys={input && !isLoading ? ['⏎'] : undefined}
|
||||
onClick={() => handleSendMessage(records)}
|
||||
disabled={!input || isLoading}
|
||||
variant="primary"
|
||||
accent="blue"
|
||||
size="small"
|
||||
hotkeys={input && !isLoading ? ['⏎'] : undefined}
|
||||
disabled={!input || isLoading}
|
||||
title={t`Send`}
|
||||
onClick={handleSendMessage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { SendMessageButton } from '@/ai/components/internal/SendMessageButton';
|
||||
import { useFindManyRecordsSelectedInContextStore } from '@/context-store/hooks/useFindManyRecordsSelectedInContextStore';
|
||||
|
||||
export const SendMessageWithRecordsContextButton = () => {
|
||||
const { records } = useFindManyRecordsSelectedInContextStore({
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
return <SendMessageButton records={records} />;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export const AI_CHAT_INPUT_ID = 'ai-chat-input';
|
||||
@@ -0,0 +1,26 @@
|
||||
import { type AIChatObjectMetadataAndRecordContext } from '@/ai/states/agentChatContextState';
|
||||
import { type UIMessageWithMetadata } from '@/ai/types/UIMessageWithMetadata';
|
||||
import { createContext } from 'react';
|
||||
import { type ObjectRecord } from '../../object-record/types/ObjectRecord';
|
||||
|
||||
export type AgentChatContextValue = {
|
||||
messages: UIMessageWithMetadata[];
|
||||
isStreaming: boolean;
|
||||
isLoading: boolean;
|
||||
error?: Error;
|
||||
|
||||
input: string;
|
||||
handleInputChange: (value: string) => void;
|
||||
|
||||
handleSendMessage: (records?: ObjectRecord[]) => Promise<void>;
|
||||
handleSetContext: (
|
||||
items: Array<AIChatObjectMetadataAndRecordContext>,
|
||||
) => Promise<void>;
|
||||
|
||||
scrollWrapperId: string;
|
||||
context: Array<AIChatObjectMetadataAndRecordContext>;
|
||||
};
|
||||
|
||||
export const AgentChatContext = createContext<AgentChatContextValue | null>(
|
||||
null,
|
||||
);
|
||||
@@ -34,14 +34,6 @@ export const GET_AGENT_CHAT_MESSAGES = gql`
|
||||
providerMetadata
|
||||
createdAt
|
||||
}
|
||||
files {
|
||||
id
|
||||
name
|
||||
fullPath
|
||||
size
|
||||
type
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
import { agentChatSelectedFilesComponentState } from '@/ai/states/agentChatSelectedFilesComponentState';
|
||||
import { agentChatUploadedFilesComponentState } from '@/ai/states/agentChatUploadedFilesComponentState';
|
||||
import { agentChatSelectedFilesState } from '@/ai/states/agentChatSelectedFilesState';
|
||||
import { agentChatUploadedFilesState } from '@/ai/states/agentChatUploadedFilesState';
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { type FileUIPart } from 'ai';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { buildSignedPath, isDefined } from 'twenty-shared/utils';
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
import { useUploadFileMutation } from '~/generated-metadata/graphql';
|
||||
import { FileFolder } from '~/generated/graphql';
|
||||
|
||||
export const useAIChatFileUpload = ({ agentId }: { agentId: string }) => {
|
||||
export const useAIChatFileUpload = () => {
|
||||
const coreClient = useApolloCoreClient();
|
||||
const [uploadFile] = useUploadFileMutation({ client: coreClient });
|
||||
const { t } = useLingui();
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
const [agentChatSelectedFiles, setAgentChatSelectedFiles] =
|
||||
useRecoilComponentState(agentChatSelectedFilesComponentState, agentId);
|
||||
const [agentChatUploadedFiles, setAgentChatUploadedFiles] =
|
||||
useRecoilComponentState(agentChatUploadedFilesComponentState, agentId);
|
||||
const [agentChatSelectedFiles, setAgentChatSelectedFiles] = useRecoilState(
|
||||
agentChatSelectedFilesState,
|
||||
);
|
||||
const [agentChatUploadedFiles, setAgentChatUploadedFiles] = useRecoilState(
|
||||
agentChatUploadedFilesState,
|
||||
);
|
||||
|
||||
const sendFile = async (file: File): Promise<FileUIPart | null> => {
|
||||
try {
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
|
||||
import {
|
||||
type AIChatObjectMetadataAndRecordContext,
|
||||
agentChatObjectMetadataAndRecordContextState,
|
||||
} from '@/ai/states/agentChatObjectMetadataAndRecordContextState';
|
||||
import { agentChatSelectedFilesComponentState } from '@/ai/states/agentChatSelectedFilesComponentState';
|
||||
import { agentChatUploadedFilesComponentState } from '@/ai/states/agentChatUploadedFilesComponentState';
|
||||
import { currentAIChatThreadComponentState } from '@/ai/states/currentAIChatThreadComponentState';
|
||||
agentChatContextState,
|
||||
} from '@/ai/states/agentChatContextState';
|
||||
import { agentChatSelectedFilesState } from '@/ai/states/agentChatSelectedFilesState';
|
||||
import { agentChatUploadedFilesState } from '@/ai/states/agentChatUploadedFilesState';
|
||||
import { currentAIChatThreadState } from '@/ai/states/currentAIChatThreadState';
|
||||
import { isAgentChatCurrentContextActiveState } from '@/ai/states/isAgentChatCurrentContextActiveState';
|
||||
import { type UIMessageWithMetadata } from '@/ai/types/UIMessageWithMetadata';
|
||||
import { getTokenPair } from '@/apollo/utils/getTokenPair';
|
||||
import { useFindManyRecordsSelectedInContextStore } from '@/context-store/hooks/useFindManyRecordsSelectedInContextStore';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { useGetObjectMetadataItemById } from '@/object-metadata/hooks/useGetObjectMetadataItemById';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
|
||||
import { useScrollWrapperHTMLElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperHTMLElement';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useChat } from '@ai-sdk/react';
|
||||
import { DefaultChatTransport } from 'ai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -35,33 +32,22 @@ export const useAgentChat = (
|
||||
contextStoreCurrentObjectMetadataItemIdComponentState,
|
||||
);
|
||||
|
||||
const { records } = useFindManyRecordsSelectedInContextStore({
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
const isAgentChatCurrentContextActive = useRecoilComponentValue(
|
||||
const isAgentChatCurrentContextActive = useRecoilValue(
|
||||
isAgentChatCurrentContextActiveState,
|
||||
agentId,
|
||||
);
|
||||
|
||||
const agentChatSelectedFiles = useRecoilComponentValue(
|
||||
agentChatSelectedFilesComponentState,
|
||||
agentId,
|
||||
const agentChatSelectedFiles = useRecoilValue(agentChatSelectedFilesState);
|
||||
|
||||
const [agentChatContext, setAgentChatContext] = useRecoilState(
|
||||
agentChatContextState,
|
||||
);
|
||||
|
||||
const [agentChatContext, setAgentChatContext] = useRecoilComponentState(
|
||||
agentChatObjectMetadataAndRecordContextState,
|
||||
agentId,
|
||||
);
|
||||
const currentAIChatThread = useRecoilValue(currentAIChatThreadState);
|
||||
|
||||
const currentThreadId = useRecoilComponentValue(
|
||||
currentAIChatThreadComponentState,
|
||||
agentId,
|
||||
const [agentChatUploadedFiles, setAgentChatUploadedFiles] = useRecoilState(
|
||||
agentChatUploadedFilesState,
|
||||
);
|
||||
|
||||
const [agentChatUploadedFiles, setAgentChatUploadedFiles] =
|
||||
useRecoilComponentState(agentChatUploadedFilesComponentState, agentId);
|
||||
|
||||
const [agentChatInput, setAgentChatInput] =
|
||||
useRecoilState(agentChatInputState);
|
||||
|
||||
@@ -80,7 +66,7 @@ export const useAgentChat = (
|
||||
}),
|
||||
}),
|
||||
messages: uiMessages,
|
||||
id: currentThreadId as string,
|
||||
id: `${currentAIChatThread}-${uiMessages.length}`,
|
||||
onError: (error) => {
|
||||
enqueueErrorSnackBar({ message: error.message });
|
||||
},
|
||||
@@ -96,9 +82,16 @@ export const useAgentChat = (
|
||||
};
|
||||
|
||||
const isLoading =
|
||||
!currentThreadId || isStreaming || agentChatSelectedFiles.length > 0;
|
||||
!currentAIChatThread || isStreaming || agentChatSelectedFiles.length > 0;
|
||||
|
||||
const handleSendMessage = async (records?: ObjectRecord[]) => {
|
||||
if (agentChatInput.trim() === '' || isLoading === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const content = agentChatInput.trim();
|
||||
setAgentChatInput('');
|
||||
|
||||
const sendChatMessage = async (content: string) => {
|
||||
const recordIdsByObjectMetadataNameSingular = [];
|
||||
|
||||
if (
|
||||
@@ -121,7 +114,7 @@ export const useAgentChat = (
|
||||
},
|
||||
{
|
||||
body: {
|
||||
threadId: currentThreadId,
|
||||
threadId: currentAIChatThread,
|
||||
recordIdsByObjectMetadataNameSingular,
|
||||
},
|
||||
},
|
||||
@@ -131,36 +124,12 @@ export const useAgentChat = (
|
||||
setTimeout(scrollToBottom, 100);
|
||||
};
|
||||
|
||||
const handleSendMessage = async () => {
|
||||
if (agentChatInput.trim() === '' || isLoading === true) {
|
||||
return;
|
||||
}
|
||||
const content = agentChatInput.trim();
|
||||
setAgentChatInput('');
|
||||
await sendChatMessage(content);
|
||||
};
|
||||
|
||||
const handleSetContext = async (
|
||||
items: Array<AIChatObjectMetadataAndRecordContext>,
|
||||
) => {
|
||||
setAgentChatContext(items);
|
||||
};
|
||||
|
||||
useHotkeysOnFocusedElement({
|
||||
keys: [Key.Enter],
|
||||
callback: (event: KeyboardEvent) => {
|
||||
if (!event.ctrlKey && !event.metaKey) {
|
||||
event.preventDefault();
|
||||
handleSendMessage();
|
||||
}
|
||||
},
|
||||
focusId: `${agentId}-chat-input`,
|
||||
dependencies: [agentChatInput, isLoading],
|
||||
options: {
|
||||
enableOnFormTags: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
handleInputChange: (value: string) => setAgentChatInput(value),
|
||||
messages,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { AgentChatContext } from '@/ai/contexts/AgentChatContext';
|
||||
import { useContext } from 'react';
|
||||
|
||||
export const useAgentChatContextOrThrow = () => {
|
||||
const context = useContext(AgentChatContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'AgentChatContext not found. Please wrap your component tree with <AgentChatContextProvider> before using useAgentChatContextOrThrow().',
|
||||
);
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import { currentAIChatThreadState } from '@/ai/states/currentAIChatThreadState';
|
||||
import { mapDBMessagesToUIMessages } from '@/ai/utils/mapDBMessagesToUIMessages';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
useGetAgentChatMessagesQuery,
|
||||
useGetAgentChatThreadsQuery,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const useAgentChatData = (agentId: string) => {
|
||||
const [currentAIChatThread, setCurrentAIChatThread] = useRecoilState(
|
||||
currentAIChatThreadState,
|
||||
);
|
||||
|
||||
const { loading: threadsLoading } = useGetAgentChatThreadsQuery({
|
||||
variables: { agentId },
|
||||
skip: isDefined(currentAIChatThread) || !isDefined(agentId),
|
||||
onCompleted: (data) => {
|
||||
if (data.agentChatThreads.length > 0) {
|
||||
setCurrentAIChatThread(data.agentChatThreads[0].id);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { loading: messagesLoading, data } = useGetAgentChatMessagesQuery({
|
||||
variables: { threadId: currentAIChatThread! },
|
||||
skip: !isDefined(currentAIChatThread),
|
||||
});
|
||||
|
||||
const uiMessages = mapDBMessagesToUIMessages(data?.agentChatMessages || []);
|
||||
const isLoading = messagesLoading || threadsLoading;
|
||||
|
||||
return {
|
||||
uiMessages,
|
||||
isLoading,
|
||||
};
|
||||
};
|
||||
@@ -1,19 +1,16 @@
|
||||
import { currentAIChatThreadComponentState } from '@/ai/states/currentAIChatThreadComponentState';
|
||||
import { currentAIChatThreadState } from '@/ai/states/currentAIChatThreadState';
|
||||
import { useOpenAskAIPageInCommandMenu } from '@/command-menu/hooks/useOpenAskAIPageInCommandMenu';
|
||||
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { useCreateAgentChatThreadMutation } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useCreateNewAIChatThread = ({ agentId }: { agentId: string }) => {
|
||||
const [, setCurrentThreadId] = useRecoilComponentState(
|
||||
currentAIChatThreadComponentState,
|
||||
agentId,
|
||||
);
|
||||
const [, setCurrentAIChatThread] = useRecoilState(currentAIChatThreadState);
|
||||
|
||||
const { openAskAIPage } = useOpenAskAIPageInCommandMenu();
|
||||
const [createAgentChatThread] = useCreateAgentChatThreadMutation({
|
||||
variables: { input: { agentId } },
|
||||
onCompleted: (data) => {
|
||||
setCurrentThreadId(data.createAgentChatThread.id);
|
||||
setCurrentAIChatThread(data.createAgentChatThread.id);
|
||||
openAskAIPage();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export type AIChatObjectMetadataAndRecordContext = {
|
||||
type: 'objectMetadataId' | 'recordId';
|
||||
id: string;
|
||||
};
|
||||
|
||||
export const agentChatContextState = atom<
|
||||
Array<AIChatObjectMetadataAndRecordContext>
|
||||
>({
|
||||
key: 'ai/agentChatContextState',
|
||||
default: [],
|
||||
});
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
import { AgentChatMessagesComponentInstanceContext } from '@/ai/states/agentChatMessagesComponentState';
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
|
||||
export type AIChatObjectMetadataAndRecordContext = {
|
||||
type: 'objectMetadataId' | 'recordId';
|
||||
id: string;
|
||||
};
|
||||
|
||||
export const agentChatObjectMetadataAndRecordContextState =
|
||||
createComponentState<Array<AIChatObjectMetadataAndRecordContext>>({
|
||||
defaultValue: [],
|
||||
key: 'agentChatObjectMetadataAndRecordContextState',
|
||||
componentInstanceContext: AgentChatMessagesComponentInstanceContext,
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
import { AgentChatMessagesComponentInstanceContext } from '@/ai/states/agentChatMessagesComponentState';
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
|
||||
export const agentChatSelectedFilesComponentState = createComponentState<
|
||||
File[]
|
||||
>({
|
||||
key: 'agentChatSelectedFilesComponentState',
|
||||
defaultValue: [],
|
||||
componentInstanceContext: AgentChatMessagesComponentInstanceContext,
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const agentChatSelectedFilesState = atom<File[]>({
|
||||
key: 'ai/agentChatSelectedFilesState',
|
||||
default: [],
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import { AgentChatMessagesComponentInstanceContext } from '@/ai/states/agentChatMessagesComponentState';
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
import { type FileUIPart } from 'ai';
|
||||
|
||||
export const agentChatUploadedFilesComponentState = createComponentState<
|
||||
FileUIPart[]
|
||||
>({
|
||||
key: 'agentChatUploadedFilesComponentState',
|
||||
defaultValue: [],
|
||||
componentInstanceContext: AgentChatMessagesComponentInstanceContext,
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { type FileUIPart } from 'ai';
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const agentChatUploadedFilesState = atom<FileUIPart[]>({
|
||||
key: 'ai/agentChatUploadedFilesState',
|
||||
default: [],
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
import { CommandMenuPageComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuPageComponentInstanceContext';
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
|
||||
export const currentAIChatThreadComponentState = createComponentState<
|
||||
string | null
|
||||
>({
|
||||
key: 'currentAIChatThreadComponentState',
|
||||
defaultValue: null,
|
||||
componentInstanceContext: CommandMenuPageComponentInstanceContext,
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const currentAIChatThreadState = atom<string | null>({
|
||||
key: 'ai/currentAIChatThreadState',
|
||||
default: null,
|
||||
});
|
||||
+5
-11
@@ -1,12 +1,6 @@
|
||||
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const IsAgentChatCurrentContextActiveInstanceContext =
|
||||
createComponentInstanceContext();
|
||||
|
||||
export const isAgentChatCurrentContextActiveState =
|
||||
createComponentState<boolean>({
|
||||
defaultValue: true,
|
||||
key: 'isAgentChatCurrentContextActiveState',
|
||||
componentInstanceContext: IsAgentChatCurrentContextActiveInstanceContext,
|
||||
});
|
||||
export const isAgentChatCurrentContextActiveState = atom<boolean>({
|
||||
key: 'ai/isAgentChatCurrentContextActiveState',
|
||||
default: true,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
||||
import { AgentChatProvider } from '@/ai/components/AgentChatProvider';
|
||||
import { CommandMenuOpenContainer } from '@/command-menu/components/CommandMenuOpenContainer';
|
||||
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
|
||||
import { useCommandMenuCloseAnimationCompleteCleanup } from '@/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup';
|
||||
@@ -55,14 +56,16 @@ export const CommandMenuContainer = ({
|
||||
<ActionMenuComponentInstanceContext.Provider
|
||||
value={{ instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID }}
|
||||
>
|
||||
<AnimatePresence
|
||||
mode="wait"
|
||||
onExitComplete={commandMenuCloseAnimationCompleteCleanup}
|
||||
>
|
||||
{isCommandMenuOpened && (
|
||||
<CommandMenuOpenContainer>{children}</CommandMenuOpenContainer>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<AgentChatProvider>
|
||||
<AnimatePresence
|
||||
mode="wait"
|
||||
onExitComplete={commandMenuCloseAnimationCompleteCleanup}
|
||||
>
|
||||
{isCommandMenuOpened && (
|
||||
<CommandMenuOpenContainer>{children}</CommandMenuOpenContainer>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</AgentChatProvider>
|
||||
</ActionMenuComponentInstanceContext.Provider>
|
||||
</ContextStoreComponentInstanceContext.Provider>
|
||||
</RecordComponentInstanceContextsWrapper>
|
||||
|
||||
+2
-41
@@ -1,17 +1,8 @@
|
||||
import { LazyAIChatTab } from '@/ai/components/LazyAIChatTab';
|
||||
import { AIChatSkeletonLoader } from '@/ai/components/internal/AIChatSkeletonLoader';
|
||||
import { currentAIChatThreadComponentState } from '@/ai/states/currentAIChatThreadComponentState';
|
||||
import { mapDBMessagesToUIMessages } from '@/ai/utils/mapDBMessagesToUIMessages';
|
||||
import { AIChatTab } from '@/ai/components/AIChatTab';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
useGetAgentChatMessagesQuery,
|
||||
useGetAgentChatThreadsQuery,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
height: 100%;
|
||||
@@ -31,32 +22,6 @@ export const CommandMenuAskAIPage = () => {
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
const agentId = currentWorkspace?.defaultAgent?.id;
|
||||
|
||||
const [currentThreadId, setCurrentThreadId] = useRecoilComponentState(
|
||||
currentAIChatThreadComponentState,
|
||||
agentId,
|
||||
);
|
||||
|
||||
const { loading: threadsLoading } = useGetAgentChatThreadsQuery({
|
||||
variables: { agentId: agentId! },
|
||||
skip: isDefined(currentThreadId) || !isDefined(agentId),
|
||||
onCompleted: (data) => {
|
||||
if (data.agentChatThreads.length > 0) {
|
||||
setCurrentThreadId(data.agentChatThreads[0].id);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { loading, data } = useGetAgentChatMessagesQuery({
|
||||
variables: { threadId: currentThreadId ?? '' },
|
||||
skip: !isDefined(currentThreadId),
|
||||
});
|
||||
|
||||
const isLoading = loading || !currentThreadId || threadsLoading;
|
||||
|
||||
if (isLoading) {
|
||||
return <AIChatSkeletonLoader />;
|
||||
}
|
||||
|
||||
if (!agentId) {
|
||||
return (
|
||||
<StyledContainer>
|
||||
@@ -67,11 +32,7 @@ export const CommandMenuAskAIPage = () => {
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<LazyAIChatTab
|
||||
agentId={agentId}
|
||||
key={currentThreadId}
|
||||
uiMessages={mapDBMessagesToUIMessages(data?.agentChatMessages || [])}
|
||||
/>
|
||||
<AIChatTab agentId={agentId} />
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user