@c8y/ngx-components 1023.68.6 → 1023.70.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/ai/agent-chat/index.d.ts +235 -75
  2. package/ai/agent-chat/index.d.ts.map +1 -1
  3. package/ai/ai-chat/index.d.ts +176 -26
  4. package/ai/ai-chat/index.d.ts.map +1 -1
  5. package/ai/index.d.ts +309 -75
  6. package/ai/index.d.ts.map +1 -1
  7. package/asset-properties/index.d.ts.map +1 -1
  8. package/echart/index.d.ts +4 -0
  9. package/echart/index.d.ts.map +1 -1
  10. package/echart/models/index.d.ts +2 -0
  11. package/echart/models/index.d.ts.map +1 -1
  12. package/fesm2022/c8y-ngx-components-ai-agent-chat.mjs +680 -242
  13. package/fesm2022/c8y-ngx-components-ai-agent-chat.mjs.map +1 -1
  14. package/fesm2022/c8y-ngx-components-ai-ai-chat.mjs +343 -44
  15. package/fesm2022/c8y-ngx-components-ai-ai-chat.mjs.map +1 -1
  16. package/fesm2022/c8y-ngx-components-ai.mjs +187 -75
  17. package/fesm2022/c8y-ngx-components-ai.mjs.map +1 -1
  18. package/fesm2022/c8y-ngx-components-asset-properties.mjs +11 -5
  19. package/fesm2022/c8y-ngx-components-asset-properties.mjs.map +1 -1
  20. package/fesm2022/{c8y-ngx-components-computed-asset-properties-alarm-count-config.component-B2cy8gI7.mjs → c8y-ngx-components-computed-asset-properties-alarm-count-config.component-CPLDClTp.mjs} +3 -3
  21. package/fesm2022/{c8y-ngx-components-computed-asset-properties-alarm-count-config.component-B2cy8gI7.mjs.map → c8y-ngx-components-computed-asset-properties-alarm-count-config.component-CPLDClTp.mjs.map} +1 -1
  22. package/fesm2022/{c8y-ngx-components-computed-asset-properties-c8y-ngx-components-computed-asset-properties-BYHnA-5R.mjs → c8y-ngx-components-computed-asset-properties-c8y-ngx-components-computed-asset-properties-9be_iMQg.mjs} +30 -13
  23. package/fesm2022/c8y-ngx-components-computed-asset-properties-c8y-ngx-components-computed-asset-properties-9be_iMQg.mjs.map +1 -0
  24. package/fesm2022/{c8y-ngx-components-computed-asset-properties-configuration-snapshot-config.component-C4oL39m8.mjs → c8y-ngx-components-computed-asset-properties-configuration-snapshot-config.component-B2em01_W.mjs} +3 -3
  25. package/fesm2022/c8y-ngx-components-computed-asset-properties-configuration-snapshot-config.component-B2em01_W.mjs.map +1 -0
  26. package/fesm2022/{c8y-ngx-components-computed-asset-properties-event-count-config.component-DGwm6_C9.mjs → c8y-ngx-components-computed-asset-properties-event-count-config.component-CQuGa1RI.mjs} +3 -3
  27. package/fesm2022/c8y-ngx-components-computed-asset-properties-event-count-config.component-CQuGa1RI.mjs.map +1 -0
  28. package/fesm2022/{c8y-ngx-components-computed-asset-properties-fieldbus-item-status-config.component-BxfCjbYY.mjs → c8y-ngx-components-computed-asset-properties-fieldbus-item-status-config.component-CkmurxJv.mjs} +5 -5
  29. package/fesm2022/{c8y-ngx-components-computed-asset-properties-fieldbus-item-status-config.component-BxfCjbYY.mjs.map → c8y-ngx-components-computed-asset-properties-fieldbus-item-status-config.component-CkmurxJv.mjs.map} +1 -1
  30. package/fesm2022/c8y-ngx-components-computed-asset-properties-last-measurement-config.component-CTK9zNUh.mjs +86 -0
  31. package/fesm2022/c8y-ngx-components-computed-asset-properties-last-measurement-config.component-CTK9zNUh.mjs.map +1 -0
  32. package/fesm2022/c8y-ngx-components-computed-asset-properties.mjs +1 -1
  33. package/fesm2022/c8y-ngx-components-echart-models.mjs.map +1 -1
  34. package/fesm2022/c8y-ngx-components-echart.mjs +39 -18
  35. package/fesm2022/c8y-ngx-components-echart.mjs.map +1 -1
  36. package/fesm2022/c8y-ngx-components-widgets-definitions-html-widget-ai-config.mjs +106 -4
  37. package/fesm2022/c8y-ngx-components-widgets-definitions-html-widget-ai-config.mjs.map +1 -1
  38. package/fesm2022/c8y-ngx-components-widgets-implementations-html-widget.mjs +14 -122
  39. package/fesm2022/c8y-ngx-components-widgets-implementations-html-widget.mjs.map +1 -1
  40. package/fesm2022/c8y-ngx-components.mjs +15 -13
  41. package/fesm2022/c8y-ngx-components.mjs.map +1 -1
  42. package/index.d.ts.map +1 -1
  43. package/locales/de.po +3 -0
  44. package/locales/es.po +3 -0
  45. package/locales/fr.po +3 -0
  46. package/locales/ja_JP.po +3 -0
  47. package/locales/ko.po +3 -0
  48. package/locales/locales.pot +83 -30
  49. package/locales/nl.po +3 -0
  50. package/locales/pl.po +3 -0
  51. package/locales/pt_BR.po +3 -0
  52. package/locales/zh_CN.po +3 -0
  53. package/locales/zh_TW.po +3 -0
  54. package/package.json +1 -1
  55. package/widgets/implementations/html-widget/index.d.ts +11 -50
  56. package/widgets/implementations/html-widget/index.d.ts.map +1 -1
  57. package/fesm2022/c8y-ngx-components-computed-asset-properties-c8y-ngx-components-computed-asset-properties-BYHnA-5R.mjs.map +0 -1
  58. package/fesm2022/c8y-ngx-components-computed-asset-properties-configuration-snapshot-config.component-C4oL39m8.mjs.map +0 -1
  59. package/fesm2022/c8y-ngx-components-computed-asset-properties-event-count-config.component-DGwm6_C9.mjs.map +0 -1
  60. package/fesm2022/c8y-ngx-components-computed-asset-properties-last-measurement-config.component-C1cuxN3L.mjs +0 -92
  61. package/fesm2022/c8y-ngx-components-computed-asset-properties-last-measurement-config.component-C1cuxN3L.mjs.map +0 -1
@@ -1,20 +1,282 @@
1
1
  import * as i0 from '@angular/core';
2
- import { EventEmitter, Output, Input, Component, input, inject, computed, ContentChildren } from '@angular/core';
3
- import { IconDirective, C8yTranslatePipe, MarkdownToHtmlPipe, DatePipe, TextareaAutoresizeDirective } from '@c8y/ngx-components';
4
- import * as i1 from 'ngx-bootstrap/tooltip';
5
- import { TooltipModule } from 'ngx-bootstrap/tooltip';
2
+ import { input, signal, inject, ChangeDetectionStrategy, Component, effect, EventEmitter, Output, Input, computed, output, contentChildren, viewChild } from '@angular/core';
3
+ import { IconDirective, C8yTranslateModule, C8yTranslatePipe, MarkdownToHtmlPipe, DatePipe, TextareaAutoresizeDirective } from '@c8y/ngx-components';
6
4
  import { gettext } from '@c8y/ngx-components/gettext';
7
- import { NgClass, AsyncPipe } from '@angular/common';
8
5
  import { TranslateService } from '@ngx-translate/core';
9
- import * as i1$1 from '@angular/forms';
6
+ import * as i1 from 'ngx-bootstrap/collapse';
7
+ import { CollapseModule } from 'ngx-bootstrap/collapse';
8
+ import { NgComponentOutlet, JsonPipe, AsyncPipe, NgClass, NgTemplateOutlet } from '@angular/common';
9
+ import * as i1$1 from 'ngx-bootstrap/tooltip';
10
+ import { TooltipModule } from 'ngx-bootstrap/tooltip';
11
+ import * as i1$2 from '@angular/forms';
10
12
  import { FormsModule } from '@angular/forms';
