@c8y/ngx-components 1024.15.13 → 1024.16.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/c8y-ngx-components-ai-agent-chat.mjs +303 -63
- package/fesm2022/c8y-ngx-components-ai-agent-chat.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-ai-agents-html.mjs +9 -0
- package/fesm2022/c8y-ngx-components-ai-agents-html.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-ai-agents-scada.mjs +4 -0
- package/fesm2022/c8y-ngx-components-ai-agents-scada.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-ai-ai-chat.mjs +394 -52
- package/fesm2022/c8y-ngx-components-ai-ai-chat.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-ai.mjs +329 -19
- package/fesm2022/c8y-ngx-components-ai.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-context-dashboard.mjs +7 -1
- package/fesm2022/c8y-ngx-components-context-dashboard.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-datapoint-explorer-view.mjs +95 -76
- package/fesm2022/c8y-ngx-components-datapoint-explorer-view.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-datapoint-selector.mjs +92 -47
- package/fesm2022/c8y-ngx-components-datapoint-selector.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-echart.mjs +39 -12
- package/fesm2022/c8y-ngx-components-echart.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-time-context.mjs +13 -9
- package/fesm2022/c8y-ngx-components-time-context.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-widgets-definitions-html-widget-ai-config.mjs +3 -2
- package/fesm2022/c8y-ngx-components-widgets-definitions-html-widget-ai-config.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-widgets-implementations-asset-table.mjs +4 -6
- package/fesm2022/c8y-ngx-components-widgets-implementations-asset-table.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-widgets-implementations-scada.mjs +3 -2
- package/fesm2022/c8y-ngx-components-widgets-implementations-scada.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components.mjs +65 -14
- package/fesm2022/c8y-ngx-components.mjs.map +1 -1
- package/locales/de.po +147 -16
- package/locales/es.po +143 -11
- package/locales/fr.po +459 -324
- package/locales/ja_JP.po +143 -11
- package/locales/ko.po +143 -11
- package/locales/locales.pot +139 -10
- package/locales/nl.po +143 -11
- package/locales/pl.po +143 -11
- package/locales/pt_BR.po +143 -11
- package/locales/zh_CN.po +143 -11
- package/locales/zh_TW.po +143 -11
- package/package.json +1 -1
- package/types/c8y-ngx-components-ai-agent-chat.d.ts +111 -4
- package/types/c8y-ngx-components-ai-agent-chat.d.ts.map +1 -1
- package/types/c8y-ngx-components-ai-agents-html.d.ts.map +1 -1
- package/types/c8y-ngx-components-ai-agents-scada.d.ts.map +1 -1
- package/types/c8y-ngx-components-ai-ai-chat.d.ts +224 -10
- package/types/c8y-ngx-components-ai-ai-chat.d.ts.map +1 -1
- package/types/c8y-ngx-components-ai.d.ts +176 -6
- package/types/c8y-ngx-components-ai.d.ts.map +1 -1
- package/types/c8y-ngx-components-context-dashboard.d.ts.map +1 -1
- package/types/c8y-ngx-components-datapoint-explorer-view.d.ts +7 -2
- package/types/c8y-ngx-components-datapoint-explorer-view.d.ts.map +1 -1
- package/types/c8y-ngx-components-datapoint-selector.d.ts +63 -50
- package/types/c8y-ngx-components-datapoint-selector.d.ts.map +1 -1
- package/types/c8y-ngx-components-echart.d.ts +12 -0
- package/types/c8y-ngx-components-echart.d.ts.map +1 -1
- package/types/c8y-ngx-components-time-context.d.ts +2 -0
- package/types/c8y-ngx-components-time-context.d.ts.map +1 -1
- package/types/c8y-ngx-components-widgets-implementations-scada.d.ts.map +1 -1
- package/types/c8y-ngx-components.d.ts +10 -1
- 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,
|
|
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
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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 b-r-4\"\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
|
-
|
|
90
|
-
|
|
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'
|
|
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'
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
512
|
-
|
|
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
|
-
|
|
621
|
-
|
|
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
|