,
index: number,
) => {
@@ -99,16 +101,16 @@ export const AIChatAssistantMessageRenderer = ({
}
};
- if (!messageParts.length) {
- return ;
+ if (!messageParts.length && !hasError) {
+ return ;
}
return (
-
- {messageParts.map(renderStep)}
-
- {isLastMessageStreaming && }
+
+ {messageParts.map(renderMessagePart)}
+
+ {isLastMessageStreaming && !hasError && }
);
};
diff --git a/packages/twenty-front/src/modules/ai/components/AIChatErrorMessage.tsx b/packages/twenty-front/src/modules/ai/components/AIChatErrorMessage.tsx
new file mode 100644
index 00000000000..b41c15db8f3
--- /dev/null
+++ b/packages/twenty-front/src/modules/ai/components/AIChatErrorMessage.tsx
@@ -0,0 +1,87 @@
+import { useAgentChatContextOrThrow } from '@/ai/hooks/useAgentChatContextOrThrow';
+import { useAgentChatRequestBody } from '@/ai/hooks/useAgentChatRequestBody';
+import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
+import { useChat } from '@ai-sdk/react';
+import { useTheme } from '@emotion/react';
+import styled from '@emotion/styled';
+import { t } from '@lingui/core/macro';
+import { IconAlertCircle, IconRefresh } from 'twenty-ui/display';
+import { Button } from 'twenty-ui/input';
+
+const StyledErrorContainer = styled.div`
+ align-items: center;
+ background: ${({ theme }) => theme.background.danger};
+ border: 1px solid ${({ theme }) => theme.border.color.danger};
+ border-radius: ${({ theme }) => theme.border.radius.md};
+ display: flex;
+ gap: ${({ theme }) => theme.spacing(2)};
+ padding: ${({ theme }) => theme.spacing(2, 3)};
+`;
+
+const StyledErrorIcon = styled.div`
+ align-items: center;
+ color: ${({ theme }) => theme.color.red};
+ display: flex;
+`;
+
+const StyledErrorContent = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${({ theme }) => theme.spacing(0.5)};
+ flex: 1;
+`;
+
+const StyledErrorTitle = styled.div`
+ color: ${({ theme }) => theme.font.color.primary};
+ font-size: ${({ theme }) => theme.font.size.sm};
+ font-weight: ${({ theme }) => theme.font.weight.medium};
+`;
+
+const StyledErrorMessage = styled.div`
+ color: ${({ theme }) => theme.font.color.secondary};
+ font-size: ${({ theme }) => theme.font.size.xs};
+ word-break: break-word;
+`;
+
+type AIChatErrorMessageProps = {
+ error: Error;
+ records?: ObjectRecord[];
+};
+
+export const AIChatErrorMessage = ({
+ error,
+ records,
+}: AIChatErrorMessageProps) => {
+ const theme = useTheme();
+ const { chat } = useAgentChatContextOrThrow();
+ const { buildRequestBody } = useAgentChatRequestBody();
+ const { regenerate, status } = useChat({ chat });
+
+ const handleRetry = () => {
+ regenerate({
+ body: buildRequestBody(records),
+ });
+ };
+
+ return (
+
+
+
+
+
+ {t`Failed to get response`}
+
+ {error.message || t`An error occurred while processing your message`}
+
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/ai/components/AIChatMessage.tsx b/packages/twenty-front/src/modules/ai/components/AIChatMessage.tsx
index 54236df66af..bd7acb72395 100644
--- a/packages/twenty-front/src/modules/ai/components/AIChatMessage.tsx
+++ b/packages/twenty-front/src/modules/ai/components/AIChatMessage.tsx
@@ -7,8 +7,13 @@ import { AgentChatFilePreview } from '@/ai/components/internal/AgentChatFilePrev
import { AgentChatMessageRole } from '@/ai/constants/AgentChatMessageRole';
import { AIChatAssistantMessageRenderer } from '@/ai/components/AIChatAssistantMessageRenderer';
+import { AIChatErrorMessage } from '@/ai/components/AIChatErrorMessage';
+import { AIChatErrorMessageWithRecordsContext } from '@/ai/components/internal/AIChatErrorMessageWithRecordsContext';
import { type UIMessageWithMetadata } from '@/ai/types/UIMessageWithMetadata';
+import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { LightCopyIconButton } from '@/object-record/record-field/ui/components/LightCopyIconButton';
+import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
+import { isDefined } from 'twenty-shared/utils';
import { dateLocaleState } from '~/localization/states/dateLocaleState';
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
const StyledMessageBubble = styled.div<{ isUser?: boolean }>`
@@ -139,13 +144,22 @@ const StyledFilesContainer = styled.div`
export const AIChatMessage = ({
message,
isLastMessageStreaming,
+ error,
}: {
message: UIMessageWithMetadata;
isLastMessageStreaming: boolean;
+ error?: Error | null;
}) => {
const theme = useTheme();
const { localeCatalog } = useRecoilValue(dateLocaleState);
+ const contextStoreCurrentObjectMetadataItemId = useRecoilComponentValue(
+ contextStoreCurrentObjectMetadataItemIdComponentState,
+ );
+
+ const showError =
+ isDefined(error) && message.role === AgentChatMessageRole.ASSISTANT;
+
return (
{message.parts.length > 0 && (
@@ -185,6 +200,12 @@ export const AIChatMessage = ({
))}
)}
+ {showError &&
+ (contextStoreCurrentObjectMetadataItemId ? (
+
+ ) : (
+
+ ))}
{message.parts.length > 0 && message.metadata?.createdAt && (
diff --git a/packages/twenty-front/src/modules/ai/components/AIChatTab.tsx b/packages/twenty-front/src/modules/ai/components/AIChatTab.tsx
index 2676cef8fb4..6248bfd3466 100644
--- a/packages/twenty-front/src/modules/ai/components/AIChatTab.tsx
+++ b/packages/twenty-front/src/modules/ai/components/AIChatTab.tsx
@@ -17,7 +17,7 @@ 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 { useAgentChatContextOrThrow } from '@/ai/hooks/useAgentChatContextOrThrow';
+import { useAgentChat } from '@/ai/hooks/useAgentChat';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { t } from '@lingui/core/macro';
@@ -69,7 +69,8 @@ export const AIChatTab = ({ agentId }: { agentId: string }) => {
scrollWrapperId,
messages,
isStreaming,
- } = useAgentChatContextOrThrow();
+ error,
+ } = useAgentChat(agentId);
const contextStoreCurrentObjectMetadataItemId = useRecoilComponentValue(
contextStoreCurrentObjectMetadataItemIdComponentState,
@@ -94,16 +95,20 @@ export const AIChatTab = ({ agentId }: { agentId: string }) => {
<>
{messages.length !== 0 && (
- {messages.map((message) => (
-
- ))}
+ {messages.map((message, index) => {
+ const isLastMessage = index === messages.length - 1;
+ const isLastMessageStreaming = isStreaming && isLastMessage;
+ const shouldShowError = error && isLastMessage;
+
+ return (
+
+ );
+ })}
)}
{messages.length === 0 && }
@@ -140,9 +145,9 @@ export const AIChatTab = ({ agentId }: { agentId: string }) => {
/>
{contextStoreCurrentObjectMetadataItemId ? (
-
+
) : (
-
+
)}
diff --git a/packages/twenty-front/src/modules/ai/components/AgentChatProvider.tsx b/packages/twenty-front/src/modules/ai/components/AgentChatProvider.tsx
index 68ccc49edae..45e74526d66 100644
--- a/packages/twenty-front/src/modules/ai/components/AgentChatProvider.tsx
+++ b/packages/twenty-front/src/modules/ai/components/AgentChatProvider.tsx
@@ -1,12 +1,27 @@
import { AgentChatContext } from '@/ai/contexts/AgentChatContext';
-import { useAgentChat } from '@/ai/hooks/useAgentChat';
import { useAgentChatData } from '@/ai/hooks/useAgentChatData';
+import { currentAIChatThreadState } from '@/ai/states/currentAIChatThreadState';
+import { type UIMessageWithMetadata } from '@/ai/types/UIMessageWithMetadata';
+import { REST_API_BASE_URL } from '@/apollo/constant/rest-api-base-url';
+import { getTokenPair } from '@/apollo/utils/getTokenPair';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
+import { Chat } from '@ai-sdk/react';
+import { DefaultChatTransport } from 'ai';
import { Suspense } from 'react';
import { useRecoilValue } from 'recoil';
import { FeatureFlagKey } from '~/generated/graphql';
+const createLoadingChat = () =>
+ new Chat({
+ transport: new DefaultChatTransport({
+ api: `${REST_API_BASE_URL}/agent-chat/stream`,
+ headers: () => ({}),
+ }),
+ messages: [],
+ id: 'loading',
+ });
+
const AgentChatProviderContent = ({
agentId,
children,
@@ -15,15 +30,24 @@ const AgentChatProviderContent = ({
children: React.ReactNode;
}) => {
const { uiMessages, isLoading } = useAgentChatData(agentId);
- const chatState = useAgentChat(agentId, uiMessages);
- const combinedIsLoading = chatState.isLoading || isLoading;
+ const currentAIChatThread = useRecoilValue(currentAIChatThreadState);
+
+ const chatConfig = isLoading
+ ? createLoadingChat()
+ : new Chat({
+ transport: new DefaultChatTransport({
+ api: `${REST_API_BASE_URL}/agent-chat/stream`,
+ headers: () => ({
+ Authorization: `Bearer ${getTokenPair()?.accessOrWorkspaceAgnosticToken.token}`,
+ }),
+ }),
+ messages: uiMessages,
+ id: `${currentAIChatThread}-${uiMessages.length}`,
+ });
return (
{children}
@@ -41,14 +65,30 @@ export const AgentChatProvider = ({
if (!isAiEnabled || !agentId) {
return (
-
+
{children}
);
}
return (
-
+
+ {children}
+
+ }
+ >
{children}
diff --git a/packages/twenty-front/src/modules/ai/components/AgentChatSessionProvider.tsx b/packages/twenty-front/src/modules/ai/components/AgentChatSessionProvider.tsx
deleted file mode 100644
index 8db3836edaa..00000000000
--- a/packages/twenty-front/src/modules/ai/components/AgentChatSessionProvider.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-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 (
-
- {children}
-
- );
-};
-
-export const AgentChatProvider = ({
- children,
-}: {
- children: React.ReactNode;
-}) => {
- const currentWorkspace = useRecoilValue(currentWorkspaceState);
- const agentId = currentWorkspace?.defaultAgent?.id;
-
- if (!agentId) {
- return (
-
- {children}
-
- );
- }
-
- return (
-
-
- {children}
-
-
- );
-};
diff --git a/packages/twenty-front/src/modules/ai/components/ErrorStepRenderer.tsx b/packages/twenty-front/src/modules/ai/components/ErrorStepRenderer.tsx
deleted file mode 100644
index 628c4a11b95..00000000000
--- a/packages/twenty-front/src/modules/ai/components/ErrorStepRenderer.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import { extractErrorMessage } from '@/ai/utils/extractErrorMessage';
-import { useTheme } from '@emotion/react';
-import styled from '@emotion/styled';
-import { t } from '@lingui/core/macro';
-import { IconAlertCircle } from 'twenty-ui/display';
-
-const StyledContainer = styled.div`
- align-items: flex-start;
- background-color: ${({ theme }) => theme.color.red10};
- border: 1px solid ${({ theme }) => theme.color.red20};
- border-radius: ${({ theme }) => theme.border.radius.md};
- display: flex;
- gap: ${({ theme }) => theme.spacing(2)};
- margin-block: ${({ theme }) => theme.spacing(2)};
- padding: ${({ theme }) => theme.spacing(3)};
-`;
-
-const StyledIconContainer = styled.div`
- align-items: center;
- color: ${({ theme }) => theme.color.red60};
- display: flex;
- flex-shrink: 0;
- justify-content: center;
-`;
-
-const StyledContent = styled.div`
- flex: 1;
-`;
-
-const StyledTitle = styled.div`
- font-weight: ${({ theme }) => theme.font.weight.medium};
- color: ${({ theme }) => theme.color.red80};
- margin-bottom: ${({ theme }) => theme.spacing(1)};
-`;
-
-const StyledMessage = styled.div`
- color: ${({ theme }) => theme.color.red70};
- line-height: ${({ theme }) => theme.text.lineHeight.lg};
-`;
-
-export const ErrorStepRenderer = ({
- message,
- error,
-}: {
- message: string;
- error?: unknown;
-}) => {
- const theme = useTheme();
- const errorMessage = error ? extractErrorMessage(error) : message;
-
- return (
-
-
-
-
-
- {t`Error`}
- {errorMessage}
-
-
- );
-};
diff --git a/packages/twenty-front/src/modules/ai/components/internal/AIChatErrorMessageWithRecordsContext.tsx b/packages/twenty-front/src/modules/ai/components/internal/AIChatErrorMessageWithRecordsContext.tsx
new file mode 100644
index 00000000000..08a6bc3aa43
--- /dev/null
+++ b/packages/twenty-front/src/modules/ai/components/internal/AIChatErrorMessageWithRecordsContext.tsx
@@ -0,0 +1,14 @@
+import { AIChatErrorMessage } from '@/ai/components/AIChatErrorMessage';
+import { useFindManyRecordsSelectedInContextStore } from '@/context-store/hooks/useFindManyRecordsSelectedInContextStore';
+
+export const AIChatErrorMessageWithRecordsContext = ({
+ error,
+}: {
+ error: Error;
+}) => {
+ const { records } = useFindManyRecordsSelectedInContextStore({
+ limit: 10,
+ });
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/ai/components/internal/SendMessageButton.tsx b/packages/twenty-front/src/modules/ai/components/internal/SendMessageButton.tsx
index 392c4aa617c..cff2d61880e 100644
--- a/packages/twenty-front/src/modules/ai/components/internal/SendMessageButton.tsx
+++ b/packages/twenty-front/src/modules/ai/components/internal/SendMessageButton.tsx
@@ -1,24 +1,57 @@
import { AI_CHAT_INPUT_ID } from '@/ai/constants/AiChatInputId';
+import { useAgentChat } from '@/ai/hooks/useAgentChat';
import { useAgentChatContextOrThrow } from '@/ai/hooks/useAgentChatContextOrThrow';
+import { useAgentChatRequestBody } from '@/ai/hooks/useAgentChatRequestBody';
+import { agentChatUploadedFilesState } from '@/ai/states/agentChatUploadedFilesState';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
+import { useChat } from '@ai-sdk/react';
import { t } from '@lingui/core/macro';
+import { useRecoilState } from 'recoil';
import { Key } from 'ts-key-enum';
import { Button } from 'twenty-ui/input';
export const SendMessageButton = ({
+ agentId,
records,
}: {
+ agentId: string;
records?: ObjectRecord[];
}) => {
- const { handleSendMessage, isLoading, input } = useAgentChatContextOrThrow();
+ const { input, isLoading, handleInputChange } = useAgentChat(agentId);
+ const { chat } = useAgentChatContextOrThrow();
+ const { buildRequestBody } = useAgentChatRequestBody();
+ const { sendMessage } = useChat({ chat });
+
+ const [agentChatUploadedFiles, setAgentChatUploadedFiles] = useRecoilState(
+ agentChatUploadedFilesState,
+ );
+
+ const handleSendMessage = () => {
+ if (input.trim() === '' || isLoading) {
+ return;
+ }
+
+ sendMessage(
+ {
+ text: input,
+ files: agentChatUploadedFiles,
+ },
+ {
+ body: buildRequestBody(records),
+ },
+ );
+
+ handleInputChange('');
+ setAgentChatUploadedFiles([]);
+ };
useHotkeysOnFocusedElement({
keys: [Key.Enter],
callback: (event: KeyboardEvent) => {
if (!event.ctrlKey && !event.metaKey) {
event.preventDefault();
- handleSendMessage(records);
+ handleSendMessage();
}
},
focusId: AI_CHAT_INPUT_ID,
@@ -31,7 +64,7 @@ export const SendMessageButton = ({
return (