11
13
 
14
+ class AiChatToolCallComponent {
15
+ constructor() {
16
+ /**
17
+ * The tool call part to render. This includes all information about the tool call, including the name of the tool,
18
+ * the input provided and (once available) the output from the tool.
19
+ */
20
+ this.tool = input.required(...(ngDevMode ? [{ debugName: "tool" }] : []));
21
+ /**
22
+ * Whether the tool call is still in progress.
23
+ */
24
+ this.isExecuting = input.required(...(ngDevMode ? [{ debugName: "isExecuting" }] : []));
25
+ /**
26
+ * A custom component to render the details of the tool call. If not provided, the default details section will be shown.
27
+ */
28
+ this.toolDetailsComponent = input(undefined, ...(ngDevMode ? [{ debugName: "toolDetailsComponent" }] : []));
29
+ /**
30
+ * Shows the default details section for a tool call.
31
+ */
32
+ this.showDefaultToolDetails = input(false, ...(ngDevMode ? [{ debugName: "showDefaultToolDetails" }] : []));
33
+ /**
34
+ * The label to show while the tool is executing. If not provided, a default label will be generated based on the tool name.
35
+ */
36
+ this.executingLabel = input(undefined, ...(ngDevMode ? [{ debugName: "executingLabel" }] : []));
37
+ /**
38
+ * The label to show once the tool has completed. If not provided, a default label will be generated based on the tool name and whether the tool call was successful or resulted in an error.
39
+ */
40
+ this.completedLabel = input(undefined, ...(ngDevMode ? [{ debugName: "completedLabel" }] : []));
41
+ /**
42
+ * A function that can generate a label for this tool call based on the tool call data.
43
+ */
44
+ this.labelProvider = input(...(ngDevMode ? [undefined, { debugName: "labelProvider" }] : []));
45
+ this.expanded = signal(false, ...(ngDevMode ? [{ debugName: "expanded" }] : []));
46
+ this.everExpanded = signal(false, ...(ngDevMode ? [{ debugName: "everExpanded" }] : []));
47
+ this.translateService = inject(TranslateService);
48
+ }
49
+ getToolLabel(tool) {
50
+ const isExecuting = tool.type !== 'tool-result';
51
+ if (this.labelProvider()) {
52
+ try {
53
+ const dynamicLabel = this.labelProvider()(tool, this.translateService);
54
+ if (dynamicLabel)
55
+ return dynamicLabel;
56
+ }
57
+ catch (err) {
58
+ // Should never happen, report it then fall back to default logic
59
+ console.error(`Error in labelProvider for tool ${tool.toolName}:`, err);
60
+ }
61
+ }
62
+ if (isExecuting && this.executingLabel()) {
63
+ return this.executingLabel();
64
+ }
65
+ if (!isExecuting && this.completedLabel()) {
66
+ return this.completedLabel();
67
+ }
68
+ return this.translateService.instant(isExecuting ? gettext('Calling tool {{toolName}}') : gettext('Used tool {{toolName}}'), { toolName: tool.toolName });
69
+ }
70
+ toggleExpanded() {
71
+ const willExpand = !this.expanded();
72
+ this.expanded.set(willExpand);
73
+ if (willExpand) {
74
+ this.everExpanded.set(true);
75
+ }
76
+ }
77
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AiChatToolCallComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
78
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", 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();\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", "ngComponentOutletNgModuleFactory"], 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 }); }
79
+ }
80
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AiChatToolCallComponent, decorators: [{
81
+ type: Component,
82
+ args: [{ selector: 'c8y-ai-chat-tool-call', changeDetection: ChangeDetectionStrategy.OnPush, imports: [
83
+ NgComponentOutlet,
84
+ JsonPipe,
85
+ C8yTranslatePipe,
86
+ IconDirective,
87
+ CollapseModule,
88
+ C8yTranslateModule
89
+ ], template: "@let _tool = tool();\n@let showDetails = showDefaultToolDetails();\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" }]
90
+ }], propDecorators: { tool: [{ type: i0.Input, args: [{ isSignal: true, alias: "tool", required: true }] }], isExecuting: [{ type: i0.Input, args: [{ isSignal: true, alias: "isExecuting", required: true }] }], toolDetailsComponent: [{ type: i0.Input, args: [{ isSignal: true, alias: "toolDetailsComponent", required: false }] }], showDefaultToolDetails: [{ type: i0.Input, args: [{ isSignal: true, alias: "showDefaultToolDetails", required: false }] }], executingLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "executingLabel", required: false }] }], completedLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "completedLabel", required: false }] }], labelProvider: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelProvider", required: false }] }] } });
91
+
92
+ /** This renders a part of an assistant message. Currently only ToolCallPart is supported, but later we can expand this to deal with all part types. */
93
+ class AiChatAssistantPartComponent {
94
+ constructor() {
95
+ this.part = input.required(...(ngDevMode ? [{ debugName: "part" }] : []));
96
+ /** Whether this part is rendered as part of the main answer, or is an intermediate step with more muted rendering. */
97
+ this.displayAsPartOfMainAnswer = input(true, ...(ngDevMode ? [{ debugName: "displayAsPartOfMainAnswer" }] : []));
98
+ /**
99
+ * The context needed to render a message.
100
+ * This is a single input so we can extend in future without breaking people who have a custom rendering implementation.
101
+ */
102
+ this.assistantMessageContext = input.required(...(ngDevMode ? [{ debugName: "assistantMessageContext" }] : []));
103
+ /** Tracks whether this is currently expanded (used for reasoning section). */
104
+ this.expanded = signal(false, ...(ngDevMode ? [{ debugName: "expanded" }] : []));
105
+ }
106
+ toggleExpanded() {
107
+ this.expanded.update(v => !v);
108
+ }
109
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AiChatAssistantPartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
110
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", 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\"\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 <div\n class=\"collapse reasoning-content\"\n [attr.aria-label]=\"'Reasoning for message latest' + ctx.messageDisplayIndex\"\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 === '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 >--- (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 >--- Tool call: {{ tool.toolName }} ---</span\n >\n\n @if (toolDisplay?.component) {\n <div data-cy=\"ai-tool-with-custom-component\">\n <ng-container\n [ngComponentOutlet]=\"toolDisplay.component\"\n [ngComponentOutletInputs]=\"{\n tool: tool,\n ctx: ctx\n }\"\n ></ng-container>\n </div>\n } @else if (!toolDisplay?.isHidden) {\n <!-- Don't allow expanding while tool call is in progress - it's unlikely to have anything worth rendering,\n and complicates implementation for the client to have to check for the isExecuting case.\n -->\n <c8y-ai-chat-tool-call\n [tool]=\"tool\"\n [isExecuting]=\"tool.type !== 'tool-result' && ctx.isMessageLoading\"\n [toolDetailsComponent]=\"_config.toolDetailsComponent?.(tool)\"\n [showDefaultToolDetails]=\"_config.showDefaultToolDetails === 'all'\"\n [executingLabel]=\"toolDisplay?.executingLabel\"\n [completedLabel]=\"toolDisplay?.completedLabel\"\n [labelProvider]=\"toolDisplay?.labelProvider\"\n ></c8y-ai-chat-tool-call>\n }\n}\n", dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"], 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: "component", type: AiChatToolCallComponent, selector: "c8y-ai-chat-tool-call", inputs: ["tool", "isExecuting", "toolDetailsComponent", "showDefaultToolDetails", "executingLabel", "completedLabel", "labelProvider"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: MarkdownToHtmlPipe, name: "markdownToHtml" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
111
+ }
112
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AiChatAssistantPartComponent, decorators: [{
113
+ type: Component,
114
+ args: [{ selector: 'c8y-ai-chat-assistant-part', standalone: true, imports: [
115
+ NgComponentOutlet,
116
+ C8yTranslatePipe,
117
+ IconDirective,
118
+ CollapseModule,
119
+ MarkdownToHtmlPipe,
120
+ AsyncPipe,
121
+ AiChatToolCallComponent
122
+ ], 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\"\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 <div\n class=\"collapse reasoning-content\"\n [attr.aria-label]=\"'Reasoning for message latest' + ctx.messageDisplayIndex\"\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 === '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 >--- (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 >--- Tool call: {{ tool.toolName }} ---</span\n >\n\n @if (toolDisplay?.component) {\n <div data-cy=\"ai-tool-with-custom-component\">\n <ng-container\n [ngComponentOutlet]=\"toolDisplay.component\"\n [ngComponentOutletInputs]=\"{\n tool: tool,\n ctx: ctx\n }\"\n ></ng-container>\n </div>\n } @else if (!toolDisplay?.isHidden) {\n <!-- Don't allow expanding while tool call is in progress - it's unlikely to have anything worth rendering,\n and complicates implementation for the client to have to check for the isExecuting case.\n -->\n <c8y-ai-chat-tool-call\n [tool]=\"tool\"\n [isExecuting]=\"tool.type !== 'tool-result' && ctx.isMessageLoading\"\n [toolDetailsComponent]=\"_config.toolDetailsComponent?.(tool)\"\n [showDefaultToolDetails]=\"_config.showDefaultToolDetails === 'all'\"\n [executingLabel]=\"toolDisplay?.executingLabel\"\n [completedLabel]=\"toolDisplay?.completedLabel\"\n [labelProvider]=\"toolDisplay?.labelProvider\"\n ></c8y-ai-chat-tool-call>\n }\n}\n" }]
123
+ }], 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 }] }] } });
124
+
125
+ /**
126
+ * This is the default component used to render the contents of a message from the AI assistant, including
127
+ * the main answer text, text from earlier steps, reasoning text and tool calls.
128
+ */
129
+ class AiChatAssistantMessageComponent {
130
+ constructor() {
131
+ /**
132
+ * The context needed to render a message.
133
+ * This is a single input so we can extend in future without breaking people who have a custom rendering implementation.
134
+ */
135
+ this.assistantMessageContext = input.required(...(ngDevMode ? [{ debugName: "assistantMessageContext" }] : []));
136
+ /**
137
+ * By default this component will render a "Working..." indicator while streaming results. This input can turn that off if required.
138
+ */
139
+ this.showWorkingIndicator = input(true, ...(ngDevMode ? [{ debugName: "showWorkingIndicator" }] : []));
140
+ /**
141
+ * Whether the thinking section is expanded. Initialized to false unless the message is still loading.
142
+ * Call setThinkingExpanded() via viewChild to control this programmatically. Does nothing if appearance does not distinguish thinking steps.
143
+ */
144
+ this.thinkingExpanded = signal(false, ...(ngDevMode ? [{ debugName: "thinkingExpanded" }] : []));
145
+ /**
146
+ * Tracks which reasoning sections are expanded, keyed by step index.
147
+ */
148
+ this.expandedReasoningStepIndices = signal(new Set(), ...(ngDevMode ? [{ debugName: "expandedReasoningStepIndices" }] : []));
149
+ this.translateService = inject(TranslateService);
150
+ /**
151
+ * For now be conservative with the old behaviour, but hope to change this once we've got working really smoothly.
152
+ */
153
+ this.defaultNonFinalStepTextDisplay = 'main-answer';
154
+ // Expand when we initially set the message loading flag, without overwriting the value
155
+ // any other time
156
+ effect(() => {
157
+ if (this.assistantMessageContext().isMessageLoading) {
158
+ this.thinkingExpanded.set(true);
159
+ }
160
+ });
161
+ }
162
+ /**
163
+ * This is public and exists for agent-chat to collapse older messages.
164
+ * @param expanded Whether the thinking section should be expanded or not.
165
+ */
166
+ setThinkingExpanded(expanded) {
167
+ this.thinkingExpanded.set(expanded);
168
+ }
169
+ /**
170
+ * Get a translated/translateable label for the specified tool.
171
+ * @param tool The tool call part to get the label for.
172
+ * @return The label to show for this tool call.
173
+ */
174
+ getToolLabel(tool) {
175
+ const isExecuting = tool.type !== 'tool-result';
176
+ const config = this.assistantMessageContext().config;
177
+ const toolCallConfig = config?.toolCallConfig?.[tool.toolName];
178
+ if (toolCallConfig?.labelProvider) {
179
+ try {
180
+ const dynamicLabel = toolCallConfig.labelProvider(tool, this.translateService);
181
+ // Run it through translate in case it contains gettext values
182
+ if (dynamicLabel)
183
+ return dynamicLabel;
184
+ }
185
+ catch (err) {
186
+ // Should never happen, so report this - then fallback to default logic
187
+ console.warn(`Error in labelProvider for tool ${tool.toolName}:`, err);
188
+ }
189
+ }
190
+ if (isExecuting && toolCallConfig?.executingLabel) {
191
+ return toolCallConfig.executingLabel;
192
+ }
193
+ if (!isExecuting && toolCallConfig?.completedLabel) {
194
+ return toolCallConfig.completedLabel;
195
+ }
196
+ // If we have no specific label configured, use the default
197
+ return this.translateService.instant(isExecuting ? gettext('Calling tool {{toolName}}') : gettext('Used tool {{toolName}}'),
198
+ // Have to use the tool id since that's all we have
199
+ { toolName: tool.toolName });
200
+ }
201
+ getToolParts(step) {
202
+ // This method only exists because of the current structure of AIMessage; can remove this when we refactor
203
+ return [...(step.toolCalls || []), ...(step.toolResults || [])];
204
+ }
205
+ toggleReasoningExpanded(stepIndex) {
206
+ this.expandedReasoningStepIndices.update(s => {
207
+ const next = new Set(s);
208
+ s.has(stepIndex) ? next.delete(stepIndex) : next.add(stepIndex);
209
+ return next;
210
+ });
211
+ }
212
+ /**
213
+ * Note: ctx is passed as a parameter rather than reading the signal in the template to ensure
214
+ * it's totally aligned with what the template is rendering.
215
+ * @param ctx The context for the assistant message.
216
+ * @return The label to display for the thinking section.
217
+ */
218
+ getThinkingLabel(ctx) {
219
+ return ctx.isMessageLoading
220
+ ? gettext('Thinking')
221
+ : this.thinkingExpanded()
222
+ ? gettext('Hide thinking')
223
+ : gettext('Show thinking');
224
+ }
225
+ getMainAnswerText(ctx) {
226
+ // If it's a simple plain message, do nothing
227
+ if (!ctx.message.steps)
228
+ return ctx.message.content;
229
+ // Get the latest (non-empty) step text (the final one can occasionally be empty it seems)
230
+ const lastStepWithText = ctx.message.steps.filter(step => step?.text && step.text !== '').pop();
231
+ const lastStepText = lastStepWithText?.text || '';
232
+ // The final step text goes in the main answer area if there are no pending tool calls (since otherwise the tools would show below it), and
233
+ // message has finished loading, or we're expecting more steps than this.
234
+ // Note that we check all steps for toolCalls not just the current one, for the sake of "fake" tool calls created by preprocessAgentMessage
235
+ // where additional step boundaries may be added to simplify parsing.
236
+ if (lastStepText &&
237
+ !ctx.message.steps.some(step => step.toolCalls) &&
238
+ !lastStepWithText?.toolResults &&
239
+ (!ctx.isMessageLoading ||
240
+ ctx.message.steps.length >= (ctx.config?.experimental_expectedStepCount || 2))) {
241
+ return lastStepText;
242
+ }
243
+ return '';
244
+ }
245
+ hasThinkingContent(ctx, mainAnswer) {
246
+ // This logic matches the rendering in the template to ensure we don't show an empty thinking section
247
+ if (!ctx.message?.steps)
248
+ return false;
249
+ const result = ctx.message.steps.some(step => (step.text && step.text !== mainAnswer) ||
250
+ step.reasoning ||
251
+ [...(step.toolResults || []), ...(step.toolCalls || [])].some(tool => !ctx.config?.toolCallConfig ||
252
+ (!ctx.config?.toolCallConfig[tool.toolName]?.isHidden &&
253
+ !ctx.config?.toolCallConfig[tool.toolName]?.isShownWithMainAnswer)));
254
+ return result;
255
+ }
256
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AiChatAssistantMessageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
257
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", 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 steps = ctx.message.steps ?? [{ type: 'text', text: ctx.message.content }];\n@let _config = ctx.config;\n\n<!-- NB: be sure to update hasThinkingContent() if changing the anything that affects what content \nis rendered in the thinking area.\n-->\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 mainAnswer = getMainAnswerText(ctx);\n\n<!-- Thinking steps section - holds reasoning, text from non-final steps, and standard/non-artifact tool calls -->\n\n@if (hasThinkingContent(ctx, mainAnswer)) {\n @let nonFinalStepTextDisplay =\n _config?.experimental_nonFinalStepTextDisplay || defaultNonFinalStepTextDisplay;\n <div\n class=\"m-b-8\"\n [class.thinking-block]=\"nonFinalStepTextDisplay === 'collapsible-thinking-block'\"\n [class.text-muted]=\"nonFinalStepTextDisplay !== 'main-answer'\"\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 thinking section\"\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 <div\n class=\"collapse\"\n [attr.aria-label]=\"'Thinking details for message latest' + ctx.messageDisplayIndex\"\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 b-r-4 border-left-accent small m-b-0 text-muted'\n : ''\n \"\n >\n @for (step of steps; track $index) {\n <c8y-ai-chat-assistant-part\n [part]=\"{ type: 'step-start' }\"\n [assistantMessageContext]=\"ctx\"\n ></c8y-ai-chat-assistant-part>\n\n <!-- Non-final step text: avoid duplicating the final message content since we show that below -->\n @if (step.text && step.text !== mainAnswer) {\n <c8y-ai-chat-assistant-part\n [part]=\"{ type: 'text', text: step.text }\"\n [assistantMessageContext]=\"ctx\"\n [displayAsPartOfMainAnswer]=\"nonFinalStepTextDisplay !== 'muted-main-answer'\"\n ></c8y-ai-chat-assistant-part>\n }\n\n <!-- Expandable section for reasoning (only some models provide this). Collapse by default. -->\n @if (step.reasoning) {\n <c8y-ai-chat-assistant-part\n [part]=\"{ type: 'reasoning', text: step.reasoning }\"\n [assistantMessageContext]=\"ctx\"\n [displayAsPartOfMainAnswer]=\"nonFinalStepTextDisplay !== 'muted-main-answer'\"\n ></c8y-ai-chat-assistant-part>\n }\n\n @for (tool of getToolParts(step); track tool.toolCallId) {\n @if (!ctx.config.toolCallConfig?.[tool.toolName]?.isShownWithMainAnswer) {\n <c8y-ai-chat-assistant-part\n [part]=\"tool\"\n [assistantMessageContext]=\"ctx\"\n ></c8y-ai-chat-assistant-part>\n }\n }\n }\n </div>\n </div>\n </div>\n}\n\n<c8y-ai-chat-assistant-part\n [part]=\"{ type: 'step-start' }\"\n [assistantMessageContext]=\"ctx\"\n></c8y-ai-chat-assistant-part>\n\n<!-- Show tools which generate artifacts/outputs the user cares about in a dedicated area (outside the thinking section, never muted) -->\n\n<span data-cy=\"ai-tools-with-main-answer\">\n @for (step of steps; track $index) {\n @for (tool of getToolParts(step); track tool.toolCallId) {\n @if (ctx.config.toolCallConfig?.[tool.toolName]?.isShownWithMainAnswer) {\n <c8y-ai-chat-assistant-part\n [part]=\"tool\"\n [assistantMessageContext]=\"ctx\"\n ></c8y-ai-chat-assistant-part>\n }\n }\n }\n</span>\n\n<!-- Main/final text from the final step goes here -->\n@if (mainAnswer) {\n <div\n class=\"message-content m-t-8 text-default ai-main-answer-content\"\n data-cy=\"ai-main-message-content\"\n >\n <c8y-ai-chat-assistant-part\n [part]=\"{ type: 'text', text: mainAnswer }\"\n [assistantMessageContext]=\"ctx\"\n [displayAsPartOfMainAnswer]=\"true\"\n ></c8y-ai-chat-assistant-part>\n </div>\n}\n\n@if (ctx.isMessageLoading && showWorkingIndicator()) {\n <!-- Once the message starts to stream, show a small/low-key thinking indicator -->\n <div\n class=\"text-muted text-12 fade-in-out d-flex j-c-end\"\n aria-live=\"polite\"\n role=\"status\"\n data-cy=\"working-indicator\"\n >\n {{ 'Working\u2026' | translate }}\n </div>\n}\n", dependencies: [{ kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "ngmodule", type: CollapseModule }, { kind: "directive", type: i1.CollapseDirective, selector: "[collapse]", inputs: ["display", "isAnimated", "collapse"], outputs: ["collapsed", "collapses", "expanded", "expands"], exportAs: ["bs-collapse"] }, { kind: "ngmodule", type: C8yTranslateModule }, { kind: "component", type: AiChatAssistantPartComponent, selector: "c8y-ai-chat-assistant-part", inputs: ["part", "displayAsPartOfMainAnswer", "assistantMessageContext"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }] }); }
258
+ }
259
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AiChatAssistantMessageComponent, decorators: [{
260
+ type: Component,
261
+ args: [{ selector: 'c8y-ai-chat-assistant-message', standalone: true, imports: [
262
+ C8yTranslatePipe,
263
+ IconDirective,
264
+ CollapseModule,
265
+ C8yTranslateModule,
266
+ AiChatAssistantPartComponent
267
+ ], template: "@let ctx = assistantMessageContext();\n@let steps = ctx.message.steps ?? [{ type: 'text', text: ctx.message.content }];\n@let _config = ctx.config;\n\n<!-- NB: be sure to update hasThinkingContent() if changing the anything that affects what content \nis rendered in the thinking area.\n-->\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 mainAnswer = getMainAnswerText(ctx);\n\n<!-- Thinking steps section - holds reasoning, text from non-final steps, and standard/non-artifact tool calls -->\n\n@if (hasThinkingContent(ctx, mainAnswer)) {\n @let nonFinalStepTextDisplay =\n _config?.experimental_nonFinalStepTextDisplay || defaultNonFinalStepTextDisplay;\n <div\n class=\"m-b-8\"\n [class.thinking-block]=\"nonFinalStepTextDisplay === 'collapsible-thinking-block'\"\n [class.text-muted]=\"nonFinalStepTextDisplay !== 'main-answer'\"\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 thinking section\"\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 <div\n class=\"collapse\"\n [attr.aria-label]=\"'Thinking details for message latest' + ctx.messageDisplayIndex\"\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 b-r-4 border-left-accent small m-b-0 text-muted'\n : ''\n \"\n >\n @for (step of steps; track $index) {\n <c8y-ai-chat-assistant-part\n [part]=\"{ type: 'step-start' }\"\n [assistantMessageContext]=\"ctx\"\n ></c8y-ai-chat-assistant-part>\n\n <!-- Non-final step text: avoid duplicating the final message content since we show that below -->\n @if (step.text && step.text !== mainAnswer) {\n <c8y-ai-chat-assistant-part\n [part]=\"{ type: 'text', text: step.text }\"\n [assistantMessageContext]=\"ctx\"\n [displayAsPartOfMainAnswer]=\"nonFinalStepTextDisplay !== 'muted-main-answer'\"\n ></c8y-ai-chat-assistant-part>\n }\n\n <!-- Expandable section for reasoning (only some models provide this). Collapse by default. -->\n @if (step.reasoning) {\n <c8y-ai-chat-assistant-part\n [part]=\"{ type: 'reasoning', text: step.reasoning }\"\n [assistantMessageContext]=\"ctx\"\n [displayAsPartOfMainAnswer]=\"nonFinalStepTextDisplay !== 'muted-main-answer'\"\n ></c8y-ai-chat-assistant-part>\n }\n\n @for (tool of getToolParts(step); track tool.toolCallId) {\n @if (!ctx.config.toolCallConfig?.[tool.toolName]?.isShownWithMainAnswer) {\n <c8y-ai-chat-assistant-part\n [part]=\"tool\"\n [assistantMessageContext]=\"ctx\"\n ></c8y-ai-chat-assistant-part>\n }\n }\n }\n </div>\n </div>\n </div>\n}\n\n<c8y-ai-chat-assistant-part\n [part]=\"{ type: 'step-start' }\"\n [assistantMessageContext]=\"ctx\"\n></c8y-ai-chat-assistant-part>\n\n<!-- Show tools which generate artifacts/outputs the user cares about in a dedicated area (outside the thinking section, never muted) -->\n\n<span data-cy=\"ai-tools-with-main-answer\">\n @for (step of steps; track $index) {\n @for (tool of getToolParts(step); track tool.toolCallId) {\n @if (ctx.config.toolCallConfig?.[tool.toolName]?.isShownWithMainAnswer) {\n <c8y-ai-chat-assistant-part\n [part]=\"tool\"\n [assistantMessageContext]=\"ctx\"\n ></c8y-ai-chat-assistant-part>\n }\n }\n }\n</span>\n\n<!-- Main/final text from the final step goes here -->\n@if (mainAnswer) {\n <div\n class=\"message-content m-t-8 text-default ai-main-answer-content\"\n data-cy=\"ai-main-message-content\"\n >\n <c8y-ai-chat-assistant-part\n [part]=\"{ type: 'text', text: mainAnswer }\"\n [assistantMessageContext]=\"ctx\"\n [displayAsPartOfMainAnswer]=\"true\"\n ></c8y-ai-chat-assistant-part>\n </div>\n}\n\n@if (ctx.isMessageLoading && showWorkingIndicator()) {\n <!-- Once the message starts to stream, show a small/low-key thinking indicator -->\n <div\n class=\"text-muted text-12 fade-in-out d-flex j-c-end\"\n aria-live=\"polite\"\n role=\"status\"\n data-cy=\"working-indicator\"\n >\n {{ 'Working\u2026' | translate }}\n </div>\n}\n" }]
268
+ }], ctorParameters: () => [], propDecorators: { assistantMessageContext: [{ type: i0.Input, args: [{ isSignal: true, alias: "assistantMessageContext", required: true }] }], showWorkingIndicator: [{ type: i0.Input, args: [{ isSignal: true, alias: "showWorkingIndicator", required: false }] }] } });
269
+
12
270
  /**
13
271
  * An action button that can be added to chat messages.
14
272
  * Typically used for actions like copying, regenerating, or providing feedback on messages.
15
273
  */
