diff --git a/packages/twenty-front/src/modules/ai/components/AIChatMessage.tsx b/packages/twenty-front/src/modules/ai/components/AIChatMessage.tsx index 282703f3dd..b161c229f1 100644 --- a/packages/twenty-front/src/modules/ai/components/AIChatMessage.tsx +++ b/packages/twenty-front/src/modules/ai/components/AIChatMessage.tsx @@ -1,7 +1,5 @@ -import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { useRecoilValue } from 'recoil'; -import { Avatar, IconSparkles } from 'twenty-ui/display'; import { AgentChatFilePreview } from '@/ai/components/internal/AgentChatFilePreview'; import { AgentMessageRole } from '@/ai/constants/AgentMessageRole'; @@ -13,10 +11,11 @@ import { type ExtendedUIMessage } from 'twenty-shared/ai'; import { isDefined } from 'twenty-shared/utils'; import { dateLocaleState } from '~/localization/states/dateLocaleState'; import { beautifyPastDateRelativeToNow } from '~/utils/date-utils'; + const StyledMessageBubble = styled.div<{ isUser?: boolean }>` + align-items: ${({ isUser }) => (isUser ? 'flex-end' : 'flex-start')}; display: flex; flex-direction: column; - align-items: flex-start; position: relative; width: 100%; @@ -26,26 +25,17 @@ const StyledMessageBubble = styled.div<{ isUser?: boolean }>` } `; -const StyledMessageRow = styled.div` - display: flex; - flex-direction: row; - align-items: flex-start; - gap: ${({ theme }) => theme.spacing(3)}; - width: 100%; -`; - const StyledMessageText = styled.div<{ isUser?: boolean }>` background: ${({ theme, isUser }) => - isUser ? theme.background.secondary : theme.background.transparent}; - border-radius: ${({ theme }) => theme.border.radius.md}; - padding: ${({ theme, isUser }) => (isUser ? theme.spacing(1, 2) : 0)}; - border: ${({ isUser, theme }) => - !isUser ? 'none' : `1px solid ${theme.border.color.light}`}; + isUser ? theme.background.tertiary : theme.background.transparent}; + border-radius: ${({ theme, isUser }) => + isUser ? theme.border.radius.sm : '0'}; color: ${({ theme, isUser }) => - isUser ? theme.font.color.light : theme.font.color.primary}; + isUser ? theme.font.color.secondary : theme.font.color.primary}; font-weight: ${({ isUser }) => (isUser ? 500 : 400)}; - width: fit-content; max-width: 100%; + padding: ${({ theme, isUser }) => (isUser ? theme.spacing(1, 2) : 0)}; + width: fit-content; word-wrap: break-word; overflow-wrap: break-word; /* Pre-wrap within the whole container turns every newline between block @@ -114,23 +104,10 @@ const StyledMessageFooter = styled.div` width: 100%; `; -const StyledAvatarContainer = styled.div<{ isUser?: boolean }>` - align-items: center; - background: ${({ theme, isUser }) => - isUser - ? theme.background.transparent.light - : theme.background.transparent.blue}; - display: flex; - justify-content: center; - height: 24px; - min-width: 24px; - border-radius: ${({ theme }) => theme.border.radius.sm}; - padding: 1px; -`; - -const StyledMessageContainer = styled.div` +const StyledMessageContainer = styled.div<{ isUser?: boolean }>` + max-width: 100%; min-width: 0; - width: 100%; + width: ${({ isUser }) => (isUser ? 'fit-content' : '100%')}; `; const StyledFilesContainer = styled.div` @@ -150,68 +127,48 @@ export const AIChatMessage = ({ isLastMessageStreaming: boolean; error?: Error | null; }) => { - const theme = useTheme(); const { localeCatalog } = useRecoilValue(dateLocaleState); + const isUser = message.role === AgentMessageRole.USER; const showError = isDefined(error) && message.role === AgentMessageRole.ASSISTANT; const fileParts = message.parts.filter((part) => part.type === 'file'); return ( - - - {message.role === AgentMessageRole.ASSISTANT && ( - - - + + + + + + {fileParts.length > 0 && ( + + {fileParts.map((file) => ( + + ))} + )} - {message.role === AgentMessageRole.USER && ( - - - - )} - - - - - {fileParts.length > 0 && ( - - {fileParts.map((file) => ( - - ))} - - )} - {showError && } - {message.parts.length > 0 && message.metadata?.createdAt && ( - - - {beautifyPastDateRelativeToNow( - message.metadata?.createdAt, - localeCatalog, - )} - - part.type === 'text')?.text ?? '' - } - /> - - )} - - + {showError && } + + {message.parts.length > 0 && message.metadata?.createdAt && ( + + + {beautifyPastDateRelativeToNow( + message.metadata?.createdAt, + localeCatalog, + )} + + part.type === 'text')?.text ?? '' + } + /> + + )} ); }; diff --git a/packages/twenty-front/src/modules/ai/components/AIChatStandaloneError.tsx b/packages/twenty-front/src/modules/ai/components/AIChatStandaloneError.tsx index 3b10da24dc..6148861a82 100644 --- a/packages/twenty-front/src/modules/ai/components/AIChatStandaloneError.tsx +++ b/packages/twenty-front/src/modules/ai/components/AIChatStandaloneError.tsx @@ -1,30 +1,11 @@ -import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { Avatar, IconSparkles } from 'twenty-ui/display'; import { AIChatErrorRenderer } from '@/ai/components/AIChatErrorRenderer'; const StyledErrorContainer = styled.div` display: flex; - flex-direction: row; + flex-direction: column; align-items: flex-start; - gap: ${({ theme }) => theme.spacing(3)}; - width: 100%; -`; - -const StyledAvatarContainer = styled.div` - align-items: center; - background: ${({ theme }) => theme.background.transparent.blue}; - display: flex; - justify-content: center; - height: 24px; - min-width: 24px; - border-radius: ${({ theme }) => theme.border.radius.sm}; - padding: 1px; -`; - -const StyledContent = styled.div` - min-width: 0; width: 100%; `; @@ -35,21 +16,9 @@ type AIChatStandaloneErrorProps = { export const AIChatStandaloneError = ({ error, }: AIChatStandaloneErrorProps) => { - const theme = useTheme(); - return ( - - - - - - + ); }; diff --git a/packages/twenty-front/src/modules/ai/components/AIChatTab.tsx b/packages/twenty-front/src/modules/ai/components/AIChatTab.tsx index 64d64c9bef..2a251d67c2 100644 --- a/packages/twenty-front/src/modules/ai/components/AIChatTab.tsx +++ b/packages/twenty-front/src/modules/ai/components/AIChatTab.tsx @@ -96,9 +96,10 @@ const StyledScrollWrapper = styled(ScrollWrapper)` `; const StyledButtonsContainer = styled.div` + align-items: center; display: flex; flex-direction: row; - gap: ${({ theme }) => theme.spacing(1)}; + gap: ${({ theme }) => theme.spacing(0.5)}; justify-content: flex-end; `; 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 523a514a96..7ab88934a3 100644 --- a/packages/twenty-front/src/modules/ai/components/internal/SendMessageButton.tsx +++ b/packages/twenty-front/src/modules/ai/components/internal/SendMessageButton.tsx @@ -4,12 +4,13 @@ import { agentChatInputState } from '@/ai/states/agentChatInputState'; import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement'; import { useRecoilValue } from 'recoil'; import { Key } from 'ts-key-enum'; -import { IconArrowUp } from 'twenty-ui/display'; +import { IconArrowUp, IconPlayerStop } from 'twenty-ui/display'; import { RoundedIconButton } from 'twenty-ui/input'; export const SendMessageButton = () => { const agentChatInput = useRecoilValue(agentChatInputState); - const { handleSendMessage, isLoading } = useAgentChatContextOrThrow(); + const { handleSendMessage, handleStop, isLoading, isStreaming } = + useAgentChatContextOrThrow(); useHotkeysOnFocusedElement({ keys: [Key.Enter], @@ -26,9 +27,20 @@ export const SendMessageButton = () => { }, }); + if (isStreaming) { + return ( + handleStop()} + /> + ); + } + return ( handleSendMessage()} disabled={!agentChatInput || isLoading} /> diff --git a/packages/twenty-front/src/modules/ai/components/suggested-prompts/AIChatSuggestedPrompts.tsx b/packages/twenty-front/src/modules/ai/components/suggested-prompts/AIChatSuggestedPrompts.tsx index 8f24ef7e5c..969a41221c 100644 --- a/packages/twenty-front/src/modules/ai/components/suggested-prompts/AIChatSuggestedPrompts.tsx +++ b/packages/twenty-front/src/modules/ai/components/suggested-prompts/AIChatSuggestedPrompts.tsx @@ -13,15 +13,15 @@ import { agentChatInputState } from '@/ai/states/agentChatInputState'; const StyledContainer = styled.div` display: flex; flex-direction: column; - gap: ${({ theme }) => theme.spacing(1)}; - padding: ${({ theme }) => theme.spacing(3)}; + gap: ${({ theme }) => theme.spacing(2)}; + padding: ${({ theme }) => theme.spacing(2)}; `; const StyledTitle = styled.div` color: ${({ theme }) => theme.font.color.primary}; font-size: ${({ theme }) => theme.font.size.sm}; font-weight: ${({ theme }) => theme.font.weight.medium}; - margin-bottom: ${({ theme }) => theme.spacing(1)}; + padding: ${({ theme }) => `0 ${theme.spacing(2)}`}; `; const StyledSuggestedPromptButton = styled(LightButton)` diff --git a/packages/twenty-front/src/modules/ai/contexts/AgentChatContext.ts b/packages/twenty-front/src/modules/ai/contexts/AgentChatContext.ts index 943e6a3151..68d058ea77 100644 --- a/packages/twenty-front/src/modules/ai/contexts/AgentChatContext.ts +++ b/packages/twenty-front/src/modules/ai/contexts/AgentChatContext.ts @@ -8,7 +8,7 @@ export type AgentChatContextValue = { error?: Error; handleSendMessage: () => Promise; - + handleStop: () => void; handleRetry: () => void; }; diff --git a/packages/twenty-front/src/modules/ai/hooks/useAgentChat.ts b/packages/twenty-front/src/modules/ai/hooks/useAgentChat.ts index 942a7fbae3..fac9d9569f 100644 --- a/packages/twenty-front/src/modules/ai/hooks/useAgentChat.ts +++ b/packages/twenty-front/src/modules/ai/hooks/useAgentChat.ts @@ -80,7 +80,7 @@ export const useAgentChat = (uiMessages: ExtendedUIMessage[]) => { } }; - const { sendMessage, messages, status, error, regenerate } = useChat({ + const { sendMessage, messages, status, error, regenerate, stop } = useChat({ transport: new DefaultChatTransport({ api: `${REST_API_BASE_URL}/agent-chat/stream`, headers: () => ({ @@ -185,6 +185,7 @@ export const useAgentChat = (uiMessages: ExtendedUIMessage[]) => { return { messages, handleSendMessage, + handleStop: stop, isLoading, isStreaming, error, diff --git a/packages/twenty-ui/src/input/button/components/RoundedIconButton.tsx b/packages/twenty-ui/src/input/button/components/RoundedIconButton.tsx index 9f59e68649..161b165abe 100644 --- a/packages/twenty-ui/src/input/button/components/RoundedIconButton.tsx +++ b/packages/twenty-ui/src/input/button/components/RoundedIconButton.tsx @@ -2,20 +2,18 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { type IconComponent } from '@ui/display'; -const StyledIconButton = styled.button` +export type RoundedIconButtonSize = 'small' | 'medium'; + +const StyledIconButton = styled.button<{ size: RoundedIconButtonSize }>` align-items: center; background: ${({ theme }) => theme.color.blue}; border: none; - border-radius: 50%; color: ${({ theme }) => theme.font.color.inverted}; - cursor: pointer; display: flex; - height: 20px; - + height: ${({ size }) => (size === 'small' ? '20px' : '24px')}; justify-content: center; - outline: none; padding: 0; transition: @@ -27,11 +25,12 @@ const StyledIconButton = styled.button` color: ${({ theme }) => theme.font.color.tertiary}; cursor: default; } - width: 20px; + width: ${({ size }) => (size === 'small' ? '20px' : '24px')}; `; type RoundedIconButtonProps = { Icon: IconComponent; + size?: RoundedIconButtonSize; } & React.ButtonHTMLAttributes; export const RoundedIconButton = ({ @@ -39,12 +38,18 @@ export const RoundedIconButton = ({ onClick, disabled, className, + size = 'small', }: RoundedIconButtonProps) => { const theme = useTheme(); return ( - - {} + + ); }; diff --git a/packages/twenty-ui/src/input/index.ts b/packages/twenty-ui/src/input/index.ts index 1cc4a53128..0e148b1653 100644 --- a/packages/twenty-ui/src/input/index.ts +++ b/packages/twenty-ui/src/input/index.ts @@ -66,6 +66,7 @@ export type { LightIconButtonGroupProps } from './button/components/LightIconBut export { LightIconButtonGroup } from './button/components/LightIconButtonGroup'; export type { MainButtonVariant } from './button/components/MainButton'; export { MainButton } from './button/components/MainButton'; +export type { RoundedIconButtonSize } from './button/components/RoundedIconButton'; export { RoundedIconButton } from './button/components/RoundedIconButton'; export { StyledTabButton,