mesauth-angular 1.18.0 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { signal, Injectable, InjectionToken, makeEnvironmentProviders, provideAppInitializer, inject, NgModule, afterNextRender, input, booleanAttribute, computed, HostBinding, Component, output, effect, HostListener, ViewChild, ElementRef, DestroyRef, Directive } from '@angular/core';
2
+ import { signal, Injectable, InjectionToken, makeEnvironmentProviders, provideAppInitializer, inject, NgModule, afterNextRender, input, booleanAttribute, computed, HostBinding, Component, output, effect, HostListener, Injector, Pipe, viewChild, ViewChild, ElementRef, DestroyRef, Directive } from '@angular/core';
3
3
  import { toObservable, toSignal, takeUntilDestroyed, rxResource } from '@angular/core/rxjs-interop';
4
4
  import { catchError, of, Subject, EMPTY, timer, throwError, firstValueFrom, distinctUntilChanged, switchMap as switchMap$1, forkJoin } from 'rxjs';
5
5
  import { HttpClient, HttpResponse } from '@angular/common/http';
@@ -7,10 +7,10 @@ import { HubConnectionBuilder, LogLevel } from '@microsoft/signalr';
7
7
  import { map, tap, catchError as catchError$1, switchMap } from 'rxjs/operators';
8
8
  import { Router } from '@angular/router';
9
9
  import { DomSanitizer } from '@angular/platform-browser';
10
- import { DatePipe } from '@angular/common';
10
+ import { DatePipe, JsonPipe } from '@angular/common';
11
11
 
12
12
  /** Current installed package version — keep in sync with package.json. */
13
- const PACKAGE_VERSION = '1.18.0';
13
+ const PACKAGE_VERSION = '1.19.0';
14
14
  /**
15
15
  * Provides server-driven UI configuration loaded from the hosted manifest.
16
16
  * Components read `labels()` and `features()` signals instead of hardcoded strings.
@@ -25,6 +25,7 @@ class MaUiConfigService {
25
25
  }, ...(ngDevMode ? [{ debugName: "_features" }] : /* istanbul ignore next */ []));
26
26
  _updateRequired = signal(false, ...(ngDevMode ? [{ debugName: "_updateRequired" }] : /* istanbul ignore next */ []));
27
27
  _manifestVersion = signal(null, ...(ngDevMode ? [{ debugName: "_manifestVersion" }] : /* istanbul ignore next */ []));
28
+ _quickPrompts = signal([], ...(ngDevMode ? [{ debugName: "_quickPrompts" }] : /* istanbul ignore next */ []));
28
29
  /** Reactive map of UI string overrides from the server manifest. */
29
30
  labels = this._labels.asReadonly();
30
31
  /** Reactive map of feature flags from the server manifest. */