16
274
  class AiChatMessageActionComponent {
17
275
  constructor() {
276
+ /**
277
+ * Set to true to use content projection for custom action button content.
278
+ */
279
+ this.custom = false;
18
280
  /**
19
281
  * Disables the action button when true.
20
282
  */
@@ -30,24 +292,32 @@ class AiChatMessageActionComponent {
30
292
  /**
31
293
  * Emitted when the action button is clicked.
32
294
  */
33
- this.click = new EventEmitter();
295
+ this.actionClicked = new EventEmitter();
34
296
  }
35
297
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AiChatMessageActionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
36
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.18", type: AiChatMessageActionComponent, isStandalone: true, selector: "c8y-ai-chat-message-action", inputs: { disabled: "disabled", tooltip: "tooltip", icon: "icon" }, outputs: { click: "click" }, ngImport: i0, template: "<button\n class=\"btn btn-dot text-muted\"\n [attr.aria-label]=\"tooltip | translate\"\n [tooltip]=\"tooltip | translate\"\n [adaptivePosition]=\"true\"\n [delay]=\"500\"\n (click)=\"click.emit()\"\n [disabled]=\"disabled\"\n>\n <i\n class=\"text-12\"\n [c8yIcon]=\"icon\"\n ></i>\n</button>\n", dependencies: [{ kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i1.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: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }] }); }
298
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: AiChatMessageActionComponent, isStandalone: true, selector: "c8y-ai-chat-message-action", inputs: { custom: "custom", disabled: "disabled", tooltip: "tooltip", icon: "icon" }, outputs: { actionClicked: "actionClicked" }, ngImport: i0, template: "@if (!custom) {\n <button\n class=\"btn btn-dot text-muted\"\n [attr.aria-label]=\"tooltip | translate\"\n [tooltip]=\"tooltip | translate\"\n [adaptivePosition]=\"true\"\n [delay]=\"500\"\n (click)=\"actionClicked.emit()\"\n [disabled]=\"disabled\"\n >\n <i\n class=\"text-12\"\n [c8yIcon]=\"icon\"\n ></i>\n </button>\n} @else {\n <ng-content></ng-content>\n}\n", dependencies: [{ 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: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }] }); }
37
299
  }
