@siemens/element-ng 48.2.0 → 48.4.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 (80) hide show
  1. package/accordion/index.d.ts +5 -2
  2. package/application-header/index.d.ts +15 -2
  3. package/chat-messages/index.d.ts +670 -0
  4. package/chat-messages/package.json +3 -0
  5. package/dashboard/index.d.ts +3 -3
  6. package/fesm2022/siemens-element-ng-accordion.mjs +7 -4
  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 +959 -0
  13. package/fesm2022/siemens-element-ng-chat-messages.mjs.map +1 -0
  14. package/fesm2022/siemens-element-ng-dashboard.mjs +15 -15
  15. package/fesm2022/siemens-element-ng-dashboard.mjs.map +1 -1
  16. package/fesm2022/siemens-element-ng-datepicker.mjs +5 -3
  17. package/fesm2022/siemens-element-ng-datepicker.mjs.map +1 -1
  18. package/fesm2022/siemens-element-ng-file-uploader.mjs +277 -118
  19. package/fesm2022/siemens-element-ng-file-uploader.mjs.map +1 -1
  20. package/fesm2022/siemens-element-ng-filtered-search.mjs +7 -12
  21. package/fesm2022/siemens-element-ng-filtered-search.mjs.map +1 -1
  22. package/fesm2022/siemens-element-ng-icon.mjs +3 -1
  23. package/fesm2022/siemens-element-ng-icon.mjs.map +1 -1
  24. package/fesm2022/siemens-element-ng-ip-input.mjs +92 -89
  25. package/fesm2022/siemens-element-ng-ip-input.mjs.map +1 -1
  26. package/fesm2022/siemens-element-ng-markdown-renderer.mjs +253 -0
  27. package/fesm2022/siemens-element-ng-markdown-renderer.mjs.map +1 -0
  28. package/fesm2022/siemens-element-ng-navbar-vertical.mjs +2 -9
  29. package/fesm2022/siemens-element-ng-navbar-vertical.mjs.map +1 -1
  30. package/fesm2022/siemens-element-ng-phone-number.mjs +5 -4
  31. package/fesm2022/siemens-element-ng-phone-number.mjs.map +1 -1
  32. package/fesm2022/siemens-element-ng-popover.mjs +3 -4
  33. package/fesm2022/siemens-element-ng-popover.mjs.map +1 -1
  34. package/fesm2022/siemens-element-ng-resize-observer.mjs +13 -0
  35. package/fesm2022/siemens-element-ng-resize-observer.mjs.map +1 -1
  36. package/fesm2022/siemens-element-ng-select.mjs +4 -0
  37. package/fesm2022/siemens-element-ng-select.mjs.map +1 -1
  38. package/fesm2022/siemens-element-ng-side-panel.mjs +3 -5
  39. package/fesm2022/siemens-element-ng-side-panel.mjs.map +1 -1
  40. package/fesm2022/siemens-element-ng-status-bar.mjs +3 -4
  41. package/fesm2022/siemens-element-ng-status-bar.mjs.map +1 -1
  42. package/fesm2022/siemens-element-ng-tour.mjs +58 -21
  43. package/fesm2022/siemens-element-ng-tour.mjs.map +1 -1
  44. package/fesm2022/siemens-element-ng-translate.mjs.map +1 -1
  45. package/fesm2022/siemens-element-ng-tree-view.mjs +43 -4
  46. package/fesm2022/siemens-element-ng-tree-view.mjs.map +1 -1
  47. package/file-uploader/index.d.ts +119 -15
  48. package/filtered-search/index.d.ts +6 -7
  49. package/icon/index.d.ts +3 -1
  50. package/ip-input/index.d.ts +13 -0
  51. package/markdown-renderer/index.d.ts +36 -0
  52. package/markdown-renderer/package.json +3 -0
  53. package/navbar-vertical/index.d.ts +2 -4
  54. package/package.json +15 -7
  55. package/resize-observer/index.d.ts +13 -0
  56. package/schematics/migrations/action-modal-migration/action-modal-migration.js +47 -6
  57. package/schematics/migrations/data/attribute-selectors.js +6 -0
  58. package/schematics/migrations/data/component-names.js +78 -0
  59. package/schematics/migrations/data/element-selectors.js +10 -0
  60. package/schematics/migrations/data/index.js +17 -0
  61. package/schematics/migrations/data/output-names.js +7 -0
  62. package/schematics/migrations/data/symbol-removals.js +49 -0
  63. package/schematics/migrations/element-migration/element-migration.js +93 -0
  64. package/schematics/migrations/element-migration/index.js +5 -0
  65. package/schematics/migrations/index.js +7 -2
  66. package/schematics/migrations/wizard-migration/index.js +80 -0
  67. package/schematics/scss-import-to-siemens-migration/index.js +3 -3
  68. package/schematics/ts-import-to-siemens-migration/index.js +2 -2
  69. package/schematics/utils/index.js +3 -3
  70. package/schematics/utils/project-utils.js +24 -35
  71. package/schematics/utils/template-utils.js +78 -2
  72. package/schematics/utils/ts-utils.js +5 -5
  73. package/select/index.d.ts +5 -0
  74. package/status-bar/index.d.ts +0 -1
  75. package/template-i18n.json +10 -0
  76. package/tour/index.d.ts +4 -2
  77. package/translate/index.d.ts +10 -0
  78. package/tree-view/index.d.ts +40 -1
  79. package/schematics/migrations/to-legacy-migration/to-legacy-migration.js +0 -55
  80. package/schematics/migrations/to-legacy-migration/to-legacy-replacement.js +0 -35
