ai/ai-chat/ai-chat-assistant-message.component.ts

Description

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.

Example

Metadata

Relationships

Used by

No results matching.

Depends on

@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>
}

results matching ""

    No results matching ""