@c8y/ngx-components 1024.15.16 → 1024.16.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/fesm2022/c8y-ngx-components-ai-agent-chat.mjs +303 -63
  2. package/fesm2022/c8y-ngx-components-ai-agent-chat.mjs.map +1 -1
  3. package/fesm2022/c8y-ngx-components-ai-agents-html.mjs +9 -0
  4. package/fesm2022/c8y-ngx-components-ai-agents-html.mjs.map +1 -1
  5. package/fesm2022/c8y-ngx-components-ai-agents-scada.mjs +4 -0
  6. package/fesm2022/c8y-ngx-components-ai-agents-scada.mjs.map +1 -1
  7. package/fesm2022/c8y-ngx-components-ai-ai-chat.mjs +394 -52
  8. package/fesm2022/c8y-ngx-components-ai-ai-chat.mjs.map +1 -1
  9. package/fesm2022/c8y-ngx-components-ai.mjs +329 -19
  10. package/fesm2022/c8y-ngx-components-ai.mjs.map +1 -1
  11. package/fesm2022/c8y-ngx-components-bookmarks.mjs +1 -1
  12. package/fesm2022/c8y-ngx-components-bookmarks.mjs.map +1 -1
  13. package/fesm2022/c8y-ngx-components-context-dashboard.mjs +5 -1
  14. package/fesm2022/c8y-ngx-components-context-dashboard.mjs.map +1 -1
  15. package/fesm2022/c8y-ngx-components-datapoint-explorer-view.mjs +96 -77
  16. package/fesm2022/c8y-ngx-components-datapoint-explorer-view.mjs.map +1 -1
  17. package/fesm2022/c8y-ngx-components-datapoint-selector.mjs +92 -47
  18. package/fesm2022/c8y-ngx-components-datapoint-selector.mjs.map +1 -1
  19. package/fesm2022/c8y-ngx-components-device-profile.mjs +1 -1
  20. package/fesm2022/c8y-ngx-components-device-profile.mjs.map +1 -1
  21. package/fesm2022/c8y-ngx-components-echart.mjs +25 -4
  22. package/fesm2022/c8y-ngx-components-echart.mjs.map +1 -1
  23. package/fesm2022/c8y-ngx-components-ecosystem.mjs +1 -1
  24. package/fesm2022/c8y-ngx-components-ecosystem.mjs.map +1 -1
  25. package/fesm2022/c8y-ngx-components-protocol-opcua.mjs +1 -1
  26. package/fesm2022/c8y-ngx-components-protocol-opcua.mjs.map +1 -1
  27. package/fesm2022/c8y-ngx-components-register-device.mjs +5 -6
  28. package/fesm2022/c8y-ngx-components-register-device.mjs.map +1 -1
  29. package/fesm2022/c8y-ngx-components-repository-firmware.mjs +1 -1
  30. package/fesm2022/c8y-ngx-components-repository-firmware.mjs.map +1 -1
  31. package/fesm2022/c8y-ngx-components-repository-software.mjs +1 -1
  32. package/fesm2022/c8y-ngx-components-repository-software.mjs.map +1 -1
  33. package/fesm2022/c8y-ngx-components-sub-assets.mjs +30 -10
  34. package/fesm2022/c8y-ngx-components-sub-assets.mjs.map +1 -1
  35. package/fesm2022/c8y-ngx-components-time-context.mjs +13 -9
  36. package/fesm2022/c8y-ngx-components-time-context.mjs.map +1 -1
  37. package/fesm2022/c8y-ngx-components-trusted-certificates.mjs +1 -1
  38. package/fesm2022/c8y-ngx-components-trusted-certificates.mjs.map +1 -1
  39. package/fesm2022/c8y-ngx-components-widgets-definitions-html-widget-ai-config.mjs +3 -2
  40. package/fesm2022/c8y-ngx-components-widgets-definitions-html-widget-ai-config.mjs.map +1 -1
  41. package/fesm2022/c8y-ngx-components-widgets-implementations-asset-table.mjs +4 -6
  42. package/fesm2022/c8y-ngx-components-widgets-implementations-asset-table.mjs.map +1 -1
  43. package/fesm2022/c8y-ngx-components-widgets-implementations-scada.mjs +3 -2
  44. package/fesm2022/c8y-ngx-components-widgets-implementations-scada.mjs.map +1 -1
  45. package/fesm2022/c8y-ngx-components.mjs +122 -24
  46. package/fesm2022/c8y-ngx-components.mjs.map +1 -1
  47. package/locales/de.po +147 -16
  48. package/locales/es.po +143 -11
  49. package/locales/fr.po +459 -324
  50. package/locales/ja_JP.po +143 -11
  51. package/locales/ko.po +143 -11
  52. package/locales/locales.pot +136 -10
  53. package/locales/nl.po +143 -11
  54. package/locales/pl.po +143 -11
  55. package/locales/pt_BR.po +143 -11
  56. package/locales/zh_CN.po +143 -11
  57. package/locales/zh_TW.po +143 -11
  58. package/package.json +1 -1
  59. package/types/c8y-ngx-components-ai-agent-chat.d.ts +111 -4
  60. package/types/c8y-ngx-components-ai-agent-chat.d.ts.map +1 -1
  61. package/types/c8y-ngx-components-ai-agents-html.d.ts.map +1 -1
  62. package/types/c8y-ngx-components-ai-agents-scada.d.ts.map +1 -1
  63. package/types/c8y-ngx-components-ai-ai-chat.d.ts +224 -10
  64. package/types/c8y-ngx-components-ai-ai-chat.d.ts.map +1 -1
  65. package/types/c8y-ngx-components-ai.d.ts +176 -6
  66. package/types/c8y-ngx-components-ai.d.ts.map +1 -1
  67. package/types/c8y-ngx-components-context-dashboard.d.ts.map +1 -1
  68. package/types/c8y-ngx-components-datapoint-explorer-view.d.ts +7 -2
  69. package/types/c8y-ngx-components-datapoint-explorer-view.d.ts.map +1 -1
  70. package/types/c8y-ngx-components-datapoint-selector.d.ts +63 -50
  71. package/types/c8y-ngx-components-datapoint-selector.d.ts.map +1 -1
  72. package/types/c8y-ngx-components-echart.d.ts +12 -0
  73. package/types/c8y-ngx-components-echart.d.ts.map +1 -1
  74. package/types/c8y-ngx-components-register-device.d.ts.map +1 -1
  75. package/types/c8y-ngx-components-sub-assets.d.ts +10 -1
  76. package/types/c8y-ngx-components-sub-assets.d.ts.map +1 -1
  77. package/types/c8y-ngx-components-time-context.d.ts +2 -0
  78. package/types/c8y-ngx-components-time-context.d.ts.map +1 -1
  79. package/types/c8y-ngx-components-widgets-implementations-scada.d.ts.map +1 -1
  80. package/types/c8y-ngx-components.d.ts +37 -5
  81. package/types/c8y-ngx-components.d.ts.map +1 -1
@@ -1,16 +1,53 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, signal, inject, ChangeDetectionStrategy, Component, Pipe, computed, effect, EventEmitter, Output, Input, output, contentChildren, viewChild } from '@angular/core';
3
- import { IconDirective, C8yTranslateModule, C8yTranslatePipe, C8yTranslateDirective, MarkdownToHtmlPipe, DatePipe, TextareaAutoresizeDirective, NumberPipe } from '@c8y/ngx-components';
2
+ import { input, viewChild, TemplateRef, ChangeDetectionStrategy, Component, output, signal, inject, effect, Pipe, computed, EventEmitter, Output, Input, NgZone, Injectable, DestroyRef, model, contentChildren } from '@angular/core';
3
+ import { IconDirective, C8yTranslateModule, DropdownFocusTrapDirective, C8yTranslatePipe, C8yTranslateDirective, MarkdownToHtmlPipe, DatePipe, AlertService, AppStateService, TextareaAutoresizeDirective, NumberPipe } from '@c8y/ngx-components';
4
4
  import { gettext } from '@c8y/ngx-components/gettext';
5
5
  import { TranslateService } from '@ngx-translate/core';
6
6
  import * as i1 from 'ngx-bootstrap/collapse';
7
7
  import { CollapseModule } from 'ngx-bootstrap/collapse';
8
8
  import { NgComponentOutlet, JsonPipe, AsyncPipe, NgClass, NgTemplateOutlet } from '@angular/common';
9
+ import * as i2 from 'ngx-bootstrap/dropdown';
10
+ import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
9
11
  import * as i1$1 from 'ngx-bootstrap/tooltip';
10
12
  import { TooltipModule } from 'ngx-bootstrap/tooltip';
13
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
14
+ import { Observable } from 'rxjs';
15
+ import { CdkTrapFocus } from '@angular/cdk/a11y';
11
16
  import * as i1$2 from '@angular/forms';
12
17
  import { FormsModule } from '@angular/forms';
13
18
 
