@acorex-platform/extension-ai-conversation-client 0.0.38 → 0.0.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (12) hide show
  1. package/fesm2022/{acorex-platform-extension-ai-conversation-client-acorex-platform-extension-ai-conversation-client-CRjLZIU5.mjs → acorex-platform-extension-ai-conversation-client-acorex-platform-extension-ai-conversation-client-CTr53paj.mjs} +829 -265
  2. package/fesm2022/acorex-platform-extension-ai-conversation-client-acorex-platform-extension-ai-conversation-client-CTr53paj.mjs.map +1 -0
  3. package/fesm2022/{acorex-platform-extension-ai-conversation-client-send-assist-chat-message.command-B3rJWdsz.mjs → acorex-platform-extension-ai-conversation-client-send-assist-chat-message.command-DIwe0Kes.mjs} +7 -5
  4. package/fesm2022/acorex-platform-extension-ai-conversation-client-send-assist-chat-message.command-DIwe0Kes.mjs.map +1 -0
  5. package/fesm2022/{acorex-platform-extension-ai-conversation-client-start-assist-chat.command-qYtaBoTP.mjs → acorex-platform-extension-ai-conversation-client-start-assist-chat.command-CQM2377i.mjs} +3 -2
  6. package/fesm2022/{acorex-platform-extension-ai-conversation-client-start-assist-chat.command-qYtaBoTP.mjs.map → acorex-platform-extension-ai-conversation-client-start-assist-chat.command-CQM2377i.mjs.map} +1 -1
  7. package/fesm2022/acorex-platform-extension-ai-conversation-client.mjs +2 -1
  8. package/fesm2022/acorex-platform-extension-ai-conversation-client.mjs.map +1 -1
  9. package/package.json +3 -1
  10. package/types/acorex-platform-extension-ai-conversation-client.d.ts +42 -3
  11. package/fesm2022/acorex-platform-extension-ai-conversation-client-acorex-platform-extension-ai-conversation-client-CRjLZIU5.mjs.map +0 -1
  12. package/fesm2022/acorex-platform-extension-ai-conversation-client-send-assist-chat-message.command-B3rJWdsz.mjs.map +0 -1
@@ -1,38 +1,41 @@
1
1
  import { AXP_SEARCH_PROVIDER, AXP_SEARCH_DEFINITION_PROVIDER } from '@acorex-platform/framework-client/common';
2
2
  import * as i1$1 from '@acorex-platform/framework-client/core';
