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,