@@ -33,6 +34,8 @@ class MaUiConfigService {
33
34
  updateRequired = this._updateRequired.asReadonly();
34
35
  /** The manifest version string loaded from the server, or null if not yet loaded. */
35
36
  manifestVersion = this._manifestVersion.asReadonly();
37
+ /** AI panel "quick prompts" — populated from the manifest if provided, else empty. */
38
+ quickPrompts = this._quickPrompts.asReadonly();
36
39
  /**
37
40
  * Returns a label by key, falling back to `defaultValue` when the server
38
41
  * manifest has not provided an override.
@@ -64,6 +67,8 @@ class MaUiConfigService {
64
67
  this._updateRequired.set(true);
65
68
  if (manifest.version)
66
69
  this._manifestVersion.set(manifest.version);
70
+ if (manifest.quickPrompts)
71
+ this._quickPrompts.set(manifest.quickPrompts);
67
72
  // Version-check: warn dev team when a newer package is available
68
73
  const latest = manifest.latestPackageVersion;
69
74
  if (latest && latest !== PACKAGE_VERSION) {
@@ -693,15 +698,90 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImpo
693
698
  args: ['class']
694
699
  }] } });
695
700
 
701
+ const MES_AUTH_AI_CONFIG = new InjectionToken('MES_AUTH_AI_CONFIG');
702
+ /** Default configuration when the consumer doesn't call provideMesAuthAi(). */
703
+ const DEFAULT_AI_CONFIG = {
704
+ enabled: true,
705
+ tools: [],
706
+ systemPromptExtensions: [],
707
+ appName: undefined
708
+ };
709
+ /**
710
+ * Provide AI assistant configuration to mesauth-angular.
711
+ *
712
+ * @example
713
+ * ```ts
714
+ * providers: [
715
+ * provideMesAuth(...),
716
+ * provideMesAuthAi({
717
+ * enabled: true,
718
+ * appName: 'MesExtensionSite',
719
+ * tools: [
720
+ * {
721
+ * name: 'spc_open_chart',
722
+ * description: 'Open the SPC chart for a given product line',
723
+ * parameters: {
724
+ * type: 'object',
725
+ * properties: { lineId: { type: 'string' } },
726
+ * required: ['lineId']
727
+ * },
728
+ * readOnly: true,
729
+ * handler: (args) => inject(Router).navigateByUrl(`/spc/${args.lineId}`)
730
+ * }
731
+ * ]
732
+ * })
733
+ * ]
734
+ * ```
735
+ */
736
+ function provideMesAuthAi(config) {
737
+ return makeEnvironmentProviders([
738
+ { provide: MES_AUTH_AI_CONFIG, useValue: { ...DEFAULT_AI_CONFIG, ...config } }
739
+ ]);
740
+ }
741
+
742
+ class MaAiButtonComponent {
743
+ config = inject(MES_AUTH_AI_CONFIG, { optional: true }) ?? DEFAULT_AI_CONFIG;
744
+ enabled = computed(() => this.config.enabled !== false, ...(ngDevMode ? [{ debugName: "enabled" }] : /* istanbul ignore next */ []));
745
+ clicked = output();
746
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
747
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: MaAiButtonComponent, isStandalone: true, selector: "ma-ai-button", outputs: { clicked: "clicked" }, ngImport: i0, template: `
748
+ @if (enabled()) {
749
+ <button class="ai-btn" (click)="clicked.emit()" title="Ask AI" aria-label="Ask AI">
750
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
751
+ fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
752
+ <path d="M12 2 L13.5 7.5 L19 9 L13.5 10.5 L12 16 L10.5 10.5 L5 9 L10.5 7.5 Z"/>
753
+ <path d="M19 16 L19.8 18.2 L22 19 L19.8 19.8 L19 22 L18.2 19.8 L16 19 L18.2 18.2 Z"/>
754
+ </svg>
755
+ </button>
756
+ }
757
+ `, isInline: true, styles: [":host{display:inline-flex}.ai-btn{background:none;border:none;cursor:pointer;color:var(--cui-body-color, #6e6e9a);width:36px;height:36px;border-radius:8px;display:inline-flex;align-items:center;justify-content:center;transition:background .15s,color .15s}.ai-btn:hover{background:var(--cui-tertiary-bg, rgba(120,120,200,.12));color:#7c3aed}\n"] });
758
+ }
759
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiButtonComponent, decorators: [{
760
+ type: Component,
761
+ args: [{ selector: 'ma-ai-button', template: `
762
+ @if (enabled()) {
763
+ <button class="ai-btn" (click)="clicked.emit()" title="Ask AI" aria-label="Ask AI">
764
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
765
+ fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
766
+ <path d="M12 2 L13.5 7.5 L19 9 L13.5 10.5 L12 16 L10.5 10.5 L5 9 L10.5 7.5 Z"/>
767
+ <path d="M19 16 L19.8 18.2 L22 19 L19.8 19.8 L19 22 L18.2 19.8 L16 19 L18.2 18.2 Z"/>
768
+ </svg>
769
+ </button>
770
+ }
771
+ `, styles: [":host{display:inline-flex}.ai-btn{background:none;border:none;cursor:pointer;color:var(--cui-body-color, #6e6e9a);width:36px;height:36px;border-radius:8px;display:inline-flex;align-items:center;justify-content:center;transition:background .15s,color .15s}.ai-btn:hover{background:var(--cui-tertiary-bg, rgba(120,120,200,.12));color:#7c3aed}\n"] }]
772
+ }], propDecorators: { clicked: [{ type: i0.Output, args: ["clicked"] }] } });
773
+
696
774
  class UserProfileComponent {
697
775
  inputAvatarShape = input('circle', ...(ngDevMode ? [{ debugName: "inputAvatarShape" }] : /* istanbul ignore next */ []));
698
776
  showBell = input(true, ...(ngDevMode ? [{ debugName: "showBell" }] : /* istanbul ignore next */ []));
699
777
  showApproval = input(true, ...(ngDevMode ? [{ debugName: "showApproval" }] : /* istanbul ignore next */ []));
700
778
  showName = input(false, ...(ngDevMode ? [{ debugName: "showName" }] : /* istanbul ignore next */ []));
779
+ showAi = input(true, ...(ngDevMode ? [{ debugName: "showAi" }] : /* istanbul ignore next */ []));
701
780
  showSignature = input([false, false], ...(ngDevMode ? [{ debugName: "showSignature" }] : /* istanbul ignore next */ []));
702
781
  signatureHeight = input(40, ...(ngDevMode ? [{ debugName: "signatureHeight" }] : /* istanbul ignore next */ []));
703
782
  notificationClick = output();
704
783
  approvalClick = output();
784
+ aiClick = output();
705
785
  get themeClass() {
706
786
  return `theme-${this.themeService.currentTheme()}`;
707
787
  }
@@ -783,6 +863,9 @@ class UserProfileComponent {
783
863
  onApprovalClick() {
784
864
  this.approvalClick.emit();
785
865
  }
866
+ onAiClick() {
867
+ this.aiClick.emit();
868
+ }
786
869
  getAvatarUrl(user) {
787
870
  // Use the refresh signal to force update
788
871
  const refresh = this.avatarRefresh();
@@ -870,12 +953,12 @@ class UserProfileComponent {
870
953
  this.signatureBroken.set(true);
871
954
  }
872
955
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: UserProfileComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
873
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: UserProfileComponent, isStandalone: true, selector: "ma-user-profile", inputs: { inputAvatarShape: { classPropertyName: "inputAvatarShape", publicName: "inputAvatarShape", isSignal: true, isRequired: false, transformFunction: null }, showBell: { classPropertyName: "showBell", publicName: "showBell", isSignal: true, isRequired: false, transformFunction: null }, showApproval: { classPropertyName: "showApproval", publicName: "showApproval", isSignal: true, isRequired: false, transformFunction: null }, showName: { classPropertyName: "showName", publicName: "showName", isSignal: true, isRequired: false, transformFunction: null }, showSignature: { classPropertyName: "showSignature", publicName: "showSignature", isSignal: true, isRequired: false, transformFunction: null }, signatureHeight: { classPropertyName: "signatureHeight", publicName: "signatureHeight", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { notificationClick: "notificationClick", approvalClick: "approvalClick" }, host: { listeners: { "document:click": "onDocumentClick($event)" }, properties: { "class": "this.themeClass" } }, ngImport: i0, template: "<div class=\"user-profile-container\">\n @if (!currentUser()) {\n <!-- Not logged in -->\n <button class=\"login-btn\" (click)=\"onLogin()\">Login</button>\n } @else {\n <!-- Logged in -->\n <div class=\"user-header\">\n <!-- Notification Bell -->\n @if (showBell()) {\n <button class=\"notification-btn\" [class.has-unread]=\"unreadCount() > 0\" (click)=\"onNotificationClick()\" title=\"Notifications\" aria-label=\"Notifications\">\n <svg class=\"bell-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9\"/>\n <path d=\"M13.73 21a2 2 0 0 1-3.46 0\"/>\n </svg>\n @if (unreadCount() > 0) {\n <span class=\"badge\">{{ unreadCount() > 99 ? '99+' : unreadCount() }}</span>\n }\n </button>\n }\n \n\n <!-- Approval Button -->\n @if (showApproval()) {\n <button class=\"notification-btn\" [class.has-unread]=\"pendingApprovalCount() > 0\" (click)=\"onApprovalClick()\" title=\"Approvals\" aria-label=\"Approvals\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M9 11l3 3L22 4\"/>\n <path d=\"M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11\"/>\n </svg>\n @if (pendingApprovalCount() > 0) {\n <span class=\"badge\">{{ pendingApprovalCount() > 99 ? '99+' : pendingApprovalCount() }}</span>\n }\n </button>\n } \n\n <!-- User Avatar + Dropdown -->\n <div class=\"user-menu-wrapper\">\n <button class=\"user-menu-btn\" (click)=\"toggleDropdown()\" [attr.aria-label]=\"'User menu for ' + (currentUser().fullName || currentUser().userName)\" aria-haspopup=\"true\" [attr.aria-expanded]=\"dropdownOpen()\">\n <ma-avatar\n [src]=\"getAvatarUrl(currentUser())\"\n [alt]=\"currentUser().fullName || currentUser().userName || ''\"\n [initials]=\"getLastNameInitial(currentUser())\"\n [size]=\"navAvatarSize()\"\n [shape]=\"avatarShape()\"\n [ratio]=\"avatarRatio()\"\n [frame]=\"avatarFrame()\" \n [ring]=\"true\"\n [ringActive]=\"dropdownOpen()\" /> \n </button>\n\n @if (dropdownOpen()) {\n <div class=\"mes-dropdown-menu\">\n <!-- User info header -->\n <div class=\"mes-dropdown-header\">\n <div class=\"dropdown-avatar-wrap\">\n <ma-avatar\n [src]=\"getAvatarUrl(currentUser())\"\n [alt]=\"currentUser().fullName || currentUser().userName || ''\"\n [initials]=\"getLastNameInitial(currentUser())\"\n [size]=\"dropAvatarSize()\"\n [shape]=\"avatarShape()\"\n [ratio]=\"avatarRatio()\"\n [frame]=\"avatarFrame()\"\n [scale]=\"1.5\" /> \n </div> \n <div class=\"dropdown-info-col\">\n <div class=\"dropdown-user-info\">\n <span class=\"dropdown-user-name\">{{ currentUser().fullName || currentUser().userName }}</span> \n <span class=\"dropdown-user-sub\">\n @if (currentUser().position || currentUser().department) {\n {{ currentUser().position || currentUser().department }} \n }\n @if (currentUser().givenTitle) {\n <span class=\"given-title-badge given-title\"\n [class]=\"'given-color-' + givenStyle()\">{{ currentUser().givenTitle }}</span>\n }\n </span>\n </div>\n <div class=\"dropdown-user-actions\">\n @if (showSignature()[1] && signatureUrl(); as sigUrl) {\n <img class=\"ma-ux-signature\"\n [src]=\"sigUrl\"\n [style.height.px]=\"signatureHeight()\"\n (error)=\"onSigErr($event)\"\n alt=\"signature\" />\n }\n <button class=\"icon-action profile-link\" (click)=\"onViewProfile()\" title=\"View Profile\" aria-label=\"View Profile\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2\"/><circle cx=\"12\" cy=\"7\" r=\"4\"/>\n </svg>\n </button>\n <button class=\"icon-action logout-item\" (click)=\"onLogout()\" title=\"Logout\" aria-label=\"Logout\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\"/><polyline points=\"16 17 21 12 16 7\"/><line x1=\"21\" y1=\"12\" x2=\"9\" y2=\"12\"/>\n </svg>\n </button>\n </div>\n </div>\n </div>\n\n <div class=\"mes-dropdown-items\">\n <ng-content></ng-content>\n </div>\n </div>\n }\n </div>\n\n @if (showName()){\n <div class=\"mes-user-header\">\n {{currentUser().fullName || currentUser().userName}}\n </div>\n }\n @if (showSignature()[0] && signatureUrl(); as sigUrl) {\n <img class=\"ma-ux-signature\"\n [src]=\"sigUrl\"\n [style.height.px]=\"signatureHeight()\"\n (error)=\"onSigErr($event)\"\n alt=\"signature\" />\n }\n </div>\n }\n</div>\n", styles: [".user-profile-container{display:flex;align-items:center;gap:4px}.login-btn{padding:7px 18px;background-color:var(--primary-color);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:500;font-size:13px;letter-spacing:.2px;transition:background-color .2s,transform .15s}.login-btn:hover{background-color:var(--primary-hover);transform:translateY(-1px)}.user-header{display:flex;align-items:center;gap:4px}.mes-user-header{margin-left:10px;font-weight:600;font-size:14px;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.notification-btn{position:relative;background:none;border:none;cursor:pointer;padding:8px;border-radius:10px;color:var(--text-secondary);display:flex;align-items:center;justify-content:center;transition:color .2s,background-color .2s}.notification-btn:hover{background-color:var(--primary-light);color:var(--primary-color)}.notification-btn.has-unread{color:var(--primary-color)}.bell-icon{display:block;transition:transform .35s cubic-bezier(.34,1.56,.64,1)}.notification-btn:hover .bell-icon{transform:rotate(-20deg) scale(1.15)}.badge{position:absolute;top:2px;right:2px;background-color:var(--error-color);color:#fff;border-radius:10px;min-width:17px;height:17px;padding:0 4px;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;line-height:1;box-shadow:0 0 0 2px var(--bg-primary);animation:badge-pop .25s cubic-bezier(.34,1.56,.64,1)}@keyframes badge-pop{0%{transform:scale(0)}to{transform:scale(1)}}.user-menu-wrapper{position:relative}.user-menu-btn{background:none;border:none;cursor:pointer;padding:4px;display:flex;align-items:center;justify-content:center;transition:transform .2s}.user-menu-btn:hover{transform:scale(1.06)}.mes-dropdown-menu{position:absolute;top:calc(100% + 10px);right:0;background:var(--bg-primary);border:1px solid var(--border-color);border-radius:14px;box-shadow:0 8px 32px var(--shadow-lg),0 2px 8px var(--shadow);min-width:220px;z-index:1000;overflow:hidden;animation:dropdown-in .16s cubic-bezier(.16,1,.3,1)}@keyframes dropdown-in{0%{opacity:0;transform:translateY(-8px) scale(.96)}to{opacity:1;transform:translateY(0) scale(1)}}.mes-dropdown-header{display:flex;align-items:center;gap:12px;padding:16px;background:var(--bg-secondary)}.dropdown-avatar-wrap{flex-shrink:0}.mes-dropdown-header .dropdown-avatar-wrap{margin-right:5px}.dropdown-user-info{display:flex;flex-direction:column;gap:3px;min-width:0}.dropdown-user-name{font-weight:600;font-size:14px;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:5px;min-width:180px}.dropdown-user-sub{font-size:11px;color:var(--primary-color);font-weight:500;white-space:nowrap;text-overflow:ellipsis;margin-bottom:12px}.given-title{float:inline-end}.mes-dropdown-divider{height:1px;background:var(--border-color)}.dropdown-info-col{display:flex;flex-direction:column;gap:6px;min-width:0;flex:1}.dropdown-user-actions{display:flex;align-items:center;justify-content:flex-end;gap:4px}.icon-action{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border:none;background:none;border-radius:8px;cursor:pointer;color:var(--text-secondary);transition:background-color .15s,color .15s,transform .15s}.icon-action:hover{transform:translateY(-1px)}.icon-action.profile-link{color:var(--primary-color)}.icon-action.profile-link:hover{background-color:var(--primary-light)}.icon-action.logout-item{color:var(--error-color)}.icon-action.logout-item:hover{background-color:var(--error-light)}.mes-dropdown-items:not(:empty){border-top:1px solid var(--border-color)}.ma-ux-signature{object-fit:contain;max-width:160px;vertical-align:middle;background:transparent}\n"], dependencies: [{ kind: "component", type: MaAvatarComponent, selector: "ma-avatar", inputs: ["src", "alt", "initials", "size", "shape", "frame", "ratio", "scale", "ring", "ringActive"] }] });
956
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: UserProfileComponent, isStandalone: true, selector: "ma-user-profile", inputs: { inputAvatarShape: { classPropertyName: "inputAvatarShape", publicName: "inputAvatarShape", isSignal: true, isRequired: false, transformFunction: null }, showBell: { classPropertyName: "showBell", publicName: "showBell", isSignal: true, isRequired: false, transformFunction: null }, showApproval: { classPropertyName: "showApproval", publicName: "showApproval", isSignal: true, isRequired: false, transformFunction: null }, showName: { classPropertyName: "showName", publicName: "showName", isSignal: true, isRequired: false, transformFunction: null }, showAi: { classPropertyName: "showAi", publicName: "showAi", isSignal: true, isRequired: false, transformFunction: null }, showSignature: { classPropertyName: "showSignature", publicName: "showSignature", isSignal: true, isRequired: false, transformFunction: null }, signatureHeight: { classPropertyName: "signatureHeight", publicName: "signatureHeight", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { notificationClick: "notificationClick", approvalClick: "approvalClick", aiClick: "aiClick" }, host: { listeners: { "document:click": "onDocumentClick($event)" }, properties: { "class": "this.themeClass" } }, ngImport: i0, template: "<div class=\"user-profile-container\">\n @if (!currentUser()) {\n <!-- Not logged in -->\n <button class=\"login-btn\" (click)=\"onLogin()\">Login</button>\n } @else {\n <!-- Logged in -->\n <div class=\"user-header\">\n <!-- Ask AI -->\n @if (showAi()) {\n <ma-ai-button (clicked)=\"onAiClick()\"></ma-ai-button>\n }\n\n <!-- Notification Bell -->\n @if (showBell()) {\n <button class=\"notification-btn\" [class.has-unread]=\"unreadCount() > 0\" (click)=\"onNotificationClick()\" title=\"Notifications\" aria-label=\"Notifications\">\n <svg class=\"bell-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9\"/>\n <path d=\"M13.73 21a2 2 0 0 1-3.46 0\"/>\n </svg>\n @if (unreadCount() > 0) {\n <span class=\"badge\">{{ unreadCount() > 99 ? '99+' : unreadCount() }}</span>\n }\n </button>\n }\n \n\n <!-- Approval Button -->\n @if (showApproval()) {\n <button class=\"notification-btn\" [class.has-unread]=\"pendingApprovalCount() > 0\" (click)=\"onApprovalClick()\" title=\"Approvals\" aria-label=\"Approvals\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M9 11l3 3L22 4\"/>\n <path d=\"M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11\"/>\n </svg>\n @if (pendingApprovalCount() > 0) {\n <span class=\"badge\">{{ pendingApprovalCount() > 99 ? '99+' : pendingApprovalCount() }}</span>\n }\n </button>\n } \n\n <!-- User Avatar + Dropdown -->\n <div class=\"user-menu-wrapper\">\n <button class=\"user-menu-btn\" (click)=\"toggleDropdown()\" [attr.aria-label]=\"'User menu for ' + (currentUser().fullName || currentUser().userName)\" aria-haspopup=\"true\" [attr.aria-expanded]=\"dropdownOpen()\">\n <ma-avatar\n [src]=\"getAvatarUrl(currentUser())\"\n [alt]=\"currentUser().fullName || currentUser().userName || ''\"\n [initials]=\"getLastNameInitial(currentUser())\"\n [size]=\"navAvatarSize()\"\n [shape]=\"avatarShape()\"\n [ratio]=\"avatarRatio()\"\n [frame]=\"avatarFrame()\" \n [ring]=\"true\"\n [ringActive]=\"dropdownOpen()\" /> \n </button>\n\n @if (dropdownOpen()) {\n <div class=\"mes-dropdown-menu\">\n <!-- User info header -->\n <div class=\"mes-dropdown-header\">\n <div class=\"dropdown-avatar-wrap\">\n <ma-avatar\n [src]=\"getAvatarUrl(currentUser())\"\n [alt]=\"currentUser().fullName || currentUser().userName || ''\"\n [initials]=\"getLastNameInitial(currentUser())\"\n [size]=\"dropAvatarSize()\"\n [shape]=\"avatarShape()\"\n [ratio]=\"avatarRatio()\"\n [frame]=\"avatarFrame()\"\n [scale]=\"1.5\" /> \n </div> \n <div class=\"dropdown-info-col\">\n <div class=\"dropdown-user-info\">\n <span class=\"dropdown-user-name\">{{ currentUser().fullName || currentUser().userName }}</span> \n <span class=\"dropdown-user-sub\">\n @if (currentUser().position || currentUser().department) {\n {{ currentUser().position || currentUser().department }} \n }\n @if (currentUser().givenTitle) {\n <span class=\"given-title-badge given-title\"\n [class]=\"'given-color-' + givenStyle()\">{{ currentUser().givenTitle }}</span>\n }\n </span>\n </div>\n <div class=\"dropdown-user-actions\">\n @if (showSignature()[1] && signatureUrl(); as sigUrl) {\n <img class=\"ma-ux-signature\"\n [src]=\"sigUrl\"\n [style.height.px]=\"signatureHeight()\"\n (error)=\"onSigErr($event)\"\n alt=\"signature\" />\n }\n <button class=\"icon-action profile-link\" (click)=\"onViewProfile()\" title=\"View Profile\" aria-label=\"View Profile\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2\"/><circle cx=\"12\" cy=\"7\" r=\"4\"/>\n </svg>\n </button>\n <button class=\"icon-action logout-item\" (click)=\"onLogout()\" title=\"Logout\" aria-label=\"Logout\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\"/><polyline points=\"16 17 21 12 16 7\"/><line x1=\"21\" y1=\"12\" x2=\"9\" y2=\"12\"/>\n </svg>\n </button>\n </div>\n </div>\n </div>\n\n <div class=\"mes-dropdown-items\">\n <ng-content></ng-content>\n </div>\n </div>\n }\n </div>\n\n @if (showName()){\n <div class=\"mes-user-header\">\n {{currentUser().fullName || currentUser().userName}}\n </div>\n }\n @if (showSignature()[0] && signatureUrl(); as sigUrl) {\n <img class=\"ma-ux-signature\"\n [src]=\"sigUrl\"\n [style.height.px]=\"signatureHeight()\"\n (error)=\"onSigErr($event)\"\n alt=\"signature\" />\n }\n </div>\n }\n</div>\n", styles: [".user-profile-container{display:flex;align-items:center;gap:4px}.login-btn{padding:7px 18px;background-color:var(--primary-color);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:500;font-size:13px;letter-spacing:.2px;transition:background-color .2s,transform .15s}.login-btn:hover{background-color:var(--primary-hover);transform:translateY(-1px)}.user-header{display:flex;align-items:center;gap:4px}.mes-user-header{margin-left:10px;font-weight:600;font-size:14px;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.notification-btn{position:relative;background:none;border:none;cursor:pointer;padding:8px;border-radius:10px;color:var(--text-secondary);display:flex;align-items:center;justify-content:center;transition:color .2s,background-color .2s}.notification-btn:hover{background-color:var(--primary-light);color:var(--primary-color)}.notification-btn.has-unread{color:var(--primary-color)}.bell-icon{display:block;transition:transform .35s cubic-bezier(.34,1.56,.64,1)}.notification-btn:hover .bell-icon{transform:rotate(-20deg) scale(1.15)}.badge{position:absolute;top:2px;right:2px;background-color:var(--error-color);color:#fff;border-radius:10px;min-width:17px;height:17px;padding:0 4px;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;line-height:1;box-shadow:0 0 0 2px var(--bg-primary);animation:badge-pop .25s cubic-bezier(.34,1.56,.64,1)}@keyframes badge-pop{0%{transform:scale(0)}to{transform:scale(1)}}.user-menu-wrapper{position:relative}.user-menu-btn{background:none;border:none;cursor:pointer;padding:4px;display:flex;align-items:center;justify-content:center;transition:transform .2s}.user-menu-btn:hover{transform:scale(1.06)}.mes-dropdown-menu{position:absolute;top:calc(100% + 10px);right:0;background:var(--bg-primary);border:1px solid var(--border-color);border-radius:14px;box-shadow:0 8px 32px var(--shadow-lg),0 2px 8px var(--shadow);min-width:220px;z-index:1000;overflow:hidden;animation:dropdown-in .16s cubic-bezier(.16,1,.3,1)}@keyframes dropdown-in{0%{opacity:0;transform:translateY(-8px) scale(.96)}to{opacity:1;transform:translateY(0) scale(1)}}.mes-dropdown-header{display:flex;align-items:center;gap:12px;padding:16px;background:var(--bg-secondary)}.dropdown-avatar-wrap{flex-shrink:0}.mes-dropdown-header .dropdown-avatar-wrap{margin-right:5px}.dropdown-user-info{display:flex;flex-direction:column;gap:3px;min-width:0}.dropdown-user-name{font-weight:600;font-size:14px;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:5px;min-width:180px}.dropdown-user-sub{font-size:11px;color:var(--primary-color);font-weight:500;white-space:nowrap;text-overflow:ellipsis;margin-bottom:12px}.given-title{float:inline-end}.mes-dropdown-divider{height:1px;background:var(--border-color)}.dropdown-info-col{display:flex;flex-direction:column;gap:6px;min-width:0;flex:1}.dropdown-user-actions{display:flex;align-items:center;justify-content:flex-end;gap:4px}.icon-action{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border:none;background:none;border-radius:8px;cursor:pointer;color:var(--text-secondary);transition:background-color .15s,color .15s,transform .15s}.icon-action:hover{transform:translateY(-1px)}.icon-action.profile-link{color:var(--primary-color)}.icon-action.profile-link:hover{background-color:var(--primary-light)}.icon-action.logout-item{color:var(--error-color)}.icon-action.logout-item:hover{background-color:var(--error-light)}.mes-dropdown-items:not(:empty){border-top:1px solid var(--border-color)}.ma-ux-signature{object-fit:contain;max-width:160px;vertical-align:middle;background:transparent}\n"], dependencies: [{ kind: "component", type: MaAvatarComponent, selector: "ma-avatar", inputs: ["src", "alt", "initials", "size", "shape", "frame", "ratio", "scale", "ring", "ringActive"] }, { kind: "component", type: MaAiButtonComponent, selector: "ma-ai-button", outputs: ["clicked"] }] });
874
957
  }
875
958
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: UserProfileComponent, decorators: [{
876
959
  type: Component,
877
- args: [{ selector: 'ma-user-profile', imports: [MaAvatarComponent], template: "<div class=\"user-profile-container\">\n @if (!currentUser()) {\n <!-- Not logged in -->\n <button class=\"login-btn\" (click)=\"onLogin()\">Login</button>\n } @else {\n <!-- Logged in -->\n <div class=\"user-header\">\n <!-- Notification Bell -->\n @if (showBell()) {\n <button class=\"notification-btn\" [class.has-unread]=\"unreadCount() > 0\" (click)=\"onNotificationClick()\" title=\"Notifications\" aria-label=\"Notifications\">\n <svg class=\"bell-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9\"/>\n <path d=\"M13.73 21a2 2 0 0 1-3.46 0\"/>\n </svg>\n @if (unreadCount() > 0) {\n <span class=\"badge\">{{ unreadCount() > 99 ? '99+' : unreadCount() }}</span>\n }\n </button>\n }\n \n\n <!-- Approval Button -->\n @if (showApproval()) {\n <button class=\"notification-btn\" [class.has-unread]=\"pendingApprovalCount() > 0\" (click)=\"onApprovalClick()\" title=\"Approvals\" aria-label=\"Approvals\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M9 11l3 3L22 4\"/>\n <path d=\"M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11\"/>\n </svg>\n @if (pendingApprovalCount() > 0) {\n <span class=\"badge\">{{ pendingApprovalCount() > 99 ? '99+' : pendingApprovalCount() }}</span>\n }\n </button>\n } \n\n <!-- User Avatar + Dropdown -->\n <div class=\"user-menu-wrapper\">\n <button class=\"user-menu-btn\" (click)=\"toggleDropdown()\" [attr.aria-label]=\"'User menu for ' + (currentUser().fullName || currentUser().userName)\" aria-haspopup=\"true\" [attr.aria-expanded]=\"dropdownOpen()\">\n <ma-avatar\n [src]=\"getAvatarUrl(currentUser())\"\n [alt]=\"currentUser().fullName || currentUser().userName || ''\"\n [initials]=\"getLastNameInitial(currentUser())\"\n [size]=\"navAvatarSize()\"\n [shape]=\"avatarShape()\"\n [ratio]=\"avatarRatio()\"\n [frame]=\"avatarFrame()\" \n [ring]=\"true\"\n [ringActive]=\"dropdownOpen()\" /> \n </button>\n\n @if (dropdownOpen()) {\n <div class=\"mes-dropdown-menu\">\n <!-- User info header -->\n <div class=\"mes-dropdown-header\">\n <div class=\"dropdown-avatar-wrap\">\n <ma-avatar\n [src]=\"getAvatarUrl(currentUser())\"\n [alt]=\"currentUser().fullName || currentUser().userName || ''\"\n [initials]=\"getLastNameInitial(currentUser())\"\n [size]=\"dropAvatarSize()\"\n [shape]=\"avatarShape()\"\n [ratio]=\"avatarRatio()\"\n [frame]=\"avatarFrame()\"\n [scale]=\"1.5\" /> \n </div> \n <div class=\"dropdown-info-col\">\n <div class=\"dropdown-user-info\">\n <span class=\"dropdown-user-name\">{{ currentUser().fullName || currentUser().userName }}</span> \n <span class=\"dropdown-user-sub\">\n @if (currentUser().position || currentUser().department) {\n {{ currentUser().position || currentUser().department }} \n }\n @if (currentUser().givenTitle) {\n <span class=\"given-title-badge given-title\"\n [class]=\"'given-color-' + givenStyle()\">{{ currentUser().givenTitle }}</span>\n }\n </span>\n </div>\n <div class=\"dropdown-user-actions\">\n @if (showSignature()[1] && signatureUrl(); as sigUrl) {\n <img class=\"ma-ux-signature\"\n [src]=\"sigUrl\"\n [style.height.px]=\"signatureHeight()\"\n (error)=\"onSigErr($event)\"\n alt=\"signature\" />\n }\n <button class=\"icon-action profile-link\" (click)=\"onViewProfile()\" title=\"View Profile\" aria-label=\"View Profile\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2\"/><circle cx=\"12\" cy=\"7\" r=\"4\"/>\n </svg>\n </button>\n <button class=\"icon-action logout-item\" (click)=\"onLogout()\" title=\"Logout\" aria-label=\"Logout\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\"/><polyline points=\"16 17 21 12 16 7\"/><line x1=\"21\" y1=\"12\" x2=\"9\" y2=\"12\"/>\n </svg>\n </button>\n </div>\n </div>\n </div>\n\n <div class=\"mes-dropdown-items\">\n <ng-content></ng-content>\n </div>\n </div>\n }\n </div>\n\n @if (showName()){\n <div class=\"mes-user-header\">\n {{currentUser().fullName || currentUser().userName}}\n </div>\n }\n @if (showSignature()[0] && signatureUrl(); as sigUrl) {\n <img class=\"ma-ux-signature\"\n [src]=\"sigUrl\"\n [style.height.px]=\"signatureHeight()\"\n (error)=\"onSigErr($event)\"\n alt=\"signature\" />\n }\n </div>\n }\n</div>\n", styles: [".user-profile-container{display:flex;align-items:center;gap:4px}.login-btn{padding:7px 18px;background-color:var(--primary-color);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:500;font-size:13px;letter-spacing:.2px;transition:background-color .2s,transform .15s}.login-btn:hover{background-color:var(--primary-hover);transform:translateY(-1px)}.user-header{display:flex;align-items:center;gap:4px}.mes-user-header{margin-left:10px;font-weight:600;font-size:14px;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.notification-btn{position:relative;background:none;border:none;cursor:pointer;padding:8px;border-radius:10px;color:var(--text-secondary);display:flex;align-items:center;justify-content:center;transition:color .2s,background-color .2s}.notification-btn:hover{background-color:var(--primary-light);color:var(--primary-color)}.notification-btn.has-unread{color:var(--primary-color)}.bell-icon{display:block;transition:transform .35s cubic-bezier(.34,1.56,.64,1)}.notification-btn:hover .bell-icon{transform:rotate(-20deg) scale(1.15)}.badge{position:absolute;top:2px;right:2px;background-color:var(--error-color);color:#fff;border-radius:10px;min-width:17px;height:17px;padding:0 4px;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;line-height:1;box-shadow:0 0 0 2px var(--bg-primary);animation:badge-pop .25s cubic-bezier(.34,1.56,.64,1)}@keyframes badge-pop{0%{transform:scale(0)}to{transform:scale(1)}}.user-menu-wrapper{position:relative}.user-menu-btn{background:none;border:none;cursor:pointer;padding:4px;display:flex;align-items:center;justify-content:center;transition:transform .2s}.user-menu-btn:hover{transform:scale(1.06)}.mes-dropdown-menu{position:absolute;top:calc(100% + 10px);right:0;background:var(--bg-primary);border:1px solid var(--border-color);border-radius:14px;box-shadow:0 8px 32px var(--shadow-lg),0 2px 8px var(--shadow);min-width:220px;z-index:1000;overflow:hidden;animation:dropdown-in .16s cubic-bezier(.16,1,.3,1)}@keyframes dropdown-in{0%{opacity:0;transform:translateY(-8px) scale(.96)}to{opacity:1;transform:translateY(0) scale(1)}}.mes-dropdown-header{display:flex;align-items:center;gap:12px;padding:16px;background:var(--bg-secondary)}.dropdown-avatar-wrap{flex-shrink:0}.mes-dropdown-header .dropdown-avatar-wrap{margin-right:5px}.dropdown-user-info{display:flex;flex-direction:column;gap:3px;min-width:0}.dropdown-user-name{font-weight:600;font-size:14px;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:5px;min-width:180px}.dropdown-user-sub{font-size:11px;color:var(--primary-color);font-weight:500;white-space:nowrap;text-overflow:ellipsis;margin-bottom:12px}.given-title{float:inline-end}.mes-dropdown-divider{height:1px;background:var(--border-color)}.dropdown-info-col{display:flex;flex-direction:column;gap:6px;min-width:0;flex:1}.dropdown-user-actions{display:flex;align-items:center;justify-content:flex-end;gap:4px}.icon-action{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border:none;background:none;border-radius:8px;cursor:pointer;color:var(--text-secondary);transition:background-color .15s,color .15s,transform .15s}.icon-action:hover{transform:translateY(-1px)}.icon-action.profile-link{color:var(--primary-color)}.icon-action.profile-link:hover{background-color:var(--primary-light)}.icon-action.logout-item{color:var(--error-color)}.icon-action.logout-item:hover{background-color:var(--error-light)}.mes-dropdown-items:not(:empty){border-top:1px solid var(--border-color)}.ma-ux-signature{object-fit:contain;max-width:160px;vertical-align:middle;background:transparent}\n"] }]
878
- }], ctorParameters: () => [], propDecorators: { inputAvatarShape: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputAvatarShape", required: false }] }], showBell: [{ type: i0.Input, args: [{ isSignal: true, alias: "showBell", required: false }] }], showApproval: [{ type: i0.Input, args: [{ isSignal: true, alias: "showApproval", required: false }] }], showName: [{ type: i0.Input, args: [{ isSignal: true, alias: "showName", required: false }] }], showSignature: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSignature", required: false }] }], signatureHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "signatureHeight", required: false }] }], notificationClick: [{ type: i0.Output, args: ["notificationClick"] }], approvalClick: [{ type: i0.Output, args: ["approvalClick"] }], themeClass: [{
960
+ args: [{ selector: 'ma-user-profile', imports: [MaAvatarComponent, MaAiButtonComponent], template: "<div class=\"user-profile-container\">\n @if (!currentUser()) {\n <!-- Not logged in -->\n <button class=\"login-btn\" (click)=\"onLogin()\">Login</button>\n } @else {\n <!-- Logged in -->\n <div class=\"user-header\">\n <!-- Ask AI -->\n @if (showAi()) {\n <ma-ai-button (clicked)=\"onAiClick()\"></ma-ai-button>\n }\n\n <!-- Notification Bell -->\n @if (showBell()) {\n <button class=\"notification-btn\" [class.has-unread]=\"unreadCount() > 0\" (click)=\"onNotificationClick()\" title=\"Notifications\" aria-label=\"Notifications\">\n <svg class=\"bell-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9\"/>\n <path d=\"M13.73 21a2 2 0 0 1-3.46 0\"/>\n </svg>\n @if (unreadCount() > 0) {\n <span class=\"badge\">{{ unreadCount() > 99 ? '99+' : unreadCount() }}</span>\n }\n </button>\n }\n \n\n <!-- Approval Button -->\n @if (showApproval()) {\n <button class=\"notification-btn\" [class.has-unread]=\"pendingApprovalCount() > 0\" (click)=\"onApprovalClick()\" title=\"Approvals\" aria-label=\"Approvals\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M9 11l3 3L22 4\"/>\n <path d=\"M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11\"/>\n </svg>\n @if (pendingApprovalCount() > 0) {\n <span class=\"badge\">{{ pendingApprovalCount() > 99 ? '99+' : pendingApprovalCount() }}</span>\n }\n </button>\n } \n\n <!-- User Avatar + Dropdown -->\n <div class=\"user-menu-wrapper\">\n <button class=\"user-menu-btn\" (click)=\"toggleDropdown()\" [attr.aria-label]=\"'User menu for ' + (currentUser().fullName || currentUser().userName)\" aria-haspopup=\"true\" [attr.aria-expanded]=\"dropdownOpen()\">\n <ma-avatar\n [src]=\"getAvatarUrl(currentUser())\"\n [alt]=\"currentUser().fullName || currentUser().userName || ''\"\n [initials]=\"getLastNameInitial(currentUser())\"\n [size]=\"navAvatarSize()\"\n [shape]=\"avatarShape()\"\n [ratio]=\"avatarRatio()\"\n [frame]=\"avatarFrame()\" \n [ring]=\"true\"\n [ringActive]=\"dropdownOpen()\" /> \n </button>\n\n @if (dropdownOpen()) {\n <div class=\"mes-dropdown-menu\">\n <!-- User info header -->\n <div class=\"mes-dropdown-header\">\n <div class=\"dropdown-avatar-wrap\">\n <ma-avatar\n [src]=\"getAvatarUrl(currentUser())\"\n [alt]=\"currentUser().fullName || currentUser().userName || ''\"\n [initials]=\"getLastNameInitial(currentUser())\"\n [size]=\"dropAvatarSize()\"\n [shape]=\"avatarShape()\"\n [ratio]=\"avatarRatio()\"\n [frame]=\"avatarFrame()\"\n [scale]=\"1.5\" /> \n </div> \n <div class=\"dropdown-info-col\">\n <div class=\"dropdown-user-info\">\n <span class=\"dropdown-user-name\">{{ currentUser().fullName || currentUser().userName }}</span> \n <span class=\"dropdown-user-sub\">\n @if (currentUser().position || currentUser().department) {\n {{ currentUser().position || currentUser().department }} \n }\n @if (currentUser().givenTitle) {\n <span class=\"given-title-badge given-title\"\n [class]=\"'given-color-' + givenStyle()\">{{ currentUser().givenTitle }}</span>\n }\n </span>\n </div>\n <div class=\"dropdown-user-actions\">\n @if (showSignature()[1] && signatureUrl(); as sigUrl) {\n <img class=\"ma-ux-signature\"\n [src]=\"sigUrl\"\n [style.height.px]=\"signatureHeight()\"\n (error)=\"onSigErr($event)\"\n alt=\"signature\" />\n }\n <button class=\"icon-action profile-link\" (click)=\"onViewProfile()\" title=\"View Profile\" aria-label=\"View Profile\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2\"/><circle cx=\"12\" cy=\"7\" r=\"4\"/>\n </svg>\n </button>\n <button class=\"icon-action logout-item\" (click)=\"onLogout()\" title=\"Logout\" aria-label=\"Logout\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\"/><polyline points=\"16 17 21 12 16 7\"/><line x1=\"21\" y1=\"12\" x2=\"9\" y2=\"12\"/>\n </svg>\n </button>\n </div>\n </div>\n </div>\n\n <div class=\"mes-dropdown-items\">\n <ng-content></ng-content>\n </div>\n </div>\n }\n </div>\n\n @if (showName()){\n <div class=\"mes-user-header\">\n {{currentUser().fullName || currentUser().userName}}\n </div>\n }\n @if (showSignature()[0] && signatureUrl(); as sigUrl) {\n <img class=\"ma-ux-signature\"\n [src]=\"sigUrl\"\n [style.height.px]=\"signatureHeight()\"\n (error)=\"onSigErr($event)\"\n alt=\"signature\" />\n }\n </div>\n }\n</div>\n", styles: [".user-profile-container{display:flex;align-items:center;gap:4px}.login-btn{padding:7px 18px;background-color:var(--primary-color);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:500;font-size:13px;letter-spacing:.2px;transition:background-color .2s,transform .15s}.login-btn:hover{background-color:var(--primary-hover);transform:translateY(-1px)}.user-header{display:flex;align-items:center;gap:4px}.mes-user-header{margin-left:10px;font-weight:600;font-size:14px;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.notification-btn{position:relative;background:none;border:none;cursor:pointer;padding:8px;border-radius:10px;color:var(--text-secondary);display:flex;align-items:center;justify-content:center;transition:color .2s,background-color .2s}.notification-btn:hover{background-color:var(--primary-light);color:var(--primary-color)}.notification-btn.has-unread{color:var(--primary-color)}.bell-icon{display:block;transition:transform .35s cubic-bezier(.34,1.56,.64,1)}.notification-btn:hover .bell-icon{transform:rotate(-20deg) scale(1.15)}.badge{position:absolute;top:2px;right:2px;background-color:var(--error-color);color:#fff;border-radius:10px;min-width:17px;height:17px;padding:0 4px;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;line-height:1;box-shadow:0 0 0 2px var(--bg-primary);animation:badge-pop .25s cubic-bezier(.34,1.56,.64,1)}@keyframes badge-pop{0%{transform:scale(0)}to{transform:scale(1)}}.user-menu-wrapper{position:relative}.user-menu-btn{background:none;border:none;cursor:pointer;padding:4px;display:flex;align-items:center;justify-content:center;transition:transform .2s}.user-menu-btn:hover{transform:scale(1.06)}.mes-dropdown-menu{position:absolute;top:calc(100% + 10px);right:0;background:var(--bg-primary);border:1px solid var(--border-color);border-radius:14px;box-shadow:0 8px 32px var(--shadow-lg),0 2px 8px var(--shadow);min-width:220px;z-index:1000;overflow:hidden;animation:dropdown-in .16s cubic-bezier(.16,1,.3,1)}@keyframes dropdown-in{0%{opacity:0;transform:translateY(-8px) scale(.96)}to{opacity:1;transform:translateY(0) scale(1)}}.mes-dropdown-header{display:flex;align-items:center;gap:12px;padding:16px;background:var(--bg-secondary)}.dropdown-avatar-wrap{flex-shrink:0}.mes-dropdown-header .dropdown-avatar-wrap{margin-right:5px}.dropdown-user-info{display:flex;flex-direction:column;gap:3px;min-width:0}.dropdown-user-name{font-weight:600;font-size:14px;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:5px;min-width:180px}.dropdown-user-sub{font-size:11px;color:var(--primary-color);font-weight:500;white-space:nowrap;text-overflow:ellipsis;margin-bottom:12px}.given-title{float:inline-end}.mes-dropdown-divider{height:1px;background:var(--border-color)}.dropdown-info-col{display:flex;flex-direction:column;gap:6px;min-width:0;flex:1}.dropdown-user-actions{display:flex;align-items:center;justify-content:flex-end;gap:4px}.icon-action{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border:none;background:none;border-radius:8px;cursor:pointer;color:var(--text-secondary);transition:background-color .15s,color .15s,transform .15s}.icon-action:hover{transform:translateY(-1px)}.icon-action.profile-link{color:var(--primary-color)}.icon-action.profile-link:hover{background-color:var(--primary-light)}.icon-action.logout-item{color:var(--error-color)}.icon-action.logout-item:hover{background-color:var(--error-light)}.mes-dropdown-items:not(:empty){border-top:1px solid var(--border-color)}.ma-ux-signature{object-fit:contain;max-width:160px;vertical-align:middle;background:transparent}\n"] }]
961
+ }], ctorParameters: () => [], propDecorators: { inputAvatarShape: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputAvatarShape", required: false }] }], showBell: [{ type: i0.Input, args: [{ isSignal: true, alias: "showBell", required: false }] }], showApproval: [{ type: i0.Input, args: [{ isSignal: true, alias: "showApproval", required: false }] }], showName: [{ type: i0.Input, args: [{ isSignal: true, alias: "showName", required: false }] }], showAi: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAi", required: false }] }], showSignature: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSignature", required: false }] }], signatureHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "signatureHeight", required: false }] }], notificationClick: [{ type: i0.Output, args: ["notificationClick"] }], approvalClick: [{ type: i0.Output, args: ["approvalClick"] }], aiClick: [{ type: i0.Output, args: ["aiClick"] }], themeClass: [{
879
962
  type: HostBinding,