38
300
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AiChatMessageActionComponent, decorators: [{
39
301
  type: Component,
40
- args: [{ selector: 'c8y-ai-chat-message-action', standalone: true, imports: [TooltipModule, C8yTranslatePipe, IconDirective], template: "<button\n class=\"btn btn-dot text-muted\"\n [attr.aria-label]=\"tooltip | translate\"\n [tooltip]=\"tooltip | translate\"\n [adaptivePosition]=\"true\"\n [delay]=\"500\"\n (click)=\"click.emit()\"\n [disabled]=\"disabled\"\n>\n <i\n class=\"text-12\"\n [c8yIcon]=\"icon\"\n ></i>\n</button>\n" }]
41
- }], propDecorators: { disabled: [{
302
+ args: [{ selector: 'c8y-ai-chat-message-action', standalone: true, imports: [TooltipModule, C8yTranslatePipe, IconDirective], template: "@if (!custom) {\n <button\n class=\"btn btn-dot text-muted\"\n [attr.aria-label]=\"tooltip | translate\"\n [tooltip]=\"tooltip | translate\"\n [adaptivePosition]=\"true\"\n [delay]=\"500\"\n (click)=\"actionClicked.emit()\"\n [disabled]=\"disabled\"\n >\n <i\n class=\"text-12\"\n [c8yIcon]=\"icon\"\n ></i>\n </button>\n} @else {\n <ng-content></ng-content>\n}\n" }]
303
+ }], propDecorators: { custom: [{
304
+ type: Input
305
+ }], disabled: [{
42
306
  type: Input
43
307
  }], tooltip: [{
44
308
  type: Input
45
309
  }], icon: [{
46
310
  type: Input
47
- }], click: [{
311
+ }], actionClicked: [{
48
312
  type: Output
49
313
  }] } });