19
+ /**
20
+ * Projects arbitrary content into the action bar beneath the `c8y-ai-chat` input box.
21
+ *
22
+ * Place one or more `<c8y-ai-chat-action>` elements inside a `<c8y-ai-chat>` (or `<c8y-agent-chat>`)
23
+ * and use the `placement` input to position them. The projected content is rendered as-is, so it can
24
+ * hold any component or control (buttons, dropdowns, tags, and so on).
25
+ *
26
+ * @example
27
+ * ```html
28
+ * <c8y-ai-chat-action placement="left">
29
+ * <button class="btn btn-link btn-sm"><i c8yIcon="attach"></i></button>
30
+ * </c8y-ai-chat-action>
31
+ * ```
32
+ */
33
+ class AiChatActionComponent {
34
+ constructor() {
35
+ /**
36
+ * Where the action is placed in the action bar. `more` collects the action into an overflow
37
+ * dropdown on the right side.
38
+ */
39
+ this.placement = input('left', ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
40
+ /** The projected content, exposed as a template so the host chat can render it in the right group. */
41
+ this.template = viewChild.required(TemplateRef);
42
+ }
43
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: AiChatActionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
44
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.19", type: AiChatActionComponent, isStandalone: true, selector: "c8y-ai-chat-action", inputs: { placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "d-contents" }, viewQueries: [{ propertyName: "template", first: true, predicate: TemplateRef, descendants: true, isSignal: true }], ngImport: i0, template: "<ng-template>\n <ng-content></ng-content>\n</ng-template>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
45
+ }
46
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: AiChatActionComponent, decorators: [{
47
+ type: Component,
48
+ args: [{ selector: 'c8y-ai-chat-action', changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'd-contents' }, template: "<ng-template>\n <ng-content></ng-content>\n</ng-template>\n" }]
49
+ }], propDecorators: { placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], template: [{ type: i0.ViewChild, args: [i0.forwardRef(() => TemplateRef), { isSignal: true }] }] } });
50
+
14
51
  class AiChatToolCallComponent {
15
52
  constructor() {
16
53
  /**
@@ -39,26 +76,60 @@ class AiChatToolCallComponent {
39
76
  */
40
77
  this.completedLabel = input(undefined, ...(ngDevMode ? [{ debugName: "completedLabel" }] : /* istanbul ignore next */ []));
41
78
  /**
42
- * A function that can generate a label for this tool call based on the tool call data.
79
+ * A function that can generate a label for this tool call based on the tool call data,
80
+ * including for the `tool-approval-requested` state - e.g. "Delete device weather-station-12"
81
+ * instead of the generic "Tool delete_device requires approval". When this returns nothing for
82
+ * a pending approval, the details section is auto-expanded so the raw input is visible instead.
43
83
  */
44
84
  this.labelProvider = input(...(ngDevMode ? [undefined, { debugName: "labelProvider" }] : /* istanbul ignore next */ []));
85
+ /**
86
+ * Whether the approve/deny actions are offered when the tool is in the `tool-approval-requested` state.
87
+ * Only enabled for the latest message while no response is streaming.
88
+ */
89
+ this.approvalEnabled = input(false, ...(ngDevMode ? [{ debugName: "approvalEnabled" }] : /* istanbul ignore next */ []));
90
+ /**
91
+ * Whether the "always approve" options are offered next to the approve action. Disabled for
92
+ * tools that must be approved explicitly on every call (configured via `needsApproval`),
93
+ * which show plain approve/deny buttons without the dropdown.
94
+ */
95
+ this.rememberApprovalEnabled = input(true, ...(ngDevMode ? [{ debugName: "rememberApprovalEnabled" }] : /* istanbul ignore next */ []));
96
+ /** Emits the user's decision for a tool call awaiting approval. */
97
+ this.approvalResponse = output();
45
98
  this.expanded = signal(false, ...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
46
99
  this.everExpanded = signal(false, ...(ngDevMode ? [{ debugName: "everExpanded" }] : /* istanbul ignore next */ []));
47
100
  this.translateService = inject(TranslateService);
101
+ /** Guards the auto-expand-on-approval effect so it only ever applies its initial decision once. */
102
+ this.hasSetInitialApprovalExpansion = false;
103
+ // A tool call awaiting approval with no dynamic label (see `getDynamicLabel`) has nothing but
104
+ // generic text in its header, so auto-expand the details once so the user can inspect the raw
105
+ // input before deciding. Tools with a dynamic label already convey what's about to happen, so
106
+ // details stay collapsed as usual.
107
+ effect(() => {
108
+ const tool = this.tool();
109
+ if (this.hasSetInitialApprovalExpansion || tool.type !== 'tool-approval-requested')
110
+ return;
111
+ this.hasSetInitialApprovalExpansion = true;
112
+ if (!this.getDynamicLabel(tool)) {
113
+ this.expanded.set(true);
114
+ this.everExpanded.set(true);
115
+ }
116
+ });
48
117
  }
49
118
  getToolLabel(tool) {
50
- const isExecuting = tool.type !== 'tool-result';
51
- if (this.labelProvider()) {
52
- try {
53
- const dynamicLabel = this.labelProvider()(tool, this.translateService);
54
- if (dynamicLabel)
55
- return dynamicLabel;
56
- }
57
- catch (err) {
58
- // Should never happen, report it then fall back to default logic
59
- console.error(`Error in labelProvider for tool ${tool.toolName}:`, err);
60
- }
119
+ const dynamicLabel = this.getDynamicLabel(tool);
120
+ if (dynamicLabel)
121
+ return dynamicLabel;
122
+ if (tool.type === 'tool-approval-requested') {
123
+ return this.translateService.instant(gettext('Tool {{toolName}} requires approval'), {
124
+ toolName: tool.toolName
125
+ });
126
+ }
127
+ if (tool.type === 'tool-approval-responded') {
128
+ return this.translateService.instant(tool.approval?.approved
129
+ ? gettext('Approved tool {{toolName}}')
130
+ : gettext('Denied tool {{toolName}}'), { toolName: tool.toolName });
61
131
  }
132
+ const isExecuting = tool.type !== 'tool-result';
62
133
  if (isExecuting && this.executingLabel()) {
63
134
  return this.executingLabel();
64
135
  }
@@ -67,6 +138,15 @@ class AiChatToolCallComponent {
67
138
  }
68
139
  return this.translateService.instant(isExecuting ? gettext('Calling tool {{toolName}}') : gettext('Used tool {{toolName}}'), { toolName: tool.toolName });
69
140
  }
141
+ /**
142
+ * The fieldset's accessible name (and, for a pending approval, also its visible legend - see the
143
+ * template). Both must stay in sync so sighted and screen-reader users see/hear the same thing.
144
+ */
145
+ getFieldsetLabel(tool) {
146
+ return this.translateService.instant(tool.type === 'tool-approval-requested'
147
+ ? gettext('Tool request: {{toolName}}')
148
+ : gettext('Tool call: {{toolName}}'), { toolName: tool.toolName });
149
+ }
70
150
  toggleExpanded() {
71
151
  const willExpand = !this.expanded();
72
152
  this.expanded.set(willExpand);
@@ -74,8 +154,21 @@ class AiChatToolCallComponent {
74
154
  this.everExpanded.set(true);
75
155
  }
76
156
  }
157
+ getDynamicLabel(tool) {
158
+ const labelProvider = this.labelProvider();
159
+ if (!labelProvider)
160
+ return undefined;
161
+ try {
162
+ return labelProvider(tool, this.translateService);
163
+ }
164
+ catch (err) {
165
+ // Should never happen, report it then fall back to default logic
166
+ console.error(`Error in labelProvider for tool ${tool.toolName}:`, err);
167
+ return undefined;
168
+ }
169
+ }
77
170
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: AiChatToolCallComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
78
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: AiChatToolCallComponent, isStandalone: true, selector: "c8y-ai-chat-tool-call", inputs: { tool: { classPropertyName: "tool", publicName: "tool", isSignal: true, isRequired: true, transformFunction: null }, isExecuting: { classPropertyName: "isExecuting", publicName: "isExecuting", isSignal: true, isRequired: true, transformFunction: null }, toolDetailsComponent: { classPropertyName: "toolDetailsComponent", publicName: "toolDetailsComponent", isSignal: true, isRequired: false, transformFunction: null }, showDefaultToolDetails: { classPropertyName: "showDefaultToolDetails", publicName: "showDefaultToolDetails", isSignal: true, isRequired: false, transformFunction: null }, executingLabel: { classPropertyName: "executingLabel", publicName: "executingLabel", isSignal: true, isRequired: false, transformFunction: null }, completedLabel: { classPropertyName: "completedLabel", publicName: "completedLabel", isSignal: true, isRequired: false, transformFunction: null }, labelProvider: { classPropertyName: "labelProvider", publicName: "labelProvider", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@let _tool = tool();\n@let showDetails = showDefaultToolDetails() || toolDetailsComponent() || _tool.error;\n@let isToolExpanded = expanded();\n\n<fieldset\n class=\"c8y-fieldset p-b-4 ai-tool-call__fieldset\"\n [attr.aria-label]=\"'Tool call: ' + _tool.toolName\"\n>\n <button\n class=\"btn-clean ai-tool-call__btn\"\n [attr.aria-expanded]=\"showDetails ? isToolExpanded : null\"\n [attr.aria-controls]=\"showDetails ? 'tool-call-' + _tool.toolCallId : null\"\n type=\"button\"\n [attr.data-cy]=\"'tool-call-' + _tool.toolName\"\n [disabled]=\"!showDetails || null\"\n (click)=\"toggleExpanded()\"\n >\n @if (isExecuting()) {\n <i\n class=\"icon-spin icon-14 text-primary m-r-4\"\n c8yIcon=\"spinner\"\n aria-hidden=\"true\"\n ></i>\n } @else {\n <!-- Treat it as an error if a tool still thinks its executing when the message containing it is not -->\n @if (_tool.error || isExecuting()) {\n <i\n class=\"icon-14 text-danger m-r-4\"\n [c8yIcon]=\"'exclamation-circle'\"\n aria-hidden=\"true\"\n ></i>\n } @else {\n <i\n class=\"icon-14 text-success m-r-4\"\n [c8yIcon]=\"'check'\"\n aria-hidden=\"true\"\n ></i>\n }\n }\n <span class=\"small\">{{ getToolLabel(_tool) | translate }}</span>\n\n @if (showDetails) {\n <i\n class=\"m-l-4 icon-12\"\n [c8yIcon]=\"isToolExpanded ? 'collapse-arrow' : 'expand-arrow'\"\n aria-hidden=\"true\"\n ></i>\n }\n </button>\n\n <!-- If this is an artifact tool, render it - but do so lazily only on first open,\n since these components could be heavyweight (e.g. Monaco)\n -->\n @if (showDetails && (isToolExpanded || everExpanded())) {\n <div\n class=\"collapse tool-details m-t-8 p-8\"\n [attr.aria-label]=\"'Tool details for message latest tool: ' + _tool.toolName\"\n role=\"region\"\n [collapse]=\"!isToolExpanded\"\n [id]=\"'tool-call-' + _tool.toolCallId\"\n [isAnimated]=\"true\"\n data-cy=\"ai-tool-component\"\n >\n @if (toolDetailsComponent()) {\n <ng-container\n [ngComponentOutlet]=\"toolDetailsComponent()\"\n [ngComponentOutletInputs]=\"{ tool: _tool }\"\n ></ng-container>\n } @else {\n @let noneLabel = '(no data)' | translate;\n\n <p class=\"text-label-small\">{{ 'Tool input' | translate }}</p>\n <pre class=\"fit-w small\">{{ _tool.input || noneLabel | json }}</pre>\n\n @if (_tool.type === 'tool-result') {\n <p class=\"text-label-small\">{{ 'Tool output' | translate }}</p>\n @if (\n typeof _tool.output === 'string' || _tool.output === undefined || _tool.output === null\n ) {\n <pre class=\"fit-w small\">{{ _tool.output || noneLabel }}</pre>\n } @else {\n <pre class=\"fit-w small\">{{ _tool.output | json }}</pre>\n }\n }\n }\n </div>\n }\n</fieldset>\n", dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "ngmodule", type: CollapseModule }, { kind: "directive", type: i1.CollapseDirective, selector: "[collapse]", inputs: ["display", "isAnimated", "collapse"], outputs: ["collapsed", "collapses", "expanded", "expands"], exportAs: ["bs-collapse"] }, { kind: "ngmodule", type: C8yTranslateModule }, { kind: "pipe", type: JsonPipe, name: "json" }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
171
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: AiChatToolCallComponent, isStandalone: true, selector: "c8y-ai-chat-tool-call", inputs: { tool: { classPropertyName: "tool", publicName: "tool", isSignal: true, isRequired: true, transformFunction: null }, isExecuting: { classPropertyName: "isExecuting", publicName: "isExecuting", isSignal: true, isRequired: true, transformFunction: null }, toolDetailsComponent: { classPropertyName: "toolDetailsComponent", publicName: "toolDetailsComponent", isSignal: true, isRequired: false, transformFunction: null }, showDefaultToolDetails: { classPropertyName: "showDefaultToolDetails", publicName: "showDefaultToolDetails", isSignal: true, isRequired: false, transformFunction: null }, executingLabel: { classPropertyName: "executingLabel", publicName: "executingLabel", isSignal: true, isRequired: false, transformFunction: null }, completedLabel: { classPropertyName: "completedLabel", publicName: "completedLabel", isSignal: true, isRequired: false, transformFunction: null }, labelProvider: { classPropertyName: "labelProvider", publicName: "labelProvider", isSignal: true, isRequired: false, transformFunction: null }, approvalEnabled: { classPropertyName: "approvalEnabled", publicName: "approvalEnabled", isSignal: true, isRequired: false, transformFunction: null }, rememberApprovalEnabled: { classPropertyName: "rememberApprovalEnabled", publicName: "rememberApprovalEnabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { approvalResponse: "approvalResponse" }, ngImport: i0, template: "@let _tool = tool();\n@let isApprovalRequested = _tool.type === 'tool-approval-requested';\n@let showDetails =\n showDefaultToolDetails() || toolDetailsComponent() || _tool.error || isApprovalRequested;\n@let isToolExpanded = expanded();\n\n<fieldset\n class=\"c8y-fieldset p-b-4 ai-tool-call__fieldset\"\n [class.fit-w]=\"isApprovalRequested\"\n [attr.aria-label]=\"getFieldsetLabel(_tool)\"\n>\n @if (isApprovalRequested) {\n <legend>\n <i\n class=\"icon-16 text-warning m-r-4\"\n [c8yIcon]=\"'exclamation-triangle'\"\n aria-hidden=\"true\"\n ></i>\n {{ getFieldsetLabel(_tool) }}\n </legend>\n }\n\n <button\n class=\"btn-clean ai-tool-call__btn\"\n [attr.aria-expanded]=\"showDetails ? isToolExpanded : null\"\n [attr.aria-controls]=\"showDetails ? 'tool-call-' + _tool.toolCallId : null\"\n type=\"button\"\n [attr.data-cy]=\"'tool-call-' + _tool.toolName\"\n [disabled]=\"!showDetails || null\"\n (click)=\"toggleExpanded()\"\n >\n @if (_tool.type === 'tool-approval-responded') {\n <i\n class=\"icon-16 m-r-4\"\n [class.text-success]=\"_tool.approval?.approved\"\n [class.text-muted]=\"!_tool.approval?.approved\"\n [c8yIcon]=\"_tool.approval?.approved ? 'check' : 'ban'\"\n aria-hidden=\"true\"\n ></i>\n } @else if (isExecuting()) {\n <i\n class=\"icon-spin icon-16 text-primary m-r-4\"\n c8yIcon=\"spinner\"\n aria-hidden=\"true\"\n ></i>\n } @else {\n <!-- Treat it as an error if a tool still thinks its executing when the message containing it is not -->\n @if (_tool.error || isExecuting()) {\n <i\n class=\"icon-16 text-danger m-r-4\"\n [c8yIcon]=\"'exclamation-circle'\"\n aria-hidden=\"true\"\n ></i>\n } @else if (!isApprovalRequested) {\n <i\n class=\"icon-16 text-success m-r-4\"\n [c8yIcon]=\"'check'\"\n aria-hidden=\"true\"\n ></i>\n }\n }\n @if (showDetails) {\n <i\n class=\"m-r-4\"\n [c8yIcon]=\"isToolExpanded ? 'expand-arrow' : 'forward'\"\n aria-hidden=\"true\"\n ></i>\n }\n <span\n [class.small]=\"!isApprovalRequested\"\n [class.flex-grow]=\"isApprovalRequested\"\n >\n {{ getToolLabel(_tool) | translate }}\n </span>\n </button>\n <!-- If this is an artifact tool, render it - but do so lazily only on first open,\n since these components could be heavyweight (e.g. Monaco)\n -->\n @if (showDetails && (isToolExpanded || everExpanded())) {\n <div\n class=\"collapse tool-details m-t-8 p-8\"\n [attr.aria-label]=\"'Tool details for message latest tool: ' + _tool.toolName\"\n role=\"region\"\n [collapse]=\"!isToolExpanded\"\n [id]=\"'tool-call-' + _tool.toolCallId\"\n [isAnimated]=\"true\"\n data-cy=\"ai-tool-component\"\n >\n @if (toolDetailsComponent()) {\n <ng-container\n [ngComponentOutlet]=\"toolDetailsComponent()\"\n [ngComponentOutletInputs]=\"{ tool: _tool }\"\n ></ng-container>\n } @else {\n @let noneLabel = '(no data)' | translate;\n <pre\n class=\"fit-w small text-pre-wrap text-break-word\"\n [attr.data-label]=\"'Tool input' | translate\"\n >{{ _tool.input || noneLabel | json }}</pre\n >\n\n @if (_tool.type === 'tool-result') {\n @if (\n typeof _tool.output === 'string' || _tool.output === undefined || _tool.output === null\n ) {\n <pre\n class=\"fit-w small text-pre-wrap text-break-word\"\n [attr.data-label]=\"'Tool output' | translate\"\n >{{ _tool.output || noneLabel }}</pre\n >\n } @else {\n <pre class=\"fit-w small text-pre-wrap text-break-word\">{{ _tool.output | json }}</pre>\n }\n }\n }\n </div>\n }\n\n @if (isApprovalRequested && approvalEnabled()) {\n <div\n class=\"btn-toolbar m-l-0 m-t-8 m-b-8\"\n [attr.aria-label]=\"'Tool approval actions' | translate\"\n role=\"toolbar\"\n >\n <button\n class=\"btn btn-danger btn-sm\"\n type=\"button\"\n [attr.data-cy]=\"'tool-deny-' + _tool.toolName\"\n (click)=\"approvalResponse.emit({ approved: false })\"\n >\n <i\n class=\"m-r-4\"\n [c8yIcon]=\"'block'\"\n aria-hidden=\"true\"\n ></i>\n {{ 'Deny' | translate }}\n </button>\n @if (!rememberApprovalEnabled()) {\n <!-- Tools requiring explicit approval on every call get no \"always approve\" options -->\n <button\n class=\"btn btn-default btn-sm\"\n type=\"button\"\n [attr.data-cy]=\"'tool-approve-' + _tool.toolName\"\n (click)=\"approvalResponse.emit({ approved: true })\"\n >\n {{ 'Approve' | translate }}\n </button>\n } @else {\n <div\n class=\"btn-group\"\n role=\"group\"\n >\n <button\n class=\"btn btn-default btn-sm\"\n type=\"button\"\n [attr.data-cy]=\"'tool-approve-' + _tool.toolName\"\n (click)=\"approvalResponse.emit({ approved: true })\"\n >\n {{ 'Approve' | translate }}\n </button>\n <div\n class=\"btn-group\"\n dropdown\n [container]=\"'body'\"\n c8yDropdownFocusTrap\n #approveDropdown=\"bs-dropdown\"\n >\n <button\n class=\"btn btn-default btn-sm dropdown-toggle\"\n aria-label=\"{{ 'More approval options' | translate }}\"\n aria-haspopup=\"true\"\n [attr.aria-expanded]=\"approveDropdown.isOpen\"\n type=\"button\"\n dropdownToggle\n [attr.data-cy]=\"'tool-approve-options-' + _tool.toolName\"\n >\n <i\n c8yIcon=\"caret-down\"\n aria-hidden=\"true\"\n ></i>\n </button>\n <ul\n class=\"dropdown-menu\"\n role=\"menu\"\n *dropdownMenu\n >\n <li>\n <button\n class=\"a-i-start\"\n title=\"{{ 'Always approve this tool' | translate }}\"\n type=\"button\"\n [attr.data-cy]=\"'tool-approve-always-' + _tool.toolName\"\n (click)=\"approvalResponse.emit({ approved: true, remember: 'tool' })\"\n >\n <i\n class=\"icon-16\"\n c8yIcon=\"ok\"\n aria-hidden=\"true\"\n ></i>\n <span>\n {{ 'Always approve this tool' | translate }}\n <small class=\"d-block text-muted\">\n {{ 'For this conversation only' | translate }}\n </small>\n </span>\n </button>\n </li>\n <li>\n <button\n class=\"a-i-start\"\n title=\"{{ 'Always approve all tools' | translate }}\"\n type=\"button\"\n [attr.data-cy]=\"'tool-approve-always-all-' + _tool.toolName\"\n (click)=\"approvalResponse.emit({ approved: true, remember: 'all' })\"\n >\n <i\n class=\"icon-16\"\n c8yIcon=\"approval\"\n aria-hidden=\"true\"\n ></i>\n <span>\n {{ 'Always approve all tools' | translate }}\n <small class=\"d-block text-muted\">\n {{\n 'For this conversation only, unlike the global auto-approve mode'\n | translate\n }}\n </small>\n </span>\n </button>\n </li>\n </ul>\n </div>\n </div>\n }\n </div>\n }\n</fieldset>\n", dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "ngmodule", type: CollapseModule }, { kind: "directive", type: i1.CollapseDirective, selector: "[collapse]", inputs: ["display", "isAnimated", "collapse"], outputs: ["collapsed", "collapses", "expanded", "expands"], exportAs: ["bs-collapse"] }, { kind: "ngmodule", type: C8yTranslateModule }, { kind: "ngmodule", type: BsDropdownModule }, { kind: "directive", type: i2.BsDropdownMenuDirective, selector: "[bsDropdownMenu],[dropdownMenu]", exportAs: ["bs-dropdown-menu"] }, { kind: "directive", type: i2.BsDropdownToggleDirective, selector: "[bsDropdownToggle],[dropdownToggle]", exportAs: ["bs-dropdown-toggle"] }, { kind: "directive", type: i2.BsDropdownDirective, selector: "[bsDropdown], [dropdown]", inputs: ["placement", "triggers", "container", "dropup", "autoClose", "isAnimated", "insideClick", "isDisabled", "isOpen"], outputs: ["isOpenChange", "onShown", "onHidden"], exportAs: ["bs-dropdown"] }, { kind: "directive", type: DropdownFocusTrapDirective, selector: "[c8yDropdownFocusTrap]" }, { kind: "pipe", type: JsonPipe, name: "json" }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
79
172
  }
80
173
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: AiChatToolCallComponent, decorators: [{
81
174
  type: Component,
@@ -85,9 +178,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
85
178
  C8yTranslatePipe,
86
179
  IconDirective,
87
180
  CollapseModule,
88
- C8yTranslateModule
89
- ], template: "@let _tool = tool();\n@let showDetails = showDefaultToolDetails() || toolDetailsComponent() || _tool.error;\n@let isToolExpanded = expanded();\n\n<fieldset\n class=\"c8y-fieldset p-b-4 ai-tool-call__fieldset\"\n [attr.aria-label]=\"'Tool call: ' + _tool.toolName\"\n>\n <button\n class=\"btn-clean ai-tool-call__btn\"\n [attr.aria-expanded]=\"showDetails ? isToolExpanded : null\"\n [attr.aria-controls]=\"showDetails ? 'tool-call-' + _tool.toolCallId : null\"\n type=\"button\"\n [attr.data-cy]=\"'tool-call-' + _tool.toolName\"\n [disabled]=\"!showDetails || null\"\n (click)=\"toggleExpanded()\"\n >\n @if (isExecuting()) {\n <i\n class=\"icon-spin icon-14 text-primary m-r-4\"\n c8yIcon=\"spinner\"\n aria-hidden=\"true\"\n ></i>\n } @else {\n <!-- Treat it as an error if a tool still thinks its executing when the message containing it is not -->\n @if (_tool.error || isExecuting()) {\n <i\n class=\"icon-14 text-danger m-r-4\"\n [c8yIcon]=\"'exclamation-circle'\"\n aria-hidden=\"true\"\n ></i>\n } @else {\n <i\n class=\"icon-14 text-success m-r-4\"\n [c8yIcon]=\"'check'\"\n aria-hidden=\"true\"\n ></i>\n }\n }\n <span class=\"small\">{{ getToolLabel(_tool) | translate }}</span>\n\n @if (showDetails) {\n <i\n class=\"m-l-4 icon-12\"\n [c8yIcon]=\"isToolExpanded ? 'collapse-arrow' : 'expand-arrow'\"\n aria-hidden=\"true\"\n ></i>\n }\n </button>\n\n <!-- If this is an artifact tool, render it - but do so lazily only on first open,\n since these components could be heavyweight (e.g. Monaco)\n -->\n @if (showDetails && (isToolExpanded || everExpanded())) {\n <div\n class=\"collapse tool-details m-t-8 p-8\"\n [attr.aria-label]=\"'Tool details for message latest tool: ' + _tool.toolName\"\n role=\"region\"\n [collapse]=\"!isToolExpanded\"\n [id]=\"'tool-call-' + _tool.toolCallId\"\n [isAnimated]=\"true\"\n data-cy=\"ai-tool-component\"\n >\n @if (toolDetailsComponent()) {\n <ng-container\n [ngComponentOutlet]=\"toolDetailsComponent()\"\n [ngComponentOutletInputs]=\"{ tool: _tool }\"\n ></ng-container>\n } @else {\n @let noneLabel = '(no data)' | translate;\n\n <p class=\"text-label-small\">{{ 'Tool input' | translate }}</p>\n <pre class=\"fit-w small\">{{ _tool.input || noneLabel | json }}</pre>\n\n @if (_tool.type === 'tool-result') {\n <p class=\"text-label-small\">{{ 'Tool output' | translate }}</p>\n @if (\n typeof _tool.output === 'string' || _tool.output === undefined || _tool.output === null\n ) {\n <pre class=\"fit-w small\">{{ _tool.output || noneLabel }}</pre>\n } @else {\n <pre class=\"fit-w small\">{{ _tool.output | json }}</pre>\n }\n }\n }\n </div>\n }\n</fieldset>\n" }]
90
- }], propDecorators: { tool: [{ type: i0.Input, args: [{ isSignal: true, alias: "tool", required: true }] }], isExecuting: [{ type: i0.Input, args: [{ isSignal: true, alias: "isExecuting", required: true }] }], toolDetailsComponent: [{ type: i0.Input, args: [{ isSignal: true, alias: "toolDetailsComponent", required: false }] }], showDefaultToolDetails: [{ type: i0.Input, args: [{ isSignal: true, alias: "showDefaultToolDetails", required: false }] }], executingLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "executingLabel", required: false }] }], completedLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "completedLabel", required: false }] }], labelProvider: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelProvider", required: false }] }] } });
181
+ C8yTranslateModule,
182
+ BsDropdownModule,
183
+ DropdownFocusTrapDirective
184
+ ], template: "@let _tool = tool();\n@let isApprovalRequested = _tool.type === 'tool-approval-requested';\n@let showDetails =\n showDefaultToolDetails() || toolDetailsComponent() || _tool.error || isApprovalRequested;\n@let isToolExpanded = expanded();\n\n<fieldset\n class=\"c8y-fieldset p-b-4 ai-tool-call__fieldset\"\n [class.fit-w]=\"isApprovalRequested\"\n [attr.aria-label]=\"getFieldsetLabel(_tool)\"\n>\n @if (isApprovalRequested) {\n <legend>\n <i\n class=\"icon-16 text-warning m-r-4\"\n [c8yIcon]=\"'exclamation-triangle'\"\n aria-hidden=\"true\"\n ></i>\n {{ getFieldsetLabel(_tool) }}\n </legend>\n }\n\n <button\n class=\"btn-clean ai-tool-call__btn\"\n [attr.aria-expanded]=\"showDetails ? isToolExpanded : null\"\n [attr.aria-controls]=\"showDetails ? 'tool-call-' + _tool.toolCallId : null\"\n type=\"button\"\n [attr.data-cy]=\"'tool-call-' + _tool.toolName\"\n [disabled]=\"!showDetails || null\"\n (click)=\"toggleExpanded()\"\n >\n @if (_tool.type === 'tool-approval-responded') {\n <i\n class=\"icon-16 m-r-4\"\n [class.text-success]=\"_tool.approval?.approved\"\n [class.text-muted]=\"!_tool.approval?.approved\"\n [c8yIcon]=\"_tool.approval?.approved ? 'check' : 'ban'\"\n aria-hidden=\"true\"\n ></i>\n } @else if (isExecuting()) {\n <i\n class=\"icon-spin icon-16 text-primary m-r-4\"\n c8yIcon=\"spinner\"\n aria-hidden=\"true\"\n ></i>\n } @else {\n <!-- Treat it as an error if a tool still thinks its executing when the message containing it is not -->\n @if (_tool.error || isExecuting()) {\n <i\n class=\"icon-16 text-danger m-r-4\"\n [c8yIcon]=\"'exclamation-circle'\"\n aria-hidden=\"true\"\n ></i>\n } @else if (!isApprovalRequested) {\n <i\n class=\"icon-16 text-success m-r-4\"\n [c8yIcon]=\"'check'\"\n aria-hidden=\"true\"\n ></i>\n }\n }\n @if (showDetails) {\n <i\n class=\"m-r-4\"\n [c8yIcon]=\"isToolExpanded ? 'expand-arrow' : 'forward'\"\n aria-hidden=\"true\"\n ></i>\n }\n <span\n [class.small]=\"!isApprovalRequested\"\n [class.flex-grow]=\"isApprovalRequested\"\n >\n {{ getToolLabel(_tool) | translate }}\n </span>\n </button>\n <!-- If this is an artifact tool, render it - but do so lazily only on first open,\n since these components could be heavyweight (e.g. Monaco)\n -->\n @if (showDetails && (isToolExpanded || everExpanded())) {\n <div\n class=\"collapse tool-details m-t-8 p-8\"\n [attr.aria-label]=\"'Tool details for message latest tool: ' + _tool.toolName\"\n role=\"region\"\n [collapse]=\"!isToolExpanded\"\n [id]=\"'tool-call-' + _tool.toolCallId\"\n [isAnimated]=\"true\"\n data-cy=\"ai-tool-component\"\n >\n @if (toolDetailsComponent()) {\n <ng-container\n [ngComponentOutlet]=\"toolDetailsComponent()\"\n [ngComponentOutletInputs]=\"{ tool: _tool }\"\n ></ng-container>\n } @else {\n @let noneLabel = '(no data)' | translate;\n <pre\n class=\"fit-w small text-pre-wrap text-break-word\"\n [attr.data-label]=\"'Tool input' | translate\"\n >{{ _tool.input || noneLabel | json }}</pre\n >\n\n @if (_tool.type === 'tool-result') {\n @if (\n typeof _tool.output === 'string' || _tool.output === undefined || _tool.output === null\n ) {\n <pre\n class=\"fit-w small text-pre-wrap text-break-word\"\n [attr.data-label]=\"'Tool output' | translate\"\n >{{ _tool.output || noneLabel }}</pre\n >\n } @else {\n <pre class=\"fit-w small text-pre-wrap text-break-word\">{{ _tool.output | json }}</pre>\n }\n }\n }\n </div>\n }\n\n @if (isApprovalRequested && approvalEnabled()) {\n <div\n class=\"btn-toolbar m-l-0 m-t-8 m-b-8\"\n [attr.aria-label]=\"'Tool approval actions' | translate\"\n role=\"toolbar\"\n >\n <button\n class=\"btn btn-danger btn-sm\"\n type=\"button\"\n [attr.data-cy]=\"'tool-deny-' + _tool.toolName\"\n (click)=\"approvalResponse.emit({ approved: false })\"\n >\n <i\n class=\"m-r-4\"\n [c8yIcon]=\"'block'\"\n aria-hidden=\"true\"\n ></i>\n {{ 'Deny' | translate }}\n </button>\n @if (!rememberApprovalEnabled()) {\n <!-- Tools requiring explicit approval on every call get no \"always approve\" options -->\n <button\n class=\"btn btn-default btn-sm\"\n type=\"button\"\n [attr.data-cy]=\"'tool-approve-' + _tool.toolName\"\n (click)=\"approvalResponse.emit({ approved: true })\"\n >\n {{ 'Approve' | translate }}\n </button>\n } @else {\n <div\n class=\"btn-group\"\n role=\"group\"\n >\n <button\n class=\"btn btn-default btn-sm\"\n type=\"button\"\n [attr.data-cy]=\"'tool-approve-' + _tool.toolName\"\n (click)=\"approvalResponse.emit({ approved: true })\"\n >\n {{ 'Approve' | translate }}\n </button>\n <div\n class=\"btn-group\"\n dropdown\n [container]=\"'body'\"\n c8yDropdownFocusTrap\n #approveDropdown=\"bs-dropdown\"\n >\n <button\n class=\"btn btn-default btn-sm dropdown-toggle\"\n aria-label=\"{{ 'More approval options' | translate }}\"\n aria-haspopup=\"true\"\n [attr.aria-expanded]=\"approveDropdown.isOpen\"\n type=\"button\"\n dropdownToggle\n [attr.data-cy]=\"'tool-approve-options-' + _tool.toolName\"\n >\n <i\n c8yIcon=\"caret-down\"\n aria-hidden=\"true\"\n ></i>\n </button>\n <ul\n class=\"dropdown-menu\"\n role=\"menu\"\n *dropdownMenu\n >\n <li>\n <button\n class=\"a-i-start\"\n title=\"{{ 'Always approve this tool' | translate }}\"\n type=\"button\"\n [attr.data-cy]=\"'tool-approve-always-' + _tool.toolName\"\n (click)=\"approvalResponse.emit({ approved: true, remember: 'tool' })\"\n >\n <i\n class=\"icon-16\"\n c8yIcon=\"ok\"\n aria-hidden=\"true\"\n ></i>\n <span>\n {{ 'Always approve this tool' | translate }}\n <small class=\"d-block text-muted\">\n {{ 'For this conversation only' | translate }}\n </small>\n </span>\n </button>\n </li>\n <li>\n <button\n class=\"a-i-start\"\n title=\"{{ 'Always approve all tools' | translate }}\"\n type=\"button\"\n [attr.data-cy]=\"'tool-approve-always-all-' + _tool.toolName\"\n (click)=\"approvalResponse.emit({ approved: true, remember: 'all' })\"\n >\n <i\n class=\"icon-16\"\n c8yIcon=\"approval\"\n aria-hidden=\"true\"\n ></i>\n <span>\n {{ 'Always approve all tools' | translate }}\n <small class=\"d-block text-muted\">\n {{\n 'For this conversation only, unlike the global auto-approve mode'\n | translate\n }}\n </small>\n </span>\n </button>\n </li>\n </ul>\n </div>\n </div>\n }\n </div>\n }\n</fieldset>\n" }]
185
+ }], ctorParameters: () => [], propDecorators: { tool: [{ type: i0.Input, args: [{ isSignal: true, alias: "tool", required: true }] }], isExecuting: [{ type: i0.Input, args: [{ isSignal: true, alias: "isExecuting", required: true }] }], toolDetailsComponent: [{ type: i0.Input, args: [{ isSignal: true, alias: "toolDetailsComponent", required: false }] }], showDefaultToolDetails: [{ type: i0.Input, args: [{ isSignal: true, alias: "showDefaultToolDetails", required: false }] }], executingLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "executingLabel", required: false }] }], completedLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "completedLabel", required: false }] }], labelProvider: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelProvider", required: false }] }], approvalEnabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "approvalEnabled", required: false }] }], rememberApprovalEnabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "rememberApprovalEnabled", required: false }] }], approvalResponse: [{ type: i0.Output, args: ["approvalResponse"] }] } });
91
186
 
