@sinequa/assistant 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/chat/chat-message/chat-message.component.d.ts +157 -0
  2. package/chat/chat-reference/chat-reference.component.d.ts +118 -0
  3. package/chat/chat-settings-v3/chat-settings-v3.component.d.ts +41 -0
  4. package/chat/chat.component.d.ts +1112 -0
  5. package/chat/chat.service.d.ts +1046 -0
  6. package/chat/format-icon/format-icon.component.d.ts +10 -0
  7. package/chat/format-icon/icons.d.ts +5 -0
  8. package/chat/index.d.ts +5 -0
  9. package/chat/initials-avatar/initials-avatar.component.d.ts +35 -0
  10. package/chat/instance-manager.service.d.ts +28 -0
  11. package/chat/messages/de.d.ts +4 -0
  12. package/chat/messages/en.d.ts +4 -0
  13. package/chat/messages/fr.d.ts +4 -0
  14. package/chat/messages/index.d.ts +4 -0
  15. package/chat/public-api.d.ts +11 -0
  16. package/chat/rest-chat.service.d.ts +28 -0
  17. package/chat/saved-chats/saved-chats.component.d.ts +37 -0
  18. package/chat/styles/assistant.scss +61 -0
  19. package/chat/styles/references.scss +23 -0
  20. package/chat/types.d.ts +1241 -0
  21. package/chat/websocket-chat.service.d.ts +97 -0
  22. package/esm2020/chat/chat-message/chat-message.component.mjs +181 -0
  23. package/esm2020/chat/chat-reference/chat-reference.component.mjs +40 -0
  24. package/esm2020/chat/chat-settings-v3/chat-settings-v3.component.mjs +103 -0
  25. package/esm2020/chat/chat.component.mjs +369 -0
  26. package/esm2020/chat/chat.service.mjs +185 -0
  27. package/esm2020/chat/format-icon/format-icon.component.mjs +23 -0
  28. package/esm2020/chat/format-icon/icons.mjs +138 -0
  29. package/esm2020/chat/initials-avatar/initials-avatar.component.mjs +60 -0
  30. package/esm2020/chat/instance-manager.service.mjs +46 -0
  31. package/esm2020/chat/messages/de.mjs +4 -0
  32. package/esm2020/chat/messages/en.mjs +4 -0
  33. package/esm2020/chat/messages/fr.mjs +4 -0
  34. package/esm2020/chat/messages/index.mjs +9 -0
  35. package/esm2020/chat/public-api.mjs +12 -0
  36. package/esm2020/chat/rest-chat.service.mjs +164 -0
  37. package/esm2020/chat/saved-chats/saved-chats.component.mjs +132 -0
  38. package/esm2020/chat/sinequa-assistant-chat.mjs +5 -0
  39. package/esm2020/chat/types.mjs +85 -0
  40. package/esm2020/chat/websocket-chat.service.mjs +430 -0
  41. package/esm2020/public-api.mjs +3 -0
  42. package/esm2020/sinequa-assistant.mjs +5 -0
  43. package/fesm2015/sinequa-assistant-chat.mjs +1925 -0
  44. package/fesm2015/sinequa-assistant-chat.mjs.map +1 -0
  45. package/fesm2015/sinequa-assistant.mjs +9 -0
  46. package/fesm2015/sinequa-assistant.mjs.map +1 -0
  47. package/fesm2020/sinequa-assistant-chat.mjs +1911 -0
  48. package/fesm2020/sinequa-assistant-chat.mjs.map +1 -0
  49. package/fesm2020/sinequa-assistant.mjs +9 -0
  50. package/fesm2020/sinequa-assistant.mjs.map +1 -0
  51. package/index.d.ts +5 -0
  52. package/package.json +46 -0
  53. package/public-api.d.ts +1 -0