50
314
 
315
+ /**
316
+ * A container for content and actions that should be rendered for each chat message.
317
+ *
318
+ * Project content into this component to display the message, for example add an `<ai-chat-assistant-message>`
319
+ * for assistant messages, and a simple `markdownToHtml | async` rendering of the content for user messages.
320
+ */
51
321
  class AiChatMessageComponent {
52
322
  constructor() {
53
323
  this.role = input(...(ngDevMode ? [undefined, { debugName: "role" }] : []));
@@ -90,11 +360,11 @@ class AiChatMessageComponent {
90
360
  }, ...(ngDevMode ? [{ debugName: "messageContentAriaLabel" }] : []));
91
361
  }
92
362
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AiChatMessageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
93
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: AiChatMessageComponent, isStandalone: true, selector: "c8y-ai-chat-message", inputs: { role: { classPropertyName: "role", publicName: "role", isSignal: true, isRequired: false, transformFunction: null }, message: { classPropertyName: "message", publicName: "message", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div\n class=\"d-col p-b-16\"\n [attr.aria-label]=\"ariaLabel()\"\n role=\"article\"\n>\n <div\n class=\"chat-message text-break-word\"\n [ngClass]=\"{\n 'user-message': message()?.role === 'user' || role() === 'user',\n 'agent-message': message()?.role === 'assistant' || role() === 'assistant'\n }\"\n >\n <div\n class=\"message-content\"\n [attr.aria-label]=\"messageContentAriaLabel()\"\n [innerHTML]=\"message()?.content | markdownToHtml | async\"\n ></div>\n <ng-content select=\":not(c8y-ai-chat-message-action)\"></ng-content>\n @if (message()?.timestamp) {\n <div class=\"message-timestamp\">\n <span [tooltip]=\"message()?.timestamp | c8yDate\">\n {{ message()?.timestamp | c8yDate: 'adaptiveDate' }}\n </span>\n </div>\n }\n </div>\n <div\n class=\"message-action\"\n [attr.aria-label]=\"'Message actions' | translate\"\n role=\"toolbar\"\n [ngClass]=\"{\n 'user-action showOnHover': message()?.role === 'user' || role() === 'user',\n 'agent-action p-l-16': message()?.role === 'assistant' || role() === 'assistant'\n }\"\n >\n <ng-content select=\"c8y-ai-chat-message-action\"></ng-content>\n </div>\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i1.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: MarkdownToHtmlPipe, name: "markdownToHtml" }, { kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: DatePipe, name: "c8yDate" }] }); }
363
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: AiChatMessageComponent, isStandalone: true, selector: "c8y-ai-chat-message", inputs: { role: { classPropertyName: "role", publicName: "role", isSignal: true, isRequired: false, transformFunction: null }, message: { classPropertyName: "message", publicName: "message", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div\n class=\"d-col p-t-16\"\n [attr.aria-label]=\"ariaLabel()\"\n role=\"article\"\n>\n <div\n class=\"chat-message text-break-word\"\n [ngClass]=\"{\n 'user-message': message()?.role === 'user' || role() === 'user',\n 'agent-message': message()?.role === 'assistant' || role() === 'assistant'\n }\"\n >\n <!-- Visually hidden label included when users copy-paste from the chat, e.g. to report issues -->\n <span\n class=\"hidden-copy-label\"\n aria-hidden=\"true\"\n >\n {{ `====== ${messageContentAriaLabel()} ======` }}</span\n >\n\n <!-- Apply the class and aria-label to both the main content and whatever is projected, so we get the same styling by default -->\n <div\n class=\"message-content\"\n [attr.aria-label]=\"messageContentAriaLabel()\"\n >\n <div style=\"display: contents\">\n <ng-content select=\":not(c8y-ai-chat-message-action)\"></ng-content>\n </div>\n </div>\n @if (message()?.timestamp) {\n <div class=\"message-timestamp\">\n <span [tooltip]=\"message()?.timestamp | c8yDate\">\n {{ message()?.timestamp | c8yDate: 'adaptiveDate' }}\n </span>\n </div>\n }\n </div>\n <div\n class=\"message-action\"\n [attr.aria-label]=\"'Message actions' | translate\"\n role=\"toolbar\"\n [ngClass]=\"{\n 'user-action showOnHover': message()?.role === 'user' || role() === 'user',\n 'agent-action p-l-16': message()?.role === 'assistant' || role() === 'assistant'\n }\"\n >\n <ng-content select=\"c8y-ai-chat-message-action\"></ng-content>\n </div>\n</div>\n", styles: [".hidden-copy-label{display:block;font-size:0;line-height:0;-webkit-user-select:text;user-select:text}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i1$1.TooltipDirective, selector: "[tooltip], [tooltipHtml]", inputs: ["adaptivePosition", "tooltip", "placement", "triggers", "container", "containerClass", "boundariesElement", "isOpen", "isDisabled", "delay", "tooltipHtml", "tooltipPlacement", "tooltipIsOpen", "tooltipEnable", "tooltipAppendToBody", "tooltipAnimation", "tooltipClass", "tooltipContext", "tooltipPopupDelay", "tooltipFadeDuration", "tooltipTrigger"], outputs: ["tooltipChange", "onShown", "onHidden", "tooltipStateChanged"], exportAs: ["bs-tooltip"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: DatePipe, name: "c8yDate" }] }); }
94
364
  }
95
365
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AiChatMessageComponent, decorators: [{
96
366
  type: Component,
97
- args: [{ selector: 'c8y-ai-chat-message', standalone: true, imports: [MarkdownToHtmlPipe, AsyncPipe, NgClass, TooltipModule, C8yTranslatePipe, DatePipe], template: "<div\n class=\"d-col p-b-16\"\n [attr.aria-label]=\"ariaLabel()\"\n role=\"article\"\n>\n <div\n class=\"chat-message text-break-word\"\n [ngClass]=\"{\n 'user-message': message()?.role === 'user' || role() === 'user',\n 'agent-message': message()?.role === 'assistant' || role() === 'assistant'\n }\"\n >\n <div\n class=\"message-content\"\n [attr.aria-label]=\"messageContentAriaLabel()\"\n [innerHTML]=\"message()?.content | markdownToHtml | async\"\n ></div>\n <ng-content select=\":not(c8y-ai-chat-message-action)\"></ng-content>\n @if (message()?.timestamp) {\n <div class=\"message-timestamp\">\n <span [tooltip]=\"message()?.timestamp | c8yDate\">\n {{ message()?.timestamp | c8yDate: 'adaptiveDate' }}\n </span>\n </div>\n }\n </div>\n <div\n class=\"message-action\"\n [attr.aria-label]=\"'Message actions' | translate\"\n role=\"toolbar\"\n [ngClass]=\"{\n 'user-action showOnHover': message()?.role === 'user' || role() === 'user',\n 'agent-action p-l-16': message()?.role === 'assistant' || role() === 'assistant'\n }\"\n >\n <ng-content select=\"c8y-ai-chat-message-action\"></ng-content>\n </div>\n</div>\n" }]
367
+ args: [{ selector: 'c8y-ai-chat-message', standalone: true, imports: [NgClass, TooltipModule, C8yTranslatePipe, DatePipe], template: "<div\n class=\"d-col p-t-16\"\n [attr.aria-label]=\"ariaLabel()\"\n role=\"article\"\n>\n <div\n class=\"chat-message text-break-word\"\n [ngClass]=\"{\n 'user-message': message()?.role === 'user' || role() === 'user',\n 'agent-message': message()?.role === 'assistant' || role() === 'assistant'\n }\"\n >\n <!-- Visually hidden label included when users copy-paste from the chat, e.g. to report issues -->\n <span\n class=\"hidden-copy-label\"\n aria-hidden=\"true\"\n >\n {{ `====== ${messageContentAriaLabel()} ======` }}</span\n >\n\n <!-- Apply the class and aria-label to both the main content and whatever is projected, so we get the same styling by default -->\n <div\n class=\"message-content\"\n [attr.aria-label]=\"messageContentAriaLabel()\"\n >\n <div style=\"display: contents\">\n <ng-content select=\":not(c8y-ai-chat-message-action)\"></ng-content>\n </div>\n </div>\n @if (message()?.timestamp) {\n <div class=\"message-timestamp\">\n <span [tooltip]=\"message()?.timestamp | c8yDate\">\n {{ message()?.timestamp | c8yDate: 'adaptiveDate' }}\n </span>\n </div>\n }\n </div>\n <div\n class=\"message-action\"\n [attr.aria-label]=\"'Message actions' | translate\"\n role=\"toolbar\"\n [ngClass]=\"{\n 'user-action showOnHover': message()?.role === 'user' || role() === 'user',\n 'agent-action p-l-16': message()?.role === 'assistant' || role() === 'assistant'\n }\"\n >\n <ng-content select=\"c8y-ai-chat-message-action\"></ng-content>\n </div>\n</div>\n", styles: [".hidden-copy-label{display:block;font-size:0;line-height:0;-webkit-user-select:text;user-select:text}\n"] }]
98
368
  }], propDecorators: { role: [{ type: i0.Input, args: [{ isSignal: true, alias: "role", required: false }] }], message: [{ type: i0.Input, args: [{ isSignal: true, alias: "message", required: false }] }] } });
99
369
 
100
370
  /**
@@ -103,52 +373,48 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
103
373
  */
104
374
  class AiChatSuggestionComponent {
105
375
  constructor() {
376
+ /**
377
+ * The visible label text displayed on the suggestion chip.
378
+ */
379
+ this.label = input.required(...(ngDevMode ? [{ debugName: "label" }] : []));
380
+ /**
381
+ * The prompt text that will be sent when the suggestion is clicked.
382
+ */
383
+ this.prompt = input.required(...(ngDevMode ? [{ debugName: "prompt" }] : []));
106
384
  /**
107
385
  * Icon to display alongside the suggestion label.
108
386
  */
109
- this.icon = 'c8y-bulb';
387
+ this.icon = input('c8y-bulb', ...(ngDevMode ? [{ debugName: "icon" }] : []));
110
388
  /**
111
389
  * When true, uses AI-styled buttons instead of default styling.
112
390
  */
113
- this.useAiButtons = false;
391
+ this.useAiButtons = input(false, ...(ngDevMode ? [{ debugName: "useAiButtons" }] : []));
114
392
  /**
115
393
  * Disables the suggestion chip when true.
116
394
  */
117
- this.disabled = false;
395
+ this.disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
118
396
  /**
119
397
  * Emitted when the suggestion is clicked, providing the prompt as an AIMessage.
120
398
  */
121
- this.suggestionClicked = new EventEmitter();
399
+ this.suggestionClicked = output();
122
400
  }
123
401
  /**
124
402
  * Handles suggestion click and emits the prompt as a user message.
125
403
  */
126
404
  suggest() {
127
405
  this.suggestionClicked.emit({
128
- content: this.prompt,
406
+ content: this.prompt(),
129
407
  role: 'user',
130
408
  timestamp: new Date().toISOString()
131
409
  });
132
410
  }
133
411
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AiChatSuggestionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
134
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: AiChatSuggestionComponent, isStandalone: true, selector: "c8y-ai-chat-suggestion", inputs: { label: "label", prompt: "prompt", icon: "icon", useAiButtons: "useAiButtons", disabled: "disabled" }, outputs: { suggestionClicked: "suggestionClicked" }, ngImport: i0, template: "@if (!useAiButtons) {\n <button\n class=\"btn btn-default btn-sm\"\n [title]=\"prompt | translate\"\n (click)=\"suggest()\"\n [disabled]=\"disabled\"\n >\n <i [c8yIcon]=\"icon\"></i>\n {{ label | translate }}\n </button>\n} @else {\n <button\n class=\"btn btn-sm btn-ai\"\n [title]=\"prompt | translate\"\n (click)=\"suggest()\"\n [disabled]=\"disabled\"\n >\n <span>{{ label | translate }}</span>\n </button>\n}\n", dependencies: [{ kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }] }); }
412
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: AiChatSuggestionComponent, isStandalone: true, selector: "c8y-ai-chat-suggestion", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, prompt: { classPropertyName: "prompt", publicName: "prompt", isSignal: true, isRequired: true, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, useAiButtons: { classPropertyName: "useAiButtons", publicName: "useAiButtons", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { suggestionClicked: "suggestionClicked" }, host: { classAttribute: "d-contents" }, ngImport: i0, template: "@if (!useAiButtons()) {\n <button\n class=\"btn btn-default btn-sm\"\n [title]=\"prompt() | translate\"\n (click)=\"suggest()\"\n [disabled]=\"disabled()\"\n >\n <i [c8yIcon]=\"icon()\"></i>\n {{ label() | translate }}\n </button>\n} @else {\n <button\n class=\"btn btn-sm btn-ai\"\n [title]=\"prompt() | translate\"\n (click)=\"suggest()\"\n [disabled]=\"disabled()\"\n >\n <span>{{ label() | translate }}</span>\n </button>\n}\n", dependencies: [{ kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }] }); }
135
413
  }