92
187
  /**
93
188
  * Converts a JSON string to a markdown code block.
@@ -129,7 +224,7 @@ class AiChatAssistantPartComponent {
129
224
  this.expanded.update(v => !v);
130
225
  }
131
226
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: AiChatAssistantPartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
132
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: AiChatAssistantPartComponent, isStandalone: true, selector: "c8y-ai-chat-assistant-part", inputs: { part: { classPropertyName: "part", publicName: "part", isSignal: true, isRequired: true, transformFunction: null }, displayAsPartOfMainAnswer: { classPropertyName: "displayAsPartOfMainAnswer", publicName: "displayAsPartOfMainAnswer", isSignal: true, isRequired: false, transformFunction: null }, assistantMessageContext: { classPropertyName: "assistantMessageContext", publicName: "assistantMessageContext", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "@let ctx = assistantMessageContext();\n@let _config = ctx.config;\n@let toolCallConfig = _config.toolCallConfig || {};\n@let _part = part();\n\n@if (_part.type === 'text') {\n <p\n [class.text-muted]=\"!displayAsPartOfMainAnswer()\"\n [innerHTML]=\"_part.text | markdownToHtml | async\"\n ></p>\n} @else if (_part.type === 'reasoning') {\n @let reasoningExpanded = expanded();\n\n <fieldset\n class=\"c8y-fieldset p-b-4 ai-tool-call__fieldset\"\n data-cy=\"ai-reasoning\"\n >\n <button\n class=\"btn-clean ai-tool-call__btn\"\n aria-label=\"{{ 'Toggle display of the reasoning section' | translate }}\"\n [attr.aria-expanded]=\"reasoningExpanded\"\n [attr.aria-controls]=\"'reasoning-content-message' + ctx.messageDisplayIndex\"\n type=\"button\"\n (click)=\"toggleExpanded()\"\n >\n <i\n class=\"m-r-4\"\n [c8yIcon]=\"'ai-sparkles'\"\n ></i>\n @let hideReasoning = 'Hide reasoning' | translate;\n @let showReasoning = 'Show reasoning' | translate;\n <span class=\"small\">{{ reasoningExpanded ? hideReasoning : showReasoning }}</span>\n <i\n class=\"m-l-4 icon-12 text-muted\"\n [c8yIcon]=\"reasoningExpanded ? 'collapse-arrow' : 'expand-arrow'\"\n ></i>\n </button>\n\n @let ariaLabel =\n 'Reasoning for message number {{ number }}' | translate: { number: ctx.messageDisplayIndex };\n <div\n class=\"collapse reasoning-content\"\n [attr.aria-label]=\"ariaLabel\"\n role=\"region\"\n [collapse]=\"!reasoningExpanded\"\n [id]=\"'reasoning-content-message' + ctx.messageDisplayIndex\"\n [isAnimated]=\"true\"\n data-cy=\"ai-reasoning-content\"\n >\n <div\n class=\"m-t-8 p-b-4 text-muted\"\n [innerHTML]=\"_part.text | markdownToHtml | async\"\n ></div>\n </div>\n </fieldset>\n} @else if (_part.type === 'object') {\n <p\n [class.text-muted]=\"!displayAsPartOfMainAnswer()\"\n [innerHTML]=\"_part.jsonContent | jsonToMarkdown | markdownToHtml | async\"\n data-cy=\"ai-object-content\"\n ></p>\n} @else if (_part.type === 'step-start') {\n <!-- Visually hidden label included when users copy-paste from the chat, e.g. to report issues -->\n <span\n class=\"ai-step-start hidden-copy-label\"\n aria-hidden=\"true\"\n data-cy=\"ai-step-start-separator\"\n translate\n >--- (step separator) ---</span\n >\n} @else if (_part.type.startsWith('tool')) {\n @let tool = $any(_part);\n @let toolDisplay = toolCallConfig[tool.toolName];\n\n <!-- Visually hidden label included when users copy-paste from the chat, e.g. to report issues -->\n <span\n class=\"hidden-copy-label\"\n aria-hidden=\"true\"\n translate\n [translateParams]=\"{ toolName: tool.toolName }\"\n ngNonBindable\n >--- Tool call: {{ toolName }} ---</span\n >\n\n @if (toolDisplay?.component) {\n <div data-cy=\"ai-tool-with-custom-component\">\n <ng-container\n [ngComponentOutlet]=\"toolDisplay.component\"\n [ngComponentOutletInputs]=\"{\n tool: tool,\n ctx: ctx\n }\"\n ></ng-container>\n </div>\n } @else if (!toolDisplay?.isHidden) {\n <!-- Don't allow expanding while tool call is in progress - it's unlikely to have anything worth rendering,\n and complicates implementation for the client to have to check for the isExecuting case.\n -->\n <c8y-ai-chat-tool-call\n [tool]=\"tool\"\n [isExecuting]=\"tool.type !== 'tool-result' && ctx.isMessageLoading\"\n [toolDetailsComponent]=\"_config.toolDetailsComponent?.(tool)\"\n [showDefaultToolDetails]=\"_config.showDefaultToolDetails === 'all'\"\n [executingLabel]=\"toolDisplay?.executingLabel\"\n [completedLabel]=\"toolDisplay?.completedLabel\"\n [labelProvider]=\"toolDisplay?.labelProvider\"\n ></c8y-ai-chat-tool-call>\n }\n}\n", dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "directive", type: C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "ngmodule", type: CollapseModule }, { kind: "directive", type: i1.CollapseDirective, selector: "[collapse]", inputs: ["display", "isAnimated", "collapse"], outputs: ["collapsed", "collapses", "expanded", "expands"], exportAs: ["bs-collapse"] }, { kind: "component", type: AiChatToolCallComponent, selector: "c8y-ai-chat-tool-call", inputs: ["tool", "isExecuting", "toolDetailsComponent", "showDefaultToolDetails", "executingLabel", "completedLabel", "labelProvider"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: MarkdownToHtmlPipe, name: "markdownToHtml" }, { kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "pipe", type: JsonToMarkdownPipe, name: "jsonToMarkdown" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
227
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: AiChatAssistantPartComponent, isStandalone: true, selector: "c8y-ai-chat-assistant-part", inputs: { part: { classPropertyName: "part", publicName: "part", isSignal: true, isRequired: true, transformFunction: null }, displayAsPartOfMainAnswer: { classPropertyName: "displayAsPartOfMainAnswer", publicName: "displayAsPartOfMainAnswer", isSignal: true, isRequired: false, transformFunction: null }, assistantMessageContext: { classPropertyName: "assistantMessageContext", publicName: "assistantMessageContext", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "@let ctx = assistantMessageContext();\n@let _config = ctx.config;\n@let toolCallConfig = _config.toolCallConfig || {};\n@let _part = part();\n\n@if (_part.type === 'text') {\n <p\n [class.text-muted]=\"!displayAsPartOfMainAnswer()\"\n [innerHTML]=\"_part.text | markdownToHtml | async\"\n ></p>\n} @else if (_part.type === 'reasoning') {\n @let reasoningExpanded = expanded();\n\n <fieldset\n class=\"c8y-fieldset p-b-4 ai-tool-call__fieldset\"\n data-cy=\"ai-reasoning\"\n >\n <button\n class=\"btn-clean ai-tool-call__btn\"\n aria-label=\"{{ 'Toggle display of the reasoning section' | translate }}\"\n [attr.aria-expanded]=\"reasoningExpanded\"\n [attr.aria-controls]=\"'reasoning-content-message' + ctx.messageDisplayIndex\"\n type=\"button\"\n (click)=\"toggleExpanded()\"\n >\n <i\n class=\"m-r-4\"\n [c8yIcon]=\"'ai-sparkles'\"\n ></i>\n @let hideReasoning = 'Hide reasoning' | translate;\n @let showReasoning = 'Show reasoning' | translate;\n <span class=\"small\">{{ reasoningExpanded ? hideReasoning : showReasoning }}</span>\n <i\n class=\"m-l-4 icon-12 text-muted\"\n [c8yIcon]=\"reasoningExpanded ? 'collapse-arrow' : 'expand-arrow'\"\n ></i>\n </button>\n\n @let ariaLabel =\n 'Reasoning for message number {{ number }}' | translate: { number: ctx.messageDisplayIndex };\n <div\n class=\"collapse reasoning-content\"\n [attr.aria-label]=\"ariaLabel\"\n role=\"region\"\n [collapse]=\"!reasoningExpanded\"\n [id]=\"'reasoning-content-message' + ctx.messageDisplayIndex\"\n [isAnimated]=\"true\"\n data-cy=\"ai-reasoning-content\"\n >\n <div\n class=\"m-t-8 p-b-4 text-muted\"\n [innerHTML]=\"_part.text | markdownToHtml | async\"\n ></div>\n </div>\n </fieldset>\n} @else if (_part.type === 'object') {\n <p\n [class.text-muted]=\"!displayAsPartOfMainAnswer()\"\n [innerHTML]=\"_part.jsonContent | jsonToMarkdown | markdownToHtml | async\"\n data-cy=\"ai-object-content\"\n ></p>\n} @else if (_part.type === 'step-start') {\n <!-- Visually hidden label included when users copy-paste from the chat, e.g. to report issues -->\n <span\n class=\"ai-step-start hidden-copy-label\"\n aria-hidden=\"true\"\n data-cy=\"ai-step-start-separator\"\n translate\n >--- (step separator) ---</span\n >\n} @else if (_part.type.startsWith('tool')) {\n @let tool = $any(_part);\n @let toolDisplay = toolCallConfig[tool.toolName];\n\n <!-- Visually hidden label included when users copy-paste from the chat, e.g. to report issues -->\n <span\n class=\"hidden-copy-label\"\n aria-hidden=\"true\"\n translate\n [translateParams]=\"{ toolName: tool.toolName }\"\n ngNonBindable\n >--- Tool call: {{ toolName }} ---</span\n >\n\n @if (toolDisplay?.component) {\n <div data-cy=\"ai-tool-with-custom-component\">\n <ng-container\n [ngComponentOutlet]=\"toolDisplay.component\"\n [ngComponentOutletInputs]=\"{\n tool: tool,\n ctx: ctx\n }\"\n ></ng-container>\n </div>\n } @else if (!toolDisplay?.isHidden) {\n <!-- Don't allow expanding while tool call is in progress - it's unlikely to have anything worth rendering,\n and complicates implementation for the client to have to check for the isExecuting case.\n -->\n <c8y-ai-chat-tool-call\n [tool]=\"tool\"\n [isExecuting]=\"\n tool.type !== 'tool-result' &&\n !tool.type.startsWith('tool-approval') &&\n ctx.isMessageLoading\n \"\n [toolDetailsComponent]=\"_config.toolDetailsComponent?.(tool)\"\n [showDefaultToolDetails]=\"_config.showDefaultToolDetails === 'all'\"\n [executingLabel]=\"toolDisplay?.executingLabel\"\n [completedLabel]=\"toolDisplay?.completedLabel\"\n [labelProvider]=\"toolDisplay?.labelProvider\"\n [approvalEnabled]=\"\n ctx.messageDisplayIndex === 0 && !ctx.isMessageLoading && !!ctx.onToolApprovalResponse\n \"\n [rememberApprovalEnabled]=\"!ctx.canRememberToolApproval || ctx.canRememberToolApproval(tool)\"\n (approvalResponse)=\"ctx.onToolApprovalResponse?.(tool, $event)\"\n ></c8y-ai-chat-tool-call>\n }\n}\n", dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "directive", type: C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "ngmodule", type: CollapseModule }, { kind: "directive", type: i1.CollapseDirective, selector: "[collapse]", inputs: ["display", "isAnimated", "collapse"], outputs: ["collapsed", "collapses", "expanded", "expands"], exportAs: ["bs-collapse"] }, { kind: "component", type: AiChatToolCallComponent, selector: "c8y-ai-chat-tool-call", inputs: ["tool", "isExecuting", "toolDetailsComponent", "showDefaultToolDetails", "executingLabel", "completedLabel", "labelProvider", "approvalEnabled", "rememberApprovalEnabled"], outputs: ["approvalResponse"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: MarkdownToHtmlPipe, name: "markdownToHtml" }, { kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "pipe", type: JsonToMarkdownPipe, name: "jsonToMarkdown" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
133
228
  }
134
229
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: AiChatAssistantPartComponent, decorators: [{
135
230
  type: Component,
@@ -143,7 +238,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
143
238
  AsyncPipe,
144
239
  AiChatToolCallComponent,
145
240
  JsonToMarkdownPipe
146
- ], changeDetection: ChangeDetectionStrategy.OnPush, template: "@let ctx = assistantMessageContext();\n@let _config = ctx.config;\n@let toolCallConfig = _config.toolCallConfig || {};\n@let _part = part();\n\n@if (_part.type === 'text') {\n <p\n [class.text-muted]=\"!displayAsPartOfMainAnswer()\"\n [innerHTML]=\"_part.text | markdownToHtml | async\"\n ></p>\n} @else if (_part.type === 'reasoning') {\n @let reasoningExpanded = expanded();\n\n <fieldset\n class=\"c8y-fieldset p-b-4 ai-tool-call__fieldset\"\n data-cy=\"ai-reasoning\"\n >\n <button\n class=\"btn-clean ai-tool-call__btn\"\n aria-label=\"{{ 'Toggle display of the reasoning section' | translate }}\"\n [attr.aria-expanded]=\"reasoningExpanded\"\n [attr.aria-controls]=\"'reasoning-content-message' + ctx.messageDisplayIndex\"\n type=\"button\"\n (click)=\"toggleExpanded()\"\n >\n <i\n class=\"m-r-4\"\n [c8yIcon]=\"'ai-sparkles'\"\n ></i>\n @let hideReasoning = 'Hide reasoning' | translate;\n @let showReasoning = 'Show reasoning' | translate;\n <span class=\"small\">{{ reasoningExpanded ? hideReasoning : showReasoning }}</span>\n <i\n class=\"m-l-4 icon-12 text-muted\"\n [c8yIcon]=\"reasoningExpanded ? 'collapse-arrow' : 'expand-arrow'\"\n ></i>\n </button>\n\n @let ariaLabel =\n 'Reasoning for message number {{ number }}' | translate: { number: ctx.messageDisplayIndex };\n <div\n class=\"collapse reasoning-content\"\n [attr.aria-label]=\"ariaLabel\"\n role=\"region\"\n [collapse]=\"!reasoningExpanded\"\n [id]=\"'reasoning-content-message' + ctx.messageDisplayIndex\"\n [isAnimated]=\"true\"\n data-cy=\"ai-reasoning-content\"\n >\n <div\n class=\"m-t-8 p-b-4 text-muted\"\n [innerHTML]=\"_part.text | markdownToHtml | async\"\n ></div>\n </div>\n </fieldset>\n} @else if (_part.type === 'object') {\n <p\n [class.text-muted]=\"!displayAsPartOfMainAnswer()\"\n [innerHTML]=\"_part.jsonContent | jsonToMarkdown | markdownToHtml | async\"\n data-cy=\"ai-object-content\"\n ></p>\n} @else if (_part.type === 'step-start') {\n <!-- Visually hidden label included when users copy-paste from the chat, e.g. to report issues -->\n <span\n class=\"ai-step-start hidden-copy-label\"\n aria-hidden=\"true\"\n data-cy=\"ai-step-start-separator\"\n translate\n >--- (step separator) ---</span\n >\n} @else if (_part.type.startsWith('tool')) {\n @let tool = $any(_part);\n @let toolDisplay = toolCallConfig[tool.toolName];\n\n <!-- Visually hidden label included when users copy-paste from the chat, e.g. to report issues -->\n <span\n class=\"hidden-copy-label\"\n aria-hidden=\"true\"\n translate\n [translateParams]=\"{ toolName: tool.toolName }\"\n ngNonBindable\n >--- Tool call: {{ toolName }} ---</span\n >\n\n @if (toolDisplay?.component) {\n <div data-cy=\"ai-tool-with-custom-component\">\n <ng-container\n [ngComponentOutlet]=\"toolDisplay.component\"\n [ngComponentOutletInputs]=\"{\n tool: tool,\n ctx: ctx\n }\"\n ></ng-container>\n </div>\n } @else if (!toolDisplay?.isHidden) {\n <!-- Don't allow expanding while tool call is in progress - it's unlikely to have anything worth rendering,\n and complicates implementation for the client to have to check for the isExecuting case.\n -->\n <c8y-ai-chat-tool-call\n [tool]=\"tool\"\n [isExecuting]=\"tool.type !== 'tool-result' && ctx.isMessageLoading\"\n [toolDetailsComponent]=\"_config.toolDetailsComponent?.(tool)\"\n [showDefaultToolDetails]=\"_config.showDefaultToolDetails === 'all'\"\n [executingLabel]=\"toolDisplay?.executingLabel\"\n [completedLabel]=\"toolDisplay?.completedLabel\"\n [labelProvider]=\"toolDisplay?.labelProvider\"\n ></c8y-ai-chat-tool-call>\n }\n}\n" }]
241
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "@let ctx = assistantMessageContext();\n@let _config = ctx.config;\n@let toolCallConfig = _config.toolCallConfig || {};\n@let _part = part();\n\n@if (_part.type === 'text') {\n <p\n [class.text-muted]=\"!displayAsPartOfMainAnswer()\"\n [innerHTML]=\"_part.text | markdownToHtml | async\"\n ></p>\n} @else if (_part.type === 'reasoning') {\n @let reasoningExpanded = expanded();\n\n <fieldset\n class=\"c8y-fieldset p-b-4 ai-tool-call__fieldset\"\n data-cy=\"ai-reasoning\"\n >\n <button\n class=\"btn-clean ai-tool-call__btn\"\n aria-label=\"{{ 'Toggle display of the reasoning section' | translate }}\"\n [attr.aria-expanded]=\"reasoningExpanded\"\n [attr.aria-controls]=\"'reasoning-content-message' + ctx.messageDisplayIndex\"\n type=\"button\"\n (click)=\"toggleExpanded()\"\n >\n <i\n class=\"m-r-4\"\n [c8yIcon]=\"'ai-sparkles'\"\n ></i>\n @let hideReasoning = 'Hide reasoning' | translate;\n @let showReasoning = 'Show reasoning' | translate;\n <span class=\"small\">{{ reasoningExpanded ? hideReasoning : showReasoning }}</span>\n <i\n class=\"m-l-4 icon-12 text-muted\"\n [c8yIcon]=\"reasoningExpanded ? 'collapse-arrow' : 'expand-arrow'\"\n ></i>\n </button>\n\n @let ariaLabel =\n 'Reasoning for message number {{ number }}' | translate: { number: ctx.messageDisplayIndex };\n <div\n class=\"collapse reasoning-content\"\n [attr.aria-label]=\"ariaLabel\"\n role=\"region\"\n [collapse]=\"!reasoningExpanded\"\n [id]=\"'reasoning-content-message' + ctx.messageDisplayIndex\"\n [isAnimated]=\"true\"\n data-cy=\"ai-reasoning-content\"\n >\n <div\n class=\"m-t-8 p-b-4 text-muted\"\n [innerHTML]=\"_part.text | markdownToHtml | async\"\n ></div>\n </div>\n </fieldset>\n} @else if (_part.type === 'object') {\n <p\n [class.text-muted]=\"!displayAsPartOfMainAnswer()\"\n [innerHTML]=\"_part.jsonContent | jsonToMarkdown | markdownToHtml | async\"\n data-cy=\"ai-object-content\"\n ></p>\n} @else if (_part.type === 'step-start') {\n <!-- Visually hidden label included when users copy-paste from the chat, e.g. to report issues -->\n <span\n class=\"ai-step-start hidden-copy-label\"\n aria-hidden=\"true\"\n data-cy=\"ai-step-start-separator\"\n translate\n >--- (step separator) ---</span\n >\n} @else if (_part.type.startsWith('tool')) {\n @let tool = $any(_part);\n @let toolDisplay = toolCallConfig[tool.toolName];\n\n <!-- Visually hidden label included when users copy-paste from the chat, e.g. to report issues -->\n <span\n class=\"hidden-copy-label\"\n aria-hidden=\"true\"\n translate\n [translateParams]=\"{ toolName: tool.toolName }\"\n ngNonBindable\n >--- Tool call: {{ toolName }} ---</span\n >\n\n @if (toolDisplay?.component) {\n <div data-cy=\"ai-tool-with-custom-component\">\n <ng-container\n [ngComponentOutlet]=\"toolDisplay.component\"\n [ngComponentOutletInputs]=\"{\n tool: tool,\n ctx: ctx\n }\"\n ></ng-container>\n </div>\n } @else if (!toolDisplay?.isHidden) {\n <!-- Don't allow expanding while tool call is in progress - it's unlikely to have anything worth rendering,\n and complicates implementation for the client to have to check for the isExecuting case.\n -->\n <c8y-ai-chat-tool-call\n [tool]=\"tool\"\n [isExecuting]=\"\n tool.type !== 'tool-result' &&\n !tool.type.startsWith('tool-approval') &&\n ctx.isMessageLoading\n \"\n [toolDetailsComponent]=\"_config.toolDetailsComponent?.(tool)\"\n [showDefaultToolDetails]=\"_config.showDefaultToolDetails === 'all'\"\n [executingLabel]=\"toolDisplay?.executingLabel\"\n [completedLabel]=\"toolDisplay?.completedLabel\"\n [labelProvider]=\"toolDisplay?.labelProvider\"\n [approvalEnabled]=\"\n ctx.messageDisplayIndex === 0 && !ctx.isMessageLoading && !!ctx.onToolApprovalResponse\n \"\n [rememberApprovalEnabled]=\"!ctx.canRememberToolApproval || ctx.canRememberToolApproval(tool)\"\n (approvalResponse)=\"ctx.onToolApprovalResponse?.(tool, $event)\"\n ></c8y-ai-chat-tool-call>\n }\n}\n" }]
147
242
  }], propDecorators: { part: [{ type: i0.Input, args: [{ isSignal: true, alias: "part", required: true }] }], displayAsPartOfMainAnswer: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayAsPartOfMainAnswer", required: false }] }], assistantMessageContext: [{ type: i0.Input, args: [{ isSignal: true, alias: "assistantMessageContext", required: true }] }] } });
148
243
 
149
244
  /**
@@ -222,7 +317,12 @@ class AiChatAssistantMessageComponent {
222
317
  toggleReasoningExpanded(stepIndex) {
223
318
  this.expandedReasoningStepIndices.update(s => {
224
319
  const next = new Set(s);
225
- s.has(stepIndex) ? next.delete(stepIndex) : next.add(stepIndex);
320
+ if (s.has(stepIndex)) {
321
+ next.delete(stepIndex);
322
+ }
323
+ else {
324
+ next.add(stepIndex);
325
+ }
226
326
  return next;
227
327
  });
228
328
  }
@@ -240,11 +340,7 @@ class AiChatAssistantMessageComponent {
240
340
  : gettext('Show reasoning');
241
341
  }
242
342
  getToolName(part) {
243
- return part.type === 'tool-input-streaming' ||
244
- part.type === 'tool-executing' ||
245
- part.type === 'tool-result'
246
- ? part.toolName
247
- : undefined;
343
+ return part.type.startsWith('tool-') ? part.toolName : undefined;
248
344
  }
249
345
  experimental_nonFinalStepTextDisplay(ctx) {
250
346
  // For now be conservative with the old behaviour, but hope to change this once we've got working really smoothly.
@@ -273,9 +369,7 @@ class AiChatAssistantMessageComponent {
273
369
  return { thinkingParts: [], mainAnswerParts: content };
274
370
  }
275
371
  const { finalStepStart, finalStepHasText } = this.findFinalStepWithText(content);
276
- const hasToolCalls = content.some(part => part.type === 'tool-input-streaming' ||
277
- part.type === 'tool-executing' ||
278
- part.type === 'tool-result');
372
+ const hasToolCalls = content.some(part => part.type.startsWith('tool-'));
279
373
  // Since we don't bother with a step-start at index 0 for the first step, add one
280
374
  const stepCount = content.filter((part, index) => index > 0 && part.type === 'step-start').length + 1;
281
375
  // The final step text goes in the main answer area if there are no pending tool calls
@@ -338,7 +432,7 @@ class AiChatAssistantMessageComponent {
338
432
  return { finalStepStart, finalStepHasText };
339
433
  }
340
434
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: AiChatAssistantMessageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
341
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: AiChatAssistantMessageComponent, isStandalone: true, selector: "c8y-ai-chat-assistant-message", inputs: { assistantMessageContext: { classPropertyName: "assistantMessageContext", publicName: "assistantMessageContext", isSignal: true, isRequired: true, transformFunction: null }, showWorkingIndicator: { classPropertyName: "showWorkingIndicator", publicName: "showWorkingIndicator", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@let ctx = assistantMessageContext();\n@let parts = ctx.message.content;\n@let _config = ctx.config;\n\n<!-- \nWe show the assistant message in (up to) 3 slots:\n- collapsible thinking block (for intermediate step text, standard tool calls, etc)\n- special \"artifact\" tools that update user artifacts and need prominent display (e.g. for code edits made by the AI)\n- the main answer (typically from the final step)\n\nThis is a standard approach for AI chats - step text and tools from an LLM are treated as a work log, i.e. useful to \ndisplay progress while streaming the response, but only the text from the last step after all tool calls gives the main answer of the AI to the user. \n\nDepending on configuration, we have an experimental option (if we need it) to style the thinking block the same as the main answer. \n\nOnce the message finishes loading we know the final step IS the main content, but if it's still steaming \nwe don't know for sure which step has the final/main answer until the LLM has actually finished so have to take a heuistic guess \n(and minimize jumping in the UI). \n\nFor applications where we expect tool calls in most responses, assume the 2nd step is probably the final text; \nfor applications where tool calls are less ubiquitous, assume the first step is main answer until proved otherwise. \nThis minimizes jumping (except when there are >2 steps, which is rare).\n\n-->\n\n@let contentSplit = thinkingAndMainAnswerParts();\n@let nonFinalStepTextDisplay = experimental_nonFinalStepTextDisplay(ctx);\n\n<!-- Thinking steps section - holds reasoning, text from non-final steps, and standard/non-artifact tool calls -->\n\n@if (contentSplit.thinkingParts.length > 0) {\n <div\n class=\"m-b-8 text-muted\"\n [class.thinking-block]=\"nonFinalStepTextDisplay === 'collapsible-thinking-block'\"\n [class.small]=\"nonFinalStepTextDisplay === 'muted-main-answer'\"\n [class.thinking-steps-appearance]=\"nonFinalStepTextDisplay\"\n data-cy=\"thinking-steps\"\n >\n @if (nonFinalStepTextDisplay === 'collapsible-thinking-block') {\n <button\n class=\"btn btn-clean btn-xs text-muted p-l-0\"\n aria-label=\"{{ 'Toggle collapse of the reasoning section' | translate }}\"\n [attr.aria-expanded]=\"thinkingExpanded()\"\n [attr.aria-controls]=\"'thinking-content-' + ctx.messageDisplayIndex\"\n type=\"button\"\n (click)=\"thinkingExpanded.set(!thinkingExpanded())\"\n >\n <span class=\"small\">{{ getThinkingLabel(ctx) }}</span>\n <i\n class=\"m-l-4 icon-12\"\n [c8yIcon]=\"thinkingExpanded() ? 'collapse-arrow' : 'expand-arrow'\"\n ></i>\n </button>\n }\n\n @let ariaLabel =\n 'Reasoning for message number {{ number }}' | translate: { number: ctx.messageDisplayIndex };\n <div\n class=\"collapse\"\n [attr.aria-label]=\"ariaLabel\"\n role=\"region\"\n [collapse]=\"nonFinalStepTextDisplay === 'collapsible-thinking-block' && !thinkingExpanded()\"\n [id]=\"'thinking-content-' + ctx.messageDisplayIndex\"\n [isAnimated]=\"true\"\n >\n <div\n [class]=\"\n nonFinalStepTextDisplay === 'collapsible-thinking-block'\n ? 'thinking-content bg-level-1 m-t-8 p-8 border-left-accent small m-b-0 text-muted'\n : ''\n \"\n >\n @for (part of contentSplit.thinkingParts; track $index) {\n @if (part.type !== 'step-start' || $index > 0) {\n <c8y-ai-chat-assistant-part\n [part]=\"part\"\n [assistantMessageContext]=\"ctx\"\n [displayAsPartOfMainAnswer]=\"nonFinalStepTextDisplay !== 'muted-main-answer'\"\n ></c8y-ai-chat-assistant-part>\n }\n }\n </div>\n </div>\n </div>\n}\n\n<!-- Main answer area contains the text from the final step, and \n any promoted tool calls which are too important to be hidden in the collapeable thinking area -->\n<div\n class=\"message-content\"\n data-cy=\"ai-main-answer-content\"\n>\n @for (part of contentSplit.mainAnswerParts; track $index) {\n <c8y-ai-chat-assistant-part\n [part]=\"part\"\n [assistantMessageContext]=\"ctx\"\n [displayAsPartOfMainAnswer]=\"true\"\n ></c8y-ai-chat-assistant-part>\n }\n</div>\n\n@if (ctx.isMessageLoading && showWorkingIndicator()) {\n <!-- Once the message starts to stream, show a small/low-key thinking indicator -->\n <div\n class=\"text-muted text-12 fade-in-out d-flex j-c-end\"\n aria-live=\"polite\"\n role=\"status\"\n data-cy=\"working-indicator\"\n >\n {{ 'Working\u2026' | translate }}\n </div>\n}\n", dependencies: [{ kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "ngmodule", type: CollapseModule }, { kind: "directive", type: i1.CollapseDirective, selector: "[collapse]", inputs: ["display", "isAnimated", "collapse"], outputs: ["collapsed", "collapses", "expanded", "expands"], exportAs: ["bs-collapse"] }, { kind: "ngmodule", type: C8yTranslateModule }, { kind: "component", type: AiChatAssistantPartComponent, selector: "c8y-ai-chat-assistant-part", inputs: ["part", "displayAsPartOfMainAnswer", "assistantMessageContext"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
435
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: AiChatAssistantMessageComponent, isStandalone: true, selector: "c8y-ai-chat-assistant-message", inputs: { assistantMessageContext: { classPropertyName: "assistantMessageContext", publicName: "assistantMessageContext", isSignal: true, isRequired: true, transformFunction: null }, showWorkingIndicator: { classPropertyName: "showWorkingIndicator", publicName: "showWorkingIndicator", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@let ctx = assistantMessageContext();\n@let parts = ctx.message.content;\n@let _config = ctx.config;\n\n<!-- \nWe show the assistant message in (up to) 3 slots:\n- collapsible thinking block (for intermediate step text, standard tool calls, etc)\n- special \"artifact\" tools that update user artifacts and need prominent display (e.g. for code edits made by the AI)\n- the main answer (typically from the final step)\n\nThis is a standard approach for AI chats - step text and tools from an LLM are treated as a work log, i.e. useful to \ndisplay progress while streaming the response, but only the text from the last step after all tool calls gives the main answer of the AI to the user. \n\nDepending on configuration, we have an experimental option (if we need it) to style the thinking block the same as the main answer. \n\nOnce the message finishes loading we know the final step IS the main content, but if it's still steaming \nwe don't know for sure which step has the final/main answer until the LLM has actually finished so have to take a heuistic guess \n(and minimize jumping in the UI). \n\nFor applications where we expect tool calls in most responses, assume the 2nd step is probably the final text; \nfor applications where tool calls are less ubiquitous, assume the first step is main answer until proved otherwise. \nThis minimizes jumping (except when there are >2 steps, which is rare).\n\n-->\n\n@let contentSplit = thinkingAndMainAnswerParts();\n@let nonFinalStepTextDisplay = experimental_nonFinalStepTextDisplay(ctx);\n\n<!-- Thinking steps section - holds reasoning, text from non-final steps, and standard/non-artifact tool calls -->\n\n@if (contentSplit.thinkingParts.length > 0) {\n <div\n class=\"m-b-8 text-muted\"\n [class.thinking-block]=\"nonFinalStepTextDisplay === 'collapsible-thinking-block'\"\n [class.small]=\"nonFinalStepTextDisplay === 'muted-main-answer'\"\n [class.thinking-steps-appearance]=\"nonFinalStepTextDisplay\"\n data-cy=\"thinking-steps\"\n >\n @if (nonFinalStepTextDisplay === 'collapsible-thinking-block') {\n <button\n class=\"btn btn-clean btn-xs text-muted p-l-0\"\n aria-label=\"{{ 'Toggle collapse of the reasoning section' | translate }}\"\n [attr.aria-expanded]=\"thinkingExpanded()\"\n [attr.aria-controls]=\"'thinking-content-' + ctx.messageDisplayIndex\"\n type=\"button\"\n (click)=\"thinkingExpanded.set(!thinkingExpanded())\"\n >\n <span class=\"small\">{{ getThinkingLabel(ctx) }}</span>\n <i\n class=\"m-l-4 icon-12\"\n [c8yIcon]=\"thinkingExpanded() ? 'collapse-arrow' : 'expand-arrow'\"\n ></i>\n </button>\n }\n\n @let ariaLabel =\n 'Reasoning for message number {{ number }}' | translate: { number: ctx.messageDisplayIndex };\n <div\n class=\"collapse\"\n [attr.aria-label]=\"ariaLabel\"\n role=\"region\"\n [collapse]=\"nonFinalStepTextDisplay === 'collapsible-thinking-block' && !thinkingExpanded()\"\n [id]=\"'thinking-content-' + ctx.messageDisplayIndex\"\n [isAnimated]=\"true\"\n >\n <div\n [class]=\"\n nonFinalStepTextDisplay === 'collapsible-thinking-block'\n ? 'thinking-content bg-level-1 m-t-8 p-8 border-left-accent small m-b-0 text-muted'\n : ''\n \"\n >\n @for (part of contentSplit.thinkingParts; track $index) {\n @if (part.type !== 'step-start' || $index > 0) {\n <c8y-ai-chat-assistant-part\n [part]=\"part\"\n [assistantMessageContext]=\"ctx\"\n [displayAsPartOfMainAnswer]=\"nonFinalStepTextDisplay !== 'muted-main-answer'\"\n ></c8y-ai-chat-assistant-part>\n }\n }\n </div>\n </div>\n </div>\n}\n\n<!-- Main answer area contains the text from the final step, and \n any promoted tool calls which are too important to be hidden in the collapeable thinking area -->\n<div\n class=\"message-content\"\n data-cy=\"ai-main-answer-content\"\n>\n @for (part of contentSplit.mainAnswerParts; track $index) {\n <c8y-ai-chat-assistant-part\n [part]=\"part\"\n [assistantMessageContext]=\"ctx\"\n [displayAsPartOfMainAnswer]=\"true\"\n ></c8y-ai-chat-assistant-part>\n }\n</div>\n\n@if (ctx.isMessageLoading && showWorkingIndicator()) {\n <!-- Once the message starts to stream, show a small/low-key thinking indicator -->\n <div\n class=\"text-muted text-12 gap-4 fade-in-out d-flex j-c-start a-i-center\"\n aria-live=\"polite\"\n role=\"status\"\n data-cy=\"working-indicator\"\n >\n <em>\n {{ 'Working' | translate }}\n </em>\n <span\n class=\"working-indicator__dots\"\n aria-hidden=\"true\"\n >\n <span></span>\n <span></span>\n <span></span>\n </span>\n </div>\n}\n", dependencies: [{ kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "ngmodule", type: CollapseModule }, { kind: "directive", type: i1.CollapseDirective, selector: "[collapse]", inputs: ["display", "isAnimated", "collapse"], outputs: ["collapsed", "collapses", "expanded", "expands"], exportAs: ["bs-collapse"] }, { kind: "ngmodule", type: C8yTranslateModule }, { kind: "component", type: AiChatAssistantPartComponent, selector: "c8y-ai-chat-assistant-part", inputs: ["part", "displayAsPartOfMainAnswer", "assistantMessageContext"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
342
436
  }
343
437
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: AiChatAssistantMessageComponent, decorators: [{
344
438
  type: Component,
@@ -348,7 +442,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
348
442
  CollapseModule,
349
443
  C8yTranslateModule,
350
444
  AiChatAssistantPartComponent
351
- ], template: "@let ctx = assistantMessageContext();\n@let parts = ctx.message.content;\n@let _config = ctx.config;\n\n<!-- \nWe show the assistant message in (up to) 3 slots:\n- collapsible thinking block (for intermediate step text, standard tool calls, etc)\n- special \"artifact\" tools that update user artifacts and need prominent display (e.g. for code edits made by the AI)\n- the main answer (typically from the final step)\n\nThis is a standard approach for AI chats - step text and tools from an LLM are treated as a work log, i.e. useful to \ndisplay progress while streaming the response, but only the text from the last step after all tool calls gives the main answer of the AI to the user. \n\nDepending on configuration, we have an experimental option (if we need it) to style the thinking block the same as the main answer. \n\nOnce the message finishes loading we know the final step IS the main content, but if it's still steaming \nwe don't know for sure which step has the final/main answer until the LLM has actually finished so have to take a heuistic guess \n(and minimize jumping in the UI). \n\nFor applications where we expect tool calls in most responses, assume the 2nd step is probably the final text; \nfor applications where tool calls are less ubiquitous, assume the first step is main answer until proved otherwise. \nThis minimizes jumping (except when there are >2 steps, which is rare).\n\n-->\n\n@let contentSplit = thinkingAndMainAnswerParts();\n@let nonFinalStepTextDisplay = experimental_nonFinalStepTextDisplay(ctx);\n\n<!-- Thinking steps section - holds reasoning, text from non-final steps, and standard/non-artifact tool calls -->\n\n@if (contentSplit.thinkingParts.length > 0) {\n <div\n class=\"m-b-8 text-muted\"\n [class.thinking-block]=\"nonFinalStepTextDisplay === 'collapsible-thinking-block'\"\n [class.small]=\"nonFinalStepTextDisplay === 'muted-main-answer'\"\n [class.thinking-steps-appearance]=\"nonFinalStepTextDisplay\"\n data-cy=\"thinking-steps\"\n >\n @if (nonFinalStepTextDisplay === 'collapsible-thinking-block') {\n <button\n class=\"btn btn-clean btn-xs text-muted p-l-0\"\n aria-label=\"{{ 'Toggle collapse of the reasoning section' | translate }}\"\n [attr.aria-expanded]=\"thinkingExpanded()\"\n [attr.aria-controls]=\"'thinking-content-' + ctx.messageDisplayIndex\"\n type=\"button\"\n (click)=\"thinkingExpanded.set(!thinkingExpanded())\"\n >\n <span class=\"small\">{{ getThinkingLabel(ctx) }}</span>\n <i\n class=\"m-l-4 icon-12\"\n [c8yIcon]=\"thinkingExpanded() ? 'collapse-arrow' : 'expand-arrow'\"\n ></i>\n </button>\n }\n\n @let ariaLabel =\n 'Reasoning for message number {{ number }}' | translate: { number: ctx.messageDisplayIndex };\n <div\n class=\"collapse\"\n [attr.aria-label]=\"ariaLabel\"\n role=\"region\"\n [collapse]=\"nonFinalStepTextDisplay === 'collapsible-thinking-block' && !thinkingExpanded()\"\n [id]=\"'thinking-content-' + ctx.messageDisplayIndex\"\n [isAnimated]=\"true\"\n >\n <div\n [class]=\"\n nonFinalStepTextDisplay === 'collapsible-thinking-block'\n ? 'thinking-content bg-level-1 m-t-8 p-8 border-left-accent small m-b-0 text-muted'\n : ''\n \"\n >\n @for (part of contentSplit.thinkingParts; track $index) {\n @if (part.type !== 'step-start' || $index > 0) {\n <c8y-ai-chat-assistant-part\n [part]=\"part\"\n [assistantMessageContext]=\"ctx\"\n [displayAsPartOfMainAnswer]=\"nonFinalStepTextDisplay !== 'muted-main-answer'\"\n ></c8y-ai-chat-assistant-part>\n }\n }\n </div>\n </div>\n </div>\n}\n\n<!-- Main answer area contains the text from the final step, and \n any promoted tool calls which are too important to be hidden in the collapeable thinking area -->\n<div\n class=\"message-content\"\n data-cy=\"ai-main-answer-content\"\n>\n @for (part of contentSplit.mainAnswerParts; track $index) {\n <c8y-ai-chat-assistant-part\n [part]=\"part\"\n [assistantMessageContext]=\"ctx\"\n [displayAsPartOfMainAnswer]=\"true\"\n ></c8y-ai-chat-assistant-part>\n }\n</div>\n\n@if (ctx.isMessageLoading && showWorkingIndicator()) {\n <!-- Once the message starts to stream, show a small/low-key thinking indicator -->\n <div\n class=\"text-muted text-12 fade-in-out d-flex j-c-end\"\n aria-live=\"polite\"\n role=\"status\"\n data-cy=\"working-indicator\"\n >\n {{ 'Working\u2026' | translate }}\n </div>\n}\n" }]
445
+ ], template: "@let ctx = assistantMessageContext();\n@let parts = ctx.message.content;\n@let _config = ctx.config;\n\n<!-- \nWe show the assistant message in (up to) 3 slots:\n- collapsible thinking block (for intermediate step text, standard tool calls, etc)\n- special \"artifact\" tools that update user artifacts and need prominent display (e.g. for code edits made by the AI)\n- the main answer (typically from the final step)\n\nThis is a standard approach for AI chats - step text and tools from an LLM are treated as a work log, i.e. useful to \ndisplay progress while streaming the response, but only the text from the last step after all tool calls gives the main answer of the AI to the user. \n\nDepending on configuration, we have an experimental option (if we need it) to style the thinking block the same as the main answer. \n\nOnce the message finishes loading we know the final step IS the main content, but if it's still steaming \nwe don't know for sure which step has the final/main answer until the LLM has actually finished so have to take a heuistic guess \n(and minimize jumping in the UI). \n\nFor applications where we expect tool calls in most responses, assume the 2nd step is probably the final text; \nfor applications where tool calls are less ubiquitous, assume the first step is main answer until proved otherwise. \nThis minimizes jumping (except when there are >2 steps, which is rare).\n\n-->\n\n@let contentSplit = thinkingAndMainAnswerParts();\n@let nonFinalStepTextDisplay = experimental_nonFinalStepTextDisplay(ctx);\n\n<!-- Thinking steps section - holds reasoning, text from non-final steps, and standard/non-artifact tool calls -->\n\n@if (contentSplit.thinkingParts.length > 0) {\n <div\n class=\"m-b-8 text-muted\"\n [class.thinking-block]=\"nonFinalStepTextDisplay === 'collapsible-thinking-block'\"\n [class.small]=\"nonFinalStepTextDisplay === 'muted-main-answer'\"\n [class.thinking-steps-appearance]=\"nonFinalStepTextDisplay\"\n data-cy=\"thinking-steps\"\n >\n @if (nonFinalStepTextDisplay === 'collapsible-thinking-block') {\n <button\n class=\"btn btn-clean btn-xs text-muted p-l-0\"\n aria-label=\"{{ 'Toggle collapse of the reasoning section' | translate }}\"\n [attr.aria-expanded]=\"thinkingExpanded()\"\n [attr.aria-controls]=\"'thinking-content-' + ctx.messageDisplayIndex\"\n type=\"button\"\n (click)=\"thinkingExpanded.set(!thinkingExpanded())\"\n >\n <span class=\"small\">{{ getThinkingLabel(ctx) }}</span>\n <i\n class=\"m-l-4 icon-12\"\n [c8yIcon]=\"thinkingExpanded() ? 'collapse-arrow' : 'expand-arrow'\"\n ></i>\n </button>\n }\n\n @let ariaLabel =\n 'Reasoning for message number {{ number }}' | translate: { number: ctx.messageDisplayIndex };\n <div\n class=\"collapse\"\n [attr.aria-label]=\"ariaLabel\"\n role=\"region\"\n [collapse]=\"nonFinalStepTextDisplay === 'collapsible-thinking-block' && !thinkingExpanded()\"\n [id]=\"'thinking-content-' + ctx.messageDisplayIndex\"\n [isAnimated]=\"true\"\n >\n <div\n [class]=\"\n nonFinalStepTextDisplay === 'collapsible-thinking-block'\n ? 'thinking-content bg-level-1 m-t-8 p-8 border-left-accent small m-b-0 text-muted'\n : ''\n \"\n >\n @for (part of contentSplit.thinkingParts; track $index) {\n @if (part.type !== 'step-start' || $index > 0) {\n <c8y-ai-chat-assistant-part\n [part]=\"part\"\n [assistantMessageContext]=\"ctx\"\n [displayAsPartOfMainAnswer]=\"nonFinalStepTextDisplay !== 'muted-main-answer'\"\n ></c8y-ai-chat-assistant-part>\n }\n }\n </div>\n </div>\n </div>\n}\n\n<!-- Main answer area contains the text from the final step, and \n any promoted tool calls which are too important to be hidden in the collapeable thinking area -->\n<div\n class=\"message-content\"\n data-cy=\"ai-main-answer-content\"\n>\n @for (part of contentSplit.mainAnswerParts; track $index) {\n <c8y-ai-chat-assistant-part\n [part]=\"part\"\n [assistantMessageContext]=\"ctx\"\n [displayAsPartOfMainAnswer]=\"true\"\n ></c8y-ai-chat-assistant-part>\n }\n</div>\n\n@if (ctx.isMessageLoading && showWorkingIndicator()) {\n <!-- Once the message starts to stream, show a small/low-key thinking indicator -->\n <div\n class=\"text-muted text-12 gap-4 fade-in-out d-flex j-c-start a-i-center\"\n aria-live=\"polite\"\n role=\"status\"\n data-cy=\"working-indicator\"\n >\n <em>\n {{ 'Working' | translate }}\n </em>\n <span\n class=\"working-indicator__dots\"\n aria-hidden=\"true\"\n >\n <span></span>\n <span></span>\n <span></span>\n </span>\n </div>\n}\n" }]
352
446
  }], ctorParameters: () => [], propDecorators: { assistantMessageContext: [{ type: i0.Input, args: [{ isSignal: true, alias: "assistantMessageContext", required: true }] }], showWorkingIndicator: [{ type: i0.Input, args: [{ isSignal: true, alias: "showWorkingIndicator", required: false }] }] } });
353
447
 
354
448
  /**
@@ -411,17 +505,15 @@ class AiChatMessageComponent {
411
505
  return this.message()?.role || this.role();
412
506
  }, ...(ngDevMode ? [{ debugName: "roleResolved" }] : /* istanbul ignore next */ []));