880
963
  args: ['class']
881
964
  }], onDocumentClick: [{
@@ -1368,12 +1451,743 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImpo
1368
1451
  args: [{ selector: 'ma-approval-panel', imports: [DatePipe], template: "<div class=\"approval-backdrop\" [class.open]=\"isOpen()\" (click)=\"close()\"></div>\n<div class=\"approval-panel\" [class.open]=\"isOpen()\">\n\n <!-- Header -->\n <div class=\"panel-header\">\n <div class=\"panel-header-left\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M9 11l3 3L22 4\"/>\n <path d=\"M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11\"/>\n </svg>\n <h3>Approvals</h3>\n </div>\n <button class=\"close-btn\" (click)=\"close()\" aria-label=\"Close\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"/><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"/>\n </svg>\n </button>\n </div>\n\n <!-- Tabs -->\n <div class=\"tabs\">\n <button class=\"tab-btn\" [class.active]=\"activeTab() === 'processing'\" (click)=\"switchTab('processing')\">\n Processing\n @if (processingItems().length > 0) {\n <span class=\"tab-badge\">{{ processingItems().length }}</span>\n }\n </button>\n <button class=\"tab-btn\" [class.active]=\"activeTab() === 'approved'\" (click)=\"switchTab('approved')\">\n Approved\n </button>\n <button class=\"tab-btn\" [class.active]=\"activeTab() === 'rejected'\" (click)=\"switchTab('rejected')\">\n Rejected\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"panel-content\">\n <!-- Loading -->\n @if (loading()) {\n <div class=\"loading-state\">\n <div class=\"spinner\"></div>\n <span>Loading...</span>\n </div>\n }\n\n <!-- Processing tab -->\n @if (!loading() && activeTab() === 'processing') {\n @if (processingItems().length === 0) {\n <div class=\"empty-state\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"32\" height=\"32\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" opacity=\"0.4\"><path d=\"M9 11l3 3L22 4\"/><path d=\"M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11\"/></svg>\n <p>No pending approvals</p>\n </div>\n }\n @for (item of processingItems(); track item.id) {\n <div class=\"approval-item\" (click)=\"navigateToDetail(item.id)\">\n <div class=\"item-title\">{{ item.title }}</div>\n <div class=\"item-meta\">\n <span class=\"item-requester\">By {{ item.requestedByUserName }}</span>\n @if (item.currentStepName) {\n <span class=\"item-step\">\u00B7 {{ item.currentStepName }}</span>\n }\n </div>\n <div class=\"item-footer\">\n <span class=\"item-time\">{{ item.createdAt | date:'shortDate' }}</span>\n <span class=\"item-link\">View &rarr;</span>\n </div>\n </div>\n }\n }\n\n <!-- Approved tab -->\n @if (!loading() && activeTab() === 'approved') {\n @if (approvedItems().length === 0) {\n <div class=\"empty-state\">\n <p>No approved documents</p>\n </div>\n }\n @for (item of approvedItems(); track item.id) {\n <div class=\"approval-item approved\" (click)=\"navigateToDetail(item.id)\">\n <div class=\"item-title\">{{ item.title }}</div>\n <div class=\"item-meta\">\n <span class=\"item-requester\">By {{ item.requestedByUserName }}</span>\n </div>\n <div class=\"item-footer\">\n <span class=\"status-badge approved-badge\">Approved</span>\n <span class=\"item-time\">{{ item.completedAt | date:'shortDate' }}</span>\n <span class=\"item-link\">View &rarr;</span>\n </div>\n </div>\n }\n @if (approvedItems().length >= 10) {\n <div class=\"show-more\" (click)=\"showMore('approved')\">Show more &rarr;</div>\n }\n }\n\n <!-- Rejected tab -->\n @if (!loading() && activeTab() === 'rejected') {\n @if (rejectedItems().length === 0) {\n <div class=\"empty-state\">\n <p>No rejected documents</p>\n </div>\n }\n @for (item of rejectedItems(); track item.id) {\n <div class=\"approval-item rejected\" (click)=\"navigateToDetail(item.id)\">\n <div class=\"item-title\">{{ item.title }}</div>\n <div class=\"item-meta\">\n <span class=\"item-requester\">By {{ item.requestedByUserName }}</span>\n </div>\n <div class=\"item-footer\">\n <span class=\"status-badge rejected-badge\">Rejected</span>\n <span class=\"item-time\">{{ item.completedAt | date:'shortDate' }}</span>\n <span class=\"item-link\">View &rarr;</span>\n </div>\n </div>\n }\n @if (rejectedItems().length >= 10) {\n <div class=\"show-more\" (click)=\"showMore('rejected')\">Show more &rarr;</div>\n }\n }\n </div>\n</div>\n", styles: [".panel-header{display:flex;justify-content:space-between;align-items:center;padding:16px 18px;border-bottom:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0}.panel-header-left{display:flex;align-items:center;gap:9px;color:var(--primary)}.panel-header h3{margin:0;font-size:16px;font-weight:700;color:var(--text-primary)}.close-btn{background:none;border:none;cursor:pointer;color:var(--text-muted);width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:background .15s,color .15s}.close-btn:hover{background:var(--bg-hover);color:var(--text-primary)}.tabs{display:flex;border-bottom:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0}.tab-btn{flex:1;display:flex;align-items:center;justify-content:center;gap:6px;padding:11px 8px;background:none;border:none;border-bottom:2px solid transparent;color:var(--text-muted);cursor:pointer;font-size:13px;font-weight:500;transition:color .15s,border-color .15s}.tab-btn.active{color:var(--primary);border-bottom-color:var(--primary)}.tab-btn:hover:not(.active){color:var(--text-secondary)}.tab-badge{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;background:var(--primary);color:#fff;font-size:11px;font-weight:700;border-radius:9px}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 20px;gap:12px;color:var(--text-muted)}.empty-state p{margin:0;font-size:13px}.loading-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px;gap:12px;color:var(--text-muted);font-size:13px}.spinner{width:24px;height:24px;border:2px solid var(--border-color);border-top-color:var(--primary);border-radius:50%;animation:spin .7s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}\n", ":host{--primary: #90caf9;--success: #66bb6a;--error: #ef5350;--text-primary: #e0e0e0;--text-secondary: #b0b0b0;--text-muted: #757575;--bg-primary: #1e1e2e;--bg-secondary: #27273a;--bg-hover: #2a2d4a;--border-color: #383850;--shadow: rgba(0,0,0,.4)}:host(.theme-light){--primary: #1565c0;--success: #2e7d32;--error: #c62828;--text-primary: #212121;--text-secondary: #616161;--text-muted: #9e9e9e;--bg-primary: #ffffff;--bg-secondary: #f5f5f5;--bg-hover: #e8eaf6;--border-color: #e0e0e0;--shadow: rgba(0,0,0,.15)}.approval-backdrop{display:none;position:fixed;inset:0;background:#0006;z-index:1029}.approval-backdrop.open{display:block}.approval-panel{position:fixed;top:0;right:-380px;width:380px;height:100vh;background:var(--bg-primary);box-shadow:-4px 0 24px var(--shadow);display:flex;flex-direction:column;z-index:1030;transition:right .3s cubic-bezier(.16,1,.3,1)}.approval-panel.open{right:0}.panel-content{flex:1;overflow-y:auto;padding:8px 0}.approval-item{padding:14px 18px;border-bottom:1px solid var(--border-color);cursor:pointer;transition:background .15s}.approval-item:hover{background:var(--bg-hover)}.approval-item:last-child{border-bottom:none}.item-title{font-size:14px;font-weight:600;color:var(--text-primary);margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.item-meta{font-size:12px;color:var(--text-muted);margin-bottom:8px;display:flex;gap:4px;flex-wrap:wrap}.item-footer{display:flex;align-items:center;gap:8px}.item-time{font-size:11px;color:var(--text-muted);margin-right:auto}.item-link{font-size:12px;color:var(--primary);font-weight:500}.status-badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600;letter-spacing:.3px}.approved-badge{background:#66bb6a26;color:var(--success)}.rejected-badge{background:#ef53501f;color:var(--error)}.show-more{text-align:center;padding:14px;font-size:13px;color:var(--primary);cursor:pointer;font-weight:500}.show-more:hover{text-decoration:underline}\n"] }]
1369
1452
  }], ctorParameters: () => [], propDecorators: { approvalActioned: [{ type: i0.Output, args: ["approvalActioned"] }] } });
