ai/ai-chat/ai-chat.component.ts
An interactive chat interface component for AI-powered conversations. Displays messages in a conversation format with support for loading states, custom configuration, and markdown formatting.
| changeDetection | ChangeDetectionStrategy.OnPush |
| host | { |
| selector | c8y-ai-chat |
| standalone | true |
| imports |
C8yTranslatePipe
MarkdownToHtmlPipe
TooltipModule
NumberPipe
|
| templateUrl | ./ai-chat.component.html |
C8yTranslatePipe
FormsModule
TextareaAutoresizeDirective
IconDirective
NgClass
NgTemplateOutlet
MarkdownToHtmlPipe
AsyncPipe
TooltipModule
NumberPipe
<div
class="d-col fit-h fit-w flex-grow min-height-0"
[attr.aria-label]="config.headline | translate"
role="region"
>
@if (messages().length > 0) {
<div
[attr.aria-label]="'Chat conversation' | translate"
aria-live="polite"
aria-atomic="false"
role="log"
#chatScrollContainer
[ngClass]="{
'inner-scroll': true,
'd-col-reverse': true,
'scrollbar-only-on-hover': config.scrollbarOnlyOnHover,
'flex-grow': true,
'min-height-0': true,
'bg-level-0': true
}"
>
<div class="d-col p-l-16 p-r-16">
<ng-content select="[slot='before-messages']"></ng-content>
<ng-content select="c8y-ai-chat-message"></ng-content>
<ng-content select="[slot='after-messages']"></ng-content>
</div>
</div>
}
<div
[ngClass]="{
'd-col fit-h': messages().length === 0
}"
>
@if (messages().length === 0) {
<div
class="p-24 d-col fit-h inner-scroll"
aria-live="polite"
role="status"
[ngClass]="{
'j-c-start': config.welcomePosition === 'top',
'j-c-center': config.welcomePosition === 'center',
'j-c-end': config.welcomePosition === 'bottom'
}"
>
<h4 class="m-b-16 text-medium">{{ config.headline | translate }}</h4>
@if (config.title.length > 0) {
<p class="p-b-8 text-balance">{{ config.title | translate }}</p>
}
<div class="text-balance chat-message min-height-0">
<div
class="text-muted"
[innerHTML]="config.welcomeText | translate | markdownToHtml | async"
></div>
@if (welcomeTemplate) {
<ng-container *ngTemplateOutlet="welcomeTemplate"></ng-container>
}
</div>
</div>
}
<div
class="chat-input"
[class.bg-level-1]="config.appearance !== 'flat'"
>
<!-- For simple cases allow ng-content projection; however this doesn't seem to work with dynamic
suggestion lists from a signal (e.g. returned from the AI) so also support `suggestionsTemplate` for that.
-->
@if (!isLoading) {
<div
[class]="
'd-flex inner-scroll gap-8 p-l-16 p-r-16 p-b-8 ' +
(config.suggestionsLayout === 'vertical' ? 'flex-wrap' : 'a-i-center')
"
>
<ng-content select="c8y-ai-chat-suggestion"></ng-content>
@if (suggestionsTemplate) {
<ng-container *ngTemplateOutlet="suggestionsTemplate"></ng-container>
}
</div>
}
<div class="chat-input-group">
<label
class="sr-only"
for="chat-input-{{ componentId }}"
>
{{ config.placeholder | translate }}
</label>
<textarea
class="form-control no-resize"
[class.text-muted]="isLoading"
style="max-height: 200px !important"
[attr.aria-label]="config.placeholder | translate"
id="chat-input-{{ componentId }}"
placeholder="{{ config.placeholder | translate }}"
[attr.aria-describedby]="config.disclaimerText ? 'chat-disclaimer-' + componentId : null"
[attr.aria-busy]="isLoading"
[(ngModel)]="prompt"
(keydown.enter)="!isLoading && sendMessage($event)"
[disabled]="disabled"
c8y-textarea-autoresize
></textarea>
<div class="chat-input-group-btn">
@if (!isLoading) {
<button
class="btn btn-dot"
[attr.title]="config.sendButtonText || '' | translate"
[attr.aria-label]="config.sendButtonText || '' | translate"
type="button"
(click)="sendMessage($event)"
[disabled]="disabled || prompt.trim().length === 0"
>
<i [c8yIcon]="config.userInterfaceIcons.send || 'arrow-circle-right'"></i>
</button>
} @else {
<button
class="btn btn-dot btn-dot--danger"
[attr.title]="config.cancelButtonText || '' | translate"
[attr.aria-label]="config.cancelButtonText || '' | translate"
type="button"
(click)="cancel()"
>
<i [c8yIcon]="config.userInterfaceIcons.cancel || 'stop'"></i>
</button>
}
</div>
</div>
<div class="d-flex">
@if (config.disclaimerText) {
<div
class="text-muted m-b-8 text-10 p-l-16"
id="chat-disclaimer-{{ componentId }}"
role="note"
>
{{ config.disclaimerText | translate }}
</div>
}
@if (cumulativeUsage) {
<span
class="tag tag--info m-l-auto"
[tooltip]="
'Input tokens: {{ input }} \n Output tokens: {{ output }}'
| translate
: {
input: cumulativeUsage?.inputTokens || 0 | c8yNumber: 'floor' : '1.0-0',
output: cumulativeUsage?.outputTokens || 0 | c8yNumber: 'floor' : '1.0-0'
}
"
>
{{ 'Tokens used: {{total}}' | translate: { total: (cumulativeUsage?.totalTokens || 0) |
c8yNumber : 'floor':'1.0-0' } }}
</span>
}
</div>
</div>
</div>
</div>