136
414
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AiChatSuggestionComponent, decorators: [{
137
415
  type: Component,
138
- args: [{ selector: 'c8y-ai-chat-suggestion', standalone: true, imports: [IconDirective, C8yTranslatePipe], template: "@if (!useAiButtons) {\n <button\n class=\"btn btn-default btn-sm\"\n [title]=\"prompt | translate\"\n (click)=\"suggest()\"\n [disabled]=\"disabled\"\n >\n <i [c8yIcon]=\"icon\"></i>\n {{ label | translate }}\n </button>\n} @else {\n <button\n class=\"btn btn-sm btn-ai\"\n [title]=\"prompt | translate\"\n (click)=\"suggest()\"\n [disabled]=\"disabled\"\n >\n <span>{{ label | translate }}</span>\n </button>\n}\n" }]
139
- }], propDecorators: { label: [{
140
- type: Input
141
- }], prompt: [{
142
- type: Input
143
- }], icon: [{
144
- type: Input
145
- }], useAiButtons: [{
146
- type: Input
147
- }], disabled: [{
148
- type: Input
149
- }], suggestionClicked: [{
150
- type: Output
151
- }] } });
416
+ args: [{ selector: 'c8y-ai-chat-suggestion', host: { class: 'd-contents' }, standalone: true, imports: [IconDirective, C8yTranslatePipe], template: "@if (!useAiButtons()) {\n <button\n class=\"btn btn-default btn-sm\"\n [title]=\"prompt() | translate\"\n (click)=\"suggest()\"\n [disabled]=\"disabled()\"\n >\n <i [c8yIcon]=\"icon()\"></i>\n {{ label() | translate }}\n </button>\n} @else {\n <button\n class=\"btn btn-sm btn-ai\"\n [title]=\"prompt() | translate\"\n (click)=\"suggest()\"\n [disabled]=\"disabled()\"\n >\n <span>{{ label() | translate }}</span>\n </button>\n}\n" }]
417
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], prompt: [{ type: i0.Input, args: [{ isSignal: true, alias: "prompt", required: true }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], useAiButtons: [{ type: i0.Input, args: [{ isSignal: true, alias: "useAiButtons", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], suggestionClicked: [{ type: i0.Output, args: ["suggestionClicked"] }] } });
152
418
 
153
419
  /**
154
420
  * An interactive chat interface component for AI-powered conversations.
@@ -179,13 +445,22 @@ class AiChatComponent {
179
445
  * Emitted when the user cancels an ongoing operation during loading state.
180
446
  */
181
447
  this.onCancel = new EventEmitter();
448
+ /**
449
+ * Child message components displayed in the chat.
450
+ */
451
+ this.messages = contentChildren(AiChatMessageComponent, ...(ngDevMode ? [{ debugName: "messages" }] : []));
452
+ /**
453
+ * Reference to the scroll container for the chat messages.
454
+ */
455
+ this.scrollContainer = viewChild('chatScrollContainer', ...(ngDevMode ? [{ debugName: "scrollContainer" }] : []));
182
456
  this.componentId = `chat-${crypto.randomUUID()}`;
183
457
  this._config = {
184
458
  headline: gettext('Welcome!'),
185
459
  welcomeText: '',
460
+ welcomePosition: 'top',
186
461
  title: gettext('What can I help you with?'),
187
- placeholder: gettext('Type your message here'),
188
- placeholderAfterFirstMessage: gettext('Type your next message here…'),
462
+ placeholder: gettext('Type your message here...'),
463
+ scrollbarOnlyOnHover: false,
189
464
  sendButtonText: gettext('Send'),
190
465
  cancelButtonText: gettext('Cancel'),
191
466
  disclaimerText: gettext('AI-generated responses can contain errors. Verify the details before use.'),
@@ -194,10 +469,24 @@ class AiChatComponent {
194
469
  cancel: 'stop-circle'
195
470
  }
196
471
  };
472
+ // Auto-scroll to bottom when messages change
473
+ effect(() => {
474
+ this.messages(); // track changes
475
+ const container = this.scrollContainer()?.nativeElement;
476
+ if (!container)
477
+ return;
478
+ // In column-reverse, scrollTop=0 IS the visual bottom.
479
+ // Only snap back if user is already near the bottom (wasn't scrolled up).
480
+ if (container.scrollTop < 50) {
481
+ container.scrollTop = 0;
482
+ }
483
+ });
197
484
  }
198
485
  /**
199
486
  * Configuration object for customizing labels, placeholders, and icons.
200
487
  * Accepts partial configuration to override defaults.
488
+ * @param value Partial configuration to merge with defaults.
489
+ * @returns The complete configuration object.
201
490
  */
202
491
  set config(value) {
203
492
  this._config = { ...this._config, ...value };
@@ -208,7 +497,7 @@ class AiChatComponent {
208
497
  /**
209
498
  * Handles message submission when the user sends a message.
210
499
  * Emits the onMessage event and clears the input.
211
- * @param $event - The event object from the form submission
500
+ * @param $event The event object from the form submission
212
501
  */
213
502
  sendMessage($event) {
214
503
  $event.preventDefault();
@@ -229,25 +518,35 @@ class AiChatComponent {
229
518
  this.onCancel.emit();
230
519
  }
231
520
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AiChatComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
232
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: AiChatComponent, isStandalone: true, selector: "c8y-ai-chat", inputs: { isLoading: "isLoading", disabled: "disabled", prompt: "prompt", config: "config" }, outputs: { onMessage: "onMessage", onCancel: "onCancel" }, host: { classAttribute: "d-contents" }, queries: [{ propertyName: "messages", predicate: AiChatMessageComponent }], ngImport: i0, template: "<div\n class=\"d-col fit-h fit-w flex-grow\"\n [attr.aria-label]=\"config.headline | translate\"\n role=\"region\"\n>\n @if (messages.length > 0) {\n <div\n class=\"inner-scroll flex-grow d-col-reverse min-height-0 bg-level-0\"\n [attr.aria-label]=\"'Chat conversation' | translate\"\n aria-live=\"polite\"\n aria-atomic=\"false\"\n role=\"log\"\n >\n <div class=\"d-col p-l-16 p-r-16\">\n <ng-content select=\"c8y-ai-chat-message\"></ng-content>\n <ng-content></ng-content>\n </div>\n </div>\n }\n <div [ngClass]=\"{ 'd-col fit-h': messages.length === 0 }\">\n @if (messages.length === 0) {\n <div\n class=\"p-24\"\n aria-live=\"polite\"\n role=\"status\"\n >\n <h4 class=\"m-b-16 text-medium\">{{ config.headline | translate }}</h4>\n <p class=\"p-b-8 text-balance\">{{ config.title | translate }}</p>\n <p class=\"text-muted text-balance\">{{ config.welcomeText | translate }}</p>\n </div>\n }\n <div class=\"chat-input bg-level-1\">\n <div class=\"d-flex inner-scroll a-i-center gap-8 p-l-16 p-r-16 p-b-8\">\n <ng-content select=\"c8y-ai-chat-suggestion\"></ng-content>\n </div>\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 style=\"max-height: 200px !important\"\n [attr.aria-label]=\"config.placeholder | translate\"\n id=\"chat-input-{{ componentId }}\"\n [attr.aria-describedby]=\"config.disclaimerText ? 'chat-disclaimer-' + componentId : null\"\n [attr.aria-busy]=\"isLoading\"\n [placeholder]=\"\n (messages.length === 0 ? config.placeholder : config.placeholderAfterFirstMessage)\n | translate\n \"\n [(ngModel)]=\"prompt\"\n (keydown.enter)=\"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 @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 </div>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.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: "pipe", type: C8yTranslatePipe, name: "translate" }] }); }
521
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.18", type: AiChatComponent, isStandalone: true, selector: "c8y-ai-chat", inputs: { isLoading: "isLoading", disabled: "disabled", prompt: "prompt", suggestionsTemplate: "suggestionsTemplate", welcomeTemplate: "welcomeTemplate", 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\"\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 @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 </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: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: MarkdownToHtmlPipe, name: "markdownToHtml" }, { kind: "pipe", type: AsyncPipe, name: "async" }] }); }
233
522
  }
234
523
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: AiChatComponent, decorators: [{
235
524
  type: Component,
236
- args: [{ selector: 'c8y-ai-chat', imports: [C8yTranslatePipe, FormsModule, TextareaAutoresizeDirective, IconDirective, NgClass], standalone: true, host: { class: 'd-contents' }, template: "<div\n class=\"d-col fit-h fit-w flex-grow\"\n [attr.aria-label]=\"config.headline | translate\"\n role=\"region\"\n>\n @if (messages.length > 0) {\n <div\n class=\"inner-scroll flex-grow d-col-reverse min-height-0 bg-level-0\"\n [attr.aria-label]=\"'Chat conversation' | translate\"\n aria-live=\"polite\"\n aria-atomic=\"false\"\n role=\"log\"\n >\n <div class=\"d-col p-l-16 p-r-16\">\n <ng-content select=\"c8y-ai-chat-message\"></ng-content>\n <ng-content></ng-content>\n </div>\n </div>\n }\n <div [ngClass]=\"{ 'd-col fit-h': messages.length === 0 }\">\n @if (messages.length === 0) {\n <div\n class=\"p-24\"\n aria-live=\"polite\"\n role=\"status\"\n >\n <h4 class=\"m-b-16 text-medium\">{{ config.headline | translate }}</h4>\n <p class=\"p-b-8 text-balance\">{{ config.title | translate }}</p>\n <p class=\"text-muted text-balance\">{{ config.welcomeText | translate }}</p>\n </div>\n }\n <div class=\"chat-input bg-level-1\">\n <div class=\"d-flex inner-scroll a-i-center gap-8 p-l-16 p-r-16 p-b-8\">\n <ng-content select=\"c8y-ai-chat-suggestion\"></ng-content>\n </div>\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 style=\"max-height: 200px !important\"\n [attr.aria-label]=\"config.placeholder | translate\"\n id=\"chat-input-{{ componentId }}\"\n [attr.aria-describedby]=\"config.disclaimerText ? 'chat-disclaimer-' + componentId : null\"\n [attr.aria-busy]=\"isLoading\"\n [placeholder]=\"\n (messages.length === 0 ? config.placeholder : config.placeholderAfterFirstMessage)\n | translate\n \"\n [(ngModel)]=\"prompt\"\n (keydown.enter)=\"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 @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 </div>\n </div>\n</div>\n" }]
237
- }], propDecorators: { isLoading: [{
525
+ args: [{ selector: 'c8y-ai-chat', imports: [
526
+ C8yTranslatePipe,
527
+ FormsModule,
528
+ TextareaAutoresizeDirective,
529
+ IconDirective,
530
+ NgClass,
531
+ NgTemplateOutlet,
532
+ MarkdownToHtmlPipe,
533
+ AsyncPipe
534
+ ], standalone: true, host: { class: 'd-contents' }, template: "<div\n class=\"d-col fit-h fit-w flex-grow\"\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 @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 </div>\n </div>\n</div>\n" }]
535
+ }], ctorParameters: () => [], propDecorators: { isLoading: [{
238
536
  type: Input
239
537
  }], disabled: [{
240
538
  type: Input
241
539
  }], prompt: [{
242
540
  type: Input
541
+ }], suggestionsTemplate: [{
542
+ type: Input
543
+ }], welcomeTemplate: [{
544
+ type: Input
243
545
  }], onMessage: [{
244
546
  type: Output
245
547
  }], onCancel: [{
246
548
  type: Output
247
- }], messages: [{
248
- type: ContentChildren,
249
- args: [AiChatMessageComponent]
250
- }], config: [{
549
+ }], messages: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => AiChatMessageComponent), { isSignal: true }] }], scrollContainer: [{ type: i0.ViewChild, args: ['chatScrollContainer', { isSignal: true }] }], config: [{
251
550
  type: Input
252
551
  }] } });
253
552
 
@@ -255,5 +554,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
255
554
  * Generated bundle index. Do not edit.
256
555
  */
257
556
 
258
- export { AiChatComponent, AiChatMessageActionComponent, AiChatMessageComponent, AiChatSuggestionComponent };
557
+ export { AiChatAssistantMessageComponent, AiChatAssistantPartComponent, AiChatComponent, AiChatMessageActionComponent, AiChatMessageComponent, AiChatSuggestionComponent, AiChatToolCallComponent };
259
558
  //# sourceMappingURL=c8y-ngx-components-ai-ai-chat.mjs.map