@@ -0,0 +1,1911 @@
1
+ import * as i0 from '@angular/core';
2
+ import { Injectable, EventEmitter, inject, Component, Input, Output, ViewEncapsulation, ChangeDetectionStrategy, ChangeDetectorRef, ViewChild, ContentChild } from '@angular/core';
3
+ import { Subscription, filter, tap, switchMap, BehaviorSubject, Subject, forkJoin, map, catchError, throwError, shareReplay, fromEvent, merge, takeUntil, finalize } from 'rxjs';
4
+ import * as i3 from '@sinequa/core/web-services';
5
+ import { PrincipalWebService, UserSettingsWebService, AuditWebService, SignalRWebService, JsonMethodPluginService } from '@sinequa/core/web-services';
6
+ import * as i1 from '@angular/common';
7
+ import { CommonModule } from '@angular/common';
8
+ import * as i2 from '@angular/forms';
9
+ import { FormsModule } from '@angular/forms';
10
+ import { LoginService, AuthenticationService } from '@sinequa/core/login';
11
+ import { Action } from '@sinequa/components/action';
12
+ import { AbstractFacet } from '@sinequa/components/facet';
13
+ import * as i1$1 from '@sinequa/components/search';
14
+ import { SearchService } from '@sinequa/components/search';
15
+ import { UserPreferences } from '@sinequa/components/user-settings';
16
+ import { NotificationsService } from '@sinequa/core/notification';
17
+ import { z } from 'zod';
18
+ import { AppService } from '@sinequa/core/app-utils';
19
+ import { IntlService } from '@sinequa/core/intl';
20
+ import get from 'lodash/get';
21
+ import { Utils } from '@sinequa/core/base';
22
+ import { HttpTransportType, LogLevel } from '@microsoft/signalr';
23
+ import { unified } from 'unified';
24
+ import remarkParse from 'remark-parse';
25
+ import { visit, CONTINUE, EXIT } from 'unist-util-visit';
26
+ import * as i2$1 from '@sinequa/components/utils';
27
+ import { UtilsModule } from '@sinequa/components/utils';
28
+ import remarkGfm from 'remark-gfm';
29
+ import * as i5 from '@sinequa/components/collapse';
30
+ import { CollapseModule } from '@sinequa/components/collapse';
31
+ import * as i6 from 'ngx-remark';
32
+ import { RemarkModule } from 'ngx-remark';
33
+ import SafeColor from 'safecolor';
34
+ import { ModalService, ModalButton, ModalModule } from '@sinequa/core/modal';
35
+ import { parseISO, isToday, isYesterday, isThisWeek, differenceInDays, endOfYesterday, isThisMonth, differenceInMonths, isThisQuarter, isThisYear, differenceInYears, format } from 'date-fns';
36
+
37
+ /**
38
+ * A service to create and manage instances of ChatService dynamically based on the provided component references and the implementation type (http or websocket)
39
+ * All chat-related components should share the same instance of this InstanceManagerService, which in turn provides the appropriate instance of ChatService
40
+ */
41
+ class InstanceManagerService {
42
+ constructor() {
43
+ this._serviceInstances = new Map();
44
+ }
45
+ /**
46
+ * Store the instance of ChatService in the map
47
+ * @param key key differentiator between components used to store their corresponding ChatService instance
48
+ * @param service The ChatService instance
49
+ */
50
+ storeInstance(key, service) {
51
+ this._serviceInstances.set(key, service);
52
+ }
53
+ /**
54
+ * @param key key differentiator between components based on which the corresponding ChatService instance is fetched
55
+ * @returns The instance of the service corresponding to the instance of the component
56
+ */
57
+ getInstance(key) {
58
+ if (!this.checkInstance(key)) {
59
+ throw new Error(`No chat instance found for the given key : '${key}'`);
60
+ }
61
+ return this._serviceInstances.get(key);
62
+ }
63
+ /**
64
+ *
65
+ * @param key key differentiator between components based on which the check for an existent ChatService instance is performed
66
+ * @returns True if a ChatService instance has been already instantiated for the given key. Otherwise, false.
67
+ */
68
+ checkInstance(key) {
69
+ return this._serviceInstances.has(key);
70
+ }
71
+ }
72
+ InstanceManagerService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.6", ngImport: i0, type: InstanceManagerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
73
+ InstanceManagerService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.0.6", ngImport: i0, type: InstanceManagerService, providedIn: 'root' });
74
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.6", ngImport: i0, type: InstanceManagerService, decorators: [{
75
+ type: Injectable,
76
+ args: [{
77
+ providedIn: 'root',
78
+ }]
79
+ }] });
80
+
81
+ class ChatSettingsV3Component {
82
+ constructor() {
83
+ this._update = new EventEmitter();
84
+ this._cancel = new EventEmitter();
85
+ this.subscription = new Subscription();
86
+ this.isAdmin = false;
87
+ this.loginService = inject(LoginService);
88
+ this.instanceManagerService = inject(InstanceManagerService);
89
+ this.principalService = inject(PrincipalWebService);
90
+ }
91
+ ngOnInit() {
92
+ this.subscription.add(this.loginService.events.pipe(filter(e => e.type === 'login-complete'), tap(_ => this.instantiateChatService()), switchMap(() => this.chatService.initConfig$), filter(initConfig => !!initConfig)).subscribe(_ => {
93
+ this.isAdmin = this.principalService.principal.isAdministrator;
94
+ // Init config with a copy of the original chat config, so that it won't be modified by the user until he clicks on save
95
+ this.config = JSON.parse(JSON.stringify(this.chatService.chatConfig$.value));
96
+ this.selectedModel = this.chatService.getModel(this.config.serviceSettings.service_id, this.config.serviceSettings.model_id);
97
+ this.updateFunctionsCheckboxes();
98
+ }));
99
+ }
100
+ ngOnDestroy() {
101
+ this.subscription.unsubscribe();
102
+ }
103
+ get hasPrompts() {
104
+ return this.isAdmin
105
+ || !!this.config.uiSettings.systemPrompt
106
+ || !!this.config.uiSettings.userPrompt;
107
+ }
108
+ get hasAdvancedParameters() {
109
+ return this.isAdmin
110
+ || !!this.config.uiSettings.temperature
111
+ || !!this.config.uiSettings.top_p
112
+ || !!this.config.uiSettings.maxTokens;
113
+ }
114
+ get hasModel() {
115
+ return this.isAdmin
116
+ || !!this.config.uiSettings.servicesModels
117
+ || !!this.config.uiSettings.functions
118
+ || !!this.config.uiSettings.debug
119
+ || !!this.config.uiSettings.temperature
120
+ || !!this.config.uiSettings.top_p
121
+ || !!this.config.uiSettings.maxTokens;
122
+ }
123
+ instantiateChatService() {
124
+ this.chatService = this.instanceManagerService.getInstance(this.instanceId);
125
+ }
126
+ onChatModelChange(selectedModel) {
127
+ // Update properties based on the selected model
128
+ this.config.serviceSettings.service_id = selectedModel.serviceId;
129
+ this.config.serviceSettings.model_id = selectedModel.modelId;
130
+ }
131
+ toggleFunctionsSelection(name) {
132
+ if (this.config.functions.includes(name)) {
133
+ this.config.functions = this.config.functions.filter(item => item !== name);
134
+ }
135
+ else {
136
+ this.config.functions.push(name);
137
+ }
138
+ }
139
+ updateFunctionsCheckboxes() {
140
+ // Update the checkboxes based on config.functions
141
+ (this.chatService.functions || []).forEach(item => {
142
+ item['selected'] = this.config.functions.includes(item.functionName);
143
+ });
144
+ }
145
+ /**
146
+ * Save the new chat config in the chat service and the user preferences
147
+ */
148
+ save() {
149
+ this.chatService.updateChatConfig(this.config);
150
+ this._update.emit();
151
+ }
152
+ /**
153
+ * Cancel the current changes
154
+ */
155
+ cancel() {
156
+ this._cancel.emit();
157
+ }
158
+ }
159
+ ChatSettingsV3Component.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.6", ngImport: i0, type: ChatSettingsV3Component, deps: [], target: i0.ɵɵFactoryTarget.Component });
160
+ ChatSettingsV3Component.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.0.6", type: ChatSettingsV3Component, isStandalone: true, selector: "sq-chat-settings-v3", inputs: { instanceId: "instanceId" }, outputs: { _update: "update", _cancel: "cancel" }, ngImport: i0, template: "<div class=\"sq-chat-settings\">\n <div class=\"settings-panel card-body small\" *ngIf=\"config\">\n\n <h5 *ngIf=\"hasModel\">Model</h5>\n <div class=\"mb-2\" *ngIf=\"isAdmin || config.uiSettings.servicesModels\">\n <label for=\"gllmModel\" class=\"form-label\">Model</label>\n <select class=\"form-select\" id=\"gllmModel\" [(ngModel)]=\"selectedModel\" (ngModelChange)=\"onChatModelChange($event)\">\n <option *ngFor=\"let model of chatService.models\" [ngValue]=\"model\">{{model.displayName}}</option>\n </select>\n </div>\n \n <div class=\"mb-4\" *ngIf=\"isAdmin || config.uiSettings.functions\">\n <label for=\"gllmFunctions\" class=\"form-label\">Functions</label>\n <div id=\"gllmFunctions\" *ngFor=\"let func of chatService.functions\" class=\"multi-option form-check form-switch\">\n <input class=\"form-check-input\" type=\"checkbox\" role=\"switch\" [id]=\"func.functionName\" [(ngModel)]=\"func.selected\"\n (ngModelChange)=\"toggleFunctionsSelection(func.functionName)\">\n <label class=\"form-check-label\" [for]=\"func.functionName\" [title]=\"func.description\">{{ func.functionName }}</label>\n </div>\n </div>\n \n <div class=\"form-check form-switch mb-2\" *ngIf=\"isAdmin || config.uiSettings.debug\">\n <input class=\"form-check-input\" type=\"checkbox\" role=\"switch\" id=\"debug\" [(ngModel)]=\"config.debug\">\n <label class=\"form-check-label\" for=\"debug\">Debug</label>\n </div>\n \n <details *ngIf=\"hasAdvancedParameters\">\n <summary>Advanced parameters</summary>\n <div class=\"mb-2\" *ngIf=\"isAdmin || config.uiSettings.temperature\">\n <label for=\"temperature\" class=\"form-label\">Temperature: {{config.serviceSettings.temperature}}</label>\n <input type=\"range\" class=\"form-range form-range-sm\" min=\"0\" max=\"2\" step=\"0.1\" id=\"temperature\"\n [(ngModel)]=\"config.serviceSettings.temperature\">\n </div>\n <div class=\"mb-2\" *ngIf=\"isAdmin || config.uiSettings.top_p\">\n <label for=\"top-p\" class=\"form-label\">Top P: {{config.serviceSettings.top_p}}</label>\n <input type=\"range\" class=\"form-range form-range-sm\" min=\"0\" max=\"1\" step=\"0.05\" id=\"top-p\"\n [(ngModel)]=\"config.serviceSettings.top_p\">\n </div>\n <div class=\"mb-2\" *ngIf=\"isAdmin || config.uiSettings.maxTokens\">\n <label for=\"max-tokens\" class=\"form-label\">Max generated tokens per answer:\n {{config.serviceSettings.maxTokens}}</label>\n <input type=\"range\" class=\"form-range form-range-sm\" min=\"1\" max=\"2048\" step=\"1\" id=\"max-tokens\"\n [(ngModel)]=\"config.serviceSettings.maxTokens\">\n </div>\n </details>\n \n <hr>\n \n <h5 *ngIf=\"hasPrompts\">Prompts</h5>\n <div class=\"mb-2\" *ngIf=\"isAdmin || config.uiSettings.systemPrompt\">\n <label for=\"initialSystemPrompt\" class=\"form-label\">System prompt (hidden)</label>\n <textarea class=\"form-control\" id=\"initialSystemPrompt\" [(ngModel)]=\"config.uiSettings.systemPrompt\"></textarea>\n </div>\n <div class=\"mb-2\" *ngIf=\"isAdmin || config.uiSettings.userPrompt\">\n <label for=\"initialUserPrompt\" class=\"form-label\">Initial user prompt</label>\n <textarea class=\"form-control\" id=\"initialUserPrompt\" [(ngModel)]=\"config.uiSettings.userPrompt\"></textarea>\n </div>\n \n </div>\n \n <div class=\"buttons-panel d-flex justify-content-end\">\n <button class=\"btn btn-light\" (click)=\"cancel()\">Cancel</button>\n <button class=\"btn btn-primary\" *ngIf=\"config\" (click)=\"save()\">Save</button>\n </div>\n \n</div>", styles: [":host{display:block;width:var(--ast-chat-settings-width, 100%);max-width:100%;height:100%;margin-left:auto;margin-right:auto;padding-top:var(--ast-chat-settings-padding-top, 0);padding-bottom:var(--ast-chat-settings-padding-bottom, 0)}.sq-chat-settings{display:flex;flex-direction:column;height:100%}.sq-chat-settings .settings-panel{flex-grow:1;overflow:auto}.sq-chat-settings .buttons-panel{padding-top:.5rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.RangeValueAccessor, selector: "input[type=range][formControlName],input[type=range][formControl],input[type=range][ngModel]" }, { kind: "directive", type: i2.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { 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"] }] });
161
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.6", ngImport: i0, type: ChatSettingsV3Component, decorators: [{
162
+ type: Component,
163
+ args: [{ selector: 'sq-chat-settings-v3', standalone: true, imports: [CommonModule, FormsModule], template: "<div class=\"sq-chat-settings\">\n <div class=\"settings-panel card-body small\" *ngIf=\"config\">\n\n <h5 *ngIf=\"hasModel\">Model</h5>\n <div class=\"mb-2\" *ngIf=\"isAdmin || config.uiSettings.servicesModels\">\n <label for=\"gllmModel\" class=\"form-label\">Model</label>\n <select class=\"form-select\" id=\"gllmModel\" [(ngModel)]=\"selectedModel\" (ngModelChange)=\"onChatModelChange($event)\">\n <option *ngFor=\"let model of chatService.models\" [ngValue]=\"model\">{{model.displayName}}</option>\n </select>\n </div>\n \n <div class=\"mb-4\" *ngIf=\"isAdmin || config.uiSettings.functions\">\n <label for=\"gllmFunctions\" class=\"form-label\">Functions</label>\n <div id=\"gllmFunctions\" *ngFor=\"let func of chatService.functions\" class=\"multi-option form-check form-switch\">\n <input class=\"form-check-input\" type=\"checkbox\" role=\"switch\" [id]=\"func.functionName\" [(ngModel)]=\"func.selected\"\n (ngModelChange)=\"toggleFunctionsSelection(func.functionName)\">\n <label class=\"form-check-label\" [for]=\"func.functionName\" [title]=\"func.description\">{{ func.functionName }}</label>\n </div>\n </div>\n \n <div class=\"form-check form-switch mb-2\" *ngIf=\"isAdmin || config.uiSettings.debug\">\n <input class=\"form-check-input\" type=\"checkbox\" role=\"switch\" id=\"debug\" [(ngModel)]=\"config.debug\">\n <label class=\"form-check-label\" for=\"debug\">Debug</label>\n </div>\n \n <details *ngIf=\"hasAdvancedParameters\">\n <summary>Advanced parameters</summary>\n <div class=\"mb-2\" *ngIf=\"isAdmin || config.uiSettings.temperature\">\n <label for=\"temperature\" class=\"form-label\">Temperature: {{config.serviceSettings.temperature}}</label>\n <input type=\"range\" class=\"form-range form-range-sm\" min=\"0\" max=\"2\" step=\"0.1\" id=\"temperature\"\n [(ngModel)]=\"config.serviceSettings.temperature\">\n </div>\n <div class=\"mb-2\" *ngIf=\"isAdmin || config.uiSettings.top_p\">\n <label for=\"top-p\" class=\"form-label\">Top P: {{config.serviceSettings.top_p}}</label>\n <input type=\"range\" class=\"form-range form-range-sm\" min=\"0\" max=\"1\" step=\"0.05\" id=\"top-p\"\n [(ngModel)]=\"config.serviceSettings.top_p\">\n </div>\n <div class=\"mb-2\" *ngIf=\"isAdmin || config.uiSettings.maxTokens\">\n <label for=\"max-tokens\" class=\"form-label\">Max generated tokens per answer:\n {{config.serviceSettings.maxTokens}}</label>\n <input type=\"range\" class=\"form-range form-range-sm\" min=\"1\" max=\"2048\" step=\"1\" id=\"max-tokens\"\n [(ngModel)]=\"config.serviceSettings.maxTokens\">\n </div>\n </details>\n \n <hr>\n \n <h5 *ngIf=\"hasPrompts\">Prompts</h5>\n <div class=\"mb-2\" *ngIf=\"isAdmin || config.uiSettings.systemPrompt\">\n <label for=\"initialSystemPrompt\" class=\"form-label\">System prompt (hidden)</label>\n <textarea class=\"form-control\" id=\"initialSystemPrompt\" [(ngModel)]=\"config.uiSettings.systemPrompt\"></textarea>\n </div>\n <div class=\"mb-2\" *ngIf=\"isAdmin || config.uiSettings.userPrompt\">\n <label for=\"initialUserPrompt\" class=\"form-label\">Initial user prompt</label>\n <textarea class=\"form-control\" id=\"initialUserPrompt\" [(ngModel)]=\"config.uiSettings.userPrompt\"></textarea>\n </div>\n \n </div>\n \n <div class=\"buttons-panel d-flex justify-content-end\">\n <button class=\"btn btn-light\" (click)=\"cancel()\">Cancel</button>\n <button class=\"btn btn-primary\" *ngIf=\"config\" (click)=\"save()\">Save</button>\n </div>\n \n</div>", styles: [":host{display:block;width:var(--ast-chat-settings-width, 100%);max-width:100%;height:100%;margin-left:auto;margin-right:auto;padding-top:var(--ast-chat-settings-padding-top, 0);padding-bottom:var(--ast-chat-settings-padding-bottom, 0)}.sq-chat-settings{display:flex;flex-direction:column;height:100%}.sq-chat-settings .settings-panel{flex-grow:1;overflow:auto}.sq-chat-settings .buttons-panel{padding-top:.5rem}\n"] }]
164
+ }], propDecorators: { instanceId: [{
165
+ type: Input
166
+ }], _update: [{
167
+ type: Output,
168
+ args: ["update"]
169
+ }], _cancel: [{
170
+ type: Output,
171
+ args: ["cancel"]
172
+ }] } });
173
+
174
+ // Define the Zod representation for the globalSettings object
175
+ const globalSettingsSchema = z.object({
176
+ restEndpoint: z.string().optional(),
177
+ websocketEndpoint: z.string().optional(),
178
+ signalRTransport: z.enum(["WebSockets", "ServerSentEvents", "LongPolling", "None"]),
179
+ signalRLogLevel: z.enum(["Critical", "Debug", "Error", "Information", "None", "Trace", "Warning"])
180
+ }).refine(data => (!!data.restEndpoint || !!data.websocketEndpoint), {
181
+ message: "Based on the provided input() protocol ('REST' or 'WEBSOCKET') to the Chat Component, either 'restEndpoint' or 'websocketEndpoint' property should be provided in the 'globalSettings' of the assistant instance.",
182
+ });
183
+ // Define the Zod representation for the serviceSettings object
184
+ const serviceSettingsSchema = z.object({
185
+ service_id: z.string(),
186
+ model_id: z.string(),
187
+ temperature: z.number(),
188
+ top_p: z.number(),
189
+ maxTokens: z.number(),
190
+ results_per_prompt: z.number(),
191
+ presence_penalty: z.number(),
192
+ frequency_penalty: z.number(),
193
+ });
194
+ // Define the Zod representation for the TextChunksOptions interface
195
+ const textChunksOptionsSchema = z.object({
196
+ extendMode: z.enum(['None', 'Sentence', 'Chars']).optional(),
197
+ extendScope: z.number().optional(),
198
+ });
199
+ // Define the Zod representation for the contextOptions object
200
+ const contextOptionsSchema = z.object({
201
+ docColumns: z.array(z.string()),
202
+ textChunkFillGaps: z.number(),
203
+ html: z.boolean(),
204
+ topPassagesOptions: z.object({
205
+ topPassages: z.number(),
206
+ topPassagesMinScore: z.number(),
207
+ textChunkOptions: textChunksOptionsSchema,
208
+ }),
209
+ matchingPassagesOptions: z.object({
210
+ matchingPassagesPerDoc: z.number(),
211
+ fromTopDocuments: z.number(),
212
+ matchingPassagesMinScore: z.number(),
213
+ textChunkOptions: textChunksOptionsSchema,
214
+ }),
215
+ relevantExtractsOptions: z.object({
216
+ topRelevantExtractsPerDoc: z.number(),
217
+ fromTopDocuments: z.number(),
218
+ textChunkOptions: textChunksOptionsSchema,
219
+ }),
220
+ htmlOptions: z.object({
221
+ images: z.boolean(),
222
+ links: z.boolean(),
223
+ tables: z.boolean(),
224
+ extendTables: z.boolean(),
225
+ }),
226
+ });
227
+ // Define the Zod representation for the contextSettings object
228
+ const contextSettingsSchema = z.object({
229
+ app: z.string().optional(),
230
+ query: z.object({}).optional(),
231
+ contextOptions: contextOptionsSchema,
232
+ });
233
+ // Define the Zod representation for the uiSettings object
234
+ const uiSettingsSchema = z.object({
235
+ display: z.boolean(),
236
+ servicesModels: z.boolean(),
237
+ functions: z.boolean(),
238
+ temperature: z.boolean(),
239
+ top_p: z.boolean(),
240
+ maxTokens: z.boolean(),
241
+ debug: z.boolean(),
242
+ displaySystemPrompt: z.boolean(),
243
+ systemPrompt: z.string(),
244
+ displayUserPrompt: z.boolean(),
245
+ userPrompt: z.string(),
246
+ });
247
+ // Define the Zod representation for the entire ChatConfig object
248
+ const chatConfigSchema = z.object({
249
+ globalSettings: globalSettingsSchema,
250
+ serviceSettings: serviceSettingsSchema,
251
+ contextSettings: contextSettingsSchema,
252
+ uiSettings: uiSettingsSchema,
253
+ functions: z.array(z.string()),
254
+ debug: z.boolean(),
255
+ saveChats: z.boolean()
256
+ });
257
+
258
+ class ChatService {
259
+ constructor() {
260
+ /** Emit true once the initialization of the chat process is done*/
261
+ this.initProcess$ = new BehaviorSubject(false);
262
+ /** Emit true once the initialization of the chat config is done*/
263
+ this.initConfig$ = new BehaviorSubject(false);
264
+ /** Global configuration of the chat */
265
+ this.chatConfig$ = new BehaviorSubject(undefined);
266
+ /** Streaming status of the chat endpoint */
267
+ this.streaming$ = new BehaviorSubject(false);
268
+ /** List of saved chats */
269
+ this.savedChats$ = new BehaviorSubject([]);
270
+ /** Emit the saved chat to load */
271
+ this.loadSavedChat$ = new BehaviorSubject(undefined);
272
+ this.searchService = inject(SearchService);
273
+ this.userSettingsService = inject(UserSettingsWebService);
274
+ this.notificationsService = inject(NotificationsService);
275
+ this.auditService = inject(AuditWebService);
276
+ this.prefs = inject(UserPreferences);
277
+ this.loginService = inject(LoginService);
278
+ this.appService = inject(AppService);
279
+ this.intlService = inject(IntlService);
280
+ }
281
+ get assistants() {
282
+ if (!this.userSettingsService.userSettings)
283
+ this.userSettingsService.userSettings = {};
284
+ if (!this.userSettingsService.userSettings["assistants"])
285
+ this.userSettingsService.userSettings["assistants"] = {};
286
+ return this.userSettingsService.userSettings["assistants"];
287
+ }
288
+ /**
289
+ * Get the instance ID of the chat service
290
+ * @returns The instance ID of the chat service
291
+ */
292
+ get chatInstanceId() {
293
+ return this._chatInstanceId;
294
+ }
295
+ /**
296
+ * Persist the instance ID of the chat service
297
+ * @param instanceId The instance ID of the chat service
298
+ */
299
+ setChatInstanceId(instanceId) {
300
+ this._chatInstanceId = instanceId;
301
+ }
302
+ /**
303
+ * Get the ID of the current chat discussion which is used to save/get/delete it
304
+ * @returns The ID of the current chat discussion
305
+ */
306
+ get savedChatId() {
307
+ return this._savedChatId;
308
+ }
309
+ /**
310
+ * Persist the ID of the current chat discussion which is used to save/get/delete it
311
+ * @param savedChatId The ID of the current chat discussion which is used to save/get/delete it
312
+ */
313
+ setSavedChatId(savedChatId) {
314
+ this._savedChatId = savedChatId;
315
+ }
316
+ /**
317
+ * Initialize the chat config by merging the default config with the user preferences and the app config
318
+ * override desc order: user preferences > app config > default config
319
+ */
320
+ initChatConfig() {
321
+ const key = this.chatInstanceId;
322
+ const userSettingsConfig = this.assistants[key] || {};
323
+ const defaultChatConfig = this.appService.app?.data?.assistants?.[key];
324
+ // Validate the object against the schema
325
+ try {
326
+ chatConfigSchema.parse(defaultChatConfig);
327
+ // Merge configs with override desc order: user preferences > app config > default config
328
+ const chatConfig = {
329
+ ...defaultChatConfig,
330
+ ...userSettingsConfig
331
+ };
332
+ // Update the chat config and store it in the user preferences
333
+ this.updateChatConfig(chatConfig, false);
334
+ this.initConfig$.next(true);
335
+ }
336
+ catch (error) {
337
+ this.notificationsService.error(`Missing valid configuration for the assistant instance '${key}'`);
338
+ throw new Error(`Missing valid configuration for the assistant instance '${key}' . \n ${JSON.stringify(error.issues, null, 2)}`);
339
+ }
340
+ }
341
+ /**
342
+ * Update the chat config and store it in the user preferences
343
+ * @param config The updated chat config
344
+ * @param notify Whether to notify the user about the update
345
+ * @param successCallback The callback to execute if the update is successful
346
+ * @param errorCallback The callback to execute if the update fails
347
+ */
348
+ updateChatConfig(config, notify = true, successCallback, errorCallback) {
349
+ this.chatConfig$.next(config);
350
+ const assistants = Object.assign({}, this.assistants);
351
+ assistants[this.chatInstanceId] = config;
352
+ this.userSettingsService.patch({ assistants }).subscribe(next => {
353
+ if (notify) {
354
+ successCallback ? successCallback() : this.notificationsService.success(`The configuration of the assistant instance '${this.chatInstanceId}' has been successfully updated`);
355
+ }
356
+ }, error => {
357
+ if (notify) {
358
+ errorCallback ? errorCallback() : this.notificationsService.error(`The update of the assistant instance '${this.chatInstanceId}' configuration failed`);
359
+ }
360
+ console.error("Could not patch assistants!", error);
361
+ });
362
+ }
363
+ /**
364
+ * Get the model description for the given (serviceId + modelId)
365
+ * If a model is not found, an error message is returned
366
+ * @param serviceId The serviceId of the model
367
+ * @param modelId The modelId of the model
368
+ * @returns The model description
369
+ */
370
+ getModel(serviceId, modelId) {
371
+ let model = this.models?.find(m => m.serviceId === serviceId && m.modelId === modelId);
372
+ // Handle obsolete config
373
+ if (!model) {
374
+ this.notificationsService.error(`FATAL ERROR : The model (serviceId = '${serviceId}', modelId = '${modelId}') is no longer available. Please contact an admin for further information.`);
375
+ throw new Error(`FATAL ERROR : The model (serviceId = '${serviceId}', modelId = '${modelId}') is no longer available`);
376
+ }
377
+ return model;
378
+ }
379
+ // Todo: pending implementation
380
+ notifyAudit(messagesHistory, model) {
381
+ // let numberOfUserMessages = 0;
382
+ // let numberOfAttachments = 0;
383
+ // let numberOfAssistantMessages = 0;
384
+ // for(let m of messagesHistory) {
385
+ // if(m.$attachment) numberOfAttachments++;
386
+ // else if(m.role === 'user') numberOfUserMessages++;
387
+ // else if (m.role === 'assistant') numberOfAssistantMessages++;
388
+ // }
389
+ // this.auditService.notify({
390
+ // type: 'Chat_Messages',
391
+ // detail: {
392
+ // // message: messagesHistory.map(m => m.role.toUpperCase() + ': '+ (m.$attachment? `attachment ${m.$attachment?.$record.title}` : m.content)).join('\n\n'),
393
+ // message: messagesHistory.map(m => m.role.toUpperCase() + ': '+ m.content).join('\n\n'),
394
+ // // numberOfUserMessages,
395
+ // // numberOfAttachments,
396
+ // // numberOfAssistantMessages,
397
+ // model
398
+ // }
399
+ // });
400
+ }
401
+ /**
402
+ * Format a date string in UTC to a local date string
403
+ * @param value Date string in UTC to format
404
+ * @returns A formatted local date string
405
+ */
406
+ formatDateTime(value) {
407
+ return this.intlService["formatTime"](value, { day: "numeric", month: "short", year: "numeric" });
408
+ }
409
+ /**
410
+ * Takes a text prompt that may contain placeholders for variables
411
+ * and replaces these placeholders if it finds a match in the given
412
+ * context object.
413
+ */
414
+ static formatPrompt(prompt, context) {
415
+ return prompt.replace(/{{(.*?)}}/g, (match, expr) => get(context, expr) ?? match);
416
+ }
417
+ /**
418
+ * @returns A Globally Unique Identifier
419
+ */
420
+ static generateGUID() {
421
+ return Utils.guid();
422
+ }
423
+ }
424
+ ChatService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.6", ngImport: i0, type: ChatService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
425
+ ChatService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.0.6", ngImport: i0, type: ChatService });
426
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.6", ngImport: i0, type: ChatService, decorators: [{
427
+ type: Injectable
428
+ }] });
429
+
430
+ class WebSocketChatService extends ChatService {
431
+ constructor() {
432
+ super();
433
+ this.connectionBuilt$ = new Subject(); // Emit when the connection is built
434
+ this.connectionStarted$ = new Subject(); // Emit when the connection is started
435
+ this.messageHandlers = new Map();
436
+ this.actionMap = new Map();
437
+ this.content = "";
438
+ this.attachments = [];
439
+ this.signalRService = inject(SignalRWebService);
440
+ this.authenticationService = inject(AuthenticationService);
441
+ }
442
+ /**
443
+ * Initialize the chat process after the login is complete.
444
+ * It includes building and starting a connection, executing parallel requests for models and functions, and handling errors during the process.
445
+ *
446
+ * @returns An Observable<boolean> indicating the success of the initialization process.
447
+ */
448
+ init() {
449
+ return this.loginService.events.pipe(filter((e) => e.type === 'login-complete'), tap(() => this.getRequestsUrl()),
450
+ // Build the connection and handle the completion with the connectionBuilt$ subject
451
+ switchMap(() => this.buildConnection()), tap(() => {
452
+ this.initMessageHandlers();
453
+ this.connectionBuilt$.next();
454
+ }),
455
+ // Start the connection and handle the completion with the connectionStarted$ subject
456
+ switchMap(() => this.startConnection()),
457
+ // Execute parallel requests for models and functions
458
+ switchMap(() => {
459
+ this.connectionStarted$.next();
460
+ return forkJoin([
461
+ this.listModels(),
462
+ this.listFunctions()
463
+ ]);
464
+ }),
465
+ // Map the results of parallel requests to a boolean indicating success
466
+ map(([models, functions]) => {
467
+ this.initProcess$.next(true);
468
+ return !!models && !!functions;
469
+ }),
470
+ // Any errors during the process are caught, logged, and re-thrown to propagate the error further
471
+ catchError((error) => {
472
+ console.error('Error occurred:', error);
473
+ return throwError(() => error);
474
+ }),
475
+ // Cache and replay the emitted value for subsequent subscribers
476
+ shareReplay(1));
477
+ }
478
+ /**
479
+ * Define the assistant endpoint to use for the websocket requests
480
+ * It can be overridden by the app config
481
+ */
482
+ getRequestsUrl() {
483
+ if (this.chatConfig$.value.globalSettings.websocketEndpoint) {
484
+ this.REQUEST_URL = this.chatConfig$.value.globalSettings.websocketEndpoint;
485
+ }
486
+ else {
487
+ throw new Error(`The property 'websocketEndpoint' must be provided when attempting to use 'WebSocket' in assistant instance`);
488
+ }
489
+ }
490
+ listModels() {
491
+ const modelsSubject = new Subject();
492
+ this.connection.on('ListModels', (res) => {
493
+ this.models = res.models?.filter(model => !!model.enable);
494
+ modelsSubject.next(this.models);
495
+ modelsSubject.complete();
496
+ });
497
+ // Send the request to get the list of models
498
+ this.connection.invoke('ListModels', { debug: this.chatConfig$.value.debug })
499
+ .catch(error => {
500
+ console.error('Error invoking ListModels:', error);
501
+ modelsSubject.complete();
502
+ return Promise.resolve(); // Return a resolved promise to handle the error and prevent unhandled promise rejection
503
+ });
504
+ return modelsSubject.asObservable();
505
+ }
506
+ listFunctions() {
507
+ const functionsSubject = new Subject();
508
+ this.connection.on('ListFunctions', (res) => {
509
+ this.functions = res.functions;
510
+ functionsSubject.next(this.functions);
511
+ functionsSubject.complete();
512
+ });
513
+ // Send the request to get the list of functions
514
+ this.connection.invoke('ListFunctions', { debug: this.chatConfig$.value.debug })
515
+ .catch(error => {
516
+ console.error('Error invoking ListFunctions:', error);
517
+ functionsSubject.complete();
518
+ return Promise.resolve(); // Return a resolved promise to handle the error and prevent unhandled promise rejection
519
+ });
520
+ return functionsSubject.asObservable();
521
+ }
522
+ fetch(messages, query = this.searchService.query) {
523
+ // Start streaming by invoking the Chat method
524
+ this.streaming$.next(true);
525
+ // Prepare the payload to send to the Chat method
526
+ const data = {
527
+ history: messages,
528
+ functions: this.chatConfig$.value.functions,
529
+ debug: this.chatConfig$.value.debug,
530
+ serviceSettings: this.chatConfig$.value.serviceSettings,
531
+ contextSettings: {
532
+ ...this.chatConfig$.value.contextSettings,
533
+ app: this.appService.appName,
534
+ query
535
+ }
536
+ };
537
+ if (this.chatConfig$.value.saveChats) {
538
+ data.instanceId = this.chatInstanceId;
539
+ data.savedChatId = this.savedChatId;
540
+ }
541
+ let response = { role: "assistant", content: "", additionalProperties: { display: true } }; // here display: true is needed in order to be able to show the progress
542
+ // Create a Subject to signal completion
543
+ const completion$ = new Subject();
544
+ // Create observables for each non-global handler in the messageHandlers map (default and eventual custom ones) once it is triggered by the hub connection
545
+ const observables = Array
546
+ .from(this.messageHandlers.entries())
547
+ .filter(([eventName, eventHandler]) => !eventHandler.isGlobalHandler)
548
+ .map(([eventName, eventHandler]) => {
549
+ return fromEvent(this.connection, eventName).pipe(map((event) => eventHandler.handler(event)) // Execute the corresponding handler
550
+ );
551
+ });
552
+ // Then merge them into a single observable in order to simulate the streaming behavior
553
+ const combined$ = merge(...observables).pipe(takeUntil(completion$) // Complete the observable when completion$ emits
554
+ );
555
+ // Invoke the Chat method
556
+ this.connection.invoke('Chat', data)
557
+ .then(() => this.notifyAudit(this.chatHistory, this.chatConfig$.value.serviceSettings.service_id)) // When the server indicates it has successfully finished invoking the method, notify the audit service with the recent chat history
558
+ .catch(error => {
559
+ console.error('Error invoking Chat:', error);
560
+ return Promise.resolve(); // Return a resolved promise to handle the error and prevent unhandled promise rejection
561
+ })
562
+ .finally(() => {
563
+ this.streaming$.next(false); // Complete streaming regardless of success or error
564
+ this.actionMap.clear(); // Clear the actionMap
565
+ this.content = ""; // Clear the content
566
+ this.attachments = []; // Clear the attachments
567
+ this.executionTime = ""; // Clear the executionTime
568
+ completion$.next(); // Emit a signal to complete the observables
569
+ completion$.complete(); // Complete the subject
570
+ });
571
+ // Return the merged observables
572
+ return combined$.pipe(map(() => {
573
+ // Define $progress from the actionMap
574
+ const actions = Array.from(this.actionMap.values());
575
+ const $progress = actions.length > 0
576
+ ? actions.map((a) => ({
577
+ title: a.displayName ?? "",
578
+ content: a.displayValue ?? "",
579
+ done: a.executionTime !== undefined,
580
+ time: a.executionTime,
581
+ }))
582
+ : undefined;
583
+ // Define the attachment used in the context of the response message
584
+ const $attachment = this.attachments;
585
+ // As soon as the first content or $progress is defined, the assistant is considered as streaming
586
+ if (!!this.content || $progress || $attachment.length > 0) {
587
+ response = { ...response, content: this.content, additionalProperties: { ...response.additionalProperties, $progress, $attachment } };
588
+ }
589
+ // Return the result
590
+ return { history: [...messages, response], executionTime: this.executionTime };
591
+ }));
592
+ }
593
+ listSavedChat() {
594
+ const data = {
595
+ instanceId: this.chatInstanceId,
596
+ debug: this.chatConfig$.value.debug
597
+ };
598
+ this.connection.on('SavedChatList', (res) => {
599
+ this.savedChats$.next(res.savedChats); // emits the result to the savedChats$ subject
600
+ });
601
+ // Invoke the method SavedChatList
602
+ this.connection.invoke('SavedChatList', data)
603
+ .catch(error => {
604
+ console.error('Error invoking SavedChatList:', error);
605
+ return Promise.resolve();
606
+ });
607
+ }
608
+ getSavedChat(id) {
609
+ const savedChatSubject = new Subject();
610
+ const data = {
611
+ instanceId: this.chatInstanceId,
612
+ savedChatId: id,
613
+ debug: this.chatConfig$.value.debug
614
+ };
615
+ this.connection.on('SavedChatGet', (res) => {
616
+ savedChatSubject.next(res.savedChat);
617
+ savedChatSubject.complete();
618
+ });
619
+ // Invoke the method SavedChatGet
620
+ this.connection.invoke('SavedChatGet', data)
621
+ .catch(error => {
622
+ console.error('Error invoking SavedChatGet:', error);
623
+ savedChatSubject.complete();
624
+ return Promise.resolve();
625
+ });
626
+ return savedChatSubject.asObservable();
627
+ }
628
+ deleteSavedChat(ids) {
629
+ const deleteSavedChatSubject = new Subject();
630
+ const data = {
631
+ instanceId: this.chatInstanceId,
632
+ SavedChatIds: ids,
633
+ debug: this.chatConfig$.value.debug
634
+ };
635
+ this.connection.on('SavedChatDelete', (res) => {
636
+ deleteSavedChatSubject.next(res.deleteCount);
637
+ deleteSavedChatSubject.complete();
638
+ });
639
+ // Invoke the method SavedChatDelete
640
+ this.connection.invoke('SavedChatDelete', data)
641
+ .catch(error => {
642
+ console.error('Error invoking SavedChatDelete:', error);
643
+ deleteSavedChatSubject.complete();
644
+ return Promise.resolve();
645
+ });
646
+ return deleteSavedChatSubject.asObservable();
647
+ }
648
+ /**
649
+ * Initialize out-of-the-box handlers
650
+ * It is a placeholder for non-streaming scenarios, where you invoke a specific hub method, and the server responds with a single message or a result
651
+ */
652
+ initMessageHandlers() {
653
+ this.addMessageHandler("Debug", { handler: (debug) => console.log(debug),
654
+ isGlobalHandler: true });
655
+ this.addMessageHandler("ActionStart", { handler: (action) => this.actionMap.set(action.guid, action),
656
+ isGlobalHandler: false });
657
+ this.addMessageHandler("ActionResult", {
658
+ handler: (action) => this.actionMap.set(action.guid, { ...this.actionMap.get(action.guid), ...action }),
659
+ isGlobalHandler: false
660
+ });
661
+ this.addMessageHandler("ActionStop", {
662
+ handler: (action) => this.actionMap.set(action.guid, { ...this.actionMap.get(action.guid), ...action }),
663
+ isGlobalHandler: false
664
+ });
665
+ this.addMessageHandler("ContextMessage", {
666
+ handler: (message) => this.attachments.push(message.metadata),
667
+ isGlobalHandler: false
668
+ });
669
+ this.addMessageHandler("Message", {
670
+ handler: (message) => this.content += message ?? "",
671
+ isGlobalHandler: false
672
+ });
673
+ this.addMessageHandler("History", {
674
+ handler: (history) => {
675
+ this.chatHistory = history.history;
676
+ this.executionTime = history.executionTime;
677
+ },
678
+ isGlobalHandler: false
679
+ });
680
+ this.addMessageHandler("Error", {
681
+ handler: (error) => {
682
+ console.error(error);
683
+ this.notificationsService.error(error);
684
+ },
685
+ isGlobalHandler: true
686
+ });
687
+ this.addMessageHandler("Quota", {
688
+ handler: (message) => {
689
+ if (message.quota.maxQuotaReached) {
690
+ const msg = `Sorry, you have exceeded the allowed quota. Please retry starting from ${this.formatDateTime(message.quota.nextResetUTC)}.`;
691
+ console.error(msg);
692
+ this.notificationsService.error(msg);
693
+ }
694
+ },
695
+ isGlobalHandler: true
696
+ });
697
+ }
698
+ /**
699
+ * Override and register the entire messageHandlers map by merging the provided map with the default one
700
+ * @param messageHandlers
701
+ */
702
+ overrideMessageHandlers(messageHandlers) {
703
+ // Clear the already registered global chat handlers before merging the new ones
704
+ this.messageHandlers.forEach((eventHandler, eventName) => {
705
+ if (eventHandler.isGlobalHandler) {
706
+ this.unsubscribeMessageHandler(eventName);
707
+ }
708
+ });
709
+ // Merge the new event handlers with the existing ones
710
+ this.messageHandlers = new Map([...this.messageHandlers, ...messageHandlers]);
711
+ // Register the global chat handlers among the merged map
712
+ this.messageHandlers.forEach((eventHandler, eventName) => {
713
+ if (eventHandler.isGlobalHandler) {
714
+ this.registerMessageHandler(eventName, eventHandler);
715
+ }
716
+ });
717
+ }
718
+ /**
719
+ * Add a listener for a specific event.
720
+ * If a listener for this same event already exists, it will be overridden.
721
+ * If the listener has "isChatGlobalHandler" set to true, it will be registered to the hub connection.
722
+ * @param eventName Name of the event to register a listener for
723
+ * @param eventHandler The handler to be called when the event is received
724
+ */
725
+ addMessageHandler(eventName, eventHandler) {
726
+ this.messageHandlers.set(eventName, eventHandler);
727
+ if (eventHandler.isGlobalHandler) {
728
+ this.registerMessageHandler(eventName, eventHandler);
729
+ }
730
+ }
731
+ /**
732
+ * Dynamically register a listener for a specific event.
733
+ * If a listener for this event already exists, it will be overridden.
734
+ * @param eventName Name of the event to register a listener for
735
+ * @param eventHandler The handler to be called when the event is received
736
+ */
737
+ registerMessageHandler(eventName, eventHandler) {
738
+ if (!this.connection) {
739
+ console.log("No connection found to register the listener" + eventName);
740
+ return;
741
+ }
742
+ this.connection.on(eventName, (data) => {
743
+ eventHandler.handler(data);
744
+ });
745
+ }
746
+ /**
747
+ * Remove a listener for a specific event from the messageHandlers map and unsubscribe from receiving messages for this event from the SignalR hub.
748
+ * @param eventName Name of the event to remove the listener for
749
+ */
750
+ removeMessageHandler(eventName) {
751
+ this.messageHandlers.delete(eventName);
752
+ this.unsubscribeMessageHandler(eventName);
753
+ }
754
+ /**
755
+ * Unsubscribe from receiving messages for a specific event from the SignalR hub.
756
+ * ALL its related listeners will be removed from hub connection
757
+ * This is needed to prevent accumulating old listeners when overriding the entire messageHandlers map
758
+ * @param eventName Name of the event
759
+ */
760
+ unsubscribeMessageHandler(eventName) {
761
+ this.connection.off(eventName);
762
+ }
763
+ /**
764
+ * Build a connection to the signalR websocket and register default listeners to the methods defined in the server hub class
765
+ * @param options The options for the connection. It overrides the default options
766
+ * @param logLevel Define the log level displayed in the console
767
+ * @returns Promise that resolves when the connection is built
768
+ */
769
+ buildConnection(options) {
770
+ return new Promise((resolve, reject) => {
771
+ if (!this.REQUEST_URL) {
772
+ reject(new Error("No endpoint provided to connect the websocket to"));
773
+ return;
774
+ }
775
+ const logLevel = this.getLogLevel();
776
+ this.connection = this.signalRService.buildConnection(this.REQUEST_URL, { ...this.defaultOptions, ...options }, logLevel);
777
+ resolve();
778
+ });
779
+ }
780
+ /**
781
+ * Start the connection
782
+ * @returns Promise that resolves when the connection is started
783
+ */
784
+ startConnection() {
785
+ return this.signalRService.startConnection(this.connection);
786
+ }
787
+ /**
788
+ * Stop the connection
789
+ * @returns Promise that resolves when the connection is stopped
790
+ */
791
+ stopConnection() {
792
+ return this.signalRService.stopConnection(this.connection);
793
+ }
794
+ getTransports() {
795
+ switch (this.chatConfig$.value?.globalSettings.signalRTransport) {
796
+ case "WebSockets":
797
+ return HttpTransportType.WebSockets;
798
+ case "ServerSentEvents":
799
+ return HttpTransportType.ServerSentEvents;
800
+ case "LongPolling":
801
+ return HttpTransportType.LongPolling;
802
+ default:
803
+ return HttpTransportType.None;
804
+ }
805
+ }
806
+ getLogLevel() {
807
+ switch (this.chatConfig$.value?.globalSettings.signalRLogLevel) {
808
+ case "Critical":
809
+ return LogLevel.Critical; // Log level for diagnostic messages that indicate a failure that will terminate the entire application.
810
+ case "Debug":
811
+ return LogLevel.Debug; // Log level for low severity diagnostic messages.
812
+ case "Error":
813
+ return LogLevel.Error; // Log level for diagnostic messages that indicate a failure in the current operation.
814
+ case "Information":
815
+ return LogLevel.Information; // Log level for informational diagnostic messages.
816
+ case "None":
817
+ return LogLevel.None; // The highest possible log level. Used when configuring logging to indicate that no log messages should be emitted.
818
+ case "Trace":
819
+ return LogLevel.Trace; // Log level for very low severity diagnostic messages.
820
+ case "Warning":
821
+ return LogLevel.Warning; // Log level for diagnostic messages that indicate a non-fatal problem.
822
+ default:
823
+ return LogLevel.None; // The highest possible log level. Used when configuring logging to indicate that no log messages should be emitted.
824
+ }
825
+ }
826
+ get defaultOptions() {
827
+ let headers = {
828
+ "sinequa-force-camel-case": "true",
829
+ "x-language": this.intlService.currentLocale.name,
830
+ "ui-language": this.intlService.currentLocale.name,
831
+ };
832
+ if (this.authenticationService.processedCredentials) {
833
+ headers = { ...headers, "sinequa-csrf-token": this.authenticationService.processedCredentials.data.csrfToken };
834
+ }
835
+ ;
836
+ // For the first GET request sent by signalR to start a WebSocket protocol,
837
+ // as far as we know, signalR only lets us tweak the request with this access token factory
838
+ // so we pass along the Sinequa CSRF token to pass the CSRF check..
839
+ return {
840
+ transport: this.getTransports(),
841
+ withCredentials: true,
842
+ headers,
843
+ accessTokenFactory: () => this.authenticationService.processedCredentials?.data?.csrfToken || ""
844
+ };
845
+ }
846
+ }
847
+ WebSocketChatService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.6", ngImport: i0, type: WebSocketChatService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
848
+ WebSocketChatService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.0.6", ngImport: i0, type: WebSocketChatService });
849
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.6", ngImport: i0, type: WebSocketChatService, decorators: [{
850
+ type: Injectable
851
+ }], ctorParameters: function () { return []; } });
852
+
853
+ class InitialsAvatarComponent {
854
+ constructor() {
855
+ this.fullName = '';
856
+ this.size = 1.5; // in rem
857
+ }
858
+ /**
859
+ * Gives initials of a name and a safe color background to use,
860
+ * assuming text color will be white
861
+ * @param fullName full name to evaluate intials for
862
+ * @param split string to use has splitter for `fullName`
863
+ * @returns an `object` containing initials and color
864
+ */
865
+ getInitialsAndColorFromFullName(fullName, split = ' ') {
866
+ return { initials: this.getInitialsFromFullName(fullName, split), color: this.getColorFromName(fullName) };
867
+ }
868
+ /**
869
+ * Gives initials of a name, ie:
870
+ * ```
871
+ * getInitialForFullName('John Snow') => 'JS'
872
+ * getInitialForFullName('Geralt of Rivia', ' of ') => 'GR'
873
+ * ```
874
+ * @param fullName full name to evaluate intial for
875
+ * @param split string to use has splitter
876
+ * @returns string containg the first letter of splitted name
877
+ */
878
+ getInitialsFromFullName(fullName, split = ' ') {
879
+ if (!fullName)
880
+ return '';
881
+ const names = fullName.split(split);
882
+ return names[0][0] + (names[1]?.[0] ?? '');
883
+ }
884
+ /**
885
+ * Gets a random color using text as seed
886
+ * @param text string to use for color generation
887
+ * @returns string formatted like `rgb(xxx, xxx, xxx)`
888
+ */
889
+ getColorFromName(text) {
890
+ const safeColor = new SafeColor({
891
+ color: [255, 255, 255],
892
+ contrast: 4.5
893
+ });
894
+ return safeColor.random(text);
895
+ }
896
+ }
897
+ InitialsAvatarComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.6", ngImport: i0, type: InitialsAvatarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
898
+ InitialsAvatarComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.0.6", type: InitialsAvatarComponent, isStandalone: true, selector: "sq-initials-avatar", inputs: { fullName: "fullName", size: "size" }, ngImport: i0, template: "<span class=\"sq-initials-avatar\" *ngIf=\"getInitialsAndColorFromFullName(fullName) as meta\"\n [ngStyle]=\"{ 'background-color': meta.color }\" [style.height.rem]=\"size\" [style.width.rem]=\"size\"\n [style.line-height.rem]=\"size\" [style.font-size.rem]=\"size/2\">\n {{ meta.initials | uppercase }}\n</span>", styles: [".sq-initials-avatar{display:block;border-radius:50%;text-align:center;color:#fff}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "pipe", type: i1.UpperCasePipe, name: "uppercase" }] });
899
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.6", ngImport: i0, type: InitialsAvatarComponent, decorators: [{
900
+ type: Component,
901
+ args: [{ selector: 'sq-initials-avatar', standalone: true, imports: [CommonModule], template: "<span class=\"sq-initials-avatar\" *ngIf=\"getInitialsAndColorFromFullName(fullName) as meta\"\n [ngStyle]=\"{ 'background-color': meta.color }\" [style.height.rem]=\"size\" [style.width.rem]=\"size\"\n [style.line-height.rem]=\"size\" [style.font-size.rem]=\"size/2\">\n {{ meta.initials | uppercase }}\n</span>", styles: [".sq-initials-avatar{display:block;border-radius:50%;text-align:center;color:#fff}\n"] }]
902
+ }], propDecorators: { fullName: [{
903
+ type: Input
904
+ }], size: [{
905
+ type: Input
906
+ }] } });
907
+
908
+ const defaultFormatIcons = {
909
+ "extractslocations": { icon: "far fa-file-alt" },
910
+ "matchlocations": { icon: "far fa-flag" },
911
+ "geo": { icon: "fas fa-map-marker-alt" },
912
+ "person": { icon: "fas fa-user" },
913
+ "company": { icon: "fas fa-building" },
914
+ "title": { icon: "fas fa-tag" },
915
+ "modified": { icon: "far fa-calendar-alt" },
916
+ "size": { icon: "fas fa-weight-hanging" },
917
+ "treepath": { icon: "fas fa-folder-open" },
918
+ "filename": { icon: "far fa-file-alt" },
919
+ "authors": { icon: "fas fa-user-edit" },
920
+ "accesslists": { icon: "fas fa-lock" },
921
+ "doctype": { icon: "far fa-file" },
922
+ "documentlanguages": { icon: "fas fa-globe-americas" },
923
+ "globalrelevance": { icon: "far fa-star" },
924
+ "indexationtime": { icon: "fas fa-search" },
925
+ "concepts": { icon: "far fa-comment-dots" },
926
+ "keywords": { icon: "fas fa-tags" },
927
+ "matchingpartnames": { icon: "fas fa-align-left" },
928
+ "msgfrom": { icon: "fas fa-envelope" },
929
+ "msgto": { icon: "fas fa-envelope-open-text" },
930
+ "file": { icon: "far fa-file" },
931
+ "htm": { icon: "fas fa-globe-europe", color: '#4545bf' },
932
+ "html": { icon: "fas fa-globe-europe", color: '#4545bf' },
933
+ "xhtm": { icon: "fas fa-globe-europe", color: '#4545bf' },
934
+ "xhtml": { icon: "fas fa-globe-europe", color: '#4545bf' },
935
+ "mht": { icon: "fas fa-globe-europe", color: '#4545bf' },
936
+ "doc": { icon: "far fa-file-word", color: '#3f3fca' },
937
+ "docx": { icon: "far fa-file-word", color: '#3f3fca' },
938
+ "docm": { icon: "far fa-file-word", color: '#3f3fca' },
939
+ "dot": { icon: "far fa-file-word", color: '#3f3fca' },
940
+ "dotx": { icon: "far fa-file-word", color: '#3f3fca' },
941
+ "dotm": { icon: "far fa-file-word", color: '#3f3fca' },
942
+ "rtf": { icon: "far fa-file-word", color: '#3f3fca' },
943
+ "odt": { icon: "far fa-file-word", color: 'grey' },
944
+ "ott": { icon: "far fa-file-word", color: 'grey' },
945
+ "gdoc": { icon: "far fa-file-word", color: 'blue' },
946
+ "xls": { icon: "far fa-file-excel", color: 'green' },
947
+ "xlsx": { icon: "far fa-file-excel", color: 'green' },
948
+ "xlt": { icon: "far fa-file-excel", color: 'green' },
949
+ "xltx": { icon: "far fa-file-excel", color: 'green' },
950
+ "xlsm": { icon: "far fa-file-excel", color: 'green' },
951
+ "xltm": { icon: "far fa-file-excel", color: 'green' },
952
+ "gsheet": { icon: "far fa-file-excel", color: 'darkgreen' },
953
+ "ods": { icon: "far fa-file-excel", color: 'lightgreen' },
954
+ "ots": { icon: "far fa-file-excel", color: 'lightgreen' },
955
+ "ppt": { icon: "far fa-file-powerpoint", color: '#e64b30' },
956
+ "pptx": { icon: "far fa-file-powerpoint", color: '#e64b30' },
957
+ "pptm": { icon: "far fa-file-powerpoint", color: '#e64b30' },
958
+ "pptm2": { icon: "far fa-file-powerpoint", color: '#e64b30' },
959
+ "pps": { icon: "far fa-file-powerpoint", color: '#e64b30' },
960
+ "ppsx": { icon: "far fa-file-powerpoint", color: '#e64b30' },
961
+ "ppsm": { icon: "far fa-file-powerpoint", color: '#e64b30' },
962
+ "pot": { icon: "far fa-file-powerpoint", color: '#e64b30' },
963
+ "potx": { icon: "far fa-file-powerpoint", color: '#e64b30' },
964
+ "potm": { icon: "far fa-file-powerpoint", color: '#e64b30' },
965
+ "odp": { icon: "far fa-file-powerpoint", color: 'red' },
966
+ "otp": { icon: "far fa-file-powerpoint", color: 'red' },
967
+ "gslides": { icon: "far fa-file-powerpoint", color: 'orange' },
968
+ "pdf": { icon: "far fa-file-pdf", color: '#ec2e2e' },
969
+ "jpg": { icon: "far fa-file-image", color: '#4545bf' },
970
+ "jpeg": { icon: "far fa-file-image", color: '#4545bf' },
971
+ "bmp": { icon: "far fa-file-image", color: '#4545bf' },
972
+ "tiff": { icon: "far fa-file-image", color: '#4545bf' },
973
+ "tif": { icon: "far fa-file-image", color: '#4545bf' },
974
+ "gif": { icon: "far fa-file-image", color: '#4545bf' },
975
+ "png": { icon: "far fa-file-image", color: '#4545bf' },
976
+ "mp4": { icon: "far fa-file-video", color: '#4545bf' },
977
+ "flv": { icon: "far fa-file-video", color: '#4545bf' },
978
+ "swf": { icon: "far fa-file-video", color: '#4545bf' },
979
+ "mts": { icon: "far fa-file-video", color: '#4545bf' },
980
+ "divx": { icon: "far fa-file-video", color: '#4545bf' },
981
+ "wmv": { icon: "far fa-file-video", color: '#4545bf' },
982
+ "avi": { icon: "far fa-file-video", color: '#4545bf' },
983
+ "mov": { icon: "far fa-file-video", color: '#4545bf' },
984
+ "mpg": { icon: "far fa-file-video", color: '#4545bf' },
985
+ "mpeg": { icon: "far fa-file-video", color: '#4545bf' },
986
+ "asf": { icon: "far fa-file-video", color: '#4545bf' },
987
+ "rm": { icon: "far fa-file-video", color: '#4545bf' },
988
+ "mp3": { icon: "far fa-file-audio", color: 'lightblue' },
989
+ "wav": { icon: "far fa-file-audio", color: 'lightblue' },
990
+ "ogg": { icon: "far fa-file-audio", color: 'lightblue' },
991
+ "wma": { icon: "far fa-file-audio", color: 'lightblue' },
992
+ "aac": { icon: "far fa-file-audio", color: 'lightblue' },
993
+ "m3u": { icon: "far fa-file-audio", color: 'lightblue' },
994
+ "txt": { icon: "far fa-file-alt", color: '#202020' },
995
+ "text": { icon: "far fa-file-alt", color: '#202020' },
996
+ "xml": { icon: "far fa-file-code", color: '#4545bf' },
997
+ "cs": { icon: "far fa-file-code", color: '#4545bf' },
998
+ "java": { icon: "far fa-file-code", color: '#4545bf' },
999
+ "cpp": { icon: "far fa-file-code", color: '#4545bf' },
1000
+ "c": { icon: "far fa-file-code", color: '#4545bf' },
1001
+ "h": { icon: "far fa-file-code", color: '#4545bf' },
1002
+ "hpp": { icon: "far fa-file-code", color: '#4545bf' },
1003
+ "js": { icon: "far fa-file-code", color: '#4545bf' },
1004
+ "ts": { icon: "far fa-file-code", color: '#4545bf' },
1005
+ "zip": { icon: "far fa-file-archive", color: 'yellow' },
1006
+ "7zip": { icon: "far fa-file-archive", color: 'yellow' },
1007
+ "7z": { icon: "far fa-file-archive", color: 'yellow' },
1008
+ "rar": { icon: "far fa-file-archive", color: 'yellow' },
1009
+ "gz": { icon: "far fa-file-archive", color: 'yellow' },
1010
+ "notes": { icon: "fas fa-file-invoice", color: 'orange' },
1011
+ "quickr": { icon: "fas fa-file-invoice", color: 'orange' },
1012
+ "email": { icon: "far fa-envelope", color: 'black' },
1013
+ "mail": { icon: "far fa-envelope", color: 'black' },
1014
+ "msg": { icon: "far fa-envelope", color: 'black' },
1015
+ "mdb": { icon: "far fa-database", color: 'purple' },
1016
+ "odb": { icon: "far fa-database", color: 'darkred' },
1017
+ "otb": { icon: "far fa-database", color: 'darkred' },
1018
+ "xsn": { icon: "fas fa-file-excel", color: 'purple' },
1019
+ "gform": { icon: "fas fa-file-excel", color: 'purple' },
1020
+ "one": { icon: "far fa-book", color: 'purple' },
1021
+ "odf": { icon: "fas fa-file-medical-alt", color: 'grey' },
1022
+ "otf": { icon: "fas fa-file-medical-alt", color: 'grey' },
1023
+ "vsdx": { icon: "far fa-object-group", color: 'purple' },
1024
+ "vsx": { icon: "far fa-object-group", color: 'purple' },
1025
+ "vtx": { icon: "far fa-object-group", color: 'purple' },
1026
+ "vdx": { icon: "far fa-object-group", color: 'purple' },
1027
+ "vssx": { icon: "far fa-object-group", color: 'purple' },
1028
+ "vstx": { icon: "far fa-object-group", color: 'purple' },
1029
+ "vsdm": { icon: "far fa-object-group", color: 'purple' },
1030
+ "vssm": { icon: "far fa-object-group", color: 'purple' },
1031
+ "vstm": { icon: "far fa-object-group", color: 'purple' },
1032
+ "vdw": { icon: "far fa-object-group", color: 'purple' },
1033
+ "vsd": { icon: "far fa-object-group", color: 'purple' },
1034
+ "vss": { icon: "far fa-object-group", color: 'purple' },
1035
+ "vst": { icon: "far fa-object-group", color: 'purple' },
1036
+ "odg": { icon: "far fa-object-group", color: 'orange' },
1037
+ "otg": { icon: "far fa-object-group", color: 'orange' },
1038
+ "gdraw": { icon: "far fa-object-group", color: 'red' },
1039
+ "pub": { icon: "far fa-object-group", color: 'darkgreen' },
1040
+ "ldap": { icon: "far fa-users", color: 'brown' },
1041
+ "ad": { icon: "far fa-users", color: 'brown' },
1042
+ "mmp": { icon: "fas fa-file-medical", color: 'grey' },
1043
+ "mppx": { icon: "fas fa-file-medical", color: 'grey' },
1044
+ };
1045
+
1046
+ class FormatIconComponent {
1047
+ constructor() {
1048
+ this._formatIcons = defaultFormatIcons;
1049
+ }
1050
+ ngOnChanges() {
1051
+ const icon = this.extension ? this._formatIcons[this.extension] : undefined;
1052
+ this.icon = icon?.icon || this._formatIcons.file.icon;
1053
+ }
1054
+ }
1055
+ FormatIconComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.6", ngImport: i0, type: FormatIconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1056
+ FormatIconComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.0.6", type: FormatIconComponent, isStandalone: true, selector: "sq-format-icon", inputs: { extension: "extension" }, usesOnChanges: true, ngImport: i0, template: "<span *ngIf=\"icon\" class=\"{{icon}}\"></span>", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], encapsulation: i0.ViewEncapsulation.None });
1057
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.6", ngImport: i0, type: FormatIconComponent, decorators: [{
1058
+ type: Component,
1059
+ args: [{ selector: 'sq-format-icon', encapsulation: ViewEncapsulation.None, standalone: true, imports: [CommonModule], template: "<span *ngIf=\"icon\" class=\"{{icon}}\"></span>" }]
1060
+ }], propDecorators: { extension: [{
1061
+ type: Input
1062
+ }] } });
1063
+
1064
+ class ChatReferenceComponent {
1065
+ constructor() {
1066
+ this.openDocument = new EventEmitter();
1067
+ this.openPreview = new EventEmitter();
1068
+ }
1069
+ get parts() {
1070
+ if (!this.attachment)
1071
+ return [];
1072
+ return this.attachment.parts.filter(part => (!this.partId || part.partId === this.partId) && !!part.text);
1073
+ }
1074
+ expandAttachment() {
1075
+ if (this.partId)
1076
+ return;
1077
+ this.attachment['$expanded'] = !this.attachment['$expanded'];
1078
+ }
1079
+ }
1080
+ ChatReferenceComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.6", ngImport: i0, type: ChatReferenceComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1081
+ ChatReferenceComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.0.6", type: ChatReferenceComponent, isStandalone: true, selector: "sq-chat-reference", inputs: { reference: "reference", attachment: "attachment", partId: "partId" }, outputs: { openDocument: "openDocument", openPreview: "openPreview" }, ngImport: i0, template: "<div [class.reference-tooltip]=\"!!partId\">\n <div class=\"reference-data\" [class.expanded]=\"attachment['$expanded'] || !!partId\">\n <span class=\"reference me-1\">{{reference}}</span>\n <sq-format-icon [extension]=\"attachment.record.fileext\"></sq-format-icon>\n <a [id]=\"'attachment-'+attachment.recordId\" (click)=\"expandAttachment()\">\n {{attachment.record.title}}\n </a>\n <i class=\"fas fa-eye\" (click)=\"openPreview.emit(attachment)\" [sqTooltip]=\"!partId ? 'Preview document' : ''\"></i>\n <i class=\"fas fa-arrow-up-right-from-square\" *ngIf=\"attachment.record.url1 || attachment.record.originalUrl\"\n (click)=\"openDocument.emit(attachment.record)\" [sqTooltip]=\"!partId ? 'Open document' : ''\"></i>\n </div>\n <div class=\"reference-passages\" *ngIf=\"!!partId || (attachment['$expanded']) && parts.length\">\n <div class=\"reference-passage\" *ngFor=\"let part of parts\">\n <span class=\"reference me-1\">{{reference}}.{{part.partId}}</span>\n <span [innerHTML]=\"part.text\"></span>\n </div>\n </div>\n</div>", styles: [".ast-primary{color:var(--ast-primary-color, #005DA7);background-color:var(--ast-primary-bg, #f2f8fe)}.ast-primary-hover{background-color:var(--ast-primary-bg, #f2f8fe)}.ast-primary-hover:hover{color:var(--ast-primary-color, #005DA7)}.ast-secondary{color:var(--ast-secondary-color, #FF732E);background-color:var(--ast-secondary-bg, #FFF8F1)}.ast-btn{border:0;text-align:left;padding-top:.5rem;padding-bottom:.5rem;display:flex;align-items:center}:host ::ng-deep .reference,:host ::ng-deep .message-content .reference,:host ::ng-deep .attachment .reference{position:relative;bottom:var(--ast-reference-bottom, .3em);font-weight:var(--ast-reference-font-weight, bold);padding:var(--ast-reference-padding, 0 .2em);margin:var(--ast-reference-margin, 0 .1em);border-radius:var(--ast-reference-border-radius, .2em);background-color:var(--ast-reference-background-color, lightblue);color:var(--ast-reference-color, black)}:host ::ng-deep .reference,:host ::ng-deep .message-content .reference{font-size:var(--ast-reference-message-font-size, .7em)}:host ::ng-deep .attachment .reference{font-size:var(--ast-reference-attachment-font-size, 13px)}:host{display:block}:host.expanded,:host:hover{background-color:#fff}.reference-data{display:flex;flex-direction:row;align-items:baseline;padding:var(--ast-size-1, .25rem)}.reference-data a{color:var(--ast-secondary-color, #FF732E);flex-grow:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:var(--font-weight-bold, 500);cursor:pointer}.reference-data i{padding:var(--ast-size-1, .25rem);margin-left:var(--ast-size-1, .25rem);cursor:pointer;color:#000}.reference-data i.active{color:#fff;background-color:var(--ast-secondary-color, #FF732E)}.reference-data:not(.expanded) i{opacity:0}.reference-data:not(.expanded):hover i{opacity:1}.reference-passages{white-space:normal;font-style:italic;font-weight:400;padding:1rem 0;color:#000}.reference-passages .reference-passage{display:flex;align-items:baseline;padding-left:2.5rem;padding-right:1rem;word-wrap:break-word}.reference-passages .reference-passage+.reference-passage{padding-top:1rem}.reference-passages .reference-passage .reference{margin-right:var(--ast-size-2, .5rem)}sq-format-icon{margin-left:var(--ast-size-1, .25rem);margin-right:var(--ast-size-2, .5rem);color:var(--ast-secondary-color, #FF732E)}.reference-tooltip{max-width:600px!important;box-shadow:0 .5rem 1rem #00000026;padding:.5rem;font-size:.875rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: UtilsModule }, { kind: "directive", type: i2$1.TooltipDirective, selector: "[sqTooltip]", inputs: ["sqTooltip", "sqTooltipData", "sqTooltipTemplate", "placement", "fallbackPlacements", "delay", "hoverableTooltip", "tooltipClass"] }, { kind: "component", type: FormatIconComponent, selector: "sq-format-icon", inputs: ["extension"] }] });
1082
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.6", ngImport: i0, type: ChatReferenceComponent, decorators: [{
1083
+ type: Component,
1084
+ args: [{ selector: 'sq-chat-reference', standalone: true, imports: [CommonModule, UtilsModule, FormatIconComponent], template: "<div [class.reference-tooltip]=\"!!partId\">\n <div class=\"reference-data\" [class.expanded]=\"attachment['$expanded'] || !!partId\">\n <span class=\"reference me-1\">{{reference}}</span>\n <sq-format-icon [extension]=\"attachment.record.fileext\"></sq-format-icon>\n <a [id]=\"'attachment-'+attachment.recordId\" (click)=\"expandAttachment()\">\n {{attachment.record.title}}\n </a>\n <i class=\"fas fa-eye\" (click)=\"openPreview.emit(attachment)\" [sqTooltip]=\"!partId ? 'Preview document' : ''\"></i>\n <i class=\"fas fa-arrow-up-right-from-square\" *ngIf=\"attachment.record.url1 || attachment.record.originalUrl\"\n (click)=\"openDocument.emit(attachment.record)\" [sqTooltip]=\"!partId ? 'Open document' : ''\"></i>\n </div>\n <div class=\"reference-passages\" *ngIf=\"!!partId || (attachment['$expanded']) && parts.length\">\n <div class=\"reference-passage\" *ngFor=\"let part of parts\">\n <span class=\"reference me-1\">{{reference}}.{{part.partId}}</span>\n <span [innerHTML]=\"part.text\"></span>\n </div>\n </div>\n</div>", styles: [".ast-primary{color:var(--ast-primary-color, #005DA7);background-color:var(--ast-primary-bg, #f2f8fe)}.ast-primary-hover{background-color:var(--ast-primary-bg, #f2f8fe)}.ast-primary-hover:hover{color:var(--ast-primary-color, #005DA7)}.ast-secondary{color:var(--ast-secondary-color, #FF732E);background-color:var(--ast-secondary-bg, #FFF8F1)}.ast-btn{border:0;text-align:left;padding-top:.5rem;padding-bottom:.5rem;display:flex;align-items:center}:host ::ng-deep .reference,:host ::ng-deep .message-content .reference,:host ::ng-deep .attachment .reference{position:relative;bottom:var(--ast-reference-bottom, .3em);font-weight:var(--ast-reference-font-weight, bold);padding:var(--ast-reference-padding, 0 .2em);margin:var(--ast-reference-margin, 0 .1em);border-radius:var(--ast-reference-border-radius, .2em);background-color:var(--ast-reference-background-color, lightblue);color:var(--ast-reference-color, black)}:host ::ng-deep .reference,:host ::ng-deep .message-content .reference{font-size:var(--ast-reference-message-font-size, .7em)}:host ::ng-deep .attachment .reference{font-size:var(--ast-reference-attachment-font-size, 13px)}:host{display:block}:host.expanded,:host:hover{background-color:#fff}.reference-data{display:flex;flex-direction:row;align-items:baseline;padding:var(--ast-size-1, .25rem)}.reference-data a{color:var(--ast-secondary-color, #FF732E);flex-grow:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:var(--font-weight-bold, 500);cursor:pointer}.reference-data i{padding:var(--ast-size-1, .25rem);margin-left:var(--ast-size-1, .25rem);cursor:pointer;color:#000}.reference-data i.active{color:#fff;background-color:var(--ast-secondary-color, #FF732E)}.reference-data:not(.expanded) i{opacity:0}.reference-data:not(.expanded):hover i{opacity:1}.reference-passages{white-space:normal;font-style:italic;font-weight:400;padding:1rem 0;color:#000}.reference-passages .reference-passage{display:flex;align-items:baseline;padding-left:2.5rem;padding-right:1rem;word-wrap:break-word}.reference-passages .reference-passage+.reference-passage{padding-top:1rem}.reference-passages .reference-passage .reference{margin-right:var(--ast-size-2, .5rem)}sq-format-icon{margin-left:var(--ast-size-1, .25rem);margin-right:var(--ast-size-2, .5rem);color:var(--ast-secondary-color, #FF732E)}.reference-tooltip{max-width:600px!important;box-shadow:0 .5rem 1rem #00000026;padding:.5rem;font-size:.875rem}\n"] }]
1085
+ }], propDecorators: { reference: [{
1086
+ type: Input
1087
+ }], attachment: [{
1088
+ type: Input
1089
+ }], partId: [{
1090
+ type: Input
1091
+ }], openDocument: [{
1092
+ type: Output
1093
+ }], openPreview: [{
1094
+ type: Output
1095
+ }] } });
1096
+
1097
+ class ChatMessageComponent {
1098
+ constructor(searchService, ui, principalService, cdr, el) {
1099
+ this.searchService = searchService;
1100
+ this.ui = ui;
1101
+ this.principalService = principalService;
1102
+ this.cdr = cdr;
1103
+ this.el = el;
1104
+ this.canEdit = false;
1105
+ this.canRegenerate = false;
1106
+ this.canCopy = false;
1107
+ this.referenceClicked = new EventEmitter();
1108
+ this.edit = new EventEmitter();
1109
+ this.regenerate = new EventEmitter();
1110
+ this.openPreview = new EventEmitter();
1111
+ this.references = [];
1112
+ this.referenceMap = new Map();
1113
+ this.showReferences = true;
1114
+ /**
1115
+ * This Unified plugin looks a text nodes and replaces any reference in the
1116
+ * form [1], [2.3], etc. with custom nodes of type "chat-reference".
1117
+ */
1118
+ this.referencePlugin = (tree) => {
1119
+ const references = new Set();
1120
+ // Visit all text nodes
1121
+ visit(tree, "text", (node, index, parent) => {
1122
+ let text = node.value;
1123
+ text = this.reformatReferences(text);
1124
+ const matches = this.getReferenceMatches(text);
1125
+ // Quit if no references were found
1126
+ if (matches.length === 0) {
1127
+ return CONTINUE;
1128
+ }
1129
+ const nodes = [];
1130
+ for (let match of matches) {
1131
+ const refId = match[1].trim();
1132
+ const [ref] = refId.split(".");
1133
+ // We find a valid reference in the text
1134
+ if (!isNaN(+ref)) {
1135
+ references.add(+ref); // Add it to the set of used references
1136
+ // If needed, insert a text node before the reference
1137
+ const current = nodes.at(-1) ?? { end: 0 };
1138
+ if (match.index > current.end) {
1139
+ nodes.push({ type: "text", value: text.substring(current.end, match.index), end: match.index });
1140
+ }
1141
+ // Add a custom reference node
1142
+ nodes.push({ type: "chat-reference", refId, end: match.index + match[0].length });
1143
+ }
1144
+ }
1145
+ // Quit if no references were found
1146
+ if (nodes.length === 0) {
1147
+ return CONTINUE;
1148
+ }
1149
+ if (nodes.at(-1).end < text.length) {
1150
+ nodes.push({ type: "text", value: text.substring(nodes.at(-1).end, text.length), end: text.length });
1151
+ }
1152
+ // Delete the current text node from the parent and replace it with the new nodes
1153
+ parent.children.splice(index, 1, ...nodes);
1154
+ return index + nodes.length; // Visit the next node after the inserted ones
1155
+ });
1156
+ if (references.size > 0) {
1157
+ this.references = Array.from(references.values())
1158
+ .sort((a, b) => a - b)
1159
+ .map(r => '' + r);
1160
+ this.cdr.detectChanges();
1161
+ }
1162
+ return tree;
1163
+ };
1164
+ this.placeholderPlugin = (tree) => {
1165
+ visit(tree, "text", (node, index, parent) => {
1166
+ parent.children.push({ type: "streaming-placeholder" });
1167
+ return EXIT;
1168
+ }, true);
1169
+ return tree;
1170
+ };
1171
+ }
1172
+ get name() {
1173
+ return !this.principalService.principal ? ''
1174
+ : this.principalService.principal['fullName'] || this.principalService.principal.name;
1175
+ }
1176
+ ngOnChanges(changes) {
1177
+ if (changes.streaming) {
1178
+ this.collapseProgress = !this.streaming;
1179
+ }
1180
+ if (changes.message && this.message.role === "assistant") {
1181
+ this.references = [];
1182
+ this.referenceMap.clear();
1183
+ for (let m of this.conversation) {
1184
+ if (m.additionalProperties.$attachment) {
1185
+ for (const attachment of m.additionalProperties.$attachment) {
1186
+ this.referenceMap.set('' + attachment.contextId, { ...attachment, $partId: attachment.parts?.[0].partId });
1187
+ for (let i = 0; i < attachment.parts.length; i++) {
1188
+ const refId = `${attachment.contextId}.${attachment.parts[i].partId}`;
1189
+ this.referenceMap.set(refId, { ...attachment, $partId: attachment.parts[i].partId });
1190
+ }
1191
+ }
1192
+ }
1193
+ }
1194
+ this.processor = unified()
1195
+ .use(remarkParse)
1196
+ .use(remarkGfm)
1197
+ .use(() => this.referencePlugin);
1198
+ if (this.streaming) {
1199
+ this.processor = this.processor.use(() => this.placeholderPlugin);
1200
+ }
1201
+ }
1202
+ }
1203
+ ngAfterViewInit() {
1204
+ Prism?.highlightAllUnder?.(this.el.nativeElement);
1205
+ }
1206
+ openDocument(record) {
1207
+ const url = record.url1 || record.originalUrl;
1208
+ if (url) {
1209
+ // Open the URL in a new tab
1210
+ window.open(url, '_blank');
1211
+ }
1212
+ this.referenceClicked.emit(record);
1213
+ }
1214
+ /**
1215
+ * Reformat [ids: 12.2, 42.5] to [12.2][42.5]
1216
+ */
1217
+ reformatReferences(content) {
1218
+ return content.replace(/\[(?:ids?:?\s*)?(?:documents?:?\s*)?(\s*(?:,?\s*\d+(?:\.\d+)?(?:\.part)?\s*)+)\]/g, (str, match) => `[${match.replace(/\.part/g, "").split(',').join("] [")}]`);
1219
+ }
1220
+ /**
1221
+ * Match all references in a given message
1222
+ */
1223
+ getReferenceMatches(content) {
1224
+ return Array.from(content.matchAll(/\[(\s*\d+(?:\.\d+)?\s*)\]/g));
1225
+ }
1226
+ copyToClipboard(text) {
1227
+ this.ui.copyToClipboard(text);
1228
+ }
1229
+ }
1230
+ ChatMessageComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.6", ngImport: i0, type: ChatMessageComponent, deps: [{ token: i1$1.SearchService }, { token: i2$1.UIService }, { token: i3.PrincipalWebService }, { token: i0.ChangeDetectorRef }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
1231
+ ChatMessageComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.0.6", type: ChatMessageComponent, isStandalone: true, selector: "sq-chat-message", inputs: { message: "message", conversation: "conversation", assistantIcon: "assistantIcon", streaming: "streaming", canEdit: "canEdit", canRegenerate: "canRegenerate", canCopy: "canCopy" }, outputs: { referenceClicked: "referenceClicked", edit: "edit", regenerate: "regenerate", openPreview: "openPreview" }, usesOnChanges: true, ngImport: i0, template: "<!-- Message icon -->\n<span class=\"message-icon\" [title]=\"message.role\">\n <i [ngClass]=\"assistantIcon\" [style.--sq-size.px]=\"24\" *ngIf=\"message.role === 'assistant'\"></i>\n <sq-initials-avatar [fullName]=\"name\" *ngIf=\"message.role !== 'assistant'\"></sq-initials-avatar>\n</span>\n\n<!-- Message body -->\n<div class=\"flex-grow-1\" style=\"min-width: 0;\" [ngClass]=\"'message-'+message.role\">\n\n <div *ngIf=\"message.additionalProperties.$progress as progress\" class=\"small ms-3 mb-2\">\n <a href=\"#\" (click)=\"collapseProgress = !collapseProgress; false\" class=\"text-muted\">\n View progress\n <i class=\"fas fa-chevron-{{collapseProgress? 'right' : 'down'}}\"></i>\n </a>\n <sq-collapse [collapsed]=\"collapseProgress\">\n <ng-template>\n <ul class=\"list-unstyled\">\n <li *ngFor=\"let step of progress\" [title]=\"step.time ?? ''\">\n <i class=\"fas fa-fw fa-check text-success\" *ngIf=\"step.done\"></i>\n <i class=\"fas fa-fw fa-spin fa-circle-notch\" *ngIf=\"!step.done\"></i>\n <span class=\"ms-2 fw-bold\">{{step.title}}</span>\n <span *ngIf=\"step.content\">: {{step.content}}</span>\n </li>\n </ul>\n </ng-template>\n </sq-collapse>\n </div>\n\n <div class=\"message-content\" *ngIf=\"message.content\">\n\n <remark *ngIf=\"message.role === 'assistant'\" [markdown]=\"message.content\" [processor]=\"processor\">\n\n <ng-template remarkTemplate=\"chat-reference\" let-ref>\n\n <a *ngIf=\"referenceMap.get(ref.refId) as attachment; else staticRefTpl\"\n (click)=\"openDocument(attachment.record)\"\n class=\"reference\"\n [sqTooltip]=\"attachment\"\n [sqTooltipTemplate]=\"tooltipTpl\"\n [hoverableTooltip]=\"true\"\n >{{ref.refId}}</a>\n\n <ng-template #staticRefTpl>\n <span class=\"reference\">{{ref.refId}}</span>\n </ng-template>\n\n </ng-template>\n\n <ng-template remarkTemplate=\"streaming-placeholder\">\n <span class=\"placeholder-glow\" *ngIf=\"streaming\">\n <span class=\"placeholder ms-1\"></span>\n </span>\n </ng-template>\n\n <ng-template remarkTemplate=\"code\" let-node>\n <div class=\"card mb-2\">\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <span>{{node.lang}}</span>\n <button class=\"btn btn-light btn-sm\" (click)=\"copyToClipboard(node.value)\"><i class=\"far fa-fw fa-clipboard\"></i> Copy code</button>\n </div>\n <pre class=\"language-{{node.lang}} my-0 rounded-0 rounded-bottom\"><code class=\"language-{{node.lang}}\">{{node.value}}</code></pre>\n </div>\n </ng-template>\n\n </remark>\n\n <p *ngIf=\"message.role === 'user'\">{{message.content}}</p>\n\n <!-- List of reference, if any -->\n <div *ngIf=\"references?.length\" class=\"references\">\n <span class=\"references-title\">References</span> <i class=\"fas references-collapse\" [class.fa-chevron-down]=\"showReferences\" [class.fa-chevron-right]=\"!showReferences\" (click)=\"showReferences=!showReferences\"></i>\n <sq-collapse [collapsed]=\"!showReferences\">\n <ng-template>\n <ul>\n <ng-container *ngFor=\"let reference of references\">\n <li *ngIf=\"referenceMap.get(reference) as attachment\" class=\"text-truncate\">\n <sq-chat-reference [class.expanded]=\"attachment.$expanded\" [attachment]=\"attachment\" [reference]=\"reference\"\n (openPreview)=\"openPreview.emit($event)\" (openDocument)=\"openDocument($event)\"></sq-chat-reference>\n </li>\n </ng-container>\n </ul>\n </ng-template>\n </sq-collapse>\n </div>\n \n </div>\n\n <!-- Edit / Regenerate floating actions -->\n <div class=\"sq-chat-message-action\">\n <button class=\"btn btn-sm\" *ngIf=\"canEdit\" sqTooltip=\"Edit message\"\n (click)=\"edit.emit(message)\">\n <i class=\"fas fa-edit\"></i>\n </button>\n <button class=\"btn btn-sm\" *ngIf=\"canCopy\" sqTooltip=\"Copy to clipboard\"\n (click)=\"copyToClipboard(message.content)\">\n <i class=\"far fa-clipboard\"></i>\n </button>\n <button class=\"btn btn-sm\" *ngIf=\"canRegenerate\" sqTooltip=\"Regenerate message\"\n (click)=\"regenerate.emit(message)\">\n <i class=\"fas fa-sync-alt\"></i>\n </button>\n </div>\n\n <ng-template #tooltipTpl let-ref>\n <sq-chat-reference class=\"expanded\"\n [attachment]=\"ref\"\n [reference]=\"ref.contextId\"\n [partId]=\"ref.$partId\"\n (openPreview)=\"openPreview.emit($event)\"\n (openDocument)=\"openDocument($event)\">\n </sq-chat-reference>\n </ng-template>\n\n</div>\n", styles: [".ast-primary{color:var(--ast-primary-color, #005DA7);background-color:var(--ast-primary-bg, #f2f8fe)}.ast-primary-hover{background-color:var(--ast-primary-bg, #f2f8fe)}.ast-primary-hover:hover{color:var(--ast-primary-color, #005DA7)}.ast-secondary{color:var(--ast-secondary-color, #FF732E);background-color:var(--ast-secondary-bg, #FFF8F1)}.ast-btn{border:0;text-align:left;padding-top:.5rem;padding-bottom:.5rem;display:flex;align-items:center}:host{display:flex}:host:not(:hover) .sq-chat-message-action{display:none}.message-content{padding:var(--ast-message-padding, var(--ast-size-3, .75rem));border-radius:var(--ast-message-border-radius, var(--ast-size-4, 1rem));display:inline-block;max-width:100%}.message-content .references{margin-top:var(--ast-size-5, 1.25rem)}.message-content .references ul{border-left:.2rem solid var(--ast-secondary-color, #FF732E);padding-left:var(--ast-size-5, 1.25rem);margin-top:var(--ast-size-2, .5rem)}.message-content .references .references-title{font-weight:var(--font-weight-bold, 500)}.message-content .references .references-collapse{cursor:pointer;margin-left:var(--ast-size-2, .5rem)}.message-content ::ng-deep p:last-child{margin-bottom:0}.message-content ::ng-deep .placeholder-glow .placeholder{animation-duration:.4s;width:12px;height:var(--ast-size-4, 1rem);vertical-align:text-bottom}.message-content ::ng-deep img{max-width:100%}.message-content ::ng-deep table{display:block;border:1px solid #ccc;border-collapse:collapse;margin:0;padding:0;min-width:100%;overflow-x:auto;table-layout:fixed}.message-content ::ng-deep table tr{background-color:#f8f8f8;border:1px solid #ddd;padding:.35em}.message-content ::ng-deep table th,.message-content ::ng-deep table td{padding:.625em;text-align:center}.message-content ::ng-deep table th{font-size:.85em;letter-spacing:.1em;text-transform:uppercase}.message-assistant .message-content{background:var(--ast-secondary-bg, #FFF8F1)}.message-user .message-content{background:var(--ast-primary-bg, #f2f8fe);font-weight:var(--ast-user-font-weight, var(--font-weight-bold, 500))}.sq-chat-message-action{position:absolute;right:calc(0rem - var(--ast-size-2, .5rem));top:var(--ast-size-2, .5rem);display:flex;flex-direction:column}.message-icon{margin-top:var(--ast-size-3, .75rem);margin-right:var(--ast-size-4, 1rem)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: UtilsModule }, { kind: "directive", type: i2$1.TooltipDirective, selector: "[sqTooltip]", inputs: ["sqTooltip", "sqTooltipData", "sqTooltipTemplate", "placement", "fallbackPlacements", "delay", "hoverableTooltip", "tooltipClass"] }, { kind: "ngmodule", type: CollapseModule }, { kind: "component", type: i5.Collapse, selector: "sq-collapse", inputs: ["collapsed"] }, { kind: "ngmodule", type: RemarkModule }, { kind: "component", type: i6.RemarkComponent, selector: "remark", inputs: ["markdown", "processor", "debug"] }, { kind: "directive", type: i6.RemarkTemplateDirective, selector: "[remarkTemplate]", inputs: ["remarkTemplate"] }, { kind: "component", type: InitialsAvatarComponent, selector: "sq-initials-avatar", inputs: ["fullName", "size"] }, { kind: "component", type: ChatReferenceComponent, selector: "sq-chat-reference", inputs: ["reference", "attachment", "partId"], outputs: ["openDocument", "openPreview"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1232
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.6", ngImport: i0, type: ChatMessageComponent, decorators: [{
1233
+ type: Component,
1234
+ args: [{ selector: "sq-chat-message", changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [CommonModule, UtilsModule, CollapseModule, RemarkModule,
1235
+ InitialsAvatarComponent, ChatReferenceComponent], template: "<!-- Message icon -->\n<span class=\"message-icon\" [title]=\"message.role\">\n <i [ngClass]=\"assistantIcon\" [style.--sq-size.px]=\"24\" *ngIf=\"message.role === 'assistant'\"></i>\n <sq-initials-avatar [fullName]=\"name\" *ngIf=\"message.role !== 'assistant'\"></sq-initials-avatar>\n</span>\n\n<!-- Message body -->\n<div class=\"flex-grow-1\" style=\"min-width: 0;\" [ngClass]=\"'message-'+message.role\">\n\n <div *ngIf=\"message.additionalProperties.$progress as progress\" class=\"small ms-3 mb-2\">\n <a href=\"#\" (click)=\"collapseProgress = !collapseProgress; false\" class=\"text-muted\">\n View progress\n <i class=\"fas fa-chevron-{{collapseProgress? 'right' : 'down'}}\"></i>\n </a>\n <sq-collapse [collapsed]=\"collapseProgress\">\n <ng-template>\n <ul class=\"list-unstyled\">\n <li *ngFor=\"let step of progress\" [title]=\"step.time ?? ''\">\n <i class=\"fas fa-fw fa-check text-success\" *ngIf=\"step.done\"></i>\n <i class=\"fas fa-fw fa-spin fa-circle-notch\" *ngIf=\"!step.done\"></i>\n <span class=\"ms-2 fw-bold\">{{step.title}}</span>\n <span *ngIf=\"step.content\">: {{step.content}}</span>\n </li>\n </ul>\n </ng-template>\n </sq-collapse>\n </div>\n\n <div class=\"message-content\" *ngIf=\"message.content\">\n\n <remark *ngIf=\"message.role === 'assistant'\" [markdown]=\"message.content\" [processor]=\"processor\">\n\n <ng-template remarkTemplate=\"chat-reference\" let-ref>\n\n <a *ngIf=\"referenceMap.get(ref.refId) as attachment; else staticRefTpl\"\n (click)=\"openDocument(attachment.record)\"\n class=\"reference\"\n [sqTooltip]=\"attachment\"\n [sqTooltipTemplate]=\"tooltipTpl\"\n [hoverableTooltip]=\"true\"\n >{{ref.refId}}</a>\n\n <ng-template #staticRefTpl>\n <span class=\"reference\">{{ref.refId}}</span>\n </ng-template>\n\n </ng-template>\n\n <ng-template remarkTemplate=\"streaming-placeholder\">\n <span class=\"placeholder-glow\" *ngIf=\"streaming\">\n <span class=\"placeholder ms-1\"></span>\n </span>\n </ng-template>\n\n <ng-template remarkTemplate=\"code\" let-node>\n <div class=\"card mb-2\">\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <span>{{node.lang}}</span>\n <button class=\"btn btn-light btn-sm\" (click)=\"copyToClipboard(node.value)\"><i class=\"far fa-fw fa-clipboard\"></i> Copy code</button>\n </div>\n <pre class=\"language-{{node.lang}} my-0 rounded-0 rounded-bottom\"><code class=\"language-{{node.lang}}\">{{node.value}}</code></pre>\n </div>\n </ng-template>\n\n </remark>\n\n <p *ngIf=\"message.role === 'user'\">{{message.content}}</p>\n\n <!-- List of reference, if any -->\n <div *ngIf=\"references?.length\" class=\"references\">\n <span class=\"references-title\">References</span> <i class=\"fas references-collapse\" [class.fa-chevron-down]=\"showReferences\" [class.fa-chevron-right]=\"!showReferences\" (click)=\"showReferences=!showReferences\"></i>\n <sq-collapse [collapsed]=\"!showReferences\">\n <ng-template>\n <ul>\n <ng-container *ngFor=\"let reference of references\">\n <li *ngIf=\"referenceMap.get(reference) as attachment\" class=\"text-truncate\">\n <sq-chat-reference [class.expanded]=\"attachment.$expanded\" [attachment]=\"attachment\" [reference]=\"reference\"\n (openPreview)=\"openPreview.emit($event)\" (openDocument)=\"openDocument($event)\"></sq-chat-reference>\n </li>\n </ng-container>\n </ul>\n </ng-template>\n </sq-collapse>\n </div>\n \n </div>\n\n <!-- Edit / Regenerate floating actions -->\n <div class=\"sq-chat-message-action\">\n <button class=\"btn btn-sm\" *ngIf=\"canEdit\" sqTooltip=\"Edit message\"\n (click)=\"edit.emit(message)\">\n <i class=\"fas fa-edit\"></i>\n </button>\n <button class=\"btn btn-sm\" *ngIf=\"canCopy\" sqTooltip=\"Copy to clipboard\"\n (click)=\"copyToClipboard(message.content)\">\n <i class=\"far fa-clipboard\"></i>\n </button>\n <button class=\"btn btn-sm\" *ngIf=\"canRegenerate\" sqTooltip=\"Regenerate message\"\n (click)=\"regenerate.emit(message)\">\n <i class=\"fas fa-sync-alt\"></i>\n </button>\n </div>\n\n <ng-template #tooltipTpl let-ref>\n <sq-chat-reference class=\"expanded\"\n [attachment]=\"ref\"\n [reference]=\"ref.contextId\"\n [partId]=\"ref.$partId\"\n (openPreview)=\"openPreview.emit($event)\"\n (openDocument)=\"openDocument($event)\">\n </sq-chat-reference>\n </ng-template>\n\n</div>\n", styles: [".ast-primary{color:var(--ast-primary-color, #005DA7);background-color:var(--ast-primary-bg, #f2f8fe)}.ast-primary-hover{background-color:var(--ast-primary-bg, #f2f8fe)}.ast-primary-hover:hover{color:var(--ast-primary-color, #005DA7)}.ast-secondary{color:var(--ast-secondary-color, #FF732E);background-color:var(--ast-secondary-bg, #FFF8F1)}.ast-btn{border:0;text-align:left;padding-top:.5rem;padding-bottom:.5rem;display:flex;align-items:center}:host{display:flex}:host:not(:hover) .sq-chat-message-action{display:none}.message-content{padding:var(--ast-message-padding, var(--ast-size-3, .75rem));border-radius:var(--ast-message-border-radius, var(--ast-size-4, 1rem));display:inline-block;max-width:100%}.message-content .references{margin-top:var(--ast-size-5, 1.25rem)}.message-content .references ul{border-left:.2rem solid var(--ast-secondary-color, #FF732E);padding-left:var(--ast-size-5, 1.25rem);margin-top:var(--ast-size-2, .5rem)}.message-content .references .references-title{font-weight:var(--font-weight-bold, 500)}.message-content .references .references-collapse{cursor:pointer;margin-left:var(--ast-size-2, .5rem)}.message-content ::ng-deep p:last-child{margin-bottom:0}.message-content ::ng-deep .placeholder-glow .placeholder{animation-duration:.4s;width:12px;height:var(--ast-size-4, 1rem);vertical-align:text-bottom}.message-content ::ng-deep img{max-width:100%}.message-content ::ng-deep table{display:block;border:1px solid #ccc;border-collapse:collapse;margin:0;padding:0;min-width:100%;overflow-x:auto;table-layout:fixed}.message-content ::ng-deep table tr{background-color:#f8f8f8;border:1px solid #ddd;padding:.35em}.message-content ::ng-deep table th,.message-content ::ng-deep table td{padding:.625em;text-align:center}.message-content ::ng-deep table th{font-size:.85em;letter-spacing:.1em;text-transform:uppercase}.message-assistant .message-content{background:var(--ast-secondary-bg, #FFF8F1)}.message-user .message-content{background:var(--ast-primary-bg, #f2f8fe);font-weight:var(--ast-user-font-weight, var(--font-weight-bold, 500))}.sq-chat-message-action{position:absolute;right:calc(0rem - var(--ast-size-2, .5rem));top:var(--ast-size-2, .5rem);display:flex;flex-direction:column}.message-icon{margin-top:var(--ast-size-3, .75rem);margin-right:var(--ast-size-4, 1rem)}\n"] }]
1236
+ }], ctorParameters: function () { return [{ type: i1$1.SearchService }, { type: i2$1.UIService }, { type: i3.PrincipalWebService }, { type: i0.ChangeDetectorRef }, { type: i0.ElementRef }]; }, propDecorators: { message: [{
1237
+ type: Input
1238
+ }], conversation: [{
1239
+ type: Input
1240
+ }], assistantIcon: [{
1241
+ type: Input
1242
+ }], streaming: [{
1243
+ type: Input
1244
+ }], canEdit: [{
1245
+ type: Input
1246
+ }], canRegenerate: [{
1247
+ type: Input
1248
+ }], canCopy: [{
1249
+ type: Input
1250
+ }], referenceClicked: [{
1251
+ type: Output
1252
+ }], edit: [{
1253
+ type: Output
1254
+ }], regenerate: [{
1255
+ type: Output
1256
+ }], openPreview: [{
1257
+ type: Output
1258
+ }] } });
1259
+
1260
+ class RestChatService extends ChatService {
1261
+ constructor() {
1262
+ super();
1263
+ this.jsonMethodWebService = inject(JsonMethodPluginService);
1264
+ }
1265
+ /**
1266
+ * Initialize the chat process after the login is complete.
1267
+ * It listens for the 'login-complete' event, initializes necessary URL, and performs parallel requests for models, functions and quota data.
1268
+ * @returns An Observable<boolean> indicating the success of the initialization process.
1269
+ */
1270
+ init() {
1271
+ return this.loginService.events.pipe(filter((e) => e.type === 'login-complete'), tap(() => this.getRequestsUrl()),
1272
+ // Execute parallel requests for models and functions
1273
+ switchMap(() => forkJoin([
1274
+ this.listModels(),
1275
+ this.listFunctions()
1276
+ ])),
1277
+ // Map the results of parallel requests to a boolean indicating success
1278
+ map(([models, functions]) => !!models && !!functions),
1279
+ // Any errors during the process are caught, logged, and re-thrown to propagate the error further
1280
+ catchError((error) => {
1281
+ console.error('Error occurred:', error);
1282
+ return throwError(() => error);
1283
+ }),
1284
+ // cache and replay the emitted value for subsequent subscribers, ensuring the initialization logic is only executed once even if there are multiple subscribers
1285
+ shareReplay(1));
1286
+ }
1287
+ /**
1288
+ * Define the GLLM plugin to use for the http requests
1289
+ * It can be overridden by the app config
1290
+ */
1291
+ getRequestsUrl() {
1292
+ if (this.chatConfig$.value.globalSettings.restEndpoint) {
1293
+ this.REQUEST_URL = this.chatConfig$.value.globalSettings.restEndpoint;
1294
+ }
1295
+ else {
1296
+ throw new Error(`The property 'restEndpoint' must be provided when attempting to use 'REST' in assistant instance`);
1297
+ }
1298
+ }
1299
+ listModels() {
1300
+ const data = {
1301
+ action: "listmodels",
1302
+ debug: this.chatConfig$.value.debug
1303
+ };
1304
+ return this.jsonMethodWebService.get(this.REQUEST_URL, data).pipe(map(res => res.models), tap(models => this.models = models?.filter(model => !!model.enable)), catchError((error) => {
1305
+ console.error('Error invoking listmodels:', error);
1306
+ return throwError(() => error);
1307
+ }));
1308
+ }
1309
+ listFunctions() {
1310
+ const data = {
1311
+ action: "listfunctions",
1312
+ debug: this.chatConfig$.value.debug
1313
+ };
1314
+ return this.jsonMethodWebService.get(this.REQUEST_URL, data).pipe(map(res => res.functions), tap(functions => this.functions = functions), catchError((error) => {
1315
+ console.error('Error invoking listfunctions:', error);
1316
+ return throwError(() => error);
1317
+ }));
1318
+ }
1319
+ fetch(messages, query = this.searchService.query) {
1320
+ // Start streaming by invoking the Chat method
1321
+ this.streaming$.next(true);
1322
+ // Prepare the payload to send to the Chat method
1323
+ const data = {
1324
+ action: "chat",
1325
+ history: messages,
1326
+ functions: this.chatConfig$.value.functions,
1327
+ debug: this.chatConfig$.value.debug,
1328
+ serviceSettings: this.chatConfig$.value.serviceSettings,
1329
+ contextSettings: {
1330
+ ...this.chatConfig$.value.contextSettings,
1331
+ app: this.appService.appName,
1332
+ query
1333
+ }
1334
+ };
1335
+ if (this.chatConfig$.value.saveChats) {
1336
+ data.instanceId = this.chatInstanceId;
1337
+ data.savedChatId = this.savedChatId;
1338
+ }
1339
+ // Request the Chat endpoint
1340
+ return this.jsonMethodWebService.post(this.REQUEST_URL, data).pipe(tap((res) => {
1341
+ if (res.quota.maxQuotaReached) {
1342
+ const msg = `Sorry, you have exceeded the allowed quota. Please retry starting from ${this.formatDateTime(res.quota.nextResetUTC)}.`;
1343
+ this.notificationsService.error(msg);
1344
+ throw new Error(msg);
1345
+ }
1346
+ }), map((res) => {
1347
+ // Define $progress from the actions property of the response
1348
+ let $progress;
1349
+ if (res.actions?.length > 0) {
1350
+ const actions = Object.values(res.actions.reduce((acc, item) => {
1351
+ acc[item.guid] = { ...(acc[item.guid] || {}), ...item };
1352
+ return acc;
1353
+ }, {}));
1354
+ $progress = actions.map((a) => ({
1355
+ title: a.displayName ?? "",
1356
+ content: a.displayValue ?? "",
1357
+ done: a.executionTime !== undefined,
1358
+ time: a.executionTime,
1359
+ }));
1360
+ }
1361
+ // Define the response message
1362
+ const response = { ...res.history.at(-1), additionalProperties: { display: true } };
1363
+ if ($progress) {
1364
+ response.additionalProperties.$progress = $progress;
1365
+ }
1366
+ if (res.context) {
1367
+ response.additionalProperties.$attachment = res.context.map((ctx) => ctx.additionalProperties);
1368
+ }
1369
+ // Update the chat history with the history property of the res
1370
+ this.chatHistory = res.history;
1371
+ // Return the result
1372
+ return { history: [...messages, response], executionTime: res.executionTime };
1373
+ }), tap(() => this.notifyAudit(this.chatHistory, this.chatConfig$.value.serviceSettings.service_id)), finalize(() => this.streaming$.next(false)));
1374
+ }
1375
+ listSavedChat() {
1376
+ const data = {
1377
+ action: "SavedChatList",
1378
+ instanceId: this.chatInstanceId,
1379
+ debug: this.chatConfig$.value.debug
1380
+ };
1381
+ this.jsonMethodWebService.get(this.REQUEST_URL, data).subscribe(res => this.savedChats$.next(res.savedChats), error => {
1382
+ console.error('Error occurred while calling the SavedChatList API:', error);
1383
+ this.notificationsService.error('Error occurred while calling the SavedChatList API');
1384
+ });
1385
+ }
1386
+ getSavedChat(id) {
1387
+ const data = {
1388
+ action: "SavedChatGet",
1389
+ instanceId: this.chatInstanceId,
1390
+ savedChatId: id,
1391
+ debug: this.chatConfig$.value.debug
1392
+ };
1393
+ return this.jsonMethodWebService.get(this.REQUEST_URL, data).pipe(map(res => res.savedChat), catchError((error) => {
1394
+ console.error('Error occurred while calling the SavedChatGet API:', error);
1395
+ this.notificationsService.error('Error occurred while calling the SavedChatGet API');
1396
+ return throwError(() => error);
1397
+ }));
1398
+ }
1399
+ deleteSavedChat(ids) {
1400
+ const data = {
1401
+ action: "SavedChatDelete",
1402
+ instanceId: this.chatInstanceId,
1403
+ savedChatIds: ids,
1404
+ debug: this.chatConfig$.value.debug
1405
+ };
1406
+ return this.jsonMethodWebService.post(this.REQUEST_URL, data).pipe(map(res => res.deletedCount), catchError((error) => {
1407
+ console.error('Error occurred while calling the SavedChatDelete API:', error);
1408
+ this.notificationsService.error('Error occurred while calling the SavedChatDelete API ');
1409
+ return throwError(() => error);
1410
+ }));
1411
+ }
1412
+ }
1413
+ RestChatService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.6", ngImport: i0, type: RestChatService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1414
+ RestChatService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.0.6", ngImport: i0, type: RestChatService });
1415
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.6", ngImport: i0, type: RestChatService, decorators: [{
1416
+ type: Injectable
1417
+ }], ctorParameters: function () { return []; } });
1418
+
1419
+ class ChatComponent extends AbstractFacet {
1420
+ constructor() {
1421
+ super();
1422
+ /** Define the protocol to be used for this chat instance*/
1423
+ this.protocol = "WEBSOCKET";
1424
+ /** Map of listeners overriding default registered ones*/
1425
+ this.messageHandlers = new Map();
1426
+ /** When the assistant answer a user question, automatically scroll down to the bottom of the discussion */
1427
+ this.automaticScrollToLastResponse = false;
1428
+ this.enableChat = true;
1429
+ this.showCredits = true;
1430
+ this.customAssistantIcon = '';
1431
+ this.data = new EventEmitter();
1432
+ this.referenceClicked = new EventEmitter();
1433
+ this.openPreview = new EventEmitter();
1434
+ this.loading$ = new EventEmitter(false);
1435
+ this.error = new EventEmitter();
1436
+ this._config = new EventEmitter();
1437
+ this.messages$ = new BehaviorSubject(undefined);
1438
+ this.question = '';
1439
+ this._actions = [];
1440
+ this.sub = new Subscription();
1441
+ this.changes$ = new BehaviorSubject(undefined);
1442
+ this.handleFirstChanges = false;
1443
+ this.isAtBottom = true;
1444
+ this.initializationError = false;
1445
+ this.loginService = inject(LoginService);
1446
+ this.websocketService = inject(WebSocketChatService);
1447
+ this.restService = inject(RestChatService);
1448
+ this.instanceManagerService = inject(InstanceManagerService);
1449
+ this.searchService = inject(SearchService);
1450
+ this.principalService = inject(PrincipalWebService);
1451
+ this.cdr = inject(ChangeDetectorRef);
1452
+ this._actions.push(new Action({
1453
+ icon: 'fas fa-sync',
1454
+ title: 'Reset chat',
1455
+ action: () => this.loadDefaultChat()
1456
+ }));
1457
+ }
1458
+ ngOnInit() {
1459
+ this.sub.add(this.loginService.events.pipe(filter(e => e.type === 'login-complete'), tap(_ => this.instantiateChatService()), map(_ => this.chatService.initChatConfig()), switchMap(() => this.chatService.initConfig$), filter(initConfig => !!initConfig), switchMap(_ => this.chatService.init()), filter(success => !!success), tap(_ => this.onLoadChat()), switchMap(_ => this.chatService.chatConfig$), tap(config => {
1460
+ this.config = config;
1461
+ this._config.emit(config);
1462
+ try {
1463
+ this.updateModelDescription();
1464
+ if (!this.handleFirstChanges) {
1465
+ this.handleChanges();
1466
+ this.addScrollListener();
1467
+ this.handleFirstChanges = true;
1468
+ }
1469
+ }
1470
+ catch (error) {
1471
+ this.initializationError = true;
1472
+ throw error;
1473
+ }
1474
+ })).subscribe());
1475
+ }
1476
+ ngOnChanges(changes) {
1477
+ this.changes$.next(changes);
1478
+ if (this.config) {
1479
+ this.handleChanges();
1480
+ }
1481
+ }
1482
+ ngOnDestroy() {
1483
+ this.sub.unsubscribe();
1484
+ this.dataSubscription?.unsubscribe();
1485
+ }
1486
+ instantiateChatService() {
1487
+ switch (this.protocol) {
1488
+ case 'REST':
1489
+ this.chatService = this.restService;
1490
+ break;
1491
+ case 'WEBSOCKET':
1492
+ this.chatService = this.websocketService;
1493
+ break;
1494
+ default:
1495
+ throw new Error(`Could not found a ChatService implementation corresponding to the provided protocol: '${this.protocol}'`);
1496
+ }
1497
+ this.chatService.setChatInstanceId(this.instanceId);
1498
+ this.instanceManagerService.storeInstance(this.instanceId, this.chatService);
1499
+ }
1500
+ get actions() { return this._actions; }
1501
+ handleChanges() {
1502
+ const changes = this.changes$.value;
1503
+ if (changes?.messageHandlers && this.messageHandlers && this.chatService instanceof WebSocketChatService) {
1504
+ this.chatService.overrideMessageHandlers(this.messageHandlers);
1505
+ }
1506
+ if (!this.messages$.value || changes?.chat) {
1507
+ // Load the chat
1508
+ this.chat ? this.openChat(this.chat.messages) : this.loadDefaultChat();
1509
+ }
1510
+ }
1511
+ addScrollListener() {
1512
+ this.sub.add(merge(this.loading$, this.messages$, this.chatService.streaming$, fromEvent(this.messageList.nativeElement, 'scroll')).subscribe(() => {
1513
+ this.isAtBottom = this.toggleScrollButtonVisibility();
1514
+ this.cdr.detectChanges();
1515
+ }));
1516
+ }
1517
+ updateModelDescription() {
1518
+ this.modelDescription = this.chatService.getModel(this.config.serviceSettings.service_id, this.config.serviceSettings.model_id);
1519
+ this.assistantIcon = !!this.customAssistantIcon ? this.customAssistantIcon : 'sq-sinequa';
1520
+ switch (this.modelDescription?.provider) {
1521
+ case 'Google':
1522
+ this.privacyUrl = '';
1523
+ break;
1524
+ case 'AzureOpenAI':
1525
+ this.privacyUrl = 'https://learn.microsoft.com/en-us/legal/cognitive-services/openai/data-privacy';
1526
+ break;
1527
+ case 'OpenAI':
1528
+ this.privacyUrl = 'https://openai.com/enterprise-privacy';
1529
+ break;
1530
+ case 'Cohere':
1531
+ this.privacyUrl = 'https://cohere.com/security';
1532
+ break;
1533
+ }
1534
+ this.cdr.detectChanges();
1535
+ }
1536
+ submitQuestion() {
1537
+ if (this.question.trim() && this.messages$.value && this.chatService.chatHistory) {
1538
+ if (this.messageToEdit !== undefined) {
1539
+ // Update the messages in the UI
1540
+ this.messages$.next(this.messages$.value.slice(0, this.messageToEdit));
1541
+ // Update the raw messages in the chat history which is the clean version used to make the next request
1542
+ this.chatService.chatHistory = this.chatService.chatHistory.slice(0, this.messageToEdit);
1543
+ this.messageToEdit = undefined;
1544
+ }
1545
+ // Re-attach the $progress and $attachment of the last response to the last assistant's response in the chat history
1546
+ this.chatService.chatHistory.at(-1).additionalProperties.$progress = this.messages$.value.at(-1).additionalProperties.$progress;
1547
+ this.chatService.chatHistory.at(-1).additionalProperties.$attachment = this.messages$.value.at(-1).additionalProperties.$attachment;
1548
+ // Fetch the answer
1549
+ this.fetchAnswer(this.question.trim(), this.chatService.chatHistory);
1550
+ // Clear the input value in the UI
1551
+ this.questionInput.nativeElement.value = '';
1552
+ }
1553
+ }
1554
+ fetchAnswer(question, conversation) {
1555
+ const userMsg = { role: 'user', content: question, additionalProperties: { display: true } };
1556
+ const messages = [...conversation, userMsg];
1557
+ this.messages$.next(messages);
1558
+ this.fetch(messages);
1559
+ }
1560
+ /**
1561
+ * Given a list of messages, fetch the server for a continuation and updates
1562
+ * the list of messages accordingly.
1563
+ * @param messages
1564
+ */
1565
+ fetch(messages) {
1566
+ this.cdr.detectChanges();
1567
+ this.loading$.next(true);
1568
+ this.dataSubscription?.unsubscribe();
1569
+ this.dataSubscription = this.chatService.fetch(messages, this.query)
1570
+ .subscribe({
1571
+ next: res => this.updateData(res.history),
1572
+ error: err => {
1573
+ this.terminateFetch();
1574
+ console.error(err);
1575
+ this.error.emit(err);
1576
+ },
1577
+ complete: () => {
1578
+ this.terminateFetch();
1579
+ }
1580
+ });
1581
+ if (this.automaticScrollToLastResponse) {
1582
+ this.scrollDown();
1583
+ }
1584
+ }
1585
+ /**
1586
+ * Update the UI with the new messages
1587
+ * @param messages
1588
+ */
1589
+ updateData(messages) {
1590
+ this.messages$.next(messages);
1591
+ this.data.emit(messages);
1592
+ this.loading$.next(false);
1593
+ this.question = '';
1594
+ if (this.automaticScrollToLastResponse) {
1595
+ this.scrollDown();
1596
+ }
1597
+ }
1598
+ toggleScrollButtonVisibility() {
1599
+ if (this.messageList?.nativeElement) {
1600
+ return Math.round(this.messageList?.nativeElement.scrollHeight - this.messageList?.nativeElement.scrollTop - 1) <= this.messageList?.nativeElement.clientHeight;
1601
+ }
1602
+ return true;
1603
+ }
1604
+ scrollDown() {
1605
+ setTimeout(() => {
1606
+ if (this.messageList?.nativeElement) {
1607
+ this.messageList.nativeElement.scrollTop = this.messageList.nativeElement.scrollHeight;
1608
+ this.cdr.detectChanges();
1609
+ }
1610
+ }, 10);
1611
+ }
1612
+ newChat() {
1613
+ this.chatService.listSavedChat(); // Refresh the list of saved chats
1614
+ this.loadDefaultChat(); // Start a new chat
1615
+ }
1616
+ loadDefaultChat() {
1617
+ this.openChat([
1618
+ { role: 'system', content: this.config.uiSettings.systemPrompt, additionalProperties: { display: false } },
1619
+ { role: 'user', content: ChatService.formatPrompt(this.config.uiSettings.userPrompt, { principal: this.principalService.principal }), additionalProperties: { display: true } },
1620
+ ]);
1621
+ }
1622
+ openChat(messages, chatId) {
1623
+ if (!messages || !Array.isArray(messages)) {
1624
+ console.error('Error occurs while trying to load the chat discussion. Invalid messages received :', messages);
1625
+ return;
1626
+ }
1627
+ this.chatService.setSavedChatId(chatId || ChatService.generateGUID());
1628
+ this.resetChat();
1629
+ this.messages$.next(messages);
1630
+ this.chatService.chatHistory = messages;
1631
+ const lastMessage = messages.at(-1);
1632
+ if (lastMessage && lastMessage.role === 'user') {
1633
+ this.fetch(messages); // If the last message if from a user, an answer from the assistant is expected
1634
+ }
1635
+ else {
1636
+ this.updateData(messages); // If the last message if from the assistant, we can load the conversation right away
1637
+ this.terminateFetch();
1638
+ }
1639
+ }
1640
+ resetChat() {
1641
+ if (this.messages$.value) {
1642
+ this.messages$.next(undefined); // Reset chat
1643
+ }
1644
+ this.chatService.chatHistory = undefined; // Reset chat history
1645
+ this.question = '';
1646
+ this.terminateFetch();
1647
+ }
1648
+ onLoadChat() {
1649
+ this.loading$.next(true);
1650
+ this.sub.add(this.chatService.loadSavedChat$
1651
+ .pipe(filter(savedChat => !!savedChat), switchMap(savedChat => this.chatService.getSavedChat(savedChat.id)), filter(savedChatHistory => !!savedChatHistory), tap(savedChatHistory => this.openChat(savedChatHistory.History, savedChatHistory.id)), finalize(() => this.chatService.listSavedChat()) // Refresh the list of saved chats
1652
+ ).subscribe());
1653
+ }
1654
+ terminateFetch() {
1655
+ this.dataSubscription?.unsubscribe();
1656
+ this.dataSubscription = undefined;
1657
+ this.loading$.next(false);
1658
+ setTimeout(() => {
1659
+ this.questionInput?.nativeElement.focus();
1660
+ });
1661
+ this.cdr.detectChanges();
1662
+ }
1663
+ editMessage(index) {
1664
+ this.messageToEdit = index;
1665
+ this.question = this.chatService.chatHistory[index].content;
1666
+ this.questionInput?.nativeElement.focus();
1667
+ }
1668
+ regenerateMessage(index) {
1669
+ // Define the chat history based on which the assistant will generate a new answer
1670
+ const slicedMessages = this.chatService.chatHistory.slice(0, index);
1671
+ // Accordingly update the messages in the UI
1672
+ this.messages$.next(slicedMessages);
1673
+ // Fetch the answer
1674
+ this.fetch(slicedMessages);
1675
+ }
1676
+ onKeyUp(event) {
1677
+ switch (event.key) {
1678
+ case 'ArrowUp':
1679
+ this.navigateMessage(-1);
1680
+ break;
1681
+ case 'ArrowDown':
1682
+ this.navigateMessage(1);
1683
+ break;
1684
+ case 'Enter':
1685
+ this.submitQuestion();
1686
+ break;
1687
+ default:
1688
+ break;
1689
+ }
1690
+ // Handle Shift + Enter
1691
+ if (event.shiftKey && event.key === 'Enter') {
1692
+ this.submitQuestion();
1693
+ }
1694
+ }
1695
+ navigateMessage(direction) {
1696
+ if (!this.chatService.chatHistory || this.chatService.chatHistory.length < 1) {
1697
+ return;
1698
+ }
1699
+ const userMessages = this.chatService.chatHistory.filter(m => m.role === 'user');
1700
+ if (userMessages.length < 1) {
1701
+ return;
1702
+ }
1703
+ this.currentMessageIndex = (this.currentMessageIndex ?? userMessages.length) + direction;
1704
+ if (this.currentMessageIndex < 0) {
1705
+ // If the user presses up arrow on the first message, stay at the first message
1706
+ this.currentMessageIndex = 0;
1707
+ }
1708
+ else if (this.currentMessageIndex >= userMessages.length) {
1709
+ // If the user presses down arrow on the last previous message, clear the input
1710
+ this.currentMessageIndex = undefined;
1711
+ this.question = '';
1712
+ return;
1713
+ }
1714
+ this.question = userMessages[this.currentMessageIndex].content;
1715
+ }
1716
+ }
1717
+ ChatComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.6", ngImport: i0, type: ChatComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1718
+ ChatComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.0.6", type: ChatComponent, isStandalone: true, selector: "sq-chat-v3", inputs: { instanceId: "instanceId", query: "query", protocol: "protocol", messageHandlers: "messageHandlers", automaticScrollToLastResponse: "automaticScrollToLastResponse", chat: "chat", enableChat: "enableChat", showCredits: "showCredits", customAssistantIcon: "customAssistantIcon" }, outputs: { data: "data", referenceClicked: "referenceClicked", openPreview: "openPreview", loading$: "loading", error: "error", _config: "config" }, providers: [
1719
+ RestChatService,
1720
+ WebSocketChatService
1721
+ ], queries: [{ propertyName: "loadingTpl", first: true, predicate: ["loadingTpl"], descendants: true }], viewQueries: [{ propertyName: "messageList", first: true, predicate: ["messageList"], descendants: true }, { propertyName: "questionInput", first: true, predicate: ["questionInput"], descendants: true }], usesInheritance: true, usesOnChanges: true, ngImport: i0, template: "\n<ng-container *ngIf=\"!initializationError\">\n <div *ngIf=\"messages$ | async as messages; else loadingTpl || loadingTplDefault\" class=\"h-100 d-flex flex-column\">\n\n <ul class=\"list-group list-group-flush overflow-auto pb-5\" #messageList>\n <ng-container *ngFor=\"let message of messages; let index = index; let last = last\">\n <!-- Regular messages -->\n <li class=\"list-group-item\" *ngIf=\"message.additionalProperties.display\"\n [class.opacity-50]=\"messageToEdit && messageToEdit < index + 1\">\n <sq-chat-message\n [class.sq-user-message]=\"message.role !== 'assistant'\"\n [message]=\"message\"\n [conversation]=\"messages\"\n [assistantIcon]=\"customAssistantIcon || assistantIcon\"\n [streaming]=\"last && (chatService.streaming$ | async)\"\n [canEdit]=\"(loading$ | async) === false && (chatService.streaming$ | async) === false && messageToEdit === undefined && message.role !== 'assistant'\"\n [canRegenerate]=\"(loading$ | async) === false && (chatService.streaming$ | async) === false && messageToEdit === undefined && message.role === 'assistant' && last\"\n [canCopy]=\"!(last && (chatService.streaming$ | async)) && messageToEdit === undefined && message.role === 'assistant'\"\n (edit)=\"editMessage(index)\" (regenerate)=\"regenerateMessage(index)\"\n (referenceClicked)=\"referenceClicked.emit($event)\"\n (openPreview)=\"openPreview.emit($event)\">\n </sq-chat-message>\n </li>\n </ng-container>\n\n <li class=\"list-group-item\" *ngIf=\"(loading$ | async) === true\">\n <ng-container *ngTemplateOutlet=\"loadingTpl || loadingTplDefault\"></ng-container>\n </li>\n </ul>\n\n <div class=\"user-input mt-auto\" *ngIf=\"enableChat\">\n <div class=\"py-2\">\n <ng-container *ngTemplateOutlet=\"inputTpl\"></ng-container>\n <div class=\"text-end small text-muted px-3\" *ngIf=\"showCredits\">\n powered by {{modelDescription?.displayName}} <ng-container *ngIf=\"privacyUrl\"> - <a [href]=\"privacyUrl\" target=\"_blank\">privacy notice</a></ng-container>\n </div>\n </div>\n </div>\n </div>\n</ng-container>\n\n<!-- NG TEMPLATES-->\n\n<ng-template #loadingTplDefault>\n <div class=\"spinner-grow text-success d-block mx-auto my-5\" role=\"status\">\n <span class=\"visually-hidden\">Loading...</span>\n </div>\n</ng-template>\n\n<ng-template #inputTpl>\n <div class=\"px-3 py-1\">\n <div class=\"ast-input-container\">\n <i class=\"fas fa-search\"></i>\n <input #questionInput\n type=\"text\" class=\"form-control\"\n placeholder=\"Ask something\" autofocus\n [(ngModel)]=\"question\"\n (keyup)=\"onKeyUp($event)\"\n [disabled]=\"(loading$ | async) || (chatService.streaming$ | async)\">\n <button\n *ngIf=\"!(chatService.streaming$ | async) && !(loading$ | async)\"\n type=\"button\"\n class=\"btn btn-light ms-2\"\n title=\"Send message\"\n (click)=\"submitQuestion()\">\n <i class=\"fas fa-paper-plane\"></i>\n </button>\n <!--<button\n *ngIf=\"(chatService.streaming$ | async)\"\n type=\"button\"\n class=\"btn btn-light ms-2\"\n title=\"Stop generating\"\n (click)=\"terminateFetch()\">\n <i class=\"fas fa-stop\"></i>\n </button>-->\n <button\n *ngIf=\"messageToEdit\"\n type=\"button\"\n class=\"btn btn-light ms-2\"\n title=\"Cancel edition\"\n (click)=\"messageToEdit = undefined; question = ''\">\n <i class=\"fas fa-undo-alt\"></i>\n </button>\n <div class=\"sq-floating-scroll\" *ngIf=\"!isAtBottom\">\n <button class=\"btn shadow\" (click)=\"scrollDown()\">\n <i class=\"fas fa-angle-double-down\"></i>\n </button>\n </div>\n </div>\n </div>\n</ng-template>\n", styles: [".ast-primary{color:var(--ast-primary-color, #005DA7);background-color:var(--ast-primary-bg, #f2f8fe)}.ast-primary-hover{background-color:var(--ast-primary-bg, #f2f8fe)}.ast-primary-hover:hover{color:var(--ast-primary-color, #005DA7)}.ast-secondary{color:var(--ast-secondary-color, #FF732E);background-color:var(--ast-secondary-bg, #FFF8F1)}.ast-btn{border:0;text-align:left;padding-top:.5rem;padding-bottom:.5rem;display:flex;align-items:center}:host ::ng-deep .reference,:host ::ng-deep .message-content .reference,:host ::ng-deep .attachment .reference{position:relative;bottom:var(--ast-reference-bottom, .3em);font-weight:var(--ast-reference-font-weight, bold);padding:var(--ast-reference-padding, 0 .2em);margin:var(--ast-reference-margin, 0 .1em);border-radius:var(--ast-reference-border-radius, .2em);background-color:var(--ast-reference-background-color, lightblue);color:var(--ast-reference-color, black)}:host ::ng-deep .reference,:host ::ng-deep .message-content .reference{font-size:var(--ast-reference-message-font-size, .7em)}:host ::ng-deep .attachment .reference{font-size:var(--ast-reference-attachment-font-size, 13px)}:host{font-size:.875rem}:host>div>.user-input>div:not(.progress),:host>div>ul>li{width:var(--ast-chat-container-width, 100%);max-width:100%;margin-left:auto;margin-right:auto}:host>div>ul{padding-top:var(--ast-chat-padding-top, 0);padding-bottom:var(--ast-chat-padding-bottom, 0)}li.attachment>p{display:-webkit-box;-webkit-box-orient:vertical;overflow:hidden;-webkit-line-clamp:3}li.attachment.expanded>p{display:block}.progress{--bs-progress-height: 3px}.progress.disabled{--bs-progress-height: 20px;--bs-progress-bar-bg: var(--bs-danger)}.user-input{z-index:1}.user-input ul.list-group{max-height:30vh}.form-control:disabled{background-color:#ededed}a.disabled{cursor:default;opacity:.5}.no-max-height{max-height:initial!important}.sq-floating-scroll{position:absolute;right:50%;bottom:75px;text-align:center}.sq-floating-scroll .btn{background-color:#fff}.sq-floating-scroll .btn:hover{background-color:#fff;opacity:.9}.ast-input-container{display:flex;align-items:center;background-color:var(--ast-input-bg, #F8F8F8);border-radius:var(--ast-size-3, .75rem)}.ast-input-container>i{padding-left:var(--ast-size-3, .75rem);color:#212529bf}.ast-input-container input{padding-left:var(--ast-size-3, .75rem);padding-right:var(--ast-size-3, .75rem)}.ast-input-container input,.ast-input-container button,.ast-input-container button:hover{background-color:transparent;border:0}.ast-input-container button:not(:hover){color:#212529bf}sq-chat-message.sq-user-message{float:var(--ast-user-message-float, none)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: 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: ChatMessageComponent, selector: "sq-chat-message", inputs: ["message", "conversation", "assistantIcon", "streaming", "canEdit", "canRegenerate", "canCopy"], outputs: ["referenceClicked", "edit", "regenerate", "openPreview"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1722
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.6", ngImport: i0, type: ChatComponent, decorators: [{
1723
+ type: Component,
1724
+ args: [{ selector: 'sq-chat-v3', providers: [
1725
+ RestChatService,
1726
+ WebSocketChatService
1727
+ ], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [CommonModule, FormsModule, ChatMessageComponent], template: "\n<ng-container *ngIf=\"!initializationError\">\n <div *ngIf=\"messages$ | async as messages; else loadingTpl || loadingTplDefault\" class=\"h-100 d-flex flex-column\">\n\n <ul class=\"list-group list-group-flush overflow-auto pb-5\" #messageList>\n <ng-container *ngFor=\"let message of messages; let index = index; let last = last\">\n <!-- Regular messages -->\n <li class=\"list-group-item\" *ngIf=\"message.additionalProperties.display\"\n [class.opacity-50]=\"messageToEdit && messageToEdit < index + 1\">\n <sq-chat-message\n [class.sq-user-message]=\"message.role !== 'assistant'\"\n [message]=\"message\"\n [conversation]=\"messages\"\n [assistantIcon]=\"customAssistantIcon || assistantIcon\"\n [streaming]=\"last && (chatService.streaming$ | async)\"\n [canEdit]=\"(loading$ | async) === false && (chatService.streaming$ | async) === false && messageToEdit === undefined && message.role !== 'assistant'\"\n [canRegenerate]=\"(loading$ | async) === false && (chatService.streaming$ | async) === false && messageToEdit === undefined && message.role === 'assistant' && last\"\n [canCopy]=\"!(last && (chatService.streaming$ | async)) && messageToEdit === undefined && message.role === 'assistant'\"\n (edit)=\"editMessage(index)\" (regenerate)=\"regenerateMessage(index)\"\n (referenceClicked)=\"referenceClicked.emit($event)\"\n (openPreview)=\"openPreview.emit($event)\">\n </sq-chat-message>\n </li>\n </ng-container>\n\n <li class=\"list-group-item\" *ngIf=\"(loading$ | async) === true\">\n <ng-container *ngTemplateOutlet=\"loadingTpl || loadingTplDefault\"></ng-container>\n </li>\n </ul>\n\n <div class=\"user-input mt-auto\" *ngIf=\"enableChat\">\n <div class=\"py-2\">\n <ng-container *ngTemplateOutlet=\"inputTpl\"></ng-container>\n <div class=\"text-end small text-muted px-3\" *ngIf=\"showCredits\">\n powered by {{modelDescription?.displayName}} <ng-container *ngIf=\"privacyUrl\"> - <a [href]=\"privacyUrl\" target=\"_blank\">privacy notice</a></ng-container>\n </div>\n </div>\n </div>\n </div>\n</ng-container>\n\n<!-- NG TEMPLATES-->\n\n<ng-template #loadingTplDefault>\n <div class=\"spinner-grow text-success d-block mx-auto my-5\" role=\"status\">\n <span class=\"visually-hidden\">Loading...</span>\n </div>\n</ng-template>\n\n<ng-template #inputTpl>\n <div class=\"px-3 py-1\">\n <div class=\"ast-input-container\">\n <i class=\"fas fa-search\"></i>\n <input #questionInput\n type=\"text\" class=\"form-control\"\n placeholder=\"Ask something\" autofocus\n [(ngModel)]=\"question\"\n (keyup)=\"onKeyUp($event)\"\n [disabled]=\"(loading$ | async) || (chatService.streaming$ | async)\">\n <button\n *ngIf=\"!(chatService.streaming$ | async) && !(loading$ | async)\"\n type=\"button\"\n class=\"btn btn-light ms-2\"\n title=\"Send message\"\n (click)=\"submitQuestion()\">\n <i class=\"fas fa-paper-plane\"></i>\n </button>\n <!--<button\n *ngIf=\"(chatService.streaming$ | async)\"\n type=\"button\"\n class=\"btn btn-light ms-2\"\n title=\"Stop generating\"\n (click)=\"terminateFetch()\">\n <i class=\"fas fa-stop\"></i>\n </button>-->\n <button\n *ngIf=\"messageToEdit\"\n type=\"button\"\n class=\"btn btn-light ms-2\"\n title=\"Cancel edition\"\n (click)=\"messageToEdit = undefined; question = ''\">\n <i class=\"fas fa-undo-alt\"></i>\n </button>\n <div class=\"sq-floating-scroll\" *ngIf=\"!isAtBottom\">\n <button class=\"btn shadow\" (click)=\"scrollDown()\">\n <i class=\"fas fa-angle-double-down\"></i>\n </button>\n </div>\n </div>\n </div>\n</ng-template>\n", styles: [".ast-primary{color:var(--ast-primary-color, #005DA7);background-color:var(--ast-primary-bg, #f2f8fe)}.ast-primary-hover{background-color:var(--ast-primary-bg, #f2f8fe)}.ast-primary-hover:hover{color:var(--ast-primary-color, #005DA7)}.ast-secondary{color:var(--ast-secondary-color, #FF732E);background-color:var(--ast-secondary-bg, #FFF8F1)}.ast-btn{border:0;text-align:left;padding-top:.5rem;padding-bottom:.5rem;display:flex;align-items:center}:host ::ng-deep .reference,:host ::ng-deep .message-content .reference,:host ::ng-deep .attachment .reference{position:relative;bottom:var(--ast-reference-bottom, .3em);font-weight:var(--ast-reference-font-weight, bold);padding:var(--ast-reference-padding, 0 .2em);margin:var(--ast-reference-margin, 0 .1em);border-radius:var(--ast-reference-border-radius, .2em);background-color:var(--ast-reference-background-color, lightblue);color:var(--ast-reference-color, black)}:host ::ng-deep .reference,:host ::ng-deep .message-content .reference{font-size:var(--ast-reference-message-font-size, .7em)}:host ::ng-deep .attachment .reference{font-size:var(--ast-reference-attachment-font-size, 13px)}:host{font-size:.875rem}:host>div>.user-input>div:not(.progress),:host>div>ul>li{width:var(--ast-chat-container-width, 100%);max-width:100%;margin-left:auto;margin-right:auto}:host>div>ul{padding-top:var(--ast-chat-padding-top, 0);padding-bottom:var(--ast-chat-padding-bottom, 0)}li.attachment>p{display:-webkit-box;-webkit-box-orient:vertical;overflow:hidden;-webkit-line-clamp:3}li.attachment.expanded>p{display:block}.progress{--bs-progress-height: 3px}.progress.disabled{--bs-progress-height: 20px;--bs-progress-bar-bg: var(--bs-danger)}.user-input{z-index:1}.user-input ul.list-group{max-height:30vh}.form-control:disabled{background-color:#ededed}a.disabled{cursor:default;opacity:.5}.no-max-height{max-height:initial!important}.sq-floating-scroll{position:absolute;right:50%;bottom:75px;text-align:center}.sq-floating-scroll .btn{background-color:#fff}.sq-floating-scroll .btn:hover{background-color:#fff;opacity:.9}.ast-input-container{display:flex;align-items:center;background-color:var(--ast-input-bg, #F8F8F8);border-radius:var(--ast-size-3, .75rem)}.ast-input-container>i{padding-left:var(--ast-size-3, .75rem);color:#212529bf}.ast-input-container input{padding-left:var(--ast-size-3, .75rem);padding-right:var(--ast-size-3, .75rem)}.ast-input-container input,.ast-input-container button,.ast-input-container button:hover{background-color:transparent;border:0}.ast-input-container button:not(:hover){color:#212529bf}sq-chat-message.sq-user-message{float:var(--ast-user-message-float, none)}\n"] }]
1728
+ }], ctorParameters: function () { return []; }, propDecorators: { instanceId: [{
1729
+ type: Input
1730
+ }], query: [{
1731
+ type: Input
1732
+ }], protocol: [{
1733
+ type: Input
1734
+ }], messageHandlers: [{
1735
+ type: Input
1736
+ }], automaticScrollToLastResponse: [{
1737
+ type: Input
1738
+ }], chat: [{
1739
+ type: Input
1740
+ }], enableChat: [{
1741
+ type: Input
1742
+ }], showCredits: [{
1743
+ type: Input
1744
+ }], customAssistantIcon: [{
1745
+ type: Input
1746
+ }], data: [{
1747
+ type: Output
1748
+ }], referenceClicked: [{
1749
+ type: Output
1750
+ }], openPreview: [{
1751
+ type: Output
1752
+ }], loading$: [{
1753
+ type: Output,
1754
+ args: ["loading"]
1755
+ }], error: [{
1756
+ type: Output
1757
+ }], _config: [{
1758
+ type: Output,
1759
+ args: ["config"]
1760
+ }], messageList: [{
1761
+ type: ViewChild,
1762
+ args: ['messageList']
1763
+ }], questionInput: [{
1764
+ type: ViewChild,
1765
+ args: ['questionInput']
1766
+ }], loadingTpl: [{
1767
+ type: ContentChild,
1768
+ args: ['loadingTpl']
1769
+ }] } });
1770
+
1771
+ class SavedChatsComponent {
1772
+ constructor() {
1773
+ this.load = new EventEmitter();
1774
+ this.delete = new EventEmitter();
1775
+ this.subscription = new Subscription();
1776
+ this.groupedSavedChats$ = new BehaviorSubject([]);
1777
+ this.loginService = inject(LoginService);
1778
+ this.instanceManagerService = inject(InstanceManagerService);
1779
+ this.auditService = inject(AuditWebService);
1780
+ this.modalService = inject(ModalService);
1781
+ this.notificationsService = inject(NotificationsService);
1782
+ }
1783
+ ngOnInit() {
1784
+ this.subscription.add(this.loginService.events.pipe(filter(e => e.type === 'login-complete'), tap(_ => this.instantiateChatService()), switchMap(_ => this.chatService.initProcess$), filter(success => !!success), tap(_ => {
1785
+ this.onListSavedChat();
1786
+ this.chatService.listSavedChat();
1787
+ })).subscribe());
1788
+ }
1789
+ ngOnDestroy() {
1790
+ this.subscription.unsubscribe();
1791
+ }
1792
+ instantiateChatService() {
1793
+ this.chatService = this.instanceManagerService.getInstance(this.instanceId);
1794
+ }
1795
+ onListSavedChat() {
1796
+ this.subscription.add(this.chatService.savedChats$.subscribe((savedChats) => this.groupedSavedChats$.next(this._groupSavedChatsByDate(savedChats))));
1797
+ }
1798
+ onLoad(savedChat) {
1799
+ this.chatService.loadSavedChat$.next(savedChat);
1800
+ this.load.emit(savedChat);
1801
+ }
1802
+ onDelete(savedChat) {
1803
+ this.modalService
1804
+ .confirm({
1805
+ title: "Delete saved discussion",
1806
+ message: `You are about to delete the discussion "${savedChat.title}". Do you want to continue?`,
1807
+ buttons: [
1808
+ new ModalButton({ result: -2 /* ModalResult.Cancel */ }),
1809
+ new ModalButton({ result: -1 /* ModalResult.OK */, text: "Confirm", primary: true })
1810
+ ],
1811
+ confirmType: 2 /* ConfirmType.Warning */
1812
+ }).then(res => {
1813
+ if (res === -1 /* ModalResult.OK */) {
1814
+ this.subscription.add(this.chatService.deleteSavedChat([savedChat.id])
1815
+ .pipe(tap(() => {
1816
+ this.notificationsService.success(`The saved discussion "${savedChat.title}" has been successfully deleted.`);
1817
+ this.delete.emit(savedChat);
1818
+ this.chatService.listSavedChat();
1819
+ }), catchError((error) => {
1820
+ console.error('Error occurred while deleting the saved chat:', error);
1821
+ this.notificationsService.error(`Error occurred while deleting the saved discussion "${savedChat.title}"`);
1822
+ return throwError(() => error);
1823
+ })).subscribe());
1824
+ }
1825
+ });
1826
+ }
1827
+ _groupSavedChatsByDate(savedChats) {
1828
+ const groupedSavedChats = new Map();
1829
+ savedChats
1830
+ .sort((a, b) => parseISO(b.modifiedUTC).getTime() - parseISO(a.modifiedUTC).getTime())
1831
+ .forEach(savedChat => {
1832
+ const groupKey = this._getTimeKey(parseISO(savedChat.modifiedUTC));
1833
+ if (!groupedSavedChats.has(groupKey)) {
1834
+ groupedSavedChats.set(groupKey, []);
1835
+ }
1836
+ groupedSavedChats.get(groupKey).push(savedChat);
1837
+ });
1838
+ return Array.from(groupedSavedChats, ([key, value]) => ({ key, value }));
1839
+ ;
1840
+ }
1841
+ _getTimeKey(date) {
1842
+ if (isToday(date)) {
1843
+ return 'Today';
1844
+ }
1845
+ else if (isYesterday(date)) {
1846
+ return 'Yesterday';
1847
+ }
1848
+ else if (isThisWeek(date)) {
1849
+ return 'This week';
1850
+ }
1851
+ else if (differenceInDays(endOfYesterday(), date) <= 7) {
1852
+ return 'Last week';
1853
+ }
1854
+ else if (isThisMonth(date)) {
1855
+ return 'This month';
1856
+ }
1857
+ else if (differenceInMonths(endOfYesterday(), date) <= 1) {
1858
+ return 'Last month';
1859
+ }
1860
+ else if (isThisQuarter(date)) {
1861
+ return 'This quarter';
1862
+ }
1863
+ else if (differenceInMonths(endOfYesterday(), date) <= 3) {
1864
+ return 'Last quarter';
1865
+ }
1866
+ else if (isThisYear(date)) {
1867
+ return 'This year';
1868
+ }
1869
+ else if (differenceInYears(endOfYesterday(), date) === 1) {
1870
+ return 'Last year';
1871
+ }
1872
+ else {
1873
+ return format(date, 'yyyy');
1874
+ }
1875
+ }
1876
+ }
1877
+ SavedChatsComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.6", ngImport: i0, type: SavedChatsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1878
+ SavedChatsComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.0.6", type: SavedChatsComponent, isStandalone: true, selector: "sq-saved-chats-v3", inputs: { instanceId: "instanceId" }, outputs: { load: "load", delete: "delete" }, ngImport: i0, template: "<div *ngFor=\"let group of (groupedSavedChats$ | async)\" class=\"saved-chats\">\n <div class=\"saved-chat-date\">{{group.key}}</div>\n <div *ngFor=\"let savedChat of group.value\" class=\"saved-chat p-2\">\n <span class=\"title me-1\" (click)=\"onLoad(savedChat)\">{{savedChat.title}}</span>\n <i class=\"saved-chat-actions fas fa-trash ms-1\" [sqTooltip]=\"'Delete'\" (click)=\"onDelete(savedChat)\"></i>\n </div>\n</div>\n", styles: [".saved-chats .saved-chat-date{font-weight:500;color:#a9a9a9;margin-top:.5rem}.saved-chats .saved-chat{display:flex;align-items:center;cursor:pointer;margin-left:.25rem}.saved-chats .saved-chat span{flex-grow:1;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.saved-chats .saved-chat .saved-chat-actions{display:none}.saved-chats .saved-chat:hover{color:#ff732e;background-color:#fff8f1}.saved-chats .saved-chat:hover .saved-chat-actions{display:block}.saved-chats .title{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }, { kind: "ngmodule", type: ModalModule }, { kind: "ngmodule", type: UtilsModule }, { kind: "directive", type: i2$1.TooltipDirective, selector: "[sqTooltip]", inputs: ["sqTooltip", "sqTooltipData", "sqTooltipTemplate", "placement", "fallbackPlacements", "delay", "hoverableTooltip", "tooltipClass"] }] });
1879
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.6", ngImport: i0, type: SavedChatsComponent, decorators: [{
1880
+ type: Component,
1881
+ args: [{ selector: 'sq-saved-chats-v3', standalone: true, imports: [CommonModule, ModalModule, UtilsModule], template: "<div *ngFor=\"let group of (groupedSavedChats$ | async)\" class=\"saved-chats\">\n <div class=\"saved-chat-date\">{{group.key}}</div>\n <div *ngFor=\"let savedChat of group.value\" class=\"saved-chat p-2\">\n <span class=\"title me-1\" (click)=\"onLoad(savedChat)\">{{savedChat.title}}</span>\n <i class=\"saved-chat-actions fas fa-trash ms-1\" [sqTooltip]=\"'Delete'\" (click)=\"onDelete(savedChat)\"></i>\n </div>\n</div>\n", styles: [".saved-chats .saved-chat-date{font-weight:500;color:#a9a9a9;margin-top:.5rem}.saved-chats .saved-chat{display:flex;align-items:center;cursor:pointer;margin-left:.25rem}.saved-chats .saved-chat span{flex-grow:1;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.saved-chats .saved-chat .saved-chat-actions{display:none}.saved-chats .saved-chat:hover{color:#ff732e;background-color:#fff8f1}.saved-chats .saved-chat:hover .saved-chat-actions{display:block}.saved-chats .title{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"] }]
1882
+ }], propDecorators: { instanceId: [{
1883
+ type: Input
1884
+ }], load: [{
1885
+ type: Output
1886
+ }], delete: [{
1887
+ type: Output
1888
+ }] } });
1889
+
1890
+ var _enAssistant = {
1891
+ "assistant": {}
1892
+ };
1893
+
1894
+ var _frAssistant = {
1895
+ "assistant": {}
1896
+ };
1897
+
1898
+ var _deAssistant = {
1899
+ "assistant": {}
1900
+ };
1901
+
1902
+ const enAssistant = Utils.merge({}, _enAssistant);
1903
+ const frAssistant = Utils.merge({}, _frAssistant);
1904
+ const deAssistant = Utils.merge({}, _deAssistant);
1905
+
1906
+ /**
1907
+ * Generated bundle index. Do not edit.
1908
+ */
1909
+
1910
+ export { ChatComponent, ChatService, ChatSettingsV3Component, FormatIconComponent, InitialsAvatarComponent, InstanceManagerService, RestChatService, SavedChatsComponent, WebSocketChatService, chatConfigSchema, deAssistant, enAssistant, frAssistant, globalSettingsSchema };
1911
+ //# sourceMappingURL=sinequa-assistant-chat.mjs.map