@siemens/element-ng 48.2.0 → 48.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/accordion/index.d.ts +5 -1
  2. package/application-header/index.d.ts +15 -2
  3. package/chat-messages/index.d.ts +654 -0
  4. package/chat-messages/package.json +3 -0
  5. package/dashboard/index.d.ts +1 -0
  6. package/fesm2022/siemens-element-ng-accordion.mjs +5 -1
  7. package/fesm2022/siemens-element-ng-accordion.mjs.map +1 -1
  8. package/fesm2022/siemens-element-ng-application-header.mjs +62 -1
  9. package/fesm2022/siemens-element-ng-application-header.mjs.map +1 -1
  10. package/fesm2022/siemens-element-ng-card.mjs +4 -4
  11. package/fesm2022/siemens-element-ng-card.mjs.map +1 -1
  12. package/fesm2022/siemens-element-ng-chat-messages.mjs +863 -0
  13. package/fesm2022/siemens-element-ng-chat-messages.mjs.map +1 -0
  14. package/fesm2022/siemens-element-ng-dashboard.mjs +8 -4
  15. package/fesm2022/siemens-element-ng-dashboard.mjs.map +1 -1
  16. package/fesm2022/siemens-element-ng-file-uploader.mjs +277 -118
  17. package/fesm2022/siemens-element-ng-file-uploader.mjs.map +1 -1
  18. package/fesm2022/siemens-element-ng-filtered-search.mjs +3 -4
  19. package/fesm2022/siemens-element-ng-filtered-search.mjs.map +1 -1
  20. package/fesm2022/siemens-element-ng-icon.mjs +3 -1
  21. package/fesm2022/siemens-element-ng-icon.mjs.map +1 -1
  22. package/fesm2022/siemens-element-ng-ip-input.mjs +92 -89
  23. package/fesm2022/siemens-element-ng-ip-input.mjs.map +1 -1
  24. package/fesm2022/siemens-element-ng-markdown-renderer.mjs +253 -0
  25. package/fesm2022/siemens-element-ng-markdown-renderer.mjs.map +1 -0
  26. package/fesm2022/siemens-element-ng-phone-number.mjs +5 -4
  27. package/fesm2022/siemens-element-ng-phone-number.mjs.map +1 -1
  28. package/fesm2022/siemens-element-ng-popover.mjs +3 -4
  29. package/fesm2022/siemens-element-ng-popover.mjs.map +1 -1
  30. package/fesm2022/siemens-element-ng-resize-observer.mjs +13 -0
  31. package/fesm2022/siemens-element-ng-resize-observer.mjs.map +1 -1
  32. package/fesm2022/siemens-element-ng-translate.mjs.map +1 -1
  33. package/fesm2022/siemens-element-ng-tree-view.mjs +41 -2
  34. package/fesm2022/siemens-element-ng-tree-view.mjs.map +1 -1
  35. package/file-uploader/index.d.ts +119 -15
  36. package/icon/index.d.ts +3 -1
  37. package/ip-input/index.d.ts +13 -0
  38. package/markdown-renderer/index.d.ts +36 -0
  39. package/markdown-renderer/package.json +3 -0
  40. package/package.json +11 -3
  41. package/resize-observer/index.d.ts +13 -0
  42. package/schematics/migrations/action-modal-migration/action-modal-migration.js +2 -2
  43. package/schematics/migrations/data/attribute-selectors.js +6 -0
  44. package/schematics/migrations/data/component-names.js +78 -0
  45. package/schematics/migrations/data/element-selectors.js +10 -0
  46. package/schematics/migrations/data/index.js +17 -0
  47. package/schematics/migrations/data/output-names.js +8 -0
  48. package/schematics/migrations/data/symbol-removals.js +58 -0
  49. package/schematics/migrations/element-migration/element-migration.js +101 -0
  50. package/schematics/migrations/element-migration/index.js +5 -0
  51. package/schematics/migrations/index.js +7 -2
  52. package/schematics/migrations/wizard-migration/index.js +88 -0
  53. package/schematics/scss-import-to-siemens-migration/index.js +3 -3
  54. package/schematics/ts-import-to-siemens-migration/index.js +2 -2
  55. package/schematics/utils/index.js +3 -3
  56. package/schematics/utils/project-utils.js +24 -35
  57. package/schematics/utils/template-utils.js +78 -2
  58. package/schematics/utils/ts-utils.js +5 -5
  59. package/template-i18n.json +9 -0
  60. package/translate/index.d.ts +9 -0
  61. package/tree-view/index.d.ts +40 -1
  62. package/schematics/migrations/to-legacy-migration/to-legacy-migration.js +0 -55
  63. package/schematics/migrations/to-legacy-migration/to-legacy-replacement.js +0 -35