1370
1453
 
1454
+ /**
1455
+ * Resolves the full set of client tools available to the AI: built-ins + consumer-registered.
1456
+ * Handlers are invoked through `runTool(name, args)` which automatically threads the Injector
1457
+ * so consumer handlers can use inject() patterns.
1458
+ */
1459
+ class MaAiToolsRegistry {
1460
+ config = inject(MES_AUTH_AI_CONFIG, { optional: true }) ?? DEFAULT_AI_CONFIG;
1461
+ injector = inject(Injector);
1462
+ /** Built-in client tools shipped by the library. */
1463
+ builtIn = [
1464
+ {
1465
+ name: 'navigate',
1466
+ description: 'Navigate the user to a path within the application. Use relative paths starting with /.',
1467
+ parameters: {
1468
+ type: 'object',
1469
+ properties: {
1470
+ path: { type: 'string', description: 'Path to navigate to, e.g. "/auth/users"' }
1471
+ },
1472
+ required: ['path']
1473
+ },
1474
+ readOnly: true,
1475
+ handler: (args) => {
1476
+ const router = this.injector.get(Router, null);
1477
+ if (!router)
1478
+ return 'Router is not available in this app.';
1479
+ router.navigateByUrl(args.path);
1480
+ return `Navigated to ${args.path}`;
1481
+ }
1482
+ },
1483
+ {
1484
+ name: 'toggle_theme',
1485
+ description: 'Toggle between light and dark theme.',
1486
+ parameters: { type: 'object', properties: {} },
1487
+ readOnly: true,
1488
+ handler: () => {
1489
+ const theme = this.injector.get(ThemeService);
1490
+ const next = theme.currentTheme() === 'light' ? 'dark' : 'light';
1491
+ theme.setFixTheme(next);
1492
+ return `Theme switched to ${next}.`;
1493
+ }
1494
+ },
1495
+ {
1496
+ name: 'show_toast',
1497
+ description: 'Show a small toast notification at the top of the screen.',
1498
+ parameters: {
1499
+ type: 'object',
1500
+ properties: {
1501
+ message: { type: 'string', description: 'Toast body text' },
1502
+ title: { type: 'string', description: 'Optional title' },
1503
+ severity: { type: 'string', enum: ['info', 'success', 'warning', 'error'], description: 'Toast severity' }
1504
+ },
1505
+ required: ['message']
1506
+ },
1507
+ readOnly: true,
1508
+ handler: (args) => {
1509
+ const toast = this.injector.get(ToastService);
1510
+ toast.show(args.message, args.title, args.severity ?? 'info');
1511
+ return 'Toast shown.';
1512
+ }
1513
+ },
1514
+ {
1515
+ name: 'who_am_i_client',
1516
+ description: 'Get the currently signed-in user as seen by the browser (id, name, roles loaded so far).',
1517
+ parameters: { type: 'object', properties: {} },
1518
+ readOnly: true,
1519
+ handler: () => {
1520
+ const auth = this.injector.get(MesAuthService);
1521
+ const u = auth.currentUser;
1522
+ if (!u)
1523
+ return 'No user signed in.';
1524
+ return JSON.stringify({
1525
+ id: u.userId ?? u.id,
1526
+ userName: u.userName,
1527
+ fullName: u.fullName,
1528
+ department: u.department,
1529
+ position: u.position,
1530
+ email: u.email
1531
+ });
1532
+ }
1533
+ },
1534
+ {
1535
+ name: 'reload_page',
1536
+ description: 'Reload the current browser page. Use as a last resort if state seems stuck.',
1537
+ parameters: { type: 'object', properties: {} },
1538
+ readOnly: false,
1539
+ handler: () => {
1540
+ window.location.reload();
1541
+ return 'Reloading page…';
1542
+ }
1543
+ }
1544
+ ];
1545
+ /** All tools, deduplicated by name (consumer wins on conflict). */
1546
+ list() {
1547
+ const fromConfig = this.config.tools ?? [];
1548
+ const byName = new Map();
1549
+ for (const t of this.builtIn)
1550
+ byName.set(t.name, t);
1551
+ for (const t of fromConfig)
1552
+ byName.set(t.name, t);
1553
+ return Array.from(byName.values());
1554
+ }
1555
+ resolve(name) {
1556
+ return this.list().find(t => t.name === name);
1557
+ }
1558
+ /** Run a registered client tool and serialize its result for posting back to the agent loop. */
1559
+ async runTool(name, args) {
1560
+ const tool = this.resolve(name);
1561
+ if (!tool)
1562
+ return { ok: false, result: `Unknown client tool: ${name}` };
1563
+ try {
1564
+ const raw = await Promise.resolve(tool.handler(args ?? {}));
1565
+ const result = typeof raw === 'string' ? raw : JSON.stringify(raw ?? null);
1566
+ return { ok: true, result };
1567
+ }
1568
+ catch (err) {
1569
+ return { ok: false, result: err?.message ?? String(err) };
1570
+ }
1571
+ }
1572
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiToolsRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1573
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiToolsRegistry, providedIn: 'root' });
1574
+ }
1575
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiToolsRegistry, decorators: [{
1576
+ type: Injectable,
1577
+ args: [{ providedIn: 'root' }]
1578
+ }] });
1579
+
1580
+ /**
1581
+ * Drives the chat panel.
1582
+ *
1583
+ * Uses `fetch` + `ReadableStream` (not `EventSource`, which can't POST a body) to talk to
1584
+ * `/ai/chat` SSE. State lives in signals so the panel can re-render reactively.
1585
+ */
1586
+ class MaAiService {
1587
+ auth = inject(MesAuthService);
1588
+ aiConfig = inject(MES_AUTH_AI_CONFIG, { optional: true }) ?? DEFAULT_AI_CONFIG;
1589
+ tools = inject(MaAiToolsRegistry);
1590
+ messages = signal([], ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
1591
+ streaming = signal(false, ...(ngDevMode ? [{ debugName: "streaming" }] : /* istanbul ignore next */ []));
1592
+ /** When non-null, a write-class client tool is awaiting user confirmation. */
1593
+ pendingApproval = signal(null, ...(ngDevMode ? [{ debugName: "pendingApproval" }] : /* istanbul ignore next */ []));
1594
+ lastError = signal(null, ...(ngDevMode ? [{ debugName: "lastError" }] : /* istanbul ignore next */ []));
1595
+ sessionId = null;
1596
+ /** Verbs the user already chose "always approve" for in this session. */
1597
+ alwaysApproved = new Set();
1598
+ abortController = null;
1599
+ /** ID of the current assistant bubble we're streaming tokens into. */
1600
+ currentAssistantId = null;
1601
+ get enabled() {
1602
+ return this.aiConfig.enabled !== false;
1603
+ }
1604
+ /** Start a brand-new conversation. */
1605
+ resetSession() {
1606
+ this.cancel();
1607
+ this.sessionId = null;
1608
+ this.messages.set([]);
1609
+ this.pendingApproval.set(null);
1610
+ this.lastError.set(null);
1611
+ this.alwaysApproved.clear();
1612
+ }
1613
+ /** Cancel the in-flight stream (if any). The session id is preserved so the user can resume. */
1614
+ cancel() {
1615
+ if (this.abortController) {
1616
+ this.abortController.abort();
1617
+ this.abortController = null;
1618
+ }
1619
+ this.streaming.set(false);
1620
+ this.currentAssistantId = null;
1621
+ // If a client-tool call was awaiting the user, clearing it lets them start over.
1622
+ this.pendingApproval.set(null);
1623
+ }
1624
+ /** User typed and submitted a message. */
1625
+ async send(text, currentRoute) {
1626
+ if (!text.trim())
1627
+ return;
1628
+ this.lastError.set(null);
1629
+ this.appendBubble({ id: this.uid(), role: 'user', text });
1630
+ await this.runStream({ message: text }, currentRoute);
1631
+ }
1632
+ /**
1633
+ * Approve (or decline) a pending client tool call. If approved (and alwaysApprove),
1634
+ * remember the verb so we skip the prompt next time in this session.
1635
+ */
1636
+ async resolvePendingApproval(decision) {
1637
+ const pending = this.pendingApproval();
1638
+ if (!pending)
1639
+ return;
1640
+ this.pendingApproval.set(null);
1641
+ if (decision === 'decline') {
1642
+ await this.continueWithToolResult(pending.callId, false, 'User declined this tool call.');
1643
+ this.markToolStatus(pending.callId, 'declined');
1644
+ return;
1645
+ }
1646
+ if (decision === 'always') {
1647
+ this.alwaysApproved.add(this.verbOf(pending.name));
1648
+ }
1649
+ await this.executeClientTool(pending.callId, pending.name, pending.args);
1650
+ }
1651
+ // ── private ────────────────────────────────────────────────────────────────
1652
+ async runStream(payload, currentRoute) {
1653
+ if (!this.enabled) {
1654
+ this.lastError.set('AI assistant is disabled.');
1655
+ return;
1656
+ }
1657
+ const config = this.auth.getConfig();
1658
+ const base = config?.apiBaseUrl?.replace(/\/$/, '') ?? '';
1659
+ if (!base) {
1660
+ this.lastError.set('MesAuth is not configured.');
1661
+ return;
1662
+ }
1663
+ // Start a fresh assistant bubble that we stream tokens into.
1664
+ const assistantId = this.uid();
1665
+ this.currentAssistantId = assistantId;
1666
+ this.appendBubble({ id: assistantId, role: 'assistant', text: '', pending: true, toolEvents: [] });
1667
+ this.streaming.set(true);
1668
+ this.abortController = new AbortController();
1669
+ try {
1670
+ const res = await fetch(`${base}/ai/chat`, {
1671
+ method: 'POST',
1672
+ headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream' },
1673
+ credentials: config?.withCredentials !== false ? 'include' : 'same-origin',
1674
+ body: JSON.stringify({
1675
+ sessionId: this.sessionId,
1676
+ message: payload.message,
1677
+ toolResult: payload.toolResult,
1678
+ clientTools: this.tools.list().map(t => ({
1679
+ name: t.name,
1680
+ description: t.description,
1681
+ parameters: t.parameters,
1682
+ readOnly: t.readOnly === true
1683
+ })),
1684
+ context: { currentRoute, appName: this.aiConfig.appName }
1685
+ }),
1686
+ signal: this.abortController.signal
1687
+ });
1688
+ if (!res.ok || !res.body) {
1689
+ const detail = await res.text().catch(() => '');
1690
+ throw new Error(`AI request failed (${res.status}). ${detail}`);
1691
+ }
1692
+ await this.readSse(res.body);
1693
+ }
1694
+ catch (err) {
1695
+ if (err?.name === 'AbortError') {
1696
+ // user cancelled
1697
+ }
1698
+ else {
1699
+ this.lastError.set(err?.message ?? String(err));
1700
+ this.finalizeAssistant(`[error: ${err?.message ?? err}]`);
1701
+ }
1702
+ }
1703
+ finally {
1704
+ this.streaming.set(false);
1705
+ this.abortController = null;
1706
+ }
1707
+ }
1708
+ async readSse(body) {
1709
+ const reader = body.getReader();
1710
+ const decoder = new TextDecoder();
1711
+ let buffer = '';
1712
+ while (true) {
1713
+ const { value, done } = await reader.read();
1714
+ if (done)
1715
+ break;
1716
+ buffer += decoder.decode(value, { stream: true });
1717
+ // SSE events are separated by a blank line.
1718
+ let idx;
1719
+ while ((idx = buffer.indexOf('\n\n')) !== -1) {
1720
+ const rawEvent = buffer.slice(0, idx);
1721
+ buffer = buffer.slice(idx + 2);
1722
+ const dataLine = rawEvent.split('\n').find(l => l.startsWith('data:'));
1723
+ if (!dataLine)
1724
+ continue;
1725
+ const json = dataLine.slice(5).trim();
1726
+ if (!json)
1727
+ continue;
1728
+ let ev;
1729
+ try {
1730
+ ev = JSON.parse(json);
1731
+ }
1732
+ catch {
1733
+ continue;
1734
+ }
1735
+ await this.handleEvent(ev);
1736
+ }
1737
+ }
1738
+ }
1739
+ async handleEvent(ev) {
1740
+ switch (ev.type) {
1741
+ case 'session':
1742
+ this.sessionId = ev.sessionId;
1743
+ break;
1744
+ case 'token':
1745
+ this.appendToken(ev.text);
1746
+ break;
1747
+ case 'tool_call_started':
1748
+ this.appendToolEvent({
1749
+ id: ev.id,
1750
+ name: ev.name,
1751
+ side: ev.side,
1752
+ status: 'running',
1753
+ args: ev.args,
1754
+ readOnly: ev.readOnly
1755
+ });
1756
+ break;
1757
+ case 'tool_call_completed':
1758
+ this.markToolStatus(ev.id, ev.ok ? 'ok' : 'error', ev.summary);
1759
+ break;
1760
+ case 'client_tool_call':
1761
+ this.appendToolEvent({
1762
+ id: ev.id,
1763
+ name: ev.name,
1764
+ side: 'client',
1765
+ status: ev.readOnly ? 'running' : 'awaiting-approval',
1766
+ args: ev.args,
1767
+ readOnly: ev.readOnly
1768
+ });
1769
+ if (ev.readOnly || this.alwaysApproved.has(this.verbOf(ev.name))) {
1770
+ // Auto-run; the next /ai/chat call will deliver the result.
1771
+ await this.executeClientTool(ev.id, ev.name, ev.args);
1772
+ }
1773
+ else {
1774
+ this.pendingApproval.set({ callId: ev.id, name: ev.name, args: ev.args, side: 'client' });
1775
+ }
1776
+ break;
1777
+ case 'error':
1778
+ this.lastError.set(ev.message);
1779
+ this.finalizeAssistant(`[error: ${ev.message}]`);
1780
+ break;
1781
+ case 'done':
1782
+ this.finalizeAssistant();
1783
+ break;
1784
+ }
1785
+ }
1786
+ async executeClientTool(callId, name, args) {
1787
+ this.markToolStatus(callId, 'running');
1788
+ const { ok, result } = await this.tools.runTool(name, args);
1789
+ this.markToolStatus(callId, ok ? 'ok' : 'error', this.summarize(result));
1790
+ await this.continueWithToolResult(callId, ok, result);
1791
+ }
1792
+ async continueWithToolResult(callId, ok, result) {
1793
+ // Open a new SSE stream that delivers the tool result.
1794
+ await this.runStream({ toolResult: { callId, ok, result } });
1795
+ }
1796
+ // ── bubble helpers ────────────────────────────────────────────────────────
1797
+ appendBubble(b) {
1798
+ this.messages.update(arr => [...arr, b]);
1799
+ }
1800
+ appendToken(text) {
1801
+ if (!this.currentAssistantId) {
1802
+ const id = this.uid();
1803
+ this.currentAssistantId = id;
1804
+ this.appendBubble({ id, role: 'assistant', text, pending: true, toolEvents: [] });
1805
+ return;
1806
+ }
1807
+ const id = this.currentAssistantId;
1808
+ this.messages.update(arr => arr.map(m => m.id === id ? { ...m, text: m.text + text } : m));
1809
+ }
1810
+ appendToolEvent(ev) {
1811
+ if (!this.currentAssistantId) {
1812
+ const id = this.uid();
1813
+ this.currentAssistantId = id;
1814
+ this.appendBubble({ id, role: 'assistant', text: '', pending: true, toolEvents: [ev] });
1815
+ return;
1816
+ }
1817
+ const id = this.currentAssistantId;
1818
+ this.messages.update(arr => arr.map(m => m.id === id
1819
+ ? { ...m, toolEvents: [...(m.toolEvents ?? []), ev] }
1820
+ : m));
1821
+ }
1822
+ markToolStatus(callId, status, summary) {
1823
+ this.messages.update(arr => arr.map(m => ({
1824
+ ...m,
1825
+ toolEvents: m.toolEvents?.map(t => t.id === callId ? { ...t, status, summary: summary ?? t.summary } : t)
1826
+ })));
1827
+ }
1828
+ finalizeAssistant(extra) {
1829
+ const id = this.currentAssistantId;
1830
+ if (!id)
1831
+ return;
1832
+ this.messages.update(arr => arr.map(m => m.id === id
1833
+ ? { ...m, pending: false, text: extra ? (m.text + (m.text ? '\n' : '') + extra) : m.text }
1834
+ : m));
1835
+ this.currentAssistantId = null;
1836
+ }
1837
+ verbOf(name) {
1838
+ return name.split('_')[0] ?? name;
1839
+ }
1840
+ summarize(s) {
1841
+ const flat = s.replace(/\s+/g, ' ').trim();
1842
+ return flat.length <= 80 ? flat : flat.slice(0, 80) + '…';
1843
+ }
1844
+ uid() {
1845
+ return Math.random().toString(36).slice(2) + Date.now().toString(36);
1846
+ }
1847
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1848
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiService, providedIn: 'root' });
1849
+ }
1850
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiService, decorators: [{
1851
+ type: Injectable,
1852
+ args: [{ providedIn: 'root' }]
1853
+ }] });
1854
+
1855
+ /**
1856
+ * Tiny hand-rolled GitHub-flavoured Markdown renderer for AI chat bubbles.
1857
+ *
1858
+ * Supports: headings, paragraphs (with single-newline → <br>), pipe tables,
1859
+ * fenced + inline code, bullet/ordered lists (flat), blockquotes, bold, italic,
1860
+ * autolinks via [text](url). All input is HTML-escaped before any markup
1861
+ * is added, so the only HTML that reaches the DOM is what this pipe emits.
1862
+ * Wrapped in `bypassSecurityTrustHtml` so [innerHTML] doesn't re-sanitize and
1863
+ * strip the markup we explicitly want.
1864
+ *
1865
+ * Not supported (intentionally — keeps the renderer tiny and safe):
1866
+ * nested lists, images, raw HTML, strikethrough, task lists, footnotes.
1867
+ * If the model emits any of those, they appear as literal text.
1868
+ */
1869
+ class MaAiMarkdownPipe {
1870
+ sanitizer = inject(DomSanitizer);
1871
+ transform(value) {
1872
+ const html = renderMarkdown(value ?? '');
1873
+ return this.sanitizer.bypassSecurityTrustHtml(html);
1874
+ }
1875
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiMarkdownPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1876
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.13", ngImport: i0, type: MaAiMarkdownPipe, isStandalone: true, name: "maAiMarkdown" });
1877
+ }
1878
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiMarkdownPipe, decorators: [{
1879
+ type: Pipe,
1880
+ args: [{ name: 'maAiMarkdown' }]
1881
+ }] });
1882
+ // ── Renderer ────────────────────────────────────────────────────────────────
1883
+ // Sentinel format that survives escapeHtml AND won't collide with markdown
1884
+ // tokens (no `*`, `_`, `[`, `]`, `(`, `)`, `>`, `#`, `-`, `|` at boundaries).
1885
+ const TOKEN_PREFIX = 'MAAIMD_TOKEN_';
1886
+ const TOKEN_SUFFIX = '_END';
1887
+ const tokenRe = new RegExp(`${TOKEN_PREFIX}(\\d+)${TOKEN_SUFFIX}`, 'g');
1888
+ function makeToken(i) { return `${TOKEN_PREFIX}${i}${TOKEN_SUFFIX}`; }
1889
+ function renderMarkdown(src) {
1890
+ if (!src)
1891
+ return '';
1892
+ const blocks = [];
1893
+ // 1. Extract fenced code blocks (raw content preserved literally, content escaped).
1894
+ let text = src.replace(/```(\w+)?\n([\s\S]*?)```/g, (_m, lang, body) => {
1895
+ const langClass = lang ? ` class="lang-${escapeAttr(lang)}"` : '';
1896
+ blocks.push(`<pre class="md-pre"><code${langClass}>${escapeHtml(body.replace(/\n$/, ''))}</code></pre>`);
1897
+ return makeToken(blocks.length - 1);
1898
+ });
1899
+ // 2. Extract inline `code` spans.
1900
+ text = text.replace(/`([^`\n]+)`/g, (_m, body) => {
1901
+ blocks.push(`<code class="md-code">${escapeHtml(body)}</code>`);
1902
+ return makeToken(blocks.length - 1);
1903
+ });
1904
+ // 3. Now escape the entire remaining content. Markdown tokens (`*`, `_`, `|`,
1905
+ // `[`, `]`, `(`, `)`, `>`, `#`, `-`) are not HTML-special, so they pass through
1906
+ // escapeHtml untouched and remain parseable in the block/inline stage below.
1907
+ text = escapeHtml(text);
1908
+ // 4. Block-level pass — split on blank lines, classify each block.
1909
+ const parts = text.split(/\n{2,}/);
1910
+ const out = [];
1911
+ for (const raw of parts) {
1912
+ const trimmed = raw.replace(/^\n+|\n+$/g, '');
1913
+ if (!trimmed)
1914
+ continue;
1915
+ out.push(renderBlock(trimmed));
1916
+ }
1917
+ // 5. Restore code placeholders.
1918
+ let html = out.join('\n');
1919
+ html = html.replace(tokenRe, (_m, n) => blocks[Number(n)] ?? '');
1920
+ return html;
1921
+ }
1922
+ function renderBlock(block) {
1923
+ const lines = block.split('\n');
1924
+ // Heading: `# foo` … `###### foo` (single-line only).
1925
+ if (lines.length === 1) {
1926
+ const h = lines[0].match(/^(#{1,6})\s+(.+)$/);
1927
+ if (h) {
1928
+ const level = h[1].length;
1929
+ return `<h${level} class="md-h${level}">${inline(h[2])}</h${level}>`;
1930
+ }
1931
+ }
1932
+ // Table: header line + separator line + body lines.
1933
+ if (lines.length >= 2 && isTableLine(lines[0]) && isTableSeparator(lines[1])) {
1934
+ return renderTable(lines);
1935
+ }
1936
+ // Blockquote: every line starts with `> ` (or `>`).
1937
+ if (lines.every(l => /^>\s?/.test(l))) {
1938
+ const inner = lines.map(l => l.replace(/^>\s?/, '')).join('\n');
1939
+ return `<blockquote class="md-quote">${inline(inner.replace(/\n/g, '<br>'))}</blockquote>`;
1940
+ }
1941
+ // Ordered list: every line `1. `, `2. `, …
1942
+ if (lines.every(l => /^\d+\.\s+/.test(l))) {
1943
+ const items = lines.map(l => `<li>${inline(l.replace(/^\d+\.\s+/, ''))}</li>`).join('');
1944
+ return `<ol class="md-list">${items}</ol>`;
1945
+ }
1946
+ // Unordered list: every line `- ` or `* `.
1947
+ if (lines.every(l => /^[-*]\s+/.test(l))) {
1948
+ const items = lines.map(l => `<li>${inline(l.replace(/^[-*]\s+/, ''))}</li>`).join('');
1949
+ return `<ul class="md-list">${items}</ul>`;
1950
+ }
1951
+ // Paragraph fallback. Single newlines become <br> for fidelity to what the AI typed.
1952
+ return `<p class="md-p">${inline(block.replace(/\n/g, '<br>'))}</p>`;
1953
+ }
1954
+ function isTableLine(s) {
1955
+ const t = s.trim();
1956
+ return t.startsWith('|') && t.endsWith('|') && t.split('|').length >= 3;
1957
+ }
1958
+ function isTableSeparator(s) {
1959
+ if (!isTableLine(s))
1960
+ return false;
1961
+ const cells = splitCells(s);
1962
+ return cells.length > 0 && cells.every(c => /^:?-{1,}:?$/.test(c.trim()));
1963
+ }
1964
+ function splitCells(line) {
1965
+ const inner = line.trim().replace(/^\|/, '').replace(/\|$/, '');
1966
+ return inner.split('|').map(c => c.trim());
1967
+ }
1968
+ function renderTable(lines) {
1969
+ const headerCells = splitCells(lines[0]);
1970
+ const aligns = splitCells(lines[1]).map(c => {
1971
+ const t = c.trim();
1972
+ if (t.startsWith(':') && t.endsWith(':'))
1973
+ return 'center';
1974
+ if (t.endsWith(':'))
1975
+ return 'right';
1976
+ if (t.startsWith(':'))
1977
+ return 'left';
1978
+ return '';
1979
+ });
1980
+ const bodyLines = lines.slice(2).filter(l => isTableLine(l));
1981
+ const renderRow = (cells, tag) => cells.map((c, i) => {
1982
+ const align = aligns[i] ? ` style="text-align:${aligns[i]}"` : '';
1983
+ return `<${tag}${align}>${inline(c)}</${tag}>`;
1984
+ }).join('');
1985
+ const thead = `<thead><tr>${renderRow(headerCells, 'th')}</tr></thead>`;
1986
+ const tbody = bodyLines.length
1987
+ ? `<tbody>${bodyLines.map(l => `<tr>${renderRow(splitCells(l), 'td')}</tr>`).join('')}</tbody>`
1988
+ : '';
1989
+ return `<div class="md-table-wrap"><table class="md-table">${thead}${tbody}</table></div>`;
1990
+ }
1991
+ // Inline pass: links, bold, italic. Operates on already-escaped text so markdown
1992
+ // tokens are unambiguous (no HTML entities to confuse the regexes).
1993
+ function inline(text) {
1994
+ let s = text;
1995
+ // Links: [label](url) — sanitize URL scheme.
1996
+ s = s.replace(/\[([^\]\n]+)\]\(([^)\s]+)\)/g, (_m, label, url) => {
1997
+ const safeUrl = sanitizeUrl(url);
1998
+ if (!safeUrl)
1999
+ return label;
2000
+ return `<a class="md-link" href="${escapeAttr(safeUrl)}" target="_blank" rel="noopener noreferrer">${label}</a>`;
2001
+ });
2002
+ // Bold: **text** or __text__
2003
+ s = s.replace(/\*\*([^*\n]+)\*\*/g, '<strong>$1</strong>');
2004
+ s = s.replace(/(^|[\s(])__([^_\n]+)__(?=[\s.,;:)!?]|$)/g, '$1<strong>$2</strong>');
2005
+ // Italic: *text* or _text_ (boundary-aware to avoid mid-word matches).
2006
+ s = s.replace(/(^|[\s(])\*([^*\n]+)\*(?=[\s.,;:)!?]|$)/g, '$1<em>$2</em>');
2007
+ s = s.replace(/(^|[\s(])_([^_\n]+)_(?=[\s.,;:)!?]|$)/g, '$1<em>$2</em>');
2008
+ return s;
2009
+ }
2010
+ function escapeHtml(s) {
2011
+ return s
2012
+ .replace(/&/g, '&amp;')
2013
+ .replace(/</g, '&lt;')
2014
+ .replace(/>/g, '&gt;')
2015
+ .replace(/"/g, '&quot;')
2016
+ .replace(/'/g, '&#39;');
2017
+ }
2018
+ function escapeAttr(s) {
2019
+ return s.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;');
2020
+ }
2021
+ function sanitizeUrl(url) {
2022
+ const trimmed = url.trim();
2023
+ // Allow http(s), mailto, root-relative, and fragments. Reject javascript:, data:, vbscript: etc.
2024
+ if (/^(https?:|mailto:)/i.test(trimmed))
2025
+ return trimmed;
2026
+ if (trimmed.startsWith('/') || trimmed.startsWith('#'))
2027
+ return trimmed;
2028
+ return null;
2029
+ }
2030
+
2031
+ const STORAGE_KEY = 'ma-ai-panel-width';
2032
+ const MIN_WIDTH = 320;
2033
+ const MAX_WIDTH = 800;
2034
+ const DEFAULT_WIDTH = 420;
2035
+ /**
2036
+ * Right-side, resizable chat drawer. Open/close via the public open/close/toggle methods.
2037
+ * Uses panel-shared.css for the header/empty-state styling so it looks like the approval
2038
+ * and notification panels.
2039
+ */
2040
+ class MaAiChatPanelComponent {
2041
+ isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
2042
+ width = signal(this.loadWidth(), ...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
2043
+ draft = signal('', ...(ngDevMode ? [{ debugName: "draft" }] : /* istanbul ignore next */ []));
2044
+ ai = inject(MaAiService);
2045
+ router = inject(Router, { optional: true });
2046
+ themeService = inject(ThemeService);
2047
+ uiConfig = inject(MaUiConfigService);
2048
+ /** Suggested prompts shown in the empty state — manifest overridable. */
2049
+ quickPrompts = computed(() => {
2050
+ const fromManifest = this.uiConfig.quickPrompts();
2051
+ if (fromManifest && fromManifest.length > 0)
2052
+ return fromManifest;
2053
+ return [
2054
+ 'Who am I?',
2055
+ 'List my pending approvals',
2056
+ 'What unread notifications do I have?'
2057
+ ];
2058
+ }, ...(ngDevMode ? [{ debugName: "quickPrompts" }] : /* istanbul ignore next */ []));
2059
+ scrollContainer = viewChild('scrollContainer', ...(ngDevMode ? [{ debugName: "scrollContainer" }] : /* istanbul ignore next */ []));
2060
+ textArea = viewChild('textArea', ...(ngDevMode ? [{ debugName: "textArea" }] : /* istanbul ignore next */ []));
2061
+ get themeClass() {
2062
+ return `theme-${this.themeService.currentTheme()}`;
2063
+ }
2064
+ messages = this.ai.messages;
2065
+ streaming = this.ai.streaming;
2066
+ pendingApproval = this.ai.pendingApproval;
2067
+ lastError = this.ai.lastError;
2068
+ hasMessages = computed(() => this.messages().length > 0, ...(ngDevMode ? [{ debugName: "hasMessages" }] : /* istanbul ignore next */ []));
2069
+ // Resize state
2070
+ isDragging = false;
2071
+ dragStartX = 0;
2072
+ dragStartWidth = 0;
2073
+ constructor() {
2074
+ // Auto-scroll on new content
2075
+ effect(() => {
2076
+ this.messages(); // dep
2077
+ this.streaming();
2078
+ queueMicrotask(() => this.scrollToBottom());
2079
+ });
2080
+ }
2081
+ ngAfterViewInit() {
2082
+ // pointer listeners attached on demand during drag
2083
+ }
2084
+ open() {
2085
+ this.isOpen.set(true);
2086
+ queueMicrotask(() => this.textArea()?.nativeElement.focus());
2087
+ }
2088
+ close() {
2089
+ this.isOpen.set(false);
2090
+ }
2091
+ toggle() {
2092
+ if (this.isOpen())
2093
+ this.close();
2094
+ else
2095
+ this.open();
2096
+ }
2097
+ newConversation() {
2098
+ this.ai.resetSession();
2099
+ }
2100
+ send() {
2101
+ const text = this.draft().trim();
2102
+ if (!text || this.streaming())
2103
+ return;
2104
+ this.draft.set('');
2105
+ const route = this.router?.url;
2106
+ this.ai.send(text, route);
2107
+ }
2108
+ cancel() {
2109
+ this.ai.cancel();
2110
+ }
2111
+ onTextareaKey(ev) {
2112
+ if (ev.key === 'Enter' && !ev.shiftKey) {
2113
+ ev.preventDefault();
2114
+ this.send();
2115
+ }
2116
+ }
2117
+ onTextareaInput(ev) {
2118
+ const t = ev.target;
2119
+ this.draft.set(t.value);
2120
+ // auto-grow
2121
+ t.style.height = 'auto';
2122
+ t.style.height = Math.min(t.scrollHeight, 160) + 'px';
2123
+ }
2124
+ approve() { this.ai.resolvePendingApproval('approve'); }
2125
+ alwaysApprove() { this.ai.resolvePendingApproval('always'); }
2126
+ decline() { this.ai.resolvePendingApproval('decline'); }
2127
+ // ── resize handle ─────────────────────────────────────────────────────────
2128
+ startResize(ev) {
2129
+ this.isDragging = true;
2130
+ this.dragStartX = ev.clientX;
2131
+ this.dragStartWidth = this.width();
2132
+ ev.target.setPointerCapture(ev.pointerId);
2133
+ ev.preventDefault();
2134
+ }
2135
+ onResizeMove(ev) {
2136
+ if (!this.isDragging)
2137
+ return;
2138
+ const dx = this.dragStartX - ev.clientX; // drag-left = wider
2139
+ const next = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, this.dragStartWidth + dx));
2140
+ this.width.set(next);
2141
+ }
2142
+ endResize(ev) {
2143
+ if (!this.isDragging)
2144
+ return;
2145
+ this.isDragging = false;
2146
+ try {
2147
+ ev.target.releasePointerCapture(ev.pointerId);
2148
+ }
2149
+ catch { }
2150
+ try {
2151
+ localStorage.setItem(STORAGE_KEY, String(this.width()));
2152
+ }
2153
+ catch { }
2154
+ }
2155
+ // ── helpers ───────────────────────────────────────────────────────────────
2156
+ loadWidth() {
2157
+ try {
2158
+ const raw = typeof localStorage !== 'undefined' ? localStorage.getItem(STORAGE_KEY) : null;
2159
+ const n = raw ? parseInt(raw, 10) : NaN;
2160
+ return isFinite(n) ? Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, n)) : DEFAULT_WIDTH;
2161
+ }
2162
+ catch {
2163
+ return DEFAULT_WIDTH;
2164
+ }
2165
+ }
2166
+ scrollToBottom() {
2167
+ const el = this.scrollContainer()?.nativeElement;
2168
+ if (!el)
2169
+ return;
2170
+ el.scrollTop = el.scrollHeight;
2171
+ }
2172
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiChatPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2173
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: MaAiChatPanelComponent, isStandalone: true, selector: "ma-ai-chat-panel", host: { properties: { "class": "this.themeClass" } }, viewQueries: [{ propertyName: "scrollContainer", first: true, predicate: ["scrollContainer"], descendants: true, isSignal: true }, { propertyName: "textArea", first: true, predicate: ["textArea"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"ai-panel\" [class.open]=\"isOpen()\" [style.width.px]=\"width()\">\n\n <!-- Resize handle (left edge) -->\n <div class=\"ai-resize-handle\"\n (pointerdown)=\"startResize($event)\"\n (pointermove)=\"onResizeMove($event)\"\n (pointerup)=\"endResize($event)\"\n (pointercancel)=\"endResize($event)\"\n aria-label=\"Resize panel\"\n title=\"Drag to resize\"></div>\n\n <!-- Header -->\n <div class=\"panel-header\">\n <div class=\"panel-header-left\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\"\n fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M12 2 L13.5 7.5 L19 9 L13.5 10.5 L12 16 L10.5 10.5 L5 9 L10.5 7.5 Z\"/>\n <path d=\"M19 16 L19.8 18.2 L22 19 L19.8 19.8 L19 22 L18.2 19.8 L16 19 L18.2 18.2 Z\"/>\n </svg>\n <h3>AI Assistant</h3>\n </div>\n <div class=\"header-actions\">\n <button class=\"icon-btn\" (click)=\"newConversation()\" title=\"New conversation\" aria-label=\"New conversation\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\"\n fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M21 12a9 9 0 1 1-3-6.7L21 8\"/><path d=\"M21 3v5h-5\"/>\n </svg>\n </button>\n <button class=\"close-btn\" (click)=\"close()\" aria-label=\"Close\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\"\n fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"/><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"/>\n </svg>\n </button>\n </div>\n </div>\n\n <!-- Conversation -->\n <div class=\"panel-content\" #scrollContainer>\n @if (!hasMessages()) {\n <div class=\"empty-state\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"36\" height=\"36\" viewBox=\"0 0 24 24\"\n fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\" opacity=\"0.5\">\n <path d=\"M12 2 L13.5 7.5 L19 9 L13.5 10.5 L12 16 L10.5 10.5 L5 9 L10.5 7.5 Z\"/>\n </svg>\n <p>Ask anything about your account, approvals, notifications, or how to use the app.</p>\n <div class=\"quick-prompts\">\n @for (q of quickPrompts(); track q) {\n <button class=\"quick-prompt\" (click)=\"draft.set(q)\">{{ q }}</button>\n }\n </div>\n </div>\n }\n\n @for (m of messages(); track m.id) {\n <div class=\"bubble\" [class.user]=\"m.role === 'user'\" [class.assistant]=\"m.role === 'assistant'\">\n @if (m.role === 'assistant' && m.toolEvents && m.toolEvents.length > 0) {\n <div class=\"tool-strip\">\n @for (t of m.toolEvents; track t.id) {\n <div class=\"tool-chip\" [class.ok]=\"t.status === 'ok'\" [class.err]=\"t.status === 'error'\"\n [class.running]=\"t.status === 'running'\" [class.await]=\"t.status === 'awaiting-approval'\"\n [class.declined]=\"t.status === 'declined'\"\n [title]=\"t.summary ?? ''\">\n <span class=\"dot\"></span>\n <span class=\"tool-name\">{{ t.side === 'server' ? '\u2699' : '\u2318' }} {{ t.name }}</span>\n @if (t.status === 'running') { <span class=\"muted\">\u2026</span> }\n @if (t.status === 'ok' && t.summary) { <span class=\"muted\">\u00B7 {{ t.summary }}</span> }\n @if (t.status === 'error') { <span class=\"muted\">\u00B7 failed</span> }\n @if (t.status === 'awaiting-approval') { <span class=\"muted\">\u00B7 awaiting approval</span> }\n @if (t.status === 'declined') { <span class=\"muted\">\u00B7 declined</span> }\n </div>\n }\n </div>\n }\n @if (m.text) {\n @if (m.role === 'assistant') {\n <div class=\"bubble-text md-body\" [innerHTML]=\"m.text | maAiMarkdown\"></div>\n } @else {\n <div class=\"bubble-text\">{{ m.text }}</div>\n }\n }\n @if (m.pending && !m.text) {\n <div class=\"thinking\"><span class=\"spinner-small\"></span> Thinking\u2026</div>\n }\n </div>\n }\n </div>\n\n <!-- Pending approval card -->\n @if (pendingApproval(); as p) {\n <div class=\"approval-card\">\n <div class=\"approval-card-head\">\n <strong>Approve action?</strong>\n <code class=\"tool-name\">{{ p.name }}</code>\n </div>\n @if (p.args && (p.args | json) !== '{}') {\n <pre class=\"approval-args\">{{ p.args | json }}</pre>\n }\n <div class=\"approval-actions\">\n <button class=\"btn primary\" (click)=\"approve()\">Approve</button>\n <button class=\"btn\" (click)=\"alwaysApprove()\">Always</button>\n <button class=\"btn danger\" (click)=\"decline()\">Decline</button>\n </div>\n </div>\n }\n\n <!-- Composer -->\n <div class=\"composer\">\n @if (lastError()) {\n <div class=\"composer-error\">{{ lastError() }}</div>\n }\n <div class=\"composer-row\">\n <textarea #textArea\n rows=\"1\"\n placeholder=\"Ask the AI\u2026\"\n [value]=\"draft()\"\n (input)=\"onTextareaInput($event)\"\n (keydown)=\"onTextareaKey($event)\"\n [disabled]=\"streaming()\"></textarea>\n @if (streaming()) {\n <button class=\"send-btn cancel\" (click)=\"cancel()\" title=\"Cancel\" aria-label=\"Cancel\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><rect x=\"6\" y=\"6\" width=\"12\" height=\"12\"/></svg>\n </button>\n } @else {\n <button class=\"send-btn\" (click)=\"send()\" [disabled]=\"!draft().trim()\" title=\"Send\" aria-label=\"Send\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"22\" y1=\"2\" x2=\"11\" y2=\"13\"/><polygon points=\"22 2 15 22 11 13 2 9 22 2\"/></svg>\n </button>\n }\n </div>\n </div>\n</div>\n", styles: [".panel-header{display:flex;justify-content:space-between;align-items:center;padding:16px 18px;border-bottom:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0}.panel-header-left{display:flex;align-items:center;gap:9px;color:var(--primary)}.panel-header h3{margin:0;font-size:16px;font-weight:700;color:var(--text-primary)}.close-btn{background:none;border:none;cursor:pointer;color:var(--text-muted);width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:background .15s,color .15s}.close-btn:hover{background:var(--bg-hover);color:var(--text-primary)}.tabs{display:flex;border-bottom:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0}.tab-btn{flex:1;display:flex;align-items:center;justify-content:center;gap:6px;padding:11px 8px;background:none;border:none;border-bottom:2px solid transparent;color:var(--text-muted);cursor:pointer;font-size:13px;font-weight:500;transition:color .15s,border-color .15s}.tab-btn.active{color:var(--primary);border-bottom-color:var(--primary)}.tab-btn:hover:not(.active){color:var(--text-secondary)}.tab-badge{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;background:var(--primary);color:#fff;font-size:11px;font-weight:700;border-radius:9px}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 20px;gap:12px;color:var(--text-muted)}.empty-state p{margin:0;font-size:13px}.loading-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px;gap:12px;color:var(--text-muted);font-size:13px}.spinner{width:24px;height:24px;border:2px solid var(--border-color);border-top-color:var(--primary);border-radius:50%;animation:spin .7s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}\n", ":host{--primary: #c4b5fd;--primary-strong: #a78bfa;--success: #66bb6a;--error: #ef5350;--text-primary: #e0e0e0;--text-secondary: #b0b0b0;--text-muted: #757575;--bg-primary: #1e1e2e;--bg-secondary: #27273a;--bg-hover: #2a2d4a;--bg-bubble-user: #312e81;--bg-bubble-assistant: #232336;--border-color: #383850;--shadow: rgba(0,0,0,.4)}:host(.theme-light){--primary: #7c3aed;--primary-strong: #6d28d9;--success: #2e7d32;--error: #c62828;--text-primary: #212121;--text-secondary: #616161;--text-muted: #9e9e9e;--bg-primary: #ffffff;--bg-secondary: #f5f3ff;--bg-hover: #ede9fe;--bg-bubble-user: #ede9fe;--bg-bubble-assistant: #f5f5f5;--border-color: #e0e0e0;--shadow: rgba(0,0,0,.15)}.ai-panel{position:fixed;top:0;right:0;height:100vh;background:var(--bg-primary);box-shadow:-4px 0 24px var(--shadow);display:flex;flex-direction:column;z-index:1040;transform:translate(100%);transition:transform .3s cubic-bezier(.16,1,.3,1);will-change:transform,width;pointer-events:none}.ai-panel.open{transform:translate(0);pointer-events:auto}.ai-resize-handle{position:absolute;top:0;left:-4px;width:8px;height:100%;cursor:ew-resize;z-index:1;background:transparent}.ai-resize-handle:hover{background:linear-gradient(to right,transparent,rgba(124,58,237,.25),transparent)}.header-actions{display:flex;align-items:center;gap:4px}.icon-btn{background:none;border:none;cursor:pointer;color:var(--text-muted);width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:background .15s,color .15s}.icon-btn:hover{background:var(--bg-hover);color:var(--text-primary)}.panel-content{flex:1;overflow-y:auto;padding:12px 14px;display:flex;flex-direction:column;gap:10px}.quick-prompts{display:flex;flex-wrap:wrap;gap:6px;justify-content:center;margin-top:8px}.quick-prompt{background:var(--bg-secondary);border:1px solid var(--border-color);color:var(--text-secondary);padding:6px 10px;border-radius:14px;font-size:12px;cursor:pointer;transition:background .15s,color .15s}.quick-prompt:hover{background:var(--bg-hover);color:var(--text-primary)}.bubble{max-width:90%;padding:10px 12px;border-radius:10px;font-size:13.5px;line-height:1.5;word-wrap:break-word;white-space:pre-wrap}.bubble.user{align-self:flex-end;background:var(--bg-bubble-user);color:var(--text-primary)}.bubble.assistant{align-self:flex-start;background:var(--bg-bubble-assistant);color:var(--text-primary)}.bubble-text{display:block}.thinking{display:flex;align-items:center;gap:6px;color:var(--text-muted);font-size:12.5px}.spinner-small{width:12px;height:12px;border:2px solid var(--border-color);border-top-color:var(--primary);border-radius:50%;animation:spin .7s linear infinite;display:inline-block}@keyframes spin{to{transform:rotate(360deg)}}.tool-strip{display:flex;flex-direction:column;gap:4px;margin-bottom:6px}.tool-chip{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border-radius:12px;background:var(--bg-hover);font-size:11.5px;color:var(--text-secondary);border:1px solid var(--border-color);align-self:flex-start}.tool-chip .dot{width:6px;height:6px;border-radius:50%;background:var(--text-muted)}.tool-chip.running .dot{background:var(--primary);animation:pulse 1s ease-in-out infinite}.tool-chip.ok .dot{background:var(--success)}.tool-chip.err .dot{background:var(--error)}.tool-chip.await .dot{background:#f59e0b}.tool-chip.declined .dot{background:var(--text-muted)}.tool-chip .tool-name{font-weight:600;color:var(--text-primary)}.tool-chip .muted{color:var(--text-muted)}@keyframes pulse{50%{opacity:.4}}.approval-card{flex-shrink:0;margin:8px 14px;padding:12px 14px;border:1px solid var(--primary);border-radius:10px;background:var(--bg-secondary)}.approval-card-head{display:flex;align-items:center;gap:10px;margin-bottom:8px;color:var(--text-primary)}.approval-card-head code{background:var(--bg-hover);padding:2px 6px;border-radius:6px;font-size:12px;color:var(--primary)}.approval-args{background:var(--bg-primary);border:1px solid var(--border-color);border-radius:6px;padding:8px;font-size:11.5px;max-height:100px;overflow:auto;margin:0 0 8px;color:var(--text-secondary)}.approval-actions{display:flex;gap:6px;flex-wrap:wrap}.btn{flex:1;padding:6px 10px;font-size:12.5px;font-weight:600;border-radius:6px;border:1px solid var(--border-color);background:var(--bg-primary);color:var(--text-primary);cursor:pointer;transition:background .15s,border-color .15s}.btn:hover{background:var(--bg-hover)}.btn.primary{background:var(--primary);color:#fff;border-color:var(--primary)}.btn.primary:hover{background:var(--primary-strong)}.btn.danger{color:var(--error)}.btn.danger:hover{background:#ef53501a}.composer{flex-shrink:0;border-top:1px solid var(--border-color);padding:10px 12px;background:var(--bg-secondary)}.composer-error{color:var(--error);font-size:12px;margin-bottom:6px}.composer-row{display:flex;align-items:flex-end;gap:8px}.composer textarea{flex:1;resize:none;padding:8px 10px;border-radius:8px;border:1px solid var(--border-color);background:var(--bg-primary);color:var(--text-primary);font-size:13px;font-family:inherit;outline:none;min-height:36px;max-height:160px;line-height:1.4}.composer textarea:focus{border-color:var(--primary)}.composer textarea:disabled{opacity:.6}.send-btn{width:36px;height:36px;flex-shrink:0;display:flex;align-items:center;justify-content:center;background:var(--primary);color:#fff;border:none;border-radius:8px;cursor:pointer;transition:background .15s}.send-btn:hover:not(:disabled){background:var(--primary-strong)}.send-btn:disabled{opacity:.5;cursor:not-allowed}.send-btn.cancel{background:var(--error)}.send-btn.cancel:hover{filter:brightness(1.1)}.md-body{font-size:13.5px;line-height:1.55}.md-body .md-p{margin:0 0 8px}.md-body .md-p:last-child{margin-bottom:0}.md-body .md-h1,.md-body .md-h2,.md-body .md-h3,.md-body .md-h4,.md-body .md-h5,.md-body .md-h6{margin:12px 0 6px;font-weight:700;color:var(--text-primary);line-height:1.3}.md-body .md-h1{font-size:17px}.md-body .md-h2{font-size:15.5px}.md-body .md-h3{font-size:14.5px}.md-body .md-h4,.md-body .md-h5,.md-body .md-h6{font-size:13.5px}.md-body strong{font-weight:700}.md-body em{font-style:italic}.md-body .md-link{color:var(--primary);text-decoration:underline;text-underline-offset:2px}.md-body .md-link:hover{color:var(--primary-strong)}.md-body .md-list{margin:0 0 8px;padding-left:22px}.md-body .md-list li{margin:2px 0}.md-body .md-list li::marker{color:var(--text-muted)}.md-body .md-quote{margin:0 0 8px;padding:6px 10px;border-left:3px solid var(--primary);background:var(--bg-hover);color:var(--text-secondary);border-radius:0 6px 6px 0}.md-body .md-code{background:var(--bg-hover);border:1px solid var(--border-color);border-radius:4px;padding:1px 5px;font-size:12px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:var(--primary)}.md-body .md-pre{background:var(--bg-primary);border:1px solid var(--border-color);border-radius:8px;padding:10px 12px;margin:0 0 8px;overflow-x:auto;font-size:12px;line-height:1.5}.md-body .md-pre code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:var(--text-primary);background:none;border:none;padding:0}.md-body .md-table-wrap{margin:0 0 8px;overflow-x:auto;border:1px solid var(--border-color);border-radius:8px}.md-body .md-table{border-collapse:collapse;width:100%;font-size:12.5px}.md-body .md-table th,.md-body .md-table td{padding:6px 10px;border-bottom:1px solid var(--border-color);vertical-align:top;text-align:left}.md-body .md-table th{background:var(--bg-secondary);color:var(--text-primary);font-weight:600}.md-body .md-table tr:last-child td{border-bottom:none}.md-body .md-table tbody tr:hover{background:var(--bg-hover)}\n"], dependencies: [{ kind: "pipe", type: JsonPipe, name: "json" }, { kind: "pipe", type: MaAiMarkdownPipe, name: "maAiMarkdown" }] });
2174
+ }
2175
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiChatPanelComponent, decorators: [{
2176
+ type: Component,
2177
+ args: [{ selector: 'ma-ai-chat-panel', imports: [JsonPipe, MaAiMarkdownPipe], template: "<div class=\"ai-panel\" [class.open]=\"isOpen()\" [style.width.px]=\"width()\">\n\n <!-- Resize handle (left edge) -->\n <div class=\"ai-resize-handle\"\n (pointerdown)=\"startResize($event)\"\n (pointermove)=\"onResizeMove($event)\"\n (pointerup)=\"endResize($event)\"\n (pointercancel)=\"endResize($event)\"\n aria-label=\"Resize panel\"\n title=\"Drag to resize\"></div>\n\n <!-- Header -->\n <div class=\"panel-header\">\n <div class=\"panel-header-left\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\"\n fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M12 2 L13.5 7.5 L19 9 L13.5 10.5 L12 16 L10.5 10.5 L5 9 L10.5 7.5 Z\"/>\n <path d=\"M19 16 L19.8 18.2 L22 19 L19.8 19.8 L19 22 L18.2 19.8 L16 19 L18.2 18.2 Z\"/>\n </svg>\n <h3>AI Assistant</h3>\n </div>\n <div class=\"header-actions\">\n <button class=\"icon-btn\" (click)=\"newConversation()\" title=\"New conversation\" aria-label=\"New conversation\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\"\n fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M21 12a9 9 0 1 1-3-6.7L21 8\"/><path d=\"M21 3v5h-5\"/>\n </svg>\n </button>\n <button class=\"close-btn\" (click)=\"close()\" aria-label=\"Close\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\"\n fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"/><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"/>\n </svg>\n </button>\n </div>\n </div>\n\n <!-- Conversation -->\n <div class=\"panel-content\" #scrollContainer>\n @if (!hasMessages()) {\n <div class=\"empty-state\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"36\" height=\"36\" viewBox=\"0 0 24 24\"\n fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\" opacity=\"0.5\">\n <path d=\"M12 2 L13.5 7.5 L19 9 L13.5 10.5 L12 16 L10.5 10.5 L5 9 L10.5 7.5 Z\"/>\n </svg>\n <p>Ask anything about your account, approvals, notifications, or how to use the app.</p>\n <div class=\"quick-prompts\">\n @for (q of quickPrompts(); track q) {\n <button class=\"quick-prompt\" (click)=\"draft.set(q)\">{{ q }}</button>\n }\n </div>\n </div>\n }\n\n @for (m of messages(); track m.id) {\n <div class=\"bubble\" [class.user]=\"m.role === 'user'\" [class.assistant]=\"m.role === 'assistant'\">\n @if (m.role === 'assistant' && m.toolEvents && m.toolEvents.length > 0) {\n <div class=\"tool-strip\">\n @for (t of m.toolEvents; track t.id) {\n <div class=\"tool-chip\" [class.ok]=\"t.status === 'ok'\" [class.err]=\"t.status === 'error'\"\n [class.running]=\"t.status === 'running'\" [class.await]=\"t.status === 'awaiting-approval'\"\n [class.declined]=\"t.status === 'declined'\"\n [title]=\"t.summary ?? ''\">\n <span class=\"dot\"></span>\n <span class=\"tool-name\">{{ t.side === 'server' ? '\u2699' : '\u2318' }} {{ t.name }}</span>\n @if (t.status === 'running') { <span class=\"muted\">\u2026</span> }\n @if (t.status === 'ok' && t.summary) { <span class=\"muted\">\u00B7 {{ t.summary }}</span> }\n @if (t.status === 'error') { <span class=\"muted\">\u00B7 failed</span> }\n @if (t.status === 'awaiting-approval') { <span class=\"muted\">\u00B7 awaiting approval</span> }\n @if (t.status === 'declined') { <span class=\"muted\">\u00B7 declined</span> }\n </div>\n }\n </div>\n }\n @if (m.text) {\n @if (m.role === 'assistant') {\n <div class=\"bubble-text md-body\" [innerHTML]=\"m.text | maAiMarkdown\"></div>\n } @else {\n <div class=\"bubble-text\">{{ m.text }}</div>\n }\n }\n @if (m.pending && !m.text) {\n <div class=\"thinking\"><span class=\"spinner-small\"></span> Thinking\u2026</div>\n }\n </div>\n }\n </div>\n\n <!-- Pending approval card -->\n @if (pendingApproval(); as p) {\n <div class=\"approval-card\">\n <div class=\"approval-card-head\">\n <strong>Approve action?</strong>\n <code class=\"tool-name\">{{ p.name }}</code>\n </div>\n @if (p.args && (p.args | json) !== '{}') {\n <pre class=\"approval-args\">{{ p.args | json }}</pre>\n }\n <div class=\"approval-actions\">\n <button class=\"btn primary\" (click)=\"approve()\">Approve</button>\n <button class=\"btn\" (click)=\"alwaysApprove()\">Always</button>\n <button class=\"btn danger\" (click)=\"decline()\">Decline</button>\n </div>\n </div>\n }\n\n <!-- Composer -->\n <div class=\"composer\">\n @if (lastError()) {\n <div class=\"composer-error\">{{ lastError() }}</div>\n }\n <div class=\"composer-row\">\n <textarea #textArea\n rows=\"1\"\n placeholder=\"Ask the AI\u2026\"\n [value]=\"draft()\"\n (input)=\"onTextareaInput($event)\"\n (keydown)=\"onTextareaKey($event)\"\n [disabled]=\"streaming()\"></textarea>\n @if (streaming()) {\n <button class=\"send-btn cancel\" (click)=\"cancel()\" title=\"Cancel\" aria-label=\"Cancel\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><rect x=\"6\" y=\"6\" width=\"12\" height=\"12\"/></svg>\n </button>\n } @else {\n <button class=\"send-btn\" (click)=\"send()\" [disabled]=\"!draft().trim()\" title=\"Send\" aria-label=\"Send\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"22\" y1=\"2\" x2=\"11\" y2=\"13\"/><polygon points=\"22 2 15 22 11 13 2 9 22 2\"/></svg>\n </button>\n }\n </div>\n </div>\n</div>\n", styles: [".panel-header{display:flex;justify-content:space-between;align-items:center;padding:16px 18px;border-bottom:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0}.panel-header-left{display:flex;align-items:center;gap:9px;color:var(--primary)}.panel-header h3{margin:0;font-size:16px;font-weight:700;color:var(--text-primary)}.close-btn{background:none;border:none;cursor:pointer;color:var(--text-muted);width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:background .15s,color .15s}.close-btn:hover{background:var(--bg-hover);color:var(--text-primary)}.tabs{display:flex;border-bottom:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0}.tab-btn{flex:1;display:flex;align-items:center;justify-content:center;gap:6px;padding:11px 8px;background:none;border:none;border-bottom:2px solid transparent;color:var(--text-muted);cursor:pointer;font-size:13px;font-weight:500;transition:color .15s,border-color .15s}.tab-btn.active{color:var(--primary);border-bottom-color:var(--primary)}.tab-btn:hover:not(.active){color:var(--text-secondary)}.tab-badge{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;background:var(--primary);color:#fff;font-size:11px;font-weight:700;border-radius:9px}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 20px;gap:12px;color:var(--text-muted)}.empty-state p{margin:0;font-size:13px}.loading-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px;gap:12px;color:var(--text-muted);font-size:13px}.spinner{width:24px;height:24px;border:2px solid var(--border-color);border-top-color:var(--primary);border-radius:50%;animation:spin .7s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}\n", ":host{--primary: #c4b5fd;--primary-strong: #a78bfa;--success: #66bb6a;--error: #ef5350;--text-primary: #e0e0e0;--text-secondary: #b0b0b0;--text-muted: #757575;--bg-primary: #1e1e2e;--bg-secondary: #27273a;--bg-hover: #2a2d4a;--bg-bubble-user: #312e81;--bg-bubble-assistant: #232336;--border-color: #383850;--shadow: rgba(0,0,0,.4)}:host(.theme-light){--primary: #7c3aed;--primary-strong: #6d28d9;--success: #2e7d32;--error: #c62828;--text-primary: #212121;--text-secondary: #616161;--text-muted: #9e9e9e;--bg-primary: #ffffff;--bg-secondary: #f5f3ff;--bg-hover: #ede9fe;--bg-bubble-user: #ede9fe;--bg-bubble-assistant: #f5f5f5;--border-color: #e0e0e0;--shadow: rgba(0,0,0,.15)}.ai-panel{position:fixed;top:0;right:0;height:100vh;background:var(--bg-primary);box-shadow:-4px 0 24px var(--shadow);display:flex;flex-direction:column;z-index:1040;transform:translate(100%);transition:transform .3s cubic-bezier(.16,1,.3,1);will-change:transform,width;pointer-events:none}.ai-panel.open{transform:translate(0);pointer-events:auto}.ai-resize-handle{position:absolute;top:0;left:-4px;width:8px;height:100%;cursor:ew-resize;z-index:1;background:transparent}.ai-resize-handle:hover{background:linear-gradient(to right,transparent,rgba(124,58,237,.25),transparent)}.header-actions{display:flex;align-items:center;gap:4px}.icon-btn{background:none;border:none;cursor:pointer;color:var(--text-muted);width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:background .15s,color .15s}.icon-btn:hover{background:var(--bg-hover);color:var(--text-primary)}.panel-content{flex:1;overflow-y:auto;padding:12px 14px;display:flex;flex-direction:column;gap:10px}.quick-prompts{display:flex;flex-wrap:wrap;gap:6px;justify-content:center;margin-top:8px}.quick-prompt{background:var(--bg-secondary);border:1px solid var(--border-color);color:var(--text-secondary);padding:6px 10px;border-radius:14px;font-size:12px;cursor:pointer;transition:background .15s,color .15s}.quick-prompt:hover{background:var(--bg-hover);color:var(--text-primary)}.bubble{max-width:90%;padding:10px 12px;border-radius:10px;font-size:13.5px;line-height:1.5;word-wrap:break-word;white-space:pre-wrap}.bubble.user{align-self:flex-end;background:var(--bg-bubble-user);color:var(--text-primary)}.bubble.assistant{align-self:flex-start;background:var(--bg-bubble-assistant);color:var(--text-primary)}.bubble-text{display:block}.thinking{display:flex;align-items:center;gap:6px;color:var(--text-muted);font-size:12.5px}.spinner-small{width:12px;height:12px;border:2px solid var(--border-color);border-top-color:var(--primary);border-radius:50%;animation:spin .7s linear infinite;display:inline-block}@keyframes spin{to{transform:rotate(360deg)}}.tool-strip{display:flex;flex-direction:column;gap:4px;margin-bottom:6px}.tool-chip{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border-radius:12px;background:var(--bg-hover);font-size:11.5px;color:var(--text-secondary);border:1px solid var(--border-color);align-self:flex-start}.tool-chip .dot{width:6px;height:6px;border-radius:50%;background:var(--text-muted)}.tool-chip.running .dot{background:var(--primary);animation:pulse 1s ease-in-out infinite}.tool-chip.ok .dot{background:var(--success)}.tool-chip.err .dot{background:var(--error)}.tool-chip.await .dot{background:#f59e0b}.tool-chip.declined .dot{background:var(--text-muted)}.tool-chip .tool-name{font-weight:600;color:var(--text-primary)}.tool-chip .muted{color:var(--text-muted)}@keyframes pulse{50%{opacity:.4}}.approval-card{flex-shrink:0;margin:8px 14px;padding:12px 14px;border:1px solid var(--primary);border-radius:10px;background:var(--bg-secondary)}.approval-card-head{display:flex;align-items:center;gap:10px;margin-bottom:8px;color:var(--text-primary)}.approval-card-head code{background:var(--bg-hover);padding:2px 6px;border-radius:6px;font-size:12px;color:var(--primary)}.approval-args{background:var(--bg-primary);border:1px solid var(--border-color);border-radius:6px;padding:8px;font-size:11.5px;max-height:100px;overflow:auto;margin:0 0 8px;color:var(--text-secondary)}.approval-actions{display:flex;gap:6px;flex-wrap:wrap}.btn{flex:1;padding:6px 10px;font-size:12.5px;font-weight:600;border-radius:6px;border:1px solid var(--border-color);background:var(--bg-primary);color:var(--text-primary);cursor:pointer;transition:background .15s,border-color .15s}.btn:hover{background:var(--bg-hover)}.btn.primary{background:var(--primary);color:#fff;border-color:var(--primary)}.btn.primary:hover{background:var(--primary-strong)}.btn.danger{color:var(--error)}.btn.danger:hover{background:#ef53501a}.composer{flex-shrink:0;border-top:1px solid var(--border-color);padding:10px 12px;background:var(--bg-secondary)}.composer-error{color:var(--error);font-size:12px;margin-bottom:6px}.composer-row{display:flex;align-items:flex-end;gap:8px}.composer textarea{flex:1;resize:none;padding:8px 10px;border-radius:8px;border:1px solid var(--border-color);background:var(--bg-primary);color:var(--text-primary);font-size:13px;font-family:inherit;outline:none;min-height:36px;max-height:160px;line-height:1.4}.composer textarea:focus{border-color:var(--primary)}.composer textarea:disabled{opacity:.6}.send-btn{width:36px;height:36px;flex-shrink:0;display:flex;align-items:center;justify-content:center;background:var(--primary);color:#fff;border:none;border-radius:8px;cursor:pointer;transition:background .15s}.send-btn:hover:not(:disabled){background:var(--primary-strong)}.send-btn:disabled{opacity:.5;cursor:not-allowed}.send-btn.cancel{background:var(--error)}.send-btn.cancel:hover{filter:brightness(1.1)}.md-body{font-size:13.5px;line-height:1.55}.md-body .md-p{margin:0 0 8px}.md-body .md-p:last-child{margin-bottom:0}.md-body .md-h1,.md-body .md-h2,.md-body .md-h3,.md-body .md-h4,.md-body .md-h5,.md-body .md-h6{margin:12px 0 6px;font-weight:700;color:var(--text-primary);line-height:1.3}.md-body .md-h1{font-size:17px}.md-body .md-h2{font-size:15.5px}.md-body .md-h3{font-size:14.5px}.md-body .md-h4,.md-body .md-h5,.md-body .md-h6{font-size:13.5px}.md-body strong{font-weight:700}.md-body em{font-style:italic}.md-body .md-link{color:var(--primary);text-decoration:underline;text-underline-offset:2px}.md-body .md-link:hover{color:var(--primary-strong)}.md-body .md-list{margin:0 0 8px;padding-left:22px}.md-body .md-list li{margin:2px 0}.md-body .md-list li::marker{color:var(--text-muted)}.md-body .md-quote{margin:0 0 8px;padding:6px 10px;border-left:3px solid var(--primary);background:var(--bg-hover);color:var(--text-secondary);border-radius:0 6px 6px 0}.md-body .md-code{background:var(--bg-hover);border:1px solid var(--border-color);border-radius:4px;padding:1px 5px;font-size:12px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:var(--primary)}.md-body .md-pre{background:var(--bg-primary);border:1px solid var(--border-color);border-radius:8px;padding:10px 12px;margin:0 0 8px;overflow-x:auto;font-size:12px;line-height:1.5}.md-body .md-pre code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:var(--text-primary);background:none;border:none;padding:0}.md-body .md-table-wrap{margin:0 0 8px;overflow-x:auto;border:1px solid var(--border-color);border-radius:8px}.md-body .md-table{border-collapse:collapse;width:100%;font-size:12.5px}.md-body .md-table th,.md-body .md-table td{padding:6px 10px;border-bottom:1px solid var(--border-color);vertical-align:top;text-align:left}.md-body .md-table th{background:var(--bg-secondary);color:var(--text-primary);font-weight:600}.md-body .md-table tr:last-child td{border-bottom:none}.md-body .md-table tbody tr:hover{background:var(--bg-hover)}\n"] }]
2178
+ }], ctorParameters: () => [], propDecorators: { scrollContainer: [{ type: i0.ViewChild, args: ['scrollContainer', { isSignal: true }] }], textArea: [{ type: i0.ViewChild, args: ['textArea', { isSignal: true }] }], themeClass: [{
2179
+ type: HostBinding,
2180
+ args: ['class']
2181
+ }] } });
2182
+
1371
2183
  class MaUserComponent {
1372
2184
  userProfile;
1373
2185
  approvalPanel;
2186
+ aiPanel;
1374
2187
  avatarShape = input('rounded', ...(ngDevMode ? [{ debugName: "avatarShape" }] : /* istanbul ignore next */ []));
1375
2188
  showBell = input(true, ...(ngDevMode ? [{ debugName: "showBell" }] : /* istanbul ignore next */ []));
1376
2189
  showApproval = input(true, ...(ngDevMode ? [{ debugName: "showApproval" }] : /* istanbul ignore next */ []));
2190
+ showAi = input(true, ...(ngDevMode ? [{ debugName: "showAi" }] : /* istanbul ignore next */ []));
1377
2191
  showName = input(false, ...(ngDevMode ? [{ debugName: "showName" }] : /* istanbul ignore next */ []));
1378
2192
  showSignature = input([false, true], ...(ngDevMode ? [{ debugName: "showSignature" }] : /* istanbul ignore next */ []));
1379
2193
  theme = input(null, ...(ngDevMode ? [{ debugName: "theme" }] : /* istanbul ignore next */ []));
@@ -1403,18 +2217,21 @@ class MaUserComponent {
1403
2217
  }
1404
2218
  }
