ai/ai-chat/ai-chat-assistant-message.component.ts
This is the default component used to render the contents of a message from the AI assistant, including the main answer text, text from earlier steps, reasoning text and tool calls.
| changeDetection | ChangeDetectionStrategy.OnPush |
||||||||||||
| selector | c8y-ai-chat-assistant-message |
||||||||||||
| standalone | true |
||||||||||||
| imports |
C8yTranslatePipe
CollapseModule
|
||||||||||||
| templateVariables |
|
||||||||||||
| templateUrl | ./ai-chat-assistant-message.component.html |
No results matching.
C8yTranslatePipe
IconDirective
CollapseModule
C8yTranslateModule
AiChatAssistantPartComponent
@let ctx = assistantMessageContext();
@let parts = ctx.message.content;
@let _config = ctx.config;
<!--
We show the assistant message in (up to) 3 slots:
- collapsible thinking block (for intermediate step text, standard tool calls, etc)
- special "artifact" tools that update user artifacts and need prominent display (e.g. for code edits made by the AI)
- the main answer (typically from the final step)
This is a standard approach for AI chats - step text and tools from an LLM are treated as a work log, i.e. useful to
display progress while streaming the response, but only the text from the last step after all tool calls gives the main answer of the AI to the user.
Depending on configuration, we have an experimental option (if we need it) to style the thinking block the same as the main answer.
Once the message finishes loading we know the final step IS the main content, but if it's still steaming
we don't know for sure which step has the final/main answer until the LLM has actually finished so have to take a heuistic guess
(and minimize jumping in the UI).
For applications where we expect tool calls in most responses, assume the 2nd step is probably the final text;
for applications where tool calls are less ubiquitous, assume the first step is main answer until proved otherwise.
This minimizes jumping (except when there are >2 steps, which is rare).
-->
@let contentSplit = thinkingAndMainAnswerParts();
@let nonFinalStepTextDisplay = experimental_nonFinalStepTextDisplay(ctx);
<!-- Thinking steps section - holds reasoning, text from non-final steps, and standard/non-artifact tool calls -->
@if (contentSplit.thinkingParts.length > 0) {
<div
class="m-b-8 text-muted"
[class.thinking-block]="nonFinalStepTextDisplay === 'collapsible-thinking-block'"
[class.small]="nonFinalStepTextDisplay === 'muted-main-answer'"
[class.thinking-steps-appearance]="nonFinalStepTextDisplay"
data-cy="thinking-steps"
>
@if (nonFinalStepTextDisplay === 'collapsible-thinking-block') {
<button
class="btn btn-clean btn-xs text-muted p-l-0"
aria-label="{{ 'Toggle collapse of the reasoning section' | translate }}"
[attr.aria-expanded]="thinkingExpanded()"
[attr.aria-controls]="'thinking-content-' + ctx.messageDisplayIndex"
type="button"
(click)="thinkingExpanded.set(!thinkingExpanded())"
>
<span class="small">{{ getThinkingLabel(ctx) }}</span>
<i
class="m-l-4 icon-12"
[c8yIcon]="thinkingExpanded() ? 'collapse-arrow' : 'expand-arrow'"
></i>
</button>
}
@let ariaLabel =
'Reasoning for message number {{ number }}' | translate: { number: ctx.messageDisplayIndex };
<div
class="collapse"
[attr.aria-label]="ariaLabel"
role="region"
[collapse]="nonFinalStepTextDisplay === 'collapsible-thinking-block' && !thinkingExpanded()"
[id]="'thinking-content-' + ctx.messageDisplayIndex"
[isAnimated]="true"
>
<div
[class]="
nonFinalStepTextDisplay === 'collapsible-thinking-block'
? 'thinking-content bg-level-1 m-t-8 p-8 b-r-4 border-left-accent small m-b-0 text-muted'
: ''
"
>
@for (part of contentSplit.thinkingParts; track $index) {
@if (part.type !== 'step-start' || $index > 0) {
<c8y-ai-chat-assistant-part
[part]="part"
[assistantMessageContext]="ctx"
[displayAsPartOfMainAnswer]="nonFinalStepTextDisplay !== 'muted-main-answer'"
></c8y-ai-chat-assistant-part>
}
}
</div>
</div>
</div>
}
<!-- Main answer area contains the text from the final step, and
any promoted tool calls which are too important to be hidden in the collapeable thinking area -->
<div
class="message-content"
data-cy="ai-main-answer-content"
>
@for (part of contentSplit.mainAnswerParts; track $index) {
<c8y-ai-chat-assistant-part
[part]="part"
[assistantMessageContext]="ctx"
[displayAsPartOfMainAnswer]="true"
></c8y-ai-chat-assistant-part>
}
</div>
@if (ctx.isMessageLoading && showWorkingIndicator()) {
<!-- Once the message starts to stream, show a small/low-key thinking indicator -->
<div
class="text-muted text-12 fade-in-out d-flex j-c-end"
aria-live="polite"
role="status"
data-cy="working-indicator"
>
{{ 'Working…' | translate }}
</div>
}