413
507
  /**
414
- * Generates an accessible label for the message container.
415
- * Includes the role (user or assistant) and timestamp if available.
508
+ * Generates an accessible label for the message container: role (user or assistant) and
509
+ * timestamp if available.
510
+ *
511
+ * Deliberately short - the actual message content is already exposed to assistive technology via
512
+ * the normal (structured) DOM content, so duplicating it here as a flat string would just repeat
513
+ * a long, unstructured block of text, especially for multi-paragraph assistant responses.
416
514
  * @returns The aria-label string for screen readers
417
515
  */
418
- this.ariaLabel = computed(() => {
419
- const tpl = this.roleResolved() === 'user'
420
- ? gettext('You said: "{{ message }}"')
421
- : gettext('Assistant said: "{{ message }}"');
422
- const msg = this.message();
423
- return this.translateService.instant(tpl, { message: msg?.content || gettext('No response.') });
424
- }, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
516
+ this.ariaLabel = computed(() => this.messageContentAriaLabel(), ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
425
517
  /**
426
518
  * Generates an accessible label for the message content.
427
519
  * Prefixes the content with contextual information about who sent it.
@@ -444,13 +536,217 @@ class AiChatMessageComponent {
444
536
  }, ...(ngDevMode ? [{ debugName: "messageContentAriaLabel" }] : /* istanbul ignore next */ []));