1405
2219
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaUserComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1406
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.13", type: MaUserComponent, isStandalone: true, selector: "ma-user", inputs: { avatarShape: { classPropertyName: "avatarShape", publicName: "avatarShape", isSignal: true, isRequired: false, transformFunction: null }, showBell: { classPropertyName: "showBell", publicName: "showBell", isSignal: true, isRequired: false, transformFunction: null }, showApproval: { classPropertyName: "showApproval", publicName: "showApproval", isSignal: true, isRequired: false, transformFunction: null }, showName: { classPropertyName: "showName", publicName: "showName", isSignal: true, isRequired: false, transformFunction: null }, showSignature: { classPropertyName: "showSignature", publicName: "showSignature", isSignal: true, isRequired: false, transformFunction: null }, theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "userProfile", first: true, predicate: UserProfileComponent, descendants: true }, { propertyName: "approvalPanel", first: true, predicate: MaApprovalPanelComponent, descendants: true }], ngImport: i0, template: "<ma-toast-container></ma-toast-container>\n<div class=\"user-header\">\n <ma-user-profile\n (notificationClick)=\"notificationPanel.open()\"\n (approvalClick)=\"approvalPanel.open()\"\n [inputAvatarShape]=\"avatarShape()\"\n [showBell]=\"showBell()\"\n [showApproval]=\"showApproval()\"\n [showName]=\"showName()\"\n [showSignature]=\"showSignature()\">\n <ng-content></ng-content>\n </ma-user-profile> \n</div>\n<ma-notification-panel #notificationPanel (notificationRead)=\"onNotificationRead()\"></ma-notification-panel>\n<ma-approval-panel #approvalPanel (approvalActioned)=\"onApprovalActioned()\"></ma-approval-panel>\n", styles: [".user-header{display:flex;justify-content:flex-end}\n"], dependencies: [{ kind: "component", type: ToastContainerComponent, selector: "ma-toast-container" }, { kind: "component", type: UserProfileComponent, selector: "ma-user-profile", inputs: ["inputAvatarShape", "showBell", "showApproval", "showName", "showSignature", "signatureHeight"], outputs: ["notificationClick", "approvalClick"] }, { kind: "component", type: NotificationPanelComponent, selector: "ma-notification-panel", outputs: ["notificationRead"] }, { kind: "component", type: MaApprovalPanelComponent, selector: "ma-approval-panel", outputs: ["approvalActioned"] }] });
2220
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.13", type: MaUserComponent, isStandalone: true, selector: "ma-user", inputs: { avatarShape: { classPropertyName: "avatarShape", publicName: "avatarShape", isSignal: true, isRequired: false, transformFunction: null }, showBell: { classPropertyName: "showBell", publicName: "showBell", isSignal: true, isRequired: false, transformFunction: null }, showApproval: { classPropertyName: "showApproval", publicName: "showApproval", isSignal: true, isRequired: false, transformFunction: null }, showAi: { classPropertyName: "showAi", publicName: "showAi", isSignal: true, isRequired: false, transformFunction: null }, showName: { classPropertyName: "showName", publicName: "showName", isSignal: true, isRequired: false, transformFunction: null }, showSignature: { classPropertyName: "showSignature", publicName: "showSignature", isSignal: true, isRequired: false, transformFunction: null }, theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "userProfile", first: true, predicate: UserProfileComponent, descendants: true }, { propertyName: "approvalPanel", first: true, predicate: MaApprovalPanelComponent, descendants: true }, { propertyName: "aiPanel", first: true, predicate: MaAiChatPanelComponent, descendants: true }], ngImport: i0, template: "<ma-toast-container></ma-toast-container>\n<div class=\"user-header\">\n <ma-user-profile\n (notificationClick)=\"notificationPanel.open()\"\n (approvalClick)=\"approvalPanel.open()\"\n (aiClick)=\"aiPanel?.open()\"\n [inputAvatarShape]=\"avatarShape()\"\n [showBell]=\"showBell()\"\n [showApproval]=\"showApproval()\"\n [showAi]=\"showAi()\"\n [showName]=\"showName()\"\n [showSignature]=\"showSignature()\">\n <ng-content></ng-content>\n </ma-user-profile>\n</div>\n<ma-notification-panel #notificationPanel (notificationRead)=\"onNotificationRead()\"></ma-notification-panel>\n<ma-approval-panel #approvalPanel (approvalActioned)=\"onApprovalActioned()\"></ma-approval-panel>\n<ma-ai-chat-panel #aiPanel></ma-ai-chat-panel>\n", styles: [".user-header{display:flex;justify-content:flex-end}\n"], dependencies: [{ kind: "component", type: ToastContainerComponent, selector: "ma-toast-container" }, { kind: "component", type: UserProfileComponent, selector: "ma-user-profile", inputs: ["inputAvatarShape", "showBell", "showApproval", "showName", "showAi", "showSignature", "signatureHeight"], outputs: ["notificationClick", "approvalClick", "aiClick"] }, { kind: "component", type: NotificationPanelComponent, selector: "ma-notification-panel", outputs: ["notificationRead"] }, { kind: "component", type: MaApprovalPanelComponent, selector: "ma-approval-panel", outputs: ["approvalActioned"] }, { kind: "component", type: MaAiChatPanelComponent, selector: "ma-ai-chat-panel" }] });
1407
2221
  }
