@siemens/element-ng 48.3.0 → 48.5.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.
- package/accordion/index.d.ts +0 -1
- package/chat-messages/index.d.ts +176 -156
- package/dashboard/index.d.ts +2 -3
- package/fesm2022/siemens-element-ng-accordion.mjs +2 -3
- package/fesm2022/siemens-element-ng-accordion.mjs.map +1 -1
- package/fesm2022/siemens-element-ng-chat-messages.mjs +291 -168
- package/fesm2022/siemens-element-ng-chat-messages.mjs.map +1 -1
- package/fesm2022/siemens-element-ng-dashboard.mjs +7 -11
- package/fesm2022/siemens-element-ng-dashboard.mjs.map +1 -1
- package/fesm2022/siemens-element-ng-datepicker.mjs +5 -3
- package/fesm2022/siemens-element-ng-datepicker.mjs.map +1 -1
- package/fesm2022/siemens-element-ng-filtered-search.mjs +4 -8
- package/fesm2022/siemens-element-ng-filtered-search.mjs.map +1 -1
- package/fesm2022/siemens-element-ng-loading-spinner.mjs +2 -2
- package/fesm2022/siemens-element-ng-loading-spinner.mjs.map +1 -1
- package/fesm2022/siemens-element-ng-navbar-vertical.mjs +21 -24
- package/fesm2022/siemens-element-ng-navbar-vertical.mjs.map +1 -1
- package/fesm2022/siemens-element-ng-select.mjs +4 -0
- package/fesm2022/siemens-element-ng-select.mjs.map +1 -1
- package/fesm2022/siemens-element-ng-side-panel.mjs +3 -5
- package/fesm2022/siemens-element-ng-side-panel.mjs.map +1 -1
- package/fesm2022/siemens-element-ng-status-bar.mjs +3 -4
- package/fesm2022/siemens-element-ng-status-bar.mjs.map +1 -1
- package/fesm2022/siemens-element-ng-tour.mjs +58 -21
- package/fesm2022/siemens-element-ng-tour.mjs.map +1 -1
- package/fesm2022/siemens-element-ng-translate.mjs.map +1 -1
- package/fesm2022/siemens-element-ng-tree-view.mjs +4 -4
- package/fesm2022/siemens-element-ng-tree-view.mjs.map +1 -1
- package/filtered-search/index.d.ts +6 -7
- package/navbar-vertical/index.d.ts +3 -4
- package/package.json +15 -15
- package/schematics/migrations/action-modal-migration/action-modal-migration.js +45 -4
- package/schematics/migrations/data/output-names.js +0 -1
- package/schematics/migrations/data/symbol-removals.js +0 -9
- package/schematics/migrations/element-migration/element-migration.js +1 -9
- package/schematics/migrations/wizard-migration/index.js +2 -10
- package/schematics/ts-import-to-siemens-migration/index.js +130 -25
- package/schematics/ts-import-to-siemens-migration/mappings/charts-ng-mappings.js +2 -1
- package/schematics/ts-import-to-siemens-migration/mappings/dashboards-ng-mappings.js +3 -1
- package/schematics/ts-import-to-siemens-migration/mappings/element-ng-mappings.js +54 -0
- package/schematics/ts-import-to-siemens-migration/mappings/element-translate-ng-mappings.js +1 -1
- package/schematics/ts-import-to-siemens-migration/mappings/index.js +1 -0
- package/schematics/ts-import-to-siemens-migration/mappings/maps-ng-mappings.js +2 -1
- package/schematics/ts-import-to-siemens-migration/mappings/native-charts-ng-mappings.js +33 -0
- package/schematics/utils/project-utils.js +1 -1
- package/schematics/utils/schematics-file-system.js +1 -1
- package/schematics/utils/template-utils.js +5 -5
- package/schematics/utils/ts-utils.js +1 -1
- package/select/index.d.ts +5 -0
- package/status-bar/index.d.ts +0 -1
- package/template-i18n.json +1 -0
- package/tour/index.d.ts +4 -2
- package/translate/index.d.ts +1 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { CdkMenuTrigger } from '@angular/cdk/menu';
|
|
2
2
|
import * as i0 from '@angular/core';
|
|
3
|
-
import { Directive, input, Component, viewChild, signal, effect, booleanAttribute, inject, output, model, computed } from '@angular/core';
|
|
3
|
+
import { Directive, input, Component, viewChild, signal, effect, booleanAttribute, inject, output, PLATFORM_ID, model, computed } from '@angular/core';
|
|
4
4
|
import { SiIconComponent } from '@siemens/element-ng/icon';
|
|
5
5
|
import { SiMenuFactoryComponent } from '@siemens/element-ng/menu';
|
|
6
6
|
import { t, SiTranslatePipe } from '@siemens/element-translate-ng/translate';
|
|
7
7
|
import * as i1 from '@siemens/element-ng/resize-observer';
|
|
8
8
|
import { SiResponsiveContainerDirective } from '@siemens/element-ng/resize-observer';
|
|
9
9
|
import { SiModalService } from '@siemens/element-ng/modal';
|
|
10
|
+
import { isPlatformBrowser } from '@angular/common';
|
|
10
11
|
import * as i1$1 from '@angular/forms';
|
|
11
12
|
import { FormsModule } from '@angular/forms';
|
|
12
13
|
import { SiFileUploadDirective } from '@siemens/element-ng/file-uploader';
|
|
@@ -16,10 +17,9 @@ import { SiFileUploadDirective } from '@siemens/element-ng/file-uploader';
|
|
|
16
17
|
* SPDX-License-Identifier: MIT
|
|
17
18
|
*/
|
|
18
19
|
/**
|
|
19
|
-
* Directive to mark content as chat message actions.
|
|
20
|
+
* Directive to mark content as chat message actions into {@link SiChatMessageComponent}.
|
|
20
21
|
* Apply this directive to e.g. buttons that should be slotted into the message actions area.
|
|
21
22
|
*
|
|
22
|
-
* @experimental
|
|
23
23
|
* @example
|
|
24
24
|
* ```html
|
|
25
25
|
* <si-chat-message>
|
|
@@ -28,6 +28,10 @@ import { SiFileUploadDirective } from '@siemens/element-ng/file-uploader';
|
|
|
28
28
|
* <button siChatMessageAction>Share</button>
|
|
29
29
|
* </si-chat-message>
|
|
30
30
|
* ```
|
|
31
|
+
*
|
|
32
|
+
* @see {@link SiChatMessageComponent} for the chat message wrapper component
|
|
33
|
+
*
|
|
34
|
+
* @experimental
|
|
31
35
|
*/
|
|
32
36
|
class SiChatMessageActionDirective {
|
|
33
37
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiChatMessageActionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
@@ -45,23 +49,33 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImpor
|
|
|
45
49
|
* SPDX-License-Identifier: MIT
|
|
46
50
|
*/
|
|
47
51
|
/**
|
|
48
|
-
* Base chat message component that provides the layout structure for
|
|
52
|
+
* Base declarative chat message component that provides the layout structure for chat messages.
|
|
49
53
|
*
|
|
50
54
|
* 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
|
|
55
|
+
* and action button as well as attachment list placement. It serves as the foundation for more specialized message components
|
|
52
56
|
* like {@link SiUserMessageComponent} and {@link SiAiMessageComponent}.
|
|
57
|
+
* Can be used within {@link SiChatContainerComponent}.
|
|
53
58
|
*
|
|
54
|
-
* @remarks
|
|
55
59
|
* The component provides:
|
|
56
60
|
* - Flexible alignment (start/end) for different message types
|
|
57
61
|
* - Avatar/icon slot for message attribution
|
|
58
62
|
* - Loading state with skeleton UI
|
|
59
63
|
* - Action buttons positioned on the side or bottom
|
|
64
|
+
* - Attachment list display slot
|
|
60
65
|
* - Responsive behavior that adapts to container size
|
|
61
|
-
* - Attachment display slot
|
|
62
66
|
*
|
|
63
|
-
* This is a low-level component
|
|
64
|
-
* message
|
|
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
|
|
65
79
|
*
|
|
66
80
|
* @experimental
|
|
67
81
|
*/
|
|
@@ -82,13 +96,13 @@ class SiChatMessageComponent {
|
|
|
82
96
|
*/
|
|
83
97
|
actionsPosition = input('side');
|
|
84
98
|
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}
|
|
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"] });
|
|
86
100
|
}
|
|
87
101
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiChatMessageComponent, decorators: [{
|
|
88
102
|
type: Component,
|
|
89
103
|
args: [{ selector: 'si-chat-message', host: {
|
|
90
104
|
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}
|
|
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"] }]
|
|
92
106
|
}] });
|
|
93
107
|
|
|
94
108
|
/**
|
|
@@ -101,63 +115,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImpor
|
|
|
101
115
|
* The AI message component renders AI-generated content in chat interfaces,
|
|
102
116
|
* supporting text formatting, markdown, loading states, and contextual actions.
|
|
103
117
|
* 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
|
|
118
|
+
* Can be used within {@link SiChatContainerComponent}.
|
|
111
119
|
*
|
|
112
120
|
* The component automatically handles:
|
|
113
|
-
* -
|
|
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})
|
|
114
123
|
* - Showing loading states with skeleton UI during generation
|
|
115
|
-
* - Displaying primary and secondary actions
|
|
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
|
-
* ```
|
|
124
|
+
* - Displaying primary and secondary actions
|
|
157
125
|
*
|
|
158
126
|
* @see {@link SiChatMessageComponent} for the base message wrapper component
|
|
159
|
-
* @see {@link SiUserMessageComponent} for the
|
|
127
|
+
* @see {@link SiUserMessageComponent} for the user message component
|
|
160
128
|
* @see {@link getMarkdownRenderer} for markdown formatting support
|
|
129
|
+
* @see {@link SiChatContainerComponent} for the chat container to use this within
|
|
161
130
|
*
|
|
162
131
|
* @experimental
|
|
163
132
|
*/
|
|
@@ -233,7 +202,7 @@ class SiAiMessageComponent {
|
|
|
233
202
|
*/
|
|
234
203
|
secondaryActionsLabel = input(t(() => $localize `:@@SI_AI_MESSAGE.SECONDARY_ACTIONS:More actions`));
|
|
235
204
|
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 } },
|
|
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" }] });
|
|
237
206
|
}
|
|
238
207
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiAiMessageComponent, decorators: [{
|
|
239
208
|
type: Component,
|
|
@@ -244,9 +213,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImpor
|
|
|
244
213
|
SiMenuFactoryComponent,
|
|
245
214
|
SiChatMessageActionDirective,
|
|
246
215
|
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"] }]
|
|
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"] }]
|
|
250
217
|
}], ctorParameters: () => [] });
|
|
251
218
|
|
|
252
219
|
/**
|
|
@@ -260,55 +227,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImpor
|
|
|
260
227
|
* preview and remove functionality. It's designed to work with chat message components
|
|
261
228
|
* to show files that have been uploaded or shared in conversations.
|
|
262
229
|
*
|
|
263
|
-
* @remarks
|
|
264
230
|
* This component provides:
|
|
265
|
-
* -
|
|
231
|
+
* - A list of pills showing each attachment's name and an icon
|
|
266
232
|
* - Optional preview modal for attachments
|
|
267
233
|
* - 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
234
|
*
|
|
280
|
-
* @
|
|
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
|
-
* ```
|
|
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}
|
|
309
236
|
*
|
|
310
237
|
* @see {@link SiUserMessageComponent} for user message display
|
|
311
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
|
|
312
241
|
* @see {@link Attachment} for attachment data structure
|
|
313
242
|
*
|
|
314
243
|
* @experimental
|
|
@@ -376,10 +305,214 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImpor
|
|
|
376
305
|
* Copyright (c) Siemens 2016 - 2025
|
|
377
306
|
* SPDX-License-Identifier: MIT
|
|
378
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
|
+
*/
|
|
379
511
|
class SiChatInputComponent {
|
|
380
512
|
static idCounter = 0;
|
|
381
513
|
textInput = viewChild('textInput');
|
|
382
514
|
projectedContent = viewChild('projected');
|
|
515
|
+
fileUploadDirective = viewChild(SiFileUploadDirective);
|
|
383
516
|
/**
|
|
384
517
|
* Current input value
|
|
385
518
|
* @defaultValue ''
|
|
@@ -546,6 +679,7 @@ class SiChatInputComponent {
|
|
|
546
679
|
});
|
|
547
680
|
buttonIcon = computed(() => this.showInterruptButton() ? 'element-stop-filled' : this.sendButtonIcon());
|
|
548
681
|
buttonLabel = computed(() => this.showInterruptButton() ? this.interruptButtonLabel() : this.sendButtonLabel());
|
|
682
|
+
dragOver = false;
|
|
549
683
|
get attachmentList() {
|
|
550
684
|
return this.attachments();
|
|
551
685
|
}
|
|
@@ -573,17 +707,22 @@ class SiChatInputComponent {
|
|
|
573
707
|
onKeyDown(event) {
|
|
574
708
|
if (event.key === 'Enter' && !event.shiftKey) {
|
|
575
709
|
event.preventDefault();
|
|
576
|
-
if (!this.
|
|
577
|
-
|
|
710
|
+
if (!this.canSend()) {
|
|
711
|
+
return;
|
|
578
712
|
}
|
|
713
|
+
if (this.showInterruptButton()) {
|
|
714
|
+
this.interrupt.emit();
|
|
715
|
+
}
|
|
716
|
+
this.onSend();
|
|
579
717
|
}
|
|
580
718
|
}
|
|
581
719
|
onFilesAdded(uploadFiles) {
|
|
582
720
|
const validFiles = uploadFiles.filter(uploadFile => uploadFile.status === 'added');
|
|
583
721
|
validFiles.forEach(uploadFile => {
|
|
722
|
+
const size = parseInt(uploadFile.size, 10);
|
|
584
723
|
const attachment = {
|
|
585
724
|
name: uploadFile.fileName,
|
|
586
|
-
size: uploadFile.file.size,
|
|
725
|
+
size: isNaN(size) ? uploadFile.file.size : size,
|
|
587
726
|
type: uploadFile.file.type,
|
|
588
727
|
file: uploadFile.file
|
|
589
728
|
};
|
|
@@ -637,6 +776,26 @@ class SiChatInputComponent {
|
|
|
637
776
|
textarea.nativeElement.focus();
|
|
638
777
|
}
|
|
639
778
|
}
|
|
779
|
+
dropHandler(event) {
|
|
780
|
+
event.preventDefault();
|
|
781
|
+
event.stopPropagation();
|
|
782
|
+
this.dragOver = false;
|
|
783
|
+
if (!this.allowAttachments() || this.disabled()) {
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
const directive = this.fileUploadDirective();
|
|
787
|
+
if (directive && event.dataTransfer?.files) {
|
|
788
|
+
directive.handleFiles(event.dataTransfer.files);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
dragOverHandler(event) {
|
|
792
|
+
if (!this.allowAttachments() || this.disabled()) {
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
event.preventDefault();
|
|
796
|
+
event.stopPropagation();
|
|
797
|
+
this.dragOver = true;
|
|
798
|
+
}
|
|
640
799
|
setTextareaHeight(textarea) {
|
|
641
800
|
textarea.style.blockSize = 'auto';
|
|
642
801
|
const computedStyle = window.getComputedStyle(textarea);
|
|
@@ -653,7 +812,7 @@ class SiChatInputComponent {
|
|
|
653
812
|
textarea.style.height = finalHeight + 'px';
|
|
654
813
|
}
|
|
655
814
|
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"] }] });
|
|
815
|
+
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 }, { propertyName: "fileUploadDirective", first: true, predicate: SiFileUploadDirective, descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"input-wrapper border rounded-3 bg-body\"\n [class.drag-over]=\"dragOver\"\n (click)=\"onContainerClick($event)\"\n (drop)=\"dropHandler($event)\"\n (dragover)=\"dragOverHandler($event)\"\n (dragleave)=\"dragOver = false\"\n>\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)}.input-wrapper.drag-over{border:1px solid var(--element-focus-default);box-shadow:0 0 0 1px var(--element-focus-default)}.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
816
|
}
|
|
658
817
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiChatInputComponent, decorators: [{
|
|
659
818
|
type: Component,
|
|
@@ -665,7 +824,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImpor
|
|
|
665
824
|
SiAttachmentListComponent,
|
|
666
825
|
SiMenuFactoryComponent,
|
|
667
826
|
SiFileUploadDirective
|
|
668
|
-
], template: "<div
|
|
827
|
+
], template: "<div\n class=\"input-wrapper border rounded-3 bg-body\"\n [class.drag-over]=\"dragOver\"\n (click)=\"onContainerClick($event)\"\n (drop)=\"dropHandler($event)\"\n (dragover)=\"dragOverHandler($event)\"\n (dragleave)=\"dragOver = false\"\n>\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)}.input-wrapper.drag-over{border:1px solid var(--element-focus-default);box-shadow:0 0 0 1px var(--element-focus-default)}.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
828
|
}] });
|
|
670
829
|
|
|
671
830
|
/**
|
|
@@ -673,7 +832,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImpor
|
|
|
673
832
|
* SPDX-License-Identifier: MIT
|
|
674
833
|
*/
|
|
675
834
|
/**
|
|
676
|
-
* Directive to mark content as chat input disclaimer.
|
|
835
|
+
* Directive to mark content as chat input disclaimer into {@link SiChatInputComponent}.
|
|
677
836
|
* Apply this directive to content that should be slotted into the disclaimer area.
|
|
678
837
|
*
|
|
679
838
|
* @example
|
|
@@ -684,6 +843,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImpor
|
|
|
684
843
|
* </div>
|
|
685
844
|
* </si-chat-input>
|
|
686
845
|
* ```
|
|
846
|
+
*
|
|
847
|
+
* @see {@link SiChatInputComponent} for the chat input wrapper component
|
|
848
|
+
*
|
|
849
|
+
* @experimental
|
|
687
850
|
*/
|
|
688
851
|
class SiChatInputDisclaimerDirective {
|
|
689
852
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiChatInputDisclaimerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
@@ -701,64 +864,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImpor
|
|
|
701
864
|
* SPDX-License-Identifier: MIT
|
|
702
865
|
*/
|
|
703
866
|
/**
|
|
704
|
-
* User message component for displaying user
|
|
867
|
+
* User message component for displaying the user's messages in conversational interfaces.
|
|
705
868
|
*
|
|
706
|
-
* The user message component renders user-submitted content in chat interfaces,
|
|
869
|
+
* The user message component renders user-submitted content in (AI) chat interfaces,
|
|
707
870
|
* supporting text, attachments, and contextual actions. It appears as a text bubble
|
|
708
871
|
* 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
|
|
872
|
+
* Can be used within {@link SiChatContainerComponent}.
|
|
715
873
|
*
|
|
716
874
|
* The component automatically handles:
|
|
717
|
-
* -
|
|
875
|
+
* - Styling for user messages distinct from AI or generic chat messages
|
|
876
|
+
* - Option to render markdown content, provide via `contentFormatter` input with a markdown renderer function (e.g., from {@link getMarkdownRenderer})
|
|
718
877
|
* - Displaying attachments above the message bubble
|
|
719
|
-
* -
|
|
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
|
-
* ```
|
|
878
|
+
* - Displaying primary and secondary actions
|
|
758
879
|
*
|
|
759
880
|
* @see {@link SiChatMessageComponent} for the base message wrapper component
|
|
760
|
-
* @see {@link
|
|
761
|
-
* @see {@link
|
|
881
|
+
* @see {@link SiAiMessageComponent} for the AI message component
|
|
882
|
+
* @see {@link SiAttachmentListComponent} for the base attachment component
|
|
883
|
+
* @see {@link getMarkdownRenderer} for markdown formatting support
|
|
884
|
+
* @see {@link SiChatContainerComponent} for the chat container to use this within
|
|
762
885
|
*
|
|
763
886
|
* @experimental
|
|
764
887
|
*/
|
|
@@ -809,7 +932,7 @@ class SiUserMessageComponent {
|
|
|
809
932
|
* ```
|
|
810
933
|
*/
|
|
811
934
|
secondaryActionsLabel = input(t(() => $localize `:@@SI_USER_MESSAGE.SECONDARY_ACTIONS:More actions`));
|
|
812
|
-
hasAttachments = computed(() => this.attachments()
|
|
935
|
+
hasAttachments = computed(() => this.attachments()?.length > 0);
|
|
813
936
|
textContent = signal(undefined);
|
|
814
937
|
constructor() {
|
|
815
938
|
effect(() => {
|
|
@@ -835,7 +958,7 @@ class SiUserMessageComponent {
|
|
|
835
958
|
});
|
|
836
959
|
}
|
|
837
960
|
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()
|
|
961
|
+
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" }] });
|
|
839
962
|
}
|
|
840
963
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: SiUserMessageComponent, decorators: [{
|
|
841
964
|
type: Component,
|
|
@@ -847,7 +970,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImpor
|
|
|
847
970
|
SiMenuFactoryComponent,
|
|
848
971
|
SiChatMessageActionDirective,
|
|
849
972
|
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()
|
|
973
|
+
], 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"] }]
|
|
851
974
|
}], ctorParameters: () => [] });
|
|
852
975
|
|
|
853
976
|
/**
|
|
@@ -859,5 +982,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImpor
|
|
|
859
982
|
* Generated bundle index. Do not edit.
|
|
860
983
|
*/
|
|
861
984
|
|
|
862
|
-
export { SiAiMessageComponent, SiAttachmentListComponent, SiChatInputComponent, SiChatInputDisclaimerDirective, SiChatMessageActionDirective, SiChatMessageComponent, SiUserMessageComponent };
|
|
985
|
+
export { SiAiMessageComponent, SiAttachmentListComponent, SiChatContainerComponent, SiChatContainerInputDirective, SiChatInputComponent, SiChatInputDisclaimerDirective, SiChatMessageActionDirective, SiChatMessageComponent, SiUserMessageComponent };
|
|
863
986
|
//# sourceMappingURL=siemens-element-ng-chat-messages.mjs.map
|