Skip to content
Draft
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,25 @@
- paragraph: Can you help me analyze these files?
- paragraph: I'm having trouble understanding the data structure and need assistance with the implementation.
- button "Export message"
- paragraph: I'd be happy to help you analyze your files! I can see you've shared a Python script and a CSV dataset.
- paragraph: Let me examine the structure and provide guidance.
- button "Good response"
- button "Bad response"
- button "Copy response"
- text: I'd be happy to help! Data processing pipelines typically follow a structured approach.
- link "Data Pipeline Design Patterns":
- /url: https://martinfowler.com/articles/data-pipeline.html
- text: ""
- text: Let me examine your files and provide detailed guidance.
- button "Add to list"
- button "Export response"
- button "Retry response"
- button "More actions"
- paragraph: Perfect! What should I focus on first
- paragraph: I also want to make sure the performance is optimized for large datasets since this will be used in production with potentially millions of rows?
- button "Export message"
- paragraph: Great question! When analyzing large datasets, it's crucial to focus on...
- button "Good response"
- button "Bad response"
- button "Copy response"
- text: Great question! When analyzing large datasets, it's crucial to focus on vectorized operations and avoid row-by-row iteration.
- link "Pandas Performance Guide":
- /url: https://pandas.pydata.org/docs/user_guide/enhancingperf.html
- text: Source for this paragraph.
- button "Add to list"
- button "Export response"
- button "Retry response"
- button "More actions"
- alert: Info AI responses are for demonstration purposes.
- group:
Expand Down
2 changes: 2 additions & 0 deletions projects/element-ng/chat-messages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
* SPDX-License-Identifier: MIT
*/
export * from './si-ai-message.component';
export * from './si-annotated-text.model';
export * from './si-attachment-list.component';
export * from './si-citation-pill.component';
export * from './si-chat-container.component';
export * from './si-chat-container-input.directive';
export * from './si-chat-input.component';
Expand Down
15 changes: 14 additions & 1 deletion projects/element-ng/chat-messages/si-ai-message.component.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
<si-chat-message alignment="start" actionsPosition="bottom" [loading]="loading()">
@if (content()) {
@if (annotatedText(); as annotated) {
<span class="annotated-content text-pre-wrap">
@for (segment of annotated.segments; track $index) {
@if (segment.type === 'text') {
<span>{{ segment.content }}</span>
} @else {
<si-citation-pill
[citation]="getCitation(segment.citationId)"
(clicked)="citationClicked.emit($event)"
/><br />
Comment thread
robertwilde marked this conversation as resolved.
}
}
</span>
} @else if (content()) {
@let content = textContent();
@if (content) {
<span class="text-pre-wrap">{{ content }}</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ si-chat-message {
margin-block-start: map.get(variables.$spacers, 5) - map.get(variables.$spacers, 2);
}

.annotated-content {
line-height: 20px;
}

// Loading spinner size adjustment (inherited from generic component)
:host ::ng-deep si-loading-spinner {
--loading-spinner-size: 1.5em;
Expand Down
27 changes: 27 additions & 0 deletions projects/element-ng/chat-messages/si-ai-message.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Component,
effect,
input,
output,
viewChild,
ElementRef,
signal
Expand All @@ -18,8 +19,10 @@ import { MenuItem, SiMenuFactoryComponent } from '@siemens/element-ng/menu';
import { SiTranslatePipe, t } from '@siemens/element-translate-ng/translate';

import { MessageAction } from './message-action.model';
import { SiChatAnnotatedText, SiChatCitation } from './si-annotated-text.model';
import { SiChatMessageActionDirective } from './si-chat-message-action.directive';
import { SiChatMessageComponent } from './si-chat-message.component';
import { SiCitationPillComponent } from './si-citation-pill.component';

/**
* AI message component for displaying AI-generated responses in conversational interfaces.
Expand Down Expand Up @@ -47,6 +50,7 @@ import { SiChatMessageComponent } from './si-chat-message.component';
imports: [
CdkMenuTrigger,
SiChatMessageComponent,
SiCitationPillComponent,
SiIconComponent,
SiMenuFactoryComponent,
SiChatMessageActionDirective,
Expand All @@ -59,6 +63,29 @@ export class SiAiMessageComponent {
protected readonly formattedContent = viewChild<ElementRef<HTMLDivElement>>('formattedContent');
protected readonly icons = addIcons({ elementOptionsVertical });

/**
* Pre-segmented annotated text containing inline citation references.
* When provided, takes precedence over the `content` input.
* Use `parseCitationMarkers` or `parseCitationOffsets` to produce this value.
* @defaultValue undefined
*/
readonly annotatedText = input<SiChatAnnotatedText | undefined>(undefined);

/**
* Emitted when a citation pill inside the message is clicked.
* The emitted value is the {@link SiChatCitation} that was clicked.
*/
readonly citationClicked = output<SiChatCitation>();

protected getCitation(id: string): SiChatCitation {
return (
this.annotatedText()?.citations.find(c => c.id === id) ?? {
id,
title: id
}
);
}
Comment thread
robertwilde marked this conversation as resolved.

/**
* The AI-generated message content
* @defaultValue ''
Expand Down
42 changes: 42 additions & 0 deletions projects/element-ng/chat-messages/si-annotated-text.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Copyright (c) Siemens 2016 - 2026
* SPDX-License-Identifier: MIT
*/

/** A single source citation referenced within an AI message. */
export interface SiChatCitation {
/** Unique identifier used to match citation segments to this citation. */
id: string;
/** Human-readable title of the source (e.g. page title or document name). */
title: string;
/** Optional URL to the original source. */
url?: string;
}

/** A plain-text run within an annotated message. */
export interface SiChatTextRun {
type: 'text';
content: string;
}

/** A citation placeholder within an annotated message that maps to a {@link SiChatCitation}. */
export interface SiChatCitationRun {
type: 'citation';
citationId: string;
}

export type SiChatTextSegment = SiChatTextRun | SiChatCitationRun;

/**
* Normalized representation of an AI message that contains inline citations.
*
* Produced by helper functions such as `parseCitationMarkers` or
* `parseCitationOffsets` and consumed by {@link SiAiMessageComponent} via its
* `annotatedText` input.
*/
export interface SiChatAnnotatedText {
/** Ordered segments that make up the full message content. */
segments: SiChatTextSegment[];
/** All citations referenced by the segments. */
citations: SiChatCitation[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
@use 'sass:map';
@use '@siemens/element-theme/src/styles/variables';

:host {
display: inline;
margin-inline: map.get(variables.$spacers, 3);
}

.citation-pill {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding-block: 0;
padding-inline: 0.375rem;
block-size: 20px;
box-sizing: border-box;
border: 1px solid variables.$element-ui-3;
border-radius: 9999px;
color: variables.$element-text-primary;
background: transparent;
font-size: 0.75rem;
line-height: 1;
font-weight: 400;
cursor: pointer;
text-decoration: none;
vertical-align: middle;
white-space: nowrap;
transition: background 0.15s ease;

&:hover,
&:active {
background: variables.$element-base-1-hover;
border-color: variables.$element-ui-3;
color: variables.$element-text-primary;
text-decoration: none;
}

&:focus-visible {
outline: 2px solid variables.$element-focus-default;
outline-offset: 2px;
}
}
Loading
Loading