1408
2222
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaUserComponent, decorators: [{
1409
2223
  type: Component,
1410
- args: [{ selector: 'ma-user', imports: [ToastContainerComponent, UserProfileComponent, NotificationPanelComponent, MaApprovalPanelComponent], template: "<ma-toast-container></ma-toast-container>\n<div class=\"user-header\">\n <ma-user-profile\n (notificationClick)=\"notificationPanel.open()\"\n (approvalClick)=\"approvalPanel.open()\"\n [inputAvatarShape]=\"avatarShape()\"\n [showBell]=\"showBell()\"\n [showApproval]=\"showApproval()\"\n [showName]=\"showName()\"\n [showSignature]=\"showSignature()\">\n <ng-content></ng-content>\n </ma-user-profile> \n</div>\n<ma-notification-panel #notificationPanel (notificationRead)=\"onNotificationRead()\"></ma-notification-panel>\n<ma-approval-panel #approvalPanel (approvalActioned)=\"onApprovalActioned()\"></ma-approval-panel>\n", styles: [".user-header{display:flex;justify-content:flex-end}\n"] }]
2224
+ args: [{ selector: 'ma-user', imports: [ToastContainerComponent, UserProfileComponent, NotificationPanelComponent, MaApprovalPanelComponent, MaAiChatPanelComponent], template: "<ma-toast-container></ma-toast-container>\n<div class=\"user-header\">\n <ma-user-profile\n (notificationClick)=\"notificationPanel.open()\"\n (approvalClick)=\"approvalPanel.open()\"\n (aiClick)=\"aiPanel?.open()\"\n [inputAvatarShape]=\"avatarShape()\"\n [showBell]=\"showBell()\"\n [showApproval]=\"showApproval()\"\n [showAi]=\"showAi()\"\n [showName]=\"showName()\"\n [showSignature]=\"showSignature()\">\n <ng-content></ng-content>\n </ma-user-profile>\n</div>\n<ma-notification-panel #notificationPanel (notificationRead)=\"onNotificationRead()\"></ma-notification-panel>\n<ma-approval-panel #approvalPanel (approvalActioned)=\"onApprovalActioned()\"></ma-approval-panel>\n<ma-ai-chat-panel #aiPanel></ma-ai-chat-panel>\n", styles: [".user-header{display:flex;justify-content:flex-end}\n"] }]
1411
2225
  }], ctorParameters: () => [], propDecorators: { userProfile: [{
1412
2226
  type: ViewChild,
1413
2227
  args: [UserProfileComponent]
1414
2228
  }], approvalPanel: [{
1415
2229
  type: ViewChild,
1416
2230
  args: [MaApprovalPanelComponent]
1417
- }], avatarShape: [{ type: i0.Input, args: [{ isSignal: true, alias: "avatarShape", required: false }] }], showBell: [{ type: i0.Input, args: [{ isSignal: true, alias: "showBell", required: false }] }], showApproval: [{ type: i0.Input, args: [{ isSignal: true, alias: "showApproval", required: false }] }], showName: [{ type: i0.Input, args: [{ isSignal: true, alias: "showName", required: false }] }], showSignature: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSignature", required: false }] }], theme: [{ type: i0.Input, args: [{ isSignal: true, alias: "theme", required: false }] }] } });
2231
+ }], aiPanel: [{
2232
+ type: ViewChild,
2233
+ args: [MaAiChatPanelComponent]
2234
+ }], avatarShape: [{ type: i0.Input, args: [{ isSignal: true, alias: "avatarShape", required: false }] }], showBell: [{ type: i0.Input, args: [{ isSignal: true, alias: "showBell", required: false }] }], showApproval: [{ type: i0.Input, args: [{ isSignal: true, alias: "showApproval", required: false }] }], showAi: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAi", required: false }] }], showName: [{ type: i0.Input, args: [{ isSignal: true, alias: "showName", required: false }] }], showSignature: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSignature", required: false }] }], theme: [{ type: i0.Input, args: [{ isSignal: true, alias: "theme", required: false }] }] } });
1418
2235
 
