diff --git a/packages/twenty-front/src/modules/ai/components/AIChatAssistantMessageRenderer.tsx b/packages/twenty-front/src/modules/ai/components/AIChatAssistantMessageRenderer.tsx index 577030eaea9..c35c0b91b23 100644 --- a/packages/twenty-front/src/modules/ai/components/AIChatAssistantMessageRenderer.tsx +++ b/packages/twenty-front/src/modules/ai/components/AIChatAssistantMessageRenderer.tsx @@ -12,13 +12,13 @@ import { type UITools, } from 'ai'; -const StyledStepsContainer = styled.div` +const StyledMessagePartsContainer = styled.div` display: flex; flex-direction: column; gap: ${({ theme }) => theme.spacing(1)}; `; -const StyledDotsIconContainer = styled.div` +const StyledLoadingIconContainer = styled.div` align-items: center; border: ${({ theme }) => `1px solid ${theme.border.color.light}`}; border-radius: ${({ theme }) => theme.border.radius.md}; @@ -27,46 +27,48 @@ const StyledDotsIconContainer = styled.div` padding-inline: ${({ theme }) => theme.spacing(1)}; `; -const StyledDotsIcon = styled(IconDotsVertical)` +const StyledLoadingIcon = styled(IconDotsVertical)` color: ${({ theme }) => theme.font.color.light}; transform: rotate(90deg); `; -const dots = keyframes` +const streamingDotsAnimation = keyframes` 0% { content: ''; } 33% { content: '.'; } 66% { content: '..'; } 100% { content: '...'; } `; -const StyledToolCallContainer = styled.div` +const StyledStreamingIndicator = styled.div` &::after { display: inline-block; content: ''; - animation: ${dots} 750ms steps(3, end) infinite; + animation: ${streamingDotsAnimation} 750ms steps(3, end) infinite; width: 2ch; text-align: left; } `; -const LoadingDotsIcon = () => { +const InitialLoadingIndicator = () => { const theme = useTheme(); return ( - - - + + + ); }; export const AIChatAssistantMessageRenderer = ({ messageParts, isLastMessageStreaming, + hasError, }: { messageParts: UIMessagePart[]; isLastMessageStreaming: boolean; + hasError?: boolean; }) => { - const renderStep = ( + const renderMessagePart = ( part: UIMessagePart, 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`} + + +