445
537
  }
446
538
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: AiChatMessageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
447
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: AiChatMessageComponent, isStandalone: true, selector: "c8y-ai-chat-message", inputs: { role: { classPropertyName: "role", publicName: "role", isSignal: true, isRequired: false, transformFunction: null }, message: { classPropertyName: "message", publicName: "message", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div\n class=\"d-col p-t-16\"\n [attr.aria-label]=\"ariaLabel()\"\n role=\"article\"\n>\n <div\n class=\"chat-message text-break-word\"\n [ngClass]=\"{\n 'user-message': message()?.role === 'user' || role() === 'user',\n 'agent-message': message()?.role === 'assistant' || role() === 'assistant'\n }\"\n >\n <!-- Visually hidden label included when users copy-paste from the chat, e.g. to report issues -->\n <span\n class=\"hidden-copy-label\"\n aria-hidden=\"true\"\n >\n {{ `====== ${messageContentAriaLabel()} ======` }}</span\n >\n\n <!-- Apply the class and aria-label to both the main content and whatever is projected, so we get the same styling by default -->\n <div\n class=\"message-content\"\n [attr.aria-label]=\"messageContentAriaLabel()\"\n >\n <div style=\"display: contents\">\n <ng-content select=\":not(c8y-ai-chat-message-action)\"></ng-content>\n </div>\n </div>\n @if (message()?.timestamp) {\n <div class=\"message-timestamp\">\n <span [tooltip]=\"message()?.timestamp | c8yDate\">\n {{ message()?.timestamp | c8yDate: 'adaptiveDate' }}\n </span>\n </div>\n }\n </div>\n <div\n class=\"message-action\"\n [attr.aria-label]=\"'Message actions' | translate\"\n role=\"toolbar\"\n [ngClass]=\"{\n 'user-action showOnHover': message()?.role === 'user' || role() === 'user',\n 'agent-action p-l-16': message()?.role === 'assistant' || role() === 'assistant'\n }\"\n >\n <ng-content select=\"c8y-ai-chat-message-action\"></ng-content>\n </div>\n</div>\n", styles: [".hidden-copy-label{display:block;font-size:0;line-height:0;-webkit-user-select:text;user-select:text}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i1$1.TooltipDirective, selector: "[tooltip], [tooltipHtml]", inputs: ["adaptivePosition", "tooltip", "placement", "triggers", "container", "containerClass", "boundariesElement", "isOpen", "isDisabled", "delay", "tooltipHtml", "tooltipPlacement", "tooltipIsOpen", "tooltipEnable", "tooltipAppendToBody", "tooltipAnimation", "tooltipClass", "tooltipContext", "tooltipPopupDelay", "tooltipFadeDuration", "tooltipTrigger"], outputs: ["tooltipChange", "onShown", "onHidden", "tooltipStateChanged"], exportAs: ["bs-tooltip"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: DatePipe, name: "c8yDate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
539
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: AiChatMessageComponent, isStandalone: true, selector: "c8y-ai-chat-message", inputs: { role: { classPropertyName: "role", publicName: "role", isSignal: true, isRequired: false, transformFunction: null }, message: { classPropertyName: "message", publicName: "message", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div\n class=\"d-col\"\n [attr.aria-label]=\"ariaLabel()\"\n role=\"article\"\n>\n <div\n class=\"chat-message text-break-word\"\n [ngClass]=\"{\n 'user-message': message()?.role === 'user' || role() === 'user',\n 'agent-message': message()?.role === 'assistant' || role() === 'assistant'\n }\"\n >\n <!-- Visually hidden label included when users copy-paste from the chat, e.g. to report issues -->\n <span\n class=\"hidden-copy-label\"\n aria-hidden=\"true\"\n >\n {{ `====== ${messageContentAriaLabel()} ======` }}</span\n >\n\n <!-- Apply the class and aria-label to both the main content and whatever is projected, so we get the same styling by default -->\n <div\n class=\"message-content\"\n [attr.aria-label]=\"messageContentAriaLabel()\"\n >\n <div style=\"display: contents\">\n <ng-content select=\":not(c8y-ai-chat-message-action)\"></ng-content>\n </div>\n </div>\n @if (message()?.timestamp) {\n <div class=\"message-timestamp\">\n <span\n tabindex=\"0\"\n [tooltip]=\"message()?.timestamp | c8yDate\"\n >\n {{ message()?.timestamp | c8yDate: 'adaptiveDate' }}\n </span>\n </div>\n }\n </div>\n <div\n class=\"message-action\"\n [attr.aria-label]=\"'Message actions' | translate\"\n role=\"toolbar\"\n [ngClass]=\"{\n 'user-action showOnHover': message()?.role === 'user' || role() === 'user',\n 'agent-action p-l-16': message()?.role === 'assistant' || role() === 'assistant'\n }\"\n >\n <ng-content select=\"c8y-ai-chat-message-action\"></ng-content>\n </div>\n</div>\n", styles: [".hidden-copy-label{display:block;font-size:0;line-height:0;-webkit-user-select:text;user-select:text}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i1$1.TooltipDirective, selector: "[tooltip], [tooltipHtml]", inputs: ["adaptivePosition", "tooltip", "placement", "triggers", "container", "containerClass", "boundariesElement", "isOpen", "isDisabled", "delay", "tooltipHtml", "tooltipPlacement", "tooltipIsOpen", "tooltipEnable", "tooltipAppendToBody", "tooltipAnimation", "tooltipClass", "tooltipContext", "tooltipPopupDelay", "tooltipFadeDuration", "tooltipTrigger"], outputs: ["tooltipChange", "onShown", "onHidden", "tooltipStateChanged"], exportAs: ["bs-tooltip"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: DatePipe, name: "c8yDate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
448
540
  }
449
541
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: AiChatMessageComponent, decorators: [{
450
542
  type: Component,
451
- args: [{ selector: 'c8y-ai-chat-message', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass, TooltipModule, C8yTranslatePipe, DatePipe], template: "<div\n class=\"d-col p-t-16\"\n [attr.aria-label]=\"ariaLabel()\"\n role=\"article\"\n>\n <div\n class=\"chat-message text-break-word\"\n [ngClass]=\"{\n 'user-message': message()?.role === 'user' || role() === 'user',\n 'agent-message': message()?.role === 'assistant' || role() === 'assistant'\n }\"\n >\n <!-- Visually hidden label included when users copy-paste from the chat, e.g. to report issues -->\n <span\n class=\"hidden-copy-label\"\n aria-hidden=\"true\"\n >\n {{ `====== ${messageContentAriaLabel()} ======` }}</span\n >\n\n <!-- Apply the class and aria-label to both the main content and whatever is projected, so we get the same styling by default -->\n <div\n class=\"message-content\"\n [attr.aria-label]=\"messageContentAriaLabel()\"\n >\n <div style=\"display: contents\">\n <ng-content select=\":not(c8y-ai-chat-message-action)\"></ng-content>\n </div>\n </div>\n @if (message()?.timestamp) {\n <div class=\"message-timestamp\">\n <span [tooltip]=\"message()?.timestamp | c8yDate\">\n {{ message()?.timestamp | c8yDate: 'adaptiveDate' }}\n </span>\n </div>\n }\n </div>\n <div\n class=\"message-action\"\n [attr.aria-label]=\"'Message actions' | translate\"\n role=\"toolbar\"\n [ngClass]=\"{\n 'user-action showOnHover': message()?.role === 'user' || role() === 'user',\n 'agent-action p-l-16': message()?.role === 'assistant' || role() === 'assistant'\n }\"\n >\n <ng-content select=\"c8y-ai-chat-message-action\"></ng-content>\n </div>\n</div>\n", styles: [".hidden-copy-label{display:block;font-size:0;line-height:0;-webkit-user-select:text;user-select:text}\n"] }]
543
+ args: [{ selector: 'c8y-ai-chat-message', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass, TooltipModule, C8yTranslatePipe, DatePipe], template: "<div\n class=\"d-col\"\n [attr.aria-label]=\"ariaLabel()\"\n role=\"article\"\n>\n <div\n class=\"chat-message text-break-word\"\n [ngClass]=\"{\n 'user-message': message()?.role === 'user' || role() === 'user',\n 'agent-message': message()?.role === 'assistant' || role() === 'assistant'\n }\"\n >\n <!-- Visually hidden label included when users copy-paste from the chat, e.g. to report issues -->\n <span\n class=\"hidden-copy-label\"\n aria-hidden=\"true\"\n >\n {{ `====== ${messageContentAriaLabel()} ======` }}</span\n >\n\n <!-- Apply the class and aria-label to both the main content and whatever is projected, so we get the same styling by default -->\n <div\n class=\"message-content\"\n [attr.aria-label]=\"messageContentAriaLabel()\"\n >\n <div style=\"display: contents\">\n <ng-content select=\":not(c8y-ai-chat-message-action)\"></ng-content>\n </div>\n </div>\n @if (message()?.timestamp) {\n <div class=\"message-timestamp\">\n <span\n tabindex=\"0\"\n [tooltip]=\"message()?.timestamp | c8yDate\"\n >\n {{ message()?.timestamp | c8yDate: 'adaptiveDate' }}\n </span>\n </div>\n }\n </div>\n <div\n class=\"message-action\"\n [attr.aria-label]=\"'Message actions' | translate\"\n role=\"toolbar\"\n [ngClass]=\"{\n 'user-action showOnHover': message()?.role === 'user' || role() === 'user',\n 'agent-action p-l-16': message()?.role === 'assistant' || role() === 'assistant'\n }\"\n >\n <ng-content select=\"c8y-ai-chat-message-action\"></ng-content>\n </div>\n</div>\n", styles: [".hidden-copy-label{display:block;font-size:0;line-height:0;-webkit-user-select:text;user-select:text}\n"] }]
452
544
  }], propDecorators: { role: [{ type: i0.Input, args: [{ isSignal: true, alias: "role", required: false }] }], message: [{ type: i0.Input, args: [{ isSignal: true, alias: "message", required: false }] }] } });
453
545
 
546
+ /**
547
+ * Thin wrapper around the browser's Web Speech API (`SpeechRecognition`) used to transcribe speech
548
+ * to text. The API is not available in every browser, so consumers must check {@link isSupported}
549
+ * before offering speech input.
550
+ */
551
+ class SpeechRecognitionService {
552
+ constructor() {
553
+ this.ngZone = inject(NgZone);
554
+ this.recognitionConstructor = this.resolveConstructor();
555
+ }
556
+ /** Whether the current browser exposes the Web Speech API required for speech-to-text. */
557
+ get isSupported() {
558
+ return !!this.recognitionConstructor;
559
+ }
560
+ /**
561
+ * Starts a recognition session and emits the cumulative transcript recognized so far on every
562
+ * update (including interim results, when enabled). The observable completes when recognition
563
+ * ends and errors with the API error code (a string such as `not-allowed`) when it fails.
564
+ * Unsubscribing aborts an in-progress recognition.
565
+ */
566
+ listen(options = {}) {
567
+ return new Observable(subscriber => {
568
+ if (!this.recognitionConstructor) {
569
+ subscriber.error('not-supported');
570
+ return undefined;
571
+ }
572
+ const recognition = new this.recognitionConstructor();
573
+ recognition.lang = options.lang || navigator.language;
574
+ recognition.interimResults = options.interimResults ?? true;
575
+ recognition.continuous = true;
576
+ recognition.maxAlternatives = 1;
577
+ // The Web Speech API callbacks are not patched by zone.js, so run them inside the Angular
578
+ // zone to keep change detection working for consumers.
579
+ recognition.onresult = event => this.ngZone.run(() => subscriber.next(this.collectTranscript(event)));
580
+ recognition.onerror = event => this.ngZone.run(() => subscriber.error(event.error));
581
+ recognition.onend = () => this.ngZone.run(() => subscriber.complete());
582
+ recognition.start();
583
+ return () => {
584
+ recognition.onresult = null;
585
+ recognition.onerror = null;
586
+ recognition.onend = null;
587
+ recognition.abort();
588
+ };
589
+ });
590
+ }
591
+ /** Joins the transcripts of every recognition result into a single string. */
592
+ collectTranscript(event) {
593
+ let transcript = '';
594
+ for (let i = 0; i < event.results.length; i++) {
595
+ transcript += event.results[i][0].transcript;
596
+ }
597
+ return transcript;
598
+ }
599
+ /** Resolves the recognition constructor from the window, handling the `webkit` prefix. */
600
+ resolveConstructor() {
601
+ if (typeof window === 'undefined') {
602
+ return undefined;
603
+ }
604
+ const speechWindow = window;
605
+ return speechWindow.SpeechRecognition ?? speechWindow.webkitSpeechRecognition;
606
+ }
607
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: SpeechRecognitionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
608
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: SpeechRecognitionService, providedIn: 'root' }); }
609
+ }
610
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: SpeechRecognitionService, decorators: [{
611
+ type: Injectable,
612
+ args: [{ providedIn: 'root' }]
613
+ }] });
614
+
615
+ /**
616
+ * A microphone button for the `c8y-ai-chat` action bar that transcribes speech into the chat input
617
+ * using the browser's Web Speech API. Recognized text is appended to the current `prompt`.
618
+ *
619
+ * The button is only rendered when the current browser supports the Web Speech API, so it stays
620
+ * hidden where speech input is unavailable. While a recognition session is running the microphone
621
+ * icon pulses.
622
+ *
623
+ * `c8y-ai-chat` renders this button automatically unless `config.enableSpeechToText` is `false`, so
624
+ * you normally do not place it yourself.
625
+ */
626
+ class AiChatSpeechToTextComponent {
627
+ constructor() {
628
+ this.speechRecognition = inject(SpeechRecognitionService);
629
+ this.alertService = inject(AlertService);
630
+ this.destroyRef = inject(DestroyRef);
631
+ this.appState = inject(AppStateService);
632
+ /**
633
+ * Disables the button, for example while the chat is loading or otherwise unavailable. The
634
+ * button is still rendered (see {@link isAvailable} for whether it is offered at all), just
635
+ * not interactive.
636
+ */
637
+ this.disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
638
+ /**
639
+ * BCP-47 language tag to recognize, e.g. `en-US`. When not set, the application's active UI
640
+ * language is used (falling back to the browser language).
641
+ */
642
+ this.lang = input(...(ngDevMode ? [undefined, { debugName: "lang" }] : /* istanbul ignore next */ []));
643
+ /**
644
+ * The chat input text. Recognized speech is appended to it. Supports two-way binding with
645
+ * `[(prompt)]`.
646
+ */
647
+ this.prompt = model('', ...(ngDevMode ? [{ debugName: "prompt" }] : /* istanbul ignore next */ []));
648
+ /**
649
+ * Whether a recognition session is currently running. Drives the pulsing microphone icon.
650
+ */
651
+ this.isListening = signal(false, ...(ngDevMode ? [{ debugName: "isListening" }] : /* istanbul ignore next */ []));
652
+ /**
653
+ * Emits whenever listening starts (`true`) or stops (`false`), so the host can react, for example
654
+ * by adapting the chat input placeholder while the user is dictating.
655
+ */
656
+ this.listeningChange = output();
657
+ /**
658
+ * Whether the button should be shown at all: the browser supports the Web Speech API. Callers
659
+ * that want to withhold the feature entirely (as `c8y-ai-chat` does via
660
+ * `config.enableSpeechToText`) should not render this component rather than pass a "disabled"
661
+ * flag here - `disabled` only controls interactivity of an already-shown button.
662
+ */
663
+ this.isAvailable = computed(() => this.speechRecognition.isSupported, ...(ngDevMode ? [{ debugName: "isAvailable" }] : /* istanbul ignore next */ []));
664
+ this.startLabel = gettext('Start voice input');
665
+ this.stopLabel = gettext('Stop voice input');
666
+ /**
667
+ * The input text present when listening started, so recognized speech is appended to it.
668
+ */
669
+ this.textBeforeListening = '';
670
+ }
671
+ /**
672
+ * Starts listening when idle, or stops the current session when already listening.
673
+ */
674
+ toggleListening() {
675
+ if (this.isListening()) {
676
+ this.stopListening();
677
+ }
678
+ else {
679
+ this.startListening();
680
+ }
681
+ }
682
+ startListening() {
683
+ this.listenSubscription?.unsubscribe();
684
+ this.textBeforeListening = (this.prompt() ?? '').trim();
685
+ this.setListening(true);
686
+ this.listenSubscription = this.speechRecognition
687
+ .listen({ lang: this.resolveLang() })
688
+ .pipe(takeUntilDestroyed(this.destroyRef))
689
+ .subscribe({
690
+ next: transcript => this.prompt.set(this.appendTranscript(transcript)),
691
+ error: error => this.handleError(error),
692
+ complete: () => this.setListening(false)
693
+ });
694
+ }
695
+ stopListening() {
696
+ this.listenSubscription?.unsubscribe();
697
+ this.setListening(false);
698
+ }
699
+ /**
700
+ * Resolves the language to recognize: the explicit {@link lang} input when set, otherwise the
701
+ * application's active UI language. The stored language uses an underscore region separator
702
+ * (e.g. `zh_CN`), so it is normalized to the hyphenated BCP-47 form the Web Speech API expects.
703
+ */
704
+ resolveLang() {
705
+ return this.lang() || this.appState.state.lang?.replace('_', '-');
706
+ }
707
+ /** Updates the listening state and notifies the host, ignoring no-op transitions. */
708
+ setListening(listening) {
709
+ if (this.isListening() === listening) {
710
+ return;
711
+ }
712
+ this.isListening.set(listening);
713
+ this.listeningChange.emit(listening);
714
+ }
715
+ /**
716
+ * Appends the recognized transcript to the text that was present when listening started.
717
+ */
718
+ appendTranscript(transcript) {
719
+ const recognized = transcript.trim();
720
+ if (!recognized) {
721
+ return this.textBeforeListening;
722
+ }
723
+ return this.textBeforeListening ? `${this.textBeforeListening} ${recognized}` : recognized;
724
+ }
725
+ /**
726
+ * Ends the session and, for errors the user can act on (such as a denied microphone permission),
727
+ * shows an alert. Benign outcomes like "no speech detected" or a user-initiated stop are ignored.
728
+ */
729
+ handleError(error) {
730
+ this.setListening(false);
731
+ if (error === 'no-speech' || error === 'aborted') {
732
+ return;
733
+ }
734
+ console.error('Speech recognition failed:', error);
735
+ if (error === 'not-allowed' || error === 'service-not-allowed') {
736
+ this.alertService.danger(gettext('Microphone access is blocked. Allow microphone access to use voice input.'));
737
+ }
738
+ else {
739
+ this.alertService.danger(gettext('Voice input failed. Try again.'));
740
+ }
741
+ }
742
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: AiChatSpeechToTextComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
743
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: AiChatSpeechToTextComponent, isStandalone: true, selector: "c8y-ai-chat-speech-to-text", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, lang: { classPropertyName: "lang", publicName: "lang", isSignal: true, isRequired: false, transformFunction: null }, prompt: { classPropertyName: "prompt", publicName: "prompt", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { prompt: "promptChange", listeningChange: "listeningChange" }, host: { classAttribute: "d-contents" }, ngImport: i0, template: "@if (isAvailable()) {\n @let label = (isListening() ? stopLabel : startLabel) | translate;\n <button\n class=\"btn btn-dot c8y-speech-to-text-btn\"\n [class.c8y-speech-to-text-btn--listening]=\"isListening()\"\n [attr.title]=\"label\"\n [attr.aria-label]=\"label\"\n [attr.aria-pressed]=\"isListening()\"\n type=\"button\"\n [disabled]=\"disabled()\"\n (click)=\"toggleListening()\"\n >\n <i\n class=\"c8y-speech-to-text-btn__icon\"\n [c8yIcon]=\"'microphone'\"\n aria-hidden=\"true\"\n ></i>\n <span\n class=\"sr-only\"\n aria-live=\"polite\"\n role=\"status\"\n >\n @if (isListening()) {\n {{ 'Listening\u2026' | translate }}\n }\n </span>\n </button>\n}\n", dependencies: [{ kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
744
+ }
745
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: AiChatSpeechToTextComponent, decorators: [{
746
+ type: Component,
747
+ args: [{ selector: 'c8y-ai-chat-speech-to-text', changeDetection: ChangeDetectionStrategy.OnPush, imports: [C8yTranslatePipe, IconDirective], host: { class: 'd-contents' }, template: "@if (isAvailable()) {\n @let label = (isListening() ? stopLabel : startLabel) | translate;\n <button\n class=\"btn btn-dot c8y-speech-to-text-btn\"\n [class.c8y-speech-to-text-btn--listening]=\"isListening()\"\n [attr.title]=\"label\"\n [attr.aria-label]=\"label\"\n [attr.aria-pressed]=\"isListening()\"\n type=\"button\"\n [disabled]=\"disabled()\"\n (click)=\"toggleListening()\"\n >\n <i\n class=\"c8y-speech-to-text-btn__icon\"\n [c8yIcon]=\"'microphone'\"\n aria-hidden=\"true\"\n ></i>\n <span\n class=\"sr-only\"\n aria-live=\"polite\"\n role=\"status\"\n >\n @if (isListening()) {\n {{ 'Listening\u2026' | translate }}\n }\n </span>\n </button>\n}\n" }]
748
+ }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], lang: [{ type: i0.Input, args: [{ isSignal: true, alias: "lang", required: false }] }], prompt: [{ type: i0.Input, args: [{ isSignal: true, alias: "prompt", required: false }] }, { type: i0.Output, args: ["promptChange"] }], listeningChange: [{ type: i0.Output, args: ["listeningChange"] }] } });
749
+
454
750
  /**
455
751
  * A suggestion chip that can be displayed in the chat interface.
456
752
  * When clicked, it automatically populates and sends a predefined prompt.
@@ -506,12 +802,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
506
802
  * custom configuration, and markdown formatting.
507
803
  */