@@ -0,0 +1,863 @@
1
+ import { CdkMenuTrigger } from '@angular/cdk/menu';
2
+ import * as i0 from '@angular/core';
3
+ import { Directive, input, Component, viewChild, signal, effect, booleanAttribute, inject, output, model, computed } from '@angular/core';
4
+ import { SiIconComponent } from '@siemens/element-ng/icon';
5
+ import { SiMenuFactoryComponent } from '@siemens/element-ng/menu';
6
+ import { t, SiTranslatePipe } from '@siemens/element-translate-ng/translate';
7
+ import * as i1 from '@siemens/element-ng/resize-observer';
8
+ import { SiResponsiveContainerDirective } from '@siemens/element-ng/resize-observer';
9
+ import { SiModalService } from '@siemens/element-ng/modal';
10
+ import * as i1$1 from '@angular/forms';
11
+ import { FormsModule } from '@angular/forms';
12
+ import { SiFileUploadDirective } from '@siemens/element-ng/file-uploader';
13
+
14
+ /**
15
+ * Copyright (c) Siemens 2016 - 2025
16
+ * SPDX-License-Identifier: MIT
17
+ */
18
+ /**
19
+ * Directive to mark content as chat message actions.
20
+ * Apply this directive to e.g. buttons that should be slotted into the message actions area.
21
+ *
22
+ * @experimental
23
+ * @example
24
+ * ```html
25
+ * <si-chat-message>
26
+ * Message content
27
+ * <button siChatMessageAction>Like</button>
28
+ * <button siChatMessageAction>Share</button>
29
+ * </si-chat-message>
30
+ * ```
31
+ */
32
+ class SiChatMessageActionDirective {
33
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiChatMessageActionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
34
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.0.6", type: SiChatMessageActionDirective, isStandalone: true, selector: "[siChatMessageAction]", ngImport: i0 });
35
+ }
36
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiChatMessageActionDirective, decorators: [{
37
+ type: Directive,
38
+ args: [{
39
+ selector: '[siChatMessageAction]'
40
+ }]
41
+ }] });
42
+
43
+ /**
44
+ * Copyright (c) Siemens 2016 - 2025
45
+ * SPDX-License-Identifier: MIT
46
+ */
47
+ /**
48
+ * Base chat message component that provides the layout structure for conversational interfaces.
49
+ *
50
+ * This component handles the core message layout including avatar positioning, loading states,
51
+ * and action button placement. It serves as the foundation for more specialized message components
52
+ * like {@link SiUserMessageComponent} and {@link SiAiMessageComponent}.
53
+ *
54
+ * @remarks
55
+ * The component provides:
56
+ * - Flexible alignment (start/end) for different message types
57
+ * - Avatar/icon slot for message attribution
58
+ * - Loading state with skeleton UI
59
+ * - Action buttons positioned on the side or bottom
60
+ * - Responsive behavior that adapts to container size
61
+ * - Attachment display slot
62
+ *
63
+ * This is a low-level component typically not used directly. Instead, use the higher-level
64
+ * message components that wrap this component with specific styling and behavior.
65
+ *
66
+ * @experimental
67
+ */
68
+ class SiChatMessageComponent {
69
+ /**
70
+ * Whether the message is currently loading
71
+ * @defaultValue false
72
+ */
73
+ loading = input(false);
74
+ /**
75
+ * Alignment of the message
76
+ * @defaultValue 'start'
77
+ */
78
+ alignment = input('start');
79
+ /**
80
+ * Where to display action buttons (if any)
81
+ * @defaultValue 'side'
82
+ */
83
+ actionsPosition = input('side');
84
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiChatMessageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
85
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: SiChatMessageComponent, isStandalone: true, selector: "si-chat-message", inputs: { loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, alignment: { classPropertyName: "alignment", publicName: "alignment", isSignal: true, isRequired: false, transformFunction: null }, actionsPosition: { classPropertyName: "actionsPosition", publicName: "actionsPosition", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "d-block" }, hostDirectives: [{ directive: i1.SiResponsiveContainerDirective }], ngImport: i0, template: "<!--- Flex-row if alignment start, flex-row-reverse if alignment end, flex-column if mobile -->\n<div class=\"d-flex si-body-2 chat-message-container\" [class.start]=\"alignment() === 'start'\">\n <div class=\"avatar-wrapper flex-shrink-0\" [class.end]=\"alignment() === 'end'\">\n <ng-content select=\"si-icon,si-avatar,img\" />\n </div>\n\n <div class=\"d-flex flex-column flex-grow-1 w-100\">\n <div class=\"attachment-slot\" [class.align-self-end]=\"alignment() === 'end'\">\n <ng-content select=\"si-attachment-list,si-badge\" />\n </div>\n\n @if (loading()) {\n <div\n class=\"message-wrapper rounded-3 w-75 text-break loading-message-bubble mb-2\"\n [class.align-self-end]=\"alignment() === 'end'\"\n >\n <div class=\"d-flex flex-column w-100 gap-2\">\n <div class=\"si-skeleton skeleton-line skeleton-line-full\"></div>\n <div class=\"si-skeleton skeleton-line skeleton-line-half\"></div>\n </div>\n </div>\n } @else {\n <!-- Flex-column if actions bottom, flex-row/flex-row-reverse if actions start -->\n <div\n class=\"message-wrapper mw-0 d-flex mb-2\"\n [class.end]=\"alignment() === 'end'\"\n [class.flex-column]=\"actionsPosition() === 'bottom'\"\n [class.flex-row]=\"actionsPosition() === 'side' && alignment() === 'start'\"\n [class.flex-row-reverse]=\"actionsPosition() === 'side' && alignment() === 'end'\"\n [class.align-items-start]=\"actionsPosition() === 'side'\"\n [class.align-items-end]=\"actionsPosition() === 'bottom' && alignment() === 'end'\"\n [class.justify-content-end]=\"alignment() === 'end' && actionsPosition() === 'bottom'\"\n [class.justify-content-start]=\"alignment() === 'start' && actionsPosition() === 'bottom'\"\n >\n <div\n class=\"rounded-3 text-break message-bubble\"\n [class.end]=\"alignment() === 'end' && actionsPosition() === 'bottom'\"\n >\n <ng-content />\n </div>\n\n <div\n class=\"actions-wrapper d-flex gap-4\"\n [class.ms-3]=\"actionsPosition() === 'side' && alignment() === 'start'\"\n [class.me-3]=\"actionsPosition() === 'side' && alignment() === 'end'\"\n [class.mt-2]=\"actionsPosition() === 'bottom'\"\n [class.actions-horizontal]=\"actionsPosition() !== 'bottom'\"\n [class.align-self-start]=\"actionsPosition() === 'side'\"\n >\n <ng-content select=\"[siChatMessageAction]\" />\n </div>\n </div>\n }\n </div>\n</div>\n", styles: [":host{display:block;--chat-message-bubble-bg: var(--element-base-1);--chat-message-bubble-padding: 12px}:host ::ng-deep si-loading-spinner{--loading-spinner-size: 1.5em}.skeleton-line-full{inline-size:100%}.skeleton-line-half{inline-size:50%}.loading-message-bubble,.message-bubble{padding:var(--chat-message-bubble-padding)}.loading-message-bubble{inline-size:max-content;min-inline-size:75%;margin-block-end:auto;background-color:var(--chat-message-bubble-bg)}.message-bubble{margin-block-end:auto;background-color:var(--chat-message-bubble-bg);min-inline-size:0;overflow-wrap:break-word;word-break:break-word}.message-bubble:empty{display:none}.message-bubble:empty~.actions-wrapper{margin-inline:0!important}.message-wrapper{min-inline-size:0}.attachment-slot:empty{display:none}.attachment-slot:not(:empty)~.message-wrapper:not(:has(.message-bubble:empty)){margin-block-start:4px}.attachment-slot:not(:empty)~.message-wrapper:has(.message-bubble:empty) .actions-wrapper.actions-horizontal{margin-block-start:4px}.actions-wrapper:empty{display:none!important}.avatar-wrapper{align-self:flex-start}.avatar-wrapper:not(.end){margin-inline-end:6px}.avatar-wrapper.end{margin-inline-start:6px}.avatar-wrapper:empty{display:none}:host-context(.si-container-md,.si-container-lg,.si-container-xl,.si-container-xxl) .chat-message-container{flex-direction:row}:host-context(.si-container-md,.si-container-lg,.si-container-xl,.si-container-xxl) .chat-message-container.start{align-items:flex-start}:host-context(.si-container-md,.si-container-lg,.si-container-xl,.si-container-xxl) .chat-message-container:not(.start){flex-direction:row-reverse}:host-context(.si-container-md,.si-container-lg,.si-container-xl,.si-container-xxl) .message-wrapper{min-inline-size:0;inline-size:auto}:host-context(.si-container-md,.si-container-lg,.si-container-xl,.si-container-xxl) .message-wrapper.end{margin-inline-start:auto;display:flex;flex-direction:column;align-items:flex-end}:host-context(.si-container-md,.si-container-lg,.si-container-xl,.si-container-xxl) .message-bubble.end{margin-inline-start:auto}.attachment-slot ::ng-deep si-attachment-list{--attachment-list-bg: transparent;--attachment-name-color: var(--element-text-secondary)}:host-context(.si-container-xs,.si-container-sm) .message-wrapper{min-inline-size:0}:host-context(.si-container-xs,.si-container-sm) .message-wrapper.end{margin-inline-start:auto}:host-context(.si-container-xs,.si-container-sm) .chat-message-container{flex-direction:column}:host-context(.si-container-xs,.si-container-sm) .avatar-wrapper{margin-block-end:6px}:host-context(.si-container-xs,.si-container-sm) .avatar-wrapper.end{align-self:flex-end}:host-context(.si-container-xs,.si-container-sm) .avatar-wrapper::ng-deep:has(si-icon) .end{margin-inline-end:4px!important}:host-context(.si-container-xs,.si-container-sm) .avatar-wrapper::ng-deep:has(si-icon):not(.end){margin-inline-start:4px!important}\n"] });
86
+ }
87
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiChatMessageComponent, decorators: [{
88
+ type: Component,
89
+ args: [{ selector: 'si-chat-message', host: {
90
+ class: 'd-block'
91
+ }, hostDirectives: [SiResponsiveContainerDirective], template: "<!--- Flex-row if alignment start, flex-row-reverse if alignment end, flex-column if mobile -->\n<div class=\"d-flex si-body-2 chat-message-container\" [class.start]=\"alignment() === 'start'\">\n <div class=\"avatar-wrapper flex-shrink-0\" [class.end]=\"alignment() === 'end'\">\n <ng-content select=\"si-icon,si-avatar,img\" />\n </div>\n\n <div class=\"d-flex flex-column flex-grow-1 w-100\">\n <div class=\"attachment-slot\" [class.align-self-end]=\"alignment() === 'end'\">\n <ng-content select=\"si-attachment-list,si-badge\" />\n </div>\n\n @if (loading()) {\n <div\n class=\"message-wrapper rounded-3 w-75 text-break loading-message-bubble mb-2\"\n [class.align-self-end]=\"alignment() === 'end'\"\n >\n <div class=\"d-flex flex-column w-100 gap-2\">\n <div class=\"si-skeleton skeleton-line skeleton-line-full\"></div>\n <div class=\"si-skeleton skeleton-line skeleton-line-half\"></div>\n </div>\n </div>\n } @else {\n <!-- Flex-column if actions bottom, flex-row/flex-row-reverse if actions start -->\n <div\n class=\"message-wrapper mw-0 d-flex mb-2\"\n [class.end]=\"alignment() === 'end'\"\n [class.flex-column]=\"actionsPosition() === 'bottom'\"\n [class.flex-row]=\"actionsPosition() === 'side' && alignment() === 'start'\"\n [class.flex-row-reverse]=\"actionsPosition() === 'side' && alignment() === 'end'\"\n [class.align-items-start]=\"actionsPosition() === 'side'\"\n [class.align-items-end]=\"actionsPosition() === 'bottom' && alignment() === 'end'\"\n [class.justify-content-end]=\"alignment() === 'end' && actionsPosition() === 'bottom'\"\n [class.justify-content-start]=\"alignment() === 'start' && actionsPosition() === 'bottom'\"\n >\n <div\n class=\"rounded-3 text-break message-bubble\"\n [class.end]=\"alignment() === 'end' && actionsPosition() === 'bottom'\"\n >\n <ng-content />\n </div>\n\n <div\n class=\"actions-wrapper d-flex gap-4\"\n [class.ms-3]=\"actionsPosition() === 'side' && alignment() === 'start'\"\n [class.me-3]=\"actionsPosition() === 'side' && alignment() === 'end'\"\n [class.mt-2]=\"actionsPosition() === 'bottom'\"\n [class.actions-horizontal]=\"actionsPosition() !== 'bottom'\"\n [class.align-self-start]=\"actionsPosition() === 'side'\"\n >\n <ng-content select=\"[siChatMessageAction]\" />\n </div>\n </div>\n }\n </div>\n</div>\n", styles: [":host{display:block;--chat-message-bubble-bg: var(--element-base-1);--chat-message-bubble-padding: 12px}:host ::ng-deep si-loading-spinner{--loading-spinner-size: 1.5em}.skeleton-line-full{inline-size:100%}.skeleton-line-half{inline-size:50%}.loading-message-bubble,.message-bubble{padding:var(--chat-message-bubble-padding)}.loading-message-bubble{inline-size:max-content;min-inline-size:75%;margin-block-end:auto;background-color:var(--chat-message-bubble-bg)}.message-bubble{margin-block-end:auto;background-color:var(--chat-message-bubble-bg);min-inline-size:0;overflow-wrap:break-word;word-break:break-word}.message-bubble:empty{display:none}.message-bubble:empty~.actions-wrapper{margin-inline:0!important}.message-wrapper{min-inline-size:0}.attachment-slot:empty{display:none}.attachment-slot:not(:empty)~.message-wrapper:not(:has(.message-bubble:empty)){margin-block-start:4px}.attachment-slot:not(:empty)~.message-wrapper:has(.message-bubble:empty) .actions-wrapper.actions-horizontal{margin-block-start:4px}.actions-wrapper:empty{display:none!important}.avatar-wrapper{align-self:flex-start}.avatar-wrapper:not(.end){margin-inline-end:6px}.avatar-wrapper.end{margin-inline-start:6px}.avatar-wrapper:empty{display:none}:host-context(.si-container-md,.si-container-lg,.si-container-xl,.si-container-xxl) .chat-message-container{flex-direction:row}:host-context(.si-container-md,.si-container-lg,.si-container-xl,.si-container-xxl) .chat-message-container.start{align-items:flex-start}:host-context(.si-container-md,.si-container-lg,.si-container-xl,.si-container-xxl) .chat-message-container:not(.start){flex-direction:row-reverse}:host-context(.si-container-md,.si-container-lg,.si-container-xl,.si-container-xxl) .message-wrapper{min-inline-size:0;inline-size:auto}:host-context(.si-container-md,.si-container-lg,.si-container-xl,.si-container-xxl) .message-wrapper.end{margin-inline-start:auto;display:flex;flex-direction:column;align-items:flex-end}:host-context(.si-container-md,.si-container-lg,.si-container-xl,.si-container-xxl) .message-bubble.end{margin-inline-start:auto}.attachment-slot ::ng-deep si-attachment-list{--attachment-list-bg: transparent;--attachment-name-color: var(--element-text-secondary)}:host-context(.si-container-xs,.si-container-sm) .message-wrapper{min-inline-size:0}:host-context(.si-container-xs,.si-container-sm) .message-wrapper.end{margin-inline-start:auto}:host-context(.si-container-xs,.si-container-sm) .chat-message-container{flex-direction:column}:host-context(.si-container-xs,.si-container-sm) .avatar-wrapper{margin-block-end:6px}:host-context(.si-container-xs,.si-container-sm) .avatar-wrapper.end{align-self:flex-end}:host-context(.si-container-xs,.si-container-sm) .avatar-wrapper::ng-deep:has(si-icon) .end{margin-inline-end:4px!important}:host-context(.si-container-xs,.si-container-sm) .avatar-wrapper::ng-deep:has(si-icon):not(.end){margin-inline-start:4px!important}\n"] }]
92
+ }] });
93
+
94
+ /**
95
+ * Copyright (c) Siemens 2016 - 2025
96
+ * SPDX-License-Identifier: MIT
97
+ */
98
+ /**
99
+ * AI message component for displaying AI-generated responses in conversational interfaces.
100
+ *
101
+ * The AI message component renders AI-generated content in chat interfaces,
102
+ * supporting text formatting, markdown, loading states, and contextual actions.
103
+ * It appears as text (no bubble) aligned to the left side without any avatar/icon slot.
104
+ *
105
+ * @remarks
106
+ * This component is designed for use in:
107
+ * - AI chat interfaces where model responses need to be displayed
108
+ * - Chatbot implementations
109
+ * - Conversational AI applications
110
+ * - AI assistant interfaces
111
+ *
112
+ * The component automatically handles:
113
+ * - Rendering markdown content with syntax highlighting
114
+ * - Showing loading states with skeleton UI during generation
115
+ * - Displaying primary and secondary actions on hover (desktop) or tap (mobile)
116
+ * - Proper alignment and styling for AI messages
117
+ * - Content sanitization for security
118
+ *
119
+ * @example
120
+ * Basic usage with content only:
121
+ * ```html
122
+ * <si-ai-message [content]="'I can help you with that.'" />
123
+ * ```
124
+ *
125
+ * @example
126
+ * With loading state and actions:
127
+ * ```typescript
128
+ * import { Component } from '@angular/core';
129
+ * import { SiAiMessageComponent } from '@siemens/element-ng/chat-messages';
130
+ *
131
+ * @Component({
132
+ * selector: 'app-chat',
133
+ * imports: [SiAiMessageComponent],
134
+ * template: `
135
+ * <si-ai-message
136
+ * [content]="message.text"
137
+ * [loading]="message.isGenerating"
138
+ * [actions]="messageActions"
139
+ * [secondaryActions]="menuActions"
140
+ * [actionParam]="message"
141
+ * />
142
+ * `
143
+ * })
144
+ * export class ChatComponent {
145
+ * messageActions = [
146
+ * { icon: 'thumbs-up', label: 'Good response', action: (id) => this.ratePositive(id) },
147
+ * { icon: 'thumbs-down', label: 'Bad response', action: (id) => this.rateNegative(id) },
148
+ * { icon: 'copy', label: 'Copy', action: (id) => this.copyMessage(id) }
149
+ * ];
150
+ *
151
+ * menuActions = [
152
+ * { label: 'Regenerate', action: (id) => this.regenerate(id) },
153
+ * { label: 'Report', action: (id) => this.reportMessage(id) }
154
+ * ];
155
+ * }
156
+ * ```
157
+ *
158
+ * @see {@link SiChatMessageComponent} for the base message wrapper component
159
+ * @see {@link SiUserMessageComponent} for the companion user message component
160
+ * @see {@link getMarkdownRenderer} for markdown formatting support
161
+ *
162
+ * @experimental
163
+ */
164
+ class SiAiMessageComponent {
165
+ formattedContent = viewChild('formattedContent');
166
+ /**
167
+ * The AI-generated message content
168
+ * @defaultValue ''
169
+ */
170
+ content = input('');
171
+ /**
172
+ * Optional formatter function to transform content before display.
173
+ * - Returns string: Content will be sanitized using Angular's DomSanitizer
174
+ * - Returns Node: DOM node will be inserted directly without sanitization
175
+ *
176
+ * **Note:** If using a markdown renderer, make sure to apply the `markdown-content` class
177
+ * to the root element to ensure proper styling using the Element theme (e.g., `div.className = 'markdown-content'`).
178
+ * The function returned by {@link getMarkdownRenderer} does this automatically.
179
+ *
180
+ * **Warning:** When returning a Node, ensure the content is safe to prevent XSS attacks
181
+ * @defaultValue undefined
182
+ */
183
+ contentFormatter = input(undefined);
184
+ textContent = signal(undefined);
185
+ constructor() {
186
+ effect(() => {
187
+ const formatter = this.contentFormatter();
188
+ const contentValue = this.content();
189
+ const container = this.formattedContent()?.nativeElement;
190
+ if (container && contentValue) {
191
+ if (formatter) {
192
+ const formatted = formatter(contentValue);
193
+ if (typeof formatted === 'string') {
194
+ this.textContent.set(formatted);
195
+ }
196
+ else if (formatted instanceof Node) {
197
+ this.textContent.set(undefined);
198
+ container.innerHTML = '';
199
+ container.appendChild(formatted);
200
+ }
201
+ }
202
+ else {
203
+ this.textContent.set(contentValue);
204
+ }
205
+ }
206
+ });
207
+ }
208
+ /**
209
+ * Whether the message is currently being generated (shows skeleton)
210
+ * @defaultValue false
211
+ */
212
+ loading = input(false, { transform: booleanAttribute });
213
+ /**
214
+ * Primary actions available for this message (thumbs up/down, copy, retry, etc.)
215
+ * All actions displayed inline
216
+ * @defaultValue []
217
+ */
218
+ actions = input([]);
219
+ /**
220
+ * Secondary actions available in dropdown menu, first use primary actions and only add secondary actions additionally
221
+ * @defaultValue []
222
+ */
223
+ secondaryActions = input([]);
224
+ /** Parameter to pass to action handlers */
225
+ actionParam = input();
226
+ /**
227
+ * More actions button aria label
228
+ *
229
+ * @defaultValue
230
+ * ```
231
+ * t(() => $localize`:@@SI_AI_MESSAGE.SECONDARY_ACTIONS:More actions`)
232
+ * ```
233
+ */
234
+ secondaryActionsLabel = input(t(() => $localize `:@@SI_AI_MESSAGE.SECONDARY_ACTIONS:More actions`));
235
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiAiMessageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
236
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: SiAiMessageComponent, isStandalone: true, selector: "si-ai-message", inputs: { content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired: false, transformFunction: null }, contentFormatter: { classPropertyName: "contentFormatter", publicName: "contentFormatter", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, actions: { classPropertyName: "actions", publicName: "actions", isSignal: true, isRequired: false, transformFunction: null }, secondaryActions: { classPropertyName: "secondaryActions", publicName: "secondaryActions", isSignal: true, isRequired: false, transformFunction: null }, actionParam: { classPropertyName: "actionParam", publicName: "actionParam", isSignal: true, isRequired: false, transformFunction: null }, secondaryActionsLabel: { classPropertyName: "secondaryActionsLabel", publicName: "secondaryActionsLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "si-ai-message" }, viewQueries: [{ propertyName: "formattedContent", first: true, predicate: ["formattedContent"], descendants: true, isSignal: true }], ngImport: i0, template: "<si-chat-message alignment=\"start\" actionsPosition=\"bottom\" [loading]=\"loading()\">\n @if (content()) {\n @let content = textContent();\n @if (content) {\n <span class=\"text-pre-wrap\">{{ content }}</span>\n } @else {\n <div #formattedContent> </div>\n }\n }\n\n @if (actions().length > 0 || secondaryActions().length > 0) {\n <div class=\"d-flex gap-4 ai-message-actions\" siChatMessageAction>\n @for (action of actions(); track $index) {\n <button\n type=\"button\"\n class=\"btn btn-ghost btn-circle btn-sm\"\n [disabled]=\"action.disabled\"\n [attr.aria-label]=\"action.label | translate\"\n (click)=\"action.action(actionParam(), action)\"\n >\n <si-icon [icon]=\"action.icon\" />\n </button>\n }\n\n @if (secondaryActions().length > 0) {\n <button\n type=\"button\"\n class=\"btn btn-ghost btn-circle btn-sm\"\n [cdkMenuTriggerFor]=\"secondaryActionsMenu\"\n [attr.aria-label]=\"secondaryActionsLabel() | translate\"\n [attr.title]=\"secondaryActionsLabel() | translate\"\n >\n <si-icon icon=\"element-optionsVertical\" />\n </button>\n\n <ng-template #secondaryActionsMenu>\n <si-menu-factory [items]=\"secondaryActions()\" [actionParam]=\"actionParam()\" />\n </ng-template>\n }\n </div>\n }\n</si-chat-message>\n", styles: [":host{display:block}si-chat-message{--chat-message-bubble-bg: transparent;--chat-message-bubble-padding: 0;margin-block-end:-4px}.ai-message-actions{margin-block-start:8px}:host ::ng-deep si-loading-spinner{--loading-spinner-size: 1.5em}\n"], dependencies: [{ kind: "directive", type: CdkMenuTrigger, selector: "[cdkMenuTriggerFor]", inputs: ["cdkMenuTriggerFor", "cdkMenuPosition", "cdkMenuTriggerData"], outputs: ["cdkMenuOpened", "cdkMenuClosed"], exportAs: ["cdkMenuTriggerFor"] }, { kind: "component", type: SiChatMessageComponent, selector: "si-chat-message", inputs: ["loading", "alignment", "actionsPosition"] }, { kind: "component", type: SiIconComponent, selector: "si-icon", inputs: ["icon"] }, { kind: "component", type: SiMenuFactoryComponent, selector: "si-menu-factory", inputs: ["items", "actionParam"] }, { kind: "directive", type: SiChatMessageActionDirective, selector: "[siChatMessageAction]" }, { kind: "pipe", type: SiTranslatePipe, name: "translate" }] });
237
+ }
238
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiAiMessageComponent, decorators: [{
239
+ type: Component,
240
+ args: [{ selector: 'si-ai-message', imports: [
241
+ CdkMenuTrigger,
242
+ SiChatMessageComponent,
243
+ SiIconComponent,
244
+ SiMenuFactoryComponent,
245
+ SiChatMessageActionDirective,
246
+ SiTranslatePipe
247
+ ], host: {
248
+ class: 'si-ai-message'
249
+ }, template: "<si-chat-message alignment=\"start\" actionsPosition=\"bottom\" [loading]=\"loading()\">\n @if (content()) {\n @let content = textContent();\n @if (content) {\n <span class=\"text-pre-wrap\">{{ content }}</span>\n } @else {\n <div #formattedContent> </div>\n }\n }\n\n @if (actions().length > 0 || secondaryActions().length > 0) {\n <div class=\"d-flex gap-4 ai-message-actions\" siChatMessageAction>\n @for (action of actions(); track $index) {\n <button\n type=\"button\"\n class=\"btn btn-ghost btn-circle btn-sm\"\n [disabled]=\"action.disabled\"\n [attr.aria-label]=\"action.label | translate\"\n (click)=\"action.action(actionParam(), action)\"\n >\n <si-icon [icon]=\"action.icon\" />\n </button>\n }\n\n @if (secondaryActions().length > 0) {\n <button\n type=\"button\"\n class=\"btn btn-ghost btn-circle btn-sm\"\n [cdkMenuTriggerFor]=\"secondaryActionsMenu\"\n [attr.aria-label]=\"secondaryActionsLabel() | translate\"\n [attr.title]=\"secondaryActionsLabel() | translate\"\n >\n <si-icon icon=\"element-optionsVertical\" />\n </button>\n\n <ng-template #secondaryActionsMenu>\n <si-menu-factory [items]=\"secondaryActions()\" [actionParam]=\"actionParam()\" />\n </ng-template>\n }\n </div>\n }\n</si-chat-message>\n", styles: [":host{display:block}si-chat-message{--chat-message-bubble-bg: transparent;--chat-message-bubble-padding: 0;margin-block-end:-4px}.ai-message-actions{margin-block-start:8px}:host ::ng-deep si-loading-spinner{--loading-spinner-size: 1.5em}\n"] }]
250
+ }], ctorParameters: () => [] });
251
+
252
+ /**
253
+ * Copyright (c) Siemens 2016 - 2025
254
+ * SPDX-License-Identifier: MIT
255
+ */
256
+ /**
257
+ * Attachment list component for displaying file attachments in chat messages.
258
+ *
259
+ * This component renders a list of file attachments with icons, names, and optional
260
+ * preview and remove functionality. It's designed to work with chat message components
261
+ * to show files that have been uploaded or shared in conversations.
262
+ *
263
+ * @remarks
264
+ * This component provides:
265
+ * - Automatic file type detection with appropriate icons
266
+ * - Optional preview modal for attachments
267
+ * - Optional remove functionality for editable messages
268
+ * - Flexible alignment (start/end) to match message alignment
269
+ * - Support for various file types (images, videos, audio, documents, archives)
270
+ *
271
+ * The component is typically used within {@link SiUserMessageComponent} or {@link SiAiMessageComponent}
272
+ * to display uploaded files, but can also be used standalone.
273
+ *
274
+ * @example
275
+ * Basic usage with attachments:
276
+ * ```html
277
+ * <si-attachment-list [attachments]="files" />
278
+ * ```
279
+ *
280
+ * @example
281
+ * With remove functionality and custom alignment:
282
+ * ```typescript
283
+ * import { Component } from '@angular/core';
284
+ * import { SiAttachmentListComponent, Attachment } from '@siemens/element-ng/chat-messages';
285
+ *
286
+ * @Component({
287
+ * selector: 'app-chat',
288
+ * imports: [SiAttachmentListComponent],
289
+ * template: `
290
+ * <si-attachment-list
291
+ * [attachments]="attachments"
292
+ * [alignment]="'end'"
293
+ * [removable]="true"
294
+ * (remove)="handleRemove($event)"
295
+ * />
296
+ * `
297
+ * })
298
+ * export class ChatComponent {
299
+ * attachments: Attachment[] = [
300
+ * { id: '1', name: 'report.pdf' },
301
+ * { id: '2', name: 'image.png', previewTemplate: this.imagePreview }
302
+ * ];
303
+ *
304
+ * handleRemove(attachment: Attachment) {
305
+ * this.attachments = this.attachments.filter(a => a !== attachment);
306
+ * }
307
+ * }
308
+ * ```
309
+ *
310
+ * @see {@link SiUserMessageComponent} for user message display
311
+ * @see {@link SiAiMessageComponent} for AI message display
312
+ * @see {@link Attachment} for attachment data structure
313
+ *
314
+ * @experimental
315
+ */
316
+ class SiAttachmentListComponent {
317
+ modalService = inject(SiModalService);
318
+ /**
319
+ * List of attachments to display
320
+ * @defaultValue []
321
+ */
322
+ attachments = input([]);
323
+ /**
324
+ * Whether to align attachments to the end (right) or start (left)
325
+ * @defaultValue 'start'
326
+ */
327
+ alignment = input('start');
328
+ /**
329
+ * Whether to show remove buttons on attachments
330
+ * @defaultValue false
331
+ */
332
+ removable = input(false, { transform: booleanAttribute });
333
+ /**
334
+ * Label for remove attachment button
335
+ *
336
+ * @defaultValue
337
+ * ```
338
+ * t(() => $localize`:@@SI_ATTACHMENT_LIST.REMOVE_ATTACHMENT:Remove attachment`)
339
+ * ```
340
+ */
341
+ removeLabel = input(t(() => $localize `:@@SI_ATTACHMENT_LIST.REMOVE_ATTACHMENT:Remove attachment`));
342
+ /**
343
+ * Emitted when an attachment should be removed
344
+ */
345
+ remove = output();
346
+ getPreviewTemplate(attachment) {
347
+ if (attachment.previewTemplate) {
348
+ return typeof attachment.previewTemplate === 'function'
349
+ ? attachment.previewTemplate()
350
+ : attachment.previewTemplate;
351
+ }
352
+ return undefined;
353
+ }
354
+ openPreview(event, attachment) {
355
+ const template = this.getPreviewTemplate(attachment);
356
+ if (template) {
357
+ event.preventDefault();
358
+ this.modalService.show(template, {
359
+ inputValues: { 'attachment': attachment }
360
+ });
361
+ }
362
+ }
363
+ getFileIcon(name) {
364
+ // TODO: Accept map and default it in file upload directive.
365
+ return 'element-document';
366
+ }
367
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiAttachmentListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
368
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: SiAttachmentListComponent, isStandalone: true, selector: "si-attachment-list", inputs: { attachments: { classPropertyName: "attachments", publicName: "attachments", isSignal: true, isRequired: false, transformFunction: null }, alignment: { classPropertyName: "alignment", publicName: "alignment", isSignal: true, isRequired: false, transformFunction: null }, removable: { classPropertyName: "removable", publicName: "removable", isSignal: true, isRequired: false, transformFunction: null }, removeLabel: { classPropertyName: "removeLabel", publicName: "removeLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { remove: "remove" }, ngImport: i0, template: "<div class=\"d-flex flex-wrap gap-4\" [class.justify-content-end]=\"alignment() === 'end'\">\n @for (attachment of attachments(); track $index) {\n <div class=\"attachment-item d-flex align-items-stretch\" role=\"group\">\n @if (attachment.previewTemplate) {\n <button\n type=\"button\"\n class=\"attachment-main focus-inside d-flex align-items-center flex-grow-1 min-width-0\"\n [attr.title]=\"attachment.name\"\n [attr.aria-label]=\"attachment.name\"\n (click)=\"openPreview($event, attachment)\"\n (keydown.enter)=\"openPreview($event, attachment)\"\n (keydown.space)=\"openPreview($event, attachment)\"\n >\n <si-icon\n class=\"attachment-icon icon flex-shrink-0 mx-1\"\n [icon]=\"getFileIcon(attachment.name)\"\n />\n <div class=\"attachment-info flex-grow-1 min-width-0\">\n <span\n class=\"attachment-name me-4 text-truncate si-body-2 d-block\"\n [title]=\"attachment.name\"\n >\n {{ attachment.name }}\n </span>\n </div>\n </button>\n } @else {\n <div class=\"attachment-main--static d-flex align-items-center flex-grow-1 min-width-0\">\n <si-icon\n class=\"attachment-icon icon flex-shrink-0 mx-1\"\n [icon]=\"getFileIcon(attachment.name)\"\n />\n <div class=\"attachment-info flex-grow-1 min-width-0\">\n <span\n class=\"attachment-name me-4 text-truncate si-body-2 d-block\"\n [title]=\"attachment.name\"\n >\n {{ attachment.name }}\n </span>\n </div>\n </div>\n }\n\n @if (removable()) {\n <button\n type=\"button\"\n class=\"btn btn-ghost btn-circle btn-sm expand-button flex-shrink-0 ms-auto align-self-center focus-inside\"\n [attr.aria-label]=\"(removeLabel() | translate) + ' ' + attachment.name\"\n (click)=\"remove.emit(attachment); $event.stopPropagation()\"\n >\n <si-icon class=\"icon\" icon=\"element-delete\" />\n </button>\n }\n </div>\n }\n</div>\n", styles: [":host{--attachment-list-bg: var(--element-base-1-hover);--attachment-name-color: var(--element-text-primary)}.attachment-item{border-radius:var(--element-radius-2);overflow:hidden;background-color:var(--attachment-list-bg);color:var(--attachment-name-color)}.attachment-main{appearance:none;border:0;background:none;padding:0;margin:0;inline-size:100%;text-align:inherit;color:inherit;cursor:pointer}.attachment-icon{display:flex;align-items:center;justify-content:center}.attachment-info{display:flex;flex-direction:column;gap:2px;min-inline-size:0}.attachment-info .attachment-name{line-height:1.2}.attachment-info .attachment-size{line-height:1}.expand-button{border-radius:0}\n"], dependencies: [{ kind: "component", type: SiIconComponent, selector: "si-icon", inputs: ["icon"] }, { kind: "pipe", type: SiTranslatePipe, name: "translate" }] });
369
+ }
370
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiAttachmentListComponent, decorators: [{
371
+ type: Component,
372
+ args: [{ selector: 'si-attachment-list', imports: [SiIconComponent, SiTranslatePipe], template: "<div class=\"d-flex flex-wrap gap-4\" [class.justify-content-end]=\"alignment() === 'end'\">\n @for (attachment of attachments(); track $index) {\n <div class=\"attachment-item d-flex align-items-stretch\" role=\"group\">\n @if (attachment.previewTemplate) {\n <button\n type=\"button\"\n class=\"attachment-main focus-inside d-flex align-items-center flex-grow-1 min-width-0\"\n [attr.title]=\"attachment.name\"\n [attr.aria-label]=\"attachment.name\"\n (click)=\"openPreview($event, attachment)\"\n (keydown.enter)=\"openPreview($event, attachment)\"\n (keydown.space)=\"openPreview($event, attachment)\"\n >\n <si-icon\n class=\"attachment-icon icon flex-shrink-0 mx-1\"\n [icon]=\"getFileIcon(attachment.name)\"\n />\n <div class=\"attachment-info flex-grow-1 min-width-0\">\n <span\n class=\"attachment-name me-4 text-truncate si-body-2 d-block\"\n [title]=\"attachment.name\"\n >\n {{ attachment.name }}\n </span>\n </div>\n </button>\n } @else {\n <div class=\"attachment-main--static d-flex align-items-center flex-grow-1 min-width-0\">\n <si-icon\n class=\"attachment-icon icon flex-shrink-0 mx-1\"\n [icon]=\"getFileIcon(attachment.name)\"\n />\n <div class=\"attachment-info flex-grow-1 min-width-0\">\n <span\n class=\"attachment-name me-4 text-truncate si-body-2 d-block\"\n [title]=\"attachment.name\"\n >\n {{ attachment.name }}\n </span>\n </div>\n </div>\n }\n\n @if (removable()) {\n <button\n type=\"button\"\n class=\"btn btn-ghost btn-circle btn-sm expand-button flex-shrink-0 ms-auto align-self-center focus-inside\"\n [attr.aria-label]=\"(removeLabel() | translate) + ' ' + attachment.name\"\n (click)=\"remove.emit(attachment); $event.stopPropagation()\"\n >\n <si-icon class=\"icon\" icon=\"element-delete\" />\n </button>\n }\n </div>\n }\n</div>\n", styles: [":host{--attachment-list-bg: var(--element-base-1-hover);--attachment-name-color: var(--element-text-primary)}.attachment-item{border-radius:var(--element-radius-2);overflow:hidden;background-color:var(--attachment-list-bg);color:var(--attachment-name-color)}.attachment-main{appearance:none;border:0;background:none;padding:0;margin:0;inline-size:100%;text-align:inherit;color:inherit;cursor:pointer}.attachment-icon{display:flex;align-items:center;justify-content:center}.attachment-info{display:flex;flex-direction:column;gap:2px;min-inline-size:0}.attachment-info .attachment-name{line-height:1.2}.attachment-info .attachment-size{line-height:1}.expand-button{border-radius:0}\n"] }]
373
+ }] });
374
+
375
+ /**
376
+ * Copyright (c) Siemens 2016 - 2025
377
+ * SPDX-License-Identifier: MIT
378
+ */
379
+ class SiChatInputComponent {
380
+ static idCounter = 0;
381
+ textInput = viewChild('textInput');
382
+ projectedContent = viewChild('projected');
383
+ /**
384
+ * Current input value
385
+ * @defaultValue ''
386
+ */
387
+ value = model('');
388
+ /**
389
+ * Placeholder text for the input
390
+ *
391
+ * @defaultValue
392
+ * ```
393
+ * t(() => $localize`:@@SI_CHAT_INPUT.PLACEHOLDER:Enter a message…`)
394
+ * ```
395
+ */
396
+ placeholder = input(t(() => $localize `:@@SI_CHAT_INPUT.PLACEHOLDER:Enter a message…`));
397
+ /**
398
+ * Whether the input is disabled
399
+ * @defaultValue false
400
+ */
401
+ disabled = input(false, { transform: booleanAttribute });
402
+ /**
403
+ * Whether a message is currently being sent, also prevent the sending of new ones while still allowing the user to type
404
+ * @defaultValue false
405
+ */
406
+ sending = input(false, { transform: booleanAttribute });
407
+ /**
408
+ * Whether the input supports interrupting ongoing operations. When active,
409
+ * the send button transforms into an interrupt button (with element-stop-filled icon).
410
+ * If sending is true, the interrupt button will be disabled.
411
+ * @defaultValue false
412
+ */
413
+ interruptible = input(false, { transform: booleanAttribute });
414
+ /**
415
+ * Maximum number of characters allowed
416
+ */
417
+ maxLength = input();
418
+ /**
419
+ * A disclaimer to display.
420
+ *
421
+ * If not provided, the component will look for projected content with the `siChatInputDisclaimer` directive.
422
+ * If both are empty, no disclaimer section will be shown (handled via CSS :empty).
423
+ */
424
+ disclaimer = input();
425
+ /**
426
+ * Primary actions available in the input (attach files, etc.)
427
+ * All actions displayed inline
428
+ * @defaultValue []
429
+ */
430
+ actions = input([]);
431
+ /**
432
+ * Secondary actions available in dropdown menu
433
+ * @defaultValue []
434
+ */
435
+ secondaryActions = input([]);
436
+ /**
437
+ * Whether file attachments are supported
438
+ * @defaultValue false
439
+ */
440
+ allowAttachments = input(false);
441
+ /**
442
+ * Accepted file types for attachments (as accept string)
443
+ * @defaultValue undefined
444
+ */
445
+ accept = input();
446
+ /**
447
+ * Maximum file size in bytes
448
+ * @defaultValue 10485760 (10MB)
449
+ */
450
+ maxFileSize = input(10485760);
451
+ /**
452
+ * Current attachments
453
+ * @defaultValue []
454
+ */
455
+ attachments = model([]);
456
+ /**
457
+ * The label for the input, used for accessibility
458
+ * @defaultValue
459
+ * ```
460
+ * t(() => $localize`:@@SI_CHAT_INPUT.LABEL:Chat message input`)
461
+ * ```
462
+ */
463
+ label = input(t(() => $localize `:@@SI_CHAT_INPUT.LABEL:Chat message input`));
464
+ /** Parameter to pass to action handlers */
465
+ actionParam = input();
466
+ /**
467
+ * Send button label
468
+ *
469
+ * @defaultValue
470
+ * ```
471
+ * t(() => $localize`:@@SI_CHAT_INPUT.SEND:Send`)
472
+ * ```
473
+ */
474
+ sendButtonLabel = input(t(() => $localize `:@@SI_CHAT_INPUT.SEND:Send`));
475
+ /**
476
+ * Send button icon
477
+ *
478
+ * @defaultValue 'element-send-filled'
479
+ */
480
+ sendButtonIcon = input('element-send-filled');
481
+ /**
482
+ * Interrupt button label
483
+ *
484
+ * @defaultValue
485
+ * ```
486
+ * t(() => $localize`:@@SI_CHAT_INPUT.INTERRUPT:Interrupt`)
487
+ * ```
488
+ */
489
+ interruptButtonLabel = input(t(() => $localize `:@@SI_CHAT_INPUT.INTERRUPT:Interrupt`));
490
+ /**
491
+ * Auto-focus the input on component initialization
492
+ * @defaultValue false
493
+ */
494
+ autoFocus = input(false, { transform: booleanAttribute });
495
+ /**
496
+ * Attach file button aria label
497
+ *
498
+ * @defaultValue
499
+ * ```
500
+ * t(() => $localize`:@@SI_CHAT_INPUT.ATTACH_FILE:Attach file`)
501
+ * ```
502
+ */
503
+ attachFileLabel = input(t(() => $localize `:@@SI_CHAT_INPUT.ATTACH_FILE:Attach file`));
504
+ /**
505
+ * Remove attachment aria label prefix
506
+ *
507
+ * @defaultValue
508
+ * ```
509
+ * t(() => $localize`:@@SI_ATTACHMENT_LIST.REMOVE_ATTACHMENT:Remove attachment`)
510
+ * ```
511
+ */
512
+ removeAttachmentLabel = input(t(() => $localize `:@@SI_ATTACHMENT_LIST.REMOVE_ATTACHMENT:Remove attachment`));
513
+ /**
514
+ * More actions button aria label
515
+ *
516
+ * @defaultValue
517
+ * ```
518
+ * t(() => $localize`:@@SI_CHAT_INPUT.SECONDARY_ACTIONS:More actions`)
519
+ * ```
520
+ */
521
+ secondaryActionsLabel = input(t(() => $localize `:@@SI_CHAT_INPUT.SECONDARY_ACTIONS:More actions`));
522
+ /**
523
+ * Emitted when the user wants to send a message
524
+ */
525
+ send = output();
526
+ /**
527
+ * Emitted when the user wants to interrupt the current operation
528
+ */
529
+ interrupt = output();
530
+ /**
531
+ * Emitted when file upload errors occur
532
+ */
533
+ fileError = output();
534
+ id = `__si-chat-input-${SiChatInputComponent.idCounter++}`;
535
+ hasContent = computed(() => this.value().trim().length > 0);
536
+ hasAttachments = computed(() => this.attachments().length > 0);
537
+ hasActions = computed(() => this.actions().length > 0);
538
+ hasSecondaryActions = computed(() => this.secondaryActions().length > 0);
539
+ canSend = computed(() => (this.hasContent() || this.hasAttachments()) && !this.disabled() && !this.sending());
540
+ showInterruptButton = computed(() => this.interruptible());
541
+ buttonDisabled = computed(() => {
542
+ if (this.showInterruptButton()) {
543
+ return this.disabled() || this.sending();
544
+ }
545
+ return !this.canSend();
546
+ });
547
+ buttonIcon = computed(() => this.showInterruptButton() ? 'element-stop-filled' : this.sendButtonIcon());
548
+ buttonLabel = computed(() => this.showInterruptButton() ? this.interruptButtonLabel() : this.sendButtonLabel());
549
+ get attachmentList() {
550
+ return this.attachments();
551
+ }
552
+ onInputChange(value) {
553
+ this.value.set(value);
554
+ }
555
+ onSend() {
556
+ if (this.canSend()) {
557
+ this.send.emit({
558
+ content: this.value(),
559
+ attachments: this.attachments()
560
+ });
561
+ this.value.set('');
562
+ this.attachments.set([]);
563
+ }
564
+ }
565
+ onButtonClick() {
566
+ if (this.showInterruptButton()) {
567
+ this.interrupt.emit();
568
+ }
569
+ else {
570
+ this.onSend();
571
+ }
572
+ }
573
+ onKeyDown(event) {
574
+ if (event.key === 'Enter' && !event.shiftKey) {
575
+ event.preventDefault();
576
+ if (!this.showInterruptButton()) {
577
+ this.onSend();
578
+ }
579
+ }
580
+ }
581
+ onFilesAdded(uploadFiles) {
582
+ const validFiles = uploadFiles.filter(uploadFile => uploadFile.status === 'added');
583
+ validFiles.forEach(uploadFile => {
584
+ const attachment = {
585
+ name: uploadFile.fileName,
586
+ size: uploadFile.file.size,
587
+ type: uploadFile.file.type,
588
+ file: uploadFile.file
589
+ };
590
+ this.attachments.update(current => [...current, attachment]);
591
+ });
592
+ }
593
+ onFileError(error) {
594
+ this.fileError.emit(error);
595
+ }
596
+ removeAttachment(attachment) {
597
+ this.attachments.update(current => {
598
+ return current.filter(a => a !== attachment);
599
+ });
600
+ }
601
+ onContainerClick(event) {
602
+ const target = event.target;
603
+ // Don't focus if clicking on interactive elements
604
+ if (target.tagName === 'BUTTON' ||
605
+ target.tagName === 'INPUT' ||
606
+ target.tagName === 'TEXTAREA' ||
607
+ target.closest('button') ||
608
+ target.closest('[siChatMessageAction]') ||
609
+ (target.closest('si-attachment-list') && target.closest('.attachment-item')) ||
610
+ this.projectedContent()?.nativeElement?.contains(target)) {
611
+ return;
612
+ }
613
+ this.focus();
614
+ }
615
+ ngAfterViewInit() {
616
+ const textarea = this.textInput();
617
+ if (textarea?.nativeElement) {
618
+ this.setTextareaHeight(textarea.nativeElement);
619
+ if (this.autoFocus()) {
620
+ // Use setTimeout to ensure the element is fully rendered
621
+ setTimeout(() => {
622
+ textarea.nativeElement.focus();
623
+ }, 0);
624
+ }
625
+ }
626
+ }
627
+ adjustTextareaHeight(event) {
628
+ const textarea = event.target;
629
+ this.setTextareaHeight(textarea);
630
+ }
631
+ /**
632
+ * Focus the textarea input
633
+ */
634
+ focus() {
635
+ const textarea = this.textInput();
636
+ if (textarea?.nativeElement) {
637
+ textarea.nativeElement.focus();
638
+ }
639
+ }
640
+ setTextareaHeight(textarea) {
641
+ textarea.style.blockSize = 'auto';
642
+ const computedStyle = window.getComputedStyle(textarea);
643
+ const lineHeight = parseInt(computedStyle.lineHeight, 10) || parseInt(computedStyle.fontSize, 10) * 1.2;
644
+ const paddingTop = parseInt(computedStyle.paddingBlockStart, 10) || 0;
645
+ const paddingBottom = parseInt(computedStyle.paddingBlockEnd, 10) || 0;
646
+ const minHeight = lineHeight + paddingTop + paddingBottom;
647
+ const viewportHeight = window.innerHeight;
648
+ const maxViewportHeight = viewportHeight * 0.3;
649
+ const maxLinesHeight = lineHeight * 8;
650
+ const maxHeight = Math.min(maxViewportHeight, maxLinesHeight) + paddingTop + paddingBottom;
651
+ const scrollHeight = textarea.scrollHeight;
652
+ const finalHeight = Math.max(Math.min(scrollHeight, maxHeight), minHeight);
653
+ textarea.style.height = finalHeight + 'px';
654
+ }
655
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiChatInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
656
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: SiChatInputComponent, isStandalone: true, selector: "si-chat-input", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, sending: { classPropertyName: "sending", publicName: "sending", isSignal: true, isRequired: false, transformFunction: null }, interruptible: { classPropertyName: "interruptible", publicName: "interruptible", isSignal: true, isRequired: false, transformFunction: null }, maxLength: { classPropertyName: "maxLength", publicName: "maxLength", isSignal: true, isRequired: false, transformFunction: null }, disclaimer: { classPropertyName: "disclaimer", publicName: "disclaimer", isSignal: true, isRequired: false, transformFunction: null }, actions: { classPropertyName: "actions", publicName: "actions", isSignal: true, isRequired: false, transformFunction: null }, secondaryActions: { classPropertyName: "secondaryActions", publicName: "secondaryActions", isSignal: true, isRequired: false, transformFunction: null }, allowAttachments: { classPropertyName: "allowAttachments", publicName: "allowAttachments", isSignal: true, isRequired: false, transformFunction: null }, accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, maxFileSize: { classPropertyName: "maxFileSize", publicName: "maxFileSize", isSignal: true, isRequired: false, transformFunction: null }, attachments: { classPropertyName: "attachments", publicName: "attachments", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, actionParam: { classPropertyName: "actionParam", publicName: "actionParam", isSignal: true, isRequired: false, transformFunction: null }, sendButtonLabel: { classPropertyName: "sendButtonLabel", publicName: "sendButtonLabel", isSignal: true, isRequired: false, transformFunction: null }, sendButtonIcon: { classPropertyName: "sendButtonIcon", publicName: "sendButtonIcon", isSignal: true, isRequired: false, transformFunction: null }, interruptButtonLabel: { classPropertyName: "interruptButtonLabel", publicName: "interruptButtonLabel", isSignal: true, isRequired: false, transformFunction: null }, autoFocus: { classPropertyName: "autoFocus", publicName: "autoFocus", isSignal: true, isRequired: false, transformFunction: null }, attachFileLabel: { classPropertyName: "attachFileLabel", publicName: "attachFileLabel", isSignal: true, isRequired: false, transformFunction: null }, removeAttachmentLabel: { classPropertyName: "removeAttachmentLabel", publicName: "removeAttachmentLabel", isSignal: true, isRequired: false, transformFunction: null }, secondaryActionsLabel: { classPropertyName: "secondaryActionsLabel", publicName: "secondaryActionsLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", attachments: "attachmentsChange", send: "send", interrupt: "interrupt", fileError: "fileError" }, viewQueries: [{ propertyName: "textInput", first: true, predicate: ["textInput"], descendants: true, isSignal: true }, { propertyName: "projectedContent", first: true, predicate: ["projected"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"input-wrapper border rounded-3 bg-body\" (click)=\"onContainerClick($event)\">\n @if (hasAttachments()) {\n <div class=\"p-4 pb-0\">\n <si-attachment-list\n [attachments]=\"attachmentList\"\n [removeLabel]=\"removeAttachmentLabel()\"\n [removable]=\"true\"\n (remove)=\"removeAttachment($event)\"\n />\n </div>\n }\n\n <div class=\"p-4 pe-2 pb-0 si-body-2\">\n <label class=\"form-label d-none\" [for]=\"id\">{{ label() | translate }}</label>\n <textarea\n #textInput\n class=\"chat-textarea w-100 border-0 p-2\"\n rows=\"1\"\n [id]=\"id\"\n [placeholder]=\"placeholder() | translate\"\n [disabled]=\"disabled()\"\n [maxlength]=\"maxLength() || null\"\n [(ngModel)]=\"value\"\n (keydown)=\"onKeyDown($event)\"\n (input)=\"adjustTextareaHeight($event)\"\n ></textarea>\n </div>\n\n <div class=\"d-flex align-items-center justify-content-between px-4 ps-5 pb-2\">\n <div class=\"d-flex align-items-center gap-4\">\n @if (allowAttachments()) {\n <input\n #fileInput\n type=\"file\"\n class=\"d-none\"\n siFileUpload\n [accept]=\"accept()\"\n [maxFileSize]=\"maxFileSize()\"\n [multiple]=\"true\"\n (validFiles)=\"onFilesAdded($event)\"\n (fileError)=\"onFileError($event)\"\n />\n\n <button\n type=\"button\"\n class=\"btn btn-ghost btn-circle btn-sm\"\n [attr.aria-label]=\"attachFileLabel() | translate\"\n [disabled]=\"disabled()\"\n (click)=\"fileInput.click()\"\n >\n <si-icon icon=\"element-attachment\" />\n </button>\n }\n\n @if (hasActions() || hasSecondaryActions()) {\n <div class=\"d-flex gap-4 ai-message-actions\" siChatMessageAction>\n @for (action of actions(); track $index) {\n <button\n type=\"button\"\n class=\"btn btn-ghost btn-circle btn-sm\"\n [disabled]=\"action.disabled\"\n [attr.aria-label]=\"action.label | translate\"\n (click)=\"action.action(actionParam(), action)\"\n >\n <si-icon [icon]=\"action.icon\" />\n </button>\n }\n\n @if (secondaryActions().length > 0) {\n <button\n type=\"button\"\n class=\"btn btn-ghost btn-circle btn-sm\"\n [cdkMenuTriggerFor]=\"secondaryActionsMenu\"\n [attr.aria-label]=\"secondaryActionsLabel() | translate\"\n [attr.title]=\"secondaryActionsLabel() | translate\"\n >\n <si-icon icon=\"element-optionsVertical\" />\n </button>\n\n <ng-template #secondaryActionsMenu>\n <si-menu-factory [items]=\"secondaryActions()\" [actionParam]=\"actionParam()\" />\n </ng-template>\n }\n </div>\n }\n <div #projected class=\"d-flex flex-wrap align-items-start gap-4\">\n <ng-content />\n </div>\n </div>\n\n <button\n type=\"button\"\n class=\"btn btn-ghost btn-circle btn-sm\"\n [disabled]=\"buttonDisabled()\"\n [attr.aria-label]=\"buttonLabel() | translate\"\n (click)=\"onButtonClick()\"\n >\n <si-icon class=\"text-primary\" [icon]=\"buttonIcon()\" />\n </button>\n </div>\n</div>\n\n<div class=\"disclaimer-wrapper text-center mt-4 px-3\">\n @if (disclaimer()) {\n <span class=\"si-caption text-secondary d-block\">{{ disclaimer() | translate }}</span>\n }\n <ng-content select=\"[siChatInputDisclaimer]\" />\n</div>\n", styles: [":host{max-inline-size:720px}.input-wrapper{border-color:var(--element-ui-4);background-color:var(--element-base-input-experimental)}.chat-textarea{min-block-size:1.7142857143em;font-family:inherit;outline:none;resize:none;background-color:transparent!important}.chat-textarea::placeholder{color:var(--element-text-secondary)}.chat-textarea:disabled{background-color:transparent!important;color:var(--element-text-disabled);cursor:not-allowed}.chat-textarea:disabled::placeholder{color:var(--element-text-disabled)}.input-wrapper:has(.chat-textarea:focus-visible){outline:var(--element-button-focus-width) solid var(--element-focus-default);outline-offset:var(--element-button-focus-overlay-width);border-color:var(--element-ui-1)}.disclaimer-wrapper:empty{display:none}\n"], dependencies: [{ kind: "directive", type: CdkMenuTrigger, selector: "[cdkMenuTriggerFor]", inputs: ["cdkMenuTriggerFor", "cdkMenuPosition", "cdkMenuTriggerData"], outputs: ["cdkMenuOpened", "cdkMenuClosed"], exportAs: ["cdkMenuTriggerFor"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: SiIconComponent, selector: "si-icon", inputs: ["icon"] }, { kind: "pipe", type: SiTranslatePipe, name: "translate" }, { kind: "component", type: SiAttachmentListComponent, selector: "si-attachment-list", inputs: ["attachments", "alignment", "removable", "removeLabel"], outputs: ["remove"] }, { kind: "component", type: SiMenuFactoryComponent, selector: "si-menu-factory", inputs: ["items", "actionParam"] }, { kind: "directive", type: SiFileUploadDirective, selector: "input[type=\"file\"][siFileUpload]", inputs: ["errorTextFileType", "errorTextFileMaxSize", "accept", "maxFileSize", "multiple", "directoryUpload"], outputs: ["validFiles", "filesAdded", "fileError"] }] });
657
+ }
658
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiChatInputComponent, decorators: [{
659
+ type: Component,
660
+ args: [{ selector: 'si-chat-input', imports: [
661
+ CdkMenuTrigger,
662
+ FormsModule,
663
+ SiIconComponent,
664
+ SiTranslatePipe,
665
+ SiAttachmentListComponent,
666
+ SiMenuFactoryComponent,
667
+ SiFileUploadDirective
668
+ ], template: "<div class=\"input-wrapper border rounded-3 bg-body\" (click)=\"onContainerClick($event)\">\n @if (hasAttachments()) {\n <div class=\"p-4 pb-0\">\n <si-attachment-list\n [attachments]=\"attachmentList\"\n [removeLabel]=\"removeAttachmentLabel()\"\n [removable]=\"true\"\n (remove)=\"removeAttachment($event)\"\n />\n </div>\n }\n\n <div class=\"p-4 pe-2 pb-0 si-body-2\">\n <label class=\"form-label d-none\" [for]=\"id\">{{ label() | translate }}</label>\n <textarea\n #textInput\n class=\"chat-textarea w-100 border-0 p-2\"\n rows=\"1\"\n [id]=\"id\"\n [placeholder]=\"placeholder() | translate\"\n [disabled]=\"disabled()\"\n [maxlength]=\"maxLength() || null\"\n [(ngModel)]=\"value\"\n (keydown)=\"onKeyDown($event)\"\n (input)=\"adjustTextareaHeight($event)\"\n ></textarea>\n </div>\n\n <div class=\"d-flex align-items-center justify-content-between px-4 ps-5 pb-2\">\n <div class=\"d-flex align-items-center gap-4\">\n @if (allowAttachments()) {\n <input\n #fileInput\n type=\"file\"\n class=\"d-none\"\n siFileUpload\n [accept]=\"accept()\"\n [maxFileSize]=\"maxFileSize()\"\n [multiple]=\"true\"\n (validFiles)=\"onFilesAdded($event)\"\n (fileError)=\"onFileError($event)\"\n />\n\n <button\n type=\"button\"\n class=\"btn btn-ghost btn-circle btn-sm\"\n [attr.aria-label]=\"attachFileLabel() | translate\"\n [disabled]=\"disabled()\"\n (click)=\"fileInput.click()\"\n >\n <si-icon icon=\"element-attachment\" />\n </button>\n }\n\n @if (hasActions() || hasSecondaryActions()) {\n <div class=\"d-flex gap-4 ai-message-actions\" siChatMessageAction>\n @for (action of actions(); track $index) {\n <button\n type=\"button\"\n class=\"btn btn-ghost btn-circle btn-sm\"\n [disabled]=\"action.disabled\"\n [attr.aria-label]=\"action.label | translate\"\n (click)=\"action.action(actionParam(), action)\"\n >\n <si-icon [icon]=\"action.icon\" />\n </button>\n }\n\n @if (secondaryActions().length > 0) {\n <button\n type=\"button\"\n class=\"btn btn-ghost btn-circle btn-sm\"\n [cdkMenuTriggerFor]=\"secondaryActionsMenu\"\n [attr.aria-label]=\"secondaryActionsLabel() | translate\"\n [attr.title]=\"secondaryActionsLabel() | translate\"\n >\n <si-icon icon=\"element-optionsVertical\" />\n </button>\n\n <ng-template #secondaryActionsMenu>\n <si-menu-factory [items]=\"secondaryActions()\" [actionParam]=\"actionParam()\" />\n </ng-template>\n }\n </div>\n }\n <div #projected class=\"d-flex flex-wrap align-items-start gap-4\">\n <ng-content />\n </div>\n </div>\n\n <button\n type=\"button\"\n class=\"btn btn-ghost btn-circle btn-sm\"\n [disabled]=\"buttonDisabled()\"\n [attr.aria-label]=\"buttonLabel() | translate\"\n (click)=\"onButtonClick()\"\n >\n <si-icon class=\"text-primary\" [icon]=\"buttonIcon()\" />\n </button>\n </div>\n</div>\n\n<div class=\"disclaimer-wrapper text-center mt-4 px-3\">\n @if (disclaimer()) {\n <span class=\"si-caption text-secondary d-block\">{{ disclaimer() | translate }}</span>\n }\n <ng-content select=\"[siChatInputDisclaimer]\" />\n</div>\n", styles: [":host{max-inline-size:720px}.input-wrapper{border-color:var(--element-ui-4);background-color:var(--element-base-input-experimental)}.chat-textarea{min-block-size:1.7142857143em;font-family:inherit;outline:none;resize:none;background-color:transparent!important}.chat-textarea::placeholder{color:var(--element-text-secondary)}.chat-textarea:disabled{background-color:transparent!important;color:var(--element-text-disabled);cursor:not-allowed}.chat-textarea:disabled::placeholder{color:var(--element-text-disabled)}.input-wrapper:has(.chat-textarea:focus-visible){outline:var(--element-button-focus-width) solid var(--element-focus-default);outline-offset:var(--element-button-focus-overlay-width);border-color:var(--element-ui-1)}.disclaimer-wrapper:empty{display:none}\n"] }]
669
+ }] });
670
+
671
+ /**
672
+ * Copyright (c) Siemens 2016 - 2025
673
+ * SPDX-License-Identifier: MIT
674
+ */
675
+ /**
676
+ * Directive to mark content as chat input disclaimer.
677
+ * Apply this directive to content that should be slotted into the disclaimer area.
678
+ *
679
+ * @example
680
+ * ```html
681
+ * <si-chat-input>
682
+ * <div siChatInputDisclaimer>
683
+ * Custom disclaimer content
684
+ * </div>
685
+ * </si-chat-input>
686
+ * ```
687
+ */
688
+ class SiChatInputDisclaimerDirective {
689
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiChatInputDisclaimerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
690
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.0.6", type: SiChatInputDisclaimerDirective, isStandalone: true, selector: "[siChatInputDisclaimer]", ngImport: i0 });
691
+ }
692
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiChatInputDisclaimerDirective, decorators: [{
693
+ type: Directive,
694
+ args: [{
695
+ selector: '[siChatInputDisclaimer]'
696
+ }]
697
+ }] });
698
+
699
+ /**
700
+ * Copyright (c) Siemens 2016 - 2025
701
+ * SPDX-License-Identifier: MIT
702
+ */
703
+ /**
704
+ * User message component for displaying user input in conversational interfaces.
705
+ *
706
+ * The user message component renders user-submitted content in chat interfaces,
707
+ * supporting text, attachments, and contextual actions. It appears as a text bubble
708
+ * aligned to the right side and supports markdown formatting for rich content.
709
+ *
710
+ * @remarks
711
+ * This component is designed for use in:
712
+ * - AI chat interfaces where user input needs to be displayed
713
+ * - Peer-to-peer conversation interfaces
714
+ * - Conversation histories or chat transcripts
715
+ *
716
+ * The component automatically handles:
717
+ * - Rendering markdown content with syntax highlighting
718
+ * - Displaying attachments above the message bubble
719
+ * - Showing primary and secondary actions on hover (desktop) or tap (mobile)
720
+ * - Proper alignment and styling for user messages
721
+ *
722
+ * @example
723
+ * Basic usage with content only:
724
+ * ```html
725
+ * <si-user-message [content]="'Hello, how can I help you?'" />
726
+ * ```
727
+ *
728
+ * @example
729
+ * With actions and attachments:
730
+ * ```typescript
731
+ * import { Component } from '@angular/core';
732
+ * import { SiUserMessageComponent } from '@siemens/element-ng/chat-messages';
733
+ *
734
+ * @Component({
735
+ * selector: 'app-chat',
736
+ * imports: [SiUserMessageComponent],
737
+ * template: `
738
+ * <si-user-message
739
+ * [content]="message.text"
740
+ * [actions]="messageActions"
741
+ * [secondaryActions]="menuActions"
742
+ * [attachments]="message.attachments"
743
+ * [actionParam]="message"
744
+ * />
745
+ * `
746
+ * })
747
+ * export class ChatComponent {
748
+ * messageActions = [
749
+ * { icon: 'copy', label: 'Copy', action: (id) => this.copyMessage(id) },
750
+ * { icon: 'edit', label: 'Edit', action: (id) => this.editMessage(id) }
751
+ * ];
752
+ *
753
+ * menuActions = [
754
+ * { label: 'Delete', action: (id) => this.deleteMessage(id) }
755
+ * ];
756
+ * }
757
+ * ```
758
+ *
759
+ * @see {@link SiChatMessageComponent} for the base message wrapper component
760
+ * @see {@link SiAttachmentListComponent} for attachment handling
761
+ * @see {@link SiMarkdownRendererComponent} for markdown rendering
762
+ *
763
+ * @experimental
764
+ */
765
+ class SiUserMessageComponent {
766
+ formattedContent = viewChild('formattedContent');
767
+ /**
768
+ * The user message content
769
+ * @defaultValue ''
770
+ */
771
+ content = input('');
772
+ /**
773
+ * Optional formatter function to transform content before display.
774
+ * - Returns string: Content will be inserted as text with built-in sanitization
775
+ * - Returns Node: DOM node will be inserted directly without sanitization
776
+ *
777
+ * **Note:** When returning a Node with formatted content, apply the `markdown-content` class
778
+ * to the root element to ensure proper styling (e.g., `div.className = 'markdown-content'`).
779
+ * The function returned by {@link getMarkdownRenderer} does this automatically.
780
+ *
781
+ * **Warning:** When returning a Node, ensure the content is safe to prevent XSS attacks
782
+ * @defaultValue undefined
783
+ */
784
+ contentFormatter = input(undefined);
785
+ /**
786
+ * Primary message actions (edit, delete, copy, etc.).
787
+ * All actions displayed inline
788
+ * @defaultValue []
789
+ */
790
+ actions = input([]);
791
+ /**
792
+ * Secondary actions available in dropdown menu, first use primary actions and only add secondary actions additionally
793
+ * @defaultValue []
794
+ */
795
+ secondaryActions = input([]);
796
+ /**
797
+ * List of attachments included with this message
798
+ * @defaultValue []
799
+ */
800
+ attachments = input([]);
801
+ /** Parameter to pass to action handlers */
802
+ actionParam = input();
803
+ /**
804
+ * More actions button aria label
805
+ *
806
+ * @defaultValue
807
+ * ```
808
+ * t(() => $localize`:@@SI_USER_MESSAGE.SECONDARY_ACTIONS:More actions`)
809
+ * ```
810
+ */
811
+ secondaryActionsLabel = input(t(() => $localize `:@@SI_USER_MESSAGE.SECONDARY_ACTIONS:More actions`));
812
+ hasAttachments = computed(() => this.attachments().length > 0);
813
+ textContent = signal(undefined);
814
+ constructor() {
815
+ effect(() => {
816
+ const formatter = this.contentFormatter();
817
+ const contentValue = this.content();
818
+ const container = this.formattedContent()?.nativeElement;
819
+ if (container && contentValue) {
820
+ if (formatter) {
821
+ const formatted = formatter(contentValue);
822
+ if (typeof formatted === 'string') {
823
+ this.textContent.set(formatted);
824
+ }
825
+ else if (formatted instanceof Node) {
826
+ this.textContent.set(undefined);
827
+ container.innerHTML = '';
828
+ container.appendChild(formatted);
829
+ }
830
+ }
831
+ else {
832
+ this.textContent.set(contentValue);
833
+ }
834
+ }
835
+ });
836
+ }
837
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiUserMessageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
838
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: SiUserMessageComponent, isStandalone: true, selector: "si-user-message", inputs: { content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired: false, transformFunction: null }, contentFormatter: { classPropertyName: "contentFormatter", publicName: "contentFormatter", isSignal: true, isRequired: false, transformFunction: null }, actions: { classPropertyName: "actions", publicName: "actions", isSignal: true, isRequired: false, transformFunction: null }, secondaryActions: { classPropertyName: "secondaryActions", publicName: "secondaryActions", isSignal: true, isRequired: false, transformFunction: null }, attachments: { classPropertyName: "attachments", publicName: "attachments", isSignal: true, isRequired: false, transformFunction: null }, actionParam: { classPropertyName: "actionParam", publicName: "actionParam", isSignal: true, isRequired: false, transformFunction: null }, secondaryActionsLabel: { classPropertyName: "secondaryActionsLabel", publicName: "secondaryActionsLabel", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "formattedContent", first: true, predicate: ["formattedContent"], descendants: true, isSignal: true }], ngImport: i0, template: "<si-chat-message alignment=\"end\" actionsPosition=\"bottom\" [loading]=\"false\">\n @if (hasAttachments()) {\n <si-attachment-list alignment=\"end\" [attachments]=\"attachments()\" [removable]=\"false\" />\n }\n\n @if (content()) {\n @let content = textContent();\n @if (content) {\n <span class=\"text-pre-wrap\">{{ content }}</span>\n } @else {\n <div #formattedContent> </div>\n }\n }\n @if (actions().length > 0 || secondaryActions().length > 0) {\n <div class=\"d-flex gap-4 user-message-actions\" siChatMessageAction>\n @for (action of actions(); track $index) {\n <button\n type=\"button\"\n class=\"btn btn-ghost btn-circle btn-sm\"\n [disabled]=\"action.disabled\"\n [attr.aria-label]=\"action.label | translate\"\n (click)=\"action.action(actionParam(), action)\"\n >\n <si-icon [icon]=\"action.icon\" />\n </button>\n }\n\n @if (secondaryActions().length > 0) {\n <button\n type=\"button\"\n class=\"btn btn-ghost btn-circle btn-sm\"\n [cdkMenuTriggerFor]=\"secondaryActionsMenu\"\n [attr.aria-label]=\"secondaryActionsLabel() | translate\"\n [attr.title]=\"secondaryActionsLabel() | translate\"\n >\n <si-icon icon=\"element-optionsVertical\" />\n </button>\n\n <ng-template #secondaryActionsMenu>\n <si-menu-factory [items]=\"secondaryActions()\" [actionParam]=\"actionParam()\" />\n </ng-template>\n }\n </div>\n }\n</si-chat-message>\n", styles: [":host{display:block}:host:not(:has([siChatMessageAction])) si-chat-message{padding-block-end:36px!important}.user-message-actions{opacity:0;transition:opacity .2s ease}si-chat-message{--chat-message-bubble-bg: var(--element-base-input-experimental);max-inline-size:600px;margin-inline-start:auto}:host:hover .user-message-actions,.user-message-actions:hover,.user-message-actions:has(::ng-deep [aria-expanded=true]),:host-context(.si-container-xs,.si-container-sm) .user-message-actions:active{opacity:1}\n"], dependencies: [{ kind: "directive", type: CdkMenuTrigger, selector: "[cdkMenuTriggerFor]", inputs: ["cdkMenuTriggerFor", "cdkMenuPosition", "cdkMenuTriggerData"], outputs: ["cdkMenuOpened", "cdkMenuClosed"], exportAs: ["cdkMenuTriggerFor"] }, { kind: "component", type: SiAttachmentListComponent, selector: "si-attachment-list", inputs: ["attachments", "alignment", "removable", "removeLabel"], outputs: ["remove"] }, { kind: "component", type: SiChatMessageComponent, selector: "si-chat-message", inputs: ["loading", "alignment", "actionsPosition"] }, { kind: "component", type: SiIconComponent, selector: "si-icon", inputs: ["icon"] }, { kind: "component", type: SiMenuFactoryComponent, selector: "si-menu-factory", inputs: ["items", "actionParam"] }, { kind: "directive", type: SiChatMessageActionDirective, selector: "[siChatMessageAction]" }, { kind: "pipe", type: SiTranslatePipe, name: "translate" }] });
839
+ }
840
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiUserMessageComponent, decorators: [{
841
+ type: Component,
842
+ args: [{ selector: 'si-user-message', imports: [
843
+ CdkMenuTrigger,
844
+ SiAttachmentListComponent,
845
+ SiChatMessageComponent,
846
+ SiIconComponent,
847
+ SiMenuFactoryComponent,
848
+ SiChatMessageActionDirective,
849
+ SiTranslatePipe
850
+ ], template: "<si-chat-message alignment=\"end\" actionsPosition=\"bottom\" [loading]=\"false\">\n @if (hasAttachments()) {\n <si-attachment-list alignment=\"end\" [attachments]=\"attachments()\" [removable]=\"false\" />\n }\n\n @if (content()) {\n @let content = textContent();\n @if (content) {\n <span class=\"text-pre-wrap\">{{ content }}</span>\n } @else {\n <div #formattedContent> </div>\n }\n }\n @if (actions().length > 0 || secondaryActions().length > 0) {\n <div class=\"d-flex gap-4 user-message-actions\" siChatMessageAction>\n @for (action of actions(); track $index) {\n <button\n type=\"button\"\n class=\"btn btn-ghost btn-circle btn-sm\"\n [disabled]=\"action.disabled\"\n [attr.aria-label]=\"action.label | translate\"\n (click)=\"action.action(actionParam(), action)\"\n >\n <si-icon [icon]=\"action.icon\" />\n </button>\n }\n\n @if (secondaryActions().length > 0) {\n <button\n type=\"button\"\n class=\"btn btn-ghost btn-circle btn-sm\"\n [cdkMenuTriggerFor]=\"secondaryActionsMenu\"\n [attr.aria-label]=\"secondaryActionsLabel() | translate\"\n [attr.title]=\"secondaryActionsLabel() | translate\"\n >\n <si-icon icon=\"element-optionsVertical\" />\n </button>\n\n <ng-template #secondaryActionsMenu>\n <si-menu-factory [items]=\"secondaryActions()\" [actionParam]=\"actionParam()\" />\n </ng-template>\n }\n </div>\n }\n</si-chat-message>\n", styles: [":host{display:block}:host:not(:has([siChatMessageAction])) si-chat-message{padding-block-end:36px!important}.user-message-actions{opacity:0;transition:opacity .2s ease}si-chat-message{--chat-message-bubble-bg: var(--element-base-input-experimental);max-inline-size:600px;margin-inline-start:auto}:host:hover .user-message-actions,.user-message-actions:hover,.user-message-actions:has(::ng-deep [aria-expanded=true]),:host-context(.si-container-xs,.si-container-sm) .user-message-actions:active{opacity:1}\n"] }]
851
+ }], ctorParameters: () => [] });
852
+
853
+ /**
854
+ * Copyright (c) Siemens 2016 - 2025
855
+ * SPDX-License-Identifier: MIT
856
+ */
857
+
858
+ /**
859
+ * Generated bundle index. Do not edit.
860
+ */
861
+
862
+ export { SiAiMessageComponent, SiAttachmentListComponent, SiChatInputComponent, SiChatInputDisclaimerDirective, SiChatMessageActionDirective, SiChatMessageComponent, SiUserMessageComponent };
863
+ //# sourceMappingURL=siemens-element-ng-chat-messages.mjs.map