3
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';
4
+ import { AXPCommandService, AXPRuntimeTransportService, provideCommandSetups } from '@acorex-platform/framework-client/runtime';
5
+ import { AXM_AI_MANAGEMENT_MODULE_NAME, AXM_AI_MANAGEMENT_ASSIST_ENTITY_NAME, AXMEntityChatService, AXMConversationEntityStoreService, axmChatAiTargetDisplayName, axmChatAiTargetDisplayDescription, axmChatAiTargetAvatarUrl, axmFilterChatAiTargets, axmChatAiPeerParticipantId, axmBuildAiChatConversationMetadata, axmEntityRefFromLaunchContext, CONVERSATION_START_ASSIST_CHAT_COMMAND, axmChatAiTargetShortcutDescriptionKey, AXMChatEmptyAssistSessionService, AXM_ASSIST_DEFAULT_RESOLVER_PORT, axmAssistStarterPromptChipItems, AXMAssistSuggestionChipsComponent, AXMChatComposerAssistPickerComponent, 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, AXMMiniChatPanelService, AXMMiniChatFooterDockComponent, 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
6
  import * as i0 from '@angular/core';
7
- import { inject, Injectable, computed, output, input, signal, viewChild, effect, ChangeDetectionStrategy, Component, NgModule } from '@angular/core';
7
+ import { inject, Injectable, DestroyRef, viewChild, output, input, computed, signal, effect, ChangeDetectionStrategy, Component, NgModule } from '@angular/core';
8
8
  import { AXPSystemActionType, runtimeCommandPayloadData } from '@acorex-platform/framework-shared/core';
9
9
  import { sortBy } from 'lodash-es';
10
- import { AXConversationService } from '@acorex/components/conversation';
10
+ import { AXConversationService, AXConversationComposerGenerationStateService } from '@acorex/components/conversation';
11
11
  import { AXToastService } from '@acorex/components/toast';
12
- import * as i6 from '@acorex/core/translation';
12
+ import * as i7 from '@acorex/core/translation';
13
13
  import { AXTranslationService, AXTranslationModule } from '@acorex/core/translation';
14
14
  import { AXPSessionService } from '@acorex-platform/framework-client/auth';
15
+ import { AXM_ASSIST_LAUNCH_CONTEXT_METADATA_KEY, axmSanitizeContextualAssistContext, AXM_CONTEXTUAL_ASSIST_STARTER_SYSTEM_PROMPT, axmBuildContextualAssistStarterUserPrompt, axmParseContextualAssistStarterItems } from '@acorex-platform/module-conversation-client/contracts';
15
16
  import { AXPEntityService } from '@acorex-platform/framework-client/layout/entity';
16
17
  import { Router } from '@angular/router';
17
- import { AXPLayoutThemeService } from '@acorex-platform/framework-client/themes/shared';
18
+ import { AIMANAGEMENT_STRUCTURED_TEXT_COMPLETION_COMMAND_KEY, axpAiChatTextMessage } from '@acorex-platform/module-ai-management-client/contracts';
18
19
  import * as i2 from '@acorex/components/button';
19
20
  import { AXButtonModule, AXButtonComponent } from '@acorex/components/button';
20
- import * as i4 from '@acorex/components/decorators';
21
+ import * as i3 from '@acorex/components/decorators';
21
22
  import { AXDecoratorModule, AXDecoratorGenericComponent } from '@acorex/components/decorators';
22
- import * as i3 from '@acorex/components/dropdown';
23
+ import * as i4 from '@acorex/components/dropdown';
23
24
  import { AXDropdownModule } from '@acorex/components/dropdown';
24
- import { AXTextBoxModule } from '@acorex/components/text-box';
25
+ import * as i5 from '@acorex/components/loading';
26
+ import { AXLoadingModule } from '@acorex/components/loading';
25
27
  import { AXLocaleService } from '@acorex/core/locale';
26
- import * as i5 from '@angular/common';
28
+ import * as i6 from '@angular/common';
27
29
  import { CommonModule } from '@angular/common';
28
- import { toSignal } from '@angular/core/rxjs-interop';
30
+ import { toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop';
29
31
  import * as i1 from '@angular/forms';
30
32
  import { FormsModule } from '@angular/forms';
31
33
  import { AXPAiManagerService } from '@acorex-platform/module-ai-management-client';
32
- import { AXMAiManagementTransportKeys, AIMANAGEMENT_STRUCTURED_TEXT_COMPLETION_COMMAND_KEY, axpAiChatTextMessage } from '@acorex-platform/module-ai-management-client/contracts';
34
+ import { AXMAiManagementTransportKeys, AIMANAGEMENT_STRUCTURED_TEXT_COMPLETION_COMMAND_KEY as AIMANAGEMENT_STRUCTURED_TEXT_COMPLETION_COMMAND_KEY$1, axpAiChatTextMessage as axpAiChatTextMessage$1 } from '@acorex-platform/module-ai-management-client/contracts';
33
35
  import { axmCompareMessagesChronologically, axmExtractAssistIdFromMetadata, AXM_ASSIST_CONVERSATION_TITLE_SYSTEM_PROMPT, axmSanitizeAiGeneratedConversationTitle, extractMessageTextFromPayload, axmEnsureAssistMessageSortsAfterReplyTarget } from '@acorex-platform/module-conversation-client/contracts';
34
36
  import { AXPopupService } from '@acorex/components/popup';
35
37
  import { AXPAvatarComponent } from '@acorex-platform/framework-client/layout/components';
38
+ import { AXPLayoutThemeService } from '@acorex-platform/framework-client/themes/shared';
36
39
  import { AXDataSource, AXComponentCloseEvent, AXClosableComponent } from '@acorex/cdk/common';
37
40
  import { AXListComponent } from '@acorex/components/list';
38
41
  import { AXBasePageComponent } from '@acorex/components/page';
@@ -169,6 +172,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
169
172
  class AXMChatAssistLauncherService {
170
173
  constructor() {
171
174
  this.conversationService = inject(AXConversationService);
175
+ this.entityChatService = inject(AXMEntityChatService);
172
176
  this.assistCatalog = inject(AXMChatAssistCatalogService);
173
177
  this.entityStore = inject(AXMConversationEntityStoreService);
174
178
  this.translationService = inject(AXTranslationService);
@@ -202,11 +206,11 @@ class AXMChatAssistLauncherService {
202
206
  this.toastService.danger(message);
203
207
  return null;
204
208
  }
205
- return this.startAssistChatFromRow(row, options.navigate !== false);
209
+ return this.startAssistChatFromRow(row, options.navigate !== false, options.launchContext);
206
210
  }
207
- async startAssistChatFromRow(row, navigate = true) {
211
+ async startAssistChatFromRow(row, navigate = true, launchContext) {
208
212
  try {
209
- const conversation = await this.#createTargetConversation(row);
213
+ const conversation = await this.#createTargetConversation(row, launchContext);
210
214
  await this.conversationService.selectConversation(conversation.id);
211
215
  if (navigate) {
212
216
  await this.navigateToConversation(conversation.id);
@@ -228,18 +232,26 @@ class AXMChatAssistLauncherService {
228
232
  }
229
233
  await this.router.navigate([this.#applicationRouteSegment(), 'chat', scoped]);
230
234
  }
231
- async #createTargetConversation(row) {
235
+ async #createTargetConversation(row, launchContext) {
232
236
  await this.assistCatalog.loadCatalogTargets();
233
237
  const participantId = axmChatAiPeerParticipantId(row.id);
234
238
  const title = this.displayName(row);
235
239
  const avatar = this.targetAvatarUrl(row);
236
240
  const assistRow = this.assistCatalog.getCachedAssist(row.id);
237
241
  const assistSnapshot = assistRow ? this.assistCatalog.toSnapshot(assistRow) : undefined;
242
+ const metadata = axmBuildAiChatConversationMetadata(row, assistSnapshot);
243
+ if (launchContext) {
244
+ metadata[AXM_ASSIST_LAUNCH_CONTEXT_METADATA_KEY] = axmSanitizeContextualAssistContext(launchContext);
245
+ }
246
+ const entityRef = axmEntityRefFromLaunchContext(launchContext);
247
+ if (entityRef) {
248
+ return this.entityChatService.createEntityChat(entityRef, row.id, metadata);
249
+ }
238
250
  const conversation = await this.conversationService.createConversation([participantId], 'bot', {
239
251
  title,
240
252
  ...(avatar ? { avatar } : {}),
241
253
  icon: 'fa-solid fa-robot',
242
- metadata: axmBuildAiChatConversationMetadata(row, assistSnapshot),
254
+ metadata,
243
255
  });
244
256
  this.entityStore.cacheConversation(conversation);
245
257
  this.entityStore.cacheEmptyMessageList(conversation.id);
@@ -320,20 +332,34 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
320
332
  type: Injectable
321
333
  }] });
322
334
 
335
+ const FALLBACK_CHIP_KEYS = ['@conversation:chat.contextual-assist.fallback.summarize', '@conversation:chat.contextual-assist.fallback.what-can-i-change', '@conversation:chat.contextual-assist.fallback.explain-record'];
336
+ const DRAFT_KIND_ICONS = {
337
+ image: 'fa-light fa-image',
338
+ video: 'fa-light fa-video',
339
+ audio: 'fa-light fa-music',
340
+ file: 'fa-light fa-file',
341
+ };
342
+ const MAX_EMPTY_DRAFT_FILES = 3;
323
343
  class AXMChatEmptyAssistComposerComponent {
344
+ //#endregion
345
+ //#region ----- Lifecycle -----
324
346
  constructor() {
325
- this.conversationService = inject(AXConversationService);
347
+ //#region ----- Services & Dependencies -----
348
+ this.commandService = inject(AXPCommandService);
326
349
  this.toastService = inject(AXToastService);
327
350
  this.translationService = inject(AXTranslationService);
328
351
  this.localeService = inject(AXLocaleService);
329
352
  this.assistCatalog = inject(AXMChatAssistCatalogService);
330
- this.layoutTheme = inject(AXPLayoutThemeService);
353
+ this.emptyAssistSession = inject(AXMChatEmptyAssistSessionService);
354
+ this.conversationService = inject(AXConversationService);
331
355
  this.defaultAssistResolver = inject(AXM_ASSIST_DEFAULT_RESOLVER_PORT, { optional: true });
332
- this.localeProfile = toSignal(this.localeService.profileChanged$, {
333
- initialValue: this.localeService.activeProfile(),
334
- });
335
- this.textDirection = computed(() => this.localeProfile()?.i18nMeta?.rtl === true ? 'rtl' : 'ltr', /* @ts-ignore */
336
- ...(ngDevMode ? [{ debugName: "textDirection" }] : /* istanbul ignore next */ []));
356
+ this.destroyRef = inject(DestroyRef);
357
+ this.draftInput = viewChild('draftInput', /* @ts-ignore */
358
+ ...(ngDevMode ? [{ debugName: "draftInput" }] : /* istanbul ignore next */ []));
359
+ this.fileInput = viewChild('fileInput', /* @ts-ignore */
360
+ ...(ngDevMode ? [{ debugName: "fileInput" }] : /* istanbul ignore next */ []));
361
+ //#endregion
362
+ //#region ----- Inputs / Outputs -----
337
363
  this.conversationCreated = output();
338
364
  this.initialAssistId = input(undefined, /* @ts-ignore */
339
365
  ...(ngDevMode ? [{ debugName: "initialAssistId" }] : /* istanbul ignore next */ []));
@@ -343,103 +369,210 @@ class AXMChatEmptyAssistComposerComponent {
343
369
  ...(ngDevMode ? [{ debugName: "entityType" }] : /* istanbul ignore next */ []));
344
370
  this.sendStarterPromptsInPlace = input(false, /* @ts-ignore */
345
371
  ...(ngDevMode ? [{ debugName: "sendStarterPromptsInPlace" }] : /* istanbul ignore next */ []));
372
+ this.launchContext = input(undefined, /* @ts-ignore */
373
+ ...(ngDevMode ? [{ debugName: "launchContext" }] : /* istanbul ignore next */ []));
346
374
  this.compact = input(false, /* @ts-ignore */
347
375
  ...(ngDevMode ? [{ debugName: "compact" }] : /* istanbul ignore next */ []));
376
+ this.assistConversationCreated = input(undefined, /* @ts-ignore */
377
+ ...(ngDevMode ? [{ debugName: "assistConversationCreated" }] : /* istanbul ignore next */ []));
378
+ //#endregion
379
+ //#region ----- State Signals -----
380
+ this.localeProfile = toSignal(this.localeService.profileChanged$, {
381
+ initialValue: this.localeService.activeProfile(),
382
+ });
383
+ this.textDirection = computed(() => (this.localeProfile()?.i18nMeta?.rtl === true ? 'rtl' : 'ltr'), /* @ts-ignore */
384
+ ...(ngDevMode ? [{ debugName: "textDirection" }] : /* istanbul ignore next */ []));
385
+ this.hasActiveConversation = computed(() => Boolean(this.conversationService.activeConversationId()?.trim()), /* @ts-ignore */
386
+ ...(ngDevMode ? [{ debugName: "hasActiveConversation" }] : /* istanbul ignore next */ []));
348
387
  this.rows = signal([], /* @ts-ignore */
349
388
  ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
350
389
  this.loading = signal(true, /* @ts-ignore */
351
390
  ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
391
+ this.sending = signal(false, /* @ts-ignore */
392
+ ...(ngDevMode ? [{ debugName: "sending" }] : /* istanbul ignore next */ []));
393
+ this.chipsLoading = signal(false, /* @ts-ignore */
394
+ ...(ngDevMode ? [{ debugName: "chipsLoading" }] : /* istanbul ignore next */ []));
352
395
  this.selectedAssistId = signal(undefined, /* @ts-ignore */
353
396
  ...(ngDevMode ? [{ debugName: "selectedAssistId" }] : /* istanbul ignore next */ []));
354
- this.starterPromptItems = signal([], /* @ts-ignore */
355
- ...(ngDevMode ? [{ debugName: "starterPromptItems" }] : /* istanbul ignore next */ []));
356
397
  this.draftText = signal('', /* @ts-ignore */
357
398
  ...(ngDevMode ? [{ debugName: "draftText" }] : /* istanbul ignore next */ []));
358
- this.sending = signal(false, /* @ts-ignore */
359
- ...(ngDevMode ? [{ debugName: "sending" }] : /* istanbul ignore next */ []));
399
+ this.draftFiles = signal([], /* @ts-ignore */
400
+ ...(ngDevMode ? [{ debugName: "draftFiles" }] : /* istanbul ignore next */ []));
401
+ this.starterPromptItems = signal([], /* @ts-ignore */
402
+ ...(ngDevMode ? [{ debugName: "starterPromptItems" }] : /* istanbul ignore next */ []));
403
+ this.maxDraftFiles = MAX_EMPTY_DRAFT_FILES;
360
404
  this.isBusy = computed(() => this.sending() || this.loading(), /* @ts-ignore */
361
405
  ...(ngDevMode ? [{ debugName: "isBusy" }] : /* istanbul ignore next */ []));
362
- this.draftInput = viewChild('draftInput', /* @ts-ignore */
363
- ...(ngDevMode ? [{ debugName: "draftInput" }] : /* istanbul ignore next */ []));
406
+ this.canSend = computed(() => Boolean(this.selectedAssistId()) &&
407
+ !this.isBusy() &&
408
+ (this.draftText().trim().length > 0 || this.draftFiles().length > 0), /* @ts-ignore */
409
+ ...(ngDevMode ? [{ debugName: "canSend" }] : /* istanbul ignore next */ []));
364
410
  this.selectedAssistLabel = computed(() => {
365
411
  const selected = this.rows().find((row) => row.id === this.selectedAssistId());
366
- return selected?.title;
412
+ return this.translationService.resolve(selected?.title) ?? selected?.name ?? '';
367
413
  }, /* @ts-ignore */
368
414
  ...(ngDevMode ? [{ debugName: "selectedAssistLabel" }] : /* istanbul ignore next */ []));
369
- this.maxDraftHeightPx = computed(() => (this.compact() ? 128 : 224), /* @ts-ignore */
370
- ...(ngDevMode ? [{ debugName: "maxDraftHeightPx" }] : /* istanbul ignore next */ []));
415
+ this.draftPlaceholder = computed(() => {
416
+ if (this.compact()) {
417
+ return this.translationService.translateSync('@conversation:mini-chat.composer.placeholder');
418
+ }
419
+ const assist = this.selectedAssistLabel();
420
+ return this.translationService.translateSync('@conversation:chat.empty-assist.placeholder', {
421
+ params: { assist },
422
+ });
423
+ }, /* @ts-ignore */
424
+ ...(ngDevMode ? [{ debugName: "draftPlaceholder" }] : /* istanbul ignore next */ []));
371
425
  this.assistsLoadPromise = null;
372
426
  this.starterPromptsLoadGeneration = 0;
427
+ this.starterAbort = null;
428
+ //#endregion
429
+ //#region ----- Event Handlers -----
430
+ this.onAssistConversationCreated = (conversationId) => {
431
+ const handler = this.assistConversationCreated();
432
+ if (handler) {
433
+ return handler(conversationId);
434
+ }
435
+ this.conversationCreated.emit(conversationId);
436
+ };
373
437
  void this.ensureAssistsLoaded();
438
+ this.destroyRef.onDestroy(() => {
439
+ this.starterAbort?.abort();
440
+ this.clearDraftFiles();
441
+ this.emptyAssistSession.closeAssistPicker();
442
+ });
443
+ effect(() => {
444
+ const sessionAssistId = this.emptyAssistSession.selectedAssistId();
445
+ if (sessionAssistId && sessionAssistId !== this.selectedAssistId()) {
446
+ this.selectedAssistId.set(sessionAssistId);
447
+ }
448
+ });
374
449
  effect(() => {
375
- void this.loadStarterPromptsForAssist(this.selectedAssistId());
450
+ this.emptyAssistSession.bind({
451
+ selectedAssistId: this.selectedAssistId(),
452
+ rows: this.rows(),
453
+ entityId: this.entityId(),
454
+ entityType: this.entityType(),
455
+ launchContext: this.launchContext(),
456
+ });
376
457
  });
377
458
  effect(() => {
378
459
  this.draftText();
379
460
  queueMicrotask(() => this.resizeDraftInput());
380
461
  });
462
+ effect(() => {
463
+ const assistId = this.selectedAssistId();
464
+ const launchContext = this.launchContext();
465
+ if (launchContext) {
466
+ void this.loadContextualChips(assistId, launchContext);
467
+ return;
468
+ }
469
+ void this.loadStarterPromptsForAssist(assistId);
470
+ });
471
+ }
472
+ draftFileIcon(kind) {
473
+ return DRAFT_KIND_ICONS[kind];
474
+ }
475
+ browseDraftFiles() {
476
+ this.fileInput()?.nativeElement.click();
381
477
  }
382
- onAssistIdChange(value) {
383
- this.selectedAssistId.set(value?.trim() || undefined);
478
+ onDraftFilesSelected(event) {
479
+ const input = event.target;
480
+ if (!(input instanceof HTMLInputElement)) {
481
+ return;
482
+ }
483
+ const selected = Array.from(input.files ?? []);
484
+ input.value = '';
485
+ if (selected.length === 0) {
486
+ return;
487
+ }
488
+ const remaining = Math.max(0, MAX_EMPTY_DRAFT_FILES - this.draftFiles().length);
489
+ const accepted = selected.slice(0, remaining);
490
+ if (accepted.length === 0) {
491
+ return;
492
+ }
493
+ this.draftFiles.update((current) => [
494
+ ...current,
495
+ ...accepted.map((file) => this.toDraftFile(file)),
496
+ ]);
384
497
  }
385
- canSend() {
386
- return Boolean(this.selectedAssistId() && this.draftText().trim().length > 0 && !this.sending() && !this.loading());
498
+ removeDraftFile(id) {
499
+ const current = this.draftFiles();
500
+ const next = current.filter((item) => item.id !== id);
501
+ current
502
+ .filter((item) => item.id === id)
503
+ .forEach((item) => this.revokePreview(item.preview));
504
+ this.draftFiles.set(next);
387
505
  }
388
506
  resizeDraftInput() {
389
507
  const textarea = this.draftInput()?.nativeElement;
390
- if (!textarea)
508
+ if (!textarea) {
391
509
  return;
510
+ }
392
511
  textarea.style.height = '0px';
393
- textarea.style.height = `${Math.min(textarea.scrollHeight, this.maxDraftHeightPx())}px`;
512
+ textarea.style.height = `${Math.min(textarea.scrollHeight, this.compact() ? 128 : 224)}px`;
394
513
  }
395
514
  onDraftKeydown(event) {
396
- if (event.key !== 'Enter' || event.shiftKey || event.isComposing)
515
+ if (event.key !== 'Enter' || event.shiftKey || event.isComposing) {
397
516
  return;
517
+ }
398
518
  event.preventDefault();
399
- if (this.canSend())
519
+ if (this.canSend()) {
400
520
  void this.send();
521
+ }
401
522
  }
402
523
  async send() {
403
- const assistId = this.selectedAssistId();
404
- const text = this.draftText().trim();
405
- if (!assistId || !text || this.sending())
406
- return;
407
- const row = this.rows().find((r) => r.id === assistId);
408
- if (!row)
524
+ if (!this.canSend()) {
409
525
  return;
526
+ }
527
+ const text = this.draftText();
528
+ const files = this.draftFiles().map((item) => item.file);
410
529
  this.sending.set(true);
411
- this.layoutTheme.setOverlayLoading(true);
412
530
  try {
413
- const participantId = axmChatAiPeerParticipantId(row.id);
414
- const avatar = row.professionImageUrl?.trim() || undefined;
415
- const entityId = this.entityId()?.trim();
416
- const entityType = this.entityType()?.trim();
417
- const assistRow = this.assistCatalog.getCachedAssist(row.id) ?? (await this.assistCatalog.getAssist(row.id));
418
- const assistSnapshot = assistRow ? this.assistCatalog.toSnapshot(assistRow) : undefined;
419
- const conversation = await this.conversationService.createConversation([participantId], 'bot', {
420
- title: this.translationService.resolve(row.title) ?? row.name,
421
- ...(avatar ? { avatar } : {}),
422
- icon: 'fa-solid fa-robot',
423
- metadata: {
424
- ...axmBuildAiChatConversationMetadata(row, assistSnapshot),
425
- ...(entityId && entityType ? { entityId, entityType } : {}),
426
- },
427
- });
428
- await this.conversationService.selectConversation(conversation.id);
429
- const payload = { type: 'text', text };
430
- await this.conversationService.sendMessage({ conversationId: conversation.id, type: 'text', payload });
431
- this.draftText.set('');
432
- this.conversationCreated.emit(conversation.id);
433
- }
434
- catch {
435
- const message = await this.translationService.translateAsync('@conversation:chat.empty-assist.errors.send');
436
- this.toastService.danger(message);
531
+ const conversationId = await this.emptyAssistSession.sendWelcome(text, files, this.onAssistConversationCreated);
532
+ if (conversationId) {
533
+ this.draftText.set('');
534
+ this.clearDraftFiles();
535
+ }
437
536
  }
438
537
  finally {
439
538
  this.sending.set(false);
440
- this.layoutTheme.setOverlayLoading(false);
441
539
  }
442
540
  }
541
+ //#endregion
542
+ //#region ----- Draft files -----
543
+ toDraftFile(file) {
544
+ const kind = this.classifyDraftFile(file);
545
+ return {
546
+ id: globalThis.crypto?.randomUUID?.() ?? `${file.name}-${file.size}-${Date.now()}`,
547
+ file,
548
+ kind,
549
+ preview: kind === 'image' || kind === 'video' ? URL.createObjectURL(file) : undefined,
550
+ };
551
+ }
552
+ classifyDraftFile(file) {
553
+ const type = file.type.toLowerCase();
554
+ if (type.startsWith('image/')) {
555
+ return 'image';
556
+ }
557
+ if (type.startsWith('video/')) {
558
+ return 'video';
559
+ }
560
+ if (type.startsWith('audio/')) {
561
+ return 'audio';
562
+ }
563
+ return 'file';
564
+ }
565
+ clearDraftFiles() {
566
+ this.draftFiles().forEach((item) => this.revokePreview(item.preview));
567
+ this.draftFiles.set([]);
568
+ }
569
+ revokePreview(preview) {
570
+ if (preview) {
571
+ URL.revokeObjectURL(preview);
572
+ }
573
+ }
574
+ //#endregion
575
+ //#region ----- Data Loading -----
443
576
  async ensureAssistsLoaded() {
444
577
  if (!this.assistsLoadPromise)
445
578
  this.assistsLoadPromise = this.loadAssistsFromApi();
@@ -488,6 +621,8 @@ class AXMChatEmptyAssistComposerComponent {
488
621
  }
489
622
  }
490
623
  async loadStarterPromptsForAssist(assistId) {
624
+ this.starterAbort?.abort();
625
+ this.chipsLoading.set(false);
491
626
  const generation = ++this.starterPromptsLoadGeneration;
492
627
  const id = assistId?.trim();
493
628
  if (!id) {
@@ -502,14 +637,15 @@ class AXMChatEmptyAssistComposerComponent {
502
637
  }
503
638
  return;
504
639
  }
505
- const entityId = this.entityId()?.trim();
506
- const entityType = this.entityType()?.trim();
507
- const isEntityChat = Boolean(entityId && entityType);
640
+ const entityRef = this.resolveEntityChatRef();
641
+ const isEntityChat = Boolean(entityRef);
508
642
  const sendInPlace = this.sendStarterPromptsInPlace();
643
+ const launchContext = this.launchContext();
509
644
  this.starterPromptItems.set(axmAssistStarterPromptChipItems(assist, {
510
645
  assistId: id,
511
- ...(isEntityChat ? { entityId, entityType } : {}),
646
+ ...(entityRef ? { entityId: entityRef.entityId, entityType: entityRef.entityType } : {}),
512
647
  ...(sendInPlace || isEntityChat ? { navigate: false } : {}),
648
+ ...(launchContext ? { launchContext } : {}),
513
649
  }));
514
650
  }
515
651
  catch {
@@ -517,8 +653,76 @@ class AXMChatEmptyAssistComposerComponent {
517
653
  this.starterPromptItems.set([]);
518
654
  }
519
655
  }
656
+ async loadContextualChips(assistId, launchContext) {
657
+ const id = assistId?.trim();
658
+ if (!id) {
659
+ this.starterPromptItems.set([]);
660
+ this.chipsLoading.set(false);
661
+ return;
662
+ }
663
+ this.starterAbort?.abort();
664
+ const abort = new AbortController();
665
+ this.starterAbort = abort;
666
+ const generation = ++this.starterPromptsLoadGeneration;
667
+ this.chipsLoading.set(true);
668
+ try {
669
+ const locale = this.localeService.activeProfile()?.localeInfo?.code ?? 'en-US';
670
+ const context = axmSanitizeContextualAssistContext(launchContext);
671
+ const result = await this.commandService.execute(AIMANAGEMENT_STRUCTURED_TEXT_COMPLETION_COMMAND_KEY, {
672
+ assistId: id,
673
+ messages: [axpAiChatTextMessage('system', AXM_CONTEXTUAL_ASSIST_STARTER_SYSTEM_PROMPT), axpAiChatTextMessage('user', axmBuildContextualAssistStarterUserPrompt(context, locale))],
674
+ maxTokens: 512,
675
+ temperature: 0.3,
676
+ });
677
+ if (abort.signal.aborted || generation !== this.starterPromptsLoadGeneration) {
678
+ return;
679
+ }
680
+ const data = runtimeCommandPayloadData(result);
681
+ const items = axmParseContextualAssistStarterItems(data?.text ?? '');
682
+ const labels = items.length > 0 ? items : await this.fallbackChipLabels();
683
+ this.starterPromptItems.set(this.toInPlaceChipItems(labels, id, context));
684
+ }
685
+ catch {
686
+ if (!abort.signal.aborted && generation === this.starterPromptsLoadGeneration) {
687
+ const labels = await this.fallbackChipLabels();
688
+ this.starterPromptItems.set(this.toInPlaceChipItems(labels, id, axmSanitizeContextualAssistContext(launchContext)));
689
+ }
690
+ }
691
+ finally {
692
+ if (!abort.signal.aborted && generation === this.starterPromptsLoadGeneration) {
693
+ this.chipsLoading.set(false);
694
+ }
695
+ }
696
+ }
697
+ toInPlaceChipItems(labels, assistId, launchContext) {
698
+ const entityRef = this.resolveEntityChatRef();
699
+ return axmAssistStarterPromptChipItems({ starterPrompts: labels.map((text) => ({ text })) }, {
700
+ assistId,
701
+ navigate: false,
702
+ launchContext,
703
+ ...(entityRef ? { entityId: entityRef.entityId, entityType: entityRef.entityType } : {}),
704
+ });
705
+ }
706
+ resolveEntityChatRef() {
707
+ const entityId = this.entityId()?.trim();
708
+ const entityType = this.entityType()?.trim();
709
+ if (entityId && entityType) {
710
+ return { entityId, entityType };
711
+ }
712
+ return axmEntityRefFromLaunchContext(this.launchContext());
713
+ }
714
+ async fallbackChipLabels() {
715
+ const labels = [];
716
+ for (const key of FALLBACK_CHIP_KEYS) {
717
+ const text = (await this.translationService.translateAsync(key))?.trim();
718
+ if (text) {
719
+ labels.push(text);
720
+ }
721
+ }
722
+ return labels;
723
+ }
520
724
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMChatEmptyAssistComposerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
521
- 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()", "attr.dir": "textDirection()", "style.direction": "textDirection()" }, classAttribute: "axm-chat-empty-assist-composer-host" }, viewQueries: [{ propertyName: "draftInput", first: true, predicate: ["draftInput"], descendants: true, isSignal: true }], ngImport: i0, template: `
725
+ 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 }, launchContext: { classPropertyName: "launchContext", publicName: "launchContext", isSignal: true, isRequired: false, transformFunction: null }, compact: { classPropertyName: "compact", publicName: "compact", isSignal: true, isRequired: false, transformFunction: null }, assistConversationCreated: { classPropertyName: "assistConversationCreated", publicName: "assistConversationCreated", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { conversationCreated: "conversationCreated" }, host: { properties: { "class.axm-chat-empty-assist-composer-host--compact": "compact()", "attr.dir": "textDirection()", "style.direction": "textDirection()" }, classAttribute: "axm-chat-empty-assist-composer-host" }, viewQueries: [{ propertyName: "draftInput", first: true, predicate: ["draftInput"], descendants: true, isSignal: true }, { propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true, isSignal: true }], ngImport: i0, template: `
522
726
  <div class="axm-chat-empty-assist-composer">
523
727
  <div class="axm-chat-empty-assist-composer__hero">
524
728
  @if (compact()) {
@@ -527,92 +731,162 @@ class AXMChatEmptyAssistComposerComponent {
527
731
  </span>
528
732
  }
529
733
  <div class="axm-chat-empty-assist-composer__caption">
530
- {{ '@conversation:chat.empty-assist.caption' | translate | async }}
734
+ {{ (compact() ? '@conversation:mini-chat.empty.title' : '@conversation:chat.empty-assist.caption') | translate | async }}
531
735
  </div>
532
- </div>
533
-
534
- <div class="axm-chat-empty-assist-composer__composer-row">
535
- @if (!compact()) {
536
- <ax-button color="default" look="blank" class="rounded-full" [disabled]="isBusy()">
537
- <ax-icon icon="fa-regular fa-sparkles"></ax-icon>
538
- <span class="axm-chat-empty-assist-composer__assist-text">
539
- @if (selectedAssistLabel(); as assistLabel) {
540
- {{ assistLabel | translate | async }}
541
- } @else {
542
- {{ '@conversation:chat.empty-assist.assist-default' | translate | async }}
543
- }
544
- </span>
545
- @if (!isBusy()) {
546
- <ax-dropdown-panel>
547
- <ax-button-item-list>
548
- @for (row of rows(); track row.id) {
549
- <ax-button-item [text]="(row.title | translate | async)!" [selected]="row.id === selectedAssistId()" (onClick)="onAssistIdChange(row.id)"></ax-button-item>
550
- }
551
- </ax-button-item-list>
552
- </ax-dropdown-panel>
553
- }
554
- </ax-button>
555
- } @else {
556
- <ax-button
557
- color="default"
558
- look="blank"
559
- class="axm-chat-empty-assist-composer__assist-trigger rounded-full"
560
- [iconOnly]="true"
561
- [disabled]="isBusy()"
562
- [attr.aria-label]="('@conversation:chat.empty-assist.assist-default' | translate | async) ?? ''"
563
- >
564
- <ax-icon icon="fa-regular fa-sparkles"></ax-icon>
565
- @if (!isBusy()) {
566
- <ax-dropdown-panel>
567
- <ax-button-item-list>
568
- @for (row of rows(); track row.id) {
569
- <ax-button-item [text]="(row.title | translate | async)!" [selected]="row.id === selectedAssistId()" (onClick)="onAssistIdChange(row.id)"></ax-button-item>
570
- }
571
- </ax-button-item-list>
572
- </ax-dropdown-panel>
573
- }
574
- </ax-button>
736
+ @if (compact()) {
737
+ <p class="axm-chat-empty-assist-composer__subtitle">
738
+ {{ '@conversation:mini-chat.empty.subtitle' | translate | async }}
739
+ </p>
575
740
  }
741
+ </div>
576
742
 
577
- @let assistTitle = (selectedAssistLabel() | translate | async) ?? '';
578
-
579
- <textarea
580
- #draftInput
581
- rows="1"
582
- class="axm-chat-empty-assist-composer__textarea border-none outline-none w-full flex-1"
583
- [ngModel]="draftText()"
584
- (ngModelChange)="draftText.set($event ?? '')"
585
- (input)="resizeDraftInput()"
586
- (keydown)="onDraftKeydown($event)"
587
- [disabled]="isBusy()"
588
- [placeholder]="('@conversation:chat.empty-assist.placeholder' | translate: { params: { assist: assistTitle } } | async) ?? ''"
589
- ></textarea>
590
- <ax-button
591
- color="primary"
592
- class="axm-chat-empty-assist-composer__send rounded-full"
593
- [disabled]="!canSend()"
594
- [text]="sending() ? '…' : ''"
595
- [attr.aria-label]="('@conversation:comments.actions.send.title' | translate | async) ?? ''"
596
- (onClick)="send()"
597
- >
598
- @if (!sending()) {
599
- <ax-icon icon="fa-light fa-arrow-up"></ax-icon>
743
+ <!-- ----- Lookalike composer: attach and type stay local until send ----- -->
744
+ @if (!hasActiveConversation()) {
745
+ <div class="axm-chat-empty-assist-composer__composer">
746
+ <!---->
747
+ <!-- ----- Local attachment chips before the first message is sent ----- -->
748
+ @if (draftFiles().length > 0) {
749
+ <div class="axm-chat-empty-assist-composer__attachments">
750
+ <div class="axm-chat-empty-assist-composer__attachments-scroll" role="list">
751
+ @for (attachment of draftFiles(); track attachment.id) {
752
+ <div
753
+ class="axm-chat-empty-assist-composer__attachment"
754
+ role="listitem"
755
+ [class.axm-chat-empty-assist-composer__attachment--media]="attachment.kind === 'image' || attachment.kind === 'video'"
756
+ [class.axm-chat-empty-assist-composer__attachment--file]="attachment.kind === 'audio' || attachment.kind === 'file'"
757
+ [title]="attachment.file.name"
758
+ >
759
+ <!---->
760
+ <!-- ----- Image thumbnail ----- -->
761
+ @if (attachment.kind === 'image' && attachment.preview) {
762
+ <span class="axm-chat-empty-assist-composer__attachment-thumb" aria-hidden="true">
763
+ <img [src]="attachment.preview" alt="" />
764
+ </span>
765
+ }
766
+ <!-- ----- Video thumbnail ----- -->
767
+ @else if (attachment.kind === 'video' && attachment.preview) {
768
+ <span class="axm-chat-empty-assist-composer__attachment-thumb" aria-hidden="true">
769
+ <video [src]="attachment.preview" muted playsinline preload="metadata"></video>
770
+ </span>
771
+ }
772
+ <!-- ----- File name fallback ----- -->
773
+ @else {
774
+ <span class="axm-chat-empty-assist-composer__attachment-icon" aria-hidden="true">
775
+ <i [class]="draftFileIcon(attachment.kind)"></i>
776
+ </span>
777
+ <span class="axm-chat-empty-assist-composer__attachment-name">{{ attachment.file.name }}</span>
778
+ }
779
+ <button
780
+ type="button"
781
+ class="axm-chat-empty-assist-composer__attachment-remove"
782
+ [attr.aria-label]="('@acorex:chat.actions.remove-attachment' | translate | async) ?? ''"
783
+ (click)="removeDraftFile(attachment.id); $event.stopPropagation()"
784
+ >
785
+ <i class="fa-solid fa-xmark"></i>
786
+ </button>
787
+ </div>
788
+ }
789
+ <button
790
+ type="button"
791
+ class="axm-chat-empty-assist-composer__attachment-add"
792
+ [disabled]="isBusy() || draftFiles().length >= maxDraftFiles"
793
+ [attr.aria-label]="('@acorex:chat.pickers.common.add-more' | translate | async) ?? ''"
794
+ (click)="browseDraftFiles()"
795
+ >
796
+ <i class="fa-light fa-plus"></i>
797
+ </button>
798
+ </div>
799
+ </div>
600
800
  }
601
- </ax-button>
602
- </div>
801
+ <div class="axm-chat-empty-assist-composer__input-row">
802
+ <div class="axm-chat-empty-assist-composer__actions-start">
803
+ <ax-button
804
+ class="rounded-full text-xl"
805
+ look="blank"
806
+ [iconOnly]="true"
807
+ [disabled]="isBusy() || draftFiles().length >= maxDraftFiles"
808
+ [text]="('@acorex:chat.tooltips.attach-file' | translate | async) ?? ''"
809
+ (onClick)="browseDraftFiles()"
810
+ >
811
+ <ax-prefix>
812
+ <i class="fa-light fa-paperclip"></i>
813
+ </ax-prefix>
814
+ </ax-button>
815
+ <ax-button
816
+ class="rounded-full axm-chat-composer-assist-picker"
817
+ look="blank"
818
+ [iconOnly]="true"
819
+ [disabled]="isBusy() || rows().length === 0"
820
+ [text]="('@conversation:chat.empty-assist.assist-default' | translate | async) ?? ''"
821
+ >
822
+ <ax-prefix>
823
+ <i class="fa-light fa-sparkles"></i>
824
+ </ax-prefix>
825
+ <!---->
826
+ <!-- ----- Assist list as a dropdown, not a popup ----- -->
827
+ @if (rows().length > 0) {
828
+ <ax-dropdown-panel>
829
+ <axm-chat-composer-assist-picker />
830
+ </ax-dropdown-panel>
831
+ }
832
+ </ax-button>
833
+ </div>
834
+ <textarea
835
+ #draftInput
836
+ rows="1"
837
+ class="axm-chat-empty-assist-composer__textarea"
838
+ [ngModel]="draftText()"
839
+ (ngModelChange)="draftText.set($event ?? '')"
840
+ (input)="resizeDraftInput()"
841
+ (keydown)="onDraftKeydown($event)"
842
+ [disabled]="isBusy()"
843
+ [placeholder]="draftPlaceholder()"
844
+ ></textarea>
845
+ <div class="axm-chat-empty-assist-composer__primary">
846
+ <ax-button
847
+ class="rounded-full"
848
+ [class.composer-primary-button--send]="canSend()"
849
+ look="blank"
850
+ color="primary"
851
+ [iconOnly]="true"
852
+ [disabled]="!canSend()"
853
+ [text]="('@acorex:chat.actions.send' | translate | async) ?? ''"
854
+ (onClick)="send()"
855
+ >
856
+ <ax-prefix>
857
+ <i class="fa-solid fa-paper-plane-top"></i>
858
+ </ax-prefix>
859
+ </ax-button>
860
+ </div>
861
+ </div>
862
+ <input #fileInput class="axm-chat-empty-assist-composer__file-input" type="file" multiple (change)="onDraftFilesSelected($event)" />
863
+ </div>
864
+ }
603
865
 
604
- @if (starterPromptItems().length > 0) {
866
+ <!-- ----- Starter chips loading indicator ----- -->
867
+ @if (chipsLoading()) {
868
+ <div class="axm-chat-empty-assist-composer__chips-loading">
869
+ <ax-loading></ax-loading>
870
+ <p class="axm-chat-empty-assist-composer__suggestions-label">
871
+ {{ '@conversation:chat.contextual-assist.chips-loading' | translate | async }}
872
+ </p>
873
+ </div>
874
+ }
875
+ <!-- ----- Starter suggestion chips ----- -->
876
+ @else if (starterPromptItems().length > 0) {
605
877
  <div class="axm-chat-empty-assist-composer__suggestions">
878
+ <!---->
879
+ <!-- ----- Compact starter section label ----- -->
606
880
  @if (compact()) {
607
881
  <p class="axm-chat-empty-assist-composer__suggestions-label">
608
- {{ '@conversation:chat.starter-prompts.section-label' | translate | async }}
882
+ {{ '@conversation:mini-chat.try-asking' | translate | async }}
609
883
  </p>
610
884
  }
611
- <axm-assist-suggestion-chips [items]="starterPromptItems()" [compact]="compact()" [sendInPlace]="sendStarterPromptsInPlace()" [disabled]="isBusy()" (conversationCreated)="conversationCreated.emit($event)" />
885
+ <axm-assist-suggestion-chips [items]="starterPromptItems()" [compact]="compact()" [sendInPlace]="sendStarterPromptsInPlace()" [disabled]="loading()" (conversationCreated)="conversationCreated.emit($event)" />
612
886
  </div>
613
887
  }
614
888
  </div>
615
- `, 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__hero{display:flex;flex-direction:column;align-items:center;gap:.625rem}.axm-chat-empty-assist-composer__hero-icon{display:grid;place-items:center;width:2.5rem;height:2.5rem;border-radius:.875rem;color:rgb(var(--ax-sys-color-primary-600));background:linear-gradient(145deg,rgba(var(--ax-sys-color-primary-500),.14),rgba(var(--ax-sys-color-primary-500),.04));border:1px solid rgba(var(--ax-sys-color-primary-500),.18)}.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;color:rgb(var(--ax-sys-color-on-surface))}.axm-chat-empty-assist-composer__composer-row{display:flex;gap:.375rem;align-items:flex-end;padding:.5rem;border-radius:2em;border:1px solid rgb(var(--ax-sys-color-border-light-surface));background:rgb(var(--ax-sys-color-lightest-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__send{flex-shrink:0}.axm-chat-empty-assist-composer__suggestions{display:flex;flex-direction:column;gap:.5rem}.axm-chat-empty-assist-composer__suggestions-label{margin:0;padding-inline:.125rem;font-size:.6875rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:rgb(var(--ax-sys-color-on-surface));opacity:.52}.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;text-align:start;color:rgb(var(--ax-sys-color-on-surface))}.axm-chat-empty-assist-composer__textarea::placeholder{color:rgb(var(--ax-sys-color-on-surface));opacity:.45}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer{width:100%;max-width:none;gap:.875rem}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__hero{gap:.5rem}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__caption{margin-bottom:0;font-size:1.125rem;line-height:1.35;font-weight:600;letter-spacing:-.015em}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__composer-row{gap:.25rem;align-items:center;padding-block:.375rem;padding-inline:.375rem .4375rem;border-radius:1.5rem;border-color:rgba(var(--ax-sys-color-border-light-surface),.95);background:rgba(var(--ax-sys-color-surface),.35)}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__assist-trigger{color:rgb(var(--ax-sys-color-primary-600));opacity:.9}: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}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__suggestions{gap:.375rem;margin-top:.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", "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 }); }
889
+ `, isInline: true, styles: [":host{display:block;width:100%}:host(.axm-chat-empty-assist-composer-host--compact){display:flex;height:100%;min-height:0}.axm-chat-empty-assist-composer{display:flex;flex-direction:column;gap:1rem;width:min(100%,64rem);margin-inline:auto}.axm-chat-empty-assist-composer__hero{display:flex;flex-direction:column;align-items:center;gap:.625rem}.axm-chat-empty-assist-composer__hero-icon{display:grid;place-items:center;width:2.5rem;height:2.5rem;border-radius:.875rem;color:rgb(var(--ax-sys-color-primary-600));background:linear-gradient(145deg,rgba(var(--ax-sys-color-primary-500),.14),rgba(var(--ax-sys-color-primary-500),.04));border:1px solid rgba(var(--ax-sys-color-primary-500),.18)}.axm-chat-empty-assist-composer__composer{position:relative;display:flex;flex-direction:column;width:100%;overflow:hidden;border-radius:2em;border:1px solid rgb(var(--ax-sys-color-border-light-surface));background:rgb(var(--ax-sys-color-lightest-surface))}.axm-chat-empty-assist-composer__attachments{display:flex;align-items:center;width:100%;min-width:0;padding:.625rem .75rem .5rem;border-bottom:1px solid rgb(var(--ax-sys-color-border-light-surface))}.axm-chat-empty-assist-composer__attachments-scroll{display:flex;flex:1 1 auto;flex-wrap:nowrap;align-items:center;gap:.5rem;min-width:0;width:0;overflow-x:auto;overflow-y:hidden;padding-block:.5rem;scrollbar-width:thin}.axm-chat-empty-assist-composer__attachment{position:relative;display:flex;flex-shrink:0;align-items:center}.axm-chat-empty-assist-composer__attachment--file{width:11rem;max-width:11rem;height:2.75rem;gap:.5rem;padding-inline:.5rem 1.75rem;border-radius:.5rem;border:1px solid rgb(var(--ax-sys-color-border-light-surface));background:rgb(var(--ax-sys-color-surface))}.axm-chat-empty-assist-composer__attachment-thumb{display:flex;width:2.75rem;height:2.75rem;align-items:center;justify-content:center;overflow:hidden;border-radius:.5rem;border:1px solid rgb(var(--ax-sys-color-border-light-surface));background:rgb(var(--ax-sys-color-surface))}.axm-chat-empty-assist-composer__attachment-thumb img,.axm-chat-empty-assist-composer__attachment-thumb video{width:100%;height:100%;object-fit:cover}.axm-chat-empty-assist-composer__attachment-icon{display:flex;width:1.75rem;height:1.75rem;flex-shrink:0;align-items:center;justify-content:center;font-size:.875rem;color:rgb(var(--ax-sys-color-on-surface));opacity:.8}.axm-chat-empty-assist-composer__attachment-name{min-width:0;flex:1 1 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.8125rem;color:rgb(var(--ax-sys-color-on-surface))}.axm-chat-empty-assist-composer__attachment-remove{position:absolute;inset-block-start:-.35rem;inset-inline-end:-.35rem;z-index:1;display:flex;align-items:center;justify-content:center;width:1.25rem;height:1.25rem;padding:0;border:none;border-radius:9999px;background:rgb(var(--ax-sys-color-danger-500));color:rgb(var(--ax-sys-color-lightest-surface));font-size:.625rem;cursor:pointer}.axm-chat-empty-assist-composer__attachment-add{display:flex;width:2.75rem;height:2.75rem;flex-shrink:0;align-items:center;justify-content:center;padding:0;border-radius:.5rem;border:1px dashed rgb(var(--ax-sys-color-border-light-surface));background:transparent;color:rgb(var(--ax-sys-color-on-surface));font-size:1rem;cursor:pointer}.axm-chat-empty-assist-composer__attachment-add:disabled{cursor:default;opacity:.4}.axm-chat-empty-assist-composer__input-row{display:flex;width:100%;min-width:0;align-items:center;gap:.5rem;padding:.25rem .5rem}.axm-chat-empty-assist-composer__actions-start,.axm-chat-empty-assist-composer__primary{display:flex;flex-shrink:0;align-items:center;gap:.25rem}.axm-chat-empty-assist-composer__actions-start ax-icon i,.axm-chat-empty-assist-composer__actions-start ax-prefix i,.axm-chat-empty-assist-composer__primary ax-prefix i{font-size:1.375rem;line-height:1}.axm-chat-empty-assist-composer__textarea{flex:1 1 auto;min-width:0;min-height:2.25rem;max-height:14rem;margin:0;padding-block:.25rem;border:none;outline:none;resize:none;overflow:hidden;background:transparent;font-size:inherit;line-height:1.5;color:rgb(var(--ax-sys-color-on-surface))}.axm-chat-empty-assist-composer__textarea::placeholder{color:rgb(var(--ax-sys-color-on-surface));opacity:.45}.axm-chat-empty-assist-composer__file-input{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)}.axm-chat-empty-assist-composer__caption{margin-bottom:1.25rem;text-align:center;font-size:clamp(1.9rem,3.4vw,2.7rem);line-height:1.15;font-weight:700;letter-spacing:-.02em;color:rgb(var(--ax-sys-color-on-surface))}.axm-chat-empty-assist-composer__suggestions{display:flex;flex-direction:column;gap:.5rem}.axm-chat-empty-assist-composer__suggestions-label{margin:0;padding-inline:.125rem;font-size:.6875rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:rgb(var(--ax-sys-color-on-surface));opacity:.52}.axm-chat-empty-assist-composer__chips-loading{display:flex;flex-direction:column;align-items:center;gap:.5rem;padding-block:.5rem}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer{width:100%;max-width:none;height:100%;min-height:0;gap:.875rem;justify-content:center;overflow:hidden;padding-block:.5rem .125rem}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__hero{flex-shrink:0;gap:.5rem;padding-block-start:.5rem}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__hero-icon{width:2.5rem;height:2.5rem;border-radius:.875rem;color:rgb(var(--ax-sys-color-primary-600))}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__caption{margin-bottom:0;font-size:1.125rem;line-height:1.3;font-weight:700;letter-spacing:-.03em}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__composer{flex-shrink:0;width:100%;border-radius:1.5rem;border-color:rgba(var(--ax-sys-color-border-light-surface),.95)}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__textarea{min-height:1.625rem;max-height:8rem;font-size:.875rem;line-height:1.4}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__subtitle{margin:0;max-width:20rem;text-align:center;font-size:.8125rem;line-height:1.5;font-weight:400;color:rgb(var(--ax-sys-color-on-surface));opacity:.58}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__suggestions,:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__chips-loading{display:flex;flex-direction:column;flex:0 1 auto;min-height:0;overflow:auto;justify-content:flex-start}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__suggestions{gap:.5rem;margin-top:.125rem}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__suggestions-label{flex-shrink:0;margin-block-end:.125rem;letter-spacing:.02em;text-transform:none;font-size:.75rem;font-weight:500;opacity:.55}:host(.axm-chat-empty-assist-composer-host--compact) axm-assist-suggestion-chips{flex:0 1 auto;min-height:0;overflow:hidden}\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", "tabIndex", "color", "look", "text", "toggleable", "selected", "iconOnly", "type", "loadingText"], outputs: ["onBlur", "onFocus", "onClick", "selectedChange", "toggleableChange", "lookChange", "colorChange", "disabledChange", "loadingTextChange"] }, { kind: "ngmodule", type: AXDecoratorModule }, { kind: "component", type: i3.AXDecoratorIconComponent, selector: "ax-icon", inputs: ["icon"] }, { kind: "component", type: i3.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: "ngmodule", type: AXDropdownModule }, { kind: "component", type: i4.AXDropdownPanelComponent, selector: "ax-dropdown-panel", inputs: ["isOpen", "fitParent", "dropdownWidth", "position", "placement", "_target", "adaptivityEnabled"], outputs: ["onOpened", "onClosed"] }, { kind: "ngmodule", type: AXLoadingModule }, { kind: "component", type: i5.AXLoadingComponent, selector: "ax-loading", inputs: ["visible", "type", "context"], outputs: ["visibleChange"] }, { kind: "ngmodule", type: AXTranslationModule }, { kind: "component", type: AXMAssistSuggestionChipsComponent, selector: "axm-assist-suggestion-chips", inputs: ["items", "compact", "sendInPlace", "disabled"], outputs: ["conversationCreated"] }, { kind: "component", type: AXMChatComposerAssistPickerComponent, selector: "axm-chat-composer-assist-picker", inputs: ["__popup__"] }, { kind: "pipe", type: i6.AsyncPipe, name: "async" }, { kind: "pipe", type: i7.AXTranslatorPipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
616
890
  }
617
891
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMChatEmptyAssistComposerComponent, decorators: [{
618
892
  type: Component,
@@ -621,7 +895,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
621
895
  '[class.axm-chat-empty-assist-composer-host--compact]': 'compact()',
622
896
  '[attr.dir]': 'textDirection()',
623
897
  '[style.direction]': 'textDirection()',
624
- }, imports: [CommonModule, FormsModule, AXButtonModule, AXDropdownModule, AXTextBoxModule, AXDecoratorModule, AXTranslationModule, AXMAssistSuggestionChipsComponent], template: `
898
+ }, imports: [
899
+ CommonModule,
900
+ FormsModule,
901
+ AXButtonModule,
902
+ AXDecoratorModule,
903
+ AXDropdownModule,
904
+ AXLoadingModule,
905
+ AXTranslationModule,
906
+ AXMAssistSuggestionChipsComponent,
907
+ AXMChatComposerAssistPickerComponent,
908
+ ], template: `
625
909
  <div class="axm-chat-empty-assist-composer">
626
910
  <div class="axm-chat-empty-assist-composer__hero">
627
911
  @if (compact()) {
@@ -630,93 +914,163 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
630
914
  </span>
631
915
  }
632
916
  <div class="axm-chat-empty-assist-composer__caption">
633
- {{ '@conversation:chat.empty-assist.caption' | translate | async }}
917
+ {{ (compact() ? '@conversation:mini-chat.empty.title' : '@conversation:chat.empty-assist.caption') | translate | async }}
634
918
  </div>
635
- </div>
636
-
637
- <div class="axm-chat-empty-assist-composer__composer-row">
638
- @if (!compact()) {
639
- <ax-button color="default" look="blank" class="rounded-full" [disabled]="isBusy()">
640
- <ax-icon icon="fa-regular fa-sparkles"></ax-icon>
641
- <span class="axm-chat-empty-assist-composer__assist-text">
642
- @if (selectedAssistLabel(); as assistLabel) {
643
- {{ assistLabel | translate | async }}
644
- } @else {
645
- {{ '@conversation:chat.empty-assist.assist-default' | translate | async }}
646
- }
647
- </span>
648
- @if (!isBusy()) {
649
- <ax-dropdown-panel>
650
- <ax-button-item-list>
651
- @for (row of rows(); track row.id) {
652
- <ax-button-item [text]="(row.title | translate | async)!" [selected]="row.id === selectedAssistId()" (onClick)="onAssistIdChange(row.id)"></ax-button-item>
653
- }
654
- </ax-button-item-list>
655
- </ax-dropdown-panel>
656
- }
657
- </ax-button>
658
- } @else {
659
- <ax-button
660
- color="default"
661
- look="blank"
662
- class="axm-chat-empty-assist-composer__assist-trigger rounded-full"
663
- [iconOnly]="true"
664
- [disabled]="isBusy()"
665
- [attr.aria-label]="('@conversation:chat.empty-assist.assist-default' | translate | async) ?? ''"
666
- >
667
- <ax-icon icon="fa-regular fa-sparkles"></ax-icon>
668
- @if (!isBusy()) {
669
- <ax-dropdown-panel>
670
- <ax-button-item-list>
671
- @for (row of rows(); track row.id) {
672
- <ax-button-item [text]="(row.title | translate | async)!" [selected]="row.id === selectedAssistId()" (onClick)="onAssistIdChange(row.id)"></ax-button-item>
673
- }
674
- </ax-button-item-list>
675
- </ax-dropdown-panel>
676
- }
677
- </ax-button>
919
+ @if (compact()) {
920
+ <p class="axm-chat-empty-assist-composer__subtitle">
921
+ {{ '@conversation:mini-chat.empty.subtitle' | translate | async }}
922
+ </p>
678
923
  }
924
+ </div>
679
925
 
680
- @let assistTitle = (selectedAssistLabel() | translate | async) ?? '';
681
-
682
- <textarea
683
- #draftInput
684
- rows="1"
685
- class="axm-chat-empty-assist-composer__textarea border-none outline-none w-full flex-1"
686
- [ngModel]="draftText()"
687
- (ngModelChange)="draftText.set($event ?? '')"
688
- (input)="resizeDraftInput()"
689
- (keydown)="onDraftKeydown($event)"
690
- [disabled]="isBusy()"
691
- [placeholder]="('@conversation:chat.empty-assist.placeholder' | translate: { params: { assist: assistTitle } } | async) ?? ''"
692
- ></textarea>
693
- <ax-button
694
- color="primary"
695
- class="axm-chat-empty-assist-composer__send rounded-full"
696
- [disabled]="!canSend()"
697
- [text]="sending() ? '…' : ''"
698
- [attr.aria-label]="('@conversation:comments.actions.send.title' | translate | async) ?? ''"
699
- (onClick)="send()"
700
- >
701
- @if (!sending()) {
702
- <ax-icon icon="fa-light fa-arrow-up"></ax-icon>
926
+ <!-- ----- Lookalike composer: attach and type stay local until send ----- -->
927
+ @if (!hasActiveConversation()) {
928
+ <div class="axm-chat-empty-assist-composer__composer">
929
+ <!---->
930
+ <!-- ----- Local attachment chips before the first message is sent ----- -->
931
+ @if (draftFiles().length > 0) {
932
+ <div class="axm-chat-empty-assist-composer__attachments">
933
+ <div class="axm-chat-empty-assist-composer__attachments-scroll" role="list">
934
+ @for (attachment of draftFiles(); track attachment.id) {
935
+ <div
936
+ class="axm-chat-empty-assist-composer__attachment"
937
+ role="listitem"
938
+ [class.axm-chat-empty-assist-composer__attachment--media]="attachment.kind === 'image' || attachment.kind === 'video'"
939
+ [class.axm-chat-empty-assist-composer__attachment--file]="attachment.kind === 'audio' || attachment.kind === 'file'"
940
+ [title]="attachment.file.name"
941
+ >
942
+ <!---->
943
+ <!-- ----- Image thumbnail ----- -->
944
+ @if (attachment.kind === 'image' && attachment.preview) {
945
+ <span class="axm-chat-empty-assist-composer__attachment-thumb" aria-hidden="true">
946
+ <img [src]="attachment.preview" alt="" />
947
+ </span>
948
+ }
949
+ <!-- ----- Video thumbnail ----- -->
950
+ @else if (attachment.kind === 'video' && attachment.preview) {
951
+ <span class="axm-chat-empty-assist-composer__attachment-thumb" aria-hidden="true">
952
+ <video [src]="attachment.preview" muted playsinline preload="metadata"></video>
953
+ </span>
954
+ }
955
+ <!-- ----- File name fallback ----- -->
956
+ @else {
957
+ <span class="axm-chat-empty-assist-composer__attachment-icon" aria-hidden="true">
958
+ <i [class]="draftFileIcon(attachment.kind)"></i>
959
+ </span>
960
+ <span class="axm-chat-empty-assist-composer__attachment-name">{{ attachment.file.name }}</span>
961
+ }
962
+ <button
963
+ type="button"
964
+ class="axm-chat-empty-assist-composer__attachment-remove"
965
+ [attr.aria-label]="('@acorex:chat.actions.remove-attachment' | translate | async) ?? ''"
966
+ (click)="removeDraftFile(attachment.id); $event.stopPropagation()"
967
+ >
968
+ <i class="fa-solid fa-xmark"></i>
969
+ </button>
970
+ </div>
971
+ }
972
+ <button
973
+ type="button"
974
+ class="axm-chat-empty-assist-composer__attachment-add"
975
+ [disabled]="isBusy() || draftFiles().length >= maxDraftFiles"
976
+ [attr.aria-label]="('@acorex:chat.pickers.common.add-more' | translate | async) ?? ''"
977
+ (click)="browseDraftFiles()"
978
+ >
979
+ <i class="fa-light fa-plus"></i>
980
+ </button>
981
+ </div>
982
+ </div>
703
983
  }
704
- </ax-button>
705
- </div>
984
+ <div class="axm-chat-empty-assist-composer__input-row">
985
+ <div class="axm-chat-empty-assist-composer__actions-start">
986
+ <ax-button
987
+ class="rounded-full text-xl"
988
+ look="blank"
989
+ [iconOnly]="true"
990
+ [disabled]="isBusy() || draftFiles().length >= maxDraftFiles"
991
+ [text]="('@acorex:chat.tooltips.attach-file' | translate | async) ?? ''"
992
+ (onClick)="browseDraftFiles()"
993
+ >
994
+ <ax-prefix>
995
+ <i class="fa-light fa-paperclip"></i>
996
+ </ax-prefix>
997
+ </ax-button>
998
+ <ax-button
999
+ class="rounded-full axm-chat-composer-assist-picker"
1000
+ look="blank"
1001
+ [iconOnly]="true"
1002
+ [disabled]="isBusy() || rows().length === 0"
1003
+ [text]="('@conversation:chat.empty-assist.assist-default' | translate | async) ?? ''"
1004
+ >
1005
+ <ax-prefix>
1006
+ <i class="fa-light fa-sparkles"></i>
1007
+ </ax-prefix>
1008
+ <!---->
1009
+ <!-- ----- Assist list as a dropdown, not a popup ----- -->
1010
+ @if (rows().length > 0) {
1011
+ <ax-dropdown-panel>
1012
+ <axm-chat-composer-assist-picker />
1013
+ </ax-dropdown-panel>
1014
+ }
1015
+ </ax-button>
1016
+ </div>
1017
+ <textarea
1018
+ #draftInput
1019
+ rows="1"
1020
+ class="axm-chat-empty-assist-composer__textarea"
1021
+ [ngModel]="draftText()"
1022
+ (ngModelChange)="draftText.set($event ?? '')"
1023
+ (input)="resizeDraftInput()"
1024
+ (keydown)="onDraftKeydown($event)"
1025
+ [disabled]="isBusy()"
1026
+ [placeholder]="draftPlaceholder()"
1027
+ ></textarea>
1028
+ <div class="axm-chat-empty-assist-composer__primary">
1029
+ <ax-button
1030
+ class="rounded-full"
1031
+ [class.composer-primary-button--send]="canSend()"
1032
+ look="blank"
1033
+ color="primary"
1034
+ [iconOnly]="true"
1035
+ [disabled]="!canSend()"
1036
+ [text]="('@acorex:chat.actions.send' | translate | async) ?? ''"
1037
+ (onClick)="send()"
1038
+ >
1039
+ <ax-prefix>
1040
+ <i class="fa-solid fa-paper-plane-top"></i>
1041
+ </ax-prefix>
1042
+ </ax-button>
1043
+ </div>
1044
+ </div>
1045
+ <input #fileInput class="axm-chat-empty-assist-composer__file-input" type="file" multiple (change)="onDraftFilesSelected($event)" />
1046
+ </div>
1047
+ }
706
1048
 
707
- @if (starterPromptItems().length > 0) {
1049
+ <!-- ----- Starter chips loading indicator ----- -->
1050
+ @if (chipsLoading()) {
1051
+ <div class="axm-chat-empty-assist-composer__chips-loading">
1052
+ <ax-loading></ax-loading>
1053
+ <p class="axm-chat-empty-assist-composer__suggestions-label">
1054
+ {{ '@conversation:chat.contextual-assist.chips-loading' | translate | async }}
1055
+ </p>
1056
+ </div>
1057
+ }
1058
+ <!-- ----- Starter suggestion chips ----- -->
1059
+ @else if (starterPromptItems().length > 0) {
708
1060
  <div class="axm-chat-empty-assist-composer__suggestions">
1061
+ <!---->
1062
+ <!-- ----- Compact starter section label ----- -->
709
1063
  @if (compact()) {
710
1064
  <p class="axm-chat-empty-assist-composer__suggestions-label">
711
- {{ '@conversation:chat.starter-prompts.section-label' | translate | async }}
1065
+ {{ '@conversation:mini-chat.try-asking' | translate | async }}
712
1066
  </p>
713
1067
  }
714
- <axm-assist-suggestion-chips [items]="starterPromptItems()" [compact]="compact()" [sendInPlace]="sendStarterPromptsInPlace()" [disabled]="isBusy()" (conversationCreated)="conversationCreated.emit($event)" />
1068
+ <axm-assist-suggestion-chips [items]="starterPromptItems()" [compact]="compact()" [sendInPlace]="sendStarterPromptsInPlace()" [disabled]="loading()" (conversationCreated)="conversationCreated.emit($event)" />
715
1069
  </div>
716
1070
  }
717
1071
  </div>
718
- `, 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__hero{display:flex;flex-direction:column;align-items:center;gap:.625rem}.axm-chat-empty-assist-composer__hero-icon{display:grid;place-items:center;width:2.5rem;height:2.5rem;border-radius:.875rem;color:rgb(var(--ax-sys-color-primary-600));background:linear-gradient(145deg,rgba(var(--ax-sys-color-primary-500),.14),rgba(var(--ax-sys-color-primary-500),.04));border:1px solid rgba(var(--ax-sys-color-primary-500),.18)}.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;color:rgb(var(--ax-sys-color-on-surface))}.axm-chat-empty-assist-composer__composer-row{display:flex;gap:.375rem;align-items:flex-end;padding:.5rem;border-radius:2em;border:1px solid rgb(var(--ax-sys-color-border-light-surface));background:rgb(var(--ax-sys-color-lightest-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__send{flex-shrink:0}.axm-chat-empty-assist-composer__suggestions{display:flex;flex-direction:column;gap:.5rem}.axm-chat-empty-assist-composer__suggestions-label{margin:0;padding-inline:.125rem;font-size:.6875rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:rgb(var(--ax-sys-color-on-surface));opacity:.52}.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;text-align:start;color:rgb(var(--ax-sys-color-on-surface))}.axm-chat-empty-assist-composer__textarea::placeholder{color:rgb(var(--ax-sys-color-on-surface));opacity:.45}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer{width:100%;max-width:none;gap:.875rem}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__hero{gap:.5rem}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__caption{margin-bottom:0;font-size:1.125rem;line-height:1.35;font-weight:600;letter-spacing:-.015em}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__composer-row{gap:.25rem;align-items:center;padding-block:.375rem;padding-inline:.375rem .4375rem;border-radius:1.5rem;border-color:rgba(var(--ax-sys-color-border-light-surface),.95);background:rgba(var(--ax-sys-color-surface),.35)}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__assist-trigger{color:rgb(var(--ax-sys-color-primary-600));opacity:.9}: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}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__suggestions{gap:.375rem;margin-top:.125rem}\n"] }]
719
- }], 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 }] }] } });
1072
+ `, styles: [":host{display:block;width:100%}:host(.axm-chat-empty-assist-composer-host--compact){display:flex;height:100%;min-height:0}.axm-chat-empty-assist-composer{display:flex;flex-direction:column;gap:1rem;width:min(100%,64rem);margin-inline:auto}.axm-chat-empty-assist-composer__hero{display:flex;flex-direction:column;align-items:center;gap:.625rem}.axm-chat-empty-assist-composer__hero-icon{display:grid;place-items:center;width:2.5rem;height:2.5rem;border-radius:.875rem;color:rgb(var(--ax-sys-color-primary-600));background:linear-gradient(145deg,rgba(var(--ax-sys-color-primary-500),.14),rgba(var(--ax-sys-color-primary-500),.04));border:1px solid rgba(var(--ax-sys-color-primary-500),.18)}.axm-chat-empty-assist-composer__composer{position:relative;display:flex;flex-direction:column;width:100%;overflow:hidden;border-radius:2em;border:1px solid rgb(var(--ax-sys-color-border-light-surface));background:rgb(var(--ax-sys-color-lightest-surface))}.axm-chat-empty-assist-composer__attachments{display:flex;align-items:center;width:100%;min-width:0;padding:.625rem .75rem .5rem;border-bottom:1px solid rgb(var(--ax-sys-color-border-light-surface))}.axm-chat-empty-assist-composer__attachments-scroll{display:flex;flex:1 1 auto;flex-wrap:nowrap;align-items:center;gap:.5rem;min-width:0;width:0;overflow-x:auto;overflow-y:hidden;padding-block:.5rem;scrollbar-width:thin}.axm-chat-empty-assist-composer__attachment{position:relative;display:flex;flex-shrink:0;align-items:center}.axm-chat-empty-assist-composer__attachment--file{width:11rem;max-width:11rem;height:2.75rem;gap:.5rem;padding-inline:.5rem 1.75rem;border-radius:.5rem;border:1px solid rgb(var(--ax-sys-color-border-light-surface));background:rgb(var(--ax-sys-color-surface))}.axm-chat-empty-assist-composer__attachment-thumb{display:flex;width:2.75rem;height:2.75rem;align-items:center;justify-content:center;overflow:hidden;border-radius:.5rem;border:1px solid rgb(var(--ax-sys-color-border-light-surface));background:rgb(var(--ax-sys-color-surface))}.axm-chat-empty-assist-composer__attachment-thumb img,.axm-chat-empty-assist-composer__attachment-thumb video{width:100%;height:100%;object-fit:cover}.axm-chat-empty-assist-composer__attachment-icon{display:flex;width:1.75rem;height:1.75rem;flex-shrink:0;align-items:center;justify-content:center;font-size:.875rem;color:rgb(var(--ax-sys-color-on-surface));opacity:.8}.axm-chat-empty-assist-composer__attachment-name{min-width:0;flex:1 1 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.8125rem;color:rgb(var(--ax-sys-color-on-surface))}.axm-chat-empty-assist-composer__attachment-remove{position:absolute;inset-block-start:-.35rem;inset-inline-end:-.35rem;z-index:1;display:flex;align-items:center;justify-content:center;width:1.25rem;height:1.25rem;padding:0;border:none;border-radius:9999px;background:rgb(var(--ax-sys-color-danger-500));color:rgb(var(--ax-sys-color-lightest-surface));font-size:.625rem;cursor:pointer}.axm-chat-empty-assist-composer__attachment-add{display:flex;width:2.75rem;height:2.75rem;flex-shrink:0;align-items:center;justify-content:center;padding:0;border-radius:.5rem;border:1px dashed rgb(var(--ax-sys-color-border-light-surface));background:transparent;color:rgb(var(--ax-sys-color-on-surface));font-size:1rem;cursor:pointer}.axm-chat-empty-assist-composer__attachment-add:disabled{cursor:default;opacity:.4}.axm-chat-empty-assist-composer__input-row{display:flex;width:100%;min-width:0;align-items:center;gap:.5rem;padding:.25rem .5rem}.axm-chat-empty-assist-composer__actions-start,.axm-chat-empty-assist-composer__primary{display:flex;flex-shrink:0;align-items:center;gap:.25rem}.axm-chat-empty-assist-composer__actions-start ax-icon i,.axm-chat-empty-assist-composer__actions-start ax-prefix i,.axm-chat-empty-assist-composer__primary ax-prefix i{font-size:1.375rem;line-height:1}.axm-chat-empty-assist-composer__textarea{flex:1 1 auto;min-width:0;min-height:2.25rem;max-height:14rem;margin:0;padding-block:.25rem;border:none;outline:none;resize:none;overflow:hidden;background:transparent;font-size:inherit;line-height:1.5;color:rgb(var(--ax-sys-color-on-surface))}.axm-chat-empty-assist-composer__textarea::placeholder{color:rgb(var(--ax-sys-color-on-surface));opacity:.45}.axm-chat-empty-assist-composer__file-input{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)}.axm-chat-empty-assist-composer__caption{margin-bottom:1.25rem;text-align:center;font-size:clamp(1.9rem,3.4vw,2.7rem);line-height:1.15;font-weight:700;letter-spacing:-.02em;color:rgb(var(--ax-sys-color-on-surface))}.axm-chat-empty-assist-composer__suggestions{display:flex;flex-direction:column;gap:.5rem}.axm-chat-empty-assist-composer__suggestions-label{margin:0;padding-inline:.125rem;font-size:.6875rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:rgb(var(--ax-sys-color-on-surface));opacity:.52}.axm-chat-empty-assist-composer__chips-loading{display:flex;flex-direction:column;align-items:center;gap:.5rem;padding-block:.5rem}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer{width:100%;max-width:none;height:100%;min-height:0;gap:.875rem;justify-content:center;overflow:hidden;padding-block:.5rem .125rem}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__hero{flex-shrink:0;gap:.5rem;padding-block-start:.5rem}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__hero-icon{width:2.5rem;height:2.5rem;border-radius:.875rem;color:rgb(var(--ax-sys-color-primary-600))}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__caption{margin-bottom:0;font-size:1.125rem;line-height:1.3;font-weight:700;letter-spacing:-.03em}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__composer{flex-shrink:0;width:100%;border-radius:1.5rem;border-color:rgba(var(--ax-sys-color-border-light-surface),.95)}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__textarea{min-height:1.625rem;max-height:8rem;font-size:.875rem;line-height:1.4}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__subtitle{margin:0;max-width:20rem;text-align:center;font-size:.8125rem;line-height:1.5;font-weight:400;color:rgb(var(--ax-sys-color-on-surface));opacity:.58}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__suggestions,:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__chips-loading{display:flex;flex-direction:column;flex:0 1 auto;min-height:0;overflow:auto;justify-content:flex-start}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__suggestions{gap:.5rem;margin-top:.125rem}:host(.axm-chat-empty-assist-composer-host--compact) .axm-chat-empty-assist-composer__suggestions-label{flex-shrink:0;margin-block-end:.125rem;letter-spacing:.02em;text-transform:none;font-size:.75rem;font-weight:500;opacity:.55}:host(.axm-chat-empty-assist-composer-host--compact) axm-assist-suggestion-chips{flex:0 1 auto;min-height:0;overflow:hidden}\n"] }]
1073
+ }], ctorParameters: () => [], propDecorators: { draftInput: [{ type: i0.ViewChild, args: ['draftInput', { isSignal: true }] }], fileInput: [{ type: i0.ViewChild, args: ['fileInput', { isSignal: true }] }], 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 }] }], launchContext: [{ type: i0.Input, args: [{ isSignal: true, alias: "launchContext", required: false }] }], compact: [{ type: i0.Input, args: [{ isSignal: true, alias: "compact", required: false }] }], assistConversationCreated: [{ type: i0.Input, args: [{ isSignal: true, alias: "assistConversationCreated", required: false }] }] } });
720
1074
 
721
1075
  const AXE_CHAT_EMPTY_STATE_PROVIDERS = [
722
1076
  {
@@ -772,6 +1126,7 @@ function coerceTimestampMs(value) {
772
1126
  * AI generation and entity persistence run on the server ({@link AiConversationAssistResponseService}).
773
1127
  */
774
1128
  class AXEConversationAssistResponseService {
1129
+ static { this.ASSIST_SYNC_MIN_INTERVAL_MS = 2000; }
775
1130
  constructor() {
776
1131
  this.transportService = inject(AXPRuntimeTransportService);
777
1132
  this.toastService = inject(AXToastService);
@@ -779,30 +1134,41 @@ class AXEConversationAssistResponseService {
779
1134
  this.store = inject(AXMConversationEntityStoreService);
780
1135
  this.focus = inject(AXMConversationFocusService);
781
1136
  this.commandService = inject(AXPCommandService);
1137
+ this.generationState = inject(AXConversationComposerGenerationStateService);
1138
+ this.destroyRef = inject(DestroyRef);
782
1139
  this.messageCache = new Map();
1140
+ this.forgottenMessageIds = new Set();
783
1141
  this.conversationCache = new Map();
784
1142
  this.streamTasks = new Map();
785
1143
  this.streamReconnectTimers = new Map();
786
1144
  this.lastAssistSyncAt = new Map();
787
1145
  this.currentUserId = '';
788
1146
  this.composerGenerationConversationId = null;
789
- this.cancelAssistRequest = null;
1147
+ this.cancelAssistByConversation = new Map();
1148
+ this.userCancelledConversationIds = new Set();
1149
+ this.generationState.onCancelled.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
1150
+ const conversationId = this.composerGenerationConversationId?.trim()
1151
+ || this.focus.getFocusedConversationId()?.trim()
1152
+ || '';
1153
+ void this.cancelAssistGeneration(conversationId);
1154
+ });
790
1155
  }
791
- static { this.ASSIST_SYNC_MIN_INTERVAL_MS = 2000; }
792
- async handleAfterUserMessage(conversationId, _userMessageId, _command, currentUserId) {
1156
+ async handleAfterUserMessage(conversationId, _userMessageId, _command, currentUserId, options) {
793
1157
  this.currentUserId = currentUserId;
794
1158
  this.ensureAssistStreamSubscription(conversationId);
795
1159
  const scoped = conversationId.trim();
1160
+ this.userCancelledConversationIds.delete(scoped);
796
1161
  if (!this.conversationCache.has(scoped)) {
797
1162
  const storeCached = this.store.getCachedConversation(scoped);
798
1163
  if (storeCached) {
799
1164
  this.conversationCache.set(scoped, storeCached);
800
1165
  }
801
1166
  }
802
- if (this.focus.isFocused(scoped)) {
1167
+ const focusedId = this.focus.getFocusedConversationId();
1168
+ if (focusedId === scoped || !focusedId) {
803
1169
  this.markComposerGenerating(scoped, true);
804
1170
  }
805
- if (!this.streamTasks.has(scoped)) {
1171
+ if (!options?.skipAssistSync && !this.streamTasks.has(scoped)) {
806
1172
  void this.requestAssistSync(conversationId, { force: true });
807
1173
  }
808
1174
  }
@@ -827,6 +1193,7 @@ class AXEConversationAssistResponseService {
827
1193
  this.messageCache.delete(messageId);
828
1194
  }
829
1195
  this.ensureAssistStreamSubscription(conversationId);
1196
+ this.userCancelledConversationIds.delete(conversationId.trim());
830
1197
  if (this.focus.isFocused(conversationId)) {
831
1198
  this.markComposerGenerating(conversationId, true);
832
1199
  }
@@ -919,18 +1286,35 @@ class AXEConversationAssistResponseService {
919
1286
  return streaming.length > 0;
920
1287
  }
921
1288
  mergeLiveConversationMessages(conversationId, persisted) {
922
- const byId = new Map(persisted.map((message) => [message.id, message]));
1289
+ const byId = new Map();
1290
+ for (const message of persisted) {
1291
+ if (message.deleted || this.forgottenMessageIds.has(message.id)) {
1292
+ continue;
1293
+ }
1294
+ byId.set(message.id, message);
1295
+ }
923
1296
  for (const message of this.messageCache.values()) {
924
- if (message.conversationId === conversationId && !message.deleted) {
925
- const existing = byId.get(message.id);
926
- if (existing && axmShouldPreferPersistedAssistOverLiveCache(existing, message)) {
927
- continue;
928
- }
929
- byId.set(message.id, this.withStreamDraftForDisplay(message));
1297
+ if (message.conversationId !== conversationId || message.deleted || this.forgottenMessageIds.has(message.id)) {
1298
+ continue;
1299
+ }
1300
+ const existing = byId.get(message.id);
1301
+ if (existing && axmShouldPreferPersistedAssistOverLiveCache(existing, message)) {
1302
+ continue;
930
1303
  }
1304
+ byId.set(message.id, this.withStreamDraftForDisplay(message));
931
1305
  }
932
1306
  return [...byId.values()].sort((a, b) => axmCompareMessagesChronologically(a, b));
933
1307
  }
1308
+ forgetLiveMessages(messageIds) {
1309
+ for (const rawId of messageIds) {
1310
+ const messageId = rawId.trim();
1311
+ if (!messageId) {
1312
+ continue;
1313
+ }
1314
+ this.forgottenMessageIds.add(messageId);
1315
+ this.messageCache.delete(messageId);
1316
+ }
1317
+ }
934
1318
  ensureAssistStreamSubscription(conversationId) {
935
1319
  const key = conversationId.trim();
936
1320
  if (!key || this.streamTasks.has(key)) {
@@ -994,6 +1378,9 @@ class AXEConversationAssistResponseService {
994
1378
  async handleAssistWireEvent(event) {
995
1379
  switch (event.type) {
996
1380
  case 'assist_started': {
1381
+ if (this.shouldIgnoreStreamingAfterUserCancel(event.conversationId)) {
1382
+ break;
1383
+ }
997
1384
  void this.markUserMessageSeenByAssist(event.conversationId, event.userMessageId);
998
1385
  const mapped = axmMessageEntityToMessage(event.message);
999
1386
  this.applyLiveMessage(mapped, { isNew: !this.messageCache.has(mapped.id) });
@@ -1002,12 +1389,16 @@ class AXEConversationAssistResponseService {
1002
1389
  }
1003
1390
  case 'message_patch': {
1004
1391
  const mapped = axmMessageEntityToMessage(event.message);
1392
+ const status = axmReadAssistResponseStatus(mapped.metadata);
1393
+ if (status === 'streaming' && this.shouldIgnoreStreamingAfterUserCancel(mapped.conversationId)) {
1394
+ break;
1395
+ }
1005
1396
  const isNew = !this.messageCache.has(mapped.id);
1006
1397
  this.applyLiveMessage(mapped, { isNew });
1007
- if (axmReadAssistResponseStatus(mapped.metadata) === 'streaming') {
1398
+ if (status === 'streaming') {
1008
1399
  this.syncComposerGeneratingForConversation(mapped.conversationId, true);
1009
1400
  }
1010
- else if (axmIsAssistTerminalStatus(axmReadAssistResponseStatus(mapped.metadata))) {
1401
+ else if (axmIsAssistTerminalStatus(status)) {
1011
1402
  this.syncComposerGeneratingForConversation(mapped.conversationId, false);
1012
1403
  }
1013
1404
  break;
@@ -1021,6 +1412,10 @@ class AXEConversationAssistResponseService {
1021
1412
  }
1022
1413
  case 'assist_completed': {
1023
1414
  const mapped = axmMessageEntityToMessage(event.message);
1415
+ if (this.shouldIgnoreStreamingAfterUserCancel(mapped.conversationId)) {
1416
+ this.syncComposerGeneratingForConversation(mapped.conversationId, false);
1417
+ break;
1418
+ }
1024
1419
  this.applyLiveMessage(mapped, { isNew: false });
1025
1420
  void this.maybeMarkFocusedIncomingRead(mapped);
1026
1421
  if (event.room) {
@@ -1033,13 +1428,17 @@ class AXEConversationAssistResponseService {
1033
1428
  case 'assist_failed': {
1034
1429
  const mapped = axmMessageEntityToMessage(event.message);
1035
1430
  this.applyLiveMessage(mapped, { isNew: false });
1036
- if (this.focus.isFocused(mapped.conversationId)) {
1431
+ if (this.focus.isFocused(mapped.conversationId) && axmReadAssistResponseStatus(mapped.metadata) === 'failed') {
1037
1432
  void this.showAssistToast('@conversation:chat.assist.errors.title', event.error);
1038
1433
  }
1039
1434
  this.syncComposerGeneratingForConversation(mapped.conversationId, false);
1040
1435
  break;
1041
1436
  }
1042
1437
  case 'frontend_tool_call': {
1438
+ const cancelId = this.composerGenerationConversationId;
1439
+ if (cancelId && this.shouldIgnoreStreamingAfterUserCancel(cancelId)) {
1440
+ break;
1441
+ }
1043
1442
  await this.submitFrontendToolResult(event.runId, event.call);
1044
1443
  break;
1045
1444
  }
@@ -1049,7 +1448,13 @@ class AXEConversationAssistResponseService {
1049
1448
  }
1050
1449
  async submitFrontendToolResult(runId, call) {
1051
1450
  try {
1052
- await this.transportService.execute(AXMAiManagementTransportKeys.Engine.SubmitToolResult, { runId, call });
1451
+ const result = await this.transportService.execute(AXMAiManagementTransportKeys.Engine.SubmitToolResult, {
1452
+ runId,
1453
+ call,
1454
+ });
1455
+ if (result && typeof result === 'object' && 'ok' in result && result.ok === false) {
1456
+ return;
1457
+ }
1053
1458
  }
1054
1459
  catch (error) {
1055
1460
  const text = error instanceof Error ? error.message : String(error);
@@ -1081,10 +1486,10 @@ class AXEConversationAssistResponseService {
1081
1486
  }
1082
1487
  const assistId = axmExtractAssistIdFromMetadata(metadata);
1083
1488
  const modelEntityId = typeof metadata['modelEntityId'] === 'string' && metadata['modelEntityId'].trim() ? metadata['modelEntityId'].trim() : undefined;
1084
- const result = await this.commandService.execute(AIMANAGEMENT_STRUCTURED_TEXT_COMPLETION_COMMAND_KEY, {
1489
+ const result = await this.commandService.execute(AIMANAGEMENT_STRUCTURED_TEXT_COMPLETION_COMMAND_KEY$1, {
1085
1490
  assistId,
1086
1491
  modelEntityId,
1087
- messages: [axpAiChatTextMessage('system', AXM_ASSIST_CONVERSATION_TITLE_SYSTEM_PROMPT), axpAiChatTextMessage('user', scopedText)],
1492
+ messages: [axpAiChatTextMessage$1('system', AXM_ASSIST_CONVERSATION_TITLE_SYSTEM_PROMPT), axpAiChatTextMessage$1('user', scopedText)],
1088
1493
  maxTokens: 48,
1089
1494
  temperature: 0.3,
1090
1495
  });
@@ -1160,14 +1565,14 @@ class AXEConversationAssistResponseService {
1160
1565
  if (!scopedId) {
1161
1566
  return;
1162
1567
  }
1163
- const message = this.messageCache.get(scopedId) ?? (await this.store.getMessage(scopedId));
1568
+ const message = this.store.getCachedMessage(scopedId) ?? this.messageCache.get(scopedId);
1164
1569
  if (!message || message.conversationId !== conversationId) {
1165
1570
  return;
1166
1571
  }
1167
1572
  if (message.senderId !== this.currentUserId || message.status === 'read') {
1168
1573
  return;
1169
1574
  }
1170
- await this.store.updateMessage({ ...message, status: 'read' });
1575
+ this.store.publishMessageUpdate({ ...message, status: 'read' });
1171
1576
  }
1172
1577
  async maybeMarkFocusedIncomingRead(message) {
1173
1578
  const conversationId = message.conversationId?.trim();
@@ -1184,6 +1589,20 @@ class AXEConversationAssistResponseService {
1184
1589
  });
1185
1590
  }
1186
1591
  applyLiveMessage(message, options) {
1592
+ if (message.deleted || this.forgottenMessageIds.has(message.id)) {
1593
+ this.forgottenMessageIds.add(message.id);
1594
+ this.messageCache.delete(message.id);
1595
+ this.store.publishMessageDeletion(message.id);
1596
+ return;
1597
+ }
1598
+ this.forgottenMessageIds.delete(message.id);
1599
+ if (this.shouldIgnoreStaleUserTextPatch(message)) {
1600
+ return;
1601
+ }
1602
+ if (axmReadAssistResponseStatus(message.metadata) === 'streaming'
1603
+ && this.shouldIgnoreStreamingAfterUserCancel(message.conversationId)) {
1604
+ return;
1605
+ }
1187
1606
  const normalized = this.coerceLaggyStreamingTerminal(message);
1188
1607
  if (this.shouldIgnoreStaleStreamingDowngrade(normalized)) {
1189
1608
  return;
@@ -1240,6 +1659,30 @@ class AXEConversationAssistResponseService {
1240
1659
  delete nextMeta['streamDraftThink'];
1241
1660
  return { ...message, metadata: nextMeta };
1242
1661
  }
1662
+ /**
1663
+ * After a user-text edit, entity cache / status-seen persist can emit the pre-edit payload.
1664
+ * Keep the newer local bubble (and never put the old text into the live cache).
1665
+ */
1666
+ shouldIgnoreStaleUserTextPatch(incoming) {
1667
+ if (incoming.metadata?.['isAssistResponse'] === true || incoming.type === 'assist-response') {
1668
+ return false;
1669
+ }
1670
+ if (incoming.type !== 'text') {
1671
+ return false;
1672
+ }
1673
+ const local = this.store.getCachedMessage(incoming.id) ?? this.messageCache.get(incoming.id);
1674
+ if (!local) {
1675
+ return false;
1676
+ }
1677
+ const localText = extractMessageTextFromPayload(local.payload);
1678
+ const incomingText = extractMessageTextFromPayload(incoming.payload);
1679
+ if (localText === incomingText) {
1680
+ return false;
1681
+ }
1682
+ const localEdited = coerceTimestampMs(local.editedAt);
1683
+ const incomingEdited = coerceTimestampMs(incoming.editedAt);
1684
+ return localEdited > 0 && incomingEdited < localEdited + 5000;
1685
+ }
1243
1686
  /** Ignore out-of-order streaming patches after a terminal assist state was applied. */
1244
1687
  shouldIgnoreStaleStreamingDowngrade(incoming) {
1245
1688
  if (axmReadAssistResponseStatus(incoming.metadata) !== 'streaming') {
@@ -1462,21 +1905,40 @@ class AXEConversationAssistResponseService {
1462
1905
  return;
1463
1906
  }
1464
1907
  const hasStreaming = await this.hasStreamingAssistPlaceholders(scoped);
1908
+ if (this.userCancelledConversationIds.has(scoped)) {
1909
+ this.markComposerGenerating(scoped, false);
1910
+ return;
1911
+ }
1912
+ if (hasStreaming && this.shouldDeferGeneratingTrue(scoped)) {
1913
+ return;
1914
+ }
1465
1915
  this.markComposerGenerating(scoped, hasStreaming);
1466
1916
  }
1467
1917
  async cancelAssistGeneration(conversationId) {
1468
- if (this.cancelAssistRequest) {
1469
- return this.cancelAssistRequest;
1918
+ const scoped = conversationId.trim()
1919
+ || this.composerGenerationConversationId?.trim()
1920
+ || this.focus.getFocusedConversationId()?.trim()
1921
+ || this.resolveActiveStreamingConversationId();
1922
+ if (!scoped) {
1923
+ this.markComposerGenerating(null, false);
1924
+ return;
1925
+ }
1926
+ this.userCancelledConversationIds.add(scoped);
1927
+ this.freezeLiveStreamingAsUserCancelled(scoped);
1928
+ const existing = this.cancelAssistByConversation.get(scoped);
1929
+ if (existing) {
1930
+ return existing;
1470
1931
  }
1471
- this.cancelAssistRequest = this.runCancelAssistGeneration(conversationId).finally(() => {
1472
- this.cancelAssistRequest = null;
1932
+ const pending = this.runCancelAssistGeneration(scoped).finally(() => {
1933
+ if (this.cancelAssistByConversation.get(scoped) === pending) {
1934
+ this.cancelAssistByConversation.delete(scoped);
1935
+ }
1473
1936
  });
1474
- return this.cancelAssistRequest;
1937
+ this.cancelAssistByConversation.set(scoped, pending);
1938
+ return pending;
1475
1939
  }
1476
1940
  async runCancelAssistGeneration(conversationId) {
1477
- const scoped = conversationId.trim()
1478
- || this.composerGenerationConversationId?.trim()
1479
- || this.resolveActiveStreamingConversationId();
1941
+ const scoped = conversationId.trim();
1480
1942
  if (!scoped) {
1481
1943
  this.markComposerGenerating(null, false);
1482
1944
  return;
@@ -1487,32 +1949,68 @@ class AXEConversationAssistResponseService {
1487
1949
  });
1488
1950
  await this.requestAssistSync(scoped, { force: true });
1489
1951
  await this.syncAssistMessagesFromStore(scoped);
1952
+ this.markComposerGenerating(scoped, false);
1490
1953
  }
1491
1954
  catch (error) {
1492
1955
  const text = error instanceof Error ? error.message : String(error);
1493
1956
  if (this.focus.isFocused(scoped)) {
1494
1957
  void this.showAssistToast('@conversation:chat.assist.errors.stream-title', text);
1495
1958
  }
1959
+ this.userCancelledConversationIds.delete(scoped);
1960
+ const hasStreaming = await this.hasStreamingAssistPlaceholders(scoped);
1961
+ this.markComposerGenerating(scoped, hasStreaming);
1496
1962
  }
1497
- finally {
1498
- this.markComposerGenerating(scoped, false);
1963
+ }
1964
+ freezeLiveStreamingAsUserCancelled(conversationId) {
1965
+ for (const message of [...this.messageCache.values()]) {
1966
+ if (message.conversationId !== conversationId) {
1967
+ continue;
1968
+ }
1969
+ if (axmReadAssistResponseStatus(message.metadata) !== 'streaming') {
1970
+ continue;
1971
+ }
1972
+ const meta = { ...(message.metadata ?? {}) };
1973
+ meta['isStreaming'] = false;
1974
+ meta['assistResponseStatus'] = 'incomplete';
1975
+ meta['assistResponseError'] = 'Cancelled by user.';
1976
+ this.applyLiveMessage({ ...message, metadata: meta }, { isNew: false });
1499
1977
  }
1500
1978
  }
1979
+ shouldIgnoreStreamingAfterUserCancel(conversationId) {
1980
+ const scoped = conversationId.trim();
1981
+ return this.userCancelledConversationIds.has(scoped) || this.cancelAssistByConversation.has(scoped);
1982
+ }
1501
1983
  markComposerGenerating(conversationId, generating) {
1502
1984
  if (generating && conversationId) {
1503
1985
  this.composerGenerationConversationId = conversationId;
1986
+ this.generationState.setGenerating(true);
1504
1987
  return;
1505
1988
  }
1506
1989
  if (!generating) {
1990
+ if (conversationId &&
1991
+ this.composerGenerationConversationId &&
1992
+ this.composerGenerationConversationId !== conversationId) {
1993
+ return;
1994
+ }
1507
1995
  this.composerGenerationConversationId = null;
1996
+ this.generationState.setGenerating(false);
1508
1997
  }
1509
1998
  }
1510
1999
  syncComposerGeneratingForConversation(conversationId, generating) {
1511
2000
  if (!this.focus.isFocused(conversationId)) {
1512
2001
  return;
1513
2002
  }
2003
+ if (generating && this.shouldDeferGeneratingTrue(conversationId)) {
2004
+ return;
2005
+ }
1514
2006
  this.markComposerGenerating(conversationId, generating);
1515
2007
  }
2008
+ /** Stream patches must not reset `stopping` back to `generating` while cancel is in flight. */
2009
+ shouldDeferGeneratingTrue(conversationId) {
2010
+ return (this.cancelAssistByConversation.has(conversationId)
2011
+ || this.userCancelledConversationIds.has(conversationId)
2012
+ || this.generationState.isStopping());
2013
+ }
1516
2014
  resolveActiveStreamingConversationId() {
1517
2015
  for (const message of this.messageCache.values()) {
1518
2016
  if (axmReadAssistResponseStatus(message.metadata) === 'streaming') {
@@ -1526,7 +2024,7 @@ class AXEConversationAssistResponseService {
1526
2024
  }
1527
2025
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEConversationAssistResponseService, decorators: [{
1528
2026
  type: Injectable
1529
- }] });
2027
+ }], ctorParameters: () => [] });
1530
2028
  /** @deprecated Use {@link AXEConversationAssistResponseService}. */
1531
2029
  const AXCConversationAssistResponseService = AXEConversationAssistResponseService;
1532
2030
 
@@ -1764,7 +2262,7 @@ class AXMChatWithAssistDialogComponent extends AXBasePageComponent {
1764
2262
  </div>
1765
2263
  }
1766
2264
  </ng-template>
1767
- `, 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", "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", "showTitle", "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 }); }
2265
+ `, 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", "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", "showTitle", "fallbackText", "placeholder", "loading"] }, { 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: i6.AsyncPipe, name: "async" }, { kind: "pipe", type: i7.AXTranslatorPipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1768
2266
  }
1769
2267
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMChatWithAssistDialogComponent, decorators: [{
1770
2268
  type: Component,
@@ -1877,6 +2375,56 @@ const AXM_CHAT_SIDEBAR_ASSIST_ACTION_PROVIDERS = [
1877
2375
  },
1878
2376
  ];
1879
2377
 
2378
+ //#region ----- Imports -----
2379
+ //#endregion
2380
+ //#region ----- Service -----
2381
+ /**
2382
+ * Opens the shared mini-chat overlay with caller-supplied assist context.
2383
+ * Same panel as the header inbox so streaming and route persistence stay aligned.
2384
+ */
2385
+ class AXMContextualAssistLauncherService {
2386
+ constructor() {
2387
+ //#region ----- Services & Dependencies -----
2388
+ this.panelService = inject(AXMMiniChatPanelService);
2389
+ }
2390
+ //#endregion
2391
+ //#region ----- Public API -----
2392
+ isOpen() {
2393
+ return this.panelService.isOpen();
2394
+ }
2395
+ async open(context, options) {
2396
+ const launchContext = axmSanitizeContextualAssistContext(context);
2397
+ const assistId = options?.assistId?.trim();
2398
+ const entityRef = axmEntityRefFromLaunchContext(launchContext);
2399
+ await this.panelService.open({
2400
+ ...(entityRef
2401
+ ? { scope: 'entity', entityId: entityRef.entityId, entityType: entityRef.entityType }
2402
+ : { scope: 'inbox' }),
2403
+ forceNewChat: options?.forceNewChat ?? true,
2404
+ showInfoBar: false,
2405
+ showComposer: true,
2406
+ launchContext,
2407
+ ...(assistId ? { assistId } : {}),
2408
+ });
2409
+ }
2410
+ /** Closes the panel when it is open; otherwise opens it (restores the last thread). */
2411
+ async toggle(context, options) {
2412
+ if (this.isOpen()) {
2413
+ this.close();
2414
+ return;
2415
+ }
2416
+ await this.open(context, { ...options, forceNewChat: false });
2417
+ }
2418
+ close() {
2419
+ this.panelService.close();
2420
+ }
2421
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMContextualAssistLauncherService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2422
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMContextualAssistLauncherService }); }
2423
+ }
2424
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXMContextualAssistLauncherService, decorators: [{
2425
+ type: Injectable
2426
+ }] });
2427
+
1880
2428
  class AXEAIConversationExtensionModule {
1881
2429
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAIConversationExtensionModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
1882
2430
  static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "22.0.8", ngImport: i0, type: AXEAIConversationExtensionModule, imports: [i1$1.AXPComponentSlotModule] }); }
@@ -1904,6 +2452,7 @@ class AXEAIConversationExtensionModule {
1904
2452
  ...AXE_MINI_CHAT_INBOX_FILTER_PROVIDERS,
1905
2453
  ...AXE_ASSIST_DEFAULT_RESOLVER_PROVIDERS,
1906
2454
  ...AXM_CHAT_SIDEBAR_ASSIST_ACTION_PROVIDERS,
2455
+ AXMContextualAssistLauncherService,
1907
2456
  provideAppStartUpTask(() => {
1908
2457
  const launcher = inject(AXMChatAssistLauncherService);
1909
2458
  return {
@@ -1917,11 +2466,11 @@ class AXEAIConversationExtensionModule {
1917
2466
  provideCommandSetups([
1918
2467
  {
1919
2468
  key: CONVERSATION_START_ASSIST_CHAT_COMMAND,
1920
- command: () => import('./acorex-platform-extension-ai-conversation-client-start-assist-chat.command-qYtaBoTP.mjs').then((m) => m.AXMConversationStartAssistChatCommand),
2469
+ command: () => import('./acorex-platform-extension-ai-conversation-client-start-assist-chat.command-CQM2377i.mjs').then((m) => m.AXMConversationStartAssistChatCommand),
1921
2470
  },
1922
2471
  {
1923
2472
  key: CONVERSATION_SEND_ASSIST_CHAT_MESSAGE_COMMAND,
1924
- command: () => import('./acorex-platform-extension-ai-conversation-client-send-assist-chat-message.command-B3rJWdsz.mjs').then((m) => m.AXMConversationSendAssistChatMessageCommand),
2473
+ command: () => import('./acorex-platform-extension-ai-conversation-client-send-assist-chat-message.command-DIwe0Kes.mjs').then((m) => m.AXMConversationSendAssistChatMessageCommand),
1925
2474
  },
1926
2475
  ]),
1927
2476
  ], imports: [AXPComponentSlotModule.forChild({
@@ -1932,6 +2481,13 @@ class AXEAIConversationExtensionModule {
1932
2481
  component: AXMMiniChatHeaderSlotComponent,
1933
2482
  },
1934
2483
  ],
2484
+ 'root-footer-end': [
2485
+ {
2486
+ priority: 5,
2487
+ name: 'conversation-mini-chat-dock',
2488
+ component: AXMMiniChatFooterDockComponent,
2489
+ },
2490
+ ],
1935
2491
  })] }); }
1936
2492
  }
1937
2493
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAIConversationExtensionModule, decorators: [{
@@ -1946,6 +2502,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
1946
2502
  component: AXMMiniChatHeaderSlotComponent,
1947
2503
  },
1948
2504
  ],
2505
+ 'root-footer-end': [
2506
+ {
2507
+ priority: 5,
2508
+ name: 'conversation-mini-chat-dock',
2509
+ component: AXMMiniChatFooterDockComponent,
2510
+ },
2511
+ ],
1949
2512
  }),
1950
2513
  ],
1951
2514
  providers: [
@@ -1972,6 +2535,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
1972
2535
  ...AXE_MINI_CHAT_INBOX_FILTER_PROVIDERS,
1973
2536
  ...AXE_ASSIST_DEFAULT_RESOLVER_PROVIDERS,
1974
2537
  ...AXM_CHAT_SIDEBAR_ASSIST_ACTION_PROVIDERS,
2538
+ AXMContextualAssistLauncherService,
1975
2539
  provideAppStartUpTask(() => {
1976
2540
  const launcher = inject(AXMChatAssistLauncherService);
1977
2541
  return {
@@ -1985,11 +2549,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
1985
2549
  provideCommandSetups([
1986
2550
  {
1987
2551
  key: CONVERSATION_START_ASSIST_CHAT_COMMAND,
1988
- command: () => import('./acorex-platform-extension-ai-conversation-client-start-assist-chat.command-qYtaBoTP.mjs').then((m) => m.AXMConversationStartAssistChatCommand),
2552
+ command: () => import('./acorex-platform-extension-ai-conversation-client-start-assist-chat.command-CQM2377i.mjs').then((m) => m.AXMConversationStartAssistChatCommand),
1989
2553
  },
1990
2554
  {
1991
2555
  key: CONVERSATION_SEND_ASSIST_CHAT_MESSAGE_COMMAND,
1992
- command: () => import('./acorex-platform-extension-ai-conversation-client-send-assist-chat-message.command-B3rJWdsz.mjs').then((m) => m.AXMConversationSendAssistChatMessageCommand),
2556
+ command: () => import('./acorex-platform-extension-ai-conversation-client-send-assist-chat-message.command-DIwe0Kes.mjs').then((m) => m.AXMConversationSendAssistChatMessageCommand),
1993
2557
  },
1994
2558
  ]),
1995
2559
  ],
@@ -2000,5 +2564,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
2000
2564
  * Generated bundle index. Do not edit.
2001
2565
  */
2002
2566
 
2003
- export { AXMChatAssistLauncherService as A, AXCConversationAssistResponseService as a, AXEAIConversationExtensionModule as b, AXEConversationAssistResponseService as c };
2004
- //# sourceMappingURL=acorex-platform-extension-ai-conversation-client-acorex-platform-extension-ai-conversation-client-CRjLZIU5.mjs.map
2567
+ export { AXMChatAssistLauncherService as A, AXCConversationAssistResponseService as a, AXEAIConversationExtensionModule as b, AXEConversationAssistResponseService as c, AXMContextualAssistLauncherService as d };
2568
+ //# sourceMappingURL=acorex-platform-extension-ai-conversation-client-acorex-platform-extension-ai-conversation-client-CTr53paj.mjs.map