508
804
  class AiChatComponent {
805
+ /**
806
+ * Indicates whether the chat is currently processing a request.
807
+ * When true, displays a cancel button instead of send button and disables the input.
808
+ */
809
+ set isLoading(value) {
810
+ if (value === this._isLoading)
811
+ return;
812
+ this._isLoading = value;
813
+ // `aria-busy` on the log holds streamed tokens back, so announce start and end instead.
814
+ this.statusAnnouncement.set(value ? gettext('Generating response…') : gettext('Response complete'));
815
+ }
816
+ get isLoading() {
817
+ return this._isLoading;
818
+ }
509
819
  constructor() {
510
- /**
511
- * Indicates whether the chat is currently processing a request.
512
- * When true, displays a cancel button instead of send button and disables the input.
513
- */
514
- this.isLoading = false;
820
+ this._isLoading = false;
821
+ /** Announced politely when the response starts and when it finishes. */
822
+ this.statusAnnouncement = signal('', ...(ngDevMode ? [{ debugName: "statusAnnouncement" }] : /* istanbul ignore next */ []));
515
823
  /**
516
824
  * Disables the chat input and send button when true.
517
825
  */
@@ -534,17 +842,41 @@ class AiChatComponent {
534
842
  * Child message components displayed in the chat.
535
843
  */
536
844
  this.messages = contentChildren(AiChatMessageComponent, ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
845
+ /**
846
+ * Action components projected into the action bar beneath the input box.
847
+ */
848
+ this.projectedActions = contentChildren(AiChatActionComponent, ...(ngDevMode ? [{ debugName: "projectedActions" }] : /* istanbul ignore next */ []));
849
+ /**
850
+ * Action components provided programmatically. Used by wrapper components (such as
851
+ * `c8y-agent-chat`) to forward actions projected by their own consumers, since content queries do
852
+ * not reach components re-projected through a nested `ng-content`.
853
+ */
854
+ this.actions = input([], ...(ngDevMode ? [{ debugName: "actions" }] : /* istanbul ignore next */ []));
855
+ /** All actions from content projection and the `actions` input, in declaration order. */
856
+ this.allActions = computed(() => [...this.projectedActions(), ...this.actions()], ...(ngDevMode ? [{ debugName: "allActions" }] : /* istanbul ignore next */ []));
857
+ /** Whether any action is present, i.e. whether the action bar should be rendered. */
858
+ this.hasActionBar = computed(() => this.allActions().length > 0, ...(ngDevMode ? [{ debugName: "hasActionBar" }] : /* istanbul ignore next */ []));
859
+ /** Actions placed on the left side of the action bar. */
860
+ this.leftActions = computed(() => this.allActions().filter(action => action.placement() === 'left'), ...(ngDevMode ? [{ debugName: "leftActions" }] : /* istanbul ignore next */ []));
861
+ /** Actions placed on the right side of the action bar. */
862
+ this.rightActions = computed(() => this.allActions().filter(action => action.placement() === 'right'), ...(ngDevMode ? [{ debugName: "rightActions" }] : /* istanbul ignore next */ []));
863
+ /** Actions collected into the overflow (`more`) dropdown on the right side. */
864
+ this.moreActions = computed(() => this.allActions().filter(action => action.placement() === 'more'), ...(ngDevMode ? [{ debugName: "moreActions" }] : /* istanbul ignore next */ []));
537
865
  /**
538
866
  * Reference to the scroll container for the chat messages.
539
867
  */
540
868
  this.scrollContainer = viewChild('chatScrollContainer', ...(ngDevMode ? [{ debugName: "scrollContainer" }] : /* istanbul ignore next */ []));
869
+ this.chatInput = viewChild('chatInput', ...(ngDevMode ? [{ debugName: "chatInput" }] : /* istanbul ignore next */ []));
541
870
  this.componentId = `chat-${crypto.randomUUID()}`;
871
+ /** Whether the speech-to-text button is currently listening; swaps the input placeholder. */
872
+ this.isListeningForSpeech = signal(false, ...(ngDevMode ? [{ debugName: "isListeningForSpeech" }] : /* istanbul ignore next */ []));
542
873
  this._config = {
543
874
  headline: gettext('Welcome!'),
544
875
  welcomeText: '',
545
876
  welcomePosition: 'top',
546
877
  title: gettext('What can I help you with?'),
547
878
  placeholder: gettext('Type your message here...'),
879
+ listeningPlaceholder: gettext('Listening…'),
548
880
  scrollbarOnlyOnHover: false,
549
881
  sendButtonText: gettext('Send'),
550
882
  cancelButtonText: gettext('Cancel'),
@@ -602,8 +934,15 @@ class AiChatComponent {
602
934
  cancel() {
603
935
  this.onCancel.emit();
604
936
  }
937
+ /**
938
+ * Moves keyboard focus to the prompt input. Call this after removing or replacing messages, so
939
+ * that focus is not lost with the element the user was on.
940
+ */
941
+ focusInput() {
942
+ this.chatInput()?.nativeElement.focus();
943
+ }
605
944
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: AiChatComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
606
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: AiChatComponent, isStandalone: true, selector: "c8y-ai-chat", inputs: { isLoading: "isLoading", disabled: "disabled", prompt: "prompt", suggestionsTemplate: "suggestionsTemplate", welcomeTemplate: "welcomeTemplate", cumulativeUsage: "cumulativeUsage", config: "config" }, outputs: { onMessage: "onMessage", onCancel: "onCancel" }, host: { classAttribute: "d-contents" }, queries: [{ propertyName: "messages", predicate: AiChatMessageComponent, isSignal: true }], viewQueries: [{ propertyName: "scrollContainer", first: true, predicate: ["chatScrollContainer"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"d-col fit-h fit-w flex-grow min-height-0\"\n [attr.aria-label]=\"config.headline | translate\"\n role=\"region\"\n>\n @if (messages().length > 0) {\n <div\n [attr.aria-label]=\"'Chat conversation' | translate\"\n aria-live=\"polite\"\n aria-atomic=\"false\"\n role=\"log\"\n #chatScrollContainer\n [ngClass]=\"{\n 'inner-scroll': true,\n 'd-col-reverse': true,\n 'scrollbar-only-on-hover': config.scrollbarOnlyOnHover,\n 'flex-grow': true,\n 'min-height-0': true,\n 'bg-level-0': true\n }\"\n >\n <div class=\"d-col p-l-16 p-r-16\">\n <ng-content select=\"[slot='before-messages']\"></ng-content>\n <ng-content select=\"c8y-ai-chat-message\"></ng-content>\n <ng-content select=\"[slot='after-messages']\"></ng-content>\n </div>\n </div>\n }\n <div\n [ngClass]=\"{\n 'd-col fit-h': messages().length === 0\n }\"\n >\n @if (messages().length === 0) {\n <div\n class=\"p-24 d-col fit-h inner-scroll\"\n aria-live=\"polite\"\n role=\"status\"\n [ngClass]=\"{\n 'j-c-start': config.welcomePosition === 'top',\n 'j-c-center': config.welcomePosition === 'center',\n 'j-c-end': config.welcomePosition === 'bottom'\n }\"\n >\n <h4 class=\"m-b-16 text-medium\">{{ config.headline | translate }}</h4>\n @if (config.title.length > 0) {\n <p class=\"p-b-8 text-balance\">{{ config.title | translate }}</p>\n }\n <div class=\"text-balance chat-message min-height-0\">\n <div\n class=\"text-muted\"\n [innerHTML]=\"config.welcomeText | translate | markdownToHtml | async\"\n ></div>\n @if (welcomeTemplate) {\n <ng-container *ngTemplateOutlet=\"welcomeTemplate\"></ng-container>\n }\n </div>\n </div>\n }\n <div\n class=\"chat-input\"\n [class.bg-level-1]=\"config.appearance !== 'flat'\"\n >\n <!-- For simple cases allow ng-content projection; however this doesn't seem to work with dynamic \n suggestion lists from a signal (e.g. returned from the AI) so also support `suggestionsTemplate` for that. \n -->\n @if (!isLoading) {\n <div\n [class]=\"\n 'd-flex inner-scroll gap-8 p-l-16 p-r-16 p-b-8 ' +\n (config.suggestionsLayout === 'vertical' ? 'flex-wrap' : 'a-i-center')\n \"\n >\n <ng-content select=\"c8y-ai-chat-suggestion\"></ng-content>\n\n @if (suggestionsTemplate) {\n <ng-container *ngTemplateOutlet=\"suggestionsTemplate\"></ng-container>\n }\n </div>\n }\n <div class=\"chat-input-group\">\n <label\n class=\"sr-only\"\n for=\"chat-input-{{ componentId }}\"\n >\n {{ config.placeholder | translate }}\n </label>\n <textarea\n class=\"form-control no-resize\"\n [class.text-muted]=\"isLoading\"\n style=\"max-height: 200px !important\"\n [attr.aria-label]=\"config.placeholder | translate\"\n id=\"chat-input-{{ componentId }}\"\n placeholder=\"{{ config.placeholder | translate }}\"\n [attr.aria-describedby]=\"config.disclaimerText ? 'chat-disclaimer-' + componentId : null\"\n [attr.aria-busy]=\"isLoading\"\n [(ngModel)]=\"prompt\"\n (keydown.enter)=\"!isLoading && sendMessage($event)\"\n [disabled]=\"disabled\"\n c8y-textarea-autoresize\n ></textarea>\n <div class=\"chat-input-group-btn\">\n @if (!isLoading) {\n <button\n class=\"btn btn-dot\"\n [attr.title]=\"config.sendButtonText || '' | translate\"\n [attr.aria-label]=\"config.sendButtonText || '' | translate\"\n type=\"button\"\n (click)=\"sendMessage($event)\"\n [disabled]=\"disabled || prompt.trim().length === 0\"\n >\n <i [c8yIcon]=\"config.userInterfaceIcons.send || 'arrow-circle-right'\"></i>\n </button>\n } @else {\n <button\n class=\"btn btn-dot btn-dot--danger\"\n [attr.title]=\"config.cancelButtonText || '' | translate\"\n [attr.aria-label]=\"config.cancelButtonText || '' | translate\"\n type=\"button\"\n (click)=\"cancel()\"\n >\n <i [c8yIcon]=\"config.userInterfaceIcons.cancel || 'stop'\"></i>\n </button>\n }\n </div>\n </div>\n <div class=\"d-flex\">\n @if (config.disclaimerText) {\n <div\n class=\"text-muted m-b-8 text-10 p-l-16\"\n id=\"chat-disclaimer-{{ componentId }}\"\n role=\"note\"\n >\n {{ config.disclaimerText | translate }}\n </div>\n }\n @if (cumulativeUsage) {\n <span\n class=\"tag tag--info m-l-auto\"\n [tooltip]=\"\n 'Input tokens: {{ input }}\\nOutput tokens: {{ output }}'\n | translate\n : {\n input: cumulativeUsage?.inputTokens || 0 | c8yNumber: 'floor' : '1.0-0',\n output: cumulativeUsage?.outputTokens || 0 | c8yNumber: 'floor' : '1.0-0'\n }\n \"\n >\n {{ 'Tokens used: {{total}}' | translate: { total: (cumulativeUsage?.totalTokens || 0) |\n c8yNumber : 'floor':'1.0-0' } }}\n </span>\n }\n </div>\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.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$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: TextareaAutoresizeDirective, selector: "[c8y-textarea-autoresize]" }, { kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i1$1.TooltipDirective, selector: "[tooltip], [tooltipHtml]", inputs: ["adaptivePosition", "tooltip", "placement", "triggers", "container", "containerClass", "boundariesElement", "isOpen", "isDisabled", "delay", "tooltipHtml", "tooltipPlacement", "tooltipIsOpen", "tooltipEnable", "tooltipAppendToBody", "tooltipAnimation", "tooltipClass", "tooltipContext", "tooltipPopupDelay", "tooltipFadeDuration", "tooltipTrigger"], outputs: ["tooltipChange", "onShown", "onHidden", "tooltipStateChanged"], exportAs: ["bs-tooltip"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: MarkdownToHtmlPipe, name: "markdownToHtml" }, { kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "pipe", type: NumberPipe, name: "c8yNumber" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
945
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: AiChatComponent, isStandalone: true, selector: "c8y-ai-chat", inputs: { isLoading: { classPropertyName: "isLoading", publicName: "isLoading", isSignal: false, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: false, isRequired: false, transformFunction: null }, prompt: { classPropertyName: "prompt", publicName: "prompt", isSignal: false, isRequired: false, transformFunction: null }, suggestionsTemplate: { classPropertyName: "suggestionsTemplate", publicName: "suggestionsTemplate", isSignal: false, isRequired: false, transformFunction: null }, welcomeTemplate: { classPropertyName: "welcomeTemplate", publicName: "welcomeTemplate", isSignal: false, isRequired: false, transformFunction: null }, cumulativeUsage: { classPropertyName: "cumulativeUsage", publicName: "cumulativeUsage", isSignal: false, isRequired: false, transformFunction: null }, actions: { classPropertyName: "actions", publicName: "actions", isSignal: true, isRequired: false, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: false, isRequired: false, transformFunction: null } }, outputs: { onMessage: "onMessage", onCancel: "onCancel" }, host: { classAttribute: "d-contents" }, queries: [{ propertyName: "messages", predicate: AiChatMessageComponent, isSignal: true }, { propertyName: "projectedActions", predicate: AiChatActionComponent, isSignal: true }], viewQueries: [{ propertyName: "scrollContainer", first: true, predicate: ["chatScrollContainer"], descendants: true, isSignal: true }, { propertyName: "chatInput", first: true, predicate: ["chatInput"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"d-col fit-w flex-grow min-height-0\"\n [attr.aria-label]=\"config.headline | translate\"\n role=\"region\"\n>\n <!-- Streamed tokens are withheld while the log is busy, so announce start and end here. -->\n <div\n class=\"sr-only\"\n aria-live=\"polite\"\n role=\"status\"\n >\n {{ statusAnnouncement() | translate }}\n </div>\n @if (messages().length > 0) {\n <div\n [attr.aria-label]=\"'Chat conversation' | translate\"\n aria-live=\"polite\"\n aria-atomic=\"false\"\n [attr.aria-busy]=\"isLoading\"\n role=\"log\"\n #chatScrollContainer\n [ngClass]=\"{\n 'inner-scroll': true,\n 'd-col-reverse': true,\n 'scrollbar-only-on-hover': config.scrollbarOnlyOnHover,\n 'flex-grow': true,\n 'min-height-0': true,\n 'bg-level-0': true\n }\"\n >\n <div class=\"d-col p-l-16 p-r-16\">\n <ng-content select=\"[slot='before-messages']\"></ng-content>\n <ng-content select=\"c8y-ai-chat-message\"></ng-content>\n <ng-content select=\"[slot='after-messages']\"></ng-content>\n </div>\n </div>\n }\n <div\n [ngClass]=\"{\n 'd-col fit-h': messages().length === 0\n }\"\n >\n @if (messages().length === 0) {\n <div\n class=\"p-24 d-col fit-h inner-scroll\"\n aria-live=\"polite\"\n role=\"status\"\n [ngClass]=\"{\n 'j-c-start': config.welcomePosition === 'top',\n 'j-c-center': config.welcomePosition === 'center',\n 'j-c-end': config.welcomePosition === 'bottom'\n }\"\n >\n <h4 class=\"m-b-16 text-medium\">{{ config.headline | translate }}</h4>\n @if (config.title.length > 0) {\n <p class=\"p-b-8 text-pretty\">{{ config.title | translate }}</p>\n }\n <div class=\"text-pretty chat-message min-height-0\">\n <div\n class=\"text-muted\"\n [innerHTML]=\"config.welcomeText | translate | markdownToHtml | async\"\n ></div>\n @if (welcomeTemplate) {\n <ng-container *ngTemplateOutlet=\"welcomeTemplate\"></ng-container>\n }\n </div>\n </div>\n }\n <div\n class=\"chat-input\"\n [class.bg-level-1]=\"config.appearance !== 'flat'\"\n >\n <!-- For simple cases allow ng-content projection; however this doesn't seem to work with dynamic \n suggestion lists from a signal (e.g. returned from the AI) so also support `suggestionsTemplate` for that. \n -->\n @if (!isLoading) {\n <div\n [class]=\"\n 'd-flex inner-scroll gap-8 p-l-16 p-r-16 p-b-16 ' +\n (config.suggestionsLayout === 'vertical' ? 'flex-wrap' : 'a-i-center')\n \"\n [attr.aria-label]=\"'Suggested prompts' | translate\"\n role=\"group\"\n >\n <ng-content select=\"c8y-ai-chat-suggestion\"></ng-content>\n\n @if (suggestionsTemplate) {\n <ng-container *ngTemplateOutlet=\"suggestionsTemplate\"></ng-container>\n }\n </div>\n }\n <div class=\"chat-input-group\">\n <label\n class=\"sr-only\"\n for=\"chat-input-{{ componentId }}\"\n >\n {{ config.placeholder | translate }}\n </label>\n <textarea\n class=\"form-control no-resize\"\n [class.text-muted]=\"isLoading\"\n style=\"max-height: 200px !important\"\n [attr.aria-label]=\"config.placeholder | translate\"\n id=\"chat-input-{{ componentId }}\"\n placeholder=\"{{\n (isListeningForSpeech() && config.listeningPlaceholder\n ? config.listeningPlaceholder\n : config.placeholder\n ) | translate\n }}\"\n [attr.aria-describedby]=\"config.disclaimerText ? 'chat-disclaimer-' + componentId : null\"\n [attr.aria-busy]=\"isLoading\"\n [(ngModel)]=\"prompt\"\n (keydown.enter)=\"!isLoading && sendMessage($event)\"\n [disabled]=\"disabled\"\n c8y-textarea-autoresize\n #chatInput\n ></textarea>\n <div class=\"chat-input-group-btn\">\n <ng-content select=\"[slot='input-actions']\"></ng-content>\n @if (config.enableSpeechToText !== false) {\n <c8y-ai-chat-speech-to-text\n [(prompt)]=\"prompt\"\n [disabled]=\"disabled || isLoading\"\n (listeningChange)=\"isListeningForSpeech.set($event)\"\n ></c8y-ai-chat-speech-to-text>\n }\n </div>\n </div>\n <div class=\"chat-action-bar d-flex a-i-center gap-4 bg-level-0\">\n <div class=\"d-flex a-i-center gap-4\">\n @if (moreActions().length > 0) {\n <div\n class=\"dropdown\"\n dropdown\n #moreChatActions=\"bs-dropdown\"\n [cdkTrapFocus]=\"moreChatActions.isOpen\"\n [dropup]=\"true\"\n >\n <button\n class=\"btn btn-dot\"\n [attr.title]=\"'More actions' | translate\"\n [attr.aria-label]=\"'More actions' | translate\"\n aria-haspopup=\"true\"\n type=\"button\"\n dropdownToggle\n >\n <i [c8yIcon]=\"'ellipsis-h'\"></i>\n </button>\n <ul\n class=\"dropdown-menu dropup\"\n role=\"menu\"\n *dropdownMenu\n >\n @for (action of moreActions(); track action) {\n <li>\n <ng-container *ngTemplateOutlet=\"action.template()\"></ng-container>\n </li>\n }\n </ul>\n </div>\n }\n @for (action of leftActions(); track action) {\n <ng-container *ngTemplateOutlet=\"action.template()\"></ng-container>\n }\n </div>\n <div class=\"d-flex a-i-center gap-4 m-l-auto\">\n @for (action of rightActions(); track action) {\n <ng-container *ngTemplateOutlet=\"action.template()\"></ng-container>\n }\n @if (!isLoading) {\n <button\n class=\"btn btn-dot\"\n [attr.title]=\"config.sendButtonText || '' | translate\"\n [attr.aria-label]=\"config.sendButtonText || '' | translate\"\n type=\"button\"\n (click)=\"sendMessage($event)\"\n [disabled]=\"disabled || prompt.trim().length === 0\"\n >\n <i [c8yIcon]=\"config.userInterfaceIcons.send || 'arrow-circle-right'\"></i>\n </button>\n } @else {\n <button\n class=\"btn btn-dot btn-dot--danger\"\n [attr.title]=\"config.cancelButtonText || '' | translate\"\n [attr.aria-label]=\"config.cancelButtonText || '' | translate\"\n type=\"button\"\n (click)=\"cancel()\"\n >\n <i [c8yIcon]=\"config.userInterfaceIcons.cancel || 'stop'\"></i>\n </button>\n }\n </div>\n </div>\n <div class=\"d-flex p-t-8 p-l-16 p-r-16 p-b-8 gap-8\">\n @if (config.disclaimerText) {\n <div\n class=\"text-muted text-10\"\n id=\"chat-disclaimer-{{ componentId }}\"\n role=\"note\"\n >\n {{ config.disclaimerText | translate }}\n </div>\n }\n @if (cumulativeUsage) {\n <button\n class=\"btn-clean m-l-auto d-flex a-s-center\"\n [attr.aria-label]=\"\n 'Input tokens: {{ input }}\\nOutput tokens: {{ output }}'\n | translate\n : {\n input: cumulativeUsage?.inputTokens || 0 | c8yNumber: 'floor' : '1.0-0',\n output: cumulativeUsage?.outputTokens || 0 | c8yNumber: 'floor' : '1.0-0'\n }\n \"\n tabindex=\"0\"\n [tooltip]=\"\n 'Input tokens: {{ input }}\\nOutput tokens: {{ output }}'\n | translate\n : {\n input: cumulativeUsage?.inputTokens || 0 | c8yNumber: 'floor' : '1.0-0',\n output: cumulativeUsage?.outputTokens || 0 | c8yNumber: 'floor' : '1.0-0'\n }\n \"\n >\n <span class=\"tag tag--info a-s-center\">\n {{ 'Tokens used: {{total}}' | translate: { total: (cumulativeUsage?.totalTokens || 0)\n | c8yNumber : 'floor':'1.0-0' } }}\n </span>\n </button>\n }\n </div>\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.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$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: TextareaAutoresizeDirective, selector: "[c8y-textarea-autoresize]" }, { kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i1$1.TooltipDirective, selector: "[tooltip], [tooltipHtml]", inputs: ["adaptivePosition", "tooltip", "placement", "triggers", "container", "containerClass", "boundariesElement", "isOpen", "isDisabled", "delay", "tooltipHtml", "tooltipPlacement", "tooltipIsOpen", "tooltipEnable", "tooltipAppendToBody", "tooltipAnimation", "tooltipClass", "tooltipContext", "tooltipPopupDelay", "tooltipFadeDuration", "tooltipTrigger"], outputs: ["tooltipChange", "onShown", "onHidden", "tooltipStateChanged"], exportAs: ["bs-tooltip"] }, { kind: "ngmodule", type: BsDropdownModule }, { kind: "directive", type: i2.BsDropdownMenuDirective, selector: "[bsDropdownMenu],[dropdownMenu]", exportAs: ["bs-dropdown-menu"] }, { kind: "directive", type: i2.BsDropdownToggleDirective, selector: "[bsDropdownToggle],[dropdownToggle]", exportAs: ["bs-dropdown-toggle"] }, { kind: "directive", type: i2.BsDropdownDirective, selector: "[bsDropdown], [dropdown]", inputs: ["placement", "triggers", "container", "dropup", "autoClose", "isAnimated", "insideClick", "isDisabled", "isOpen"], outputs: ["isOpenChange", "onShown", "onHidden"], exportAs: ["bs-dropdown"] }, { kind: "component", type: AiChatSpeechToTextComponent, selector: "c8y-ai-chat-speech-to-text", inputs: ["disabled", "lang", "prompt"], outputs: ["promptChange", "listeningChange"] }, { kind: "directive", type: CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: MarkdownToHtmlPipe, name: "markdownToHtml" }, { kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "pipe", type: NumberPipe, name: "c8yNumber" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
607
946
  }
608
947
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: AiChatComponent, decorators: [{
609
948
  type: Component,
@@ -617,8 +956,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
617
956
  MarkdownToHtmlPipe,
618
957
  AsyncPipe,
619
958
  TooltipModule,
620
- NumberPipe
621
- ], standalone: true, host: { class: 'd-contents' }, template: "<div\n class=\"d-col fit-h fit-w flex-grow min-height-0\"\n [attr.aria-label]=\"config.headline | translate\"\n role=\"region\"\n>\n @if (messages().length > 0) {\n <div\n [attr.aria-label]=\"'Chat conversation' | translate\"\n aria-live=\"polite\"\n aria-atomic=\"false\"\n role=\"log\"\n #chatScrollContainer\n [ngClass]=\"{\n 'inner-scroll': true,\n 'd-col-reverse': true,\n 'scrollbar-only-on-hover': config.scrollbarOnlyOnHover,\n 'flex-grow': true,\n 'min-height-0': true,\n 'bg-level-0': true\n }\"\n >\n <div class=\"d-col p-l-16 p-r-16\">\n <ng-content select=\"[slot='before-messages']\"></ng-content>\n <ng-content select=\"c8y-ai-chat-message\"></ng-content>\n <ng-content select=\"[slot='after-messages']\"></ng-content>\n </div>\n </div>\n }\n <div\n [ngClass]=\"{\n 'd-col fit-h': messages().length === 0\n }\"\n >\n @if (messages().length === 0) {\n <div\n class=\"p-24 d-col fit-h inner-scroll\"\n aria-live=\"polite\"\n role=\"status\"\n [ngClass]=\"{\n 'j-c-start': config.welcomePosition === 'top',\n 'j-c-center': config.welcomePosition === 'center',\n 'j-c-end': config.welcomePosition === 'bottom'\n }\"\n >\n <h4 class=\"m-b-16 text-medium\">{{ config.headline | translate }}</h4>\n @if (config.title.length > 0) {\n <p class=\"p-b-8 text-balance\">{{ config.title | translate }}</p>\n }\n <div class=\"text-balance chat-message min-height-0\">\n <div\n class=\"text-muted\"\n [innerHTML]=\"config.welcomeText | translate | markdownToHtml | async\"\n ></div>\n @if (welcomeTemplate) {\n <ng-container *ngTemplateOutlet=\"welcomeTemplate\"></ng-container>\n }\n </div>\n </div>\n }\n <div\n class=\"chat-input\"\n [class.bg-level-1]=\"config.appearance !== 'flat'\"\n >\n <!-- For simple cases allow ng-content projection; however this doesn't seem to work with dynamic \n suggestion lists from a signal (e.g. returned from the AI) so also support `suggestionsTemplate` for that. \n -->\n @if (!isLoading) {\n <div\n [class]=\"\n 'd-flex inner-scroll gap-8 p-l-16 p-r-16 p-b-8 ' +\n (config.suggestionsLayout === 'vertical' ? 'flex-wrap' : 'a-i-center')\n \"\n >\n <ng-content select=\"c8y-ai-chat-suggestion\"></ng-content>\n\n @if (suggestionsTemplate) {\n <ng-container *ngTemplateOutlet=\"suggestionsTemplate\"></ng-container>\n }\n </div>\n }\n <div class=\"chat-input-group\">\n <label\n class=\"sr-only\"\n for=\"chat-input-{{ componentId }}\"\n >\n {{ config.placeholder | translate }}\n </label>\n <textarea\n class=\"form-control no-resize\"\n [class.text-muted]=\"isLoading\"\n style=\"max-height: 200px !important\"\n [attr.aria-label]=\"config.placeholder | translate\"\n id=\"chat-input-{{ componentId }}\"\n placeholder=\"{{ config.placeholder | translate }}\"\n [attr.aria-describedby]=\"config.disclaimerText ? 'chat-disclaimer-' + componentId : null\"\n [attr.aria-busy]=\"isLoading\"\n [(ngModel)]=\"prompt\"\n (keydown.enter)=\"!isLoading && sendMessage($event)\"\n [disabled]=\"disabled\"\n c8y-textarea-autoresize\n ></textarea>\n <div class=\"chat-input-group-btn\">\n @if (!isLoading) {\n <button\n class=\"btn btn-dot\"\n [attr.title]=\"config.sendButtonText || '' | translate\"\n [attr.aria-label]=\"config.sendButtonText || '' | translate\"\n type=\"button\"\n (click)=\"sendMessage($event)\"\n [disabled]=\"disabled || prompt.trim().length === 0\"\n >\n <i [c8yIcon]=\"config.userInterfaceIcons.send || 'arrow-circle-right'\"></i>\n </button>\n } @else {\n <button\n class=\"btn btn-dot btn-dot--danger\"\n [attr.title]=\"config.cancelButtonText || '' | translate\"\n [attr.aria-label]=\"config.cancelButtonText || '' | translate\"\n type=\"button\"\n (click)=\"cancel()\"\n >\n <i [c8yIcon]=\"config.userInterfaceIcons.cancel || 'stop'\"></i>\n </button>\n }\n </div>\n </div>\n <div class=\"d-flex\">\n @if (config.disclaimerText) {\n <div\n class=\"text-muted m-b-8 text-10 p-l-16\"\n id=\"chat-disclaimer-{{ componentId }}\"\n role=\"note\"\n >\n {{ config.disclaimerText | translate }}\n </div>\n }\n @if (cumulativeUsage) {\n <span\n class=\"tag tag--info m-l-auto\"\n [tooltip]=\"\n 'Input tokens: {{ input }}\\nOutput tokens: {{ output }}'\n | translate\n : {\n input: cumulativeUsage?.inputTokens || 0 | c8yNumber: 'floor' : '1.0-0',\n output: cumulativeUsage?.outputTokens || 0 | c8yNumber: 'floor' : '1.0-0'\n }\n \"\n >\n {{ 'Tokens used: {{total}}' | translate: { total: (cumulativeUsage?.totalTokens || 0) |\n c8yNumber : 'floor':'1.0-0' } }}\n </span>\n }\n </div>\n </div>\n </div>\n</div>\n" }]
959
+ BsDropdownModule,
960
+ NumberPipe,
961
+ AiChatSpeechToTextComponent,
962
+ CdkTrapFocus
963
+ ], standalone: true, host: { class: 'd-contents' }, template: "<div\n class=\"d-col fit-w flex-grow min-height-0\"\n [attr.aria-label]=\"config.headline | translate\"\n role=\"region\"\n>\n <!-- Streamed tokens are withheld while the log is busy, so announce start and end here. -->\n <div\n class=\"sr-only\"\n aria-live=\"polite\"\n role=\"status\"\n >\n {{ statusAnnouncement() | translate }}\n </div>\n @if (messages().length > 0) {\n <div\n [attr.aria-label]=\"'Chat conversation' | translate\"\n aria-live=\"polite\"\n aria-atomic=\"false\"\n [attr.aria-busy]=\"isLoading\"\n role=\"log\"\n #chatScrollContainer\n [ngClass]=\"{\n 'inner-scroll': true,\n 'd-col-reverse': true,\n 'scrollbar-only-on-hover': config.scrollbarOnlyOnHover,\n 'flex-grow': true,\n 'min-height-0': true,\n 'bg-level-0': true\n }\"\n >\n <div class=\"d-col p-l-16 p-r-16\">\n <ng-content select=\"[slot='before-messages']\"></ng-content>\n <ng-content select=\"c8y-ai-chat-message\"></ng-content>\n <ng-content select=\"[slot='after-messages']\"></ng-content>\n </div>\n </div>\n }\n <div\n [ngClass]=\"{\n 'd-col fit-h': messages().length === 0\n }\"\n >\n @if (messages().length === 0) {\n <div\n class=\"p-24 d-col fit-h inner-scroll\"\n aria-live=\"polite\"\n role=\"status\"\n [ngClass]=\"{\n 'j-c-start': config.welcomePosition === 'top',\n 'j-c-center': config.welcomePosition === 'center',\n 'j-c-end': config.welcomePosition === 'bottom'\n }\"\n >\n <h4 class=\"m-b-16 text-medium\">{{ config.headline | translate }}</h4>\n @if (config.title.length > 0) {\n <p class=\"p-b-8 text-pretty\">{{ config.title | translate }}</p>\n }\n <div class=\"text-pretty chat-message min-height-0\">\n <div\n class=\"text-muted\"\n [innerHTML]=\"config.welcomeText | translate | markdownToHtml | async\"\n ></div>\n @if (welcomeTemplate) {\n <ng-container *ngTemplateOutlet=\"welcomeTemplate\"></ng-container>\n }\n </div>\n </div>\n }\n <div\n class=\"chat-input\"\n [class.bg-level-1]=\"config.appearance !== 'flat'\"\n >\n <!-- For simple cases allow ng-content projection; however this doesn't seem to work with dynamic \n suggestion lists from a signal (e.g. returned from the AI) so also support `suggestionsTemplate` for that. \n -->\n @if (!isLoading) {\n <div\n [class]=\"\n 'd-flex inner-scroll gap-8 p-l-16 p-r-16 p-b-16 ' +\n (config.suggestionsLayout === 'vertical' ? 'flex-wrap' : 'a-i-center')\n \"\n [attr.aria-label]=\"'Suggested prompts' | translate\"\n role=\"group\"\n >\n <ng-content select=\"c8y-ai-chat-suggestion\"></ng-content>\n\n @if (suggestionsTemplate) {\n <ng-container *ngTemplateOutlet=\"suggestionsTemplate\"></ng-container>\n }\n </div>\n }\n <div class=\"chat-input-group\">\n <label\n class=\"sr-only\"\n for=\"chat-input-{{ componentId }}\"\n >\n {{ config.placeholder | translate }}\n </label>\n <textarea\n class=\"form-control no-resize\"\n [class.text-muted]=\"isLoading\"\n style=\"max-height: 200px !important\"\n [attr.aria-label]=\"config.placeholder | translate\"\n id=\"chat-input-{{ componentId }}\"\n placeholder=\"{{\n (isListeningForSpeech() && config.listeningPlaceholder\n ? config.listeningPlaceholder\n : config.placeholder\n ) | translate\n }}\"\n [attr.aria-describedby]=\"config.disclaimerText ? 'chat-disclaimer-' + componentId : null\"\n [attr.aria-busy]=\"isLoading\"\n [(ngModel)]=\"prompt\"\n (keydown.enter)=\"!isLoading && sendMessage($event)\"\n [disabled]=\"disabled\"\n c8y-textarea-autoresize\n #chatInput\n ></textarea>\n <div class=\"chat-input-group-btn\">\n <ng-content select=\"[slot='input-actions']\"></ng-content>\n @if (config.enableSpeechToText !== false) {\n <c8y-ai-chat-speech-to-text\n [(prompt)]=\"prompt\"\n [disabled]=\"disabled || isLoading\"\n (listeningChange)=\"isListeningForSpeech.set($event)\"\n ></c8y-ai-chat-speech-to-text>\n }\n </div>\n </div>\n <div class=\"chat-action-bar d-flex a-i-center gap-4 bg-level-0\">\n <div class=\"d-flex a-i-center gap-4\">\n @if (moreActions().length > 0) {\n <div\n class=\"dropdown\"\n dropdown\n #moreChatActions=\"bs-dropdown\"\n [cdkTrapFocus]=\"moreChatActions.isOpen\"\n [dropup]=\"true\"\n >\n <button\n class=\"btn btn-dot\"\n [attr.title]=\"'More actions' | translate\"\n [attr.aria-label]=\"'More actions' | translate\"\n aria-haspopup=\"true\"\n type=\"button\"\n dropdownToggle\n >\n <i [c8yIcon]=\"'ellipsis-h'\"></i>\n </button>\n <ul\n class=\"dropdown-menu dropup\"\n role=\"menu\"\n *dropdownMenu\n >\n @for (action of moreActions(); track action) {\n <li>\n <ng-container *ngTemplateOutlet=\"action.template()\"></ng-container>\n </li>\n }\n </ul>\n </div>\n }\n @for (action of leftActions(); track action) {\n <ng-container *ngTemplateOutlet=\"action.template()\"></ng-container>\n }\n </div>\n <div class=\"d-flex a-i-center gap-4 m-l-auto\">\n @for (action of rightActions(); track action) {\n <ng-container *ngTemplateOutlet=\"action.template()\"></ng-container>\n }\n @if (!isLoading) {\n <button\n class=\"btn btn-dot\"\n [attr.title]=\"config.sendButtonText || '' | translate\"\n [attr.aria-label]=\"config.sendButtonText || '' | translate\"\n type=\"button\"\n (click)=\"sendMessage($event)\"\n [disabled]=\"disabled || prompt.trim().length === 0\"\n >\n <i [c8yIcon]=\"config.userInterfaceIcons.send || 'arrow-circle-right'\"></i>\n </button>\n } @else {\n <button\n class=\"btn btn-dot btn-dot--danger\"\n [attr.title]=\"config.cancelButtonText || '' | translate\"\n [attr.aria-label]=\"config.cancelButtonText || '' | translate\"\n type=\"button\"\n (click)=\"cancel()\"\n >\n <i [c8yIcon]=\"config.userInterfaceIcons.cancel || 'stop'\"></i>\n </button>\n }\n </div>\n </div>\n <div class=\"d-flex p-t-8 p-l-16 p-r-16 p-b-8 gap-8\">\n @if (config.disclaimerText) {\n <div\n class=\"text-muted text-10\"\n id=\"chat-disclaimer-{{ componentId }}\"\n role=\"note\"\n >\n {{ config.disclaimerText | translate }}\n </div>\n }\n @if (cumulativeUsage) {\n <button\n class=\"btn-clean m-l-auto d-flex a-s-center\"\n [attr.aria-label]=\"\n 'Input tokens: {{ input }}\\nOutput tokens: {{ output }}'\n | translate\n : {\n input: cumulativeUsage?.inputTokens || 0 | c8yNumber: 'floor' : '1.0-0',\n output: cumulativeUsage?.outputTokens || 0 | c8yNumber: 'floor' : '1.0-0'\n }\n \"\n tabindex=\"0\"\n [tooltip]=\"\n 'Input tokens: {{ input }}\\nOutput tokens: {{ output }}'\n | translate\n : {\n input: cumulativeUsage?.inputTokens || 0 | c8yNumber: 'floor' : '1.0-0',\n output: cumulativeUsage?.outputTokens || 0 | c8yNumber: 'floor' : '1.0-0'\n }\n \"\n >\n <span class=\"tag tag--info a-s-center\">\n {{ 'Tokens used: {{total}}' | translate: { total: (cumulativeUsage?.totalTokens || 0)\n | c8yNumber : 'floor':'1.0-0' } }}\n </span>\n </button>\n }\n </div>\n </div>\n </div>\n</div>\n" }]
622
964
  }], ctorParameters: () => [], propDecorators: { isLoading: [{
623
965
  type: Input
624
966
  }], disabled: [{
@@ -635,7 +977,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
635
977
  type: Output
636
978
  }], onCancel: [{
637
979
  type: Output
638
- }], messages: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => AiChatMessageComponent), { isSignal: true }] }], scrollContainer: [{ type: i0.ViewChild, args: ['chatScrollContainer', { isSignal: true }] }], config: [{
980
+ }], messages: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => AiChatMessageComponent), { isSignal: true }] }], projectedActions: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => AiChatActionComponent), { isSignal: true }] }], actions: [{ type: i0.Input, args: [{ isSignal: true, alias: "actions", required: false }] }], scrollContainer: [{ type: i0.ViewChild, args: ['chatScrollContainer', { isSignal: true }] }], chatInput: [{ type: i0.ViewChild, args: ['chatInput', { isSignal: true }] }], config: [{
639
981
  type: Input
640
982
  }] } });
641
983
 
@@ -643,5 +985,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
643
985
  * Generated bundle index. Do not edit.
644
986
  */
645
987
 
646
- export { AiChatAssistantMessageComponent, AiChatAssistantPartComponent, AiChatComponent, AiChatMessageActionComponent, AiChatMessageComponent, AiChatSuggestionComponent, AiChatToolCallComponent };
988
+ export { AiChatActionComponent, AiChatAssistantMessageComponent, AiChatAssistantPartComponent, AiChatComponent, AiChatMessageActionComponent, AiChatMessageComponent, AiChatSpeechToTextComponent, AiChatSuggestionComponent, AiChatToolCallComponent, SpeechRecognitionService };
647
989
  //# sourceMappingURL=c8y-ngx-components-ai-ai-chat.mjs.map