Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
972251d
Add reasoningSummary field to AgentChatMessage type and update relate…
abdulrahmancodes Sep 8, 2025
758aa0c
Add reasoning summary functionality to agent chat
abdulrahmancodes Sep 8, 2025
8d3d278
Add ReasoningSummaryDisplay component and integrate into AIChatMessage
abdulrahmancodes Sep 8, 2025
6511a06
Enhance AgentExecutionService to support Anthropic model options
abdulrahmancodes Sep 8, 2025
26830c4
Add reasoning summary handling in AgentStreamingService
abdulrahmancodes Sep 8, 2025
6f7bd10
add REASONING_BUDGET_TOKENS constant
abdulrahmancodes Sep 8, 2025
05d9e27
Merge branch 'main' into feat/ai-reasoning-summary-display
abdulrahmancodes Sep 9, 2025
9ea64fb
Refactor tool schemas to include loading and completion messages
abdulrahmancodes Sep 10, 2025
3967f31
Add streamData field to AgentChatMessage entity and update related files
abdulrahmancodes Sep 10, 2025
d998588
Update streamData field type in AgentChatMessage entity and related f…
abdulrahmancodes Sep 10, 2025
005e18a
Add streamData field to GraphQL types for AgentChatMessage
abdulrahmancodes Sep 10, 2025
990b8fc
Add LoadingExpandableDisplay component for enhanced loading state vis…
abdulrahmancodes Sep 10, 2025
4e628c2
Add streamData field to GraphQL query for agent chat messages
abdulrahmancodes Sep 10, 2025
7a9267b
Add AIChatMessageStreamRenderer and groupSteps utility for handling s…
abdulrahmancodes Sep 10, 2025
09a1e13
Refactor AIChatMessage component to streamline agent streaming messag…
abdulrahmancodes Sep 10, 2025
c0ac85e
Enhance AIChatMessageStreamRenderer with parsing and reasoning display
abdulrahmancodes Sep 10, 2025
98225b9
Refactor AIChatMessageStreamRenderer to utilize modular step renderers
abdulrahmancodes Sep 10, 2025
c7e1fdc
Refactor AIChatMessageStreamRenderer to use ReasoningSummaryDisplay
abdulrahmancodes Sep 10, 2025
82ab33e
Add Shimmer effect to LoadingExpandableDisplay and ReasoningSummaryDi…
abdulrahmancodes Sep 10, 2025
579efc1
Remove LoadingExpandableDisplay component and integrate its functiona…
abdulrahmancodes Sep 10, 2025
8150d10
Refactor AgentStreamingService to streamline event handling and add r…
abdulrahmancodes Sep 10, 2025
a421ef2
Refactor ToolStepRenderer and related schemas to remove completionMes…
abdulrahmancodes Sep 10, 2025
1d4fd0d
Change StyledToggleButton from button to div for improved styling fle…
abdulrahmancodes Sep 11, 2025
49ddcba
Refactor ToolService and related tools to standardize response structure
abdulrahmancodes Sep 11, 2025
14c42cf
Refactor ToolStepRenderer to enhance result handling and display mess…
abdulrahmancodes Sep 11, 2025
46369d8
Add tool icons to ToolStepRenderer for enhanced visual feedback
abdulrahmancodes Sep 11, 2025
10ba534
Update ShimmerEffect component for improved animation performance
abdulrahmancodes Sep 11, 2025
57f3bbd
fix lint warnings
abdulrahmancodes Sep 11, 2025
96e0ef8
Remove console error logging from AgentStreamingService for cleaner e…
abdulrahmancodes Sep 11, 2025
e683c68
Remove parseAgentStreamingChunk utility and its associated tests to s…
abdulrahmancodes Sep 11, 2025
64bc0bb
Refactor ToolStepRenderer and extract getToolIcon utility for improve…
abdulrahmancodes Sep 11, 2025
6975d6f
Refactor ToolAdapterService and ToolService tests to standardize resp…
abdulrahmancodes Sep 11, 2025
871b1c9
Update AgentToolGeneratorService tests to align with standardized res…
abdulrahmancodes Sep 11, 2025
64f79d7
Update ToolService tests to reflect standardized response structure
abdulrahmancodes Sep 11, 2025
1f2a8e4
Remove reasoningSummary field from AgentChatMessage and related queri…
abdulrahmancodes Sep 11, 2025
84a94b3
Refactor AIChatMessage and AIChatMessageStreamRenderer components to …
abdulrahmancodes Sep 11, 2025
5ff92db
Refactor AgentStreamingService to streamline chunk processing and rem…
abdulrahmancodes Sep 11, 2025
169bd2f
Remove console log from AIChatMessageStreamRenderer to clean up code …
abdulrahmancodes Sep 11, 2025
87b5484
Update REASONING_BUDGET_TOKENS in AGENT_CONFIG to optimize performanc…
abdulrahmancodes Sep 11, 2025
3f4100f
Add doesSupportThinking property to AIModelConfig and RegisteredAIMod…
abdulrahmancodes Sep 11, 2025
bc11610
Add message mapping utility and refactor AgentExecutionService for im…
abdulrahmancodes Sep 11, 2025
b680c9d
Refactor AgentStreamingService to enhance error handling and message …
abdulrahmancodes Sep 11, 2025
57a9278
Enhance AIChatMessageStreamRenderer with streaming indicator and anim…
abdulrahmancodes Sep 11, 2025
5e787c2
Update ShimmerEffect animation duration for improved performance
abdulrahmancodes Sep 11, 2025
7d876ea
Refactor ToolStepRenderer to simplify error display logic
abdulrahmancodes Sep 11, 2025
3723dc5
Refactor message mapping in AgentExecutionService to improve clarity …
abdulrahmancodes Sep 11, 2025
06c5c5f
Remove unused ModelProvider import and related isAnthropicModel check…
abdulrahmancodes Sep 11, 2025
0cec3d8
Add AIChatAssistantMessageRenderer component for enhanced message ren…
abdulrahmancodes Sep 11, 2025
34c844f
Refactor ErrorStepRenderer and extract error message utility
abdulrahmancodes Sep 11, 2025
a38c5b2
Refactor ReasoningSummaryDisplay component for improved prop handling
abdulrahmancodes Sep 11, 2025
cbd0ca5
Add IconBrain to ReasoningSummaryDisplay for enhanced UI feedback
abdulrahmancodes Sep 11, 2025
e5a7855
Enhance AIChatAssistantMessageRenderer with styled steps container
abdulrahmancodes Sep 11, 2025
ce41d90
Refactor ReasoningSummaryDisplay to streamline rendering logic
abdulrahmancodes Sep 11, 2025
f16c223
Refactor AIChatAssistantMessageRenderer to replace TextStepRenderer w…
abdulrahmancodes Sep 11, 2025
002c021
Update ToolResultEvent type to enhance result structure
abdulrahmancodes Sep 11, 2025
e8458d9
Refactor ToolStepRenderer for improved type handling and styling
abdulrahmancodes Sep 11, 2025
d77b657
Add input property to ToolCallEvent type for enhanced flexibility
abdulrahmancodes Sep 11, 2025
65d1146
Refactor AIChatAssistantMessageRenderer to simplify props definition
abdulrahmancodes Sep 11, 2025
e14d851
Refactor extractErrorMessage utility for improved error handling
abdulrahmancodes Sep 12, 2025
72486b1
Update default icon in getToolIcon utility for consistency
abdulrahmancodes Sep 12, 2025
4271c9c
Remove groupSteps utility for codebase simplification
abdulrahmancodes Sep 12, 2025
cee588b
Refactor constructAssistantMessageContentFromStream to remove FilePar…
abdulrahmancodes Sep 12, 2025
c831139
Fix typo in ToolResultEvent type by correcting 'sucess' to 'success' …
abdulrahmancodes Sep 12, 2025
4388604
Implement localization for thinking messages in ReasoningSummaryDispl…
abdulrahmancodes Sep 12, 2025
0cbdc9e
Update AIChatAssistantMessageRenderer to include key prop in LazyMark…
abdulrahmancodes Sep 12, 2025
e1979de
Update AgentExecutionService to handle undefined streamData by provid…
abdulrahmancodes Sep 12, 2025
fa1697c
Update parseStream function to improve tool event matching logic
abdulrahmancodes Sep 12, 2025
6075c28
Add unit tests for parseStream function to validate event parsing
abdulrahmancodes Sep 12, 2025
22d7f70
Refactor parseStream unit tests to improve event validation
abdulrahmancodes Sep 12, 2025
78ecbb2
Merge branch 'main' into feat/ai-reasoning-summary-display
abdulrahmancodes Sep 15, 2025
72fa2ec
Refactor error handling in ToolStepRenderer to use extractErrorMessag…
abdulrahmancodes Sep 15, 2025
06a3eed
Update isStreaming logic in AIChatAssistantMessageRenderer to ensure …
abdulrahmancodes Sep 15, 2025
574f1c6
Fix style
FelixMalfait Sep 15, 2025
e618d7c
Add ShimmeringText component for animated text effect
abdulrahmancodes Sep 16, 2025
512b2f3
Replace Shimmer component with ShimmeringText in ReasoningSummaryDisp…
abdulrahmancodes Sep 16, 2025
a7d59a9
Refactor AgentChatMessage structure to replace 'content' with 'rawCon…
abdulrahmancodes Sep 16, 2025
de2de07
Refactor import formatting in agent-streaming.service.ts for improved…
abdulrahmancodes Sep 16, 2025
669bc46
Merge branch 'main' into feat/ai-reasoning-summary-display
abdulrahmancodes Sep 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/twenty-front/src/generated-metadata/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,10 @@ export type Agent = {

export type AgentChatMessage = {
__typename?: 'AgentChatMessage';
content: Scalars['String'];
createdAt: Scalars['DateTime'];
files: Array<File>;
id: Scalars['UUID'];
rawContent?: Maybe<Scalars['String']>;
role: Scalars['String'];
threadId: Scalars['UUID'];
};
Expand Down Expand Up @@ -4198,7 +4198,7 @@ export type GetAgentChatMessagesQueryVariables = Exact<{
}>;


export type GetAgentChatMessagesQuery = { __typename?: 'Query', agentChatMessages: Array<{ __typename?: 'AgentChatMessage', id: string, threadId: string, role: string, content: string, createdAt: string, files: Array<{ __typename?: 'File', id: string, name: string, fullPath: string, size: number, type: string, createdAt: string }> }> };
export type GetAgentChatMessagesQuery = { __typename?: 'Query', agentChatMessages: Array<{ __typename?: 'AgentChatMessage', id: string, threadId: string, role: string, createdAt: string, rawContent?: string | null, files: Array<{ __typename?: 'File', id: string, name: string, fullPath: string, size: number, type: string, createdAt: string }> }> };

export type GetAgentChatThreadsQueryVariables = Exact<{
agentId: Scalars['UUID'];
Expand Down Expand Up @@ -6319,8 +6319,8 @@ export const GetAgentChatMessagesDocument = gql`
id
threadId
role
content
createdAt
rawContent
files {
id
name
Expand Down
2 changes: 1 addition & 1 deletion packages/twenty-front/src/generated/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,10 @@ export type Agent = {

export type AgentChatMessage = {
__typename?: 'AgentChatMessage';
content: Scalars['String'];
createdAt: Scalars['DateTime'];
files: Array<File>;
id: Scalars['UUID'];
rawContent?: Maybe<Scalars['String']>;
role: Scalars['String'];
threadId: Scalars['UUID'];
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { ErrorStepRenderer } from '@/ai/components/ErrorStepRenderer';
import { ReasoningSummaryDisplay } from '@/ai/components/ReasoningSummaryDisplay';
import { ToolStepRenderer } from '@/ai/components/ToolStepRenderer';
import type { ParsedStep } from '@/ai/types/streamTypes';
import { parseStream } from '@/ai/utils/parseStream';
import { IconDotsVertical } from 'twenty-ui/display';

import { LazyMarkdownRenderer } from '@/ai/components/LazyMarkdownRenderer';
import { agentStreamingMessageState } from '@/ai/states/agentStreamingMessageState';
import { keyframes, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';

const StyledStepsContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(1)};
`;

const StyledDotsIconContainer = styled.div`
align-items: center;
border: ${({ theme }) => `1px solid ${theme.border.color.light}`};
border-radius: ${({ theme }) => theme.border.radius.md};
display: flex;
justify-content: center;
padding-inline: ${({ theme }) => theme.spacing(1)};
`;

const StyledDotsIcon = styled(IconDotsVertical)`
color: ${({ theme }) => theme.font.color.light};
transform: rotate(90deg);
`;

const dots = keyframes`
0% { content: ''; }
33% { content: '.'; }
66% { content: '..'; }
100% { content: '...'; }
`;

const StyledToolCallContainer = styled.div`
&::after {
display: inline-block;
content: '';
animation: ${dots} 750ms steps(3, end) infinite;
width: 2ch;
text-align: left;
}
`;

const LoadingDotsIcon = () => {
const theme = useTheme();

return (
<StyledDotsIconContainer>
<StyledDotsIcon size={theme.icon.size.xl} />
</StyledDotsIconContainer>
);
};

export const AIChatAssistantMessageRenderer = ({
streamData,
}: {
streamData: string;
}) => {
const agentStreamingMessage = useRecoilValue(agentStreamingMessageState);
const isStreaming =
Boolean(agentStreamingMessage) && streamData === agentStreamingMessage;

if (!streamData) {
return <LoadingDotsIcon />;
}

const isPlainString =
!streamData.includes('\n') ||
!streamData.split('\n').some((line) => {
try {
JSON.parse(line);
return true;
} catch {
return false;
}
});

if (isPlainString) {
return <LazyMarkdownRenderer text={streamData} />;
}

const steps = parseStream(streamData);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider memoizing parseStream result with useMemo to avoid reparsing stream data on every render, especially for large streams.


if (!steps.length) {
return <LoadingDotsIcon />;
}

const renderStep = (step: ParsedStep, index: number) => {
switch (step.type) {
case 'tool':
return <ToolStepRenderer key={index} events={step.events} />;
case 'reasoning':
return (
<ReasoningSummaryDisplay
key={index}
content={step.content}
isThinking={step.isThinking}
/>
);
case 'text':
return <LazyMarkdownRenderer key={index} text={step.content} />;
case 'error':
return (
<ErrorStepRenderer
key={index}
message={step.message}
error={step.error}
/>
);
default:
return null;
}
};

return (
<div>
<StyledStepsContainer>{steps.map(renderStep)}</StyledStepsContainer>
{isStreaming && <StyledToolCallContainer />}
</div>
);
};
96 changes: 16 additions & 80 deletions packages/twenty-front/src/modules/ai/components/AIChatMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import { keyframes, useTheme } from '@emotion/react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { Avatar, IconDotsVertical, IconSparkles } from 'twenty-ui/display';
import { Avatar, IconSparkles } from 'twenty-ui/display';

import { LazyMarkdownRenderer } from '@/ai/components/LazyMarkdownRenderer';
import { AgentChatFilePreview } from '@/ai/components/internal/AgentChatFilePreview';
import { AgentChatMessageRole } from '@/ai/constants/AgentChatMessageRole';
import { LightCopyIconButton } from '@/object-record/record-field/ui/components/LightCopyIconButton';

import { AIChatAssistantMessageRenderer } from '@/ai/components/AIChatAssistantMessageRenderer';
import { type AgentChatMessage } from '~/generated/graphql';
import { dateLocaleState } from '~/localization/states/dateLocaleState';
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';

const StyledMessageBubble = styled.div<{ isUser?: boolean }>`
display: flex;
flex-direction: column;
Expand All @@ -25,11 +24,10 @@ const StyledMessageBubble = styled.div<{ isUser?: boolean }>`
}
`;

const StyledMessageRow = styled.div<{ isShowingToolCall?: boolean }>`
const StyledMessageRow = styled.div`
display: flex;
flex-direction: row;
align-items: ${({ isShowingToolCall }) =>
isShowingToolCall ? 'center' : 'flex-start'};
align-items: flex-start;
gap: ${({ theme }) => theme.spacing(3)};
width: 100%;
`;
Expand Down Expand Up @@ -137,88 +135,22 @@ const StyledFilesContainer = styled.div`
margin-top: ${({ theme }) => theme.spacing(2)};
`;

const dots = keyframes`
0% { content: ''; }
33% { content: '.'; }
66% { content: '..'; }
100% { content: '...'; }
`;

const StyledToolCallContainer = styled.div`
&::after {
display: inline-block;
content: '';
animation: ${dots} 750ms steps(3, end) infinite;
width: 2ch;
text-align: left;
}
`;

const StyledDotsIconContainer = styled.div`
align-items: center;
border: ${({ theme }) => `1px solid ${theme.border.color.light}`};
border-radius: ${({ theme }) => theme.border.radius.md};
display: flex;
justify-content: center;
padding-inline: ${({ theme }) => theme.spacing(1)};
`;

const StyledDotsIcon = styled(IconDotsVertical)`
color: ${({ theme }) => theme.font.color.light};
transform: rotate(90deg);
`;

export const AIChatMessage = ({
message,
agentStreamingMessage,
}: {
message: AgentChatMessage;
agentStreamingMessage: { streamingText: string; toolCall: string };
agentStreamingMessage: string;
}) => {
const theme = useTheme();
const { localeCatalog } = useRecoilValue(dateLocaleState);

const markdownRender = (text: string) => {
return <LazyMarkdownRenderer text={text} />;
};

const getAssistantMessageContent = (message: AgentChatMessage) => {
if (message.content !== '') {
return markdownRender(message.content);
}

if (agentStreamingMessage.streamingText !== '') {
return markdownRender(agentStreamingMessage.streamingText);
}

if (agentStreamingMessage.toolCall !== '') {
return (
<StyledToolCallContainer>
{agentStreamingMessage.toolCall}
</StyledToolCallContainer>
);
}

return (
<StyledDotsIconContainer>
<StyledDotsIcon size={theme.icon.size.xl} />
</StyledDotsIconContainer>
);
};

return (
<StyledMessageBubble
key={message.id}
isUser={message.role === AgentChatMessageRole.USER}
>
<StyledMessageRow
isShowingToolCall={
message.role === AgentChatMessageRole.ASSISTANT &&
message.content === '' &&
agentStreamingMessage.streamingText === '' &&
agentStreamingMessage.toolCall !== ''
}
>
<StyledMessageRow>
{message.role === AgentChatMessageRole.ASSISTANT && (
<StyledAvatarContainer>
<Avatar
Expand All @@ -238,9 +170,13 @@ export const AIChatMessage = ({
<StyledMessageText
isUser={message.role === AgentChatMessageRole.USER}
>
{message.role === AgentChatMessageRole.ASSISTANT
? getAssistantMessageContent(message)
: message.content}
{message.role === AgentChatMessageRole.ASSISTANT ? (
<AIChatAssistantMessageRenderer
streamData={message.rawContent || agentStreamingMessage}
/>
) : (
message.rawContent
)}
</StyledMessageText>
{message.files.length > 0 && (
<StyledFilesContainer>
Expand All @@ -249,15 +185,15 @@ export const AIChatMessage = ({
))}
</StyledFilesContainer>
)}
{message.content && (
{message.rawContent && (
<StyledMessageFooter className="message-footer">
<span>
{beautifyPastDateRelativeToNow(
message.createdAt,
localeCatalog,
)}
</span>
<LightCopyIconButton copyText={message.content} />
<LightCopyIconButton copyText={message.rawContent} />
</StyledMessageFooter>
)}
</StyledMessageContainer>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { extractErrorMessage } from '@/ai/utils/extractErrorMessage';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
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;
}) => {
Comment on lines +40 to +46
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider defining a TypeScript interface for component props to enhance maintainability, following the project's conventions seen in other components.

Suggested change
export const ErrorStepRenderer = ({
message,
error,
}: {
message: string;
error?: unknown;
}) => {
type ErrorStepRendererProps = {
message: string;
error?: unknown;
};
export const ErrorStepRenderer = ({
message,
error,
}: ErrorStepRendererProps) => {

Context Used: Context - Use TypeScript interface definitions for component props to enhance maintainability. (link)

const theme = useTheme();
const errorMessage = error ? extractErrorMessage(error) : message;

return (
<StyledContainer>
<StyledIconContainer>
<IconAlertCircle size={theme.icon.size.md} />
</StyledIconContainer>
<StyledContent>
<StyledTitle>Error</StyledTitle>
<StyledMessage>{errorMessage}</StyledMessage>
</StyledContent>
</StyledContainer>
);
};
Loading