Skip to content

Commit b5e9aba

Browse files
committed
feat(ai-chat): add inline source citation
1 parent 349c889 commit b5e9aba

19 files changed

Lines changed: 626 additions & 39 deletions

api-goldens/element-ng/chat-messages/index.api.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ export class SiAiMessageComponent {
5757
constructor();
5858
readonly actionParam: _angular_core.InputSignal<unknown>;
5959
readonly actions: _angular_core.InputSignal<MessageAction[]>;
60+
readonly annotatedText: _angular_core.InputSignal<SiChatAnnotatedText | undefined>;
61+
readonly citationClicked: _angular_core.OutputEmitterRef<SiChatCitation>;
6062
readonly content: _angular_core.InputSignal<string>;
6163
readonly contentFormatter: _angular_core.InputSignal<((text: string) => string | Node) | undefined>;
6264
readonly loading: _angular_core.InputSignalWithTransform<boolean, unknown>;
@@ -81,6 +83,27 @@ export class SiAttachmentListComponent {
8183
readonly removeLabel: _angular_core.InputSignal<_siemens_element_translate_ng_translate.TranslatableString>;
8284
}
8385

86+
// @public
87+
export interface SiChatAnnotatedText {
88+
citations: SiChatCitation[];
89+
segments: SiChatTextSegment[];
90+
}
91+
92+
// @public
93+
export interface SiChatCitation {
94+
id: string;
95+
title: string;
96+
url?: string;
97+
}
98+
99+
// @public
100+
export interface SiChatCitationRun {
101+
// (undocumented)
102+
citationId: string;
103+
// (undocumented)
104+
type: 'citation';
105+
}
106+
84107
// @public
85108
export class SiChatContainerComponent implements AfterContentInit, OnDestroy {
86109
constructor();
@@ -143,6 +166,25 @@ export class SiChatMessageComponent {
143166
readonly loading: _angular_core.InputSignal<boolean>;
144167
}
145168

169+
// @public
170+
export interface SiChatTextRun {
171+
// (undocumented)
172+
content: string;
173+
// (undocumented)
174+
type: 'text';
175+
}
176+
177+
// @public (undocumented)
178+
export type SiChatTextSegment = SiChatTextRun | SiChatCitationRun;
179+
180+
// @public
181+
export class SiCitationPillComponent {
182+
readonly citation: _angular_core.InputSignal<SiChatCitation>;
183+
readonly clicked: _angular_core.OutputEmitterRef<SiChatCitation>;
184+
readonly icon: _angular_core.InputSignal<string | undefined>;
185+
readonly label: _angular_core.InputSignal<_siemens_element_translate_ng_translate.TranslatableString>;
186+
}
187+
146188
// @public
147189
export class SiUserMessageComponent {
148190
constructor();
Lines changed: 2 additions & 2 deletions
Loading
Lines changed: 2 additions & 2 deletions
Loading
Lines changed: 2 additions & 2 deletions
Loading
Lines changed: 2 additions & 2 deletions
Loading

playwright/snapshots/static.spec.ts-snapshots/si-chat-messages--si-chat-container.yaml

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,27 @@
44
- button "dataset.csv"
55
- paragraph: Can you help me analyze these files?
66
- paragraph: I'm having trouble understanding the data structure and need assistance with the implementation.
7-
- group:
8-
- button "Export message"
9-
- paragraph: I'd be happy to help you analyze your files! I can see you've shared a Python script and a CSV dataset.
10-
- paragraph: Let me examine the structure and provide guidance.
11-
- group:
12-
- button "Good response"
13-
- button "Bad response"
14-
- button "Copy response"
15-
- button "More actions"
7+
- button "Export message"
8+
- text: I'd be happy to help! Data processing pipelines typically follow a structured approach.
9+
- link "Data Pipeline Design Patterns":
10+
- /url: https://martinfowler.com/articles/data-pipeline.html
11+
- text: ""
12+
- text: Let me examine your files and provide detailed guidance.
13+
- button "Add to list"
14+
- button "Export response"
15+
- button "Retry response"
16+
- button "More actions"
1617
- paragraph: Perfect! What should I focus on first
1718
- 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?
18-
- group:
19-
- button "Export message"
20-
- paragraph: Great question! When analyzing large datasets, it's crucial to focus on...
21-
- group:
22-
- button "Good response"
23-
- button "Bad response"
24-
- button "Copy response"
25-
- button "More actions"
19+
- button "Export message"
20+
- text: Great question! When analyzing large datasets, it's crucial to focus on vectorized operations and avoid row-by-row iteration.
21+
- link "Pandas Performance Guide":
22+
- /url: https://pandas.pydata.org/docs/user_guide/enhancingperf.html
23+
- text: Source for this paragraph.
24+
- button "Add to list"
25+
- button "Export response"
26+
- button "Retry response"
27+
- button "More actions"
2628
- alert: Info AI responses are for demonstration purposes.
2729
- group:
2830
- text: requirements.pdf
@@ -32,8 +34,8 @@
3234
- button "Remove attachment mockup.png"
3335
- textbox "Chat message input":
3436
- /placeholder: Enter a command, question or topic...
35-
- group:
36-
- button "More actions"
37-
- button "Show Welcome Screen"
37+
- button "Attach file"
38+
- button "Clear messages"
39+
- button "Show Welcome Screen"
3840
- button "Send"
3941
- text: The content is AI generated. Always verify the information for accuracy.

projects/element-ng/chat-messages/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
* SPDX-License-Identifier: MIT
44
*/
55
export * from './si-ai-message.component';
6+
export * from './si-annotated-text.model';
67
export * from './si-attachment-list.component';
8+
export * from './si-citation-pill.component';
79
export * from './si-chat-container.component';
810
export * from './si-chat-container-input.directive';
911
export * from './si-chat-input.component';

projects/element-ng/chat-messages/si-ai-message.component.html

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
<si-chat-message alignment="start" actionsPosition="bottom" [loading]="loading()">
2-
@if (content()) {
2+
@if (annotatedText(); as annotated) {
3+
<span class="annotated-content text-pre-wrap">
4+
@for (segment of annotated.segments; track $index) {
5+
@if (segment.type === 'text') {
6+
<span>{{ segment.content }}</span>
7+
} @else {
8+
<si-citation-pill
9+
[citation]="getCitation(segment.citationId)"
10+
(clicked)="citationClicked.emit($event)"
11+
/><br />
12+
}
13+
}
14+
</span>
15+
} @else if (content()) {
316
@let content = textContent();
417
@if (content) {
518
<span class="text-pre-wrap">{{ content }}</span>

projects/element-ng/chat-messages/si-ai-message.component.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ si-chat-message {
1818
margin-block-start: map.get(variables.$spacers, 5) - map.get(variables.$spacers, 2);
1919
}
2020

21+
.annotated-content {
22+
line-height: 20px;
23+
}
24+
2125
// Loading spinner size adjustment (inherited from generic component)
2226
:host ::ng-deep si-loading-spinner {
2327
--loading-spinner-size: 1.5em;

projects/element-ng/chat-messages/si-ai-message.component.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
Component,
99
effect,
1010
input,
11+
output,
1112
viewChild,
1213
ElementRef,
1314
signal
@@ -18,8 +19,10 @@ import { MenuItem, SiMenuFactoryComponent } from '@siemens/element-ng/menu';
1819
import { SiTranslatePipe, t } from '@siemens/element-translate-ng/translate';
1920

2021
import { MessageAction } from './message-action.model';
22+
import { SiChatAnnotatedText, SiChatCitation } from './si-annotated-text.model';
2123
import { SiChatMessageActionDirective } from './si-chat-message-action.directive';
2224
import { SiChatMessageComponent } from './si-chat-message.component';
25+
import { SiCitationPillComponent } from './si-citation-pill.component';
2326

2427
/**
2528
* AI message component for displaying AI-generated responses in conversational interfaces.
@@ -47,6 +50,7 @@ import { SiChatMessageComponent } from './si-chat-message.component';
4750
imports: [
4851
CdkMenuTrigger,
4952
SiChatMessageComponent,
53+
SiCitationPillComponent,
5054
SiIconComponent,
5155
SiMenuFactoryComponent,
5256
SiChatMessageActionDirective,
@@ -59,6 +63,29 @@ export class SiAiMessageComponent {
5963
protected readonly formattedContent = viewChild<ElementRef<HTMLDivElement>>('formattedContent');
6064
protected readonly icons = addIcons({ elementOptionsVertical });
6165

66+
/**
67+
* Pre-segmented annotated text containing inline citation references.
68+
* When provided, takes precedence over the `content` input.
69+
* Use `parseCitationMarkers` or `parseCitationOffsets` to produce this value.
70+
* @defaultValue undefined
71+
*/
72+
readonly annotatedText = input<SiChatAnnotatedText | undefined>(undefined);
73+
74+
/**
75+
* Emitted when a citation pill inside the message is clicked.
76+
* The emitted value is the {@link SiChatCitation} that was clicked.
77+
*/
78+
readonly citationClicked = output<SiChatCitation>();
79+
80+
protected getCitation(id: string): SiChatCitation {
81+
return (
82+
this.annotatedText()?.citations.find(c => c.id === id) ?? {
83+
id,
84+
title: id
85+
}
86+
);
87+
}
88+
6289
/**
6390
* The AI-generated message content
6491
* @defaultValue ''

0 commit comments

Comments
 (0)