@acorex-platform/extension-ai-conversation-client 0.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/acorex-platform-extension-ai-conversation-client-acorex-platform-extension-ai-conversation-client-ZUV9J76R.mjs +1913 -0
- package/fesm2022/acorex-platform-extension-ai-conversation-client-acorex-platform-extension-ai-conversation-client-ZUV9J76R.mjs.map +1 -0
- package/fesm2022/acorex-platform-extension-ai-conversation-client-send-assist-chat-message.command-CMKccVFQ.mjs +96 -0
- package/fesm2022/acorex-platform-extension-ai-conversation-client-send-assist-chat-message.command-CMKccVFQ.mjs.map +1 -0
- package/fesm2022/acorex-platform-extension-ai-conversation-client-start-assist-chat.command-BGICtlcW.mjs +41 -0
- package/fesm2022/acorex-platform-extension-ai-conversation-client-start-assist-chat.command-BGICtlcW.mjs.map +1 -0
- package/fesm2022/acorex-platform-extension-ai-conversation-client.mjs +2 -0
- package/fesm2022/acorex-platform-extension-ai-conversation-client.mjs.map +1 -0
- package/package.json +24 -0
- package/types/acorex-platform-extension-ai-conversation-client.d.ts +89 -0
|
@@ -0,0 +1,1913 @@
|
|
|
1
|
+
import { AXP_SEARCH_PROVIDER, AXP_SEARCH_DEFINITION_PROVIDER } from '@acorex-platform/framework-client/common';
|
|
2
|
+
import * as i1$1 from '@acorex-platform/framework-client/core';
|
|
3
|
+
import { AXPComponentSlotModule, provideAppStartUpTask, AXP_APP_STARTUP_PRIORITY } from '@acorex-platform/framework-client/core';
|
|
4
|
+
import { AXPRuntimeTransportService, AXPCommandService, provideCommandSetups } from '@acorex-platform/framework-client/runtime';
|
|
5
|
+
import { AXM_AI_MANAGEMENT_MODULE_NAME, AXM_AI_MANAGEMENT_ASSIST_ENTITY_NAME, AXMConversationEntityStoreService, axmChatAiTargetDisplayName, axmChatAiTargetDisplayDescription, axmChatAiTargetAvatarUrl, axmFilterChatAiTargets, axmChatAiPeerParticipantId, axmBuildAiChatConversationMetadata, CONVERSATION_START_ASSIST_CHAT_COMMAND, axmChatAiTargetShortcutDescriptionKey, AXM_ASSIST_DEFAULT_RESOLVER_PORT, axmAssistStarterPromptChipItems, AXMAssistSuggestionChipsComponent, AXM_CHAT_EMPTY_STATE_COMPONENT, AXM_MINI_CHAT_INBOX_CONVERSATION_FILTER, axmConversationHasAiChat, AXMConversationFocusService, axmIsAssistRetryableStatus, axmReadAssistResponseStatus, axmReadReplyToUserMessageId, axmMessageEntityToMessage, axmAssistPersistedMessageLooksComplete, axmShouldPreferPersistedAssistOverLiveCache, axmRoomWithAssistPeerDisplayName, axmSyncAssistPeerDisplayName, axmRoomEntityToConversation, axmIsAssistTerminalStatus, CONVERSATION_SYNC_READ_STATE_COMMAND, axmAssistHasFinalizeMetadata, AXM_CHAT_SIDEBAR_ACTION_PROVIDER, AXMMiniChatHeaderSlotComponent, AXM_CONVERSATION_ASSIST_RESPONSE_PORT, AXM_CHAT_ASSIST_CATALOG_PORT, AXM_CONVERSATION_ASSIST_BACKGROUND_OPERATION_HANDLERS, CONVERSATION_SEND_ASSIST_CHAT_MESSAGE_COMMAND } from '@acorex-platform/module-conversation-client';
|
|
6
|
+
import * as i0 from '@angular/core';
|
|
7
|
+
import { inject, Injectable, output, input, signal, computed, viewChild, effect, ChangeDetectionStrategy, Component, Injector, NgModule } from '@angular/core';
|
|
8
|
+
import { AXPSystemActionType, runtimeCommandPayloadData } from '@acorex-platform/framework-shared/core';
|
|
9
|
+
import { sortBy } from 'lodash-es';
|
|
10
|
+
import { AXConversationService } from '@acorex/components/conversation';
|
|
11
|
+
import { AXToastService } from '@acorex/components/toast';
|
|
12
|
+
import * as i6 from '@acorex/core/translation';
|
|
13
|
+
import { AXTranslationService, AXTranslationModule } from '@acorex/core/translation';
|
|
14
|
+
import { AXPSessionService } from '@acorex-platform/framework-client/auth';
|
|
15
|
+
import { AXPEntityService } from '@acorex-platform/framework-client/layout/entity';
|
|
16
|
+
import { Router } from '@angular/router';
|
|
17
|
+
import { AXPLayoutThemeService } from '@acorex-platform/framework-client/themes/shared';
|
|
18
|
+
import * as i2 from '@acorex/components/button';
|
|
19
|
+
import { AXButtonModule, AXButtonComponent } from '@acorex/components/button';
|
|
20
|
+
import * as i4 from '@acorex/components/decorators';
|
|
21
|
+
import { AXDecoratorModule, AXDecoratorGenericComponent } from '@acorex/components/decorators';
|
|
22
|
+
import * as i3 from '@acorex/components/dropdown';
|
|
23
|
+
import { AXDropdownModule } from '@acorex/components/dropdown';
|
|
24
|
+
import { AXTextBoxModule } from '@acorex/components/text-box';
|
|
25
|
+
import * as i5 from '@angular/common';
|
|
26
|
+
import { CommonModule } from '@angular/common';
|
|
27
|
+
import * as i1 from '@angular/forms';
|
|
28
|
+
import { FormsModule } from '@angular/forms';
|
|
29
|
+
import { AXPAiManagerService } from '@acorex-platform/module-ai-management-client';
|
|
30
|
+
import { AXMAiManagementTransportKeys, AIMANAGEMENT_STRUCTURED_TEXT_COMPLETION_COMMAND_KEY, axpAiChatTextMessage } from '@acorex-platform/module-ai-management-client/contracts';
|
|
31
|
+
import { axmCompareMessagesChronologically, axmExtractAssistIdFromMetadata, AXM_ASSIST_CONVERSATION_TITLE_SYSTEM_PROMPT, axmSanitizeAiGeneratedConversationTitle, extractMessageTextFromPayload, axmEnsureAssistMessageSortsAfterReplyTarget } from '@acorex-platform/module-conversation-client/contracts';
|
|
32
|
+
import { AXPopupService } from '@acorex/components/popup';
|
|
33
|
+
import { AXPAvatarComponent } from '@acorex-platform/framework-client/layout/components';
|
|
34
|
+
import { AXDataSource, AXComponentCloseEvent, AXClosableComponent } from '@acorex/cdk/common';
|
|
35
|
+
import { AXListComponent } from '@acorex/components/list';
|
|
36
|
+
import { AXBasePageComponent } from '@acorex/components/page';
|
|
37
|
+
import { AXSearchBoxComponent } from '@acorex/components/search-box';
|
|
38
|
+
import { AXSkeletonComponent } from '@acorex/components/skeleton';
|
|
39
|
+
|
|
40
|
+
//#region ---- Imports ----
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region ---- Service ----
|
|
43
|
+
/**
|
|
44
|
+
* In-memory assist catalog for chat pickers and welcome UI (AI extension).
|
|
45
|
+
* Loads assist rows once via Query and dedupes GetById for starter prompts / metadata.
|
|
46
|
+
*/
|
|
47
|
+
class AXMChatAssistCatalogService {
|
|
48
|
+
constructor() {
|
|
49
|
+
//#region ---- Services & Dependencies -----
|
|
50
|
+
this.entityService = inject(AXPEntityService);
|
|
51
|
+
this.assistData = this.entityService
|
|
52
|
+
.withEntity(AXM_AI_MANAGEMENT_MODULE_NAME, AXM_AI_MANAGEMENT_ASSIST_ENTITY_NAME)
|
|
53
|
+
.data();
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region ---- State Signals -----
|
|
56
|
+
this.catalogTargets = null;
|
|
57
|
+
this.fullRowsById = new Map();
|
|
58
|
+
this.loadPromise = null;
|
|
59
|
+
this.byKeyChains = new Map();
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region ---- Catalog ----
|
|
63
|
+
async loadCatalogTargets(force = false) {
|
|
64
|
+
// Never treat an empty first paint as a permanent cache — early opens can race entity readiness.
|
|
65
|
+
if (!force && this.catalogTargets != null && this.catalogTargets.length > 0) {
|
|
66
|
+
return this.catalogTargets;
|
|
67
|
+
}
|
|
68
|
+
if (!force && this.loadPromise) {
|
|
69
|
+
return this.loadPromise;
|
|
70
|
+
}
|
|
71
|
+
this.loadPromise = this.fetchCatalogTargets();
|
|
72
|
+
try {
|
|
73
|
+
this.catalogTargets = await this.loadPromise;
|
|
74
|
+
return this.catalogTargets;
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
this.loadPromise = null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async fetchCatalogTargets() {
|
|
81
|
+
const queryResult = await this.assistData.query({
|
|
82
|
+
skip: 0,
|
|
83
|
+
take: 500,
|
|
84
|
+
});
|
|
85
|
+
const items = (queryResult?.items ?? []);
|
|
86
|
+
const targets = [];
|
|
87
|
+
for (const item of items) {
|
|
88
|
+
const id = String(item.id ?? '').trim();
|
|
89
|
+
if (!id) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
this.fullRowsById.set(id, item);
|
|
93
|
+
targets.push(this.toTargetRow(item));
|
|
94
|
+
}
|
|
95
|
+
return targets.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }));
|
|
96
|
+
}
|
|
97
|
+
toTargetRow(item) {
|
|
98
|
+
const id = String(item.id ?? '').trim();
|
|
99
|
+
return {
|
|
100
|
+
id,
|
|
101
|
+
name: String(item.name ?? ''),
|
|
102
|
+
title: (item.title ?? item.name ?? id),
|
|
103
|
+
description: item.description,
|
|
104
|
+
professionImageUrl: typeof item.professionImageUrl === 'string' ? item.professionImageUrl : undefined,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region ---- Assist reads ----
|
|
109
|
+
getCachedAssist(assistId) {
|
|
110
|
+
const scoped = assistId?.trim();
|
|
111
|
+
if (!scoped) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
return this.fullRowsById.get(scoped) ?? null;
|
|
115
|
+
}
|
|
116
|
+
async getAssist(assistId) {
|
|
117
|
+
const scoped = assistId?.trim();
|
|
118
|
+
if (!scoped) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
const cached = this.fullRowsById.get(scoped);
|
|
122
|
+
if (cached) {
|
|
123
|
+
return cached;
|
|
124
|
+
}
|
|
125
|
+
const inflight = this.byKeyChains.get(scoped);
|
|
126
|
+
if (inflight) {
|
|
127
|
+
return inflight;
|
|
128
|
+
}
|
|
129
|
+
const task = this.assistData.byKey(scoped).then((row) => {
|
|
130
|
+
if (row) {
|
|
131
|
+
this.fullRowsById.set(scoped, row);
|
|
132
|
+
}
|
|
133
|
+
return row ?? null;
|
|
134
|
+
});
|
|
135
|
+
this.byKeyChains.set(scoped, task);
|
|
136
|
+
try {
|
|
137
|
+
return await task;
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
if (this.byKeyChains.get(scoped) === task) {
|
|
141
|
+
this.byKeyChains.delete(scoped);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
primeAssist(row) {
|
|
146
|
+
const id = row.id?.trim();
|
|
147
|
+
if (!id) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
this.fullRowsById.set(id, row);
|
|
151
|
+
}
|
|
152
|
+
toSnapshot(row) {
|
|
153
|
+
return {
|
|
154
|
+
initialMessages: row.initialMessages,
|
|
155
|
+
starterPrompts: row.starterPrompts,
|
|
156
|
+
modelId: row.modelId,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMChatAssistCatalogService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
160
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMChatAssistCatalogService, providedIn: 'root' }); }
|
|
161
|
+
}
|
|
162
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMChatAssistCatalogService, decorators: [{
|
|
163
|
+
type: Injectable,
|
|
164
|
+
args: [{ providedIn: 'root' }]
|
|
165
|
+
}] });
|
|
166
|
+
|
|
167
|
+
class AXMChatAssistLauncherService {
|
|
168
|
+
constructor() {
|
|
169
|
+
this.conversationService = inject(AXConversationService);
|
|
170
|
+
this.assistCatalog = inject(AXMChatAssistCatalogService);
|
|
171
|
+
this.entityStore = inject(AXMConversationEntityStoreService);
|
|
172
|
+
this.translationService = inject(AXTranslationService);
|
|
173
|
+
this.sessionService = inject(AXPSessionService);
|
|
174
|
+
this.router = inject(Router);
|
|
175
|
+
this.toastService = inject(AXToastService);
|
|
176
|
+
}
|
|
177
|
+
async loadTargets(force = false) {
|
|
178
|
+
return this.assistCatalog.loadCatalogTargets(force);
|
|
179
|
+
}
|
|
180
|
+
displayName(row) {
|
|
181
|
+
return axmChatAiTargetDisplayName(this.#resolveMl.bind(this), row);
|
|
182
|
+
}
|
|
183
|
+
displayDescription(row) {
|
|
184
|
+
return axmChatAiTargetDisplayDescription(this.#resolveMl.bind(this), row);
|
|
185
|
+
}
|
|
186
|
+
targetAvatarUrl(row) {
|
|
187
|
+
return axmChatAiTargetAvatarUrl(row);
|
|
188
|
+
}
|
|
189
|
+
filterTargets(rows, query) {
|
|
190
|
+
return axmFilterChatAiTargets(this.#resolveMl.bind(this), rows, query);
|
|
191
|
+
}
|
|
192
|
+
async startAssistChat(options) {
|
|
193
|
+
const targetId = (options.targetId ?? options.assistId)?.trim();
|
|
194
|
+
if (!targetId) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
const row = options.targetRow ?? (await this.loadTargets()).find((r) => r.id === targetId);
|
|
198
|
+
if (!row) {
|
|
199
|
+
const message = await this.translationService.translateAsync('@conversation:chat.assist-dialog.errors.start-chat');
|
|
200
|
+
this.toastService.danger(message);
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
return this.startAssistChatFromRow(row, options.navigate !== false);
|
|
204
|
+
}
|
|
205
|
+
async startAssistChatFromRow(row, navigate = true) {
|
|
206
|
+
try {
|
|
207
|
+
const conversation = await this.#createTargetConversation(row);
|
|
208
|
+
await this.conversationService.selectConversation(conversation.id);
|
|
209
|
+
if (navigate) {
|
|
210
|
+
await this.navigateToConversation(conversation.id);
|
|
211
|
+
}
|
|
212
|
+
return { conversation };
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
console.error('Failed to start AI chat:', error);
|
|
216
|
+
const message = await this.translationService.translateAsync('@conversation:chat.assist-dialog.errors.start-chat');
|
|
217
|
+
this.toastService.danger(message);
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/** Navigate after the picker dialog has emitted close — avoids ObjectUnsubscribedError on popup teardown. */
|
|
222
|
+
async navigateToConversation(conversationId) {
|
|
223
|
+
const scoped = conversationId.trim();
|
|
224
|
+
if (!scoped) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
await this.router.navigate([this.#applicationRouteSegment(), 'chat', scoped]);
|
|
228
|
+
}
|
|
229
|
+
async #createTargetConversation(row) {
|
|
230
|
+
await this.assistCatalog.loadCatalogTargets();
|
|
231
|
+
const participantId = axmChatAiPeerParticipantId(row.id);
|
|
232
|
+
const title = this.displayName(row);
|
|
233
|
+
const avatar = this.targetAvatarUrl(row);
|
|
234
|
+
const assistRow = this.assistCatalog.getCachedAssist(row.id);
|
|
235
|
+
const assistSnapshot = assistRow ? this.assistCatalog.toSnapshot(assistRow) : undefined;
|
|
236
|
+
const conversation = await this.conversationService.createConversation([participantId], 'bot', {
|
|
237
|
+
title,
|
|
238
|
+
...(avatar ? { avatar } : {}),
|
|
239
|
+
icon: 'fa-solid fa-robot',
|
|
240
|
+
metadata: axmBuildAiChatConversationMetadata(row, assistSnapshot),
|
|
241
|
+
});
|
|
242
|
+
this.entityStore.cacheConversation(conversation);
|
|
243
|
+
this.entityStore.cacheEmptyMessageList(conversation.id);
|
|
244
|
+
return conversation;
|
|
245
|
+
}
|
|
246
|
+
#resolveMl(value) {
|
|
247
|
+
return this.translationService.resolve(value);
|
|
248
|
+
}
|
|
249
|
+
#applicationRouteSegment() {
|
|
250
|
+
return this.sessionService.application?.name ?? 'platform';
|
|
251
|
+
}
|
|
252
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMChatAssistLauncherService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
253
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMChatAssistLauncherService, providedIn: 'root' }); }
|
|
254
|
+
}
|
|
255
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMChatAssistLauncherService, decorators: [{
|
|
256
|
+
type: Injectable,
|
|
257
|
+
args: [{ providedIn: 'root' }]
|
|
258
|
+
}] });
|
|
259
|
+
|
|
260
|
+
/** Search group key used by Conversation AI extension. */
|
|
261
|
+
const AXM_CONVERSATION_ASSIST_SEARCH_GROUP = 'conversation-assist';
|
|
262
|
+
class AXMConversationAssistSearchProvider {
|
|
263
|
+
constructor() {
|
|
264
|
+
this.launcher = inject(AXMChatAssistLauncherService);
|
|
265
|
+
}
|
|
266
|
+
async search(text) {
|
|
267
|
+
let rows;
|
|
268
|
+
try {
|
|
269
|
+
rows = await this.launcher.loadTargets();
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
console.error('Failed to load AI chat targets for shortcut search:', error);
|
|
273
|
+
return [];
|
|
274
|
+
}
|
|
275
|
+
const filtered = this.launcher.filterTargets(rows, text);
|
|
276
|
+
const sorted = sortBy(filtered, (row) => this.launcher.displayName(row).toLowerCase());
|
|
277
|
+
return sorted.map((row) => ({
|
|
278
|
+
id: row.id,
|
|
279
|
+
group: AXM_CONVERSATION_ASSIST_SEARCH_GROUP,
|
|
280
|
+
title: row.title,
|
|
281
|
+
description: row.description ?? axmChatAiTargetShortcutDescriptionKey(),
|
|
282
|
+
icon: 'fa-solid fa-robot',
|
|
283
|
+
data: { id: row.id, name: row.name },
|
|
284
|
+
command: {
|
|
285
|
+
name: CONVERSATION_START_ASSIST_CHAT_COMMAND,
|
|
286
|
+
options: {
|
|
287
|
+
targetId: row.id,
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
}));
|
|
291
|
+
}
|
|
292
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMConversationAssistSearchProvider, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
293
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMConversationAssistSearchProvider }); }
|
|
294
|
+
}
|
|
295
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMConversationAssistSearchProvider, decorators: [{
|
|
296
|
+
type: Injectable
|
|
297
|
+
}] });
|
|
298
|
+
|
|
299
|
+
class AXMConversationAssistSearchDefinitionProvider {
|
|
300
|
+
async provide(context) {
|
|
301
|
+
context.addDefinition(AXM_CONVERSATION_ASSIST_SEARCH_GROUP, '@conversation:search.assist.group-title', AXM_CONVERSATION_ASSIST_SEARCH_GROUP, 'fa-solid fa-robot', 4, {
|
|
302
|
+
format: {
|
|
303
|
+
id: '{{id}}',
|
|
304
|
+
},
|
|
305
|
+
actions: [
|
|
306
|
+
{
|
|
307
|
+
name: 'start-assist-chat',
|
|
308
|
+
type: AXPSystemActionType.View,
|
|
309
|
+
priority: 'primary',
|
|
310
|
+
},
|
|
311
|
+
],
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMConversationAssistSearchDefinitionProvider, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
315
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMConversationAssistSearchDefinitionProvider }); }
|
|
316
|
+
}
|
|
317
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMConversationAssistSearchDefinitionProvider, decorators: [{
|
|
318
|
+
type: Injectable
|
|
319
|
+
}] });
|
|
320
|
+
|
|
321
|
+
class AXMChatEmptyAssistComposerComponent {
|
|
322
|
+
constructor() {
|
|
323
|
+
this.conversationService = inject(AXConversationService);
|
|
324
|
+
this.toastService = inject(AXToastService);
|
|
325
|
+
this.translationService = inject(AXTranslationService);
|
|
326
|
+
this.assistCatalog = inject(AXMChatAssistCatalogService);
|
|
327
|
+
this.layoutTheme = inject(AXPLayoutThemeService);
|
|
328
|
+
this.defaultAssistResolver = inject(AXM_ASSIST_DEFAULT_RESOLVER_PORT, { optional: true });
|
|
329
|
+
this.conversationCreated = output();
|
|
330
|
+
this.initialAssistId = input(undefined, /* @ts-ignore */
|
|
331
|
+
...(ngDevMode ? [{ debugName: "initialAssistId" }] : /* istanbul ignore next */ []));
|
|
332
|
+
this.entityId = input(undefined, /* @ts-ignore */
|
|
333
|
+
...(ngDevMode ? [{ debugName: "entityId" }] : /* istanbul ignore next */ []));
|
|
334
|
+
this.entityType = input(undefined, /* @ts-ignore */
|
|
335
|
+
...(ngDevMode ? [{ debugName: "entityType" }] : /* istanbul ignore next */ []));
|
|
336
|
+
this.sendStarterPromptsInPlace = input(false, /* @ts-ignore */
|
|
337
|
+
...(ngDevMode ? [{ debugName: "sendStarterPromptsInPlace" }] : /* istanbul ignore next */ []));
|
|
338
|
+
this.compact = input(false, /* @ts-ignore */
|
|
339
|
+
...(ngDevMode ? [{ debugName: "compact" }] : /* istanbul ignore next */ []));
|
|
340
|
+
this.rows = signal([], /* @ts-ignore */
|
|
341
|
+
...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
|
|
342
|
+
this.loading = signal(true, /* @ts-ignore */
|
|
343
|
+
...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
344
|
+
this.selectedAssistId = signal(undefined, /* @ts-ignore */
|
|
345
|
+
...(ngDevMode ? [{ debugName: "selectedAssistId" }] : /* istanbul ignore next */ []));
|
|
346
|
+
this.starterPromptItems = signal([], /* @ts-ignore */
|
|
347
|
+
...(ngDevMode ? [{ debugName: "starterPromptItems" }] : /* istanbul ignore next */ []));
|
|
348
|
+
this.draftText = signal('', /* @ts-ignore */
|
|
349
|
+
...(ngDevMode ? [{ debugName: "draftText" }] : /* istanbul ignore next */ []));
|
|
350
|
+
this.sending = signal(false, /* @ts-ignore */
|
|
351
|
+
...(ngDevMode ? [{ debugName: "sending" }] : /* istanbul ignore next */ []));
|
|
352
|
+
this.isBusy = computed(() => this.sending() || this.loading(), /* @ts-ignore */
|
|
353
|
+
...(ngDevMode ? [{ debugName: "isBusy" }] : /* istanbul ignore next */ []));
|
|
354
|
+
this.draftInput = viewChild('draftInput', /* @ts-ignore */
|
|
355
|
+
...(ngDevMode ? [{ debugName: "draftInput" }] : /* istanbul ignore next */ []));
|
|
356
|
+
this.selectedAssistLabel = computed(() => {
|
|
357
|
+
const selected = this.rows().find((row) => row.id === this.selectedAssistId());
|
|
358
|
+
return selected?.title;
|
|
359
|
+
}, /* @ts-ignore */
|
|
360
|
+
...(ngDevMode ? [{ debugName: "selectedAssistLabel" }] : /* istanbul ignore next */ []));
|
|
361
|
+
this.maxDraftHeightPx = computed(() => (this.compact() ? 128 : 224), /* @ts-ignore */
|
|
362
|
+
...(ngDevMode ? [{ debugName: "maxDraftHeightPx" }] : /* istanbul ignore next */ []));
|
|
363
|
+
this.assistsLoadPromise = null;
|
|
364
|
+
this.starterPromptsLoadGeneration = 0;
|
|
365
|
+
void this.ensureAssistsLoaded();
|
|
366
|
+
effect(() => {
|
|
367
|
+
void this.loadStarterPromptsForAssist(this.selectedAssistId());
|
|
368
|
+
});
|
|
369
|
+
effect(() => {
|
|
370
|
+
this.draftText();
|
|
371
|
+
queueMicrotask(() => this.resizeDraftInput());
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
onAssistIdChange(value) {
|
|
375
|
+
this.selectedAssistId.set(value?.trim() || undefined);
|
|
376
|
+
}
|
|
377
|
+
canSend() {
|
|
378
|
+
return Boolean(this.selectedAssistId() && this.draftText().trim().length > 0 && !this.sending() && !this.loading());
|
|
379
|
+
}
|
|
380
|
+
resizeDraftInput() {
|
|
381
|
+
const textarea = this.draftInput()?.nativeElement;
|
|
382
|
+
if (!textarea)
|
|
383
|
+
return;
|
|
384
|
+
textarea.style.height = '0px';
|
|
385
|
+
textarea.style.height = `${Math.min(textarea.scrollHeight, this.maxDraftHeightPx())}px`;
|
|
386
|
+
}
|
|
387
|
+
onDraftKeydown(event) {
|
|
388
|
+
if (event.key !== 'Enter' || event.shiftKey || event.isComposing)
|
|
389
|
+
return;
|
|
390
|
+
event.preventDefault();
|
|
391
|
+
if (this.canSend())
|
|
392
|
+
void this.send();
|
|
393
|
+
}
|
|
394
|
+
async send() {
|
|
395
|
+
const assistId = this.selectedAssistId();
|
|
396
|
+
const text = this.draftText().trim();
|
|
397
|
+
if (!assistId || !text || this.sending())
|
|
398
|
+
return;
|
|
399
|
+
const row = this.rows().find((r) => r.id === assistId);
|
|
400
|
+
if (!row)
|
|
401
|
+
return;
|
|
402
|
+
this.sending.set(true);
|
|
403
|
+
this.layoutTheme.setOverlayLoading(true);
|
|
404
|
+
try {
|
|
405
|
+
const participantId = axmChatAiPeerParticipantId(row.id);
|
|
406
|
+
const avatar = row.professionImageUrl?.trim() || undefined;
|
|
407
|
+
const entityId = this.entityId()?.trim();
|
|
408
|
+
const entityType = this.entityType()?.trim();
|
|
409
|
+
const assistRow = this.assistCatalog.getCachedAssist(row.id) ?? (await this.assistCatalog.getAssist(row.id));
|
|
410
|
+
const assistSnapshot = assistRow ? this.assistCatalog.toSnapshot(assistRow) : undefined;
|
|
411
|
+
const conversation = await this.conversationService.createConversation([participantId], 'bot', {
|
|
412
|
+
title: this.translationService.resolve(row.title) ?? row.name,
|
|
413
|
+
...(avatar ? { avatar } : {}),
|
|
414
|
+
icon: 'fa-solid fa-robot',
|
|
415
|
+
metadata: {
|
|
416
|
+
...axmBuildAiChatConversationMetadata(row, assistSnapshot),
|
|
417
|
+
...(entityId && entityType ? { entityId, entityType } : {}),
|
|
418
|
+
},
|
|
419
|
+
});
|
|
420
|
+
await this.conversationService.selectConversation(conversation.id);
|
|
421
|
+
const payload = { type: 'text', text };
|
|
422
|
+
await this.conversationService.sendMessage({ conversationId: conversation.id, type: 'text', payload });
|
|
423
|
+
this.draftText.set('');
|
|
424
|
+
this.conversationCreated.emit(conversation.id);
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
const message = await this.translationService.translateAsync('@conversation:chat.empty-assist.errors.send');
|
|
428
|
+
this.toastService.danger(message);
|
|
429
|
+
}
|
|
430
|
+
finally {
|
|
431
|
+
this.sending.set(false);
|
|
432
|
+
this.layoutTheme.setOverlayLoading(false);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
async ensureAssistsLoaded() {
|
|
436
|
+
if (!this.assistsLoadPromise)
|
|
437
|
+
this.assistsLoadPromise = this.loadAssistsFromApi();
|
|
438
|
+
await this.assistsLoadPromise;
|
|
439
|
+
}
|
|
440
|
+
async loadAssistsFromApi() {
|
|
441
|
+
this.loading.set(true);
|
|
442
|
+
try {
|
|
443
|
+
let rows = await this.assistCatalog.loadCatalogTargets();
|
|
444
|
+
if (!rows.length) {
|
|
445
|
+
rows = await this.assistCatalog.loadCatalogTargets(true);
|
|
446
|
+
}
|
|
447
|
+
this.rows.set(rows);
|
|
448
|
+
this.selectedAssistId.set(await this.resolveInitialAssistId(rows));
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
console.error('Failed to load AI chat targets (empty composer):', error);
|
|
452
|
+
const message = await this.translationService.translateAsync('@conversation:chat.empty-assist.errors.load-assists');
|
|
453
|
+
this.toastService.danger(message);
|
|
454
|
+
this.rows.set([]);
|
|
455
|
+
this.selectedAssistId.set(undefined);
|
|
456
|
+
}
|
|
457
|
+
finally {
|
|
458
|
+
this.loading.set(false);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
async resolveInitialAssistId(rows) {
|
|
462
|
+
if (!rows.length)
|
|
463
|
+
return undefined;
|
|
464
|
+
const requestedId = this.initialAssistId()?.trim();
|
|
465
|
+
if (requestedId && rows.some((row) => row.id === requestedId))
|
|
466
|
+
return requestedId;
|
|
467
|
+
const defaultAssistId = await this.resolveDefaultAssistId();
|
|
468
|
+
if (defaultAssistId && rows.some((row) => row.id === defaultAssistId))
|
|
469
|
+
return defaultAssistId;
|
|
470
|
+
return rows[0]?.id;
|
|
471
|
+
}
|
|
472
|
+
async resolveDefaultAssistId() {
|
|
473
|
+
if (!this.defaultAssistResolver)
|
|
474
|
+
return undefined;
|
|
475
|
+
try {
|
|
476
|
+
return await this.defaultAssistResolver.getDefaultAssistId();
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
return undefined;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
async loadStarterPromptsForAssist(assistId) {
|
|
483
|
+
const generation = ++this.starterPromptsLoadGeneration;
|
|
484
|
+
const id = assistId?.trim();
|
|
485
|
+
if (!id) {
|
|
486
|
+
this.starterPromptItems.set([]);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
try {
|
|
490
|
+
const assist = await this.assistCatalog.getAssist(id);
|
|
491
|
+
if (!assist || generation !== this.starterPromptsLoadGeneration) {
|
|
492
|
+
if (generation === this.starterPromptsLoadGeneration && !assist) {
|
|
493
|
+
this.starterPromptItems.set([]);
|
|
494
|
+
}
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const entityId = this.entityId()?.trim();
|
|
498
|
+
const entityType = this.entityType()?.trim();
|
|
499
|
+
const isEntityChat = Boolean(entityId && entityType);
|
|
500
|
+
const sendInPlace = this.sendStarterPromptsInPlace();
|
|
501
|
+
this.starterPromptItems.set(axmAssistStarterPromptChipItems(assist, {
|
|
502
|
+
assistId: id,
|
|
503
|
+
...(isEntityChat ? { entityId, entityType } : {}),
|
|
504
|
+
...(sendInPlace || isEntityChat ? { navigate: false } : {}),
|
|
505
|
+
}));
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
if (generation === this.starterPromptsLoadGeneration)
|
|
509
|
+
this.starterPromptItems.set([]);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMChatEmptyAssistComposerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
513
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: AXMChatEmptyAssistComposerComponent, isStandalone: true, selector: "axm-chat-empty-assist-composer", inputs: { initialAssistId: { classPropertyName: "initialAssistId", publicName: "initialAssistId", isSignal: true, isRequired: false, transformFunction: null }, entityId: { classPropertyName: "entityId", publicName: "entityId", isSignal: true, isRequired: false, transformFunction: null }, entityType: { classPropertyName: "entityType", publicName: "entityType", isSignal: true, isRequired: false, transformFunction: null }, sendStarterPromptsInPlace: { classPropertyName: "sendStarterPromptsInPlace", publicName: "sendStarterPromptsInPlace", isSignal: true, isRequired: false, transformFunction: null }, compact: { classPropertyName: "compact", publicName: "compact", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { conversationCreated: "conversationCreated" }, host: { properties: { "class.axm-chat-empty-assist-composer-host--compact": "compact()" }, classAttribute: "axm-chat-empty-assist-composer-host" }, viewQueries: [{ propertyName: "draftInput", first: true, predicate: ["draftInput"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
514
|
+
<div class="axm-chat-empty-assist-composer">
|
|
515
|
+
<div class="axm-chat-empty-assist-composer__caption">
|
|
516
|
+
{{ '@conversation:chat.empty-assist.caption' | translate | async }}
|
|
517
|
+
</div>
|
|
518
|
+
|
|
519
|
+
<div class="axm-chat-empty-assist-composer__composer-row">
|
|
520
|
+
<ax-button color="default" look="blank" class="rounded-full" [disabled]="isBusy()">
|
|
521
|
+
<ax-icon icon="fa-regular fa-sparkles"></ax-icon>
|
|
522
|
+
<span class="axm-chat-empty-assist-composer__assist-text">
|
|
523
|
+
@if (selectedAssistLabel(); as assistLabel) {
|
|
524
|
+
{{ assistLabel | translate | async }}
|
|
525
|
+
} @else {
|
|
526
|
+
{{ '@conversation:chat.empty-assist.assist-default' | translate | async }}
|
|
527
|
+
}
|
|
528
|
+
</span>
|
|
529
|
+
@if (!isBusy()) {
|
|
530
|
+
<ax-dropdown-panel>
|
|
531
|
+
<ax-button-item-list>
|
|
532
|
+
@for (row of rows(); track row.id) {
|
|
533
|
+
<ax-button-item [text]="(row.title | translate | async)!" [selected]="row.id === selectedAssistId()" (onClick)="onAssistIdChange(row.id)"></ax-button-item>
|
|
534
|
+
}
|
|
535
|
+
</ax-button-item-list>
|
|
536
|
+
</ax-dropdown-panel>
|
|
537
|
+
}
|
|
538
|
+
</ax-button>
|
|
539
|
+
|
|
540
|
+
@let assistTitle = (selectedAssistLabel() | translate | async) ?? '';
|
|
541
|
+
|
|
542
|
+
<textarea
|
|
543
|
+
#draftInput
|
|
544
|
+
rows="1"
|
|
545
|
+
class="axm-chat-empty-assist-composer__textarea border-none outline-none w-full flex-1"
|
|
546
|
+
[ngModel]="draftText()"
|
|
547
|
+
(ngModelChange)="draftText.set($event ?? '')"
|
|
548
|
+
(input)="resizeDraftInput()"
|
|
549
|
+
(keydown)="onDraftKeydown($event)"
|
|
550
|
+
[disabled]="isBusy()"
|
|
551
|
+
[placeholder]="('@conversation:chat.empty-assist.placeholder' | translate: { params: { assist: assistTitle } } | async) ?? ''"
|
|
552
|
+
></textarea>
|
|
553
|
+
<ax-button color="default" class="rounded-full" [disabled]="!canSend()" [text]="sending() ? '…' : ''" [attr.aria-label]="('@conversation:comments.actions.send.title' | translate | async) ?? ''" (onClick)="send()">
|
|
554
|
+
@if (!sending()) {
|
|
555
|
+
<ax-icon icon="fa-light fa-arrow-up"></ax-icon>
|
|
556
|
+
}
|
|
557
|
+
</ax-button>
|
|
558
|
+
</div>
|
|
559
|
+
<axm-assist-suggestion-chips [items]="starterPromptItems()" [compact]="compact()" [sendInPlace]="sendStarterPromptsInPlace()" [disabled]="isBusy()" (conversationCreated)="conversationCreated.emit($event)" />
|
|
560
|
+
</div>
|
|
561
|
+
`, isInline: true, styles: [":host{display:block;width:100%}.axm-chat-empty-assist-composer{display:flex;flex-direction:column;gap:1rem;width:min(100%,64rem);margin-inline:auto}.axm-chat-empty-assist-composer__caption{margin-bottom:3rem;text-align:center;font-size:clamp(1.9rem,3.4vw,2.7rem);line-height:1.15;font-weight:700;letter-spacing:-.02em}.axm-chat-empty-assist-composer__composer-row{display:flex;gap:.375rem;align-items:center;padding:.5rem;border-radius:2em;border:1px solid rgb(var(--ax-sys-color-border-lighter-surface))}.axm-chat-empty-assist-composer__assist-text{display:inline-block;max-width:12rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.axm-chat-empty-assist-composer__textarea{resize:none;overflow:hidden;background:transparent;min-height:2.25rem;max-height:14rem;line-height:1.5;padding-block:.25rem;font-size:inherit}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer{width:100%;max-width:none}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__caption{margin-bottom:.75rem;font-size:1.0625rem;line-height:1.35;font-weight:600;letter-spacing:-.01em}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__composer-row{gap:.25rem;padding:.375rem .5rem;border-radius:1.5rem}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__assist-text{max-width:5.5rem;font-size:.8125rem}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__textarea{min-height:1.625rem;max-height:8rem;line-height:1.4;font-size:.875rem;padding-block:.125rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: AXButtonModule }, { kind: "component", type: i2.AXButtonComponent, selector: "ax-button", inputs: ["disabled", "size", "tabIndex", "color", "look", "text", "toggleable", "selected", "iconOnly", "type", "loadingText"], outputs: ["onBlur", "onFocus", "onClick", "selectedChange", "toggleableChange", "lookChange", "colorChange", "disabledChange", "loadingTextChange"] }, { kind: "component", type: i2.AXButtonItemComponent, selector: "ax-button-item", inputs: ["color", "disabled", "text", "selected", "divided", "data", "name"], outputs: ["onClick", "onFocus", "onBlur", "disabledChange"] }, { kind: "component", type: i2.AXButtonItemListComponent, selector: "ax-button-item-list", inputs: ["items", "closeParentOnClick", "lockOnLoading"], outputs: ["onItemClick"] }, { kind: "ngmodule", type: AXDropdownModule }, { kind: "component", type: i3.AXDropdownPanelComponent, selector: "ax-dropdown-panel", inputs: ["isOpen", "fitParent", "dropdownWidth", "position", "placement", "_target", "adaptivityEnabled"], outputs: ["onOpened", "onClosed"] }, { kind: "ngmodule", type: AXTextBoxModule }, { kind: "ngmodule", type: AXDecoratorModule }, { kind: "component", type: i4.AXDecoratorIconComponent, selector: "ax-icon", inputs: ["icon"] }, { kind: "ngmodule", type: AXTranslationModule }, { kind: "component", type: AXMAssistSuggestionChipsComponent, selector: "axm-assist-suggestion-chips", inputs: ["items", "compact", "sendInPlace", "disabled"], outputs: ["conversationCreated"] }, { kind: "pipe", type: i5.AsyncPipe, name: "async" }, { kind: "pipe", type: i6.AXTranslatorPipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
562
|
+
}
|
|
563
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMChatEmptyAssistComposerComponent, decorators: [{
|
|
564
|
+
type: Component,
|
|
565
|
+
args: [{ selector: 'axm-chat-empty-assist-composer', changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
566
|
+
class: 'axm-chat-empty-assist-composer-host',
|
|
567
|
+
'[class.axm-chat-empty-assist-composer-host--compact]': 'compact()',
|
|
568
|
+
}, imports: [CommonModule, FormsModule, AXButtonModule, AXDropdownModule, AXTextBoxModule, AXDecoratorModule, AXTranslationModule, AXMAssistSuggestionChipsComponent], template: `
|
|
569
|
+
<div class="axm-chat-empty-assist-composer">
|
|
570
|
+
<div class="axm-chat-empty-assist-composer__caption">
|
|
571
|
+
{{ '@conversation:chat.empty-assist.caption' | translate | async }}
|
|
572
|
+
</div>
|
|
573
|
+
|
|
574
|
+
<div class="axm-chat-empty-assist-composer__composer-row">
|
|
575
|
+
<ax-button color="default" look="blank" class="rounded-full" [disabled]="isBusy()">
|
|
576
|
+
<ax-icon icon="fa-regular fa-sparkles"></ax-icon>
|
|
577
|
+
<span class="axm-chat-empty-assist-composer__assist-text">
|
|
578
|
+
@if (selectedAssistLabel(); as assistLabel) {
|
|
579
|
+
{{ assistLabel | translate | async }}
|
|
580
|
+
} @else {
|
|
581
|
+
{{ '@conversation:chat.empty-assist.assist-default' | translate | async }}
|
|
582
|
+
}
|
|
583
|
+
</span>
|
|
584
|
+
@if (!isBusy()) {
|
|
585
|
+
<ax-dropdown-panel>
|
|
586
|
+
<ax-button-item-list>
|
|
587
|
+
@for (row of rows(); track row.id) {
|
|
588
|
+
<ax-button-item [text]="(row.title | translate | async)!" [selected]="row.id === selectedAssistId()" (onClick)="onAssistIdChange(row.id)"></ax-button-item>
|
|
589
|
+
}
|
|
590
|
+
</ax-button-item-list>
|
|
591
|
+
</ax-dropdown-panel>
|
|
592
|
+
}
|
|
593
|
+
</ax-button>
|
|
594
|
+
|
|
595
|
+
@let assistTitle = (selectedAssistLabel() | translate | async) ?? '';
|
|
596
|
+
|
|
597
|
+
<textarea
|
|
598
|
+
#draftInput
|
|
599
|
+
rows="1"
|
|
600
|
+
class="axm-chat-empty-assist-composer__textarea border-none outline-none w-full flex-1"
|
|
601
|
+
[ngModel]="draftText()"
|
|
602
|
+
(ngModelChange)="draftText.set($event ?? '')"
|
|
603
|
+
(input)="resizeDraftInput()"
|
|
604
|
+
(keydown)="onDraftKeydown($event)"
|
|
605
|
+
[disabled]="isBusy()"
|
|
606
|
+
[placeholder]="('@conversation:chat.empty-assist.placeholder' | translate: { params: { assist: assistTitle } } | async) ?? ''"
|
|
607
|
+
></textarea>
|
|
608
|
+
<ax-button color="default" class="rounded-full" [disabled]="!canSend()" [text]="sending() ? '…' : ''" [attr.aria-label]="('@conversation:comments.actions.send.title' | translate | async) ?? ''" (onClick)="send()">
|
|
609
|
+
@if (!sending()) {
|
|
610
|
+
<ax-icon icon="fa-light fa-arrow-up"></ax-icon>
|
|
611
|
+
}
|
|
612
|
+
</ax-button>
|
|
613
|
+
</div>
|
|
614
|
+
<axm-assist-suggestion-chips [items]="starterPromptItems()" [compact]="compact()" [sendInPlace]="sendStarterPromptsInPlace()" [disabled]="isBusy()" (conversationCreated)="conversationCreated.emit($event)" />
|
|
615
|
+
</div>
|
|
616
|
+
`, styles: [":host{display:block;width:100%}.axm-chat-empty-assist-composer{display:flex;flex-direction:column;gap:1rem;width:min(100%,64rem);margin-inline:auto}.axm-chat-empty-assist-composer__caption{margin-bottom:3rem;text-align:center;font-size:clamp(1.9rem,3.4vw,2.7rem);line-height:1.15;font-weight:700;letter-spacing:-.02em}.axm-chat-empty-assist-composer__composer-row{display:flex;gap:.375rem;align-items:center;padding:.5rem;border-radius:2em;border:1px solid rgb(var(--ax-sys-color-border-lighter-surface))}.axm-chat-empty-assist-composer__assist-text{display:inline-block;max-width:12rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.axm-chat-empty-assist-composer__textarea{resize:none;overflow:hidden;background:transparent;min-height:2.25rem;max-height:14rem;line-height:1.5;padding-block:.25rem;font-size:inherit}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer{width:100%;max-width:none}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__caption{margin-bottom:.75rem;font-size:1.0625rem;line-height:1.35;font-weight:600;letter-spacing:-.01em}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__composer-row{gap:.25rem;padding:.375rem .5rem;border-radius:1.5rem}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__assist-text{max-width:5.5rem;font-size:.8125rem}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__textarea{min-height:1.625rem;max-height:8rem;line-height:1.4;font-size:.875rem;padding-block:.125rem}\n"] }]
|
|
617
|
+
}], ctorParameters: () => [], propDecorators: { conversationCreated: [{ type: i0.Output, args: ["conversationCreated"] }], initialAssistId: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialAssistId", required: false }] }], entityId: [{ type: i0.Input, args: [{ isSignal: true, alias: "entityId", required: false }] }], entityType: [{ type: i0.Input, args: [{ isSignal: true, alias: "entityType", required: false }] }], sendStarterPromptsInPlace: [{ type: i0.Input, args: [{ isSignal: true, alias: "sendStarterPromptsInPlace", required: false }] }], compact: [{ type: i0.Input, args: [{ isSignal: true, alias: "compact", required: false }] }], draftInput: [{ type: i0.ViewChild, args: ['draftInput', { isSignal: true }] }] } });
|
|
618
|
+
|
|
619
|
+
const AXE_CHAT_EMPTY_STATE_PROVIDERS = [
|
|
620
|
+
{
|
|
621
|
+
provide: AXM_CHAT_EMPTY_STATE_COMPONENT,
|
|
622
|
+
useValue: AXMChatEmptyAssistComposerComponent,
|
|
623
|
+
},
|
|
624
|
+
];
|
|
625
|
+
|
|
626
|
+
const AXE_MINI_CHAT_INBOX_FILTER_PROVIDERS = [
|
|
627
|
+
{
|
|
628
|
+
provide: AXM_MINI_CHAT_INBOX_CONVERSATION_FILTER,
|
|
629
|
+
useValue: (conversation) => axmConversationHasAiChat(conversation.metadata),
|
|
630
|
+
},
|
|
631
|
+
];
|
|
632
|
+
|
|
633
|
+
class AXEAssistDefaultResolver {
|
|
634
|
+
constructor() {
|
|
635
|
+
this.aiManager = inject(AXPAiManagerService);
|
|
636
|
+
}
|
|
637
|
+
async getDefaultAssistId() {
|
|
638
|
+
const assist = await this.aiManager.getEffectiveAssist();
|
|
639
|
+
return assist.id?.trim() || undefined;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
const AXE_ASSIST_DEFAULT_RESOLVER_PROVIDERS = [
|
|
643
|
+
AXEAssistDefaultResolver,
|
|
644
|
+
{
|
|
645
|
+
provide: AXM_ASSIST_DEFAULT_RESOLVER_PORT,
|
|
646
|
+
useExisting: AXEAssistDefaultResolver,
|
|
647
|
+
},
|
|
648
|
+
];
|
|
649
|
+
|
|
650
|
+
//#region ---- Imports ----
|
|
651
|
+
//#endregion
|
|
652
|
+
//#region ---- Helpers ----
|
|
653
|
+
function coerceTimestampMs(value) {
|
|
654
|
+
if (value instanceof Date) {
|
|
655
|
+
return value.getTime();
|
|
656
|
+
}
|
|
657
|
+
if (typeof value === 'number') {
|
|
658
|
+
return value;
|
|
659
|
+
}
|
|
660
|
+
if (typeof value === 'string') {
|
|
661
|
+
const parsed = Date.parse(value);
|
|
662
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
663
|
+
}
|
|
664
|
+
return 0;
|
|
665
|
+
}
|
|
666
|
+
//#endregion
|
|
667
|
+
//#region ---- Conversation assist response (SSE subscriber) ----
|
|
668
|
+
/**
|
|
669
|
+
* Subscribes to server-side assist SSE and forwards message patches to the in-memory realtime bus.
|
|
670
|
+
* AI generation and entity persistence run on the server ({@link AiConversationAssistResponseService}).
|
|
671
|
+
*/
|
|
672
|
+
class AXEConversationAssistResponseService {
|
|
673
|
+
constructor() {
|
|
674
|
+
this.transportService = inject(AXPRuntimeTransportService);
|
|
675
|
+
this.toastService = inject(AXToastService);
|
|
676
|
+
this.translation = inject(AXTranslationService);
|
|
677
|
+
this.store = inject(AXMConversationEntityStoreService);
|
|
678
|
+
this.focus = inject(AXMConversationFocusService);
|
|
679
|
+
this.commandService = inject(AXPCommandService);
|
|
680
|
+
this.injector = inject(Injector);
|
|
681
|
+
this.messageCache = new Map();
|
|
682
|
+
this.conversationCache = new Map();
|
|
683
|
+
this.streamTasks = new Map();
|
|
684
|
+
this.streamReconnectTimers = new Map();
|
|
685
|
+
this.lastAssistSyncAt = new Map();
|
|
686
|
+
this.currentUserId = '';
|
|
687
|
+
this.composerGenerationConversationId = null;
|
|
688
|
+
this.cancelAssistRequest = null;
|
|
689
|
+
}
|
|
690
|
+
/** Lazy: `AXConversationService` constructs the message API, which injects this port. */
|
|
691
|
+
get conversationService() {
|
|
692
|
+
return this.injector.get(AXConversationService);
|
|
693
|
+
}
|
|
694
|
+
static { this.ASSIST_SYNC_MIN_INTERVAL_MS = 2000; }
|
|
695
|
+
async handleAfterUserMessage(conversationId, _userMessageId, _command, currentUserId) {
|
|
696
|
+
this.currentUserId = currentUserId;
|
|
697
|
+
this.ensureAssistStreamSubscription(conversationId);
|
|
698
|
+
const scoped = conversationId.trim();
|
|
699
|
+
if (!this.conversationCache.has(scoped)) {
|
|
700
|
+
const storeCached = this.store.getCachedConversation(scoped);
|
|
701
|
+
if (storeCached) {
|
|
702
|
+
this.conversationCache.set(scoped, storeCached);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (this.focus.isFocused(scoped)) {
|
|
706
|
+
this.markComposerGenerating(scoped, true);
|
|
707
|
+
}
|
|
708
|
+
if (!this.streamTasks.has(scoped)) {
|
|
709
|
+
void this.requestAssistSync(conversationId, { force: true });
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
async retryAssistResponse(conversationId, messageId) {
|
|
713
|
+
await this.warmConversationCache(conversationId);
|
|
714
|
+
await this.reconcileStaleAssistPlaceholders(conversationId);
|
|
715
|
+
const cached = this.messageCache.get(messageId) ?? null;
|
|
716
|
+
const persisted = await this.store.getMessage(messageId);
|
|
717
|
+
const message = persisted ?? cached;
|
|
718
|
+
if (!message) {
|
|
719
|
+
throw new Error('Message not found');
|
|
720
|
+
}
|
|
721
|
+
if (!axmIsAssistRetryableStatus(axmReadAssistResponseStatus(message.metadata))) {
|
|
722
|
+
throw new Error('Assist response is not retryable');
|
|
723
|
+
}
|
|
724
|
+
const replyToUserMessageId = axmReadReplyToUserMessageId(message.metadata);
|
|
725
|
+
if (!persisted && !replyToUserMessageId) {
|
|
726
|
+
this.messageCache.delete(messageId);
|
|
727
|
+
throw new Error('Message not found');
|
|
728
|
+
}
|
|
729
|
+
if (!persisted) {
|
|
730
|
+
this.messageCache.delete(messageId);
|
|
731
|
+
}
|
|
732
|
+
this.ensureAssistStreamSubscription(conversationId);
|
|
733
|
+
if (this.focus.isFocused(conversationId)) {
|
|
734
|
+
this.markComposerGenerating(conversationId, true);
|
|
735
|
+
}
|
|
736
|
+
try {
|
|
737
|
+
const entity = await this.transportService.execute(AXMAiManagementTransportKeys.ConversationAssist.RetryAssist, {
|
|
738
|
+
conversationId,
|
|
739
|
+
messageId,
|
|
740
|
+
...(replyToUserMessageId ? { replyToUserMessageId } : {}),
|
|
741
|
+
});
|
|
742
|
+
const mapped = axmMessageEntityToMessage(entity);
|
|
743
|
+
this.applyLiveMessage(mapped, { isNew: false });
|
|
744
|
+
return mapped;
|
|
745
|
+
}
|
|
746
|
+
catch (error) {
|
|
747
|
+
if (this.focus.isFocused(conversationId)) {
|
|
748
|
+
this.markComposerGenerating(conversationId, false);
|
|
749
|
+
}
|
|
750
|
+
throw error;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
async reconcileStaleAssistPlaceholders(conversationId) {
|
|
754
|
+
const scoped = conversationId?.trim();
|
|
755
|
+
if (scoped) {
|
|
756
|
+
const hasStreaming = await this.hasStreamingAssistPlaceholders(scoped);
|
|
757
|
+
if (!hasStreaming) {
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
if (!this.conversationCache.has(scoped)) {
|
|
761
|
+
await this.warmConversationCache(scoped);
|
|
762
|
+
}
|
|
763
|
+
// Show any drafts buffered while this chat was unfocused before sync may rewrite status.
|
|
764
|
+
if (this.focus.isFocused(scoped)) {
|
|
765
|
+
this.flushLiveCacheToBus(scoped);
|
|
766
|
+
}
|
|
767
|
+
this.ensureAssistStreamSubscription(scoped);
|
|
768
|
+
await this.requestAssistSync(scoped, { force: true });
|
|
769
|
+
}
|
|
770
|
+
else {
|
|
771
|
+
const streaming = await this.store.loadStreamingAssistPlaceholderMessages();
|
|
772
|
+
for (const message of streaming) {
|
|
773
|
+
this.messageCache.set(message.id, message);
|
|
774
|
+
}
|
|
775
|
+
const conversationIds = new Set(streaming.map((message) => message.conversationId).filter(Boolean));
|
|
776
|
+
for (const id of conversationIds) {
|
|
777
|
+
await this.syncAssistMessagesFromStore(id);
|
|
778
|
+
await this.requestAssistSync(id);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
for (const message of this.messageCache.values()) {
|
|
782
|
+
if (scoped && message.conversationId !== scoped) {
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
const status = axmReadAssistResponseStatus(message.metadata);
|
|
786
|
+
if (status === 'incomplete' && axmAssistPersistedMessageLooksComplete(message)) {
|
|
787
|
+
await this.repairAssistResponseAsComplete(message);
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
if (status !== 'streaming') {
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
if (axmAssistPersistedMessageLooksComplete(message)) {
|
|
794
|
+
await this.repairAssistResponseAsComplete(message);
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
this.ensureAssistStreamSubscription(message.conversationId);
|
|
798
|
+
}
|
|
799
|
+
if (scoped && this.focus.isFocused(scoped)) {
|
|
800
|
+
this.flushLiveCacheToBus(scoped);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
getLiveMessage(messageId) {
|
|
804
|
+
const cached = this.messageCache.get(messageId);
|
|
805
|
+
return cached ? this.withStreamDraftForDisplay(cached) : null;
|
|
806
|
+
}
|
|
807
|
+
async hasStreamingAssistPlaceholders(conversationId) {
|
|
808
|
+
const scoped = conversationId?.trim();
|
|
809
|
+
if (!scoped) {
|
|
810
|
+
return false;
|
|
811
|
+
}
|
|
812
|
+
for (const message of this.messageCache.values()) {
|
|
813
|
+
if (message.conversationId === scoped && axmReadAssistResponseStatus(message.metadata) === 'streaming') {
|
|
814
|
+
return true;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
const cachedList = this.store.getCachedMessageList(scoped);
|
|
818
|
+
if (cachedList?.some((message) => axmReadAssistResponseStatus(message.metadata) === 'streaming')) {
|
|
819
|
+
return true;
|
|
820
|
+
}
|
|
821
|
+
const streaming = await this.store.loadStreamingAssistPlaceholderMessages(scoped);
|
|
822
|
+
return streaming.length > 0;
|
|
823
|
+
}
|
|
824
|
+
mergeLiveConversationMessages(conversationId, persisted) {
|
|
825
|
+
const byId = new Map(persisted.map((message) => [message.id, message]));
|
|
826
|
+
for (const message of this.messageCache.values()) {
|
|
827
|
+
if (message.conversationId === conversationId && !message.deleted) {
|
|
828
|
+
const existing = byId.get(message.id);
|
|
829
|
+
if (existing && axmShouldPreferPersistedAssistOverLiveCache(existing, message)) {
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
byId.set(message.id, this.withStreamDraftForDisplay(message));
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
return [...byId.values()].sort((a, b) => axmCompareMessagesChronologically(a, b));
|
|
836
|
+
}
|
|
837
|
+
ensureAssistStreamSubscription(conversationId) {
|
|
838
|
+
const key = conversationId.trim();
|
|
839
|
+
if (!key || this.streamTasks.has(key)) {
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
const task = this.runAssistStream(key).finally(() => {
|
|
843
|
+
this.streamTasks.delete(key);
|
|
844
|
+
// Reattach in the background while placeholders are still streaming (e.g. after leaving the chat).
|
|
845
|
+
this.scheduleAssistStreamReconnect(key, { allowUnfocused: true });
|
|
846
|
+
});
|
|
847
|
+
this.streamTasks.set(key, task);
|
|
848
|
+
}
|
|
849
|
+
async runAssistStream(conversationId) {
|
|
850
|
+
try {
|
|
851
|
+
await this.transportService.execute(AXMAiManagementTransportKeys.ConversationAssist.Subscribe, {
|
|
852
|
+
conversationId,
|
|
853
|
+
onEvent: async (event) => {
|
|
854
|
+
await this.handleAssistWireEvent(event);
|
|
855
|
+
},
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
catch (error) {
|
|
859
|
+
if (this.isBenignAssistStreamClose(conversationId, error)) {
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
863
|
+
await this.handleAssistStreamTransportFailure(conversationId, message);
|
|
864
|
+
}
|
|
865
|
+
finally {
|
|
866
|
+
await this.syncAssistMessagesFromStore(conversationId);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
async requestAssistSync(conversationId, options) {
|
|
870
|
+
const key = conversationId.trim();
|
|
871
|
+
if (!key) {
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
const now = Date.now();
|
|
875
|
+
const last = this.lastAssistSyncAt.get(key) ?? 0;
|
|
876
|
+
if (!options?.force && now - last < AXEConversationAssistResponseService.ASSIST_SYNC_MIN_INTERVAL_MS) {
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
this.lastAssistSyncAt.set(key, now);
|
|
880
|
+
try {
|
|
881
|
+
const body = await this.transportService.execute(AXMAiManagementTransportKeys.ConversationAssist.AssistSync, { conversationId });
|
|
882
|
+
const messages = Array.isArray(body.messages) ? body.messages : [];
|
|
883
|
+
for (const raw of messages) {
|
|
884
|
+
try {
|
|
885
|
+
const mapped = axmMessageEntityToMessage(raw);
|
|
886
|
+
this.applyLiveMessage(mapped, { isNew: !this.messageCache.has(mapped.id) });
|
|
887
|
+
}
|
|
888
|
+
catch {
|
|
889
|
+
// ignore malformed rows
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
catch {
|
|
894
|
+
// Stream catch-up is best-effort; store sync still runs.
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
async handleAssistWireEvent(event) {
|
|
898
|
+
switch (event.type) {
|
|
899
|
+
case 'assist_started': {
|
|
900
|
+
void this.markUserMessageSeenByAssist(event.conversationId, event.userMessageId);
|
|
901
|
+
const mapped = axmMessageEntityToMessage(event.message);
|
|
902
|
+
this.applyLiveMessage(mapped, { isNew: !this.messageCache.has(mapped.id) });
|
|
903
|
+
this.syncComposerGeneratingForConversation(event.conversationId, true);
|
|
904
|
+
break;
|
|
905
|
+
}
|
|
906
|
+
case 'message_patch': {
|
|
907
|
+
const mapped = axmMessageEntityToMessage(event.message);
|
|
908
|
+
const isNew = !this.messageCache.has(mapped.id);
|
|
909
|
+
this.applyLiveMessage(mapped, { isNew });
|
|
910
|
+
if (axmReadAssistResponseStatus(mapped.metadata) === 'streaming') {
|
|
911
|
+
this.syncComposerGeneratingForConversation(mapped.conversationId, true);
|
|
912
|
+
}
|
|
913
|
+
else if (axmIsAssistTerminalStatus(axmReadAssistResponseStatus(mapped.metadata))) {
|
|
914
|
+
this.syncComposerGeneratingForConversation(mapped.conversationId, false);
|
|
915
|
+
}
|
|
916
|
+
break;
|
|
917
|
+
}
|
|
918
|
+
case 'conversation_patch': {
|
|
919
|
+
const syncedRoom = axmRoomWithAssistPeerDisplayName(event.room);
|
|
920
|
+
const conversation = axmSyncAssistPeerDisplayName(axmRoomEntityToConversation(syncedRoom, this.currentUserId));
|
|
921
|
+
this.conversationCache.set(conversation.id, conversation);
|
|
922
|
+
this.store.publishConversationUpdate(conversation);
|
|
923
|
+
break;
|
|
924
|
+
}
|
|
925
|
+
case 'assist_completed': {
|
|
926
|
+
const mapped = axmMessageEntityToMessage(event.message);
|
|
927
|
+
this.applyLiveMessage(mapped, { isNew: false });
|
|
928
|
+
void this.maybeMarkFocusedIncomingRead(mapped);
|
|
929
|
+
if (event.room) {
|
|
930
|
+
this.applyAssistConversationRoomPatch(event.room);
|
|
931
|
+
}
|
|
932
|
+
void this.maybeApplyAssistAutoTitleAfterComplete(mapped);
|
|
933
|
+
this.syncComposerGeneratingForConversation(mapped.conversationId, false);
|
|
934
|
+
break;
|
|
935
|
+
}
|
|
936
|
+
case 'assist_failed': {
|
|
937
|
+
const mapped = axmMessageEntityToMessage(event.message);
|
|
938
|
+
this.applyLiveMessage(mapped, { isNew: false });
|
|
939
|
+
if (this.focus.isFocused(mapped.conversationId)) {
|
|
940
|
+
void this.showAssistToast('@conversation:chat.assist.errors.title', event.error);
|
|
941
|
+
}
|
|
942
|
+
this.syncComposerGeneratingForConversation(mapped.conversationId, false);
|
|
943
|
+
break;
|
|
944
|
+
}
|
|
945
|
+
case 'frontend_tool_call': {
|
|
946
|
+
await this.submitFrontendToolResult(event.runId, event.call);
|
|
947
|
+
break;
|
|
948
|
+
}
|
|
949
|
+
default:
|
|
950
|
+
break;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
async submitFrontendToolResult(runId, call) {
|
|
954
|
+
try {
|
|
955
|
+
await this.transportService.execute(AXMAiManagementTransportKeys.Engine.SubmitToolResult, { runId, call });
|
|
956
|
+
}
|
|
957
|
+
catch (error) {
|
|
958
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
959
|
+
void this.showAssistToast('@conversation:chat.assist.errors.tool-title', text);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
async showAssistToast(titleKey, content) {
|
|
963
|
+
const title = await this.translation.translateAsync(titleKey);
|
|
964
|
+
this.toastService.show({
|
|
965
|
+
color: titleKey.includes('stream') ? 'warning' : 'danger',
|
|
966
|
+
title,
|
|
967
|
+
content,
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
/** Server generates the title via AI; client applies `conversation_patch` / `assist_completed.room`. */
|
|
971
|
+
async applyAssistAiTitleFromUserText(conversationId, userText) {
|
|
972
|
+
const scopedId = conversationId.trim();
|
|
973
|
+
const scopedText = userText.trim();
|
|
974
|
+
if (!scopedId || !scopedText) {
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
const conversation = this.conversationCache.get(scopedId) ?? (await this.store.getConversation(scopedId));
|
|
978
|
+
if (!conversation || !axmConversationHasAiChat(conversation.metadata)) {
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
const metadata = (conversation.metadata ?? {});
|
|
982
|
+
if (metadata['isTitleCustomized'] === true || metadata['isTitleAutoGenerated'] === true) {
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
const assistId = axmExtractAssistIdFromMetadata(metadata);
|
|
986
|
+
const modelEntityId = typeof metadata['modelEntityId'] === 'string' && metadata['modelEntityId'].trim() ? metadata['modelEntityId'].trim() : undefined;
|
|
987
|
+
const result = await this.commandService.execute(AIMANAGEMENT_STRUCTURED_TEXT_COMPLETION_COMMAND_KEY, {
|
|
988
|
+
assistId,
|
|
989
|
+
modelEntityId,
|
|
990
|
+
messages: [axpAiChatTextMessage('system', AXM_ASSIST_CONVERSATION_TITLE_SYSTEM_PROMPT), axpAiChatTextMessage('user', scopedText)],
|
|
991
|
+
maxTokens: 48,
|
|
992
|
+
temperature: 0.3,
|
|
993
|
+
});
|
|
994
|
+
const data = runtimeCommandPayloadData(result);
|
|
995
|
+
const normalizedTitle = axmSanitizeAiGeneratedConversationTitle(data?.text ?? '');
|
|
996
|
+
if (!normalizedTitle || normalizedTitle === conversation.title?.trim()) {
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
const updated = axmSyncAssistPeerDisplayName({
|
|
1000
|
+
...conversation,
|
|
1001
|
+
title: normalizedTitle,
|
|
1002
|
+
metadata: {
|
|
1003
|
+
...metadata,
|
|
1004
|
+
isTitleCustomized: false,
|
|
1005
|
+
isTitleAutoGenerated: true,
|
|
1006
|
+
},
|
|
1007
|
+
});
|
|
1008
|
+
this.conversationCache.set(updated.id, updated);
|
|
1009
|
+
this.store.publishConversationUpdate(updated);
|
|
1010
|
+
}
|
|
1011
|
+
/** After assist completes — memory update first; entity persist is async and must not block streaming UI. */
|
|
1012
|
+
applyAssistConversationRoomPatch(room) {
|
|
1013
|
+
const syncedRoom = axmRoomWithAssistPeerDisplayName(room);
|
|
1014
|
+
const conversation = axmSyncAssistPeerDisplayName(axmRoomEntityToConversation(syncedRoom, this.currentUserId));
|
|
1015
|
+
this.conversationCache.set(conversation.id, conversation);
|
|
1016
|
+
this.store.publishConversationUpdate(conversation);
|
|
1017
|
+
void this.store.persistAssistTitlePatch(syncedRoom);
|
|
1018
|
+
}
|
|
1019
|
+
/** Client fallback when server room patch is missing or peer display name is still the assist catalog label. */
|
|
1020
|
+
async maybeApplyAssistAutoTitleAfterComplete(assistMessage) {
|
|
1021
|
+
const conversationId = assistMessage.conversationId?.trim();
|
|
1022
|
+
if (!conversationId) {
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
const conversation = this.conversationCache.get(conversationId) ?? (await this.store.getConversation(conversationId));
|
|
1026
|
+
if (!conversation || !axmConversationHasAiChat(conversation.metadata)) {
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
const metadata = (conversation.metadata ?? {});
|
|
1030
|
+
if (metadata['isTitleCustomized'] === true || metadata['isTitleAutoGenerated'] === true) {
|
|
1031
|
+
const peerSynced = axmSyncAssistPeerDisplayName(conversation);
|
|
1032
|
+
if (peerSynced !== conversation) {
|
|
1033
|
+
this.conversationCache.set(peerSynced.id, peerSynced);
|
|
1034
|
+
this.store.publishConversationUpdate(peerSynced);
|
|
1035
|
+
}
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
let userText = '';
|
|
1039
|
+
const replyToId = axmReadReplyToUserMessageId(assistMessage.metadata);
|
|
1040
|
+
if (replyToId) {
|
|
1041
|
+
const userMessage = this.messageCache.get(replyToId) ?? (await this.store.getMessage(replyToId));
|
|
1042
|
+
if (userMessage?.type === 'text') {
|
|
1043
|
+
userText = extractMessageTextFromPayload(userMessage.payload).trim();
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
if (!userText) {
|
|
1047
|
+
const cachedMessages = this.store.getCachedMessageList(conversationId);
|
|
1048
|
+
const firstUserText = cachedMessages?.find((message) => !message.deleted && message.senderId === this.currentUserId && message.type === 'text' && extractMessageTextFromPayload(message.payload).trim().length > 0);
|
|
1049
|
+
userText = firstUserText ? extractMessageTextFromPayload(firstUserText.payload).trim() : '';
|
|
1050
|
+
}
|
|
1051
|
+
if (!userText) {
|
|
1052
|
+
const loadedMessages = await this.store.loadConversationMessages(conversationId);
|
|
1053
|
+
const firstUserText = loadedMessages.find((message) => !message.deleted && message.senderId === this.currentUserId && message.type === 'text' && extractMessageTextFromPayload(message.payload).trim().length > 0);
|
|
1054
|
+
userText = firstUserText ? extractMessageTextFromPayload(firstUserText.payload).trim() : '';
|
|
1055
|
+
}
|
|
1056
|
+
if (!userText) {
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
void this.applyAssistAiTitleFromUserText(conversationId, userText);
|
|
1060
|
+
}
|
|
1061
|
+
async markUserMessageSeenByAssist(conversationId, userMessageId) {
|
|
1062
|
+
const scopedId = userMessageId?.trim();
|
|
1063
|
+
if (!scopedId) {
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
const message = this.messageCache.get(scopedId) ?? (await this.store.getMessage(scopedId));
|
|
1067
|
+
if (!message || message.conversationId !== conversationId) {
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
if (message.senderId !== this.currentUserId || message.status === 'read') {
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
await this.store.updateMessage({ ...message, status: 'read' });
|
|
1074
|
+
}
|
|
1075
|
+
async maybeMarkFocusedIncomingRead(message) {
|
|
1076
|
+
const conversationId = message.conversationId?.trim();
|
|
1077
|
+
if (!conversationId || !this.focus.isFocused(conversationId)) {
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
if (message.senderId === this.currentUserId || message.status === 'read') {
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
await this.commandService.execute(CONVERSATION_SYNC_READ_STATE_COMMAND, {
|
|
1084
|
+
conversationId,
|
|
1085
|
+
messageIds: [message.id],
|
|
1086
|
+
currentUserId: this.currentUserId,
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
applyLiveMessage(message, options) {
|
|
1090
|
+
const normalized = this.coerceLaggyStreamingTerminal(message);
|
|
1091
|
+
if (this.shouldIgnoreStaleStreamingDowngrade(normalized)) {
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
if (this.shouldIgnoreStaleIncompleteWhileLiveStreaming(normalized)) {
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
if (axmReadAssistResponseStatus(normalized.metadata) === 'streaming' && axmAssistPersistedMessageLooksComplete(normalized)) {
|
|
1098
|
+
void this.repairAssistResponseAsComplete(normalized);
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
// Keep canonical streaming shape in cache (draft stays in metadata); mirror draft only for UI bus.
|
|
1102
|
+
const canonical = this.ensureAssistChronology(normalized);
|
|
1103
|
+
this.messageCache.set(canonical.id, canonical);
|
|
1104
|
+
if (!this.focus.isFocused(canonical.conversationId)) {
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
const forDisplay = this.withStreamDraftForDisplay(canonical);
|
|
1108
|
+
if (options.isNew) {
|
|
1109
|
+
this.store.publishNewMessage(forDisplay);
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
this.store.publishMessageUpdate(forDisplay);
|
|
1113
|
+
}
|
|
1114
|
+
/** Keeps assist bubbles below the linked user turn when client/server clocks disagree. */
|
|
1115
|
+
ensureAssistChronology(message) {
|
|
1116
|
+
const replyToId = axmReadReplyToUserMessageId(message.metadata);
|
|
1117
|
+
if (!replyToId) {
|
|
1118
|
+
return message;
|
|
1119
|
+
}
|
|
1120
|
+
const replyTarget = this.messageCache.get(replyToId);
|
|
1121
|
+
if (!replyTarget) {
|
|
1122
|
+
return message;
|
|
1123
|
+
}
|
|
1124
|
+
return axmEnsureAssistMessageSortsAfterReplyTarget(message, replyTarget.timestamp);
|
|
1125
|
+
}
|
|
1126
|
+
/** Status can lag behind finalize metadata or transcript payload on late patches. */
|
|
1127
|
+
coerceLaggyStreamingTerminal(message) {
|
|
1128
|
+
const status = axmReadAssistResponseStatus(message.metadata);
|
|
1129
|
+
if (status !== 'streaming') {
|
|
1130
|
+
return message;
|
|
1131
|
+
}
|
|
1132
|
+
const meta = message.metadata;
|
|
1133
|
+
const transcripts = meta?.['transcripts'];
|
|
1134
|
+
const hasTranscripts = Array.isArray(transcripts) && transcripts.length > 0;
|
|
1135
|
+
if (!axmAssistHasFinalizeMetadata(meta) && !hasTranscripts) {
|
|
1136
|
+
return message;
|
|
1137
|
+
}
|
|
1138
|
+
const nextMeta = { ...(meta ?? {}) };
|
|
1139
|
+
nextMeta['isStreaming'] = false;
|
|
1140
|
+
nextMeta['assistResponseStatus'] = 'complete';
|
|
1141
|
+
delete nextMeta['assistResponseError'];
|
|
1142
|
+
delete nextMeta['streamDraftText'];
|
|
1143
|
+
delete nextMeta['streamDraftThink'];
|
|
1144
|
+
return { ...message, metadata: nextMeta };
|
|
1145
|
+
}
|
|
1146
|
+
/** Ignore out-of-order streaming patches after a terminal assist state was applied. */
|
|
1147
|
+
shouldIgnoreStaleStreamingDowngrade(incoming) {
|
|
1148
|
+
if (axmReadAssistResponseStatus(incoming.metadata) !== 'streaming') {
|
|
1149
|
+
return false;
|
|
1150
|
+
}
|
|
1151
|
+
if (axmAssistHasFinalizeMetadata(incoming.metadata)) {
|
|
1152
|
+
return false;
|
|
1153
|
+
}
|
|
1154
|
+
const cached = this.messageCache.get(incoming.id);
|
|
1155
|
+
if (!cached) {
|
|
1156
|
+
return false;
|
|
1157
|
+
}
|
|
1158
|
+
const cachedStatus = axmReadAssistResponseStatus(cached.metadata);
|
|
1159
|
+
if (axmIsAssistTerminalStatus(cachedStatus)) {
|
|
1160
|
+
return true;
|
|
1161
|
+
}
|
|
1162
|
+
return axmAssistHasFinalizeMetadata(cached.metadata);
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Assist-sync may reclaim a row as incomplete while the live SSE subscription is still
|
|
1166
|
+
* receiving drafts (page refresh / server restart path). Never ignore cancel/supersede.
|
|
1167
|
+
*/
|
|
1168
|
+
shouldIgnoreStaleIncompleteWhileLiveStreaming(incoming) {
|
|
1169
|
+
const incomingStatus = axmReadAssistResponseStatus(incoming.metadata);
|
|
1170
|
+
if (incomingStatus !== 'incomplete' && incomingStatus !== 'failed') {
|
|
1171
|
+
return false;
|
|
1172
|
+
}
|
|
1173
|
+
const meta = incoming.metadata;
|
|
1174
|
+
const err = typeof meta?.['assistResponseError'] === 'string' ? meta['assistResponseError'] : '';
|
|
1175
|
+
// Real cancel / provider failures must always win over the live draft cache.
|
|
1176
|
+
if (!err.includes('page refresh or server restart')) {
|
|
1177
|
+
return false;
|
|
1178
|
+
}
|
|
1179
|
+
if (!this.streamTasks.has(incoming.conversationId.trim())) {
|
|
1180
|
+
return false;
|
|
1181
|
+
}
|
|
1182
|
+
const cached = this.messageCache.get(incoming.id);
|
|
1183
|
+
if (!cached || axmReadAssistResponseStatus(cached.metadata) !== 'streaming') {
|
|
1184
|
+
return false;
|
|
1185
|
+
}
|
|
1186
|
+
const cachedMeta = cached.metadata;
|
|
1187
|
+
const incomingMeta = incoming.metadata;
|
|
1188
|
+
const cachedDraft = typeof cachedMeta?.['streamDraftText'] === 'string' ? cachedMeta['streamDraftText'] : '';
|
|
1189
|
+
const incomingDraft = typeof incomingMeta?.['streamDraftText'] === 'string' ? incomingMeta['streamDraftText'] : '';
|
|
1190
|
+
const incomingText = incoming.payload?.type === 'text' ? String(incoming.payload.text ?? '') : '';
|
|
1191
|
+
return cachedDraft.length >= Math.max(incomingDraft.length, incomingText.trim().length);
|
|
1192
|
+
}
|
|
1193
|
+
/** Maps streaming draft metadata into a display-friendly message for the chat renderer. */
|
|
1194
|
+
withStreamDraftForDisplay(message) {
|
|
1195
|
+
const meta = message.metadata;
|
|
1196
|
+
if (axmReadAssistResponseStatus(meta) !== 'streaming') {
|
|
1197
|
+
return message;
|
|
1198
|
+
}
|
|
1199
|
+
const streamDraftText = typeof meta?.['streamDraftText'] === 'string' ? meta['streamDraftText'] : '';
|
|
1200
|
+
if (!streamDraftText.trim()) {
|
|
1201
|
+
return message;
|
|
1202
|
+
}
|
|
1203
|
+
const payload = message.payload;
|
|
1204
|
+
const hasVisibleText = payload?.['type'] === 'text' && typeof payload['text'] === 'string' && payload['text'].trim().length > 0;
|
|
1205
|
+
if (hasVisibleText) {
|
|
1206
|
+
return message;
|
|
1207
|
+
}
|
|
1208
|
+
return {
|
|
1209
|
+
...message,
|
|
1210
|
+
payload: { type: 'text', text: streamDraftText },
|
|
1211
|
+
metadata: { ...meta },
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
async repairAssistResponseAsComplete(message) {
|
|
1215
|
+
const meta = { ...(message.metadata ?? {}) };
|
|
1216
|
+
const streamDraftText = typeof meta['streamDraftText'] === 'string' ? meta['streamDraftText'] : '';
|
|
1217
|
+
const payload = message.payload;
|
|
1218
|
+
const hasPayloadContent = (payload?.['type'] === 'text' && typeof payload['text'] === 'string' && payload['text'].trim().length > 0) || (payload?.['type'] === 'assist-transcript-line' && payload['line'] != null);
|
|
1219
|
+
const next = { ...message };
|
|
1220
|
+
if (!hasPayloadContent && streamDraftText.trim().length > 0) {
|
|
1221
|
+
next.payload = { type: 'text', text: streamDraftText.trim() };
|
|
1222
|
+
}
|
|
1223
|
+
const nextMeta = { ...(next.metadata ?? {}) };
|
|
1224
|
+
nextMeta['isStreaming'] = false;
|
|
1225
|
+
nextMeta['assistResponseStatus'] = 'complete';
|
|
1226
|
+
delete nextMeta['assistResponseError'];
|
|
1227
|
+
delete nextMeta['streamDraftText'];
|
|
1228
|
+
delete nextMeta['streamDraftThink'];
|
|
1229
|
+
next.metadata = nextMeta;
|
|
1230
|
+
this.applyLiveMessage(next, { isNew: false });
|
|
1231
|
+
}
|
|
1232
|
+
async warmConversationCache(conversationId) {
|
|
1233
|
+
if (this.conversationCache.has(conversationId)) {
|
|
1234
|
+
await this.syncAssistMessagesFromStore(conversationId);
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
const conversation = await this.store.getConversation(conversationId);
|
|
1238
|
+
if (conversation) {
|
|
1239
|
+
this.conversationCache.set(conversationId, conversation);
|
|
1240
|
+
if (axmConversationHasAiChat(conversation.metadata)) {
|
|
1241
|
+
this.ensureAssistStreamSubscription(conversationId);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
await this.syncAssistMessagesFromStore(conversationId);
|
|
1245
|
+
}
|
|
1246
|
+
async syncAssistMessagesFromStore(conversationId) {
|
|
1247
|
+
const scoped = conversationId.trim();
|
|
1248
|
+
if (!scoped) {
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
const hasLiveStreaming = [...this.messageCache.values()].some((message) => message.conversationId === scoped && axmReadAssistResponseStatus(message.metadata) === 'streaming');
|
|
1252
|
+
const cachedList = this.store.getCachedMessageList(scoped);
|
|
1253
|
+
const hasCachedStreaming = cachedList?.some((message) => axmReadAssistResponseStatus(message.metadata) === 'streaming') ?? false;
|
|
1254
|
+
const persisted = cachedList && !hasLiveStreaming && !hasCachedStreaming ? cachedList : await this.store.loadConversationMessages(scoped);
|
|
1255
|
+
const persistedIds = new Set(persisted.map((message) => message.id));
|
|
1256
|
+
for (const [id, message] of this.messageCache.entries()) {
|
|
1257
|
+
if (message.conversationId !== scoped || persistedIds.has(id)) {
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1260
|
+
const status = axmReadAssistResponseStatus(message.metadata);
|
|
1261
|
+
if (axmIsAssistTerminalStatus(status)) {
|
|
1262
|
+
this.messageCache.delete(id);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
for (const message of persisted) {
|
|
1266
|
+
const meta = message.metadata;
|
|
1267
|
+
if (meta?.['isAssistResponse'] !== true && message.type !== 'assist-response') {
|
|
1268
|
+
continue;
|
|
1269
|
+
}
|
|
1270
|
+
const cached = this.messageCache.get(message.id);
|
|
1271
|
+
if (cached && axmShouldPreferPersistedAssistOverLiveCache(message, cached)) {
|
|
1272
|
+
this.messageCache.delete(message.id);
|
|
1273
|
+
}
|
|
1274
|
+
const status = axmReadAssistResponseStatus(message.metadata);
|
|
1275
|
+
if (status === 'streaming') {
|
|
1276
|
+
if (axmAssistPersistedMessageLooksComplete(message)) {
|
|
1277
|
+
await this.repairAssistResponseAsComplete(message);
|
|
1278
|
+
}
|
|
1279
|
+
else {
|
|
1280
|
+
this.applyLiveMessage(message, { isNew: !this.messageCache.has(message.id) });
|
|
1281
|
+
}
|
|
1282
|
+
continue;
|
|
1283
|
+
}
|
|
1284
|
+
if (status === 'failed' || status === 'complete' || status === 'incomplete') {
|
|
1285
|
+
this.applyLiveMessage(message, { isNew: false });
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
scheduleAssistStreamReconnect(conversationId, options) {
|
|
1290
|
+
const key = conversationId.trim();
|
|
1291
|
+
if (!key) {
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
if (!options?.allowUnfocused && !this.focus.isFocused(key)) {
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
const hasStreaming = [...this.messageCache.values()].some((message) => message.conversationId === key && axmReadAssistResponseStatus(message.metadata) === 'streaming');
|
|
1298
|
+
if (!hasStreaming) {
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
const existing = this.streamReconnectTimers.get(key);
|
|
1302
|
+
if (existing) {
|
|
1303
|
+
clearTimeout(existing);
|
|
1304
|
+
}
|
|
1305
|
+
const timer = setTimeout(() => {
|
|
1306
|
+
this.streamReconnectTimers.delete(key);
|
|
1307
|
+
if (this.streamTasks.has(key)) {
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
const stillStreaming = [...this.messageCache.values()].some((message) => message.conversationId === key && axmReadAssistResponseStatus(message.metadata) === 'streaming');
|
|
1311
|
+
if (!stillStreaming) {
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
this.ensureAssistStreamSubscription(key);
|
|
1315
|
+
}, 2000);
|
|
1316
|
+
this.streamReconnectTimers.set(key, timer);
|
|
1317
|
+
}
|
|
1318
|
+
isBenignAssistStreamClose(conversationId, error) {
|
|
1319
|
+
if (!this.focus.isFocused(conversationId)) {
|
|
1320
|
+
return true;
|
|
1321
|
+
}
|
|
1322
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1323
|
+
if (message === 'BodyStreamBuffer was aborted') {
|
|
1324
|
+
return true;
|
|
1325
|
+
}
|
|
1326
|
+
return error instanceof DOMException && error.name === 'AbortError';
|
|
1327
|
+
}
|
|
1328
|
+
flushLiveCacheToBus(conversationId) {
|
|
1329
|
+
if (!this.focus.isFocused(conversationId)) {
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
for (const message of this.messageCache.values()) {
|
|
1333
|
+
if (message.conversationId !== conversationId || message.deleted) {
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
1336
|
+
const next = this.withStreamDraftForDisplay(message);
|
|
1337
|
+
this.store.publishMessageUpdate(next);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
async handleAssistStreamTransportFailure(conversationId, errorMessage) {
|
|
1341
|
+
if (!this.focus.isFocused(conversationId)) {
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
await this.syncAssistMessagesFromStore(conversationId);
|
|
1345
|
+
await this.requestAssistSync(conversationId);
|
|
1346
|
+
const stillStreaming = [...this.messageCache.values()].filter((message) => message.conversationId === conversationId && axmReadAssistResponseStatus(message.metadata) === 'streaming');
|
|
1347
|
+
for (const message of stillStreaming) {
|
|
1348
|
+
const meta = { ...(message.metadata ?? {}) };
|
|
1349
|
+
meta['isStreaming'] = false;
|
|
1350
|
+
meta['assistResponseStatus'] = 'incomplete';
|
|
1351
|
+
meta['assistResponseError'] = errorMessage;
|
|
1352
|
+
this.applyLiveMessage({ ...message, metadata: meta }, { isNew: false });
|
|
1353
|
+
}
|
|
1354
|
+
if (stillStreaming.length > 0) {
|
|
1355
|
+
void this.showAssistToast('@conversation:chat.assist.errors.stream-title', errorMessage);
|
|
1356
|
+
}
|
|
1357
|
+
this.syncComposerGeneratingForConversation(conversationId, false);
|
|
1358
|
+
}
|
|
1359
|
+
//#region ----- Composer generation state -----
|
|
1360
|
+
/** Syncs composer Stop / spinner with assist streaming for the focused conversation. */
|
|
1361
|
+
async syncComposerGenerationForFocus(conversationId) {
|
|
1362
|
+
const scoped = conversationId?.trim();
|
|
1363
|
+
if (!scoped) {
|
|
1364
|
+
this.markComposerGenerating(null, false);
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
const hasStreaming = await this.hasStreamingAssistPlaceholders(scoped);
|
|
1368
|
+
this.markComposerGenerating(scoped, hasStreaming);
|
|
1369
|
+
}
|
|
1370
|
+
async cancelAssistGeneration(conversationId) {
|
|
1371
|
+
if (this.cancelAssistRequest) {
|
|
1372
|
+
return this.cancelAssistRequest;
|
|
1373
|
+
}
|
|
1374
|
+
this.cancelAssistRequest = this.runCancelAssistGeneration(conversationId).finally(() => {
|
|
1375
|
+
this.cancelAssistRequest = null;
|
|
1376
|
+
});
|
|
1377
|
+
return this.cancelAssistRequest;
|
|
1378
|
+
}
|
|
1379
|
+
async runCancelAssistGeneration(conversationId) {
|
|
1380
|
+
const scoped = conversationId.trim()
|
|
1381
|
+
|| this.composerGenerationConversationId?.trim()
|
|
1382
|
+
|| this.resolveActiveStreamingConversationId();
|
|
1383
|
+
if (!scoped) {
|
|
1384
|
+
this.conversationService.cancelGeneration();
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
try {
|
|
1388
|
+
await this.transportService.execute(AXMAiManagementTransportKeys.ConversationAssist.CancelAssist, {
|
|
1389
|
+
conversationId: scoped,
|
|
1390
|
+
});
|
|
1391
|
+
await this.requestAssistSync(scoped, { force: true });
|
|
1392
|
+
await this.syncAssistMessagesFromStore(scoped);
|
|
1393
|
+
}
|
|
1394
|
+
catch (error) {
|
|
1395
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
1396
|
+
if (this.focus.isFocused(scoped)) {
|
|
1397
|
+
void this.showAssistToast('@conversation:chat.assist.errors.stream-title', text);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
finally {
|
|
1401
|
+
this.markComposerGenerating(scoped, false);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
markComposerGenerating(conversationId, generating) {
|
|
1405
|
+
if (generating && conversationId) {
|
|
1406
|
+
this.composerGenerationConversationId = conversationId;
|
|
1407
|
+
if (this.focus.isFocused(conversationId)) {
|
|
1408
|
+
this.conversationService.setGenerating(true);
|
|
1409
|
+
}
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
if (!generating) {
|
|
1413
|
+
this.composerGenerationConversationId = null;
|
|
1414
|
+
if (!conversationId || this.focus.isFocused(conversationId)) {
|
|
1415
|
+
this.conversationService.setGenerating(false);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
syncComposerGeneratingForConversation(conversationId, generating) {
|
|
1420
|
+
if (!this.focus.isFocused(conversationId)) {
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
this.markComposerGenerating(conversationId, generating);
|
|
1424
|
+
}
|
|
1425
|
+
resolveActiveStreamingConversationId() {
|
|
1426
|
+
for (const message of this.messageCache.values()) {
|
|
1427
|
+
if (axmReadAssistResponseStatus(message.metadata) === 'streaming') {
|
|
1428
|
+
return message.conversationId;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
return this.composerGenerationConversationId;
|
|
1432
|
+
}
|
|
1433
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEConversationAssistResponseService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1434
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEConversationAssistResponseService }); }
|
|
1435
|
+
}
|
|
1436
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEConversationAssistResponseService, decorators: [{
|
|
1437
|
+
type: Injectable
|
|
1438
|
+
}] });
|
|
1439
|
+
/** @deprecated Use {@link AXEConversationAssistResponseService}. */
|
|
1440
|
+
const AXCConversationAssistResponseService = AXEConversationAssistResponseService;
|
|
1441
|
+
|
|
1442
|
+
class AXMChatWithAssistDialogComponent extends AXBasePageComponent {
|
|
1443
|
+
constructor() {
|
|
1444
|
+
super(...arguments);
|
|
1445
|
+
this.toastService = inject(AXToastService);
|
|
1446
|
+
this.translationService = inject(AXTranslationService);
|
|
1447
|
+
this.assistLauncher = inject(AXMChatAssistLauncherService);
|
|
1448
|
+
this.layoutTheme = inject(AXPLayoutThemeService);
|
|
1449
|
+
this.__popup__ = input(undefined, /* @ts-ignore */
|
|
1450
|
+
...(ngDevMode ? [{ debugName: "__popup__" }] : /* istanbul ignore next */ []));
|
|
1451
|
+
this.embeddedInTabs = input(false, /* @ts-ignore */
|
|
1452
|
+
...(ngDevMode ? [{ debugName: "embeddedInTabs" }] : /* istanbul ignore next */ []));
|
|
1453
|
+
this.rows = signal([], /* @ts-ignore */
|
|
1454
|
+
...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
|
|
1455
|
+
this.loading = signal(true, /* @ts-ignore */
|
|
1456
|
+
...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
1457
|
+
this.starting = signal(false, /* @ts-ignore */
|
|
1458
|
+
...(ngDevMode ? [{ debugName: "starting" }] : /* istanbul ignore next */ []));
|
|
1459
|
+
this.searchQuery = signal('', /* @ts-ignore */
|
|
1460
|
+
...(ngDevMode ? [{ debugName: "searchQuery" }] : /* istanbul ignore next */ []));
|
|
1461
|
+
this.selectedTargetIds = signal([], /* @ts-ignore */
|
|
1462
|
+
...(ngDevMode ? [{ debugName: "selectedTargetIds" }] : /* istanbul ignore next */ []));
|
|
1463
|
+
this.targetsLoadPromise = null;
|
|
1464
|
+
this.assistListDataSource = new AXDataSource({
|
|
1465
|
+
pageSize: 100,
|
|
1466
|
+
key: 'id',
|
|
1467
|
+
load: async (e) => {
|
|
1468
|
+
await this.ensureTargetsLoaded();
|
|
1469
|
+
const filtered = this.filteredTargetRows();
|
|
1470
|
+
return { items: filtered.slice(e.skip, e.skip + e.take), total: filtered.length };
|
|
1471
|
+
},
|
|
1472
|
+
byKey: (key) => Promise.resolve(this.rows().find((r) => r.id === key) ?? null),
|
|
1473
|
+
});
|
|
1474
|
+
this.assistsListEmptyTpl = viewChild.required('assistsListEmpty', /* @ts-ignore */
|
|
1475
|
+
...(ngDevMode ? [{ debugName: "assistsListEmptyTpl" }] : /* istanbul ignore next */ []));
|
|
1476
|
+
this.assistsListLoadingTpl = viewChild.required('assistsListLoading', /* @ts-ignore */
|
|
1477
|
+
...(ngDevMode ? [{ debugName: "assistsListLoadingTpl" }] : /* istanbul ignore next */ []));
|
|
1478
|
+
this.assistsList = viewChild('assistsList', /* @ts-ignore */
|
|
1479
|
+
...(ngDevMode ? [{ debugName: "assistsList" }] : /* istanbul ignore next */ []));
|
|
1480
|
+
this.syncPopupTitle = effect(() => {
|
|
1481
|
+
if (this.embeddedInTabs())
|
|
1482
|
+
return;
|
|
1483
|
+
const popupRef = this.__popup__();
|
|
1484
|
+
if (popupRef) {
|
|
1485
|
+
this.setTitle = (t) => popupRef.setTitle(t);
|
|
1486
|
+
void this.#updatePopupTitle(popupRef);
|
|
1487
|
+
}
|
|
1488
|
+
}, /* @ts-ignore */
|
|
1489
|
+
...(ngDevMode ? [{ debugName: "syncPopupTitle" }] : /* istanbul ignore next */ []));
|
|
1490
|
+
}
|
|
1491
|
+
targetFromItem(item) {
|
|
1492
|
+
if (!item || typeof item !== 'object')
|
|
1493
|
+
return null;
|
|
1494
|
+
const o = item;
|
|
1495
|
+
if ('data' in o && o['data'] && typeof o['data'] === 'object')
|
|
1496
|
+
return o['data'];
|
|
1497
|
+
if (typeof o['id'] === 'string' && typeof o['name'] === 'string')
|
|
1498
|
+
return o;
|
|
1499
|
+
return null;
|
|
1500
|
+
}
|
|
1501
|
+
onSelectedTargetsChange(value) {
|
|
1502
|
+
if (value == null) {
|
|
1503
|
+
this.selectedTargetIds.set([]);
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
this.selectedTargetIds.set(Array.isArray(value) ? value : [value]);
|
|
1507
|
+
}
|
|
1508
|
+
onSearchChange(value) {
|
|
1509
|
+
this.searchQuery.set(value ?? '');
|
|
1510
|
+
this.assistsList()?.refresh(false);
|
|
1511
|
+
}
|
|
1512
|
+
async ensureTargetsLoaded() {
|
|
1513
|
+
if (!this.targetsLoadPromise)
|
|
1514
|
+
this.targetsLoadPromise = this.loadTargetsFromApi();
|
|
1515
|
+
await this.targetsLoadPromise;
|
|
1516
|
+
}
|
|
1517
|
+
async loadTargetsFromApi() {
|
|
1518
|
+
this.loading.set(true);
|
|
1519
|
+
try {
|
|
1520
|
+
let targets = await this.assistLauncher.loadTargets();
|
|
1521
|
+
if (!targets.length) {
|
|
1522
|
+
targets = await this.assistLauncher.loadTargets(true);
|
|
1523
|
+
}
|
|
1524
|
+
this.rows.set(targets);
|
|
1525
|
+
}
|
|
1526
|
+
catch (error) {
|
|
1527
|
+
console.error('Failed to load AI chat targets:', error);
|
|
1528
|
+
const message = await this.translationService.translateAsync('@conversation:chat.assist-dialog.errors.load-assists');
|
|
1529
|
+
this.toastService.danger(message);
|
|
1530
|
+
this.rows.set([]);
|
|
1531
|
+
}
|
|
1532
|
+
finally {
|
|
1533
|
+
this.loading.set(false);
|
|
1534
|
+
queueMicrotask(() => this.assistsList()?.refresh(false));
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
filteredTargetRows() {
|
|
1538
|
+
return axmFilterChatAiTargets((value) => this.translationService.resolve(value), this.rows(), this.searchQuery());
|
|
1539
|
+
}
|
|
1540
|
+
displayName(row) {
|
|
1541
|
+
return axmChatAiTargetDisplayName((value) => this.translationService.resolve(value), row);
|
|
1542
|
+
}
|
|
1543
|
+
targetAvatarUrl(row) {
|
|
1544
|
+
return axmChatAiTargetAvatarUrl(row);
|
|
1545
|
+
}
|
|
1546
|
+
onCancel() {
|
|
1547
|
+
const event = new AXComponentCloseEvent();
|
|
1548
|
+
event.component = this;
|
|
1549
|
+
event.data = null;
|
|
1550
|
+
this.onClosed.emit(event);
|
|
1551
|
+
}
|
|
1552
|
+
async onStartChat() {
|
|
1553
|
+
if (this.starting()) {
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
const targetId = this.selectedTargetIds()[0];
|
|
1557
|
+
if (!targetId) {
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
const row = this.rows().find((r) => r.id === targetId);
|
|
1561
|
+
if (!row) {
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
this.starting.set(true);
|
|
1565
|
+
this.layoutTheme.setOverlayLoading(true);
|
|
1566
|
+
try {
|
|
1567
|
+
// Create + select without route change first. Navigating while the popup is open
|
|
1568
|
+
// destroys this component and unsubscribes `onClosed` → ObjectUnsubscribedError toast.
|
|
1569
|
+
const result = await this.assistLauncher.startAssistChatFromRow(row, false);
|
|
1570
|
+
if (!result?.conversation) {
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
const event = new AXComponentCloseEvent();
|
|
1574
|
+
event.component = this;
|
|
1575
|
+
event.data = result.conversation;
|
|
1576
|
+
this.#emitClosed(event);
|
|
1577
|
+
await this.assistLauncher.navigateToConversation(result.conversation.id);
|
|
1578
|
+
}
|
|
1579
|
+
catch (error) {
|
|
1580
|
+
console.error('Failed to start assist chat:', error);
|
|
1581
|
+
const message = await this.translationService.translateAsync('@conversation:chat.assist-dialog.errors.start-chat');
|
|
1582
|
+
this.toastService.danger(message);
|
|
1583
|
+
}
|
|
1584
|
+
finally {
|
|
1585
|
+
this.starting.set(false);
|
|
1586
|
+
this.layoutTheme.setOverlayLoading(false);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
#emitClosed(event) {
|
|
1590
|
+
try {
|
|
1591
|
+
this.onClosed.emit(event);
|
|
1592
|
+
}
|
|
1593
|
+
catch {
|
|
1594
|
+
// Popup may already be tearing down; conversation was created successfully.
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
async #updatePopupTitle(popupRef) {
|
|
1598
|
+
const title = await this.translationService.translateAsync('@conversation:new-conversation.actions.chat-with-ai');
|
|
1599
|
+
popupRef.setTitle(title);
|
|
1600
|
+
}
|
|
1601
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMChatWithAssistDialogComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
1602
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: AXMChatWithAssistDialogComponent, isStandalone: true, selector: "axm-chat-with-assist-dialog", inputs: { __popup__: { classPropertyName: "__popup__", publicName: "__popup__", isSignal: true, isRequired: false, transformFunction: null }, embeddedInTabs: { classPropertyName: "embeddedInTabs", publicName: "embeddedInTabs", isSignal: true, isRequired: false, transformFunction: null } }, providers: [{ provide: AXClosableComponent, useExisting: AXMChatWithAssistDialogComponent }], viewQueries: [{ propertyName: "assistsListEmptyTpl", first: true, predicate: ["assistsListEmpty"], descendants: true, isSignal: true }, { propertyName: "assistsListLoadingTpl", first: true, predicate: ["assistsListLoading"], descendants: true, isSignal: true }, { propertyName: "assistsList", first: true, predicate: ["assistsList"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: `
|
|
1603
|
+
<div class="new-conversation-panel">
|
|
1604
|
+
<div class="new-conversation-dialog-content">
|
|
1605
|
+
<div class="form-field form-field--grow">
|
|
1606
|
+
<div class="users-list-wrap">
|
|
1607
|
+
<div class="users-list-search">
|
|
1608
|
+
<ax-search-box [value]="searchQuery()" (valueChange)="onSearchChange($event)" [disabled]="starting()" [placeholder]="('@conversation:chat.assist-dialog.search.placeholder' | translate | async) ?? ''"></ax-search-box>
|
|
1609
|
+
</div>
|
|
1610
|
+
<ax-list
|
|
1611
|
+
#assistsList
|
|
1612
|
+
class="users-list"
|
|
1613
|
+
[dataSource]="assistListDataSource"
|
|
1614
|
+
[multiple]="false"
|
|
1615
|
+
[checkbox]="false"
|
|
1616
|
+
[disabled]="starting()"
|
|
1617
|
+
[valueField]="'id'"
|
|
1618
|
+
[textField]="'name'"
|
|
1619
|
+
[itemHeight]="60"
|
|
1620
|
+
[ngModel]="selectedTargetIds()"
|
|
1621
|
+
(ngModelChange)="onSelectedTargetsChange($event ?? [])"
|
|
1622
|
+
[emptyTemplate]="assistsListEmptyTpl()"
|
|
1623
|
+
[loadingTemplate]="assistsListLoadingTpl()"
|
|
1624
|
+
[itemTemplate]="assistItemTpl"
|
|
1625
|
+
></ax-list>
|
|
1626
|
+
</div>
|
|
1627
|
+
</div>
|
|
1628
|
+
</div>
|
|
1629
|
+
<ax-footer class="dialog-footer">
|
|
1630
|
+
<ax-suffix>
|
|
1631
|
+
<ax-button [text]="('@conversation:comments.actions.cancel.title' | translate | async) ?? ''" [look]="'solid'" [disabled]="starting()" (onClick)="onCancel()"></ax-button>
|
|
1632
|
+
<ax-button
|
|
1633
|
+
[text]="('@conversation:chat.assist-dialog.actions.start-chat' | translate | async) ?? ''"
|
|
1634
|
+
[look]="'solid'"
|
|
1635
|
+
[color]="'primary'"
|
|
1636
|
+
[disabled]="!selectedTargetIds().length || loading() || starting()"
|
|
1637
|
+
(onClick)="onStartChat()"
|
|
1638
|
+
></ax-button>
|
|
1639
|
+
</ax-suffix>
|
|
1640
|
+
</ax-footer>
|
|
1641
|
+
</div>
|
|
1642
|
+
|
|
1643
|
+
<ng-template #assistsListEmpty>
|
|
1644
|
+
<div class="users-list-empty">
|
|
1645
|
+
@if (searchQuery()) {
|
|
1646
|
+
{{ '@conversation:chat.assist-dialog.states.no-search-result' | translate | async }}
|
|
1647
|
+
} @else {
|
|
1648
|
+
{{ '@conversation:chat.assist-dialog.states.no-assists' | translate | async }}
|
|
1649
|
+
}
|
|
1650
|
+
</div>
|
|
1651
|
+
</ng-template>
|
|
1652
|
+
|
|
1653
|
+
<ng-template #assistsListLoading>
|
|
1654
|
+
<div class="user-item-skeleton">
|
|
1655
|
+
<ax-skeleton class="skeleton-avatar" [animated]="true"></ax-skeleton>
|
|
1656
|
+
<div class="skeleton-text">
|
|
1657
|
+
<ax-skeleton class="skeleton-name" [animated]="true"></ax-skeleton>
|
|
1658
|
+
<ax-skeleton class="skeleton-description" [animated]="true"></ax-skeleton>
|
|
1659
|
+
</div>
|
|
1660
|
+
</div>
|
|
1661
|
+
</ng-template>
|
|
1662
|
+
|
|
1663
|
+
<ng-template #assistItemTpl let-item>
|
|
1664
|
+
@if (targetFromItem(item); as row) {
|
|
1665
|
+
<div class="user-item-content">
|
|
1666
|
+
<axp-avatar [size]="48" [fullName]="displayName(row)" [src]="targetAvatarUrl(row) ?? ''" />
|
|
1667
|
+
<div class="user-item-text">
|
|
1668
|
+
<span class="truncated user-name-text">{{ row.title | translate | async }}</span>
|
|
1669
|
+
@if (row.description) {
|
|
1670
|
+
<span class="user-description-text truncated">{{ row.description | translate | async }}</span>
|
|
1671
|
+
}
|
|
1672
|
+
</div>
|
|
1673
|
+
</div>
|
|
1674
|
+
}
|
|
1675
|
+
</ng-template>
|
|
1676
|
+
`, isInline: true, styles: [":host{display:block}.new-conversation-panel{display:flex;flex-direction:column;width:100%;overflow:hidden}.new-conversation-dialog-content{flex:1;min-height:0;overflow:hidden;display:flex;flex-direction:column;gap:.75rem}.form-field--grow{flex:1;min-height:0;display:flex;flex-direction:column}.form-field{display:flex;flex-direction:column;gap:.35rem}.users-list-wrap{display:flex;flex-direction:column;height:min(50vh,36rem);overflow:hidden}.users-list-search{flex:0 0 auto;padding:.5rem .75rem;border-bottom:1px solid rgb(var(--ax-sys-color-border-light-surface));background:rgb(var(--ax-sys-color-lighter-surface))}.users-list{display:block;flex:1 1 auto;min-height:0}.user-item-content{display:flex;align-items:center;gap:.75rem;height:100%;padding:.5rem .75rem;min-width:0}.user-item-text{display:flex;flex-direction:column;justify-content:center;min-width:0;flex:1;gap:.125rem}.user-name-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.875rem;line-height:1.25rem;font-weight:500;color:rgb(var(--ax-sys-color-on-surface))}.user-description-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.875rem;line-height:1.25rem;font-weight:400;opacity:.72;color:rgb(var(--ax-sys-color-on-surface))}.users-list-empty{padding:1rem;text-align:center;font-size:.85rem;opacity:.7}.user-item-skeleton{display:flex;align-items:center;gap:.75rem;height:60px;padding:.5rem 1.25rem;min-width:0}.skeleton-avatar{flex:0 0 auto;width:3rem;height:3rem;border-radius:9999px}.skeleton-text{display:flex;flex-direction:column;min-width:0;flex:1;gap:.5rem}.skeleton-name{height:1rem;width:60%;border-radius:.25rem}.skeleton-description{height:.75rem;width:40%;border-radius:.25rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: AXButtonComponent, selector: "ax-button", inputs: ["disabled", "size", "tabIndex", "color", "look", "text", "toggleable", "selected", "iconOnly", "type", "loadingText"], outputs: ["onBlur", "onFocus", "onClick", "selectedChange", "toggleableChange", "lookChange", "colorChange", "disabledChange", "loadingTextChange"] }, { kind: "component", type: AXListComponent, selector: "ax-list", inputs: ["id", "name", "disabled", "readonly", "valueField", "textField", "textTemplate", "disabledField", "multiple", "selectionMode", "isItemTruncated", "showItemTooltip", "dataSource", "itemHeight", "itemTemplate", "emptyTemplate", "loadingTemplate", "checkbox"], outputs: ["onValueChanged", "disabledChange", "readonlyChange", "onBlur", "onFocus", "onItemClick", "onItemSelected", "onScrolledIndexChanged"] }, { kind: "component", type: AXSearchBoxComponent, selector: "ax-search-box", inputs: ["disabled", "readonly", "tabIndex", "placeholder", "value", "state", "name", "id", "look", "class", "delayTime", "type", "autoSearch"], outputs: ["valueChange", "stateChange", "onValueChanged", "onBlur", "onFocus", "readonlyChange", "disabledChange", "onKeyDown", "onKeyUp", "onKeyPress"] }, { kind: "component", type: AXPAvatarComponent, selector: "axp-avatar", inputs: ["size", "src", "fullName", "fallbackText", "placeholder"] }, { kind: "component", type: AXDecoratorGenericComponent, selector: "ax-footer, ax-header, ax-content, ax-divider, ax-form-hint, ax-prefix, ax-suffix, ax-text, ax-title, ax-subtitle, ax-placeholder, ax-overlay" }, { kind: "component", type: AXSkeletonComponent, selector: "ax-skeleton", inputs: ["animated"] }, { kind: "ngmodule", type: AXTranslationModule }, { kind: "pipe", type: i5.AsyncPipe, name: "async" }, { kind: "pipe", type: i6.AXTranslatorPipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1677
|
+
}
|
|
1678
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMChatWithAssistDialogComponent, decorators: [{
|
|
1679
|
+
type: Component,
|
|
1680
|
+
args: [{ selector: 'axm-chat-with-assist-dialog', changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, FormsModule, AXButtonComponent, AXListComponent, AXSearchBoxComponent, AXPAvatarComponent, AXDecoratorGenericComponent, AXSkeletonComponent, AXTranslationModule], providers: [{ provide: AXClosableComponent, useExisting: AXMChatWithAssistDialogComponent }], template: `
|
|
1681
|
+
<div class="new-conversation-panel">
|
|
1682
|
+
<div class="new-conversation-dialog-content">
|
|
1683
|
+
<div class="form-field form-field--grow">
|
|
1684
|
+
<div class="users-list-wrap">
|
|
1685
|
+
<div class="users-list-search">
|
|
1686
|
+
<ax-search-box [value]="searchQuery()" (valueChange)="onSearchChange($event)" [disabled]="starting()" [placeholder]="('@conversation:chat.assist-dialog.search.placeholder' | translate | async) ?? ''"></ax-search-box>
|
|
1687
|
+
</div>
|
|
1688
|
+
<ax-list
|
|
1689
|
+
#assistsList
|
|
1690
|
+
class="users-list"
|
|
1691
|
+
[dataSource]="assistListDataSource"
|
|
1692
|
+
[multiple]="false"
|
|
1693
|
+
[checkbox]="false"
|
|
1694
|
+
[disabled]="starting()"
|
|
1695
|
+
[valueField]="'id'"
|
|
1696
|
+
[textField]="'name'"
|
|
1697
|
+
[itemHeight]="60"
|
|
1698
|
+
[ngModel]="selectedTargetIds()"
|
|
1699
|
+
(ngModelChange)="onSelectedTargetsChange($event ?? [])"
|
|
1700
|
+
[emptyTemplate]="assistsListEmptyTpl()"
|
|
1701
|
+
[loadingTemplate]="assistsListLoadingTpl()"
|
|
1702
|
+
[itemTemplate]="assistItemTpl"
|
|
1703
|
+
></ax-list>
|
|
1704
|
+
</div>
|
|
1705
|
+
</div>
|
|
1706
|
+
</div>
|
|
1707
|
+
<ax-footer class="dialog-footer">
|
|
1708
|
+
<ax-suffix>
|
|
1709
|
+
<ax-button [text]="('@conversation:comments.actions.cancel.title' | translate | async) ?? ''" [look]="'solid'" [disabled]="starting()" (onClick)="onCancel()"></ax-button>
|
|
1710
|
+
<ax-button
|
|
1711
|
+
[text]="('@conversation:chat.assist-dialog.actions.start-chat' | translate | async) ?? ''"
|
|
1712
|
+
[look]="'solid'"
|
|
1713
|
+
[color]="'primary'"
|
|
1714
|
+
[disabled]="!selectedTargetIds().length || loading() || starting()"
|
|
1715
|
+
(onClick)="onStartChat()"
|
|
1716
|
+
></ax-button>
|
|
1717
|
+
</ax-suffix>
|
|
1718
|
+
</ax-footer>
|
|
1719
|
+
</div>
|
|
1720
|
+
|
|
1721
|
+
<ng-template #assistsListEmpty>
|
|
1722
|
+
<div class="users-list-empty">
|
|
1723
|
+
@if (searchQuery()) {
|
|
1724
|
+
{{ '@conversation:chat.assist-dialog.states.no-search-result' | translate | async }}
|
|
1725
|
+
} @else {
|
|
1726
|
+
{{ '@conversation:chat.assist-dialog.states.no-assists' | translate | async }}
|
|
1727
|
+
}
|
|
1728
|
+
</div>
|
|
1729
|
+
</ng-template>
|
|
1730
|
+
|
|
1731
|
+
<ng-template #assistsListLoading>
|
|
1732
|
+
<div class="user-item-skeleton">
|
|
1733
|
+
<ax-skeleton class="skeleton-avatar" [animated]="true"></ax-skeleton>
|
|
1734
|
+
<div class="skeleton-text">
|
|
1735
|
+
<ax-skeleton class="skeleton-name" [animated]="true"></ax-skeleton>
|
|
1736
|
+
<ax-skeleton class="skeleton-description" [animated]="true"></ax-skeleton>
|
|
1737
|
+
</div>
|
|
1738
|
+
</div>
|
|
1739
|
+
</ng-template>
|
|
1740
|
+
|
|
1741
|
+
<ng-template #assistItemTpl let-item>
|
|
1742
|
+
@if (targetFromItem(item); as row) {
|
|
1743
|
+
<div class="user-item-content">
|
|
1744
|
+
<axp-avatar [size]="48" [fullName]="displayName(row)" [src]="targetAvatarUrl(row) ?? ''" />
|
|
1745
|
+
<div class="user-item-text">
|
|
1746
|
+
<span class="truncated user-name-text">{{ row.title | translate | async }}</span>
|
|
1747
|
+
@if (row.description) {
|
|
1748
|
+
<span class="user-description-text truncated">{{ row.description | translate | async }}</span>
|
|
1749
|
+
}
|
|
1750
|
+
</div>
|
|
1751
|
+
</div>
|
|
1752
|
+
}
|
|
1753
|
+
</ng-template>
|
|
1754
|
+
`, styles: [":host{display:block}.new-conversation-panel{display:flex;flex-direction:column;width:100%;overflow:hidden}.new-conversation-dialog-content{flex:1;min-height:0;overflow:hidden;display:flex;flex-direction:column;gap:.75rem}.form-field--grow{flex:1;min-height:0;display:flex;flex-direction:column}.form-field{display:flex;flex-direction:column;gap:.35rem}.users-list-wrap{display:flex;flex-direction:column;height:min(50vh,36rem);overflow:hidden}.users-list-search{flex:0 0 auto;padding:.5rem .75rem;border-bottom:1px solid rgb(var(--ax-sys-color-border-light-surface));background:rgb(var(--ax-sys-color-lighter-surface))}.users-list{display:block;flex:1 1 auto;min-height:0}.user-item-content{display:flex;align-items:center;gap:.75rem;height:100%;padding:.5rem .75rem;min-width:0}.user-item-text{display:flex;flex-direction:column;justify-content:center;min-width:0;flex:1;gap:.125rem}.user-name-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.875rem;line-height:1.25rem;font-weight:500;color:rgb(var(--ax-sys-color-on-surface))}.user-description-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.875rem;line-height:1.25rem;font-weight:400;opacity:.72;color:rgb(var(--ax-sys-color-on-surface))}.users-list-empty{padding:1rem;text-align:center;font-size:.85rem;opacity:.7}.user-item-skeleton{display:flex;align-items:center;gap:.75rem;height:60px;padding:.5rem 1.25rem;min-width:0}.skeleton-avatar{flex:0 0 auto;width:3rem;height:3rem;border-radius:9999px}.skeleton-text{display:flex;flex-direction:column;min-width:0;flex:1;gap:.5rem}.skeleton-name{height:1rem;width:60%;border-radius:.25rem}.skeleton-description{height:.75rem;width:40%;border-radius:.25rem}\n"] }]
|
|
1755
|
+
}], propDecorators: { __popup__: [{ type: i0.Input, args: [{ isSignal: true, alias: "__popup__", required: false }] }], embeddedInTabs: [{ type: i0.Input, args: [{ isSignal: true, alias: "embeddedInTabs", required: false }] }], assistsListEmptyTpl: [{ type: i0.ViewChild, args: ['assistsListEmpty', { isSignal: true }] }], assistsListLoadingTpl: [{ type: i0.ViewChild, args: ['assistsListLoading', { isSignal: true }] }], assistsList: [{ type: i0.ViewChild, args: ['assistsList', { isSignal: true }] }] } });
|
|
1756
|
+
|
|
1757
|
+
class AXMChatSidebarAssistAction {
|
|
1758
|
+
constructor() {
|
|
1759
|
+
this.id = 'chat-with-ai';
|
|
1760
|
+
this.label = '@conversation:new-conversation.actions.chat-with-ai';
|
|
1761
|
+
this.icon = 'fa-light fa-robot';
|
|
1762
|
+
this.priority = 20;
|
|
1763
|
+
this.popupService = inject(AXPopupService);
|
|
1764
|
+
this.translationService = inject(AXTranslationService);
|
|
1765
|
+
this.assistLauncher = inject(AXMChatAssistLauncherService);
|
|
1766
|
+
}
|
|
1767
|
+
async run(event) {
|
|
1768
|
+
event?.nativeEvent?.stopPropagation();
|
|
1769
|
+
// Await catalog so the dialog's first AXList load is not racing an empty/incomplete query.
|
|
1770
|
+
await this.assistLauncher.loadTargets();
|
|
1771
|
+
const title = await this.translationService.translateAsync('@conversation:new-conversation.actions.chat-with-ai');
|
|
1772
|
+
await this.popupService.open(AXMChatWithAssistDialogComponent, {
|
|
1773
|
+
title,
|
|
1774
|
+
size: 'sm',
|
|
1775
|
+
closeButton: true,
|
|
1776
|
+
});
|
|
1777
|
+
return null;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
const AXM_CHAT_SIDEBAR_ASSIST_ACTION_PROVIDERS = [
|
|
1781
|
+
AXMChatSidebarAssistAction,
|
|
1782
|
+
{
|
|
1783
|
+
provide: AXM_CHAT_SIDEBAR_ACTION_PROVIDER,
|
|
1784
|
+
useExisting: AXMChatSidebarAssistAction,
|
|
1785
|
+
multi: true,
|
|
1786
|
+
},
|
|
1787
|
+
];
|
|
1788
|
+
|
|
1789
|
+
class AXEAIConversationExtensionModule {
|
|
1790
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAIConversationExtensionModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
1791
|
+
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "22.0.8", ngImport: i0, type: AXEAIConversationExtensionModule, imports: [i1$1.AXPComponentSlotModule] }); }
|
|
1792
|
+
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAIConversationExtensionModule, providers: [
|
|
1793
|
+
{
|
|
1794
|
+
provide: AXM_CONVERSATION_ASSIST_RESPONSE_PORT,
|
|
1795
|
+
useClass: AXEConversationAssistResponseService,
|
|
1796
|
+
},
|
|
1797
|
+
{
|
|
1798
|
+
provide: AXM_CHAT_ASSIST_CATALOG_PORT,
|
|
1799
|
+
useExisting: AXMChatAssistCatalogService,
|
|
1800
|
+
},
|
|
1801
|
+
...AXM_CONVERSATION_ASSIST_BACKGROUND_OPERATION_HANDLERS,
|
|
1802
|
+
{
|
|
1803
|
+
provide: AXP_SEARCH_PROVIDER,
|
|
1804
|
+
useClass: AXMConversationAssistSearchProvider,
|
|
1805
|
+
multi: true,
|
|
1806
|
+
},
|
|
1807
|
+
{
|
|
1808
|
+
provide: AXP_SEARCH_DEFINITION_PROVIDER,
|
|
1809
|
+
useClass: AXMConversationAssistSearchDefinitionProvider,
|
|
1810
|
+
multi: true,
|
|
1811
|
+
},
|
|
1812
|
+
...AXE_CHAT_EMPTY_STATE_PROVIDERS,
|
|
1813
|
+
...AXE_MINI_CHAT_INBOX_FILTER_PROVIDERS,
|
|
1814
|
+
...AXE_ASSIST_DEFAULT_RESOLVER_PROVIDERS,
|
|
1815
|
+
...AXM_CHAT_SIDEBAR_ASSIST_ACTION_PROVIDERS,
|
|
1816
|
+
provideAppStartUpTask(() => {
|
|
1817
|
+
const launcher = inject(AXMChatAssistLauncherService);
|
|
1818
|
+
return {
|
|
1819
|
+
name: 'conversation-assist-catalog-warm',
|
|
1820
|
+
priority: AXP_APP_STARTUP_PRIORITY.conversationSessionSync + 1,
|
|
1821
|
+
run: async () => {
|
|
1822
|
+
void launcher.loadTargets();
|
|
1823
|
+
},
|
|
1824
|
+
};
|
|
1825
|
+
}),
|
|
1826
|
+
provideCommandSetups([
|
|
1827
|
+
{
|
|
1828
|
+
key: CONVERSATION_START_ASSIST_CHAT_COMMAND,
|
|
1829
|
+
command: () => import('./acorex-platform-extension-ai-conversation-client-start-assist-chat.command-BGICtlcW.mjs').then((m) => m.AXMConversationStartAssistChatCommand),
|
|
1830
|
+
},
|
|
1831
|
+
{
|
|
1832
|
+
key: CONVERSATION_SEND_ASSIST_CHAT_MESSAGE_COMMAND,
|
|
1833
|
+
command: () => import('./acorex-platform-extension-ai-conversation-client-send-assist-chat-message.command-CMKccVFQ.mjs').then((m) => m.AXMConversationSendAssistChatMessageCommand),
|
|
1834
|
+
},
|
|
1835
|
+
]),
|
|
1836
|
+
], imports: [AXPComponentSlotModule.forChild({
|
|
1837
|
+
'root-header-end': [
|
|
1838
|
+
{
|
|
1839
|
+
priority: 55,
|
|
1840
|
+
name: 'conversation-mini-chat',
|
|
1841
|
+
component: AXMMiniChatHeaderSlotComponent,
|
|
1842
|
+
},
|
|
1843
|
+
],
|
|
1844
|
+
})] }); }
|
|
1845
|
+
}
|
|
1846
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAIConversationExtensionModule, decorators: [{
|
|
1847
|
+
type: NgModule,
|
|
1848
|
+
args: [{
|
|
1849
|
+
imports: [
|
|
1850
|
+
AXPComponentSlotModule.forChild({
|
|
1851
|
+
'root-header-end': [
|
|
1852
|
+
{
|
|
1853
|
+
priority: 55,
|
|
1854
|
+
name: 'conversation-mini-chat',
|
|
1855
|
+
component: AXMMiniChatHeaderSlotComponent,
|
|
1856
|
+
},
|
|
1857
|
+
],
|
|
1858
|
+
}),
|
|
1859
|
+
],
|
|
1860
|
+
providers: [
|
|
1861
|
+
{
|
|
1862
|
+
provide: AXM_CONVERSATION_ASSIST_RESPONSE_PORT,
|
|
1863
|
+
useClass: AXEConversationAssistResponseService,
|
|
1864
|
+
},
|
|
1865
|
+
{
|
|
1866
|
+
provide: AXM_CHAT_ASSIST_CATALOG_PORT,
|
|
1867
|
+
useExisting: AXMChatAssistCatalogService,
|
|
1868
|
+
},
|
|
1869
|
+
...AXM_CONVERSATION_ASSIST_BACKGROUND_OPERATION_HANDLERS,
|
|
1870
|
+
{
|
|
1871
|
+
provide: AXP_SEARCH_PROVIDER,
|
|
1872
|
+
useClass: AXMConversationAssistSearchProvider,
|
|
1873
|
+
multi: true,
|
|
1874
|
+
},
|
|
1875
|
+
{
|
|
1876
|
+
provide: AXP_SEARCH_DEFINITION_PROVIDER,
|
|
1877
|
+
useClass: AXMConversationAssistSearchDefinitionProvider,
|
|
1878
|
+
multi: true,
|
|
1879
|
+
},
|
|
1880
|
+
...AXE_CHAT_EMPTY_STATE_PROVIDERS,
|
|
1881
|
+
...AXE_MINI_CHAT_INBOX_FILTER_PROVIDERS,
|
|
1882
|
+
...AXE_ASSIST_DEFAULT_RESOLVER_PROVIDERS,
|
|
1883
|
+
...AXM_CHAT_SIDEBAR_ASSIST_ACTION_PROVIDERS,
|
|
1884
|
+
provideAppStartUpTask(() => {
|
|
1885
|
+
const launcher = inject(AXMChatAssistLauncherService);
|
|
1886
|
+
return {
|
|
1887
|
+
name: 'conversation-assist-catalog-warm',
|
|
1888
|
+
priority: AXP_APP_STARTUP_PRIORITY.conversationSessionSync + 1,
|
|
1889
|
+
run: async () => {
|
|
1890
|
+
void launcher.loadTargets();
|
|
1891
|
+
},
|
|
1892
|
+
};
|
|
1893
|
+
}),
|
|
1894
|
+
provideCommandSetups([
|
|
1895
|
+
{
|
|
1896
|
+
key: CONVERSATION_START_ASSIST_CHAT_COMMAND,
|
|
1897
|
+
command: () => import('./acorex-platform-extension-ai-conversation-client-start-assist-chat.command-BGICtlcW.mjs').then((m) => m.AXMConversationStartAssistChatCommand),
|
|
1898
|
+
},
|
|
1899
|
+
{
|
|
1900
|
+
key: CONVERSATION_SEND_ASSIST_CHAT_MESSAGE_COMMAND,
|
|
1901
|
+
command: () => import('./acorex-platform-extension-ai-conversation-client-send-assist-chat-message.command-CMKccVFQ.mjs').then((m) => m.AXMConversationSendAssistChatMessageCommand),
|
|
1902
|
+
},
|
|
1903
|
+
]),
|
|
1904
|
+
],
|
|
1905
|
+
}]
|
|
1906
|
+
}] });
|
|
1907
|
+
|
|
1908
|
+
/**
|
|
1909
|
+
* Generated bundle index. Do not edit.
|
|
1910
|
+
*/
|
|
1911
|
+
|
|
1912
|
+
export { AXMChatAssistLauncherService as A, AXCConversationAssistResponseService as a, AXEAIConversationExtensionModule as b, AXEConversationAssistResponseService as c };
|
|
1913
|
+
//# sourceMappingURL=acorex-platform-extension-ai-conversation-client-acorex-platform-extension-ai-conversation-client-ZUV9J76R.mjs.map
|