1419
2236
  class MaUserXComponent {
1420
2237
  userId = input(null, ...(ngDevMode ? [{ debugName: "userId" }] : /* istanbul ignore next */ []));
@@ -2423,6 +3240,16 @@ async function runReturnViaPostMessageIfRequested(authService) {
2423
3240
  return true;
2424
3241
  }
2425
3242
 
3243
+ /**
3244
+ * Public AI types exported from mesauth-angular.
3245
+ *
3246
+ * The library ships a chat panel that talks to MesAuth.Api's /ai/chat SSE endpoint.
3247
+ * Consumer apps may register additional client-side tools that the AI can invoke
3248
+ * to drive their UI (navigation, dialogs, etc.) via provideMesAuthAi().
3249
+ */
3250
+
3251
+ // Public surface of the AI module inside mesauth-angular.
3252
+
2426
3253
  // mesauth-angular — primary entry point (backward compatible)
2427
3254
  // Re-exports everything so existing imports from 'mesauth-angular' continue to work.
2428
3255
  // Prefer importing from 'mesauth-angular/core' or 'mesauth-angular/ui' for
@@ -2432,5 +3259,5 @@ async function runReturnViaPostMessageIfRequested(authService) {
2432
3259
  * Generated bundle index. Do not edit.
2433
3260
  */
2434
3261
 
2435
- export { ALL_ACTIONS, AVATAR_FRAMES, ApprovalActionType, ApprovalDocumentStatus, ApprovalStepMode, ApprovalStepStatus, MES_AUTH_CONFIG, MaApprovalPanelComponent, MaApprovalService, MaArvContainerComponent, MaAvatarComponent, MaIconComponent, MaThemeDirective, MaUiConfigService, MaUserComponent, MaUserMenuColor, MaUserMenuComponent, MaUserXComponent, MesAuthModule, MesAuthService, NotificationBadgeComponent, NotificationPanelComponent, NotificationType, PACKAGE_VERSION, ThemeService, ToastContainerComponent, ToastService, UserProfileComponent, extractXMaPerm, mesAuthInterceptor, provideMesAuth, runReturnViaPostMessageIfRequested, runSsoCheckHandshake, withXMaPerm, xMaResource };
3262
+ export { ALL_ACTIONS, AVATAR_FRAMES, ApprovalActionType, ApprovalDocumentStatus, ApprovalStepMode, ApprovalStepStatus, DEFAULT_AI_CONFIG, MES_AUTH_AI_CONFIG, MES_AUTH_CONFIG, MaAiButtonComponent, MaAiChatPanelComponent, MaAiMarkdownPipe, MaAiService, MaAiToolsRegistry, MaApprovalPanelComponent, MaApprovalService, MaArvContainerComponent, MaAvatarComponent, MaIconComponent, MaThemeDirective, MaUiConfigService, MaUserComponent, MaUserMenuColor, MaUserMenuComponent, MaUserXComponent, MesAuthModule, MesAuthService, NotificationBadgeComponent, NotificationPanelComponent, NotificationType, PACKAGE_VERSION, ThemeService, ToastContainerComponent, ToastService, UserProfileComponent, extractXMaPerm, mesAuthInterceptor, provideMesAuth, provideMesAuthAi, renderMarkdown, runReturnViaPostMessageIfRequested, runSsoCheckHandshake, withXMaPerm, xMaResource };
2436
3263
  //# sourceMappingURL=mesauth-angular.mjs.map