@@ -0,0 +1,959 @@
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, PLATFORM_ID, 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 { isPlatformBrowser } from '@angular/common';
11
+ import * as i1$1 from '@angular/forms';
12
+ import { FormsModule } from '@angular/forms';
13
+ import { SiFileUploadDirective } from '@siemens/element-ng/file-uploader';
14
+
15
+ /**
16
+ * Copyright (c) Siemens 2016 - 2025
17
+ * SPDX-License-Identifier: MIT
18
+ */
19
+ /**
20
+ * Directive to mark content as chat message actions into {@link SiChatMessageComponent}.
21
+ * Apply this directive to e.g. buttons that should be slotted into the message actions area.
22
+ *
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
+ * @see {@link SiChatMessageComponent} for the chat message wrapper component
33
+ *
34
+ * @experimental
35
+ */
36
+ class SiChatMessageActionDirective {
37
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiChatMessageActionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
38
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.0.6", type: SiChatMessageActionDirective, isStandalone: true, selector: "[siChatMessageAction]", ngImport: i0 });
39
+ }
40
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiChatMessageActionDirective, decorators: [{
41
+ type: Directive,
42
+ args: [{
43
+ selector: '[siChatMessageAction]'
44
+ }]
45
+ }] });
46
+
47
+ /**
48
+ * Copyright (c) Siemens 2016 - 2025
49
+ * SPDX-License-Identifier: MIT
50
+ */
51
+ /**
52
+ * Base declarative chat message component that provides the layout structure for chat messages.
53
+ *
54
+ * This component handles the core message layout including avatar positioning, loading states,
55
+ * and action button as well as attachment list placement. It serves as the foundation for more specialized message components
56
+ * like {@link SiUserMessageComponent} and {@link SiAiMessageComponent}.
57
+ * Can be used within {@link SiChatContainerComponent}.
58
+ *
59
+ * The component provides:
60
+ * - Flexible alignment (start/end) for different message types
61
+ * - Avatar/icon slot for message attribution
62
+ * - Loading state with skeleton UI
63
+ * - Action buttons positioned on the side or bottom
64
+ * - Attachment list display slot
65
+ * - Responsive behavior that adapts to container size
66
+ *
67
+ * This is a low-level component designed for slotting in custom content, it provides slots via content projection:
68
+ * - Default content: Main message content area (consider using {@link SiMarkdownRendererComponent} for markdown support)
69
+ * - `si-avatar/si-icon/img` selector: Avatar or icon representing the message sender
70
+ * - `si-chat-message-action` selector: Action buttons related to the message
71
+ * - `si-attachment-list` selector: Attachment list component for displaying file attachments
72
+ *
73
+ * @see {@link SiUserMessageComponent} for user message display
74
+ * @see {@link SiAiMessageComponent} for AI message display
75
+ * @see {@link SiAttachmentListComponent} for attachment list to slot in
76
+ * @see {@link SiChatMessageActionDirective} for action buttons to slot in
77
+ * @see {@link SiMarkdownRendererComponent} for markdown content rendering
78
+ * @see {@link SiChatContainerComponent} for the chat container to use this within
79
+ *
80
+ * @experimental
81
+ */
82
+ class SiChatMessageComponent {
83
+ /**
84
+ * Whether the message is currently loading
85
+ * @defaultValue false
86
+ */
87
+ loading = input(false);
88
+ /**
89
+ * Alignment of the message
90
+ * @defaultValue 'start'
91
+ */
92
+ alignment = input('start');
93
+ /**
94
+ * Where to display action buttons (if any)
95
+ * @defaultValue 'side'
96
+ */
97
+ actionsPosition = input('side');
98
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiChatMessageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
99
+ 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.start]=\"alignment() === 'start'\"\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}.message-wrapper{min-inline-size:0;inline-size:auto}.message-wrapper.start{margin-inline-end:auto}.message-wrapper.end{margin-inline-start:auto}: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-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) .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"] });
100
+ }
101
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiChatMessageComponent, decorators: [{
102
+ type: Component,
103
+ args: [{ selector: 'si-chat-message', host: {
104
+ class: 'd-block'
105
+ }, 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.start]=\"alignment() === 'start'\"\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}.message-wrapper{min-inline-size:0;inline-size:auto}.message-wrapper.start{margin-inline-end:auto}.message-wrapper.end{margin-inline-start:auto}: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-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) .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"] }]
106
+ }] });
107
+
108
+ /**
109
+ * Copyright (c) Siemens 2016 - 2025
110
+ * SPDX-License-Identifier: MIT
111
+ */
112
+ /**
113
+ * AI message component for displaying AI-generated responses in conversational interfaces.
114
+ *
115
+ * The AI message component renders AI-generated content in chat interfaces,
116
+ * supporting text formatting, markdown, loading states, and contextual actions.
117
+ * It appears as text (no bubble) aligned to the left side without any avatar/icon slot.
118
+ * Can be used within {@link SiChatContainerComponent}.
119
+ *
120
+ * The component automatically handles:
121
+ * - Styling for AI messages distinct from user or generic chat messages
122
+ * - Option to render markdown content, provide via `contentFormatter` input with a markdown renderer function (e.g., from {@link getMarkdownRenderer})
123
+ * - Showing loading states with skeleton UI during generation
124
+ * - Displaying primary and secondary actions
125
+ *
126
+ * @see {@link SiChatMessageComponent} for the base message wrapper component
127
+ * @see {@link SiUserMessageComponent} for the user message component
128
+ * @see {@link getMarkdownRenderer} for markdown formatting support
129
+ * @see {@link SiChatContainerComponent} for the chat container to use this within
130
+ *
131
+ * @experimental
132
+ */
133
+ class SiAiMessageComponent {
134
+ formattedContent = viewChild('formattedContent');
135
+ /**
136
+ * The AI-generated message content
137
+ * @defaultValue ''
138
+ */
139
+ content = input('');
140
+ /**
141
+ * Optional formatter function to transform content before display.
142
+ * - Returns string: Content will be sanitized using Angular's DomSanitizer
143
+ * - Returns Node: DOM node will be inserted directly without sanitization
144
+ *
145
+ * **Note:** If using a markdown renderer, make sure to apply the `markdown-content` class
146
+ * to the root element to ensure proper styling using the Element theme (e.g., `div.className = 'markdown-content'`).
147
+ * The function returned by {@link getMarkdownRenderer} does this automatically.
148
+ *
149
+ * **Warning:** When returning a Node, ensure the content is safe to prevent XSS attacks
150
+ * @defaultValue undefined
151
+ */
152
+ contentFormatter = input(undefined);
153
+ textContent = signal(undefined);
154
+ constructor() {
155
+ effect(() => {
156
+ const formatter = this.contentFormatter();
157
+ const contentValue = this.content();
158
+ const container = this.formattedContent()?.nativeElement;
159
+ if (container && contentValue) {
160
+ if (formatter) {
161
+ const formatted = formatter(contentValue);
162
+ if (typeof formatted === 'string') {
163
+ this.textContent.set(formatted);
164
+ }
165
+ else if (formatted instanceof Node) {
166
+ this.textContent.set(undefined);
167
+ container.innerHTML = '';
168
+ container.appendChild(formatted);
169
+ }
170
+ }
171
+ else {
172
+ this.textContent.set(contentValue);
173
+ }
174
+ }
175
+ });
176
+ }
177
+ /**
178
+ * Whether the message is currently being generated (shows skeleton)
179
+ * @defaultValue false
180
+ */
181
+ loading = input(false, { transform: booleanAttribute });
182
+ /**
183
+ * Primary actions available for this message (thumbs up/down, copy, retry, etc.)
184
+ * All actions displayed inline
185
+ * @defaultValue []
186
+ */
187
+ actions = input([]);
188
+ /**
189
+ * Secondary actions available in dropdown menu, first use primary actions and only add secondary actions additionally
190
+ * @defaultValue []
191
+ */
192
+ secondaryActions = input([]);
193
+ /** Parameter to pass to action handlers */
194
+ actionParam = input();
195
+ /**
196
+ * More actions button aria label
197
+ *
198
+ * @defaultValue
199
+ * ```
200
+ * t(() => $localize`:@@SI_AI_MESSAGE.SECONDARY_ACTIONS:More actions`)
201
+ * ```
202
+ */
203
+ secondaryActionsLabel = input(t(() => $localize `:@@SI_AI_MESSAGE.SECONDARY_ACTIONS:More actions`));
204
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiAiMessageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
205
+ 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 } }, 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 > 0) || (secondaryActions()?.length ?? 0 > 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 > 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" }] });
206
+ }
207
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiAiMessageComponent, decorators: [{
208
+ type: Component,
209
+ args: [{ selector: 'si-ai-message', imports: [
210
+ CdkMenuTrigger,
211
+ SiChatMessageComponent,
212
+ SiIconComponent,
213
+ SiMenuFactoryComponent,
214
+ SiChatMessageActionDirective,
215
+ SiTranslatePipe
216
+ ], 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 > 0) || (secondaryActions()?.length ?? 0 > 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 > 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"] }]
217
+ }], ctorParameters: () => [] });
218
+
219
+ /**
220
+ * Copyright (c) Siemens 2016 - 2025
221
+ * SPDX-License-Identifier: MIT
222
+ */
223
+ /**
224
+ * Attachment list component for displaying file attachments in chat messages.
225
+ *
226
+ * This component renders a list of file attachments with icons, names, and optional
227
+ * preview and remove functionality. It's designed to work with chat message components
228
+ * to show files that have been uploaded or shared in conversations.
229
+ *
230
+ * This component provides:
231
+ * - A list of pills showing each attachment's name and an icon
232
+ * - Optional preview modal for attachments
233
+ * - Optional remove functionality for editable messages
234
+ *
235
+ * The component is included within {@link SiUserMessageComponent}, {@link SiAiMessageComponent} and {@link SiChatInputComponent} but can also be used inside custom chat messages with {@link SiChatMessageComponent}
236
+ *
237
+ * @see {@link SiUserMessageComponent} for user message display
238
+ * @see {@link SiAiMessageComponent} for AI message display
239
+ * @see {@link SiChatMessageComponent} for custom chat message display
240
+ * @see {@link SiChatInputComponent} for chat input with attachment support
241
+ * @see {@link Attachment} for attachment data structure
242
+ *
243
+ * @experimental
244
+ */
245
+ class SiAttachmentListComponent {
246
+ modalService = inject(SiModalService);
247
+ /**
248
+ * List of attachments to display
249
+ * @defaultValue []
250
+ */
251
+ attachments = input([]);
252
+ /**
253
+ * Whether to align attachments to the end (right) or start (left)
254
+ * @defaultValue 'start'
255
+ */
256
+ alignment = input('start');
257
+ /**
258
+ * Whether to show remove buttons on attachments
259
+ * @defaultValue false
260
+ */
261
+ removable = input(false, { transform: booleanAttribute });
262
+ /**
263
+ * Label for remove attachment button
264
+ *
265
+ * @defaultValue
266
+ * ```
267
+ * t(() => $localize`:@@SI_ATTACHMENT_LIST.REMOVE_ATTACHMENT:Remove attachment`)
268
+ * ```
269
+ */
270
+ removeLabel = input(t(() => $localize `:@@SI_ATTACHMENT_LIST.REMOVE_ATTACHMENT:Remove attachment`));
271
+ /**
272
+ * Emitted when an attachment should be removed
273
+ */
274
+ remove = output();
275
+ getPreviewTemplate(attachment) {
276
+ if (attachment.previewTemplate) {
277
+ return typeof attachment.previewTemplate === 'function'
278
+ ? attachment.previewTemplate()
279
+ : attachment.previewTemplate;
280
+ }
281
+ return undefined;
282
+ }
283
+ openPreview(event, attachment) {
284
+ const template = this.getPreviewTemplate(attachment);
285
+ if (template) {
286
+ event.preventDefault();
287
+ this.modalService.show(template, {
288
+ inputValues: { 'attachment': attachment }
289
+ });
290
+ }
291
+ }
292
+ getFileIcon(name) {
293
+ // TODO: Accept map and default it in file upload directive.
294
+ return 'element-document';
295
+ }
296
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiAttachmentListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
297
+ 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" }] });
298
+ }
299
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiAttachmentListComponent, decorators: [{
300
+ type: Component,
301
+ 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"] }]
302
+ }] });
303
+
304
+ /**
305
+ * Copyright (c) Siemens 2016 - 2025
306
+ * SPDX-License-Identifier: MIT
307
+ */
308
+ /**
309
+ * A declarative container component for displaying a chat interface with automatic scroll-to-bottom behavior.
310
+ *
311
+ * This component provides the layout and styling for a chat interface, managing scrolling behavior
312
+ * to keep the newest messages visible while respecting user scrolling actions. It automatically
313
+ * scrolls to the bottom when new content is added, unless the user has scrolled up to view older messages.
314
+ *
315
+ * Use via content projection:
316
+ * - Default content: Chat messages displayed in the scrollable messages container or something like an empty state.
317
+ * - `si-inline-notification` selector: Notification component displayed above the input area
318
+ * - `si-chat-input` or `[siChatContainerInput]` selector: Input controls for composing messages
319
+ *
320
+ * @see {@link SiChatInputComponent} for the chat input wrapper component
321
+ * @see {@link SiChatContainerInputDirective} for other input controls to slot in
322
+ * @see {@link SiAiMessageComponent} for AI messages to slot in
323
+ * @see {@link SiUserMessageComponent} for user messages (in AI chats) to slot in
324
+ * @see {@link SiChatMessageComponent} for the chat message wrapper component to slot in other messages
325
+ *
326
+ * @experimental
327
+ */
328
+ class SiChatContainerComponent {
329
+ messagesContainer = viewChild('messagesContainer');
330
+ platformId = inject(PLATFORM_ID);
331
+ isUserAtBottom = true;
332
+ scrollTimeout;
333
+ resizeObserver;
334
+ contentObserver;
335
+ /**
336
+ * The color variant to apply to the container.
337
+ * @defaultValue 'base-0'
338
+ */
339
+ colorVariant = input('base-0');
340
+ /**
341
+ * Disables automatic scrolling to the bottom when new content is added.
342
+ * @defaultValue false
343
+ */
344
+ noAutoScroll = input(false, {
345
+ transform: (value) => value === '' || value === true
346
+ });
347
+ constructor() {
348
+ effect(() => {
349
+ if (this.messagesContainer()) {
350
+ this.setupResizeObserver();
351
+ this.setupContentObserver();
352
+ }
353
+ });
354
+ }
355
+ ngAfterContentInit() {
356
+ this.scrollToBottom();
357
+ }
358
+ ngOnDestroy() {
359
+ if (this.scrollTimeout) {
360
+ clearTimeout(this.scrollTimeout);
361
+ }
362
+ if (this.resizeObserver) {
363
+ this.resizeObserver.disconnect();
364
+ }
365
+ if (this.contentObserver) {
366
+ this.contentObserver.disconnect();
367
+ }
368
+ }
369
+ scrollToBottom() {
370
+ if (this.noAutoScroll() || !this.isUserAtBottom) {
371
+ return;
372
+ }
373
+ const container = this.messagesContainer();
374
+ if (!container) {
375
+ return;
376
+ }
377
+ const element = container.nativeElement;
378
+ element.scrollTop = element.scrollHeight;
379
+ }
380
+ debouncedScrollToBottom() {
381
+ if (this.scrollTimeout) {
382
+ clearTimeout(this.scrollTimeout);
383
+ }
384
+ this.scrollTimeout = setTimeout(() => {
385
+ this.scrollToBottom();
386
+ }, 100);
387
+ }
388
+ setupResizeObserver() {
389
+ if (!isPlatformBrowser(this.platformId)) {
390
+ return;
391
+ }
392
+ const container = this.messagesContainer();
393
+ if (!container || this.resizeObserver) {
394
+ return;
395
+ }
396
+ this.resizeObserver = new ResizeObserver(() => {
397
+ this.debouncedScrollToBottom();
398
+ });
399
+ this.resizeObserver.observe(container.nativeElement);
400
+ }
401
+ setupContentObserver() {
402
+ if (!isPlatformBrowser(this.platformId)) {
403
+ return;
404
+ }
405
+ const container = this.messagesContainer();
406
+ if (!container || this.contentObserver) {
407
+ return;
408
+ }
409
+ this.contentObserver = new MutationObserver(() => {
410
+ this.debouncedScrollToBottom();
411
+ });
412
+ this.contentObserver.observe(container.nativeElement, {
413
+ childList: true,
414
+ subtree: true,
415
+ characterData: true
416
+ });
417
+ }
418
+ checkIfUserAtBottom() {
419
+ const container = this.messagesContainer();
420
+ if (!container) {
421
+ return;
422
+ }
423
+ const element = container.nativeElement;
424
+ const threshold = 100;
425
+ this.isUserAtBottom =
426
+ element.scrollHeight - element.scrollTop - element.clientHeight < threshold;
427
+ }
428
+ onScroll() {
429
+ this.checkIfUserAtBottom();
430
+ }
431
+ /**
432
+ * Focuses the messages container element.
433
+ */
434
+ focus() {
435
+ const container = this.messagesContainer();
436
+ container?.nativeElement.focus();
437
+ }
438
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiChatContainerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
439
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "20.0.6", type: SiChatContainerComponent, isStandalone: true, selector: "si-chat-container", inputs: { colorVariant: { classPropertyName: "colorVariant", publicName: "colorVariant", isSignal: true, isRequired: false, transformFunction: null }, noAutoScroll: { classPropertyName: "noAutoScroll", publicName: "noAutoScroll", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "colorVariant()" }, classAttribute: "d-flex si-layout-inner flex-grow-1 flex-column h-100 w-100" }, viewQueries: [{ propertyName: "messagesContainer", first: true, predicate: ["messagesContainer"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"chat-container d-flex flex-column flex-grow-1 px-6 px-md-9 py-6 w-100\">\n <div\n #messagesContainer\n class=\"messages-container d-flex flex-column flex-grow-1 w-100 align-self-center\"\n tabindex=\"0\"\n (scroll)=\"onScroll()\"\n >\n <ng-content />\n </div>\n\n <div class=\"chat-input-area py-3 pt-0 mt-6 d-flex flex-column align-items-center\">\n <ng-content select=\"si-inline-notification\" />\n <ng-content select=\"si-chat-input,[siChatContainerInput]\" />\n </div>\n</div>\n", styles: [":host{overflow-y:hidden;overflow-x:visible}:host.base-0{background-color:var(--element-base-0)}:host.base-1{background-color:var(--element-base-1)}.chat-container{min-block-size:0;overflow-y:hidden;overflow-x:visible}.chat-container.initialized{scroll-behavior:smooth}.messages-container{max-inline-size:720px;overflow-y:auto;overflow-x:visible;gap:40px}.messages-container ::ng-deep si-user-message:not(.last){margin-block-end:-40px}.chat-input-area{justify-self:flex-end;align-self:center;overflow:visible;inline-size:100%;max-inline-size:720px}::ng-deep .chat-input-area:empty:not(:has(si-inline-notification)),::ng-deep .chat-input-area:has([sichatcontainerinput]:empty):not(:has(si-inline-notification)){display:none!important}.chat-input-area:empty ::ng-deep>si-inline-notification,.chat-input-area:empty ::ng-deep *>si-inline-notification,.chat-input-area:has([sichatcontainerinput]:empty) ::ng-deep>si-inline-notification,.chat-input-area:has([sichatcontainerinput]:empty) ::ng-deep *>si-inline-notification{margin-block-end:0!important}.chat-container:has(.messages-container:not(:empty)) .messages-empty-state{display:none!important}:host-context(.si-container-xs,.si-container-sm) ::ng-deep si-inline-notification .alert,:host-context(.si-container-xs,.si-container-sm) ::ng-deep si-inline-notification .alert-link{font-size:.875rem}\n"] });
440
+ }
441
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiChatContainerComponent, decorators: [{
442
+ type: Component,
443
+ args: [{ selector: 'si-chat-container', host: {
444
+ class: 'd-flex si-layout-inner flex-grow-1 flex-column h-100 w-100',
445
+ '[class]': 'colorVariant()'
446
+ }, template: "<div class=\"chat-container d-flex flex-column flex-grow-1 px-6 px-md-9 py-6 w-100\">\n <div\n #messagesContainer\n class=\"messages-container d-flex flex-column flex-grow-1 w-100 align-self-center\"\n tabindex=\"0\"\n (scroll)=\"onScroll()\"\n >\n <ng-content />\n </div>\n\n <div class=\"chat-input-area py-3 pt-0 mt-6 d-flex flex-column align-items-center\">\n <ng-content select=\"si-inline-notification\" />\n <ng-content select=\"si-chat-input,[siChatContainerInput]\" />\n </div>\n</div>\n", styles: [":host{overflow-y:hidden;overflow-x:visible}:host.base-0{background-color:var(--element-base-0)}:host.base-1{background-color:var(--element-base-1)}.chat-container{min-block-size:0;overflow-y:hidden;overflow-x:visible}.chat-container.initialized{scroll-behavior:smooth}.messages-container{max-inline-size:720px;overflow-y:auto;overflow-x:visible;gap:40px}.messages-container ::ng-deep si-user-message:not(.last){margin-block-end:-40px}.chat-input-area{justify-self:flex-end;align-self:center;overflow:visible;inline-size:100%;max-inline-size:720px}::ng-deep .chat-input-area:empty:not(:has(si-inline-notification)),::ng-deep .chat-input-area:has([sichatcontainerinput]:empty):not(:has(si-inline-notification)){display:none!important}.chat-input-area:empty ::ng-deep>si-inline-notification,.chat-input-area:empty ::ng-deep *>si-inline-notification,.chat-input-area:has([sichatcontainerinput]:empty) ::ng-deep>si-inline-notification,.chat-input-area:has([sichatcontainerinput]:empty) ::ng-deep *>si-inline-notification{margin-block-end:0!important}.chat-container:has(.messages-container:not(:empty)) .messages-empty-state{display:none!important}:host-context(.si-container-xs,.si-container-sm) ::ng-deep si-inline-notification .alert,:host-context(.si-container-xs,.si-container-sm) ::ng-deep si-inline-notification .alert-link{font-size:.875rem}\n"] }]
447
+ }], ctorParameters: () => [] });
448
+
449
+ /**
450
+ * Copyright (c) Siemens 2016 - 2025
451
+ * SPDX-License-Identifier: MIT
452
+ */
453
+ /**
454
+ * A directive to mark elements as input controls within a {@link SiChatContainerComponent}.
455
+ *
456
+ * This directive is used to identify and style input elements that belong to
457
+ * the chat container component, typically applied to form inputs or textareas
458
+ * used for composing chat messages.
459
+ *
460
+ * @example
461
+ * ```html
462
+ * <si-chat-container>
463
+ * <si-chat-message>Hello!</si-chat-message>
464
+ * <si-chat-message>How are you?</si-chat-message>
465
+ *
466
+ * <input siChatContainerInput type="text" placeholder="Type a message..." />
467
+ * </si-chat-container>
468
+ * ```
469
+ *
470
+ * @see {@link SiChatContainerComponent} for the chat container wrapper component
471
+ *
472
+ * @experimental
473
+ */
474
+ class SiChatContainerInputDirective {
475
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiChatContainerInputDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
476
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.0.6", type: SiChatContainerInputDirective, isStandalone: true, selector: "[siChatContainerInput]", ngImport: i0 });
477
+ }
478
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiChatContainerInputDirective, decorators: [{
479
+ type: Directive,
480
+ args: [{
481
+ selector: '[siChatContainerInput]'
482
+ }]
483
+ }] });
484
+
485
+ /**
486
+ * Copyright (c) Siemens 2016 - 2025
487
+ * SPDX-License-Identifier: MIT
488
+ */
489
+ /**
490
+ * Chat input component for composing and sending messages in conversational interfaces.
491
+ *
492
+ * The chat input component provides a text area for users to compose messages,
493
+ * supporting text, attachments, and contextual actions. It appears as a textarea
494
+ * with buttons for adding attachments and sending messages, as well as an optional disclaimer.
495
+ *
496
+ * The component automatically handles:
497
+ * - Styling for chat input and actions.
498
+ * - Dynamic resizing of the textarea based on content.
499
+ * - Uploading of and displaying of attachments above the input area.
500
+ * - Displaying primary and secondary actions.
501
+ *
502
+ * Additionally to the inputs and outputs documented here, the component supports content projection via the following slots:
503
+ * - Default content: Custom action buttons to display inline, prefer using the `actions` input for buttons, can be used in addition.
504
+ * - `siChatInputDisclaimer` selector: Custom disclaimer content to display below the input area, prefer using the `disclaimer` input for simple text disclaimers.
505
+ *
506
+ * @see {@link SiAttachmentListComponent} for the base attachment component
507
+ * @see {@link SiChatInputDisclaimerDirective} to slot in custom disclaimer content
508
+ *
509
+ * @experimental
510
+ */
511
+ class SiChatInputComponent {
512
+ static idCounter = 0;
513
+ textInput = viewChild('textInput');
514
+ projectedContent = viewChild('projected');
515
+ /**
516
+ * Current input value
517
+ * @defaultValue ''
518
+ */
519
+ value = model('');
520
+ /**
521
+ * Placeholder text for the input
522
+ *
523
+ * @defaultValue
524
+ * ```
525
+ * t(() => $localize`:@@SI_CHAT_INPUT.PLACEHOLDER:Enter a message…`)
526
+ * ```
527
+ */
528
+ placeholder = input(t(() => $localize `:@@SI_CHAT_INPUT.PLACEHOLDER:Enter a message…`));
529
+ /**
530
+ * Whether the input is disabled
531
+ * @defaultValue false
532
+ */
533
+ disabled = input(false, { transform: booleanAttribute });
534
+ /**
535
+ * Whether a message is currently being sent, also prevent the sending of new ones while still allowing the user to type
536
+ * @defaultValue false
537
+ */
538
+ sending = input(false, { transform: booleanAttribute });
539
+ /**
540
+ * Whether the input supports interrupting ongoing operations. When active,
541
+ * the send button transforms into an interrupt button (with element-stop-filled icon).
542
+ * If sending is true, the interrupt button will be disabled.
543
+ * @defaultValue false
544
+ */
545
+ interruptible = input(false, { transform: booleanAttribute });
546
+ /**
547
+ * Maximum number of characters allowed
548
+ */
549
+ maxLength = input();
550
+ /**
551
+ * A disclaimer to display.
552
+ *
553
+ * If not provided, the component will look for projected content with the `siChatInputDisclaimer` directive.
554
+ * If both are empty, no disclaimer section will be shown (handled via CSS :empty).
555
+ */
556
+ disclaimer = input();
557
+ /**
558
+ * Primary actions available in the input (attach files, etc.)
559
+ * All actions displayed inline
560
+ * @defaultValue []
561
+ */
562
+ actions = input([]);
563
+ /**
564
+ * Secondary actions available in dropdown menu
565
+ * @defaultValue []
566
+ */
567
+ secondaryActions = input([]);
568
+ /**
569
+ * Whether file attachments are supported
570
+ * @defaultValue false
571
+ */
572
+ allowAttachments = input(false);
573
+ /**
574
+ * Accepted file types for attachments (as accept string)
575
+ * @defaultValue undefined
576
+ */
577
+ accept = input();
578
+ /**
579
+ * Maximum file size in bytes
580
+ * @defaultValue 10485760 (10MB)
581
+ */
582
+ maxFileSize = input(10485760);
583
+ /**
584
+ * Current attachments
585
+ * @defaultValue []
586
+ */
587
+ attachments = model([]);
588
+ /**
589
+ * The label for the input, used for accessibility
590
+ * @defaultValue
591
+ * ```
592
+ * t(() => $localize`:@@SI_CHAT_INPUT.LABEL:Chat message input`)
593
+ * ```
594
+ */
595
+ label = input(t(() => $localize `:@@SI_CHAT_INPUT.LABEL:Chat message input`));
596
+ /** Parameter to pass to action handlers */
597
+ actionParam = input();
598
+ /**
599
+ * Send button label
600
+ *
601
+ * @defaultValue
602
+ * ```
603
+ * t(() => $localize`:@@SI_CHAT_INPUT.SEND:Send`)
604
+ * ```
605
+ */
606
+ sendButtonLabel = input(t(() => $localize `:@@SI_CHAT_INPUT.SEND:Send`));
607
+ /**
608
+ * Send button icon
609
+ *
610
+ * @defaultValue 'element-send-filled'
611
+ */
612
+ sendButtonIcon = input('element-send-filled');
613
+ /**
614
+ * Interrupt button label
615
+ *
616
+ * @defaultValue
617
+ * ```
618
+ * t(() => $localize`:@@SI_CHAT_INPUT.INTERRUPT:Interrupt`)
619
+ * ```
620
+ */
621
+ interruptButtonLabel = input(t(() => $localize `:@@SI_CHAT_INPUT.INTERRUPT:Interrupt`));
622
+ /**
623
+ * Auto-focus the input on component initialization
624
+ * @defaultValue false
625
+ */
626
+ autoFocus = input(false, { transform: booleanAttribute });
627
+ /**
628
+ * Attach file button aria label
629
+ *
630
+ * @defaultValue
631
+ * ```
632
+ * t(() => $localize`:@@SI_CHAT_INPUT.ATTACH_FILE:Attach file`)
633
+ * ```
634
+ */
635
+ attachFileLabel = input(t(() => $localize `:@@SI_CHAT_INPUT.ATTACH_FILE:Attach file`));
636
+ /**
637
+ * Remove attachment aria label prefix
638
+ *
639
+ * @defaultValue
640
+ * ```
641
+ * t(() => $localize`:@@SI_ATTACHMENT_LIST.REMOVE_ATTACHMENT:Remove attachment`)
642
+ * ```
643
+ */
644
+ removeAttachmentLabel = input(t(() => $localize `:@@SI_ATTACHMENT_LIST.REMOVE_ATTACHMENT:Remove attachment`));
645
+ /**
646
+ * More actions button aria label
647
+ *
648
+ * @defaultValue
649
+ * ```
650
+ * t(() => $localize`:@@SI_CHAT_INPUT.SECONDARY_ACTIONS:More actions`)
651
+ * ```
652
+ */
653
+ secondaryActionsLabel = input(t(() => $localize `:@@SI_CHAT_INPUT.SECONDARY_ACTIONS:More actions`));
654
+ /**
655
+ * Emitted when the user wants to send a message
656
+ */
657
+ send = output();
658
+ /**
659
+ * Emitted when the user wants to interrupt the current operation
660
+ */
661
+ interrupt = output();
662
+ /**
663
+ * Emitted when file upload errors occur
664
+ */
665
+ fileError = output();
666
+ id = `__si-chat-input-${SiChatInputComponent.idCounter++}`;
667
+ hasContent = computed(() => this.value().trim().length > 0);
668
+ hasAttachments = computed(() => this.attachments().length > 0);
669
+ hasActions = computed(() => this.actions().length > 0);
670
+ hasSecondaryActions = computed(() => this.secondaryActions().length > 0);
671
+ canSend = computed(() => (this.hasContent() || this.hasAttachments()) && !this.disabled() && !this.sending());
672
+ showInterruptButton = computed(() => this.interruptible());
673
+ buttonDisabled = computed(() => {
674
+ if (this.showInterruptButton()) {
675
+ return this.disabled() || this.sending();
676
+ }
677
+ return !this.canSend();
678
+ });
679
+ buttonIcon = computed(() => this.showInterruptButton() ? 'element-stop-filled' : this.sendButtonIcon());
680
+ buttonLabel = computed(() => this.showInterruptButton() ? this.interruptButtonLabel() : this.sendButtonLabel());
681
+ get attachmentList() {
682
+ return this.attachments();
683
+ }
684
+ onInputChange(value) {
685
+ this.value.set(value);
686
+ }
687
+ onSend() {
688
+ if (this.canSend()) {
689
+ this.send.emit({
690
+ content: this.value(),
691
+ attachments: this.attachments()
692
+ });
693
+ this.value.set('');
694
+ this.attachments.set([]);
695
+ }
696
+ }
697
+ onButtonClick() {
698
+ if (this.showInterruptButton()) {
699
+ this.interrupt.emit();
700
+ }
701
+ else {
702
+ this.onSend();
703
+ }
704
+ }
705
+ onKeyDown(event) {
706
+ if (event.key === 'Enter' && !event.shiftKey) {
707
+ event.preventDefault();
708
+ if (!this.showInterruptButton()) {
709
+ this.onSend();
710
+ }
711
+ }
712
+ }
713
+ onFilesAdded(uploadFiles) {
714
+ const validFiles = uploadFiles.filter(uploadFile => uploadFile.status === 'added');
715
+ validFiles.forEach(uploadFile => {
716
+ const attachment = {
717
+ name: uploadFile.fileName,
718
+ size: uploadFile.file.size,
719
+ type: uploadFile.file.type,
720
+ file: uploadFile.file
721
+ };
722
+ this.attachments.update(current => [...current, attachment]);
723
+ });
724
+ }
725
+ onFileError(error) {
726
+ this.fileError.emit(error);
727
+ }
728
+ removeAttachment(attachment) {
729
+ this.attachments.update(current => {
730
+ return current.filter(a => a !== attachment);
731
+ });
732
+ }
733
+ onContainerClick(event) {
734
+ const target = event.target;
735
+ // Don't focus if clicking on interactive elements
736
+ if (target.tagName === 'BUTTON' ||
737
+ target.tagName === 'INPUT' ||
738
+ target.tagName === 'TEXTAREA' ||
739
+ target.closest('button') ||
740
+ target.closest('[siChatMessageAction]') ||
741
+ (target.closest('si-attachment-list') && target.closest('.attachment-item')) ||
742
+ this.projectedContent()?.nativeElement?.contains(target)) {
743
+ return;
744
+ }
745
+ this.focus();
746
+ }
747
+ ngAfterViewInit() {
748
+ const textarea = this.textInput();
749
+ if (textarea?.nativeElement) {
750
+ this.setTextareaHeight(textarea.nativeElement);
751
+ if (this.autoFocus()) {
752
+ // Use setTimeout to ensure the element is fully rendered
753
+ setTimeout(() => {
754
+ textarea.nativeElement.focus();
755
+ }, 0);
756
+ }
757
+ }
758
+ }
759
+ adjustTextareaHeight(event) {
760
+ const textarea = event.target;
761
+ this.setTextareaHeight(textarea);
762
+ }
763
+ /**
764
+ * Focus the textarea input
765
+ */
766
+ focus() {
767
+ const textarea = this.textInput();
768
+ if (textarea?.nativeElement) {
769
+ textarea.nativeElement.focus();
770
+ }
771
+ }
772
+ setTextareaHeight(textarea) {
773
+ textarea.style.blockSize = 'auto';
774
+ const computedStyle = window.getComputedStyle(textarea);
775
+ const lineHeight = parseInt(computedStyle.lineHeight, 10) || parseInt(computedStyle.fontSize, 10) * 1.2;
776
+ const paddingTop = parseInt(computedStyle.paddingBlockStart, 10) || 0;
777
+ const paddingBottom = parseInt(computedStyle.paddingBlockEnd, 10) || 0;
778
+ const minHeight = lineHeight + paddingTop + paddingBottom;
779
+ const viewportHeight = window.innerHeight;
780
+ const maxViewportHeight = viewportHeight * 0.3;
781
+ const maxLinesHeight = lineHeight * 8;
782
+ const maxHeight = Math.min(maxViewportHeight, maxLinesHeight) + paddingTop + paddingBottom;
783
+ const scrollHeight = textarea.scrollHeight;
784
+ const finalHeight = Math.max(Math.min(scrollHeight, maxHeight), minHeight);
785
+ textarea.style.height = finalHeight + 'px';
786
+ }
787
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiChatInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
788
+ 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"] }] });
789
+ }
790
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiChatInputComponent, decorators: [{
791
+ type: Component,
792
+ args: [{ selector: 'si-chat-input', imports: [
793
+ CdkMenuTrigger,
794
+ FormsModule,
795
+ SiIconComponent,
796
+ SiTranslatePipe,
797
+ SiAttachmentListComponent,
798
+ SiMenuFactoryComponent,
799
+ SiFileUploadDirective
800
+ ], 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"] }]
801
+ }] });
802
+
803
+ /**
804
+ * Copyright (c) Siemens 2016 - 2025
805
+ * SPDX-License-Identifier: MIT
806
+ */
807
+ /**
808
+ * Directive to mark content as chat input disclaimer into {@link SiChatInputComponent}.
809
+ * Apply this directive to content that should be slotted into the disclaimer area.
810
+ *
811
+ * @example
812
+ * ```html
813
+ * <si-chat-input>
814
+ * <div siChatInputDisclaimer>
815
+ * Custom disclaimer content
816
+ * </div>
817
+ * </si-chat-input>
818
+ * ```
819
+ *
820
+ * @see {@link SiChatInputComponent} for the chat input wrapper component
821
+ *
822
+ * @experimental
823
+ */
824
+ class SiChatInputDisclaimerDirective {
825
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiChatInputDisclaimerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
826
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.0.6", type: SiChatInputDisclaimerDirective, isStandalone: true, selector: "[siChatInputDisclaimer]", ngImport: i0 });
827
+ }
828
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiChatInputDisclaimerDirective, decorators: [{
829
+ type: Directive,
830
+ args: [{
831
+ selector: '[siChatInputDisclaimer]'
832
+ }]
833
+ }] });
834
+
835
+ /**
836
+ * Copyright (c) Siemens 2016 - 2025
837
+ * SPDX-License-Identifier: MIT
838
+ */
839
+ /**
840
+ * User message component for displaying the user's messages in conversational interfaces.
841
+ *
842
+ * The user message component renders user-submitted content in (AI) chat interfaces,
843
+ * supporting text, attachments, and contextual actions. It appears as a text bubble
844
+ * aligned to the right side and supports markdown formatting for rich content.
845
+ * Can be used within {@link SiChatContainerComponent}.
846
+ *
847
+ * The component automatically handles:
848
+ * - Styling for user messages distinct from AI or generic chat messages
849
+ * - Option to render markdown content, provide via `contentFormatter` input with a markdown renderer function (e.g., from {@link getMarkdownRenderer})
850
+ * - Displaying attachments above the message bubble
851
+ * - Displaying primary and secondary actions
852
+ *
853
+ * @see {@link SiChatMessageComponent} for the base message wrapper component
854
+ * @see {@link SiAiMessageComponent} for the AI message component
855
+ * @see {@link SiAttachmentListComponent} for the base attachment component
856
+ * @see {@link getMarkdownRenderer} for markdown formatting support
857
+ * @see {@link SiChatContainerComponent} for the chat container to use this within
858
+ *
859
+ * @experimental
860
+ */
861
+ class SiUserMessageComponent {
862
+ formattedContent = viewChild('formattedContent');
863
+ /**
864
+ * The user message content
865
+ * @defaultValue ''
866
+ */
867
+ content = input('');
868
+ /**
869
+ * Optional formatter function to transform content before display.
870
+ * - Returns string: Content will be inserted as text with built-in sanitization
871
+ * - Returns Node: DOM node will be inserted directly without sanitization
872
+ *
873
+ * **Note:** When returning a Node with formatted content, apply the `markdown-content` class
874
+ * to the root element to ensure proper styling (e.g., `div.className = 'markdown-content'`).
875
+ * The function returned by {@link getMarkdownRenderer} does this automatically.
876
+ *
877
+ * **Warning:** When returning a Node, ensure the content is safe to prevent XSS attacks
878
+ * @defaultValue undefined
879
+ */
880
+ contentFormatter = input(undefined);
881
+ /**
882
+ * Primary message actions (edit, delete, copy, etc.).
883
+ * All actions displayed inline
884
+ * @defaultValue []
885
+ */
886
+ actions = input([]);
887
+ /**
888
+ * Secondary actions available in dropdown menu, first use primary actions and only add secondary actions additionally
889
+ * @defaultValue []
890
+ */
891
+ secondaryActions = input([]);
892
+ /**
893
+ * List of attachments included with this message
894
+ * @defaultValue []
895
+ */
896
+ attachments = input([]);
897
+ /** Parameter to pass to action handlers */
898
+ actionParam = input();
899
+ /**
900
+ * More actions button aria label
901
+ *
902
+ * @defaultValue
903
+ * ```
904
+ * t(() => $localize`:@@SI_USER_MESSAGE.SECONDARY_ACTIONS:More actions`)
905
+ * ```
906
+ */
907
+ secondaryActionsLabel = input(t(() => $localize `:@@SI_USER_MESSAGE.SECONDARY_ACTIONS:More actions`));
908
+ hasAttachments = computed(() => this.attachments()?.length > 0);
909
+ textContent = signal(undefined);
910
+ constructor() {
911
+ effect(() => {
912
+ const formatter = this.contentFormatter();
913
+ const contentValue = this.content();
914
+ const container = this.formattedContent()?.nativeElement;
915
+ if (container && contentValue) {
916
+ if (formatter) {
917
+ const formatted = formatter(contentValue);
918
+ if (typeof formatted === 'string') {
919
+ this.textContent.set(formatted);
920
+ }
921
+ else if (formatted instanceof Node) {
922
+ this.textContent.set(undefined);
923
+ container.innerHTML = '';
924
+ container.appendChild(formatted);
925
+ }
926
+ }
927
+ else {
928
+ this.textContent.set(contentValue);
929
+ }
930
+ }
931
+ });
932
+ }
933
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiUserMessageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
934
+ 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 > 0) || (secondaryActions()?.length ?? 0 > 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 > 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" }] });
935
+ }
936
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiUserMessageComponent, decorators: [{
937
+ type: Component,
938
+ args: [{ selector: 'si-user-message', imports: [
939
+ CdkMenuTrigger,
940
+ SiAttachmentListComponent,
941
+ SiChatMessageComponent,
942
+ SiIconComponent,
943
+ SiMenuFactoryComponent,
944
+ SiChatMessageActionDirective,
945
+ SiTranslatePipe
946
+ ], 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 > 0) || (secondaryActions()?.length ?? 0 > 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 > 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"] }]
947
+ }], ctorParameters: () => [] });
948
+
949
+ /**
950
+ * Copyright (c) Siemens 2016 - 2025
951
+ * SPDX-License-Identifier: MIT
952
+ */
953
+
954
+ /**
955
+ * Generated bundle index. Do not edit.
956
+ */
957
+
958
+ export { SiAiMessageComponent, SiAttachmentListComponent, SiChatContainerComponent, SiChatContainerInputDirective, SiChatInputComponent, SiChatInputDisclaimerDirective, SiChatMessageActionDirective, SiChatMessageComponent, SiUserMessageComponent };
959
+ //# sourceMappingURL=siemens-element-ng-chat-messages.mjs.map