All files / chat-messages si-user-message.component.ts

92.59% Statements 25/27
75% Branches 6/8
100% Functions 4/4
92.3% Lines 24/26

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137                                                                                                      1x 16x           16x                           16x             16x           16x           16x     16x                   16x 16x     16x   16x     16x 23x 23x 23x   23x 7x 7x   7x   7x 7x 7x 7x                  
/**
 * Copyright (c) Siemens 2016 - 2025
 * SPDX-License-Identifier: MIT
 */
import { CdkMenuTrigger } from '@angular/cdk/menu';
import { Component, effect, input, viewChild, ElementRef, computed, signal } from '@angular/core';
import { SiIconComponent } from '@siemens/element-ng/icon';
import { MenuItem, SiMenuFactoryComponent } from '@siemens/element-ng/menu';
import { SiTranslatePipe, t } from '@siemens/element-translate-ng/translate';
 
import { Attachment, MessageAction } from './chat-message.model';
import { SiAttachmentListComponent } from './si-attachment-list.component';
import { SiChatMessageActionDirective } from './si-chat-message-action.directive';
import { SiChatMessageComponent } from './si-chat-message.component';
 
/**
 * User message component for displaying the user's messages in conversational interfaces.
 *
 * The user message component renders user-submitted content in (AI) chat interfaces,
 * supporting text, attachments, and contextual actions. It appears as a text bubble
 * aligned to the right side and supports markdown formatting for rich content.
 * Can be used within {@link SiChatContainerComponent}.
 *
 * The component automatically handles:
 * - Styling for user messages distinct from AI or generic chat messages
 * - Option to render markdown content, provide via `contentFormatter` input with a markdown renderer function (e.g., from {@link getMarkdownRenderer})
 * - Displaying attachments above the message bubble
 * - Displaying primary and secondary actions
 *
 * @see {@link SiChatMessageComponent} for the base message wrapper component
 * @see {@link SiAiMessageComponent} for the AI message component
 * @see {@link SiAttachmentListComponent} for the base attachment component
 * @see {@link getMarkdownRenderer} for markdown formatting support
 * @see {@link SiChatContainerComponent} for the chat container to use this within
 *
 * @experimental
 */
@Component({
  selector: 'si-user-message',
  imports: [
    CdkMenuTrigger,
    SiAttachmentListComponent,
    SiChatMessageComponent,
    SiIconComponent,
    SiMenuFactoryComponent,
    SiChatMessageActionDirective,
    SiTranslatePipe
  ],
  templateUrl: './si-user-message.component.html',
  styleUrl: './si-user-message.component.scss'
})
export class SiUserMessageComponent {
  protected readonly formattedContent = viewChild<ElementRef<HTMLDivElement>>('formattedContent');
 
  /**
   * The user message content
   * @defaultValue ''
   */
  readonly content = input<string>('');
 
  /**
   * Optional formatter function to transform content before display.
   * - Returns string: Content will be inserted as text with built-in sanitization
   * - Returns Node: DOM node will be inserted directly without sanitization
   *
   * **Note:** When returning a Node with formatted content, apply the `markdown-content` class
   * to the root element to ensure proper styling (e.g., `div.className = 'markdown-content'`).
   * The function returned by {@link getMarkdownRenderer} does this automatically.
   *
   * **Warning:** When returning a Node, ensure the content is safe to prevent XSS attacks
   * @defaultValue undefined
   */
  readonly contentFormatter = input<((text: string) => string | Node) | undefined>(undefined);
 
  /**
   * Primary message actions (edit, delete, copy, etc.).
   * All actions displayed inline
   * @defaultValue []
   */
  readonly actions = input<MessageAction[]>([]);
 
  /**
   * Secondary actions available in dropdown menu, first use primary actions and only add secondary actions additionally
   * @defaultValue []
   */
  readonly secondaryActions = input<MenuItem[]>([]);
 
  /**
   * List of attachments included with this message
   * @defaultValue []
   */
  readonly attachments = input<Attachment[]>([]);
 
  /** Parameter to pass to action handlers */
  readonly actionParam = input<any>();
 
  /**
   * More actions button aria label
   *
   * @defaultValue
   * ```
   * t(() => $localize`:@@SI_USER_MESSAGE.SECONDARY_ACTIONS:More actions`)
   * ```
   */
  readonly secondaryActionsLabel = input(
    t(() => $localize`:@@SI_USER_MESSAGE.SECONDARY_ACTIONS:More actions`)
  );
 
  protected readonly hasAttachments = computed(() => this.attachments()?.length > 0);
 
  protected readonly textContent = signal<string | undefined>(undefined);
 
  constructor() {
    effect(() => {
      const formatter = this.contentFormatter();
      const contentValue = this.content();
      const container = this.formattedContent()?.nativeElement;
 
      if (container && contentValue) {
        if (formatter) {
          const formatted = formatter(contentValue);
 
          Iif (typeof formatted === 'string') {
            this.textContent.set(formatted);
          } else if (formatted instanceof Node) {
            this.textContent.set(undefined);
            container.innerHTML = '';
            container.appendChild(formatted);
          }
        } else E{
          this.textContent.set(contentValue);
        }
      }
    });
  }
}