@c8y/ngx-components 1024.20.1 → 1024.21.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/c8y-ngx-components-ai-agent-chat.mjs +527 -12
- package/fesm2022/c8y-ngx-components-ai-agent-chat.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-ai-ai-chat.mjs +31 -9
- package/fesm2022/c8y-ngx-components-ai-ai-chat.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-ai.mjs +503 -62
- package/fesm2022/c8y-ngx-components-ai.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-alarm-event-selector.mjs +2 -2
- package/fesm2022/c8y-ngx-components-alarm-event-selector.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-alarms.mjs +3 -3
- package/fesm2022/c8y-ngx-components-alarms.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-application-access-list.mjs +1 -1
- package/fesm2022/c8y-ngx-components-application-access-list.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-application-select.mjs +1 -1
- package/fesm2022/c8y-ngx-components-application-select.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-asset-properties.mjs +3 -2
- package/fesm2022/c8y-ngx-components-asset-properties.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-auth-configuration.mjs +1 -1
- package/fesm2022/c8y-ngx-components-auth-configuration.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-bookmarks.mjs +1 -1
- package/fesm2022/c8y-ngx-components-bookmarks.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-branding-shared-lazy.mjs +1 -1
- package/fesm2022/c8y-ngx-components-branding-shared-lazy.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-cockpit-config.mjs +5 -5
- package/fesm2022/c8y-ngx-components-cockpit-config.mjs.map +1 -1
- package/fesm2022/{c8y-ngx-components-context-dashboard-dashboard-version-history.component-Jyjkv4JT.mjs → c8y-ngx-components-context-dashboard-dashboard-version-history.component-CP1afwVe.mjs} +2 -2
- package/fesm2022/{c8y-ngx-components-context-dashboard-dashboard-version-history.component-Jyjkv4JT.mjs.map → c8y-ngx-components-context-dashboard-dashboard-version-history.component-CP1afwVe.mjs.map} +1 -1
- package/fesm2022/c8y-ngx-components-context-dashboard.mjs +81 -20
- package/fesm2022/c8y-ngx-components-context-dashboard.mjs.map +1 -1
- package/fesm2022/{c8y-ngx-components-dashboard-details-advanced-tab-dashboard-details-advanced-tab.component-UEUYsXBv.mjs → c8y-ngx-components-dashboard-details-advanced-tab-dashboard-details-advanced-tab.component-CRvgIgri.mjs} +2 -2
- package/fesm2022/{c8y-ngx-components-dashboard-details-advanced-tab-dashboard-details-advanced-tab.component-UEUYsXBv.mjs.map → c8y-ngx-components-dashboard-details-advanced-tab-dashboard-details-advanced-tab.component-CRvgIgri.mjs.map} +1 -1
- package/fesm2022/c8y-ngx-components-dashboard-details-advanced-tab.mjs +2 -2
- package/fesm2022/c8y-ngx-components-dashboard-manager.mjs +1 -1
- package/fesm2022/c8y-ngx-components-dashboard-manager.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-datapoint-explorer-view.mjs +3 -3
- package/fesm2022/c8y-ngx-components-datapoint-explorer-view.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-datapoint-selector.mjs +2 -2
- package/fesm2022/c8y-ngx-components-datapoint-selector.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-device-profile.mjs +2 -2
- package/fesm2022/c8y-ngx-components-device-profile.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-diagnostics.mjs +1 -1
- package/fesm2022/c8y-ngx-components-diagnostics.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-echart.mjs +146 -126
- package/fesm2022/c8y-ngx-components-echart.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-ecosystem-application-plugins.mjs +2 -2
- package/fesm2022/c8y-ngx-components-ecosystem-application-plugins.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-ecosystem-shared.mjs +1 -1
- package/fesm2022/c8y-ngx-components-ecosystem-shared.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-ecosystem.mjs +4 -4
- package/fesm2022/c8y-ngx-components-ecosystem.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-events-events-timeline.mjs +1 -1
- package/fesm2022/c8y-ngx-components-events-events-timeline.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-events.mjs +2 -2
- package/fesm2022/c8y-ngx-components-events.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-icon-selector.mjs +4 -4
- package/fesm2022/c8y-ngx-components-icon-selector.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-operations-bulk-operation-list-item.mjs +1 -1
- package/fesm2022/c8y-ngx-components-operations-bulk-operation-list-item.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-operations-operations-list.mjs +1 -1
- package/fesm2022/c8y-ngx-components-operations-operations-list.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-operations-operations-timeline.mjs +1 -1
- package/fesm2022/c8y-ngx-components-operations-operations-timeline.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-operations-stepper-bulk-type-configuration.mjs +1 -1
- package/fesm2022/c8y-ngx-components-operations-stepper-bulk-type-configuration.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-operations-stepper-bulk-type-device-profile.mjs +2 -2
- package/fesm2022/c8y-ngx-components-operations-stepper-bulk-type-device-profile.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-operations-stepper-bulk-type-firmware.mjs +2 -2
- package/fesm2022/c8y-ngx-components-operations-stepper-bulk-type-firmware.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-operations-stepper-bulk-type-software.mjs +2 -2
- package/fesm2022/c8y-ngx-components-operations-stepper-bulk-type-software.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-protocol-lpwan.mjs +5 -5
- package/fesm2022/c8y-ngx-components-protocol-lpwan.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-protocol-opcua.mjs +1 -1
- package/fesm2022/c8y-ngx-components-protocol-opcua.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-register-device.mjs +3 -3
- package/fesm2022/c8y-ngx-components-register-device.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-remote-access-configurations.mjs +1 -1
- package/fesm2022/c8y-ngx-components-remote-access-configurations.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-replace-device-replace-device-wizard.mjs +1 -1
- package/fesm2022/c8y-ngx-components-replace-device-replace-device-wizard.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-report-dashboard.mjs +1 -1
- package/fesm2022/c8y-ngx-components-report-dashboard.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-repository-configuration.mjs +4 -4
- package/fesm2022/c8y-ngx-components-repository-configuration.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-repository-firmware.mjs +4 -4
- package/fesm2022/c8y-ngx-components-repository-firmware.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-repository-shared.mjs +2 -2
- package/fesm2022/c8y-ngx-components-repository-shared.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-repository-software.mjs +3 -3
- package/fesm2022/c8y-ngx-components-repository-software.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-static-assets-modal.mjs +1 -1
- package/fesm2022/c8y-ngx-components-static-assets-modal.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-tracking.mjs +1 -1
- package/fesm2022/c8y-ngx-components-tracking.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-trusted-certificates.mjs +1 -1
- package/fesm2022/c8y-ngx-components-trusted-certificates.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-widgets-definitions-html-widget.mjs +17 -1
- package/fesm2022/c8y-ngx-components-widgets-definitions-html-widget.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-widgets-definitions-scada.mjs +21 -1
- package/fesm2022/c8y-ngx-components-widgets-definitions-scada.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-widgets-implementations-asset-table.mjs +60 -31
- package/fesm2022/c8y-ngx-components-widgets-implementations-asset-table.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-widgets-implementations-datapoints-graph.mjs +2 -2
- package/fesm2022/c8y-ngx-components-widgets-implementations-datapoints-graph.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-widgets-implementations-datapoints-list.mjs +2 -2
- package/fesm2022/c8y-ngx-components-widgets-implementations-datapoints-list.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-widgets-implementations-datapoints-table.mjs +1 -1
- package/fesm2022/c8y-ngx-components-widgets-implementations-datapoints-table.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-widgets-implementations-image.mjs +1 -1
- package/fesm2022/c8y-ngx-components-widgets-implementations-image.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-widgets-implementations-quick-links.mjs +1 -1
- package/fesm2022/c8y-ngx-components-widgets-implementations-quick-links.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-widgets-implementations-scada.mjs +1 -1
- package/fesm2022/c8y-ngx-components-widgets-implementations-scada.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components.mjs +500 -38
- package/fesm2022/c8y-ngx-components.mjs.map +1 -1
- package/locales/de.po +72 -0
- package/locales/es.po +72 -0
- package/locales/fr.po +72 -0
- package/locales/ja_JP.po +72 -0
- package/locales/ko.po +72 -0
- package/locales/locales.pot +72 -3
- package/locales/nl.po +72 -0
- package/locales/pl.po +73 -1
- package/locales/pt_BR.po +74 -2
- package/locales/zh_CN.po +72 -0
- package/locales/zh_TW.po +72 -0
- package/package.json +1 -1
- package/types/c8y-ngx-components-ai-agent-chat.d.ts +201 -3
- package/types/c8y-ngx-components-ai-agent-chat.d.ts.map +1 -1
- package/types/c8y-ngx-components-ai-ai-chat.d.ts +14 -4
- package/types/c8y-ngx-components-ai-ai-chat.d.ts.map +1 -1
- package/types/c8y-ngx-components-ai.d.ts +157 -2
- package/types/c8y-ngx-components-ai.d.ts.map +1 -1
- package/types/c8y-ngx-components-asset-properties.d.ts.map +1 -1
- package/types/c8y-ngx-components-context-dashboard.d.ts +37 -3
- package/types/c8y-ngx-components-context-dashboard.d.ts.map +1 -1
- package/types/c8y-ngx-components-echart.d.ts +6 -0
- package/types/c8y-ngx-components-echart.d.ts.map +1 -1
- package/types/c8y-ngx-components-widgets-implementations-asset-table.d.ts +5 -0
- package/types/c8y-ngx-components-widgets-implementations-asset-table.d.ts.map +1 -1
- package/types/c8y-ngx-components.d.ts +310 -12
- package/types/c8y-ngx-components.d.ts.map +1 -1
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
import { NgComponentOutlet, AsyncPipe } from '@angular/common';
|
|
2
2
|
import * as i0 from '@angular/core';
|
|
3
|
-
import { Injectable, inject, input, output, ChangeDetectionStrategy, Component,
|
|
4
|
-
import { GainsightService, IconDirective, C8yTranslatePipe, DatePipe, ModalService, UserPreferencesService,
|
|
3
|
+
import { Injectable, inject, input, output, ChangeDetectionStrategy, Component, signal, Injector, viewChild, effect, untracked, afterNextRender, contentChildren, computed, viewChildren, Input } from '@angular/core';
|
|
4
|
+
import { GainsightService, IconDirective, C8yTranslatePipe, AlertService, DropdownFocusTrapDirective, EmptyStateComponent, HighlightComponent, InputGroupEditableComponent, LoadingComponent, PopoverConfirmComponent, DatePipe, ModalService, UserPreferencesService, C8Y_PLUGIN_CONTEXT_PATH, AppStateService, Status, C8yTranslateDirective, GuideDocsComponent, GuideHrefDirective, MarkdownToHtmlPipe, NumberPipe, ContextRouteService } from '@c8y/ngx-components';
|
|
5
5
|
import { TranslateService } from '@ngx-translate/core';
|
|
6
|
-
import { AIService, AiToolApprovalMemoryService, defaultPruneMessagesForAgent } from '@c8y/ngx-components/ai';
|
|
6
|
+
import { defaultPruneMessagesForPersistence, toConversationMessages, AIService, AiToolApprovalMemoryService, fromConversationMessages, defaultPruneMessagesForAgent } from '@c8y/ngx-components/ai';
|
|
7
7
|
import { gettext } from '@c8y/ngx-components/gettext';
|
|
8
8
|
import { firstValueFrom, of, isObservable } from 'rxjs';
|
|
9
9
|
import { AiChatActionComponent, AiChatAssistantMessageComponent, AiChatComponent, AiChatSuggestionComponent, AiChatMessageComponent, AiChatMessageActionComponent } from '@c8y/ngx-components/ai/ai-chat';
|
|
10
10
|
import { CollapseModule } from 'ngx-bootstrap/collapse';
|
|
11
|
-
import * as
|
|
11
|
+
import * as i3 from 'ngx-bootstrap/tooltip';
|
|
12
12
|
import { TooltipModule } from 'ngx-bootstrap/tooltip';
|
|
13
13
|
import { CdkTrapFocus } from '@angular/cdk/a11y';
|
|
14
14
|
import * as i1 from 'ngx-bootstrap/dropdown';
|
|
15
15
|
import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
|
|
16
|
+
import * as i2 from '@angular/forms';
|
|
17
|
+
import { FormsModule } from '@angular/forms';
|
|
18
|
+
import { skip, debounceTime, distinctUntilChanged } from 'rxjs/operators';
|
|
19
|
+
import { toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
20
|
+
import { FetchClient } from '@c8y/client';
|
|
16
21
|
import { WidgetConfigService, WidgetConfigFeedbackComponent } from '@c8y/ngx-components/context-dashboard';
|
|
17
22
|
|
|
18
23
|
/** A stateless service used by agent-chat for managing chat history. Not public. */
|
|
@@ -129,7 +134,7 @@ class ChatHistoryService {
|
|
|
129
134
|
if (messagesArray.some(msg => {
|
|
130
135
|
if (!msg || typeof msg !== 'object' || !msg.role)
|
|
131
136
|
return true;
|
|
132
|
-
if (msg.role === 'assistant')
|
|
137
|
+
if (msg.role === 'assistant' || msg.role === 'tool')
|
|
133
138
|
return !Array.isArray(msg.content);
|
|
134
139
|
return typeof msg.content !== 'string';
|
|
135
140
|
})) {
|
|
@@ -351,6 +356,440 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
351
356
|
args: [{ selector: 'c8y-agent-chat-tool-approval-mode', changeDetection: ChangeDetectionStrategy.OnPush, imports: [C8yTranslatePipe, IconDirective, BsDropdownModule, CdkTrapFocus], host: { class: 'd-contents' }, template: "@let autoLabel = 'Auto' | translate;\n@let manualLabel = 'Manual' | translate;\n<!-- Escape must close only the menu; unstopped it also closes the drawer hosting the chat. -->\n<div\n class=\"dropdown\"\n dropdown\n [dropup]=\"true\"\n #toolApprovalDropdown=\"bs-dropdown\"\n [cdkTrapFocus]=\"toolApprovalDropdown.isOpen\"\n (onHidden)=\"toolApprovalToggle.focus()\"\n (keyup.escape)=\"toolApprovalDropdown.isOpen && $event.stopPropagation()\"\n (keydown.escape)=\"toolApprovalDropdown.isOpen && $event.stopPropagation()\"\n>\n <button\n class=\"dropdown-toggle btn btn-clean btn-sm d-flex a-i-center gap-4\"\n [title]=\"'Tool approval mode' | translate\"\n [attr.aria-expanded]=\"toolApprovalDropdown.isOpen\"\n type=\"button\"\n data-cy=\"chat-tool-approval-mode\"\n dropdownToggle\n #toolApprovalToggle\n >\n <i\n [c8yIcon]=\"globalToolApproval() ? 'bolt' : 'hand-paper-o'\"\n aria-hidden=\"true\"\n ></i>\n <span>{{ globalToolApproval() ? autoLabel : manualLabel }}</span>\n <i\n c8yIcon=\"caret-down\"\n aria-hidden=\"true\"\n ></i>\n </button>\n <ul\n class=\"dropdown-menu\"\n [attr.aria-label]=\"'Tool approval mode' | translate\"\n role=\"menu\"\n *dropdownMenu\n >\n <li\n [class.active]=\"!globalToolApproval()\"\n role=\"none\"\n >\n <button\n class=\"a-i-start\"\n [attr.aria-checked]=\"!globalToolApproval()\"\n role=\"menuitemradio\"\n type=\"button\"\n (click)=\"modeChange.emit(false)\"\n data-cy=\"chat-tool-approval-manual\"\n >\n <i\n [class.invisible]=\"globalToolApproval()\"\n class=\"icon-16 text-primary\"\n [c8yIcon]=\"'check'\"\n aria-hidden=\"true\"\n ></i>\n <i\n class=\"icon-16\"\n [c8yIcon]=\"'hand-paper-o'\"\n aria-hidden=\"true\"\n ></i>\n <span>\n {{ manualLabel }}\n <small class=\"d-block text-muted\">\n {{ 'Ask before running each tool' | translate }}\n </small>\n </span>\n </button>\n </li>\n <li\n [class.active]=\"globalToolApproval()\"\n role=\"none\"\n >\n <button\n class=\"a-i-start\"\n [attr.aria-checked]=\"globalToolApproval()\"\n role=\"menuitemradio\"\n type=\"button\"\n (click)=\"modeChange.emit(true)\"\n data-cy=\"chat-tool-approval-auto\"\n >\n <i\n class=\"text-primary icon-16\"\n [class.invisible]=\"!globalToolApproval()\"\n [c8yIcon]=\"'check'\"\n aria-hidden=\"true\"\n ></i>\n <i\n class=\"icon-16\"\n [c8yIcon]=\"'bolt'\"\n aria-hidden=\"true\"\n ></i>\n <span>\n {{ autoLabel }}\n <small class=\"d-block text-muted\">\n {{ 'Run tools without asking' | translate }}\n </small>\n </span>\n </button>\n </li>\n </ul>\n</div>\n" }]
|
|
352
357
|
}], propDecorators: { globalToolApproval: [{ type: i0.Input, args: [{ isSignal: true, alias: "globalToolApproval", required: true }] }], modeChange: [{ type: i0.Output, args: ["modeChange"] }] } });
|
|
353
358
|
|
|
359
|
+
const PAGE_SIZE = 5;
|
|
360
|
+
/** A request to the conversation storage API that the server rejected. */
|
|
361
|
+
class ConversationRequestError extends Error {
|
|
362
|
+
constructor(message, status) {
|
|
363
|
+
super(message);
|
|
364
|
+
this.name = 'ConversationRequestError';
|
|
365
|
+
this.status = status;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Throws a `ConversationRequestError` for an error response, with the server's message if it gave
|
|
370
|
+
* one, or else the fallback message.
|
|
371
|
+
*/
|
|
372
|
+
async function throwIfFailed(response, fallbackMessage) {
|
|
373
|
+
if (response.status <= 300)
|
|
374
|
+
return;
|
|
375
|
+
let serverMessage;
|
|
376
|
+
try {
|
|
377
|
+
serverMessage = (await response.json())?.message;
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
// Not every error response has a JSON body, e.g. when a proxy rejected the request.
|
|
381
|
+
}
|
|
382
|
+
throw new ConversationRequestError(typeof serverMessage === 'string' && serverMessage ? serverMessage : fallbackMessage, response.status);
|
|
383
|
+
}
|
|
384
|
+
class ConversationService {
|
|
385
|
+
constructor() {
|
|
386
|
+
this.baseUrl = '/service/ai';
|
|
387
|
+
this.fetchClient = inject(FetchClient);
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Lists the user's conversations, newest first.
|
|
391
|
+
*
|
|
392
|
+
* @param label Only return conversations carrying this label; all conversations when omitted.
|
|
393
|
+
* @param page Page to load, starting at 1.
|
|
394
|
+
* @param title Case-insensitive substring filter on the conversation title.
|
|
395
|
+
*/
|
|
396
|
+
async list(label, page = 1, title) {
|
|
397
|
+
const params = new URLSearchParams({
|
|
398
|
+
pageSize: String(PAGE_SIZE),
|
|
399
|
+
currentPage: String(page),
|
|
400
|
+
withTotalPages: 'true'
|
|
401
|
+
});
|
|
402
|
+
if (label) {
|
|
403
|
+
params.set('label', label);
|
|
404
|
+
}
|
|
405
|
+
if (title) {
|
|
406
|
+
params.set('title', title);
|
|
407
|
+
}
|
|
408
|
+
const response = await this.fetchClient.fetch(`${this.baseUrl}/conversation?${params}`);
|
|
409
|
+
await throwIfFailed(response, gettext('Failed to load conversations.'));
|
|
410
|
+
const body = await response.json();
|
|
411
|
+
const { conversations, statistics } = body;
|
|
412
|
+
return {
|
|
413
|
+
res: response,
|
|
414
|
+
data: conversations ?? [],
|
|
415
|
+
paging: {
|
|
416
|
+
currentPage: statistics.currentPage,
|
|
417
|
+
pageSize: statistics.pageSize,
|
|
418
|
+
totalPages: statistics.totalPages ?? statistics.currentPage,
|
|
419
|
+
nextPage: body.next ? statistics.currentPage + 1 : undefined,
|
|
420
|
+
next: body.next ? () => this.list(label, statistics.currentPage + 1, title) : undefined
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Loads a single conversation including its messages, which the list endpoint omits.
|
|
426
|
+
*
|
|
427
|
+
* @param id Identifier of the conversation to load.
|
|
428
|
+
*/
|
|
429
|
+
async detail(id) {
|
|
430
|
+
const response = await this.fetchClient.fetch(`${this.baseUrl}/conversation/${encodeURIComponent(id)}`);
|
|
431
|
+
await throwIfFailed(response, gettext('Failed to load the conversation.'));
|
|
432
|
+
return response.json();
|
|
433
|
+
}
|
|
434
|
+
async create(payload) {
|
|
435
|
+
const response = await this.fetchClient.fetch(`${this.baseUrl}/conversation`, {
|
|
436
|
+
method: 'POST',
|
|
437
|
+
headers: { 'Content-Type': 'application/json' },
|
|
438
|
+
body: JSON.stringify(payload)
|
|
439
|
+
});
|
|
440
|
+
await throwIfFailed(response, gettext('Failed to create conversation.'));
|
|
441
|
+
return response.json();
|
|
442
|
+
}
|
|
443
|
+
async update(id, payload) {
|
|
444
|
+
const response = await this.fetchClient.fetch(`${this.baseUrl}/conversation/${encodeURIComponent(id)}`, {
|
|
445
|
+
method: 'PUT',
|
|
446
|
+
headers: { 'Content-Type': 'application/json' },
|
|
447
|
+
body: JSON.stringify(payload)
|
|
448
|
+
});
|
|
449
|
+
await throwIfFailed(response, gettext('Failed to update conversation.'));
|
|
450
|
+
return response.json();
|
|
451
|
+
}
|
|
452
|
+
async delete(id) {
|
|
453
|
+
const response = await this.fetchClient.fetch(`${this.baseUrl}/conversation/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
454
|
+
await throwIfFailed(response, gettext('Failed to delete conversation.'));
|
|
455
|
+
}
|
|
456
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: ConversationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
457
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: ConversationService, providedIn: 'root' }); }
|
|
458
|
+
}
|
|
459
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: ConversationService, decorators: [{
|
|
460
|
+
type: Injectable,
|
|
461
|
+
args: [{
|
|
462
|
+
providedIn: 'root'
|
|
463
|
+
}]
|
|
464
|
+
}] });
|
|
465
|
+
|
|
466
|
+
class ConversationPickerComponent {
|
|
467
|
+
constructor() {
|
|
468
|
+
this.loadMoreConversationsLabel = gettext('Load more…');
|
|
469
|
+
/**
|
|
470
|
+
* Label of the feature hosting the chat. Lists are filtered by it, new conversations get it, and
|
|
471
|
+
* it is added to a conversation continued here that does not have it yet. When not set, all of
|
|
472
|
+
* the user's conversations are listed.
|
|
473
|
+
*/
|
|
474
|
+
this.label = input(undefined, ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
475
|
+
/** Name of the current agent — used as the title of a conversation without a text prompt. */
|
|
476
|
+
this.agentName = input('', ...(ngDevMode ? [{ debugName: "agentName" }] : /* istanbul ignore next */ []));
|
|
477
|
+
/** Optional pruner override forwarded from AgentChatComponent. */
|
|
478
|
+
this.pruneMessagesForPersistence = input(undefined, ...(ngDevMode ? [{ debugName: "pruneMessagesForPersistence" }] : /* istanbul ignore next */ []));
|
|
479
|
+
/** User clicked a conversation in the list — emitted with its messages loaded. */
|
|
480
|
+
this.conversationSelect = output();
|
|
481
|
+
/**
|
|
482
|
+
* The active conversation was deleted → parent should reset chat messages.
|
|
483
|
+
* Only emitted when the deleted conversation was the currently selected one.
|
|
484
|
+
*/
|
|
485
|
+
this.conversationDeleted = output();
|
|
486
|
+
/**
|
|
487
|
+
* The currently active conversation — owned by this component. Its messages are deliberately not
|
|
488
|
+
* tracked here: the live message list lives in the parent chat and is passed in via `messages`.
|
|
489
|
+
*/
|
|
490
|
+
this.selectedConversation = signal(undefined, ...(ngDevMode ? [{ debugName: "selectedConversation" }] : /* istanbul ignore next */ []));
|
|
491
|
+
this.conversationPage = signal(null, ...(ngDevMode ? [{ debugName: "conversationPage" }] : /* istanbul ignore next */ []));
|
|
492
|
+
this.isLoadingConversations = signal(false, ...(ngDevMode ? [{ debugName: "isLoadingConversations" }] : /* istanbul ignore next */ []));
|
|
493
|
+
this.isLoadingMore = signal(false, ...(ngDevMode ? [{ debugName: "isLoadingMore" }] : /* istanbul ignore next */ []));
|
|
494
|
+
this.searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : /* istanbul ignore next */ []));
|
|
495
|
+
this.deleteConfirmButtons = [
|
|
496
|
+
{ label: gettext('Cancel'), action: () => Promise.resolve(false) },
|
|
497
|
+
{ label: gettext('Delete'), status: 'danger', action: () => Promise.resolve(true) }
|
|
498
|
+
];
|
|
499
|
+
/**
|
|
500
|
+
* Bumped whenever the chat switches to another conversation or starts a new one, so a request
|
|
501
|
+
* that was started before the switch can tell it must no longer change the selection.
|
|
502
|
+
*/
|
|
503
|
+
this.selectionGeneration = 0;
|
|
504
|
+
/** Bumped by every list load, so only the latest one updates the list. */
|
|
505
|
+
this.listRequest = 0;
|
|
506
|
+
this.conversationService = inject(ConversationService);
|
|
507
|
+
this.alertService = inject(AlertService);
|
|
508
|
+
this.translateService = inject(TranslateService);
|
|
509
|
+
this.injector = inject(Injector);
|
|
510
|
+
this.historyToggle = viewChild('historyToggle', ...(ngDevMode ? [{ debugName: "historyToggle" }] : /* istanbul ignore next */ []));
|
|
511
|
+
// Reload conversations whenever the label changes (including initial render). The reload itself
|
|
512
|
+
// runs untracked: loadConversations() reads searchTerm() as an argument before its first await,
|
|
513
|
+
// and without untracked() that read would make this effect a second, non-debounced trigger for
|
|
514
|
+
// every keystroke in the search box, alongside the debounced _searchSubscription below.
|
|
515
|
+
this._labelEffect = effect(() => {
|
|
516
|
+
this.label();
|
|
517
|
+
untracked(() => void this.loadConversations());
|
|
518
|
+
}, ...(ngDevMode ? [{ debugName: "_labelEffect" }] : /* istanbul ignore next */ []));
|
|
519
|
+
// Debounced search reload when the user types.
|
|
520
|
+
this._searchSubscription = toObservable(this.searchTerm)
|
|
521
|
+
.pipe(skip(1), debounceTime(500), distinctUntilChanged(), takeUntilDestroyed())
|
|
522
|
+
.subscribe(() => void this.loadConversations());
|
|
523
|
+
}
|
|
524
|
+
/** Called by parent when a conversation is created or loaded, to sync this component's state. */
|
|
525
|
+
trackConversation(conv) {
|
|
526
|
+
this.selectionGeneration++;
|
|
527
|
+
this.selectedConversation.set(conv);
|
|
528
|
+
this.addToList(conv);
|
|
529
|
+
}
|
|
530
|
+
/** Called by parent when the chat is reset, to clear the active conversation. */
|
|
531
|
+
clearSelectedConversation() {
|
|
532
|
+
this.selectionGeneration++;
|
|
533
|
+
this.selectedConversation.set(undefined);
|
|
534
|
+
this.pendingConversationTitle = undefined;
|
|
535
|
+
}
|
|
536
|
+
/** Called by fork() in the parent to set a title prefix for the next new conversation. */
|
|
537
|
+
setPendingForkTitle(title) {
|
|
538
|
+
this.pendingConversationTitle = title;
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Called by the parent after each assistant response to persist the conversation.
|
|
542
|
+
*
|
|
543
|
+
* The messages are passed in rather than read from an input, because the parent persists
|
|
544
|
+
* synchronously after updating its message signal — an input binding would still hold the
|
|
545
|
+
* previous value at that point, losing whatever the finished response just added to the last
|
|
546
|
+
* message, such as its timestamp and token usage.
|
|
547
|
+
*
|
|
548
|
+
* A failed save is reported to the user with an alert, so the returned promise never rejects.
|
|
549
|
+
*
|
|
550
|
+
* @param chatMessages The current messages of the chat, as the parent holds them.
|
|
551
|
+
*/
|
|
552
|
+
async persistConversation(chatMessages) {
|
|
553
|
+
// The user can open another conversation or start a new one while the save is in flight.
|
|
554
|
+
const generation = this.selectionGeneration;
|
|
555
|
+
try {
|
|
556
|
+
const pruner = this.pruneMessagesForPersistence() ?? defaultPruneMessagesForPersistence;
|
|
557
|
+
const messages = toConversationMessages(pruner(chatMessages));
|
|
558
|
+
const pending = this.pendingCreate?.generation === generation ? this.pendingCreate.conversation : undefined;
|
|
559
|
+
const existing = this.selectedConversation() ?? (pending ? await pending : undefined);
|
|
560
|
+
if (existing?.id) {
|
|
561
|
+
await this.updateConversation(existing, messages, generation);
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
await this.createConversation(chatMessages, messages, generation);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
catch (ex) {
|
|
568
|
+
console.error('Error persisting conversation:', ex);
|
|
569
|
+
// 413: the stored conversation would exceed the size limit of the storage service.
|
|
570
|
+
const tooLarge = ex instanceof ConversationRequestError && ex.status === 413;
|
|
571
|
+
this.alertService.danger(tooLarge
|
|
572
|
+
? gettext('This conversation is too large to save. To keep saving, start a new conversation.')
|
|
573
|
+
: gettext('Failed to save the conversation.'));
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
async updateConversation(existing, messages, generation) {
|
|
577
|
+
const label = this.label();
|
|
578
|
+
const labels = existing.labels ?? [];
|
|
579
|
+
const missingLabel = !!label && !labels.includes(label);
|
|
580
|
+
const updated = await this.conversationService.update(existing.id, {
|
|
581
|
+
messages,
|
|
582
|
+
...(missingLabel ? { labels: [...labels, label] } : {})
|
|
583
|
+
});
|
|
584
|
+
if (missingLabel && generation === this.selectionGeneration) {
|
|
585
|
+
this.selectedConversation.set(updated);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
async createConversation(chatMessages, messages, generation) {
|
|
589
|
+
const label = this.label();
|
|
590
|
+
const creating = this.conversationService.create({
|
|
591
|
+
labels: label ? [label] : [],
|
|
592
|
+
title: this.generateConversationTitle(chatMessages),
|
|
593
|
+
messages
|
|
594
|
+
});
|
|
595
|
+
const conversation = creating.catch(() => undefined);
|
|
596
|
+
this.pendingCreate = { generation, conversation };
|
|
597
|
+
try {
|
|
598
|
+
const created = await creating;
|
|
599
|
+
// Selecting it after a switch would make the next message of the chat on screen overwrite it.
|
|
600
|
+
if (generation === this.selectionGeneration)
|
|
601
|
+
this.selectedConversation.set(created);
|
|
602
|
+
this.addToList(created);
|
|
603
|
+
}
|
|
604
|
+
finally {
|
|
605
|
+
if (this.pendingCreate?.conversation === conversation)
|
|
606
|
+
this.pendingCreate = undefined;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
addToList(conv) {
|
|
610
|
+
this.conversationPage.update(prev => {
|
|
611
|
+
if (!prev)
|
|
612
|
+
return { data: [conv] };
|
|
613
|
+
const exists = prev.data.some(c => c.id === conv.id);
|
|
614
|
+
return exists ? prev : { ...prev, data: [conv, ...prev.data] };
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
generateConversationTitle(chatMessages) {
|
|
618
|
+
if (this.pendingConversationTitle) {
|
|
619
|
+
const title = this.pendingConversationTitle;
|
|
620
|
+
this.pendingConversationTitle = undefined;
|
|
621
|
+
return title;
|
|
622
|
+
}
|
|
623
|
+
const firstUserMsg = chatMessages.find(m => m.role === 'user');
|
|
624
|
+
if (!firstUserMsg || typeof firstUserMsg.content !== 'string')
|
|
625
|
+
return this.agentName();
|
|
626
|
+
const text = firstUserMsg.content.trim();
|
|
627
|
+
return text.length > 30 ? text.slice(0, 30) : text;
|
|
628
|
+
}
|
|
629
|
+
onSearchInput(value) {
|
|
630
|
+
this.searchTerm.set(value);
|
|
631
|
+
}
|
|
632
|
+
clearSearch() {
|
|
633
|
+
this.searchTerm.set('');
|
|
634
|
+
}
|
|
635
|
+
async selectConversation(conv) {
|
|
636
|
+
if (!conv.id)
|
|
637
|
+
return;
|
|
638
|
+
try {
|
|
639
|
+
// The list omits messages, so the conversation has to be fetched before it can be opened.
|
|
640
|
+
this.conversationSelect.emit(await this.conversationService.detail(conv.id));
|
|
641
|
+
}
|
|
642
|
+
catch (ex) {
|
|
643
|
+
console.error('Error loading conversation:', ex);
|
|
644
|
+
this.alertService.danger(gettext('Failed to load the conversation.'));
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
async updateConversationTitle(title) {
|
|
648
|
+
const conv = this.selectedConversation();
|
|
649
|
+
if (!conv?.id || !title.trim())
|
|
650
|
+
return;
|
|
651
|
+
const generation = this.selectionGeneration;
|
|
652
|
+
try {
|
|
653
|
+
const updated = await this.conversationService.update(conv.id, { title: title.trim() });
|
|
654
|
+
if (generation === this.selectionGeneration)
|
|
655
|
+
this.selectedConversation.set(updated);
|
|
656
|
+
this.conversationPage.update(prev => prev ? { ...prev, data: prev.data.map(c => (c.id === updated.id ? updated : c)) } : prev);
|
|
657
|
+
}
|
|
658
|
+
catch (ex) {
|
|
659
|
+
console.error('Error updating conversation title:', ex);
|
|
660
|
+
this.alertService.danger(gettext('Failed to update the conversation title.'));
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
/** Deletes a conversation from the history once the user confirms in the given popover. */
|
|
664
|
+
async confirmAndDelete(popover, conv) {
|
|
665
|
+
if (!conv)
|
|
666
|
+
return;
|
|
667
|
+
// Read before the popover takes focus: it tells whether the delete starts from the list.
|
|
668
|
+
const menu = document.activeElement?.closest('.dropdown-menu');
|
|
669
|
+
const index = this.conversationPage()?.data.findIndex(c => c.id === conv.id) ?? -1;
|
|
670
|
+
popover.message = this.translateService.instant(gettext('You are about to delete conversation "{{ title }}". This action is irreversible.'), { title: conv.title });
|
|
671
|
+
// Clicking outside the popover rejects, which counts as cancelling.
|
|
672
|
+
const confirmed = await popover.show(this.deleteConfirmButtons).catch(() => false);
|
|
673
|
+
if (confirmed && (await this.deleteConversation(conv)))
|
|
674
|
+
this.focusAfterDelete(menu, index);
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Moves focus off the deleted conversation's controls, which are gone or disabled: in the list,
|
|
678
|
+
* to the row that took its place (or the search field once the list is empty); in the header,
|
|
679
|
+
* to the history button.
|
|
680
|
+
*/
|
|
681
|
+
focusAfterDelete(menu, index) {
|
|
682
|
+
afterNextRender(() => {
|
|
683
|
+
if (!menu?.isConnected) {
|
|
684
|
+
this.historyToggle()?.nativeElement.focus();
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const rows = menu.querySelectorAll('.conversation-picker__item > button');
|
|
688
|
+
const row = rows[Math.min(index, rows.length - 1)];
|
|
689
|
+
(row ?? menu.querySelector('input'))?.focus();
|
|
690
|
+
}, { injector: this.injector });
|
|
691
|
+
}
|
|
692
|
+
/** Returns whether the conversation was deleted. */
|
|
693
|
+
async deleteConversation(conv) {
|
|
694
|
+
if (!conv.id)
|
|
695
|
+
return false;
|
|
696
|
+
try {
|
|
697
|
+
await this.conversationService.delete(conv.id);
|
|
698
|
+
this.conversationPage.update(prev => prev ? { ...prev, data: prev.data.filter(c => c.id !== conv.id) } : prev);
|
|
699
|
+
if (this.selectedConversation()?.id === conv.id) {
|
|
700
|
+
this.selectedConversation.set(undefined);
|
|
701
|
+
this.conversationDeleted.emit();
|
|
702
|
+
}
|
|
703
|
+
this.alertService.success(gettext('Conversation deleted.'));
|
|
704
|
+
return true;
|
|
705
|
+
}
|
|
706
|
+
catch (ex) {
|
|
707
|
+
console.error('Error deleting conversation:', ex);
|
|
708
|
+
this.alertService.danger(gettext('Failed to delete the conversation.'));
|
|
709
|
+
return false;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
/** Appends the next page of conversations to the list. */
|
|
713
|
+
async loadMoreConversations() {
|
|
714
|
+
const current = this.conversationPage();
|
|
715
|
+
if (!current?.paging?.next)
|
|
716
|
+
return;
|
|
717
|
+
const loadMoreButton = document.activeElement;
|
|
718
|
+
const menu = loadMoreButton?.closest('.dropdown-menu');
|
|
719
|
+
this.isLoadingMore.set(true);
|
|
720
|
+
try {
|
|
721
|
+
const page = await current.paging.next();
|
|
722
|
+
// A search or reload replaced the list meanwhile, so this page belongs to an old one.
|
|
723
|
+
if (this.conversationPage() !== current)
|
|
724
|
+
return;
|
|
725
|
+
// Conversations created here were added on top and can show up again on a later page.
|
|
726
|
+
const known = new Set(current.data.map(c => c.id));
|
|
727
|
+
this.conversationPage.set({
|
|
728
|
+
...page,
|
|
729
|
+
data: [...current.data, ...page.data.filter(c => !known.has(c.id))]
|
|
730
|
+
});
|
|
731
|
+
this.focusFirstLoadedRow(menu, loadMoreButton, current.data.length);
|
|
732
|
+
}
|
|
733
|
+
catch (ex) {
|
|
734
|
+
console.error('Error loading more conversations:', ex);
|
|
735
|
+
this.alertService.danger(gettext('Failed to load more conversations.'));
|
|
736
|
+
}
|
|
737
|
+
finally {
|
|
738
|
+
this.isLoadingMore.set(false);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* The "Load more" button is gone after the last page, which would drop focus to the page, so
|
|
743
|
+
* move it to the first conversation that was just loaded.
|
|
744
|
+
*/
|
|
745
|
+
focusFirstLoadedRow(menu, loadMoreButton, index) {
|
|
746
|
+
afterNextRender(() => {
|
|
747
|
+
if (!menu?.isConnected || loadMoreButton?.isConnected)
|
|
748
|
+
return;
|
|
749
|
+
menu.querySelectorAll('.conversation-picker__item > button')[index]?.focus();
|
|
750
|
+
}, { injector: this.injector });
|
|
751
|
+
}
|
|
752
|
+
async loadConversations() {
|
|
753
|
+
// A slow response to an older search must not replace the result of a newer one.
|
|
754
|
+
const request = ++this.listRequest;
|
|
755
|
+
this.isLoadingConversations.set(true);
|
|
756
|
+
try {
|
|
757
|
+
const page = await this.conversationService.list(this.label(), 1, this.searchTerm() || undefined);
|
|
758
|
+
if (request === this.listRequest)
|
|
759
|
+
this.conversationPage.set(page);
|
|
760
|
+
}
|
|
761
|
+
catch (ex) {
|
|
762
|
+
console.error('Error loading conversations:', ex);
|
|
763
|
+
if (request === this.listRequest) {
|
|
764
|
+
this.alertService.danger(gettext('Failed to load conversations.'));
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
finally {
|
|
768
|
+
if (request === this.listRequest)
|
|
769
|
+
this.isLoadingConversations.set(false);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: ConversationPickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
773
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: ConversationPickerComponent, isStandalone: true, selector: "c8y-conversation-picker", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, agentName: { classPropertyName: "agentName", publicName: "agentName", isSignal: true, isRequired: false, transformFunction: null }, pruneMessagesForPersistence: { classPropertyName: "pruneMessagesForPersistence", publicName: "pruneMessagesForPersistence", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { conversationSelect: "conversationSelect", conversationDeleted: "conversationDeleted" }, viewQueries: [{ propertyName: "historyToggle", first: true, predicate: ["historyToggle"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"card-header d-flex a-i-center gap-4 separator flex-no-shrink\">\n <div class=\"flex-grow min-width-0\">\n @if (selectedConversation()) {\n <c8y-input-group-editable\n ariaLabel=\"Conversation title\"\n [ngModel]=\"selectedConversation()!.title\"\n formGroupClass=\"m-b-0\"\n [maxLength]=\"100\"\n (save)=\"updateConversationTitle($event)\"\n ></c8y-input-group-editable>\n } @else {\n <p class=\"conversation-picker__placeholder text-truncate text-muted\">\n {{ 'New conversation' | translate }}\n </p>\n }\n </div>\n\n <ng-content></ng-content>\n\n <div\n class=\"dropdown flex-no-shrink\"\n #convDropdown=\"bs-dropdown\"\n dropdown\n c8yDropdownFocusTrap\n [insideClick]=\"true\"\n >\n <button\n class=\"btn btn-icon dropdown-toggle\"\n [attr.aria-label]=\"'Conversation history' | translate\"\n [tooltip]=\"'Conversation history' | translate\"\n placement=\"top\"\n container=\"body\"\n type=\"button\"\n #historyToggle\n [adaptivePosition]=\"false\"\n [delay]=\"500\"\n dropdownToggle\n >\n <i c8yIcon=\"history\"></i>\n </button>\n <div\n class=\"dropdown-menu dropdown-menu-right conversation-picker__menu\"\n *dropdownMenu\n >\n <div class=\"sticky-top separator-bottom p-8\">\n <p class=\"p-b-8 p-l-8\">\n <strong>{{ 'Conversations' | translate }}</strong>\n </p>\n <div class=\"input-group input-group-search input-group-sm\">\n <input\n class=\"form-control\"\n [attr.aria-label]=\"'Search conversations' | translate\"\n type=\"text\"\n [value]=\"searchTerm()\"\n [placeholder]=\"'Search\u2026' | translate\"\n (input)=\"onSearchInput($any($event.target).value)\"\n />\n <span class=\"input-group-btn\">\n <button\n class=\"btn btn-clean\"\n [title]=\"(searchTerm() ? 'Clear search' : 'Search') | translate\"\n type=\"button\"\n data-dropdown-keep-open\n (click)=\"clearSearch()\"\n >\n <i [c8yIcon]=\"searchTerm() ? 'close' : 'search'\"></i>\n </button>\n </span>\n </div>\n </div>\n\n @if (isLoadingConversations()) {\n <div class=\"p-16 text-center\">\n <c8y-loading></c8y-loading>\n </div>\n } @else if ((conversationPage()?.data?.length ?? 0) === 0) {\n <!-- The list changes as the user types in the search, so announce the empty result. -->\n <div\n class=\"p-16\"\n role=\"status\"\n >\n @if (searchTerm()) {\n <c8y-ui-empty-state\n [icon]=\"'search'\"\n [title]=\"'No results to display.' | translate\"\n [subtitle]=\"'Adjust or clear the search.' | translate\"\n [horizontal]=\"true\"\n ></c8y-ui-empty-state>\n } @else {\n <c8y-ui-empty-state\n [icon]=\"'history'\"\n [title]=\"'No conversations to display.' | translate\"\n [horizontal]=\"true\"\n ></c8y-ui-empty-state>\n }\n </div>\n } @else {\n <ul class=\"list-unstyled m-0\">\n @for (conv of conversationPage()!.data; track conv.id) {\n @let isSelected = conv.id === selectedConversation()?.id;\n <li\n class=\"conversation-picker__item d-flex a-i-center gap-8 p-t-8 p-b-8 p-l-16 p-r-16 separator-bottom\"\n [class.conversation-picker__item--active]=\"isSelected\"\n >\n <button\n class=\"btn-clean d-col flex-grow min-width-0 text-left\"\n [attr.aria-current]=\"isSelected ? 'true' : null\"\n type=\"button\"\n (click)=\"convDropdown.hide(); selectConversation(conv)\"\n >\n <span class=\"text-truncate\">\n <c8y-highlight\n [text]=\"conv.title\"\n [pattern]=\"searchTerm()\"\n ></c8y-highlight>\n </span>\n @if (conv.updatedAt) {\n <small class=\"text-muted text-nowrap\">{{ conv.updatedAt | c8yDate }}</small>\n }\n </button>\n <c8y-popover-confirm\n class=\"flex-no-shrink showOnHover\"\n [class.open]=\"rowConfirm.isOpen\"\n [title]=\"'Delete conversation' | translate\"\n [placement]=\"'bottom'\"\n [outsideClick]=\"true\"\n #rowConfirm\n >\n <button\n class=\"btn-icon btn btn-sm\"\n [attr.aria-label]=\"'Delete conversation' | translate\"\n [tooltip]=\"'Delete conversation' | translate\"\n container=\"body\"\n aria-haspopup=\"dialog\"\n type=\"button\"\n [adaptivePosition]=\"false\"\n [delay]=\"500\"\n (click)=\"$event.stopPropagation(); confirmAndDelete(rowConfirm, conv)\"\n >\n <i c8yIcon=\"trash\"></i>\n </button>\n </c8y-popover-confirm>\n </li>\n }\n </ul>\n @if (conversationPage()?.paging?.next) {\n <button\n class=\"btn btn-default btn-block m-t-8 m-b-8\"\n [class.btn-pending]=\"isLoadingMore()\"\n type=\"button\"\n data-dropdown-keep-open\n [disabled]=\"isLoadingMore()\"\n (click)=\"loadMoreConversations()\"\n >\n {{ loadMoreConversationsLabel | translate }}\n </button>\n }\n }\n </div>\n </div>\n\n <c8y-popover-confirm\n class=\"flex-no-shrink\"\n [title]=\"'Delete conversation' | translate\"\n [placement]=\"'bottom'\"\n [outsideClick]=\"true\"\n [adaptivePosition]=\"true\"\n #headerConfirm\n >\n <button\n class=\"btn btn-icon\"\n [attr.aria-label]=\"'Delete conversation' | translate\"\n [tooltip]=\"'Delete conversation' | translate\"\n container=\"body\"\n aria-haspopup=\"dialog\"\n type=\"button\"\n [adaptivePosition]=\"false\"\n [delay]=\"500\"\n [disabled]=\"!selectedConversation()\"\n (click)=\"confirmAndDelete(headerConfirm, selectedConversation())\"\n >\n <i c8yIcon=\"trash\"></i>\n </button>\n </c8y-popover-confirm>\n</div>\n", styles: [".conversation-picker__placeholder{font-style:italic}.conversation-picker__menu{min-width:262px;max-width:min(400px,90vw)}.conversation-picker__item:hover{background-color:var(--c8y-component-background-hover, var(--c8y-root-component-background-hover))}.conversation-picker__item--active>button{color:var(--c8y-component-color-active, var(--c8y-root-component-color-active))}\n"], dependencies: [{ kind: "ngmodule", type: BsDropdownModule }, { kind: "directive", type: i1.BsDropdownMenuDirective, selector: "[bsDropdownMenu],[dropdownMenu]", exportAs: ["bs-dropdown-menu"] }, { kind: "directive", type: i1.BsDropdownToggleDirective, selector: "[bsDropdownToggle],[dropdownToggle]", exportAs: ["bs-dropdown-toggle"] }, { kind: "directive", type: i1.BsDropdownDirective, selector: "[bsDropdown], [dropdown]", inputs: ["placement", "triggers", "container", "dropup", "autoClose", "isAnimated", "insideClick", "isDisabled", "isOpen"], outputs: ["isOpenChange", "onShown", "onHidden"], exportAs: ["bs-dropdown"] }, { kind: "directive", type: DropdownFocusTrapDirective, selector: "[c8yDropdownFocusTrap]" }, { kind: "component", type: EmptyStateComponent, selector: "c8y-ui-empty-state", inputs: ["icon", "title", "subtitle", "horizontal"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: HighlightComponent, selector: "c8y-highlight", inputs: ["pattern", "text", "elementClass", "shouldTrimPattern"] }, { kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "component", type: InputGroupEditableComponent, selector: "c8y-input-group-editable", inputs: ["ariaLabel", "name", "size", "placeholder", "title", "multiline", "maxHeight", "maxLength", "noSaveButton", "formGroupClass", "validators", "asyncValidators", "autocomplete", "required"], outputs: ["save", "cancel"] }, { kind: "component", type: LoadingComponent, selector: "c8y-loading", inputs: ["layout", "progress", "progressLabel", "message"] }, { kind: "component", type: PopoverConfirmComponent, selector: "c8y-popover-confirm", inputs: ["buttons", "message", "title", "isOpen", "containerClass", "placement", "outsideClick", "adaptivePosition", "container"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i3.TooltipDirective, selector: "[tooltip], [tooltipHtml]", inputs: ["adaptivePosition", "tooltip", "placement", "triggers", "container", "containerClass", "boundariesElement", "isOpen", "isDisabled", "delay", "tooltipHtml", "tooltipPlacement", "tooltipIsOpen", "tooltipEnable", "tooltipAppendToBody", "tooltipAnimation", "tooltipClass", "tooltipContext", "tooltipPopupDelay", "tooltipFadeDuration", "tooltipTrigger"], outputs: ["tooltipChange", "onShown", "onHidden", "tooltipStateChanged"], exportAs: ["bs-tooltip"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: DatePipe, name: "c8yDate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
774
|
+
}
|
|
775
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: ConversationPickerComponent, decorators: [{
|
|
776
|
+
type: Component,
|
|
777
|
+
args: [{ selector: 'c8y-conversation-picker', changeDetection: ChangeDetectionStrategy.OnPush, imports: [
|
|
778
|
+
BsDropdownModule,
|
|
779
|
+
C8yTranslatePipe,
|
|
780
|
+
DatePipe,
|
|
781
|
+
DropdownFocusTrapDirective,
|
|
782
|
+
EmptyStateComponent,
|
|
783
|
+
FormsModule,
|
|
784
|
+
HighlightComponent,
|
|
785
|
+
IconDirective,
|
|
786
|
+
InputGroupEditableComponent,
|
|
787
|
+
LoadingComponent,
|
|
788
|
+
PopoverConfirmComponent,
|
|
789
|
+
TooltipModule
|
|
790
|
+
], template: "<div class=\"card-header d-flex a-i-center gap-4 separator flex-no-shrink\">\n <div class=\"flex-grow min-width-0\">\n @if (selectedConversation()) {\n <c8y-input-group-editable\n ariaLabel=\"Conversation title\"\n [ngModel]=\"selectedConversation()!.title\"\n formGroupClass=\"m-b-0\"\n [maxLength]=\"100\"\n (save)=\"updateConversationTitle($event)\"\n ></c8y-input-group-editable>\n } @else {\n <p class=\"conversation-picker__placeholder text-truncate text-muted\">\n {{ 'New conversation' | translate }}\n </p>\n }\n </div>\n\n <ng-content></ng-content>\n\n <div\n class=\"dropdown flex-no-shrink\"\n #convDropdown=\"bs-dropdown\"\n dropdown\n c8yDropdownFocusTrap\n [insideClick]=\"true\"\n >\n <button\n class=\"btn btn-icon dropdown-toggle\"\n [attr.aria-label]=\"'Conversation history' | translate\"\n [tooltip]=\"'Conversation history' | translate\"\n placement=\"top\"\n container=\"body\"\n type=\"button\"\n #historyToggle\n [adaptivePosition]=\"false\"\n [delay]=\"500\"\n dropdownToggle\n >\n <i c8yIcon=\"history\"></i>\n </button>\n <div\n class=\"dropdown-menu dropdown-menu-right conversation-picker__menu\"\n *dropdownMenu\n >\n <div class=\"sticky-top separator-bottom p-8\">\n <p class=\"p-b-8 p-l-8\">\n <strong>{{ 'Conversations' | translate }}</strong>\n </p>\n <div class=\"input-group input-group-search input-group-sm\">\n <input\n class=\"form-control\"\n [attr.aria-label]=\"'Search conversations' | translate\"\n type=\"text\"\n [value]=\"searchTerm()\"\n [placeholder]=\"'Search\u2026' | translate\"\n (input)=\"onSearchInput($any($event.target).value)\"\n />\n <span class=\"input-group-btn\">\n <button\n class=\"btn btn-clean\"\n [title]=\"(searchTerm() ? 'Clear search' : 'Search') | translate\"\n type=\"button\"\n data-dropdown-keep-open\n (click)=\"clearSearch()\"\n >\n <i [c8yIcon]=\"searchTerm() ? 'close' : 'search'\"></i>\n </button>\n </span>\n </div>\n </div>\n\n @if (isLoadingConversations()) {\n <div class=\"p-16 text-center\">\n <c8y-loading></c8y-loading>\n </div>\n } @else if ((conversationPage()?.data?.length ?? 0) === 0) {\n <!-- The list changes as the user types in the search, so announce the empty result. -->\n <div\n class=\"p-16\"\n role=\"status\"\n >\n @if (searchTerm()) {\n <c8y-ui-empty-state\n [icon]=\"'search'\"\n [title]=\"'No results to display.' | translate\"\n [subtitle]=\"'Adjust or clear the search.' | translate\"\n [horizontal]=\"true\"\n ></c8y-ui-empty-state>\n } @else {\n <c8y-ui-empty-state\n [icon]=\"'history'\"\n [title]=\"'No conversations to display.' | translate\"\n [horizontal]=\"true\"\n ></c8y-ui-empty-state>\n }\n </div>\n } @else {\n <ul class=\"list-unstyled m-0\">\n @for (conv of conversationPage()!.data; track conv.id) {\n @let isSelected = conv.id === selectedConversation()?.id;\n <li\n class=\"conversation-picker__item d-flex a-i-center gap-8 p-t-8 p-b-8 p-l-16 p-r-16 separator-bottom\"\n [class.conversation-picker__item--active]=\"isSelected\"\n >\n <button\n class=\"btn-clean d-col flex-grow min-width-0 text-left\"\n [attr.aria-current]=\"isSelected ? 'true' : null\"\n type=\"button\"\n (click)=\"convDropdown.hide(); selectConversation(conv)\"\n >\n <span class=\"text-truncate\">\n <c8y-highlight\n [text]=\"conv.title\"\n [pattern]=\"searchTerm()\"\n ></c8y-highlight>\n </span>\n @if (conv.updatedAt) {\n <small class=\"text-muted text-nowrap\">{{ conv.updatedAt | c8yDate }}</small>\n }\n </button>\n <c8y-popover-confirm\n class=\"flex-no-shrink showOnHover\"\n [class.open]=\"rowConfirm.isOpen\"\n [title]=\"'Delete conversation' | translate\"\n [placement]=\"'bottom'\"\n [outsideClick]=\"true\"\n #rowConfirm\n >\n <button\n class=\"btn-icon btn btn-sm\"\n [attr.aria-label]=\"'Delete conversation' | translate\"\n [tooltip]=\"'Delete conversation' | translate\"\n container=\"body\"\n aria-haspopup=\"dialog\"\n type=\"button\"\n [adaptivePosition]=\"false\"\n [delay]=\"500\"\n (click)=\"$event.stopPropagation(); confirmAndDelete(rowConfirm, conv)\"\n >\n <i c8yIcon=\"trash\"></i>\n </button>\n </c8y-popover-confirm>\n </li>\n }\n </ul>\n @if (conversationPage()?.paging?.next) {\n <button\n class=\"btn btn-default btn-block m-t-8 m-b-8\"\n [class.btn-pending]=\"isLoadingMore()\"\n type=\"button\"\n data-dropdown-keep-open\n [disabled]=\"isLoadingMore()\"\n (click)=\"loadMoreConversations()\"\n >\n {{ loadMoreConversationsLabel | translate }}\n </button>\n }\n }\n </div>\n </div>\n\n <c8y-popover-confirm\n class=\"flex-no-shrink\"\n [title]=\"'Delete conversation' | translate\"\n [placement]=\"'bottom'\"\n [outsideClick]=\"true\"\n [adaptivePosition]=\"true\"\n #headerConfirm\n >\n <button\n class=\"btn btn-icon\"\n [attr.aria-label]=\"'Delete conversation' | translate\"\n [tooltip]=\"'Delete conversation' | translate\"\n container=\"body\"\n aria-haspopup=\"dialog\"\n type=\"button\"\n [adaptivePosition]=\"false\"\n [delay]=\"500\"\n [disabled]=\"!selectedConversation()\"\n (click)=\"confirmAndDelete(headerConfirm, selectedConversation())\"\n >\n <i c8yIcon=\"trash\"></i>\n </button>\n </c8y-popover-confirm>\n</div>\n", styles: [".conversation-picker__placeholder{font-style:italic}.conversation-picker__menu{min-width:262px;max-width:min(400px,90vw)}.conversation-picker__item:hover{background-color:var(--c8y-component-background-hover, var(--c8y-root-component-background-hover))}.conversation-picker__item--active>button{color:var(--c8y-component-color-active, var(--c8y-root-component-color-active))}\n"] }]
|
|
791
|
+
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], agentName: [{ type: i0.Input, args: [{ isSignal: true, alias: "agentName", required: false }] }], pruneMessagesForPersistence: [{ type: i0.Input, args: [{ isSignal: true, alias: "pruneMessagesForPersistence", required: false }] }], conversationSelect: [{ type: i0.Output, args: ["conversationSelect"] }], conversationDeleted: [{ type: i0.Output, args: ["conversationDeleted"] }], historyToggle: [{ type: i0.ViewChild, args: ['historyToggle', { isSignal: true }] }] } });
|
|
792
|
+
|
|
354
793
|
/** User preference key for the global "approve all tools" option. */
|
|
355
794
|
const GLOBAL_TOOL_APPROVAL_PREFERENCE_KEY = 'ai-approve-all-tools';
|
|
356
795
|
class AgentChatComponent {
|
|
@@ -469,6 +908,14 @@ class AgentChatComponent {
|
|
|
469
908
|
* or to make what is sent to the agent different from what is rendered in the UI.
|
|
470
909
|
*/
|
|
471
910
|
this.pruneMessagesForAgent = input(undefined, ...(ngDevMode ? [{ debugName: "pruneMessagesForAgent" }] : /* istanbul ignore next */ []));
|
|
911
|
+
/**
|
|
912
|
+
* Optional function to override how the message history is prepared for persistence (conversation storage).
|
|
913
|
+
* Defaults to `defaultPruneMessagesForPersistence` which preserves tool call history while stripping
|
|
914
|
+
* transient streaming states and truncating large tool outputs.
|
|
915
|
+
*
|
|
916
|
+
* Unlike `pruneMessagesForAgent`, this should retain tool calls so stored conversations can be fully restored.
|
|
917
|
+
*/
|
|
918
|
+
this.pruneMessagesForPersistence = input(undefined, ...(ngDevMode ? [{ debugName: "pruneMessagesForPersistence" }] : /* istanbul ignore next */ []));
|
|
472
919
|
/** When true, skips the agent health check on initialization.
|
|
473
920
|
* Use this when the component is used with the test endpoint (snapshot mode) where the agent
|
|
474
921
|
* does not need to exist on the backend. Default: false. */
|
|
@@ -551,6 +998,26 @@ class AgentChatComponent {
|
|
|
551
998
|
return acc;
|
|
552
999
|
}, { inputTokens: 0, outputTokens: 0, totalTokens: 0 });
|
|
553
1000
|
}, ...(ngDevMode ? [{ debugName: "cumulativeUsage" }] : /* istanbul ignore next */ []));
|
|
1001
|
+
/**
|
|
1002
|
+
* The agent to show above each message, or undefined to show none. An agent is shown on an
|
|
1003
|
+
* assistant message only when it differs from the previous assistant message's agent — or, for
|
|
1004
|
+
* the first one, from the current agent — so a chat with a single agent shows no names. The
|
|
1005
|
+
* current agent is shown with its label, any other with its stored name.
|
|
1006
|
+
*/
|
|
1007
|
+
this.messageAgentLabels = computed(() => {
|
|
1008
|
+
const currentAgentName = this.agentName;
|
|
1009
|
+
const currentAgentLabel = this.agentDefinition?.label || currentAgentName;
|
|
1010
|
+
let previousAgentName = currentAgentName;
|
|
1011
|
+
return this.messages().map(message => {
|
|
1012
|
+
if (message.role !== 'assistant' || !message.agentName)
|
|
1013
|
+
return undefined;
|
|
1014
|
+
const changed = message.agentName !== previousAgentName;
|
|
1015
|
+
previousAgentName = message.agentName;
|
|
1016
|
+
if (!changed)
|
|
1017
|
+
return undefined;
|
|
1018
|
+
return message.agentName === currentAgentName ? currentAgentLabel : message.agentName;
|
|
1019
|
+
});
|
|
1020
|
+
}, ...(ngDevMode ? [{ debugName: "messageAgentLabels" }] : /* istanbul ignore next */ []));
|
|
554
1021
|
/**
|
|
555
1022
|
* Computed signal holding the last assistant message, which is the one which may be rapidly changing as we
|
|
556
1023
|
* stream it back. This allows us to update the UI reactively without affecting the rest of the message history.
|
|
@@ -596,6 +1063,8 @@ class AgentChatComponent {
|
|
|
596
1063
|
this.appState = inject(AppStateService);
|
|
597
1064
|
this.assistantMessageComponents = viewChildren(AiChatAssistantMessageComponent, ...(ngDevMode ? [{ debugName: "assistantMessageComponents" }] : /* istanbul ignore next */ []));
|
|
598
1065
|
this.chat = viewChild(AiChatComponent, ...(ngDevMode ? [{ debugName: "chat" }] : /* istanbul ignore next */ []));
|
|
1066
|
+
this.conversationPicker = viewChild(ConversationPickerComponent, ...(ngDevMode ? [{ debugName: "conversationPicker" }] : /* istanbul ignore next */ []));
|
|
1067
|
+
this.persistenceEnabled = computed(() => this.chatConfig()?.conversationPersistence === 'enabled', ...(ngDevMode ? [{ debugName: "persistenceEnabled" }] : /* istanbul ignore next */ []));
|
|
599
1068
|
/**
|
|
600
1069
|
* Stores client-supplied tool outputs to override tool results for a given toolCallId in the current assistant message.
|
|
601
1070
|
* Cleared at the start of each new message.
|
|
@@ -671,6 +1140,25 @@ class AgentChatComponent {
|
|
|
671
1140
|
}
|
|
672
1141
|
}
|
|
673
1142
|
}
|
|
1143
|
+
fork(userMessage) {
|
|
1144
|
+
if (!this.persistenceEnabled()) {
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
const index = this.messages().indexOf(userMessage);
|
|
1148
|
+
if (index === -1)
|
|
1149
|
+
return;
|
|
1150
|
+
const baseTitle = this.conversationPicker()?.selectedConversation()?.title ?? userMessage.content.slice(0, 30);
|
|
1151
|
+
const forkTitle = this.translateService.instant(gettext('Fork: {{ title }}'), {
|
|
1152
|
+
title: baseTitle
|
|
1153
|
+
});
|
|
1154
|
+
const trimmedMessages = this.messages().slice(0, index);
|
|
1155
|
+
this.resetMessages(); // clears header state
|
|
1156
|
+
this.conversationPicker()?.setPendingForkTitle(forkTitle);
|
|
1157
|
+
this.messages.set(trimmedMessages);
|
|
1158
|
+
this.onMessagesChange.emit(this.messages());
|
|
1159
|
+
this._restoredSuggestions = [];
|
|
1160
|
+
this.prompt = userMessage.content;
|
|
1161
|
+
}
|
|
674
1162
|
/** Rates an AI message as positive/negative, sending the feedback to Gainsight (unless telemetry is disabled) */
|
|
675
1163
|
async rate(assistantMessage, positive) {
|
|
676
1164
|
if (this.telemetry()) {
|
|
@@ -724,6 +1212,7 @@ class AgentChatComponent {
|
|
|
724
1212
|
this.toolApprovalMemory.endSession(this._chatSessionId);
|
|
725
1213
|
this._chatSessionId = this.toolApprovalMemory.newSessionId();
|
|
726
1214
|
this.messages.set([]);
|
|
1215
|
+
this.conversationPicker()?.clearSelectedConversation();
|
|
727
1216
|
this.onMessagesChange.emit(this.messages());
|
|
728
1217
|
}
|
|
729
1218
|
/** Removes the last user message and all assistant messages that follow it from the conversation. */
|
|
@@ -778,6 +1267,13 @@ class AgentChatComponent {
|
|
|
778
1267
|
}
|
|
779
1268
|
this._isLoading.set(false);
|
|
780
1269
|
}
|
|
1270
|
+
loadConversation(conv) {
|
|
1271
|
+
this.resetMessages();
|
|
1272
|
+
this.messages.set(fromConversationMessages(conv.messages));
|
|
1273
|
+
this.onMessagesChange.emit(this.messages());
|
|
1274
|
+
this._restoredSuggestions = [];
|
|
1275
|
+
this.conversationPicker()?.trackConversation(conv);
|
|
1276
|
+
}
|
|
781
1277
|
// Private methods go here:
|
|
782
1278
|
/**
|
|
783
1279
|
* Sends the current conversation to the agent and streams the next assistant message.
|
|
@@ -803,6 +1299,7 @@ class AgentChatComponent {
|
|
|
803
1299
|
}
|
|
804
1300
|
const currentAssistantMessage = {
|
|
805
1301
|
role: 'assistant',
|
|
1302
|
+
agentName: this.agentName,
|
|
806
1303
|
content: []
|
|
807
1304
|
};
|
|
808
1305
|
this.messages.set([...this.messages(), currentAssistantMessage]);
|
|
@@ -980,6 +1477,12 @@ class AgentChatComponent {
|
|
|
980
1477
|
await this._dispatchToAgent(false);
|
|
981
1478
|
}
|
|
982
1479
|
}
|
|
1480
|
+
/** Saves the conversation. Never rejects: the picker alerts the user when a save fails. */
|
|
1481
|
+
async persistConversation() {
|
|
1482
|
+
if (!this.persistenceEnabled())
|
|
1483
|
+
return;
|
|
1484
|
+
await this.conversationPicker()?.persistConversation(this.messages());
|
|
1485
|
+
}
|
|
983
1486
|
async initializeChat() {
|
|
984
1487
|
this._restoreChatHistory(this.initialChatHistory());
|
|
985
1488
|
if (this.skipHealthCheck()) {
|
|
@@ -1118,8 +1621,14 @@ class AgentChatComponent {
|
|
|
1118
1621
|
this._isLoading.set(false);
|
|
1119
1622
|
}
|
|
1120
1623
|
}
|
|
1121
|
-
// Replace streaming message with new immutable instance to trigger change detection
|
|
1624
|
+
// Replace streaming message with new immutable instance to trigger change detection.
|
|
1122
1625
|
this.messages.update(msgs => [...msgs.slice(0, -1), newMsg]);
|
|
1626
|
+
// Persist after messages.update: newMsg (with timestamp and usage set by handleMessageFinish)
|
|
1627
|
+
// is now in the signal, so this.messages() reflects the complete final state.
|
|
1628
|
+
if (updatedAssistantMsg.finishReason === 'stop' ||
|
|
1629
|
+
updatedAssistantMsg.finishReason === 'length') {
|
|
1630
|
+
void this.persistConversation();
|
|
1631
|
+
}
|
|
1123
1632
|
}
|
|
1124
1633
|
handleMessageFinish(currentAssistantMsg, updatedAssistantMsg) {
|
|
1125
1634
|
currentAssistantMsg.timestamp = new Date().toISOString();
|
|
@@ -1248,6 +1757,7 @@ class AgentChatComponent {
|
|
|
1248
1757
|
this.messages.set([...this.messages()]);
|
|
1249
1758
|
this.onMessagesChange.emit(this.messages());
|
|
1250
1759
|
this.onMessageFinish.emit(currentAssistantMessage);
|
|
1760
|
+
void this.persistConversation();
|
|
1251
1761
|
if (this.telemetry()) {
|
|
1252
1762
|
void this.getAnalyticsMetadataContext()
|
|
1253
1763
|
.then(analyticsContext => {
|
|
@@ -1276,8 +1786,11 @@ class AgentChatComponent {
|
|
|
1276
1786
|
this._isLoading.set(false);
|
|
1277
1787
|
}
|
|
1278
1788
|
}
|
|
1789
|
+
onConversationDeleted() {
|
|
1790
|
+
this.resetMessages();
|
|
1791
|
+
}
|
|
1279
1792
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: AgentChatComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1280
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: AgentChatComponent, isStandalone: true, selector: "c8y-agent-chat", inputs: { agent: { classPropertyName: "agent", publicName: "agent", isSignal: true, isRequired: true, transformFunction: null }, suggestions: { classPropertyName: "suggestions", publicName: "suggestions", isSignal: true, isRequired: false, transformFunction: null }, chatConfig: { classPropertyName: "chatConfig", publicName: "chatConfig", isSignal: true, isRequired: false, transformFunction: null }, welcomeTemplate: { classPropertyName: "welcomeTemplate", publicName: "welcomeTemplate", isSignal: true, isRequired: false, transformFunction: null }, autoCreateAgents: { classPropertyName: "autoCreateAgents", publicName: "autoCreateAgents", isSignal: true, isRequired: false, transformFunction: null }, variables: { classPropertyName: "variables", publicName: "variables", isSignal: true, isRequired: false, transformFunction: null }, contextPath: { classPropertyName: "contextPath", publicName: "contextPath", isSignal: true, isRequired: false, transformFunction: null }, assistantMessageComponent: { classPropertyName: "assistantMessageComponent", publicName: "assistantMessageComponent", isSignal: true, isRequired: false, transformFunction: null }, assistantMessageDisplayConfig: { classPropertyName: "assistantMessageDisplayConfig", publicName: "assistantMessageDisplayConfig", isSignal: true, isRequired: false, transformFunction: null }, preprocessAgentMessage: { classPropertyName: "preprocessAgentMessage", publicName: "preprocessAgentMessage", isSignal: true, isRequired: false, transformFunction: null }, pruneMessagesForAgent: { classPropertyName: "pruneMessagesForAgent", publicName: "pruneMessagesForAgent", isSignal: true, isRequired: false, transformFunction: null }, skipHealthCheck: { classPropertyName: "skipHealthCheck", publicName: "skipHealthCheck", isSignal: true, isRequired: false, transformFunction: null }, initialChatHistory: { classPropertyName: "initialChatHistory", publicName: "initialChatHistory", isSignal: true, isRequired: false, transformFunction: null }, groundingContextProvider: { classPropertyName: "groundingContextProvider", publicName: "groundingContextProvider", isSignal: true, isRequired: false, transformFunction: null }, userAnalyticsContext: { classPropertyName: "userAnalyticsContext", publicName: "userAnalyticsContext", isSignal: true, isRequired: false, transformFunction: null }, telemetry: { classPropertyName: "telemetry", publicName: "telemetry", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onMessageFinish: "onMessageFinish", onToolResult: "onToolResult", onFeedback: "onFeedback", onMessagesChange: "onMessagesChange" }, queries: [{ propertyName: "chatActions", predicate: AiChatActionComponent, isSignal: true }], viewQueries: [{ propertyName: "assistantMessageComponents", predicate: AiChatAssistantMessageComponent, descendants: true, isSignal: true }, { propertyName: "chat", first: true, predicate: AiChatComponent, descendants: true, isSignal: true }], usesOnChanges: true, ngImport: i0, template: "<!-- Instantiates any projected action-bar items so they can be queried and forwarded to c8y-ai-chat.\n The items render nothing here (their content is exposed as a template), only inside the action bar. -->\n<ng-content select=\"c8y-ai-chat-action\"></ng-content>\n\n@let agentHealthErrorMsg = _agentHealthError();\n@if (agentHealthErrorMsg) {\n @if (isLoadingAiResponse()) {\n <c8y-loading class=\"m-auto\"></c8y-loading>\n } @else {\n <c8y-ui-empty-state\n class=\"m-auto\"\n [icon]=\"'settings'\"\n [title]=\"'AI agent is not available.' | translate\"\n >\n <!-- Only the message is announced; the actions and links stay out of the live region. -->\n <span role=\"alert\">{{ agentHealthErrorMsg }}</span>\n\n @if (canCreate) {\n <div class=\"text-center m-t-16 m-b-16\">\n <button\n class=\"btn btn-primary\"\n (click)=\"createAgent()\"\n >\n {{ 'Create agent' | translate }}\n </button>\n </div>\n } @else {\n <p\n class=\"text-pre-wrap m-t-8\"\n data-cy=\"agent-health-detailed-messages\"\n >\n <small>{{ agentHealthDetailedMessages() }}</small>\n </p>\n }\n\n <p c8y-guide-docs>\n <small\n translate\n ngNonBindable\n >\n Find out more in the\n <a c8y-guide-href=\"/docs/ai\">user documentation</a>.\n </small>\n </p>\n </c8y-ui-empty-state>\n }\n}\n\n@if (!agentHealthErrorMsg) {\n @let showGlobalToolApprovalToggle = chatConfig()?.showGlobalToolApprovalToggle !== false;\n @if (showGlobalToolApprovalToggle && globalToolApproval()) {\n <div\n class=\"alert alert-info m-b-0 max-width-unset\"\n role=\"status\"\n data-cy=\"chat-global-tool-approval-banner\"\n >\n <p>\n {{\n 'Auto-approve is on. Tool calls run without asking until you turn this off. Tools requiring explicit approval will still ask.'\n | translate\n }}\n </p>\n </div>\n }\n <c8y-ai-chat\n (onMessage)=\"sendMessage($event)\"\n [isLoading]=\"isLoadingAiResponse()\"\n (onCancel)=\"cancel()\"\n [config]=\"chatConfig() ?? {}\"\n [prompt]=\"prompt\"\n [suggestionsTemplate]=\"suggestionsRef\"\n [welcomeTemplate]=\"welcomeTemplate()\"\n [actions]=\"chatActions()\"\n [cumulativeUsage]=\"chatConfig()?.showCumulativeUsage ? cumulativeUsage() : null\"\n >\n @let messages$ = messages();\n @for (message of messages$; track $index; let i = $index) {\n <c8y-ai-chat-message [message]=\"message\">\n @if (message.role !== 'user' && model) {\n <!-- Visually hidden label included when users copy-paste from the chat, e.g. to report issues. It's quite helpful to know the model. -->\n <span\n class=\"hidden-copy-label\"\n aria-hidden=\"true\"\n >\n {{ `(Using model: ${model})` }}</span\n >\n }\n\n @if (message.role === 'user') {\n <div\n class=\"message-content\"\n data-cy=\"user-message-content\"\n [innerHTML]=\"message.content | markdownToHtml | async\"\n ></div>\n } @else if (\n message.role === 'assistant' && isLoadingAiResponse() && i === messages$.length - 1\n ) {\n <div>\n <!-- Last assistant message uses reactive computed context -->\n <ng-container\n [ngComponentOutlet]=\"assistantMessageComponent()\"\n [ngComponentOutletInputs]=\"{ assistantMessageContext: lastAssistantMessageContext() }\"\n ></ng-container>\n </div>\n } @else {\n <ng-container\n [ngComponentOutlet]=\"assistantMessageComponent()\"\n [ngComponentOutletInputs]=\"{\n assistantMessageContext: {\n message: message,\n config: assistantMessageDisplayConfig(),\n isMessageLoading: isLoadingAiResponse() && i === messages$.length - 1,\n messageDisplayIndex: messages$.length - 1 - i,\n onToolApprovalResponse: respondToToolApproval,\n canRememberToolApproval: canRememberToolApproval\n }\n }\"\n ></ng-container>\n }\n\n @let isLastMessage = i === messages$.length - 1;\n\n @if (message.role === 'user') {\n <c8y-ai-chat-message-action\n icon=\"pencil\"\n [tooltip]=\"'Edit and resend this message' | translate\"\n [disabled]=\"isLoadingAiResponse()\"\n (click)=\"reprompt(message)\"\n ></c8y-ai-chat-message-action>\n }\n\n @if (\n message.role === 'assistant' &&\n i > 0 &&\n chatConfig()?.showUsagePerMessage &&\n message.usage\n ) {\n <c8y-ai-chat-message-action [custom]=\"true\">\n @let usageLabel =\n 'Input tokens: {{ input }}\\nOutput tokens: {{ output }}'\n | translate\n : {\n input: message.usage.inputTokens || 0 | c8yNumber: 'floor' : '1.0-0',\n output: message.usage.outputTokens || 0 | c8yNumber: 'floor' : '1.0-0'\n };\n <!-- The tooltip is hover-only, so repeat the breakdown as a label. -->\n <span\n class=\"tag tag--info m-l-auto\"\n [attr.aria-label]=\"usageLabel\"\n tabindex=\"0\"\n [tooltip]=\"usageLabel\"\n >\n {{ 'Tokens used: {{total}}' | translate: { total: message.usage.totalTokens || 0 |\n c8yNumber: 'floor' : '1.0-0' } }}\n </span>\n </c8y-ai-chat-message-action>\n }\n\n @if (message.role === 'assistant' && i > 0 && !isLoadingAiResponse() && telemetry()) {\n <c8y-ai-chat-message-action\n icon=\"thumbs-up\"\n [tooltip]=\"'This is useful' | translate\"\n [disabled]=\"isLoadingAiResponse()\"\n (click)=\"rate(message, true)\"\n ></c8y-ai-chat-message-action>\n }\n\n @if (message.role === 'assistant' && i > 0 && !isLoadingAiResponse() && telemetry()) {\n <c8y-ai-chat-message-action\n icon=\"thumbs-down\"\n [tooltip]=\"'This is not useful' | translate\"\n [disabled]=\"isLoadingAiResponse()\"\n (click)=\"rate(message, false)\"\n ></c8y-ai-chat-message-action>\n }\n\n @if (\n message.role === 'assistant' &&\n !isLoadingAiResponse() &&\n chatConfig()?.showDeleteAction &&\n isLastMessage\n ) {\n <c8y-ai-chat-message-action\n icon=\"delete-bin\"\n [tooltip]=\"'Delete last exchange' | translate\"\n [disabled]=\"isLoadingAiResponse()\"\n (click)=\"deleteLastExchange()\"\n ></c8y-ai-chat-message-action>\n }\n\n <!-- Only allow regenerating the last message, otherwise we could be deleting a lot of useful message history -->\n @if (message.role === 'assistant' && i > 0 && isLastMessage && !isLoadingAiResponse()) {\n <c8y-ai-chat-message-action\n icon=\"refresh\"\n [tooltip]=\"'Regenerate this response' | translate\"\n [disabled]=\"isLoadingAiResponse()\"\n (click)=\"reload(message)\"\n ></c8y-ai-chat-message-action>\n }\n </c8y-ai-chat-message>\n }\n\n @let agentErrorMsg = agentRequestError();\n @if (agentErrorMsg) {\n <c8y-ai-chat-message [message]=\"{ role: 'assistant', content: agentErrorMsg }\">\n <div\n class=\"alert alert-danger d-flex a-i-center gap-8\"\n role=\"alert\"\n >\n <!-- Since errors come from the backend the only translation is for the fallback error message supplied by the UI. -->\n <div class=\"flex-grow text-pre-wrap\">{{ agentErrorMsg | translate }}</div>\n </div>\n </c8y-ai-chat-message>\n }\n\n <ng-template #suggestionsRef>\n @if (!isLoadingAiResponse()) {\n <!-- As soon as we have any suggestions (even empty) these take priority over what we restored from history -->\n @let activeSuggestions = suggestions() === undefined ? _restoredSuggestions : suggestions();\n @for (suggestion of activeSuggestions; track $index) {\n <c8y-ai-chat-suggestion\n [icon]=\"suggestion.icon || 'c8y-bulb'\"\n [useAiButtons]=\"true\"\n [prompt]=\"suggestion.prompt\"\n [label]=\"suggestion.label ?? suggestion.prompt\"\n (suggestionClicked)=\"sendMessage($event)\"\n [disabled]=\"isLoadingAiResponse()\"\n ></c8y-ai-chat-suggestion>\n }\n }\n </ng-template>\n\n @if (showGlobalToolApprovalToggle) {\n <c8y-ai-chat-action placement=\"left\">\n <c8y-agent-chat-tool-approval-mode\n [globalToolApproval]=\"globalToolApproval()\"\n (modeChange)=\"setGlobalToolApproval($event)\"\n ></c8y-agent-chat-tool-approval-mode>\n </c8y-ai-chat-action>\n }\n </c8y-ai-chat>\n}\n", styles: ["@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}.fade-in-out{animation:fadeIn .2s ease-in}.fade-in-out.ng-leave-active{animation:fadeOut .2s ease-out}.hidden-copy-label{display:block;font-size:0;line-height:0;-webkit-user-select:text;user-select:text}\n"], dependencies: [{ kind: "component", type: AiChatComponent, selector: "c8y-ai-chat", inputs: ["isLoading", "disabled", "prompt", "suggestionsTemplate", "welcomeTemplate", "cumulativeUsage", "actions", "config"], outputs: ["onMessage", "onCancel"] }, { kind: "component", type: AiChatActionComponent, selector: "c8y-ai-chat-action", inputs: ["placement"] }, { kind: "component", type: AiChatSuggestionComponent, selector: "c8y-ai-chat-suggestion", inputs: ["label", "prompt", "icon", "useAiButtons", "disabled"], outputs: ["suggestionClicked"] }, { kind: "component", type: AiChatMessageComponent, selector: "c8y-ai-chat-message", inputs: ["role", "message"] }, { kind: "component", type: AiChatMessageActionComponent, selector: "c8y-ai-chat-message-action", inputs: ["custom", "disabled", "tooltip", "icon"], outputs: ["actionClicked"] }, { kind: "component", type: LoadingComponent, selector: "c8y-loading", inputs: ["layout", "progress", "progressLabel", "message"] }, { kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "component", type: EmptyStateComponent, selector: "c8y-ui-empty-state", inputs: ["icon", "title", "subtitle", "horizontal"] }, { kind: "ngmodule", type: CollapseModule }, { kind: "directive", type: C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "component", type: GuideDocsComponent, selector: "[c8y-guide-docs]" }, { kind: "directive", type: GuideHrefDirective, selector: "[c8y-guide-href]", inputs: ["c8y-guide-href"] }, { 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: "component", type: AgentChatToolApprovalModeComponent, selector: "c8y-agent-chat-tool-approval-mode", inputs: ["globalToolApproval"], outputs: ["modeChange"] }, { kind: "pipe", type: MarkdownToHtmlPipe, name: "markdownToHtml" }, { kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: NumberPipe, name: "c8yNumber" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1793
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: AgentChatComponent, isStandalone: true, selector: "c8y-agent-chat", inputs: { agent: { classPropertyName: "agent", publicName: "agent", isSignal: true, isRequired: true, transformFunction: null }, suggestions: { classPropertyName: "suggestions", publicName: "suggestions", isSignal: true, isRequired: false, transformFunction: null }, chatConfig: { classPropertyName: "chatConfig", publicName: "chatConfig", isSignal: true, isRequired: false, transformFunction: null }, welcomeTemplate: { classPropertyName: "welcomeTemplate", publicName: "welcomeTemplate", isSignal: true, isRequired: false, transformFunction: null }, autoCreateAgents: { classPropertyName: "autoCreateAgents", publicName: "autoCreateAgents", isSignal: true, isRequired: false, transformFunction: null }, variables: { classPropertyName: "variables", publicName: "variables", isSignal: true, isRequired: false, transformFunction: null }, contextPath: { classPropertyName: "contextPath", publicName: "contextPath", isSignal: true, isRequired: false, transformFunction: null }, assistantMessageComponent: { classPropertyName: "assistantMessageComponent", publicName: "assistantMessageComponent", isSignal: true, isRequired: false, transformFunction: null }, assistantMessageDisplayConfig: { classPropertyName: "assistantMessageDisplayConfig", publicName: "assistantMessageDisplayConfig", isSignal: true, isRequired: false, transformFunction: null }, preprocessAgentMessage: { classPropertyName: "preprocessAgentMessage", publicName: "preprocessAgentMessage", isSignal: true, isRequired: false, transformFunction: null }, pruneMessagesForAgent: { classPropertyName: "pruneMessagesForAgent", publicName: "pruneMessagesForAgent", isSignal: true, isRequired: false, transformFunction: null }, pruneMessagesForPersistence: { classPropertyName: "pruneMessagesForPersistence", publicName: "pruneMessagesForPersistence", isSignal: true, isRequired: false, transformFunction: null }, skipHealthCheck: { classPropertyName: "skipHealthCheck", publicName: "skipHealthCheck", isSignal: true, isRequired: false, transformFunction: null }, initialChatHistory: { classPropertyName: "initialChatHistory", publicName: "initialChatHistory", isSignal: true, isRequired: false, transformFunction: null }, groundingContextProvider: { classPropertyName: "groundingContextProvider", publicName: "groundingContextProvider", isSignal: true, isRequired: false, transformFunction: null }, userAnalyticsContext: { classPropertyName: "userAnalyticsContext", publicName: "userAnalyticsContext", isSignal: true, isRequired: false, transformFunction: null }, telemetry: { classPropertyName: "telemetry", publicName: "telemetry", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onMessageFinish: "onMessageFinish", onToolResult: "onToolResult", onFeedback: "onFeedback", onMessagesChange: "onMessagesChange" }, queries: [{ propertyName: "chatActions", predicate: AiChatActionComponent, isSignal: true }], viewQueries: [{ propertyName: "assistantMessageComponents", predicate: AiChatAssistantMessageComponent, descendants: true, isSignal: true }, { propertyName: "chat", first: true, predicate: AiChatComponent, descendants: true, isSignal: true }, { propertyName: "conversationPicker", first: true, predicate: ConversationPickerComponent, descendants: true, isSignal: true }], usesOnChanges: true, ngImport: i0, template: "<!-- Instantiates any projected action-bar items so they can be queried and forwarded to c8y-ai-chat.\n The items render nothing here (their content is exposed as a template), only inside the action bar. -->\n<ng-content select=\"c8y-ai-chat-action\"></ng-content>\n\n@let agentHealthErrorMsg = _agentHealthError();\n@if (agentHealthErrorMsg) {\n @if (isLoadingAiResponse()) {\n <c8y-loading class=\"m-auto\"></c8y-loading>\n } @else {\n <c8y-ui-empty-state\n class=\"m-auto\"\n [icon]=\"'settings'\"\n [title]=\"'AI agent is not available.' | translate\"\n >\n <!-- Only the message is announced; the actions and links stay out of the live region. -->\n <span role=\"alert\">{{ agentHealthErrorMsg }}</span>\n\n @if (canCreate) {\n <div class=\"text-center m-t-16 m-b-16\">\n <button\n class=\"btn btn-primary\"\n (click)=\"createAgent()\"\n >\n {{ 'Create agent' | translate }}\n </button>\n </div>\n } @else {\n <p\n class=\"text-pre-wrap m-t-8\"\n data-cy=\"agent-health-detailed-messages\"\n >\n <small>{{ agentHealthDetailedMessages() }}</small>\n </p>\n }\n\n <p c8y-guide-docs>\n <small\n translate\n ngNonBindable\n >\n Find out more in the\n <a c8y-guide-href=\"/docs/ai\">user documentation</a>.\n </small>\n </p>\n </c8y-ui-empty-state>\n }\n}\n\n@if (!agentHealthErrorMsg) {\n @if (persistenceEnabled()) {\n <c8y-conversation-picker\n [label]=\"chatConfig()?.conversationLabel\"\n [agentName]=\"agentName\"\n [pruneMessagesForPersistence]=\"pruneMessagesForPersistence()\"\n (conversationSelect)=\"loadConversation($event)\"\n (conversationDeleted)=\"onConversationDeleted()\"\n >\n @if (messages().length > 0) {\n <button\n class=\"btn btn-icon flex-no-shrink\"\n [attr.aria-label]=\"'New conversation' | translate\"\n [tooltip]=\"'New conversation' | translate\"\n placement=\"top\"\n container=\"body\"\n type=\"button\"\n [adaptivePosition]=\"false\"\n [delay]=\"500\"\n [disabled]=\"isLoadingAiResponse()\"\n (click)=\"resetMessages()\"\n >\n <i c8yIcon=\"add-circle-outline\"></i>\n </button>\n }\n </c8y-conversation-picker>\n }\n\n @let showGlobalToolApprovalToggle = chatConfig()?.showGlobalToolApprovalToggle !== false;\n @if (showGlobalToolApprovalToggle && globalToolApproval()) {\n <div\n class=\"alert alert-info m-b-0 max-width-unset\"\n role=\"status\"\n data-cy=\"chat-global-tool-approval-banner\"\n >\n <p>\n {{\n 'Auto-approve is on. Tool calls run without asking until you turn this off. Tools requiring explicit approval will still ask.'\n | translate\n }}\n </p>\n </div>\n }\n <c8y-ai-chat\n (onMessage)=\"sendMessage($event)\"\n [isLoading]=\"isLoadingAiResponse()\"\n (onCancel)=\"cancel()\"\n [config]=\"chatConfig() ?? {}\"\n [prompt]=\"prompt\"\n [suggestionsTemplate]=\"suggestionsRef\"\n [welcomeTemplate]=\"welcomeTemplate()\"\n [actions]=\"chatActions()\"\n [cumulativeUsage]=\"chatConfig()?.showCumulativeUsage ? cumulativeUsage() : null\"\n >\n @let messages$ = messages();\n @for (message of messages$; track $index; let i = $index) {\n <c8y-ai-chat-message\n [message]=\"message\"\n [agentLabel]=\"messageAgentLabels()[i]\"\n >\n @if (message.role !== 'user' && model) {\n <!-- Visually hidden label included when users copy-paste from the chat, e.g. to report issues. It's quite helpful to know the model. -->\n <span\n class=\"hidden-copy-label\"\n aria-hidden=\"true\"\n >\n {{ `(Using model: ${model})` }}</span\n >\n }\n\n @if (message.role === 'user') {\n <div\n class=\"message-content\"\n data-cy=\"user-message-content\"\n [innerHTML]=\"message.content | markdownToHtml | async\"\n ></div>\n } @else if (\n message.role === 'assistant' && isLoadingAiResponse() && i === messages$.length - 1\n ) {\n <div>\n <!-- Last assistant message uses reactive computed context -->\n <ng-container\n [ngComponentOutlet]=\"assistantMessageComponent()\"\n [ngComponentOutletInputs]=\"{ assistantMessageContext: lastAssistantMessageContext() }\"\n ></ng-container>\n </div>\n } @else {\n <ng-container\n [ngComponentOutlet]=\"assistantMessageComponent()\"\n [ngComponentOutletInputs]=\"{\n assistantMessageContext: {\n message: message,\n config: assistantMessageDisplayConfig(),\n isMessageLoading: isLoadingAiResponse() && i === messages$.length - 1,\n messageDisplayIndex: messages$.length - 1 - i,\n onToolApprovalResponse: respondToToolApproval,\n canRememberToolApproval: canRememberToolApproval\n }\n }\"\n ></ng-container>\n }\n\n @let isLastMessage = i === messages$.length - 1;\n\n @if (message.role === 'user') {\n <c8y-ai-chat-message-action\n icon=\"pencil\"\n [tooltipText]=\"'Edit and resend this message' | translate\"\n [disabled]=\"isLoadingAiResponse()\"\n (actionClicked)=\"reprompt(message)\"\n ></c8y-ai-chat-message-action>\n }\n\n @if (message.role === 'user' && persistenceEnabled()) {\n <c8y-ai-chat-message-action\n icon=\"codefork\"\n [tooltipText]=\"'Fork this conversation from this message' | translate\"\n [disabled]=\"isLoadingAiResponse()\"\n (actionClicked)=\"fork(message)\"\n ></c8y-ai-chat-message-action>\n }\n\n @if (\n message.role === 'assistant' &&\n i > 0 &&\n chatConfig()?.showUsagePerMessage &&\n message.usage\n ) {\n <c8y-ai-chat-message-action [custom]=\"true\">\n @let usageLabel =\n 'Input tokens: {{ input }}\\nOutput tokens: {{ output }}'\n | translate\n : {\n input: message.usage.inputTokens || 0 | c8yNumber: 'floor' : '1.0-0',\n output: message.usage.outputTokens || 0 | c8yNumber: 'floor' : '1.0-0'\n };\n <!-- The tooltip is hover-only, so repeat the breakdown as a label. -->\n <span\n class=\"tag tag--info m-l-auto\"\n [attr.aria-label]=\"usageLabel\"\n tabindex=\"0\"\n [tooltip]=\"usageLabel\"\n >\n {{ 'Tokens used: {{total}}' | translate: { total: message.usage.totalTokens || 0 |\n c8yNumber: 'floor' : '1.0-0' } }}\n </span>\n </c8y-ai-chat-message-action>\n }\n\n @if (message.role === 'assistant' && i > 0 && !isLoadingAiResponse() && telemetry()) {\n <c8y-ai-chat-message-action\n icon=\"thumbs-up\"\n [tooltipText]=\"'This is useful' | translate\"\n [disabled]=\"isLoadingAiResponse()\"\n (actionClicked)=\"rate(message, true)\"\n ></c8y-ai-chat-message-action>\n }\n\n @if (message.role === 'assistant' && i > 0 && !isLoadingAiResponse() && telemetry()) {\n <c8y-ai-chat-message-action\n icon=\"thumbs-down\"\n [tooltipText]=\"'This is not useful' | translate\"\n [disabled]=\"isLoadingAiResponse()\"\n (actionClicked)=\"rate(message, false)\"\n ></c8y-ai-chat-message-action>\n }\n\n @if (\n message.role === 'assistant' &&\n !isLoadingAiResponse() &&\n chatConfig()?.showDeleteAction &&\n isLastMessage\n ) {\n <c8y-ai-chat-message-action\n icon=\"delete-bin\"\n [tooltipText]=\"'Delete last exchange' | translate\"\n [disabled]=\"isLoadingAiResponse()\"\n (actionClicked)=\"deleteLastExchange()\"\n ></c8y-ai-chat-message-action>\n }\n\n <!-- Only allow regenerating the last message, otherwise we could be deleting a lot of useful message history -->\n @if (message.role === 'assistant' && i > 0 && isLastMessage && !isLoadingAiResponse()) {\n <c8y-ai-chat-message-action\n icon=\"refresh\"\n [tooltipText]=\"'Regenerate this response' | translate\"\n [disabled]=\"isLoadingAiResponse()\"\n (actionClicked)=\"reload(message)\"\n ></c8y-ai-chat-message-action>\n }\n </c8y-ai-chat-message>\n }\n\n @let agentErrorMsg = agentRequestError();\n @if (agentErrorMsg) {\n <c8y-ai-chat-message [message]=\"{ role: 'assistant', content: agentErrorMsg }\">\n <div\n class=\"alert alert-danger d-flex a-i-center gap-8\"\n role=\"alert\"\n >\n <!-- Since errors come from the backend the only translation is for the fallback error message supplied by the UI. -->\n <div class=\"flex-grow text-pre-wrap\">{{ agentErrorMsg | translate }}</div>\n </div>\n </c8y-ai-chat-message>\n }\n\n <ng-template #suggestionsRef>\n @if (!isLoadingAiResponse()) {\n <!-- As soon as we have any suggestions (even empty) these take priority over what we restored from history -->\n @let activeSuggestions = suggestions() === undefined ? _restoredSuggestions : suggestions();\n @for (suggestion of activeSuggestions; track $index) {\n <c8y-ai-chat-suggestion\n [icon]=\"suggestion.icon || 'c8y-bulb'\"\n [useAiButtons]=\"true\"\n [prompt]=\"suggestion.prompt\"\n [label]=\"suggestion.label ?? suggestion.prompt\"\n (suggestionClicked)=\"sendMessage($event)\"\n [disabled]=\"isLoadingAiResponse()\"\n ></c8y-ai-chat-suggestion>\n }\n }\n </ng-template>\n\n @if (showGlobalToolApprovalToggle) {\n <c8y-ai-chat-action placement=\"left\">\n <c8y-agent-chat-tool-approval-mode\n [globalToolApproval]=\"globalToolApproval()\"\n (modeChange)=\"setGlobalToolApproval($event)\"\n ></c8y-agent-chat-tool-approval-mode>\n </c8y-ai-chat-action>\n }\n </c8y-ai-chat>\n}\n", styles: ["@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}.fade-in-out{animation:fadeIn .2s ease-in}.fade-in-out.ng-leave-active{animation:fadeOut .2s ease-out}.hidden-copy-label{display:block;font-size:0;line-height:0;-webkit-user-select:text;user-select:text}\n"], dependencies: [{ kind: "component", type: AiChatComponent, selector: "c8y-ai-chat", inputs: ["isLoading", "disabled", "prompt", "suggestionsTemplate", "welcomeTemplate", "cumulativeUsage", "actions", "config"], outputs: ["onMessage", "onCancel"] }, { kind: "component", type: AiChatActionComponent, selector: "c8y-ai-chat-action", inputs: ["placement"] }, { kind: "component", type: AiChatSuggestionComponent, selector: "c8y-ai-chat-suggestion", inputs: ["label", "prompt", "icon", "useAiButtons", "disabled"], outputs: ["suggestionClicked"] }, { kind: "component", type: AiChatMessageComponent, selector: "c8y-ai-chat-message", inputs: ["role", "message", "agentLabel"] }, { kind: "component", type: AiChatMessageActionComponent, selector: "c8y-ai-chat-message-action", inputs: ["custom", "disabled", "tooltipText", "tooltip", "icon"], outputs: ["actionClicked"] }, { kind: "component", type: LoadingComponent, selector: "c8y-loading", inputs: ["layout", "progress", "progressLabel", "message"] }, { kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "component", type: EmptyStateComponent, selector: "c8y-ui-empty-state", inputs: ["icon", "title", "subtitle", "horizontal"] }, { kind: "ngmodule", type: CollapseModule }, { kind: "directive", type: C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "component", type: GuideDocsComponent, selector: "[c8y-guide-docs]" }, { kind: "directive", type: GuideHrefDirective, selector: "[c8y-guide-href]", inputs: ["c8y-guide-href"] }, { kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i3.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: "component", type: AgentChatToolApprovalModeComponent, selector: "c8y-agent-chat-tool-approval-mode", inputs: ["globalToolApproval"], outputs: ["modeChange"] }, { kind: "component", type: ConversationPickerComponent, selector: "c8y-conversation-picker", inputs: ["label", "agentName", "pruneMessagesForPersistence"], outputs: ["conversationSelect", "conversationDeleted"] }, { kind: "pipe", type: MarkdownToHtmlPipe, name: "markdownToHtml" }, { kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: NumberPipe, name: "c8yNumber" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1281
1794
|
}
|
|
1282
1795
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: AgentChatComponent, decorators: [{
|
|
1283
1796
|
type: Component,
|
|
@@ -1297,11 +1810,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
1297
1810
|
C8yTranslateDirective,
|
|
1298
1811
|
GuideDocsComponent,
|
|
1299
1812
|
GuideHrefDirective,
|
|
1813
|
+
IconDirective,
|
|
1300
1814
|
TooltipModule,
|
|
1301
1815
|
NumberPipe,
|
|
1302
|
-
AgentChatToolApprovalModeComponent
|
|
1303
|
-
|
|
1304
|
-
}], ctorParameters: () => [], propDecorators: { agent: [{ type: i0.Input, args: [{ isSignal: true, alias: "agent", required: true }] }], suggestions: [{ type: i0.Input, args: [{ isSignal: true, alias: "suggestions", required: false }] }], chatConfig: [{ type: i0.Input, args: [{ isSignal: true, alias: "chatConfig", required: false }] }], welcomeTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "welcomeTemplate", required: false }] }], chatActions: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => AiChatActionComponent), { isSignal: true }] }], autoCreateAgents: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoCreateAgents", required: false }] }], variables: [{ type: i0.Input, args: [{ isSignal: true, alias: "variables", required: false }] }], contextPath: [{ type: i0.Input, args: [{ isSignal: true, alias: "contextPath", required: false }] }], assistantMessageComponent: [{ type: i0.Input, args: [{ isSignal: true, alias: "assistantMessageComponent", required: false }] }], assistantMessageDisplayConfig: [{ type: i0.Input, args: [{ isSignal: true, alias: "assistantMessageDisplayConfig", required: false }] }], preprocessAgentMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "preprocessAgentMessage", required: false }] }], pruneMessagesForAgent: [{ type: i0.Input, args: [{ isSignal: true, alias: "pruneMessagesForAgent", required: false }] }], skipHealthCheck: [{ type: i0.Input, args: [{ isSignal: true, alias: "skipHealthCheck", required: false }] }], initialChatHistory: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialChatHistory", required: false }] }], groundingContextProvider: [{ type: i0.Input, args: [{ isSignal: true, alias: "groundingContextProvider", required: false }] }], userAnalyticsContext: [{ type: i0.Input, args: [{ isSignal: true, alias: "userAnalyticsContext", required: false }] }], telemetry: [{ type: i0.Input, args: [{ isSignal: true, alias: "telemetry", required: false }] }], onMessageFinish: [{ type: i0.Output, args: ["onMessageFinish"] }], onToolResult: [{ type: i0.Output, args: ["onToolResult"] }], onFeedback: [{ type: i0.Output, args: ["onFeedback"] }], onMessagesChange: [{ type: i0.Output, args: ["onMessagesChange"] }], assistantMessageComponents: [{ type: i0.ViewChildren, args: [i0.forwardRef(() => AiChatAssistantMessageComponent), { isSignal: true }] }], chat: [{ type: i0.ViewChild, args: [i0.forwardRef(() => AiChatComponent), { isSignal: true }] }] } });
|
|
1816
|
+
AgentChatToolApprovalModeComponent,
|
|
1817
|
+
ConversationPickerComponent
|
|
1818
|
+
], template: "<!-- Instantiates any projected action-bar items so they can be queried and forwarded to c8y-ai-chat.\n The items render nothing here (their content is exposed as a template), only inside the action bar. -->\n<ng-content select=\"c8y-ai-chat-action\"></ng-content>\n\n@let agentHealthErrorMsg = _agentHealthError();\n@if (agentHealthErrorMsg) {\n @if (isLoadingAiResponse()) {\n <c8y-loading class=\"m-auto\"></c8y-loading>\n } @else {\n <c8y-ui-empty-state\n class=\"m-auto\"\n [icon]=\"'settings'\"\n [title]=\"'AI agent is not available.' | translate\"\n >\n <!-- Only the message is announced; the actions and links stay out of the live region. -->\n <span role=\"alert\">{{ agentHealthErrorMsg }}</span>\n\n @if (canCreate) {\n <div class=\"text-center m-t-16 m-b-16\">\n <button\n class=\"btn btn-primary\"\n (click)=\"createAgent()\"\n >\n {{ 'Create agent' | translate }}\n </button>\n </div>\n } @else {\n <p\n class=\"text-pre-wrap m-t-8\"\n data-cy=\"agent-health-detailed-messages\"\n >\n <small>{{ agentHealthDetailedMessages() }}</small>\n </p>\n }\n\n <p c8y-guide-docs>\n <small\n translate\n ngNonBindable\n >\n Find out more in the\n <a c8y-guide-href=\"/docs/ai\">user documentation</a>.\n </small>\n </p>\n </c8y-ui-empty-state>\n }\n}\n\n@if (!agentHealthErrorMsg) {\n @if (persistenceEnabled()) {\n <c8y-conversation-picker\n [label]=\"chatConfig()?.conversationLabel\"\n [agentName]=\"agentName\"\n [pruneMessagesForPersistence]=\"pruneMessagesForPersistence()\"\n (conversationSelect)=\"loadConversation($event)\"\n (conversationDeleted)=\"onConversationDeleted()\"\n >\n @if (messages().length > 0) {\n <button\n class=\"btn btn-icon flex-no-shrink\"\n [attr.aria-label]=\"'New conversation' | translate\"\n [tooltip]=\"'New conversation' | translate\"\n placement=\"top\"\n container=\"body\"\n type=\"button\"\n [adaptivePosition]=\"false\"\n [delay]=\"500\"\n [disabled]=\"isLoadingAiResponse()\"\n (click)=\"resetMessages()\"\n >\n <i c8yIcon=\"add-circle-outline\"></i>\n </button>\n }\n </c8y-conversation-picker>\n }\n\n @let showGlobalToolApprovalToggle = chatConfig()?.showGlobalToolApprovalToggle !== false;\n @if (showGlobalToolApprovalToggle && globalToolApproval()) {\n <div\n class=\"alert alert-info m-b-0 max-width-unset\"\n role=\"status\"\n data-cy=\"chat-global-tool-approval-banner\"\n >\n <p>\n {{\n 'Auto-approve is on. Tool calls run without asking until you turn this off. Tools requiring explicit approval will still ask.'\n | translate\n }}\n </p>\n </div>\n }\n <c8y-ai-chat\n (onMessage)=\"sendMessage($event)\"\n [isLoading]=\"isLoadingAiResponse()\"\n (onCancel)=\"cancel()\"\n [config]=\"chatConfig() ?? {}\"\n [prompt]=\"prompt\"\n [suggestionsTemplate]=\"suggestionsRef\"\n [welcomeTemplate]=\"welcomeTemplate()\"\n [actions]=\"chatActions()\"\n [cumulativeUsage]=\"chatConfig()?.showCumulativeUsage ? cumulativeUsage() : null\"\n >\n @let messages$ = messages();\n @for (message of messages$; track $index; let i = $index) {\n <c8y-ai-chat-message\n [message]=\"message\"\n [agentLabel]=\"messageAgentLabels()[i]\"\n >\n @if (message.role !== 'user' && model) {\n <!-- Visually hidden label included when users copy-paste from the chat, e.g. to report issues. It's quite helpful to know the model. -->\n <span\n class=\"hidden-copy-label\"\n aria-hidden=\"true\"\n >\n {{ `(Using model: ${model})` }}</span\n >\n }\n\n @if (message.role === 'user') {\n <div\n class=\"message-content\"\n data-cy=\"user-message-content\"\n [innerHTML]=\"message.content | markdownToHtml | async\"\n ></div>\n } @else if (\n message.role === 'assistant' && isLoadingAiResponse() && i === messages$.length - 1\n ) {\n <div>\n <!-- Last assistant message uses reactive computed context -->\n <ng-container\n [ngComponentOutlet]=\"assistantMessageComponent()\"\n [ngComponentOutletInputs]=\"{ assistantMessageContext: lastAssistantMessageContext() }\"\n ></ng-container>\n </div>\n } @else {\n <ng-container\n [ngComponentOutlet]=\"assistantMessageComponent()\"\n [ngComponentOutletInputs]=\"{\n assistantMessageContext: {\n message: message,\n config: assistantMessageDisplayConfig(),\n isMessageLoading: isLoadingAiResponse() && i === messages$.length - 1,\n messageDisplayIndex: messages$.length - 1 - i,\n onToolApprovalResponse: respondToToolApproval,\n canRememberToolApproval: canRememberToolApproval\n }\n }\"\n ></ng-container>\n }\n\n @let isLastMessage = i === messages$.length - 1;\n\n @if (message.role === 'user') {\n <c8y-ai-chat-message-action\n icon=\"pencil\"\n [tooltipText]=\"'Edit and resend this message' | translate\"\n [disabled]=\"isLoadingAiResponse()\"\n (actionClicked)=\"reprompt(message)\"\n ></c8y-ai-chat-message-action>\n }\n\n @if (message.role === 'user' && persistenceEnabled()) {\n <c8y-ai-chat-message-action\n icon=\"codefork\"\n [tooltipText]=\"'Fork this conversation from this message' | translate\"\n [disabled]=\"isLoadingAiResponse()\"\n (actionClicked)=\"fork(message)\"\n ></c8y-ai-chat-message-action>\n }\n\n @if (\n message.role === 'assistant' &&\n i > 0 &&\n chatConfig()?.showUsagePerMessage &&\n message.usage\n ) {\n <c8y-ai-chat-message-action [custom]=\"true\">\n @let usageLabel =\n 'Input tokens: {{ input }}\\nOutput tokens: {{ output }}'\n | translate\n : {\n input: message.usage.inputTokens || 0 | c8yNumber: 'floor' : '1.0-0',\n output: message.usage.outputTokens || 0 | c8yNumber: 'floor' : '1.0-0'\n };\n <!-- The tooltip is hover-only, so repeat the breakdown as a label. -->\n <span\n class=\"tag tag--info m-l-auto\"\n [attr.aria-label]=\"usageLabel\"\n tabindex=\"0\"\n [tooltip]=\"usageLabel\"\n >\n {{ 'Tokens used: {{total}}' | translate: { total: message.usage.totalTokens || 0 |\n c8yNumber: 'floor' : '1.0-0' } }}\n </span>\n </c8y-ai-chat-message-action>\n }\n\n @if (message.role === 'assistant' && i > 0 && !isLoadingAiResponse() && telemetry()) {\n <c8y-ai-chat-message-action\n icon=\"thumbs-up\"\n [tooltipText]=\"'This is useful' | translate\"\n [disabled]=\"isLoadingAiResponse()\"\n (actionClicked)=\"rate(message, true)\"\n ></c8y-ai-chat-message-action>\n }\n\n @if (message.role === 'assistant' && i > 0 && !isLoadingAiResponse() && telemetry()) {\n <c8y-ai-chat-message-action\n icon=\"thumbs-down\"\n [tooltipText]=\"'This is not useful' | translate\"\n [disabled]=\"isLoadingAiResponse()\"\n (actionClicked)=\"rate(message, false)\"\n ></c8y-ai-chat-message-action>\n }\n\n @if (\n message.role === 'assistant' &&\n !isLoadingAiResponse() &&\n chatConfig()?.showDeleteAction &&\n isLastMessage\n ) {\n <c8y-ai-chat-message-action\n icon=\"delete-bin\"\n [tooltipText]=\"'Delete last exchange' | translate\"\n [disabled]=\"isLoadingAiResponse()\"\n (actionClicked)=\"deleteLastExchange()\"\n ></c8y-ai-chat-message-action>\n }\n\n <!-- Only allow regenerating the last message, otherwise we could be deleting a lot of useful message history -->\n @if (message.role === 'assistant' && i > 0 && isLastMessage && !isLoadingAiResponse()) {\n <c8y-ai-chat-message-action\n icon=\"refresh\"\n [tooltipText]=\"'Regenerate this response' | translate\"\n [disabled]=\"isLoadingAiResponse()\"\n (actionClicked)=\"reload(message)\"\n ></c8y-ai-chat-message-action>\n }\n </c8y-ai-chat-message>\n }\n\n @let agentErrorMsg = agentRequestError();\n @if (agentErrorMsg) {\n <c8y-ai-chat-message [message]=\"{ role: 'assistant', content: agentErrorMsg }\">\n <div\n class=\"alert alert-danger d-flex a-i-center gap-8\"\n role=\"alert\"\n >\n <!-- Since errors come from the backend the only translation is for the fallback error message supplied by the UI. -->\n <div class=\"flex-grow text-pre-wrap\">{{ agentErrorMsg | translate }}</div>\n </div>\n </c8y-ai-chat-message>\n }\n\n <ng-template #suggestionsRef>\n @if (!isLoadingAiResponse()) {\n <!-- As soon as we have any suggestions (even empty) these take priority over what we restored from history -->\n @let activeSuggestions = suggestions() === undefined ? _restoredSuggestions : suggestions();\n @for (suggestion of activeSuggestions; track $index) {\n <c8y-ai-chat-suggestion\n [icon]=\"suggestion.icon || 'c8y-bulb'\"\n [useAiButtons]=\"true\"\n [prompt]=\"suggestion.prompt\"\n [label]=\"suggestion.label ?? suggestion.prompt\"\n (suggestionClicked)=\"sendMessage($event)\"\n [disabled]=\"isLoadingAiResponse()\"\n ></c8y-ai-chat-suggestion>\n }\n }\n </ng-template>\n\n @if (showGlobalToolApprovalToggle) {\n <c8y-ai-chat-action placement=\"left\">\n <c8y-agent-chat-tool-approval-mode\n [globalToolApproval]=\"globalToolApproval()\"\n (modeChange)=\"setGlobalToolApproval($event)\"\n ></c8y-agent-chat-tool-approval-mode>\n </c8y-ai-chat-action>\n }\n </c8y-ai-chat>\n}\n", styles: ["@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}.fade-in-out{animation:fadeIn .2s ease-in}.fade-in-out.ng-leave-active{animation:fadeOut .2s ease-out}.hidden-copy-label{display:block;font-size:0;line-height:0;-webkit-user-select:text;user-select:text}\n"] }]
|
|
1819
|
+
}], ctorParameters: () => [], propDecorators: { agent: [{ type: i0.Input, args: [{ isSignal: true, alias: "agent", required: true }] }], suggestions: [{ type: i0.Input, args: [{ isSignal: true, alias: "suggestions", required: false }] }], chatConfig: [{ type: i0.Input, args: [{ isSignal: true, alias: "chatConfig", required: false }] }], welcomeTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "welcomeTemplate", required: false }] }], chatActions: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => AiChatActionComponent), { isSignal: true }] }], autoCreateAgents: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoCreateAgents", required: false }] }], variables: [{ type: i0.Input, args: [{ isSignal: true, alias: "variables", required: false }] }], contextPath: [{ type: i0.Input, args: [{ isSignal: true, alias: "contextPath", required: false }] }], assistantMessageComponent: [{ type: i0.Input, args: [{ isSignal: true, alias: "assistantMessageComponent", required: false }] }], assistantMessageDisplayConfig: [{ type: i0.Input, args: [{ isSignal: true, alias: "assistantMessageDisplayConfig", required: false }] }], preprocessAgentMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "preprocessAgentMessage", required: false }] }], pruneMessagesForAgent: [{ type: i0.Input, args: [{ isSignal: true, alias: "pruneMessagesForAgent", required: false }] }], pruneMessagesForPersistence: [{ type: i0.Input, args: [{ isSignal: true, alias: "pruneMessagesForPersistence", required: false }] }], skipHealthCheck: [{ type: i0.Input, args: [{ isSignal: true, alias: "skipHealthCheck", required: false }] }], initialChatHistory: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialChatHistory", required: false }] }], groundingContextProvider: [{ type: i0.Input, args: [{ isSignal: true, alias: "groundingContextProvider", required: false }] }], userAnalyticsContext: [{ type: i0.Input, args: [{ isSignal: true, alias: "userAnalyticsContext", required: false }] }], telemetry: [{ type: i0.Input, args: [{ isSignal: true, alias: "telemetry", required: false }] }], onMessageFinish: [{ type: i0.Output, args: ["onMessageFinish"] }], onToolResult: [{ type: i0.Output, args: ["onToolResult"] }], onFeedback: [{ type: i0.Output, args: ["onFeedback"] }], onMessagesChange: [{ type: i0.Output, args: ["onMessagesChange"] }], assistantMessageComponents: [{ type: i0.ViewChildren, args: [i0.forwardRef(() => AiChatAssistantMessageComponent), { isSignal: true }] }], chat: [{ type: i0.ViewChild, args: [i0.forwardRef(() => AiChatComponent), { isSignal: true }] }], conversationPicker: [{ type: i0.ViewChild, args: [i0.forwardRef(() => ConversationPickerComponent), { isSignal: true }] }] } });
|
|
1305
1820
|
|
|
1306
1821
|
class WidgetAiChatSectionComponent {
|
|
1307
1822
|
constructor() {
|
|
@@ -1387,7 +1902,7 @@ class WidgetAiChatSectionComponent {
|
|
|
1387
1902
|
}
|
|
1388
1903
|
}
|
|
1389
1904
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: WidgetAiChatSectionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1390
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.19", type: WidgetAiChatSectionComponent, isStandalone: true, selector: "c8y-widget-ai-chat-section", inputs: { loadComponentConfig: "loadComponentConfig", suggestions: "suggestions", agent: "agent", chatConfig: "chatConfig", useContextAsVariable: "useContextAsVariable", contextVariableName: "contextVariableName", variables: "variables", onToolResult: "onToolResult" }, ngImport: i0, template: "<c8y-widget-config-feedback>\n <div\n class=\"m-l-4 btn-ai btn-ai-hint btn-sm\"\n [title]=\"'AI code assistant' | translate\"\n >\n <span></span>\n </div>\n</c8y-widget-config-feedback>\n\n<c8y-agent-chat\n #agentChat\n [chatConfig]=\"chatConfig\"\n [suggestions]=\"agentChat.isWelcoming() ? suggestions : []\"\n [agent]=\"agent\"\n [variables]=\"variables | async\"\n (onToolResult)=\"toolResultHandler($event)\"\n [preprocessAgentMessage]=\"_componentConfig().preprocessAgentMessage\"\n [pruneMessagesForAgent]=\"_componentConfig().pruneMessagesForAgent\"\n [assistantMessageDisplayConfig]=\"_componentConfig().assistantMessageDisplayConfig || {}\"\n></c8y-agent-chat>\n", dependencies: [{ kind: "component", type: AgentChatComponent, selector: "c8y-agent-chat", inputs: ["agent", "suggestions", "chatConfig", "welcomeTemplate", "autoCreateAgents", "variables", "contextPath", "assistantMessageComponent", "assistantMessageDisplayConfig", "preprocessAgentMessage", "pruneMessagesForAgent", "skipHealthCheck", "initialChatHistory", "groundingContextProvider", "userAnalyticsContext", "telemetry"], outputs: ["onMessageFinish", "onToolResult", "onFeedback", "onMessagesChange"] }, { kind: "component", type: WidgetConfigFeedbackComponent, selector: "c8y-widget-config-feedback" }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.Eager }); }
|
|
1905
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.19", type: WidgetAiChatSectionComponent, isStandalone: true, selector: "c8y-widget-ai-chat-section", inputs: { loadComponentConfig: "loadComponentConfig", suggestions: "suggestions", agent: "agent", chatConfig: "chatConfig", useContextAsVariable: "useContextAsVariable", contextVariableName: "contextVariableName", variables: "variables", onToolResult: "onToolResult" }, ngImport: i0, template: "<c8y-widget-config-feedback>\n <div\n class=\"m-l-4 btn-ai btn-ai-hint btn-sm\"\n [title]=\"'AI code assistant' | translate\"\n >\n <span></span>\n </div>\n</c8y-widget-config-feedback>\n\n<c8y-agent-chat\n #agentChat\n [chatConfig]=\"chatConfig\"\n [suggestions]=\"agentChat.isWelcoming() ? suggestions : []\"\n [agent]=\"agent\"\n [variables]=\"variables | async\"\n (onToolResult)=\"toolResultHandler($event)\"\n [preprocessAgentMessage]=\"_componentConfig().preprocessAgentMessage\"\n [pruneMessagesForAgent]=\"_componentConfig().pruneMessagesForAgent\"\n [assistantMessageDisplayConfig]=\"_componentConfig().assistantMessageDisplayConfig || {}\"\n></c8y-agent-chat>\n", dependencies: [{ kind: "component", type: AgentChatComponent, selector: "c8y-agent-chat", inputs: ["agent", "suggestions", "chatConfig", "welcomeTemplate", "autoCreateAgents", "variables", "contextPath", "assistantMessageComponent", "assistantMessageDisplayConfig", "preprocessAgentMessage", "pruneMessagesForAgent", "pruneMessagesForPersistence", "skipHealthCheck", "initialChatHistory", "groundingContextProvider", "userAnalyticsContext", "telemetry"], outputs: ["onMessageFinish", "onToolResult", "onFeedback", "onMessagesChange"] }, { kind: "component", type: WidgetConfigFeedbackComponent, selector: "c8y-widget-config-feedback" }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.Eager }); }
|
|
1391
1906
|
}
|
|
1392
1907
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: WidgetAiChatSectionComponent, decorators: [{
|
|
1393
1908
|
type: Component,
|
|
@@ -1414,5 +1929,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
1414
1929
|
* Generated bundle index. Do not edit.
|
|
1415
1930
|
*/
|
|
1416
1931
|
|
|
1417
|
-
export { AgentChatComponent, AgentChatToolApprovalModeComponent, WidgetAiChatSectionComponent };
|
|
1932
|
+
export { AgentChatComponent, AgentChatToolApprovalModeComponent, ConversationPickerComponent, ConversationRequestError, ConversationService, WidgetAiChatSectionComponent };
|
|
1418
1933
|
//# sourceMappingURL=c8y-ngx-components-ai-agent-chat.mjs.map
|