Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 0 additions & 1 deletion CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ api-goldens/element-ng/data-range-filter/ @siemens/siemens-element-owners @dr-it
api-goldens/element-ng/datepicker/ @siemens/siemens-element-owners @dr-itz @spliffone
api-goldens/element-ng/chat-messages/ @siemens/siemens-element-owners @dr-itz
api-goldens/element-ng/markdown/ @siemens/siemens-element-owners @dr-itz
api-goldens/element-ng/markdown-renderer/ @siemens/siemens-element-owners @dr-itz
api-goldens/element-ng/filtered-search/ @siemens/siemens-element-owners @chintankavathia @spliffone
api-goldens/element-ng/form/ @siemens/siemens-element-owners @spliffone
api-goldens/element-ng/formly/ @siemens/siemens-element-owners @chintankavathia @spliffone
Expand Down
4 changes: 0 additions & 4 deletions api-goldens/element-ng/chat-messages/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,9 @@ export interface PromptSuggestion {

// @public
export class SiAiMessageComponent {
constructor();
readonly actionParam: _angular_core.InputSignal<unknown>;
readonly actions: _angular_core.InputSignal<MessageAction[]>;
readonly content: _angular_core.InputSignal<string>;
readonly contentFormatter: _angular_core.InputSignal<((text: string) => string | Node) | undefined>;
readonly loading: _angular_core.InputSignalWithTransform<boolean, unknown>;
readonly secondaryActions: _angular_core.InputSignal<MenuItem[]>;
readonly secondaryActionsLabel: _angular_core.InputSignal<_siemens_element_translate_ng_translate.TranslatableString>;
Expand Down Expand Up @@ -152,12 +150,10 @@ export class SiChatMessageComponent {

// @public
export class SiUserMessageComponent {
constructor();
readonly actionParam: _angular_core.InputSignal<any>;
readonly actions: _angular_core.InputSignal<MessageAction[]>;
readonly attachments: _angular_core.InputSignal<Attachment[]>;
readonly content: _angular_core.InputSignal<string>;
readonly contentFormatter: _angular_core.InputSignal<((text: string) => string | Node) | undefined>;
readonly secondaryActions: _angular_core.InputSignal<MenuItem[]>;
readonly secondaryActionsLabel: _angular_core.InputSignal<_siemens_element_translate_ng_translate.TranslatableString>;
}
Expand Down
21 changes: 0 additions & 21 deletions api-goldens/element-ng/markdown-renderer/index.api.md

This file was deleted.

8 changes: 0 additions & 8 deletions docs/components/chat-messages/chat-message.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,4 @@ The slots are:

<si-docs-api component="SiAttachmentListComponent"></si-docs-api>

### Markdown renderer

> **Note:** The markdown renderer is currently experimental and may undergo changes in future releases.

<si-docs-component example="si-markdown-renderer/si-markdown-renderer"></si-docs-component>

<si-docs-api component="SiMarkdownRendererComponent"></si-docs-api>

<si-docs-types></si-docs-types>
8 changes: 0 additions & 8 deletions playwright/e2e/element-examples/static.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,6 @@ test('typography/color-variants', ({ si }) => si.static());
test('typography/type-styles', ({ si }) => si.static());
test('typography/display-styles', ({ si }) => si.static());
test('typography/typography', ({ si }) => si.static());
test('si-markdown-renderer/si-markdown-renderer', ({ si }) =>
si.static({
disabledA11yRules: ['link-in-text-block'],
waitCallback: async page => {
await page.getByRole('heading', { name: 'AI Assistant Response' }).waitFor();
await page.evaluate(() => document.fonts.ready);
}
}));
test('si-chat-messages/si-ai-message', ({ si }) => si.static());
test('si-chat-messages/si-user-message', ({ si }) => si.static());
test('si-chat-messages/si-chat-message', ({ si }) => si.static());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
<si-chat-message alignment="start" actionsPosition="bottom" [loading]="loading()">
@if (content()) {
@let content = textContent();
@if (content) {
<span class="text-pre-wrap">{{ content }}</span>
} @else {
<div #formattedContent> </div>
}
<span class="text-pre-wrap">{{ content() }}</span>
} @else {
<ng-content />
}
Expand Down
47 changes: 28 additions & 19 deletions projects/element-ng/chat-messages/si-ai-message.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,33 @@
* Copyright (c) Siemens 2016 - 2026
* SPDX-License-Identifier: MIT
*/
import { DebugElement, inputBinding, signal, WritableSignal } from '@angular/core';
import { Component, DebugElement, inputBinding, signal, WritableSignal } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By, DomSanitizer } from '@angular/platform-browser';
import { getMarkdownRenderer } from '@siemens/element-ng/markdown-renderer';
import { By } from '@angular/platform-browser';
import { MenuItem } from '@siemens/element-ng/menu';

import { MessageAction } from './message-action.model';
import { SiAiMessageComponent as TestComponent } from './si-ai-message.component';

@Component({
imports: [TestComponent],
template: `<si-ai-message>
<span data-testid="projected-content">Projected AI content</span>
</si-ai-message>`
})
class ProjectedContentHostComponent {}

describe('SiAiMessageComponent', () => {
let fixture: ComponentFixture<TestComponent>;
let debugElement: DebugElement;
let markdownRenderer: (text: string) => string | Node;
let content: WritableSignal<string>;
let contentFormatter: WritableSignal<((text: string) => string | Node) | undefined>;
let loading: WritableSignal<boolean>;
let actions: WritableSignal<MessageAction[]>;
let secondaryActions: WritableSignal<MenuItem[]>;
let actionParam: WritableSignal<unknown>;

beforeEach(() => {
content = signal('');
contentFormatter = signal<((text: string) => string | Node) | undefined>(undefined);
loading = signal(false);
actions = signal<MessageAction[]>([]);
secondaryActions = signal<MenuItem[]>([]);
Expand All @@ -33,26 +37,13 @@ describe('SiAiMessageComponent', () => {
fixture = TestBed.createComponent(TestComponent, {
bindings: [
inputBinding('content', content),
inputBinding('contentFormatter', contentFormatter),
inputBinding('loading', loading),
inputBinding('actions', actions),
inputBinding('secondaryActions', secondaryActions),
inputBinding('actionParam', actionParam)
]
});
debugElement = fixture.debugElement;
const sanitizer = TestBed.inject(DomSanitizer);
markdownRenderer = getMarkdownRenderer(sanitizer);
});

it('should render markdown content', async () => {
Comment thread
dr-itz marked this conversation as resolved.
content.set('This is **bold** text');
contentFormatter.set(markdownRenderer);
await fixture.whenStable();

const markdownContent = fixture.nativeElement.querySelector('.markdown-content') as HTMLElement;
expect(markdownContent).toBeTruthy();
expect(markdownContent.textContent).toBeTruthy();
});

it('should pass loading state to chat message', async () => {
Expand All @@ -70,6 +61,24 @@ describe('SiAiMessageComponent', () => {
expect(chatMessage.componentInstance.alignment()).toBe('start');
});

it('should project content when content input is empty', async () => {
const hostFixture = TestBed.createComponent(ProjectedContentHostComponent);
await hostFixture.whenStable();

const projectedContent = hostFixture.nativeElement.querySelector(
'si-chat-message [data-testid="projected-content"]'
);
expect(projectedContent).toHaveTextContent('Projected AI content');
});

it('should render text content', async () => {
content.set('AI response');
await fixture.whenStable();

const textContent = fixture.nativeElement.querySelector('si-chat-message .text-pre-wrap');
expect(textContent).toHaveTextContent('AI response');
});

it('should render action buttons when actions are provided', async () => {
actions.set([
{
Expand Down
56 changes: 2 additions & 54 deletions projects/element-ng/chat-messages/si-ai-message.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,7 @@
* SPDX-License-Identifier: MIT
*/
import { CdkMenuTrigger } from '@angular/cdk/menu';
import {
booleanAttribute,
Component,
effect,
input,
viewChild,
ElementRef,
signal,
output
} from '@angular/core';
import { booleanAttribute, Component, input, output } from '@angular/core';
import { elementOptionsVertical } from '@siemens/element-icons';
import { addIcons, SiIconComponent } from '@siemens/element-ng/icon';
import { MenuItem, SiMenuFactoryComponent } from '@siemens/element-ng/menu';
Expand All @@ -27,19 +18,17 @@ import { SiChatMessageComponent } from './si-chat-message.component';
* AI message component for displaying AI-generated responses in conversational interfaces.
*
* The AI message component renders AI-generated content in chat interfaces,
* supporting text formatting, markdown, loading states, and contextual actions.
* supporting text, loading states, and contextual actions.
* It appears as text (no bubble) aligned to the left side without any avatar/icon slot.
* Can be used within {@link SiChatContainerComponent}.
*
* The component automatically handles:
* - Styling for AI messages distinct from user or generic chat messages
* - Option to render markdown content, provide via `contentFormatter` input with a markdown renderer function (e.g., from {@link getMarkdownRenderer})
* - Showing loading states with skeleton UI during generation
* - Displaying primary and secondary actions
*
* @see {@link SiChatMessageComponent} for the base message wrapper component
* @see {@link SiUserMessageComponent} for the user message component
* @see {@link getMarkdownRenderer} for markdown formatting support
* @see {@link SiChatContainerComponent} for the chat container to use this within
*
* @experimental
Expand All @@ -59,7 +48,6 @@ import { SiChatMessageComponent } from './si-chat-message.component';
styleUrl: './si-ai-message.component.scss'
})
export class SiAiMessageComponent {
protected readonly formattedContent = viewChild<ElementRef<HTMLDivElement>>('formattedContent');
protected readonly icons = addIcons({ elementOptionsVertical });

/**
Expand All @@ -68,46 +56,6 @@ export class SiAiMessageComponent {
*/
readonly content = input<string>('');

/**
* Optional formatter function to transform content before display.
* - Returns string: Content will be sanitized using Angular's DomSanitizer
* - Returns Node: DOM node will be inserted directly without sanitization
*
* **Note:** If using a markdown renderer, make sure to apply the `markdown-content` class
* to the root element to ensure proper styling using the Element theme (e.g., `div.className = 'markdown-content'`).
* The function returned by {@link getMarkdownRenderer} does this automatically.
*
* **Warning:** When returning a Node, ensure the content is safe to prevent XSS attacks
* @defaultValue undefined
*/
readonly contentFormatter = input<((text: string) => string | Node) | undefined>(undefined);

protected readonly textContent = signal<string | undefined>(undefined);

constructor() {
effect(() => {
const formatter = this.contentFormatter();
const contentValue = this.content();
const container = this.formattedContent()?.nativeElement;

if (container && contentValue) {
if (formatter) {
const formatted = formatter(contentValue);

if (typeof formatted === 'string') {
this.textContent.set(formatted);
} else if (formatted instanceof Node) {
this.textContent.set(undefined);
container.innerHTML = '';
container.appendChild(formatted);
}
} else {
this.textContent.set(contentValue);
}
}
});
}

/**
* Whether the message is currently being generated (shows skeleton)
* @defaultValue false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { SiResponsiveContainerDirective } from '@siemens/element-ng/resize-obser
* - Responsive behavior that adapts to container size
*
* This is a low-level component designed for slotting in custom content, it provides slots via content projection:
* - Default content: Main message content area (consider using {@link SiMarkdownRendererComponent} for markdown support)
* - Default content: Main message content area (consider using `si-markdown` for markdown support)
* - `si-avatar/si-icon/img` selector: Avatar or icon representing the message sender
* - `si-chat-message-action` selector: Action buttons related to the message
* - `si-attachment-list` selector: Attachment list component for displaying file attachments
Expand All @@ -31,7 +31,6 @@ import { SiResponsiveContainerDirective } from '@siemens/element-ng/resize-obser
* @see {@link SiAiMessageComponent} for AI message display
* @see {@link SiAttachmentListComponent} for attachment list to slot in
* @see {@link SiChatMessageActionDirective} for action buttons to slot in
* @see {@link SiMarkdownRendererComponent} for markdown content rendering
* @see {@link SiChatContainerComponent} for the chat container to use this within
*
* @experimental
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,7 @@
}

@if (content()) {
@let content = textContent();
@if (content) {
<span class="text-pre-wrap">{{ content }}</span>
} @else {
<div #formattedContent> </div>
}
<span class="text-pre-wrap">{{ content() }}</span>
} @else {
<ng-content />
}
Expand Down
Loading
Loading