diff --git a/webapp/channels/src/components/post/post_component.test.tsx b/webapp/channels/src/components/post/post_component.test.tsx
index 548fce1e1e4..c63f581cd04 100644
--- a/webapp/channels/src/components/post/post_component.test.tsx
+++ b/webapp/channels/src/components/post/post_component.test.tsx
@@ -550,4 +550,108 @@ describe('PostComponent', () => {
expect(screen.queryByTestId('post-priority-label')).not.toBeInTheDocument();
});
});
+
+ describe('AI-generated indicator', () => {
+ const aiGeneratedPost = TestHelper.getPostMock({
+ channel_id: channel.id,
+ props: {
+ ai_generated_by: 'ai_user_id',
+ ai_generated_by_username: 'aibot',
+ },
+ });
+
+ test('should show AI-generated indicator for AI posts in non-compact mode', () => {
+ const props = {
+ ...baseProps,
+ post: aiGeneratedPost,
+ compactDisplay: false,
+ };
+ renderWithContext();
+
+ expect(screen.getByLabelText('Message posted by @aibot')).toBeInTheDocument();
+ });
+
+ test('should not show AI-generated indicator for regular posts', () => {
+ const regularPost = TestHelper.getPostMock({
+ channel_id: channel.id,
+ });
+ const props = {
+ ...baseProps,
+ post: regularPost,
+ compactDisplay: false,
+ };
+ renderWithContext();
+
+ expect(screen.queryByLabelText(/AI-generated|Message posted by/)).not.toBeInTheDocument();
+ });
+
+ test('should not show AI-generated indicator for consecutive posts', () => {
+ const props = {
+ ...baseProps,
+ post: aiGeneratedPost,
+ compactDisplay: false,
+ isConsecutivePost: true,
+ };
+ renderWithContext();
+
+ expect(screen.queryByLabelText(/AI-generated|Message posted by/)).not.toBeInTheDocument();
+ });
+
+ test('should show AI-generated indicator in PostUserProfile for compact mode in CENTER', () => {
+ const props = {
+ ...baseProps,
+ post: aiGeneratedPost,
+ compactDisplay: true,
+ location: Locations.CENTER,
+ };
+ renderWithContext();
+
+ // In compact CENTER mode, indicator is rendered by PostUserProfile (after username)
+ // Verify it appears exactly once
+ const indicators = screen.queryAllByLabelText(/AI-generated|Message posted by/);
+ expect(indicators.length).toBe(1);
+ });
+
+ test('should hide AI-generated indicator for consecutive posts in threads', () => {
+ const threadPost = TestHelper.getPostMock({
+ channel_id: channel.id,
+ root_id: 'root_post_id',
+ props: {
+ ai_generated_by: 'ai_user_id',
+ ai_generated_by_username: 'aibot',
+ },
+ });
+ const props = {
+ ...baseProps,
+ post: threadPost,
+ compactDisplay: false,
+ isConsecutivePost: true,
+ location: Locations.RHS_COMMENT,
+ };
+ renderWithContext();
+
+ expect(screen.queryByLabelText(/AI-generated|Message posted by/)).not.toBeInTheDocument();
+ });
+
+ test('should show AI-generated indicator for non-consecutive posts in threads', () => {
+ const threadPost = TestHelper.getPostMock({
+ channel_id: channel.id,
+ root_id: 'root_post_id',
+ props: {
+ ai_generated_by: 'ai_user_id',
+ ai_generated_by_username: 'aibot',
+ },
+ });
+ const props = {
+ ...baseProps,
+ post: threadPost,
+ compactDisplay: false,
+ isConsecutivePost: false,
+ location: Locations.RHS_COMMENT,
+ };
+ renderWithContext();
+
+ expect(screen.getByLabelText('Message posted by @aibot')).toBeInTheDocument();
+ });
+ });
});
diff --git a/webapp/channels/src/components/post/post_component.tsx b/webapp/channels/src/components/post/post_component.tsx
index 81bdc0a9ee6..8fb86c9f865 100644
--- a/webapp/channels/src/components/post/post_component.tsx
+++ b/webapp/channels/src/components/post/post_component.tsx
@@ -589,12 +589,11 @@ function PostComponent(props: Props) {
/>
}
{priority}
- {Boolean(post.props && post.props.ai_generated_by && post.props.ai_generated_by_username) &&
- typeof post.props.ai_generated_by === 'string' &&
- typeof post.props.ai_generated_by_username === 'string' && (
+ {((!props.compactDisplay && !(hasSameRoot(props) && props.isConsecutivePost)) || (props.compactDisplay && isRHS)) &&
+ PostUtils.hasAiGeneratedMetadata(post) && (
)}
diff --git a/webapp/channels/src/components/post/user_profile.tsx b/webapp/channels/src/components/post/user_profile.tsx
index 7db3b7a6c80..27355b3f56b 100644
--- a/webapp/channels/src/components/post/user_profile.tsx
+++ b/webapp/channels/src/components/post/user_profile.tsx
@@ -9,12 +9,13 @@ import type {Post} from '@mattermost/types/posts';
import {ensureString} from 'mattermost-redux/utils/post_utils';
+import AiGeneratedIndicator from 'components/post_view/ai_generated_indicator/ai_generated_indicator';
import PostHeaderCustomStatus from 'components/post_view/post_header_custom_status/post_header_custom_status';
import UserProfile from 'components/user_profile';
import BotTag from 'components/widgets/tag/bot_tag';
import Tag from 'components/widgets/tag/tag';
-import {fromAutoResponder, isFromWebhook} from 'utils/post_utils';
+import {fromAutoResponder, hasAiGeneratedMetadata, isFromWebhook} from 'utils/post_utils';
type Props = {
post: Post;
@@ -25,20 +26,33 @@ type Props = {
isBot: boolean;
isSystemMessage: boolean;
isMobileView: boolean;
+ location: string;
};
const PostUserProfile = (props: Props): JSX.Element | null => {
const intl = useIntl();
- const {post, compactDisplay, isMobileView, isConsecutivePost, enablePostUsernameOverride, isBot, isSystemMessage, colorizeUsernames} = props;
+ const {post, compactDisplay, isMobileView, isConsecutivePost, enablePostUsernameOverride, isBot, isSystemMessage, colorizeUsernames, location} = props;
const isFromAutoResponder = fromAutoResponder(post);
const colorize = compactDisplay && colorizeUsernames;
let userProfile: ReactNode = null;
let botIndicator = null;
let colon = null;
+ let aiIndicator = null;
if (props.compactDisplay) {
colon = {':'};
+
+ // Add AI indicator in compact mode after username, but not in RHS thread view (it goes after timestamp there)
+ if (hasAiGeneratedMetadata(post) && !(location === 'RHS_ROOT' || location === 'RHS_COMMENT')) {
+ aiIndicator = (
+
+ );
+ }
}
const customStatus = (
@@ -148,6 +162,7 @@ const PostUserProfile = (props: Props): JSX.Element | null => {
{colon}
{botIndicator}
{customStatus}
+ {aiIndicator}
);
};
diff --git a/webapp/channels/src/components/post_view/post_message_preview/post_message_preview.tsx b/webapp/channels/src/components/post_view/post_message_preview/post_message_preview.tsx
index d33b45c0309..8730766171e 100644
--- a/webapp/channels/src/components/post_view/post_message_preview/post_message_preview.tsx
+++ b/webapp/channels/src/components/post_view/post_message_preview/post_message_preview.tsx
@@ -13,11 +13,14 @@ import {ensureString} from 'mattermost-redux/utils/post_utils';
import FileAttachmentListContainer from 'components/file_attachment_list';
import PriorityLabel from 'components/post_priority/post_priority_label';
+import AiGeneratedIndicator from 'components/post_view/ai_generated_indicator/ai_generated_indicator';
import PostAttachmentOpenGraph from 'components/post_view/post_attachment_opengraph';
import PostMessageView from 'components/post_view/post_message_view';
import Timestamp from 'components/timestamp';
import UserProfileComponent from 'components/user_profile';
+import * as PostUtils from 'utils/post_utils';
+
import PreviewPostAvatar from './avatar/avatar';
import PostAttachmentContainer from '../post_attachment_container/post_attachment_container';
@@ -161,6 +164,13 @@ const PostMessagePreview = (props: Props) => {
)}
+ {PostUtils.hasAiGeneratedMetadata(previewPost) && (
+
+ )}