mesauth-angular 1.22.0 → 1.24.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,16 +1,16 @@
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, ElementRef, Injector, Pipe, viewChild, ViewChild, DestroyRef, Directive } from '@angular/core';
2
+ import { signal, Injectable, InjectionToken, makeEnvironmentProviders, provideAppInitializer, inject, NgModule, afterNextRender, input, booleanAttribute, computed, HostBinding, Component, output, Injector, ChangeDetectionStrategy, effect, HostListener, ElementRef, Pipe, viewChild, ViewChild, DestroyRef, Directive } from '@angular/core';
3
3
  import { toObservable, toSignal, takeUntilDestroyed, rxResource } from '@angular/core/rxjs-interop';
4
- import { catchError, of, Subject, EMPTY, timer, throwError, firstValueFrom, distinctUntilChanged, switchMap as switchMap$1, forkJoin } from 'rxjs';
4
+ import { catchError, of, Subject, EMPTY, BehaviorSubject, throwError, timer, firstValueFrom, distinctUntilChanged, switchMap as switchMap$1, forkJoin } from 'rxjs';
5
5
  import { HttpClient, HttpResponse } from '@angular/common/http';
6
6
  import { HubConnectionBuilder, LogLevel } from '@microsoft/signalr';
7
- import { map, tap, catchError as catchError$1, switchMap } from 'rxjs/operators';
7
+ import { map, tap, catchError as catchError$1, switchMap, filter, take } from 'rxjs/operators';
8
8
  import { Router } from '@angular/router';
9
9
  import { DomSanitizer } from '@angular/platform-browser';
10
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.21.0';
13
+ const PACKAGE_VERSION = '1.24.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.
@@ -446,20 +446,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImpo
446
446
  type: Injectable
447
447
  }], ctorParameters: () => [] });
448
448
 
449
- // Track if we're currently redirecting to prevent loopback
449
+ // Module-scope state single instance shared across all requests because
450
+ // HttpInterceptorFn is provided once per injector.
451
+ // True while a 401-triggered refresh is in flight; concurrent 401 retries wait for it.
452
+ const refreshing$ = new BehaviorSubject(false);
453
+ // Last access token written to localStorage from an X-Refreshed-Token response header.
454
+ // Used to collapse N duplicate writes (one per parallel response) into one notifySignedIn call.
455
+ let lastWrittenAccessToken = null;
456
+ // Track if we're currently redirecting to login to prevent loopback storms.
450
457
  let isRedirecting = false;
451
458
  /**
452
- * Functional HTTP interceptor for handling 401/403 auth errors.
453
- * Redirects to login page on 401, and to 403 page on 403.
454
- * Includes loopback prevention to avoid infinite redirects.
459
+ * Functional HTTP interceptor.
460
+ *
461
+ * Responsibilities:
462
+ * 1. Attach `Authorization: Bearer` + `X-Refresh-Token` from localStorage to requests
463
+ * targeting the auth server / trusted hosts (so the server-side Authorizer middleware
464
+ * can perform silent refresh and emit X-Refreshed-* response headers).
465
+ * 2. On every successful response, if `X-Refreshed-Token` is present, write it to
466
+ * localStorage exactly once (dedup across parallel responses).
467
+ * 3. On 401, queue concurrent failures behind a single 1.5s wait so only ONE retry
468
+ * fires per refresh window. Retries strip the stale auth headers so the interceptor
469
+ * re-reads the freshly-stored tokens from localStorage.
470
+ * 4. On 403, redirect to /403.
455
471
  */
456
472
  const mesAuthInterceptor = (req, next) => {
457
473
  const authService = inject(MesAuthService);
458
474
  const router = inject(Router);
459
- // Attach access + refresh tokens from localStorage to requests going to the auth server
460
- // or any backend host running MesAuth.Authorizer (trustedHosts). This lets the authorizer
461
- // middleware silently refresh tokens and emit X-Refreshed-Token response headers.
462
- // Relative URLs are always considered trusted (same origin).
463
475
  const config = authService.getConfig();
464
476
  const apiBase = config?.apiBaseUrl ?? '';
465
477
  const trustedHosts = config?.trustedHosts ?? [];
@@ -473,9 +485,11 @@ const mesAuthInterceptor = (req, next) => {
473
485
  return true; // relative URL — same origin
474
486
  }
475
487
  }
476
- let authReq = req;
477
- if (typeof localStorage !== 'undefined' && isTrusted(req.url)) {
478
- let headers = req.headers;
488
+ // ---- Build the outgoing request, attaching localStorage tokens for trusted hosts.
489
+ function attachAuthHeaders(r) {
490
+ if (typeof localStorage === 'undefined' || !isTrusted(r.url))
491
+ return r;
492
+ let headers = r.headers;
479
493
  if (!headers.has('Authorization')) {
480
494
  const accessToken = localStorage.getItem('mes_auth_token');
481
495
  if (accessToken)
@@ -486,39 +500,50 @@ const mesAuthInterceptor = (req, next) => {
486
500
  if (refreshToken)
487
501
  headers = headers.set('X-Refresh-Token', refreshToken);
488
502
  }
489
- if (headers !== req.headers)
490
- authReq = req.clone({ headers });
503
+ return headers !== r.headers ? r.clone({ headers }) : r;
504
+ }
505
+ // Strip the headers we set so a retry forces re-read from localStorage (which by then
506
+ // contains the refreshed tokens written by the X-Refreshed-Token response handler).
507
+ function stripAuthHeaders(r) {
508
+ return r.clone({
509
+ headers: r.headers.delete('Authorization').delete('X-Refresh-Token')
510
+ });
491
511
  }
512
+ const authReq = attachAuthHeaders(req);
492
513
  return next(authReq).pipe(tap(event => {
493
514
  if (event instanceof HttpResponse) {
494
515
  const newAccessToken = event.headers.get('X-Refreshed-Token');
495
- if (newAccessToken) {
516
+ if (newAccessToken && newAccessToken !== lastWrittenAccessToken) {
517
+ lastWrittenAccessToken = newAccessToken;
496
518
  const newRefreshToken = event.headers.get('X-Refreshed-Refresh-Token') ?? undefined;
497
519
  authService.notifySignedIn(newAccessToken, newRefreshToken);
498
520
  }
499
521
  }
500
522
  }), catchError$1((error) => {
501
523
  const status = error.status;
502
- // Check if we should handle this error and prevent loopback
503
- if ((status === 401 || status === 403) && !isRedirecting) {
504
- const config = authService.getConfig();
505
- const baseUrl = config?.userBaseUrl || '';
506
- const currentUrl = router.url + (window.location.hash || '');
507
- const returnUrl = encodeURIComponent(currentUrl);
508
- // Avoid loops if already on auth/unauth pages
509
- const isLoginPage = currentUrl.includes('/login');
510
- const is403Page = currentUrl.includes('/403');
511
- const isAuthPage = currentUrl.includes('/auth');
512
- // Public pages that should never trigger a 401 redirect (e.g., register, password reset)
513
- const isPublicPage = currentUrl.includes('/register')
514
- || currentUrl.includes('/forgot-password')
515
- || currentUrl.includes('/reset-password');
516
- // Skip redirect for the initial /auth/me check (app startup when not logged in)
517
- const isMeAuthPage = req.url.includes('/auth/me');
518
- if (status === 401 && !isLoginPage && !isAuthPage && !isMeAuthPage && !isPublicPage) {
519
- // Wait 1.5s for the concurrent refresh's Set-Cookie to be processed, then retry once.
520
- // If retry also gets 401, redirect to login.
521
- return timer(1500).pipe(switchMap(() => next(req)), catchError$1((retryError) => {
524
+ if (!(status === 401 || status === 403) || isRedirecting) {
525
+ return throwError(() => error);
526
+ }
527
+ const baseUrl = config?.userBaseUrl || '';
528
+ const currentUrl = router.url + (window.location.hash || '');
529
+ const returnUrl = encodeURIComponent(currentUrl);
530
+ const isLoginPage = currentUrl.includes('/login');
531
+ const is403Page = currentUrl.includes('/403');
532
+ const isAuthPage = currentUrl.includes('/auth');
533
+ const isPublicPage = currentUrl.includes('/register')
534
+ || currentUrl.includes('/forgot-password')
535
+ || currentUrl.includes('/reset-password');
536
+ const isMeAuthPage = req.url.includes('/auth/me');
537
+ if (status === 401 && !isLoginPage && !isAuthPage && !isMeAuthPage && !isPublicPage) {
538
+ // Single-flight: the first 401 opens a 1.5s window during which all other 401s
539
+ // wait for the refresh signal to flip back to false, then retry once.
540
+ if (!refreshing$.value) {
541
+ refreshing$.next(true);
542
+ return timer(1500).pipe(switchMap(() => next(stripAuthHeaders(req))), tap({
543
+ next: () => refreshing$.next(false),
544
+ error: () => refreshing$.next(false),
545
+ complete: () => refreshing$.next(false)
546
+ }), catchError$1((retryError) => {
522
547
  if (retryError.status === 401) {
523
548
  isRedirecting = true;
524
549
  setTimeout(() => { isRedirecting = false; }, 5000);
@@ -527,32 +552,29 @@ const mesAuthInterceptor = (req, next) => {
527
552
  return throwError(() => retryError);
528
553
  }));
529
554
  }
530
- else if (status === 403 && !is403Page) {
531
- isRedirecting = true;
532
- setTimeout(() => { isRedirecting = false; }, 5000);
533
- let redirectUrl = `${baseUrl}/403?returnUrl=${returnUrl}`;
534
- if (error.error && error.error.required) {
535
- redirectUrl += `&required=${encodeURIComponent(error.error.required)}`;
555
+ // Refresh already in flight wait for it to finish, then retry once with
556
+ // headers stripped so the interceptor re-reads the now-fresh tokens.
557
+ return refreshing$.pipe(filter(v => !v), take(1), switchMap(() => next(stripAuthHeaders(req))), catchError$1((retryError) => {
558
+ if (retryError.status === 401 && !isRedirecting) {
559
+ isRedirecting = true;
560
+ setTimeout(() => { isRedirecting = false; }, 5000);
561
+ window.location.href = `${baseUrl}/login?returnUrl=${returnUrl}`;
536
562
  }
537
- window.location.href = redirectUrl;
563
+ return throwError(() => retryError);
564
+ }));
565
+ }
566
+ if (status === 403 && !is403Page) {
567
+ isRedirecting = true;
568
+ setTimeout(() => { isRedirecting = false; }, 5000);
569
+ let redirectUrl = `${baseUrl}/403?returnUrl=${returnUrl}`;
570
+ if (error.error && error.error.required) {
571
+ redirectUrl += `&required=${encodeURIComponent(error.error.required)}`;
538
572
  }
573
+ window.location.href = redirectUrl;
539
574
  }
540
575
  return throwError(() => error);
541
576
  }));
542
577
  };
543
- function appendPermissions(body, allowedActions) {
544
- if (body === null || body === undefined) {
545
- return { 'x-ma-perms': allowedActions };
546
- }
547
- if (Array.isArray(body)) {
548
- return { data: body, 'x-ma-perms': allowedActions };
549
- }
550
- if (typeof body === 'object') {
551
- return { ...body, 'x-ma-perms': allowedActions };
552
- }
553
- // Primitive (string, number, boolean)
554
- return { data: body, 'x-ma-perms': allowedActions };
555
- }
556
578
 
557
579
  class MesAuthModule {
558
580
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MesAuthModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
@@ -772,201 +794,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImpo
772
794
  `, 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"] }]
773
795
  }], propDecorators: { clicked: [{ type: i0.Output, args: ["clicked"] }] } });
774
796
 
775
- class UserProfileComponent {
776
- inputAvatarShape = input('circle', ...(ngDevMode ? [{ debugName: "inputAvatarShape" }] : /* istanbul ignore next */ []));
777
- showBell = input(true, ...(ngDevMode ? [{ debugName: "showBell" }] : /* istanbul ignore next */ []));
778
- showApproval = input(true, ...(ngDevMode ? [{ debugName: "showApproval" }] : /* istanbul ignore next */ []));
779
- showName = input(false, ...(ngDevMode ? [{ debugName: "showName" }] : /* istanbul ignore next */ []));
780
- showAi = input(true, ...(ngDevMode ? [{ debugName: "showAi" }] : /* istanbul ignore next */ []));
781
- showSignature = input([false, false], ...(ngDevMode ? [{ debugName: "showSignature" }] : /* istanbul ignore next */ []));
782
- signatureHeight = input(40, ...(ngDevMode ? [{ debugName: "signatureHeight" }] : /* istanbul ignore next */ []));
783
- notificationClick = output();
784
- approvalClick = output();
785
- aiClick = output();
786
- get themeClass() {
787
- return `theme-${this.themeService.currentTheme()}`;
788
- }
789
- currentUser = signal(null, ...(ngDevMode ? [{ debugName: "currentUser" }] : /* istanbul ignore next */ []));
790
- unreadCount = signal(0, ...(ngDevMode ? [{ debugName: "unreadCount" }] : /* istanbul ignore next */ []));
791
- pendingApprovalCount = signal(0, ...(ngDevMode ? [{ debugName: "pendingApprovalCount" }] : /* istanbul ignore next */ []));
792
- dropdownOpen = signal(false, ...(ngDevMode ? [{ debugName: "dropdownOpen" }] : /* istanbul ignore next */ []));
793
- avatarRefresh = signal(Date.now(), ...(ngDevMode ? [{ debugName: "avatarRefresh" }] : /* istanbul ignore next */ []));
794
- // Avatar style derived from per-user preferences
795
- navAvatarSize = computed(() => this.currentUser()?.avatarSize ?? 'md', ...(ngDevMode ? [{ debugName: "navAvatarSize" }] : /* istanbul ignore next */ []));
796
- // Dropdown is always one step larger than the nav avatar
797
- dropAvatarSize = computed(() => {
798
- const s = this.navAvatarSize();
799
- return s === 'sm' ? 'md' : s === 'md' ? 'lg' : 'xl';
800
- }, ...(ngDevMode ? [{ debugName: "dropAvatarSize" }] : /* istanbul ignore next */ []));
801
- avatarShape = computed(() => this.currentUser()?.avatarShape ?? this.inputAvatarShape(), ...(ngDevMode ? [{ debugName: "avatarShape" }] : /* istanbul ignore next */ []));
802
- avatarFrame = computed(() => this.currentUser()?.avatarFrame ?? null, ...(ngDevMode ? [{ debugName: "avatarFrame" }] : /* istanbul ignore next */ []));
803
- avatarRatio = computed(() => this.currentUser()?.avatarRatio ?? 'ar-11', ...(ngDevMode ? [{ debugName: "avatarRatio" }] : /* istanbul ignore next */ []));
804
- givenStyle = computed(() => this.currentUser()?.givenColor || 'indigo', ...(ngDevMode ? [{ debugName: "givenStyle" }] : /* istanbul ignore next */ []));
805
- signatureBroken = signal(false, ...(ngDevMode ? [{ debugName: "signatureBroken" }] : /* istanbul ignore next */ []));
806
- signatureUrl = computed(() => {
807
- if (!this.showSignature().some(s => s) || this.signatureBroken())
808
- return null;
809
- const baseRaw = this.authService.getConfig()?.apiBaseUrl ?? '';
810
- const base = baseRaw.replace(/\/$/, '');
811
- const u = this.currentUser();
812
- const id = u?.userId ?? u?.id;
813
- if (!id || !base)
814
- return null;
815
- return `${base}/auth/${id}/signature`;
816
- }, ...(ngDevMode ? [{ debugName: "signatureUrl" }] : /* istanbul ignore next */ []));
817
- authService = inject(MesAuthService);
818
- router = inject(Router);
819
- themeService = inject(ThemeService);
820
- http = inject(HttpClient);
821
- constructor() {
822
- const currentUserSig = toSignal(this.authService.currentUser$, { initialValue: null });
823
- const approvalEvent = toSignal(this.authService.approvalEvents$);
824
- const notification = toSignal(this.authService.notifications$);
825
- effect(() => {
826
- const user = currentUserSig();
827
- this.currentUser.set(user);
828
- this.avatarRefresh.set(Date.now());
829
- if (!user) {
830
- this.unreadCount.set(0);
831
- this.pendingApprovalCount.set(0);
832
- return;
833
- }
834
- this.loadUnreadCount();
835
- this.loadPendingApprovalCount();
836
- });
837
- effect(() => {
838
- approvalEvent(); // track SignalR approval events
839
- if (currentUserSig())
840
- this.loadPendingApprovalCount();
841
- });
842
- effect(() => {
843
- notification(); // track SignalR notification events
844
- if (currentUserSig())
845
- this.loadUnreadCount();
846
- });
847
- }
848
- loadUnreadCount() {
849
- this.authService.getUnreadCount().subscribe({
850
- next: (response) => this.unreadCount.set(response.unreadCount || 0),
851
- error: () => { }
852
- });
853
- }
854
- loadPendingApprovalCount() {
855
- const config = this.authService.getConfig();
856
- if (!config)
857
- return;
858
- const url = `${config.apiBaseUrl.replace(/\/$/, '')}/approval/dashboard`;
859
- this.http.get(url, { withCredentials: config.withCredentials ?? true }).subscribe({
860
- next: (r) => this.pendingApprovalCount.set(r?.pendingCount ?? 0),
861
- error: () => { }
862
- });
863
- }
864
- onApprovalClick() {
865
- this.approvalClick.emit();
866
- }
867
- onAiClick() {
868
- this.aiClick.emit();
869
- }
870
- getAvatarUrl(user) {
871
- // Use the refresh signal to force update
872
- const refresh = this.avatarRefresh();
873
- const config = this.authService.getConfig();
874
- const baseUrl = config?.apiBaseUrl || '';
875
- if (user.avatarPath) {
876
- if (user.avatarPath.startsWith('http://') || user.avatarPath.startsWith('https://')) {
877
- return user.avatarPath;
878
- }
879
- return `${baseUrl.replace(/\/$/, '')}${user.avatarPath}?t=${refresh}`;
880
- }
881
- const userId = user.userId;
882
- if (userId && baseUrl) {
883
- return `${baseUrl.replace(/\/$/, '')}/auth/${userId}/avatar?t=${refresh}`;
884
- }
885
- const displayName = user.userName || user.userId || 'User';
886
- return `https://ui-avatars.com/api/?name=${encodeURIComponent(displayName)}&background=1976d2&color=fff`;
887
- }
888
- getLastNameInitial(user) {
889
- const fullName = user.fullName || user.userName || 'U';
890
- const parts = fullName.split(' ');
891
- const lastPart = parts[parts.length - 1];
892
- return lastPart.charAt(0).toUpperCase();
893
- }
894
- toggleDropdown() {
895
- this.dropdownOpen.set(!this.dropdownOpen());
896
- }
897
- closeDropdown() {
898
- this.dropdownOpen.set(false);
899
- }
900
- onDocumentClick(event) {
901
- const target = event.target;
902
- const clickedInside = target.closest('.user-menu-wrapper');
903
- if (!clickedInside) {
904
- this.dropdownOpen.set(false);
905
- return;
906
- }
907
- if (target.closest('.ma-user-menu-btn')) {
908
- this.dropdownOpen.set(false);
909
- }
910
- }
911
- onLogin() {
912
- const config = this.authService.getConfig();
913
- const baseUrl = config?.userBaseUrl || '';
914
- const returnUrl = encodeURIComponent(this.router.url);
915
- window.location.href = `${baseUrl}/login?returnUrl=${returnUrl}`;
916
- }
917
- onViewProfile() {
918
- const config = this.authService.getConfig();
919
- const baseUrl = config?.userBaseUrl || '';
920
- const currentUrl = window.location.href;
921
- this.openInNewTabIfSameOrigin(currentUrl, `${baseUrl}/profile`);
922
- this.dropdownOpen.set(false);
923
- }
924
- openInNewTabIfSameOrigin(currentUrl, destinationUrl) {
925
- // Check if current page URL starts with the destination URL
926
- if (!destinationUrl.startsWith(currentUrl)) {
927
- window.open(destinationUrl, "_blank", "noopener,noreferrer");
928
- }
929
- else {
930
- // Optional: redirect in same tab
931
- window.location.href = destinationUrl;
932
- }
933
- }
934
- onLogout() {
935
- this.authService.logout().subscribe({
936
- next: () => {
937
- this.dropdownOpen.set(false);
938
- const config = this.authService.getConfig();
939
- const baseUrl = config?.userBaseUrl || '';
940
- const returnUrl = encodeURIComponent(window.location.href);
941
- window.location.href = `${baseUrl}/login?returnUrl=${returnUrl}`;
942
- },
943
- error: () => {
944
- const config = this.authService.getConfig();
945
- const baseUrl = config?.userBaseUrl || '';
946
- window.location.href = `${baseUrl}/login`;
947
- }
948
- });
949
- }
950
- onNotificationClick() {
951
- this.notificationClick.emit();
952
- }
953
- onSigErr(_ev) {
954
- this.signatureBroken.set(true);
955
- }
956
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: UserProfileComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
957
- 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"] }] });
958
- }
959
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: UserProfileComponent, decorators: [{
960
- type: Component,
961
- 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"] }]
962
- }], 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: [{
963
- type: HostBinding,
964
- args: ['class']
965
- }], onDocumentClick: [{
966
- type: HostListener,
967
- args: ['document:click', ['$event']]
968
- }] } });
969
-
970
797
  class ToastService {
971
798
  _toasts = signal([], ...(ngDevMode ? [{ debugName: "_toasts" }] : /* istanbul ignore next */ []));
972
799
  toasts = this._toasts.asReadonly();
@@ -993,900 +820,1459 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImpo
993
820
  args: [{ providedIn: 'root' }]
994
821
  }] });
995
822
 
996
- class ToastContainerComponent {
997
- get themeClass() {
998
- return `theme-${this.themeService.currentTheme()}`;
999
- }
1000
- toastService = inject(ToastService);
1001
- themeService = inject(ThemeService);
1002
- toasts = this.toastService.toasts;
1003
- close(id) {
1004
- this.toastService.remove(id);
1005
- }
1006
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: ToastContainerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1007
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: ToastContainerComponent, isStandalone: true, selector: "ma-toast-container", host: { properties: { "class": "this.themeClass" } }, ngImport: i0, template: "<div class=\"toast-container\">\n @for (toast of toasts(); track toast.id) {\n <div class=\"toast toast-{{ toast.type }}\">\n\n <!-- Type icon -->\n <div class=\"toast-icon\">\n @if (toast.type === 'info') {\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=\"M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9\"/><path d=\"M13.73 21a2 2 0 0 1-3.46 0\"/>\n </svg>\n }\n @if (toast.type === 'success') {\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=\"M22 11.08V12a10 10 0 1 1-5.93-9.14\"/><polyline points=\"22 4 12 14.01 9 11.01\"/>\n </svg>\n }\n @if (toast.type === 'warning') {\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=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/><line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"/><line x1=\"12\" y1=\"17\" x2=\"12.01\" y2=\"17\"/>\n </svg>\n }\n @if (toast.type === 'error') {\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 <circle cx=\"12\" cy=\"12\" r=\"10\"/><line x1=\"15\" y1=\"9\" x2=\"9\" y2=\"15\"/><line x1=\"9\" y1=\"9\" x2=\"15\" y2=\"15\"/>\n </svg>\n }\n </div>\n\n <!-- Content -->\n <div class=\"toast-content\">\n @if (toast.title) {\n <div class=\"toast-title\">{{ toast.title }}</div>\n }\n <div class=\"toast-message\" [innerHTML]=\"toast.message\"></div>\n </div>\n\n <!-- Close -->\n <button class=\"toast-close\" (click)=\"close(toast.id)\" aria-label=\"Close\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" 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\n <!-- Auto-dismiss progress bar \u2014 hidden when duration <= 0 (persistent toast) -->\n @if (toast.duration == null || toast.duration > 0) {\n <div class=\"toast-progress\" [style.animation-duration]=\"(toast.duration ?? 5000) + 'ms'\"></div>\n }\n </div>\n }\n</div>\n", styles: [".toast-container{position:fixed;top:20px;right:20px;z-index:9999;pointer-events:none;display:flex;flex-direction:column;gap:10px}.toast{position:relative;display:flex;align-items:flex-start;gap:11px;padding:13px 13px 16px 16px;border-radius:12px;background:var(--bg-primary);border:1px solid var(--border-color);box-shadow:0 8px 28px var(--shadow-lg),0 2px 8px var(--shadow);pointer-events:auto;min-width:300px;max-width:420px;overflow:hidden;animation:toast-in .35s cubic-bezier(.16,1,.3,1)}@keyframes toast-in{0%{opacity:0;transform:translate(36px) scale(.96)}to{opacity:1;transform:translate(0) scale(1)}}.toast:before{content:\"\";position:absolute;left:0;top:0;bottom:0;width:4px;border-radius:12px 0 0 12px}.toast-info:before{background:var(--info-color)}.toast-success:before{background:var(--success-color)}.toast-warning:before{background:var(--warning-color)}.toast-error:before{background:var(--error-color)}.toast-icon{flex-shrink:0;width:34px;height:34px;border-radius:9px;display:flex;align-items:center;justify-content:center;margin-left:2px}.toast-info .toast-icon{color:var(--info-color);background:var(--info-bg)}.toast-success .toast-icon{color:var(--success-color);background:var(--success-bg)}.toast-warning .toast-icon{color:var(--warning-color);background:var(--warning-bg)}.toast-error .toast-icon{color:var(--error-color);background:var(--error-bg)}.toast-content{flex:1;min-width:0;padding-top:1px}.toast-title{font-weight:700;font-size:13.5px;margin-bottom:3px;line-height:1.3}.toast-info .toast-title{color:var(--info-color)}.toast-success .toast-title{color:var(--success-color)}.toast-warning .toast-title{color:var(--warning-color)}.toast-error .toast-title{color:var(--error-color)}.toast-message{font-size:12.5px;line-height:1.45;color:var(--text-secondary)}.toast-message h3{margin:0 0 4px;font-size:13.5px;color:inherit}.toast-close{background:none;border:none;cursor:pointer;color:var(--text-secondary);width:26px;height:26px;border-radius:6px;display:flex;align-items:center;justify-content:center;flex-shrink:0;padding:0;transition:color .15s,background-color .15s}.toast-close:hover{color:var(--text-primary);background:var(--border-color)}.toast-progress{position:absolute;bottom:0;left:4px;right:0;height:3px;border-radius:0 0 12px;animation:toast-progress linear forwards;opacity:.7}.toast-info .toast-progress{background:var(--info-color)}.toast-success .toast-progress{background:var(--success-color)}.toast-warning .toast-progress{background:var(--warning-color)}.toast-error .toast-progress{background:var(--error-color)}@keyframes toast-progress{0%{width:calc(100% - 4px)}to{width:0}}@media(max-width:600px){.toast-container{top:10px;right:10px;left:10px}.toast{min-width:auto;max-width:100%}}\n"] });
1008
- }
1009
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: ToastContainerComponent, decorators: [{
1010
- type: Component,
1011
- args: [{ selector: 'ma-toast-container', imports: [], template: "<div class=\"toast-container\">\n @for (toast of toasts(); track toast.id) {\n <div class=\"toast toast-{{ toast.type }}\">\n\n <!-- Type icon -->\n <div class=\"toast-icon\">\n @if (toast.type === 'info') {\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=\"M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9\"/><path d=\"M13.73 21a2 2 0 0 1-3.46 0\"/>\n </svg>\n }\n @if (toast.type === 'success') {\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=\"M22 11.08V12a10 10 0 1 1-5.93-9.14\"/><polyline points=\"22 4 12 14.01 9 11.01\"/>\n </svg>\n }\n @if (toast.type === 'warning') {\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=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/><line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"/><line x1=\"12\" y1=\"17\" x2=\"12.01\" y2=\"17\"/>\n </svg>\n }\n @if (toast.type === 'error') {\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 <circle cx=\"12\" cy=\"12\" r=\"10\"/><line x1=\"15\" y1=\"9\" x2=\"9\" y2=\"15\"/><line x1=\"9\" y1=\"9\" x2=\"15\" y2=\"15\"/>\n </svg>\n }\n </div>\n\n <!-- Content -->\n <div class=\"toast-content\">\n @if (toast.title) {\n <div class=\"toast-title\">{{ toast.title }}</div>\n }\n <div class=\"toast-message\" [innerHTML]=\"toast.message\"></div>\n </div>\n\n <!-- Close -->\n <button class=\"toast-close\" (click)=\"close(toast.id)\" aria-label=\"Close\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" 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\n <!-- Auto-dismiss progress bar \u2014 hidden when duration <= 0 (persistent toast) -->\n @if (toast.duration == null || toast.duration > 0) {\n <div class=\"toast-progress\" [style.animation-duration]=\"(toast.duration ?? 5000) + 'ms'\"></div>\n }\n </div>\n }\n</div>\n", styles: [".toast-container{position:fixed;top:20px;right:20px;z-index:9999;pointer-events:none;display:flex;flex-direction:column;gap:10px}.toast{position:relative;display:flex;align-items:flex-start;gap:11px;padding:13px 13px 16px 16px;border-radius:12px;background:var(--bg-primary);border:1px solid var(--border-color);box-shadow:0 8px 28px var(--shadow-lg),0 2px 8px var(--shadow);pointer-events:auto;min-width:300px;max-width:420px;overflow:hidden;animation:toast-in .35s cubic-bezier(.16,1,.3,1)}@keyframes toast-in{0%{opacity:0;transform:translate(36px) scale(.96)}to{opacity:1;transform:translate(0) scale(1)}}.toast:before{content:\"\";position:absolute;left:0;top:0;bottom:0;width:4px;border-radius:12px 0 0 12px}.toast-info:before{background:var(--info-color)}.toast-success:before{background:var(--success-color)}.toast-warning:before{background:var(--warning-color)}.toast-error:before{background:var(--error-color)}.toast-icon{flex-shrink:0;width:34px;height:34px;border-radius:9px;display:flex;align-items:center;justify-content:center;margin-left:2px}.toast-info .toast-icon{color:var(--info-color);background:var(--info-bg)}.toast-success .toast-icon{color:var(--success-color);background:var(--success-bg)}.toast-warning .toast-icon{color:var(--warning-color);background:var(--warning-bg)}.toast-error .toast-icon{color:var(--error-color);background:var(--error-bg)}.toast-content{flex:1;min-width:0;padding-top:1px}.toast-title{font-weight:700;font-size:13.5px;margin-bottom:3px;line-height:1.3}.toast-info .toast-title{color:var(--info-color)}.toast-success .toast-title{color:var(--success-color)}.toast-warning .toast-title{color:var(--warning-color)}.toast-error .toast-title{color:var(--error-color)}.toast-message{font-size:12.5px;line-height:1.45;color:var(--text-secondary)}.toast-message h3{margin:0 0 4px;font-size:13.5px;color:inherit}.toast-close{background:none;border:none;cursor:pointer;color:var(--text-secondary);width:26px;height:26px;border-radius:6px;display:flex;align-items:center;justify-content:center;flex-shrink:0;padding:0;transition:color .15s,background-color .15s}.toast-close:hover{color:var(--text-primary);background:var(--border-color)}.toast-progress{position:absolute;bottom:0;left:4px;right:0;height:3px;border-radius:0 0 12px;animation:toast-progress linear forwards;opacity:.7}.toast-info .toast-progress{background:var(--info-color)}.toast-success .toast-progress{background:var(--success-color)}.toast-warning .toast-progress{background:var(--warning-color)}.toast-error .toast-progress{background:var(--error-color)}@keyframes toast-progress{0%{width:calc(100% - 4px)}to{width:0}}@media(max-width:600px){.toast-container{top:10px;right:10px;left:10px}.toast{min-width:auto;max-width:100%}}\n"] }]
1012
- }], propDecorators: { themeClass: [{
1013
- type: HostBinding,
1014
- args: ['class']
1015
- }] } });
1016
-
1017
- class NotificationPanelComponent {
1018
- notificationRead = output();
1019
- get themeClass() {
1020
- return `theme-${this.themeService.currentTheme()}`;
1021
- }
1022
- isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
1023
- activeTab = signal('unread', ...(ngDevMode ? [{ debugName: "activeTab" }] : /* istanbul ignore next */ []));
1024
- notifications = signal([], ...(ngDevMode ? [{ debugName: "notifications" }] : /* istanbul ignore next */ []));
1025
- selectedNotification = signal(null, ...(ngDevMode ? [{ debugName: "selectedNotification" }] : /* istanbul ignore next */ []));
1026
- selectedNotificationHtml = signal(null, ...(ngDevMode ? [{ debugName: "selectedNotificationHtml" }] : /* istanbul ignore next */ []));
1027
- selectedNotificationDate = signal('', ...(ngDevMode ? [{ debugName: "selectedNotificationDate" }] : /* istanbul ignore next */ []));
1028
- // Stable time-ago strings keyed by notification id - refreshed every 30s via signal
1029
- dateLabels = signal(new Map(), ...(ngDevMode ? [{ debugName: "dateLabels" }] : /* istanbul ignore next */ []));
1030
- unreadNotifications = computed(() => this.notifications().filter(n => !n.isRead), ...(ngDevMode ? [{ debugName: "unreadNotifications" }] : /* istanbul ignore next */ []));
1031
- readNotifications = computed(() => this.notifications().filter(n => n.isRead), ...(ngDevMode ? [{ debugName: "readNotifications" }] : /* istanbul ignore next */ []));
1032
- currentNotifications = computed(() => this.activeTab() === 'unread' ? this.unreadNotifications() : this.readNotifications(), ...(ngDevMode ? [{ debugName: "currentNotifications" }] : /* istanbul ignore next */ []));
1033
- dateTimer = null;
1034
- // Normalize type to string - API may return integer or string
1035
- // Backend enum: Info=0, Success=1, Warning=2, Error=3
1036
- typeOf(notification) {
1037
- const t = notification.type;
1038
- if (t === 0 || t === 'Info')
1039
- return 'Info';
1040
- if (t === 1 || t === 'Success')
1041
- return 'Success';
1042
- if (t === 2 || t === 'Warning')
1043
- return 'Warning';
1044
- if (t === 3 || t === 'Error')
1045
- return 'Error';
1046
- return 'Info';
1047
- }
1048
- toastType(type) {
1049
- const t = this.typeOf({ type });
1050
- return t.toLowerCase();
823
+ /**
824
+ * Resolves the full set of client tools available to the AI: built-ins + consumer-registered.
825
+ * Handlers are invoked through `runTool(name, args)` which automatically threads the Injector
826
+ * so consumer handlers can use inject() patterns.
827
+ */
828
+ class MaAiToolsRegistry {
829
+ config = inject(MES_AUTH_AI_CONFIG, { optional: true }) ?? DEFAULT_AI_CONFIG;
830
+ injector = inject(Injector);
831
+ /** Built-in client tools shipped by the library. */
832
+ builtIn = [
833
+ {
834
+ name: 'navigate',
835
+ description: 'Navigate the user to a path within the application. Use relative paths starting with /.',
836
+ parameters: {
837
+ type: 'object',
838
+ properties: {
839
+ path: { type: 'string', description: 'Path to navigate to, e.g. "/auth/users"' }
840
+ },
841
+ required: ['path']
842
+ },
843
+ readOnly: true,
844
+ handler: (args) => {
845
+ const router = this.injector.get(Router, null);
846
+ if (!router)
847
+ return 'Router is not available in this app.';
848
+ router.navigateByUrl(args.path);
849
+ return `Navigated to ${args.path}`;
850
+ }
851
+ },
852
+ {
853
+ name: 'toggle_theme',
854
+ description: 'Toggle between light and dark theme.',
855
+ parameters: { type: 'object', properties: {} },
856
+ readOnly: true,
857
+ handler: () => {
858
+ const theme = this.injector.get(ThemeService);
859
+ const next = theme.currentTheme() === 'light' ? 'dark' : 'light';
860
+ theme.setFixTheme(next);
861
+ return `Theme switched to ${next}.`;
862
+ }
863
+ },
864
+ {
865
+ name: 'show_toast',
866
+ description: 'Show a small toast notification at the top of the screen.',
867
+ parameters: {
868
+ type: 'object',
869
+ properties: {
870
+ message: { type: 'string', description: 'Toast body text' },
871
+ title: { type: 'string', description: 'Optional title' },
872
+ severity: { type: 'string', enum: ['info', 'success', 'warning', 'error'], description: 'Toast severity' }
873
+ },
874
+ required: ['message']
875
+ },
876
+ readOnly: true,
877
+ handler: (args) => {
878
+ const toast = this.injector.get(ToastService);
879
+ toast.show(args.message, args.title, args.severity ?? 'info');
880
+ return 'Toast shown.';
881
+ }
882
+ },
883
+ {
884
+ name: 'who_am_i_client',
885
+ description: 'Get the currently signed-in user as seen by the browser (id, name, roles loaded so far).',
886
+ parameters: { type: 'object', properties: {} },
887
+ readOnly: true,
888
+ handler: () => {
889
+ const auth = this.injector.get(MesAuthService);
890
+ const u = auth.currentUser;
891
+ if (!u)
892
+ return 'No user signed in.';
893
+ return JSON.stringify({
894
+ id: u.userId ?? u.id,
895
+ userName: u.userName,
896
+ fullName: u.fullName,
897
+ department: u.department,
898
+ position: u.position,
899
+ email: u.email
900
+ });
901
+ }
902
+ },
903
+ {
904
+ name: 'reload_page',
905
+ description: 'Reload the current browser page. Use as a last resort if state seems stuck.',
906
+ parameters: { type: 'object', properties: {} },
907
+ readOnly: false,
908
+ handler: () => {
909
+ window.location.reload();
910
+ return 'Reloading page…';
911
+ }
912
+ }
913
+ ];
914
+ /** All tools, deduplicated by name (consumer wins on conflict). */
915
+ list() {
916
+ const fromConfig = this.config.tools ?? [];
917
+ const byName = new Map();
918
+ for (const t of this.builtIn)
919
+ byName.set(t.name, t);
920
+ for (const t of fromConfig)
921
+ byName.set(t.name, t);
922
+ return Array.from(byName.values());
1051
923
  }
1052
- sanitizer = inject(DomSanitizer);
1053
- authService = inject(MesAuthService);
1054
- toastService = inject(ToastService);
1055
- themeService = inject(ThemeService);
1056
- host = inject(ElementRef);
1057
- constructor() {
1058
- this.loadNotifications();
1059
- // Refresh time-ago labels every 30s - signal mutation triggers CD in zoneless
1060
- this.dateTimer = setInterval(() => this.refreshDateLabels(), 30000);
1061
- const latestNotification = toSignal(this.authService.notifications$);
1062
- effect(() => {
1063
- const notification = latestNotification();
1064
- if (!notification)
1065
- return;
1066
- this.toastService.show(notification.message || '', notification.title, this.toastType(notification.type), 5000);
1067
- this.loadNotifications();
1068
- });
1069
- // Close when the user clicks outside the panel - matches ma-approval-panel UX.
1070
- effect((onCleanup) => {
1071
- if (!this.isOpen())
1072
- return;
1073
- const onDocClick = (ev) => {
1074
- const panel = this.host.nativeElement.querySelector('.notification-panel');
1075
- if (panel && !panel.contains(ev.target))
1076
- this.close();
1077
- };
1078
- // Defer to skip the same click that opened the panel.
1079
- const tid = setTimeout(() => document.addEventListener('mousedown', onDocClick), 0);
1080
- onCleanup(() => {
1081
- clearTimeout(tid);
1082
- document.removeEventListener('mousedown', onDocClick);
1083
- });
1084
- });
924
+ resolve(name) {
925
+ return this.list().find(t => t.name === name);
1085
926
  }
1086
- ngOnDestroy() {
1087
- if (this.dateTimer !== null) {
1088
- clearInterval(this.dateTimer);
1089
- this.dateTimer = null;
927
+ /** Run a registered client tool and serialize its result for posting back to the agent loop. */
928
+ async runTool(name, args) {
929
+ const tool = this.resolve(name);
930
+ if (!tool)
931
+ return { ok: false, result: `Unknown client tool: ${name}` };
932
+ try {
933
+ const raw = await Promise.resolve(tool.handler(args ?? {}));
934
+ const result = typeof raw === 'string' ? raw : JSON.stringify(raw ?? null);
935
+ return { ok: true, result };
936
+ }
937
+ catch (err) {
938
+ return { ok: false, result: err?.message ?? String(err) };
1090
939
  }
1091
940
  }
1092
- loadNotifications() {
1093
- this.authService.getNotifications(1, 50, true).subscribe({
1094
- next: (response) => {
1095
- this.notifications.set(response.items || []);
1096
- this.refreshDateLabels();
1097
- },
1098
- error: () => { }
1099
- });
941
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiToolsRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
942
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiToolsRegistry, providedIn: 'root' });
943
+ }
944
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiToolsRegistry, decorators: [{
945
+ type: Injectable,
946
+ args: [{ providedIn: 'root' }]
947
+ }] });
948
+
949
+ /**
950
+ * Drives the chat panel.
951
+ *
952
+ * Uses `fetch` + `ReadableStream` (not `EventSource`, which can't POST a body) to talk to
953
+ * `/ai/chat` SSE. State lives in signals so the panel can re-render reactively.
954
+ */
955
+ class MaAiService {
956
+ auth = inject(MesAuthService);
957
+ aiConfig = inject(MES_AUTH_AI_CONFIG, { optional: true }) ?? DEFAULT_AI_CONFIG;
958
+ tools = inject(MaAiToolsRegistry);
959
+ messages = signal([], ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
960
+ streaming = signal(false, ...(ngDevMode ? [{ debugName: "streaming" }] : /* istanbul ignore next */ []));
961
+ /** When non-null, a write-class client tool is awaiting user confirmation. */
962
+ pendingApproval = signal(null, ...(ngDevMode ? [{ debugName: "pendingApproval" }] : /* istanbul ignore next */ []));
963
+ lastError = signal(null, ...(ngDevMode ? [{ debugName: "lastError" }] : /* istanbul ignore next */ []));
964
+ sessionId = null;
965
+ /** Verbs the user already chose "always approve" for in this session. */
966
+ alwaysApproved = new Set();
967
+ abortController = null;
968
+ /** ID of the current assistant bubble we're streaming tokens into. */
969
+ currentAssistantId = null;
970
+ get enabled() {
971
+ return this.aiConfig.enabled !== false;
1100
972
  }
1101
- open() {
1102
- this.isOpen.set(true);
1103
- this.activeTab.set('unread');
973
+ /** Start a brand-new conversation. */
974
+ resetSession() {
975
+ this.cancel();
976
+ this.sessionId = null;
977
+ this.messages.set([]);
978
+ this.pendingApproval.set(null);
979
+ this.lastError.set(null);
980
+ this.alwaysApproved.clear();
1104
981
  }
1105
- close() {
1106
- this.isOpen.set(false);
982
+ /** Cancel the in-flight stream (if any). The session id is preserved so the user can resume. */
983
+ cancel() {
984
+ if (this.abortController) {
985
+ this.abortController.abort();
986
+ this.abortController = null;
987
+ }
988
+ this.streaming.set(false);
989
+ this.currentAssistantId = null;
990
+ // If a client-tool call was awaiting the user, clearing it lets them start over.
991
+ this.pendingApproval.set(null);
1107
992
  }
1108
- switchTab(tab) {
1109
- this.activeTab.set(tab);
993
+ /** User typed and submitted a message. */
994
+ async send(text, currentRoute) {
995
+ if (!text.trim())
996
+ return;
997
+ this.lastError.set(null);
998
+ this.appendBubble({ id: this.uid(), role: 'user', text });
999
+ await this.runStream({ message: text }, currentRoute);
1110
1000
  }
1111
- openDetails(notification) {
1112
- this.selectedNotification.set(notification);
1113
- const html = notification.messageHtml || notification.message || '';
1114
- this.selectedNotificationHtml.set(this.sanitizer.bypassSecurityTrustHtml(html));
1115
- this.selectedNotificationDate.set(this.formatDate(notification.createdAt));
1116
- if (!notification.isRead) {
1117
- this.authService.markAsRead(notification.id).subscribe({
1118
- next: () => {
1119
- this.notifications.update(notifs => notifs.map(n => n.id === notification.id ? { ...n, isRead: true } : n));
1120
- this.notificationRead.emit();
1121
- },
1122
- error: () => { }
1123
- });
1001
+ /**
1002
+ * Approve (or decline) a pending client tool call. If approved (and alwaysApprove),
1003
+ * remember the verb so we skip the prompt next time in this session.
1004
+ */
1005
+ async resolvePendingApproval(decision) {
1006
+ const pending = this.pendingApproval();
1007
+ if (!pending)
1008
+ return;
1009
+ this.pendingApproval.set(null);
1010
+ if (decision === 'decline') {
1011
+ await this.continueWithToolResult(pending.callId, false, 'User declined this tool call.');
1012
+ this.markToolStatus(pending.callId, 'declined');
1013
+ return;
1014
+ }
1015
+ if (decision === 'always') {
1016
+ this.alwaysApproved.add(this.verbOf(pending.name));
1124
1017
  }
1018
+ await this.executeClientTool(pending.callId, pending.name, pending.args);
1125
1019
  }
1126
- closeDetails() {
1127
- this.selectedNotification.set(null);
1128
- this.selectedNotificationHtml.set(null);
1129
- this.selectedNotificationDate.set('');
1130
- }
1131
- openUrl() {
1132
- const url = this.selectedNotification()?.url?.trim();
1133
- if (url) {
1134
- window.open(url, '_blank', 'noopener,noreferrer');
1020
+ // ── private ────────────────────────────────────────────────────────────────
1021
+ async runStream(payload, currentRoute) {
1022
+ if (!this.enabled) {
1023
+ this.lastError.set('AI assistant is disabled.');
1024
+ return;
1025
+ }
1026
+ const config = this.auth.getConfig();
1027
+ const base = config?.apiBaseUrl?.replace(/\/$/, '') ?? '';
1028
+ if (!base) {
1029
+ this.lastError.set('MesAuth is not configured.');
1030
+ return;
1031
+ }
1032
+ // Start a fresh assistant bubble that we stream tokens into.
1033
+ const assistantId = this.uid();
1034
+ this.currentAssistantId = assistantId;
1035
+ this.appendBubble({ id: assistantId, role: 'assistant', text: '', pending: true, toolEvents: [] });
1036
+ this.streaming.set(true);
1037
+ this.abortController = new AbortController();
1038
+ try {
1039
+ const res = await fetch(`${base}/ai/chat`, {
1040
+ method: 'POST',
1041
+ headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream' },
1042
+ credentials: config?.withCredentials !== false ? 'include' : 'same-origin',
1043
+ body: JSON.stringify({
1044
+ sessionId: this.sessionId,
1045
+ message: payload.message,
1046
+ toolResult: payload.toolResult,
1047
+ clientTools: this.tools.list().map(t => ({
1048
+ name: t.name,
1049
+ description: t.description,
1050
+ parameters: t.parameters,
1051
+ readOnly: t.readOnly === true
1052
+ })),
1053
+ context: { currentRoute, appName: this.aiConfig.appName }
1054
+ }),
1055
+ signal: this.abortController.signal
1056
+ });
1057
+ if (!res.ok || !res.body) {
1058
+ const detail = await res.text().catch(() => '');
1059
+ throw new Error(`AI request failed (${res.status}). ${detail}`);
1060
+ }
1061
+ await this.readSse(res.body);
1062
+ }
1063
+ catch (err) {
1064
+ if (err?.name === 'AbortError') {
1065
+ // user cancelled
1066
+ }
1067
+ else {
1068
+ this.lastError.set(err?.message ?? String(err));
1069
+ this.finalizeAssistant(`[error: ${err?.message ?? err}]`);
1070
+ }
1071
+ }
1072
+ finally {
1073
+ this.streaming.set(false);
1074
+ this.abortController = null;
1135
1075
  }
1136
1076
  }
1137
- markAsRead(notificationId, event) {
1138
- if (event)
1139
- event.stopPropagation();
1140
- this.authService.markAsRead(notificationId).subscribe({
1141
- next: () => {
1142
- this.notifications.update(notifs => notifs.map(n => n.id === notificationId ? { ...n, isRead: true } : n));
1143
- this.notificationRead.emit();
1144
- },
1145
- error: () => { }
1146
- });
1147
- }
1148
- markAllAsRead() {
1149
- this.authService.markAllAsRead().subscribe({
1150
- next: () => {
1151
- this.notifications.update(notifs => notifs.map(n => ({ ...n, isRead: true })));
1152
- this.notificationRead.emit();
1153
- },
1154
- error: () => { }
1155
- });
1156
- }
1157
- deleteAllRead() {
1158
- const readIds = this.notifications().filter(n => n.isRead).map(n => n.id);
1159
- const deletePromises = readIds.map(id => firstValueFrom(this.authService.deleteNotification(id)));
1160
- Promise.all(deletePromises).then(() => {
1161
- this.notifications.update(notifs => notifs.filter(n => !n.isRead));
1162
- }).catch(() => {
1163
- this.loadNotifications();
1164
- });
1077
+ async readSse(body) {
1078
+ const reader = body.getReader();
1079
+ const decoder = new TextDecoder();
1080
+ let buffer = '';
1081
+ while (true) {
1082
+ const { value, done } = await reader.read();
1083
+ if (done)
1084
+ break;
1085
+ buffer += decoder.decode(value, { stream: true });
1086
+ // SSE events are separated by a blank line.
1087
+ let idx;
1088
+ while ((idx = buffer.indexOf('\n\n')) !== -1) {
1089
+ const rawEvent = buffer.slice(0, idx);
1090
+ buffer = buffer.slice(idx + 2);
1091
+ const dataLine = rawEvent.split('\n').find(l => l.startsWith('data:'));
1092
+ if (!dataLine)
1093
+ continue;
1094
+ const json = dataLine.slice(5).trim();
1095
+ if (!json)
1096
+ continue;
1097
+ let ev;
1098
+ try {
1099
+ ev = JSON.parse(json);
1100
+ }
1101
+ catch {
1102
+ continue;
1103
+ }
1104
+ await this.handleEvent(ev);
1105
+ }
1106
+ }
1165
1107
  }
1166
- deleteAllUnread() {
1167
- const unreadIds = this.notifications().filter(n => !n.isRead).map(n => n.id);
1168
- const deletePromises = unreadIds.map(id => firstValueFrom(this.authService.deleteNotification(id)));
1169
- Promise.all(deletePromises).then(() => {
1170
- this.notifications.update(notifs => notifs.filter(n => n.isRead));
1171
- this.notificationRead.emit();
1172
- }).catch(() => {
1173
- this.loadNotifications();
1174
- });
1108
+ async handleEvent(ev) {
1109
+ switch (ev.type) {
1110
+ case 'session':
1111
+ this.sessionId = ev.sessionId;
1112
+ break;
1113
+ case 'token':
1114
+ this.appendToken(ev.text);
1115
+ break;
1116
+ case 'tool_call_started':
1117
+ this.appendToolEvent({
1118
+ id: ev.id,
1119
+ name: ev.name,
1120
+ side: ev.side,
1121
+ status: 'running',
1122
+ args: ev.args,
1123
+ readOnly: ev.readOnly
1124
+ });
1125
+ break;
1126
+ case 'tool_call_completed':
1127
+ this.markToolStatus(ev.id, ev.ok ? 'ok' : 'error', ev.summary);
1128
+ break;
1129
+ case 'client_tool_call':
1130
+ this.appendToolEvent({
1131
+ id: ev.id,
1132
+ name: ev.name,
1133
+ side: 'client',
1134
+ status: ev.readOnly ? 'running' : 'awaiting-approval',
1135
+ args: ev.args,
1136
+ readOnly: ev.readOnly
1137
+ });
1138
+ if (ev.readOnly || this.alwaysApproved.has(this.verbOf(ev.name))) {
1139
+ // Auto-run; the next /ai/chat call will deliver the result.
1140
+ await this.executeClientTool(ev.id, ev.name, ev.args);
1141
+ }
1142
+ else {
1143
+ this.pendingApproval.set({ callId: ev.id, name: ev.name, args: ev.args, side: 'client' });
1144
+ }
1145
+ break;
1146
+ case 'error':
1147
+ this.lastError.set(ev.message);
1148
+ this.finalizeAssistant(`[error: ${ev.message}]`);
1149
+ break;
1150
+ case 'done':
1151
+ this.finalizeAssistant();
1152
+ break;
1153
+ }
1175
1154
  }
1176
- delete(notificationId, event) {
1177
- event.stopPropagation();
1178
- const wasUnread = this.notifications().some(n => n.id === notificationId && !n.isRead);
1179
- this.authService.deleteNotification(notificationId).subscribe({
1180
- next: () => {
1181
- this.notifications.update(notifs => notifs.filter(n => n.id !== notificationId));
1182
- if (wasUnread)
1183
- this.notificationRead.emit();
1184
- },
1185
- error: () => { }
1186
- });
1155
+ async executeClientTool(callId, name, args) {
1156
+ this.markToolStatus(callId, 'running');
1157
+ const { ok, result } = await this.tools.runTool(name, args);
1158
+ this.markToolStatus(callId, ok ? 'ok' : 'error', this.summarize(result));
1159
+ await this.continueWithToolResult(callId, ok, result);
1187
1160
  }
1188
- formatDate(dateString) {
1189
- return this.computeTimeAgo(dateString, new Date());
1161
+ async continueWithToolResult(callId, ok, result) {
1162
+ // Open a new SSE stream that delivers the tool result.
1163
+ await this.runStream({ toolResult: { callId, ok, result } });
1190
1164
  }
1191
- // Pure computation - takes now as param so it never calls new Date() internally
1192
- computeTimeAgo(dateString, now) {
1193
- const normalizedDateString = this.parseUtcDate(dateString);
1194
- const date = new Date(normalizedDateString);
1195
- if (isNaN(date.getTime()))
1196
- return 'Invalid date';
1197
- const diffMs = now.getTime() - date.getTime();
1198
- const diffMins = Math.floor(diffMs / 60000);
1199
- const diffHours = Math.floor(diffMs / 3600000);
1200
- const diffDays = Math.floor(diffMs / 86400000);
1201
- if (diffMins < 1)
1202
- return 'Now';
1203
- if (diffMins < 60)
1204
- return `${diffMins}m ago`;
1205
- if (diffHours < 24)
1206
- return `${diffHours}h ago`;
1207
- if (diffDays < 7)
1208
- return `${diffDays}d ago`;
1209
- return date.toLocaleDateString();
1165
+ // ── bubble helpers ────────────────────────────────────────────────────────
1166
+ appendBubble(b) {
1167
+ this.messages.update(arr => [...arr, b]);
1210
1168
  }
1211
- // Rebuild dateLabels map and store as a new signal value so CD is triggered
1212
- refreshDateLabels() {
1213
- const now = new Date();
1214
- const newLabels = new Map();
1215
- for (const n of this.notifications()) {
1216
- newLabels.set(n.id, this.computeTimeAgo(n.createdAt, now));
1169
+ appendToken(text) {
1170
+ if (!this.currentAssistantId) {
1171
+ const id = this.uid();
1172
+ this.currentAssistantId = id;
1173
+ this.appendBubble({ id, role: 'assistant', text, pending: true, toolEvents: [] });
1174
+ return;
1217
1175
  }
1218
- this.dateLabels.set(newLabels);
1176
+ const id = this.currentAssistantId;
1177
+ this.messages.update(arr => arr.map(m => m.id === id ? { ...m, text: m.text + text } : m));
1219
1178
  }
1220
- // Parse date string from server (stored in UTC but without 'Z' suffix or 'T' separator)
1221
- parseUtcDate(dateStr) {
1222
- let normalized = dateStr.includes('T') ? dateStr : dateStr.replace(' ', 'T');
1223
- if (!normalized.endsWith('Z') && !normalized.includes('+') && !normalized.includes('-', 10)) {
1224
- normalized += 'Z';
1179
+ appendToolEvent(ev) {
1180
+ if (!this.currentAssistantId) {
1181
+ const id = this.uid();
1182
+ this.currentAssistantId = id;
1183
+ this.appendBubble({ id, role: 'assistant', text: '', pending: true, toolEvents: [ev] });
1184
+ return;
1225
1185
  }
1226
- return normalized;
1186
+ const id = this.currentAssistantId;
1187
+ this.messages.update(arr => arr.map(m => m.id === id
1188
+ ? { ...m, toolEvents: [...(m.toolEvents ?? []), ev] }
1189
+ : m));
1227
1190
  }
1228
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: NotificationPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1229
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: NotificationPanelComponent, isStandalone: true, selector: "ma-notification-panel", outputs: { notificationRead: "notificationRead" }, host: { properties: { "class": "this.themeClass" } }, ngImport: i0, template: "<div class=\"notification-panel\" [class.open]=\"isOpen()\">\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=\"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 <h3>Notifications</h3>\n </div>\n <button class=\"close-btn\" (click)=\"close()\" title=\"Close\" aria-label=\"Close notifications\">\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() === 'unread'\" (click)=\"switchTab('unread')\">\n Unread\n @if (unreadNotifications().length > 0) {\n <span class=\"tab-badge\">{{ unreadNotifications().length }}</span>\n }\n </button>\n <button class=\"tab-btn\" [class.active]=\"activeTab() === 'read'\" (click)=\"switchTab('read')\">\n Read\n @if (readNotifications().length > 0) {\n <span class=\"tab-badge read-badge\">{{ readNotifications().length }}</span>\n }\n </button>\n </div>\n\n <!-- Notifications List -->\n <div class=\"notifications-list\">\n @if (currentNotifications().length > 0) {\n @for (notification of currentNotifications(); track notification.id) {\n <div\n class=\"notification-item\"\n [class.unread]=\"!notification.isRead\"\n (click)=\"openDetails(notification)\"\n >\n @let t = typeOf(notification);\n <div class=\"notif-accent\"\n [class.type-info]=\"t === 'Info'\"\n [class.type-success]=\"t === 'Success'\"\n [class.type-warning]=\"t === 'Warning'\"\n [class.type-error]=\"t === 'Error'\"></div>\n <div class=\"notif-type-icon\"\n [class.type-info]=\"t === 'Info'\"\n [class.type-success]=\"t === 'Success'\"\n [class.type-warning]=\"t === 'Warning'\"\n [class.type-error]=\"t === 'Error'\">\n @if (t === 'Info') {\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" 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\"/><path d=\"M13.73 21a2 2 0 0 1-3.46 0\"/>\n </svg>\n }\n @if (t === 'Success') {\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"20 6 9 17 4 12\"/>\n </svg>\n }\n @if (t === 'Warning') {\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/><line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"/><line x1=\"12\" y1=\"17\" x2=\"12.01\" y2=\"17\"/>\n </svg>\n }\n @if (t === 'Error') {\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"12\" cy=\"12\" r=\"10\"/><line x1=\"15\" y1=\"9\" x2=\"9\" y2=\"15\"/><line x1=\"9\" y1=\"9\" x2=\"15\" y2=\"15\"/>\n </svg>\n }\n </div>\n <div class=\"notification-content\">\n <div class=\"notification-title\">{{ notification.title }}</div>\n <div class=\"notification-message\">{{ notification.message }}</div>\n <div class=\"notification-meta\">\n <span class=\"app-name\">{{ notification.sourceAppName }}</span>\n <span class=\"time\">{{ dateLabels().get(notification.id) }}</span>\n </div>\n </div>\n @if (!notification.isRead) {\n <button class=\"icon-btn read-btn\" (click)=\"markAsRead(notification.id, $event)\" title=\"Mark as read\" aria-label=\"Mark as read\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"20 6 9 17 4 12\"/>\n </svg>\n </button>\n }\n @if (notification.isRead) {\n <button class=\"icon-btn delete-btn\" (click)=\"delete(notification.id, $event)\" title=\"Delete\" aria-label=\"Delete notification\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"3 6 5 6 21 6\"/><path d=\"M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6\"/><path d=\"M10 11v6\"/><path d=\"M14 11v6\"/><path d=\"M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2\"/>\n </svg>\n </button>\n }\n </div>\n }\n } @else {\n <div class=\"empty-state\">\n <svg class=\"empty-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"44\" height=\"44\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" 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 <p>No {{ activeTab() }} notifications</p>\n </div>\n }\n </div>\n\n <!-- Footer Actions -->\n @if (currentNotifications().length > 0) {\n <div class=\"panel-footer\">\n @if (activeTab() === 'unread') {\n <div class=\"footer-actions\">\n @if (unreadNotifications().length > 0) {\n <button class=\"action-btn\" (click)=\"markAllAsRead()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"/></svg>\n Mark all read\n </button>\n <button class=\"action-btn delete-all-btn\" (click)=\"deleteAllUnread()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"3 6 5 6 21 6\"/><path d=\"M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6\"/></svg>\n Delete all\n </button>\n }\n </div>\n }\n @if (activeTab() === 'read' && readNotifications().length > 0) {\n <button class=\"action-btn delete-all-btn\" (click)=\"deleteAllRead()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"3 6 5 6 21 6\"/><path d=\"M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6\"/></svg>\n Delete all\n </button>\n }\n </div>\n }\n</div>\n\n<!-- Details Modal -->\n@if (selectedNotification()) {\n <div class=\"modal-overlay\" (click)=\"closeDetails()\">\n <div class=\"modal-container\" (click)=\"$event.stopPropagation()\">\n <div class=\"modal-header\"\n [class.modal-type-info]=\"typeOf(selectedNotification()!) === 'Info'\"\n [class.modal-type-success]=\"typeOf(selectedNotification()!) === 'Success'\"\n [class.modal-type-warning]=\"typeOf(selectedNotification()!) === 'Warning'\"\n [class.modal-type-error]=\"typeOf(selectedNotification()!) === 'Error'\">\n <div class=\"modal-header-left\">\n <div class=\"modal-type-icon\">\n @if (typeOf(selectedNotification()!) === 'Info') {\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=\"M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9\"/><path d=\"M13.73 21a2 2 0 0 1-3.46 0\"/>\n </svg>\n }\n @if (typeOf(selectedNotification()!) === 'Success') {\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=\"M22 11.08V12a10 10 0 1 1-5.93-9.14\"/><polyline points=\"22 4 12 14.01 9 11.01\"/>\n </svg>\n }\n @if (typeOf(selectedNotification()!) === 'Warning') {\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=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/><line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"/>\n </svg>\n }\n @if (typeOf(selectedNotification()!) === 'Error') {\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 <circle cx=\"12\" cy=\"12\" r=\"10\"/><line x1=\"15\" y1=\"9\" x2=\"9\" y2=\"15\"/><line x1=\"9\" y1=\"9\" x2=\"15\" y2=\"15\"/>\n </svg>\n }\n </div>\n <h3>{{ selectedNotification()!.title }}</h3>\n </div>\n <button class=\"close-btn\" (click)=\"closeDetails()\" title=\"Close\" aria-label=\"Close notification detail\">\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 <div class=\"modal-meta\">\n <span class=\"app-name\">{{ selectedNotification()!.sourceAppName }}</span>\n <span class=\"time\">{{ selectedNotificationDate() }}</span>\n </div>\n <div class=\"modal-body\" [innerHTML]=\"selectedNotificationHtml()\"></div>\n <div class=\"modal-footer\">\n @if (selectedNotification()?.url?.trim()) {\n <button class=\"action-btn see-details-btn\" (click)=\"openUrl()\">See Details</button>\n }\n <button class=\"action-btn\" (click)=\"closeDetails()\">Close</button>\n </div>\n </div>\n </div>\n}\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{display:block;position:relative;--primary: #1976d2;--primary-hover: #1565c0;--success: #43a047;--error: #f44336;--error-hover: #d32f2f;--info-color: #2196f3;--info-bg: rgba(33, 150, 243, .1);--success-bg: rgba(67, 160, 71, .1);--warning-color: #f57c00;--warning-bg: rgba(245, 124, 0, .1);--error-bg: rgba(244, 67, 54, .1);--text-primary: #212121;--text-secondary: #616161;--text-muted: #9e9e9e;--bg-primary: #ffffff;--bg-secondary: #f8f9fa;--bg-hover: #f0f4ff;--bg-unread: rgba(25, 118, 210, .06);--border-color: #e0e0e0;--border-light: #eeeeee;--shadow: rgba(0, 0, 0, .15)}.tab-btn:not(.active) .tab-badge{background:var(--error)}.read-badge{background:var(--text-muted)}.notification-panel{position:fixed;top:0;right:-360px;width:360px;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)}.notification-panel.open{right:var(--ma-ai-panel-width, 0px)}.notifications-list{flex:1;overflow-y:auto}.notification-item{display:flex;align-items:flex-start;gap:0;border-bottom:1px solid var(--border-light);cursor:pointer;background:var(--bg-primary);transition:background-color .15s;position:relative}.notification-item:hover{background:var(--bg-hover)}.notification-item.unread{background:var(--bg-unread)}.notif-accent{width:3px;align-self:stretch;flex-shrink:0;background:transparent;border-radius:0 2px 2px 0;opacity:.3}.notification-item.unread .notif-accent{opacity:1}.notif-accent.type-info{background:var(--info-color)}.notif-accent.type-success{background:var(--success)}.notif-accent.type-warning{background:var(--warning-color)}.notif-accent.type-error{background:var(--error)}.notif-type-icon{flex-shrink:0;width:26px;height:26px;border-radius:7px;display:flex;align-items:center;justify-content:center;align-self:center;margin-left:10px}.notif-type-icon.type-info{color:var(--info-color);background:var(--info-bg)}.notif-type-icon.type-success{color:var(--success);background:var(--success-bg)}.notif-type-icon.type-warning{color:var(--warning-color);background:var(--warning-bg)}.notif-type-icon.type-error{color:var(--error);background:var(--error-bg)}.notification-content{flex:1;min-width:0;padding:12px 8px 12px 12px}.notification-title{font-weight:600;color:var(--text-primary);font-size:13.5px;margin-bottom:3px;line-height:1.35}.notification-message{color:var(--text-secondary);font-size:12px;line-height:1.45;margin-bottom:7px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.notification-meta{display:flex;justify-content:space-between;font-size:11px;color:var(--text-muted)}.app-name{font-weight:600;color:var(--primary)}.icon-btn{background:none;border:none;cursor:pointer;width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;flex-shrink:0;align-self:center;margin-right:8px;transition:color .15s,background-color .15s;color:var(--text-muted)}.read-btn:hover{color:var(--success);background:#43a0471a}.delete-btn:hover{color:var(--error);background:#f443361a}.panel-footer{padding:10px 14px;border-top:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0}.footer-actions{display:flex;gap:8px}.footer-actions .action-btn{flex:1}.action-btn{display:flex;align-items:center;justify-content:center;gap:6px;width:100%;padding:8px 12px;background:var(--primary);color:#fff;border:none;border-radius:8px;cursor:pointer;font-size:12.5px;font-weight:600;transition:background-color .18s,transform .12s}.action-btn:hover{background:var(--primary-hover);transform:translateY(-1px)}.delete-all-btn{background:var(--error)}.delete-all-btn:hover{background:var(--error-hover)}.modal-overlay{position:fixed;inset:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:1060;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.modal-container{background:var(--bg-primary);border-radius:14px;width:92%;max-width:860px;max-height:88vh;display:flex;flex-direction:column;box-shadow:0 16px 48px #00000040;animation:modal-in .2s cubic-bezier(.16,1,.3,1)}@keyframes modal-in{0%{opacity:0;transform:scale(.94) translateY(8px)}to{opacity:1;transform:scale(1) translateY(0)}}.modal-header{display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid var(--border-color);background:var(--bg-secondary);border-radius:14px 14px 0 0;border-top:3px solid transparent}.modal-header.modal-type-info{border-top-color:var(--info-color)}.modal-header.modal-type-success{border-top-color:var(--success)}.modal-header.modal-type-warning{border-top-color:var(--warning-color)}.modal-header.modal-type-error{border-top-color:var(--error)}.modal-header-left{display:flex;align-items:center;gap:10px;min-width:0}.modal-type-icon{flex-shrink:0;width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center}.modal-type-info .modal-type-icon{color:var(--info-color);background:var(--info-bg)}.modal-type-success .modal-type-icon{color:var(--success);background:var(--success-bg)}.modal-type-warning .modal-type-icon{color:var(--warning-color);background:var(--warning-bg)}.modal-type-error .modal-type-icon{color:var(--error);background:var(--error-bg)}.modal-header h3{margin:0;font-size:15px;font-weight:700;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.modal-meta{display:flex;justify-content:space-between;padding:8px 20px;font-size:11.5px;color:var(--text-muted);border-bottom:1px solid var(--border-light)}.modal-body{padding:20px;overflow-y:auto;flex:1;color:var(--text-primary);font-size:14px;line-height:1.65}.modal-footer{padding:12px 20px;border-top:1px solid var(--border-color);background:var(--bg-secondary);border-radius:0 0 14px 14px;display:flex;justify-content:flex-end;gap:8px}.modal-footer .action-btn{width:auto;padding:8px 24px}.modal-footer .see-details-btn{background:var(--info-bg);color:var(--info-color);border:1px solid var(--info-color)}.modal-footer .see-details-btn:hover{opacity:.85}@media(max-width:600px){.notification-panel{width:100%;right:-100%}.modal-container{width:95%;max-height:90vh}}\n"] });
1230
- }
1231
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: NotificationPanelComponent, decorators: [{
1232
- type: Component,
1233
- args: [{ selector: 'ma-notification-panel', imports: [], template: "<div class=\"notification-panel\" [class.open]=\"isOpen()\">\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=\"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 <h3>Notifications</h3>\n </div>\n <button class=\"close-btn\" (click)=\"close()\" title=\"Close\" aria-label=\"Close notifications\">\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() === 'unread'\" (click)=\"switchTab('unread')\">\n Unread\n @if (unreadNotifications().length > 0) {\n <span class=\"tab-badge\">{{ unreadNotifications().length }}</span>\n }\n </button>\n <button class=\"tab-btn\" [class.active]=\"activeTab() === 'read'\" (click)=\"switchTab('read')\">\n Read\n @if (readNotifications().length > 0) {\n <span class=\"tab-badge read-badge\">{{ readNotifications().length }}</span>\n }\n </button>\n </div>\n\n <!-- Notifications List -->\n <div class=\"notifications-list\">\n @if (currentNotifications().length > 0) {\n @for (notification of currentNotifications(); track notification.id) {\n <div\n class=\"notification-item\"\n [class.unread]=\"!notification.isRead\"\n (click)=\"openDetails(notification)\"\n >\n @let t = typeOf(notification);\n <div class=\"notif-accent\"\n [class.type-info]=\"t === 'Info'\"\n [class.type-success]=\"t === 'Success'\"\n [class.type-warning]=\"t === 'Warning'\"\n [class.type-error]=\"t === 'Error'\"></div>\n <div class=\"notif-type-icon\"\n [class.type-info]=\"t === 'Info'\"\n [class.type-success]=\"t === 'Success'\"\n [class.type-warning]=\"t === 'Warning'\"\n [class.type-error]=\"t === 'Error'\">\n @if (t === 'Info') {\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" 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\"/><path d=\"M13.73 21a2 2 0 0 1-3.46 0\"/>\n </svg>\n }\n @if (t === 'Success') {\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"20 6 9 17 4 12\"/>\n </svg>\n }\n @if (t === 'Warning') {\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/><line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"/><line x1=\"12\" y1=\"17\" x2=\"12.01\" y2=\"17\"/>\n </svg>\n }\n @if (t === 'Error') {\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"12\" cy=\"12\" r=\"10\"/><line x1=\"15\" y1=\"9\" x2=\"9\" y2=\"15\"/><line x1=\"9\" y1=\"9\" x2=\"15\" y2=\"15\"/>\n </svg>\n }\n </div>\n <div class=\"notification-content\">\n <div class=\"notification-title\">{{ notification.title }}</div>\n <div class=\"notification-message\">{{ notification.message }}</div>\n <div class=\"notification-meta\">\n <span class=\"app-name\">{{ notification.sourceAppName }}</span>\n <span class=\"time\">{{ dateLabels().get(notification.id) }}</span>\n </div>\n </div>\n @if (!notification.isRead) {\n <button class=\"icon-btn read-btn\" (click)=\"markAsRead(notification.id, $event)\" title=\"Mark as read\" aria-label=\"Mark as read\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"20 6 9 17 4 12\"/>\n </svg>\n </button>\n }\n @if (notification.isRead) {\n <button class=\"icon-btn delete-btn\" (click)=\"delete(notification.id, $event)\" title=\"Delete\" aria-label=\"Delete notification\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"3 6 5 6 21 6\"/><path d=\"M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6\"/><path d=\"M10 11v6\"/><path d=\"M14 11v6\"/><path d=\"M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2\"/>\n </svg>\n </button>\n }\n </div>\n }\n } @else {\n <div class=\"empty-state\">\n <svg class=\"empty-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"44\" height=\"44\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" 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 <p>No {{ activeTab() }} notifications</p>\n </div>\n }\n </div>\n\n <!-- Footer Actions -->\n @if (currentNotifications().length > 0) {\n <div class=\"panel-footer\">\n @if (activeTab() === 'unread') {\n <div class=\"footer-actions\">\n @if (unreadNotifications().length > 0) {\n <button class=\"action-btn\" (click)=\"markAllAsRead()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"/></svg>\n Mark all read\n </button>\n <button class=\"action-btn delete-all-btn\" (click)=\"deleteAllUnread()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"3 6 5 6 21 6\"/><path d=\"M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6\"/></svg>\n Delete all\n </button>\n }\n </div>\n }\n @if (activeTab() === 'read' && readNotifications().length > 0) {\n <button class=\"action-btn delete-all-btn\" (click)=\"deleteAllRead()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"3 6 5 6 21 6\"/><path d=\"M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6\"/></svg>\n Delete all\n </button>\n }\n </div>\n }\n</div>\n\n<!-- Details Modal -->\n@if (selectedNotification()) {\n <div class=\"modal-overlay\" (click)=\"closeDetails()\">\n <div class=\"modal-container\" (click)=\"$event.stopPropagation()\">\n <div class=\"modal-header\"\n [class.modal-type-info]=\"typeOf(selectedNotification()!) === 'Info'\"\n [class.modal-type-success]=\"typeOf(selectedNotification()!) === 'Success'\"\n [class.modal-type-warning]=\"typeOf(selectedNotification()!) === 'Warning'\"\n [class.modal-type-error]=\"typeOf(selectedNotification()!) === 'Error'\">\n <div class=\"modal-header-left\">\n <div class=\"modal-type-icon\">\n @if (typeOf(selectedNotification()!) === 'Info') {\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=\"M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9\"/><path d=\"M13.73 21a2 2 0 0 1-3.46 0\"/>\n </svg>\n }\n @if (typeOf(selectedNotification()!) === 'Success') {\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=\"M22 11.08V12a10 10 0 1 1-5.93-9.14\"/><polyline points=\"22 4 12 14.01 9 11.01\"/>\n </svg>\n }\n @if (typeOf(selectedNotification()!) === 'Warning') {\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=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/><line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"/>\n </svg>\n }\n @if (typeOf(selectedNotification()!) === 'Error') {\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 <circle cx=\"12\" cy=\"12\" r=\"10\"/><line x1=\"15\" y1=\"9\" x2=\"9\" y2=\"15\"/><line x1=\"9\" y1=\"9\" x2=\"15\" y2=\"15\"/>\n </svg>\n }\n </div>\n <h3>{{ selectedNotification()!.title }}</h3>\n </div>\n <button class=\"close-btn\" (click)=\"closeDetails()\" title=\"Close\" aria-label=\"Close notification detail\">\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 <div class=\"modal-meta\">\n <span class=\"app-name\">{{ selectedNotification()!.sourceAppName }}</span>\n <span class=\"time\">{{ selectedNotificationDate() }}</span>\n </div>\n <div class=\"modal-body\" [innerHTML]=\"selectedNotificationHtml()\"></div>\n <div class=\"modal-footer\">\n @if (selectedNotification()?.url?.trim()) {\n <button class=\"action-btn see-details-btn\" (click)=\"openUrl()\">See Details</button>\n }\n <button class=\"action-btn\" (click)=\"closeDetails()\">Close</button>\n </div>\n </div>\n </div>\n}\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{display:block;position:relative;--primary: #1976d2;--primary-hover: #1565c0;--success: #43a047;--error: #f44336;--error-hover: #d32f2f;--info-color: #2196f3;--info-bg: rgba(33, 150, 243, .1);--success-bg: rgba(67, 160, 71, .1);--warning-color: #f57c00;--warning-bg: rgba(245, 124, 0, .1);--error-bg: rgba(244, 67, 54, .1);--text-primary: #212121;--text-secondary: #616161;--text-muted: #9e9e9e;--bg-primary: #ffffff;--bg-secondary: #f8f9fa;--bg-hover: #f0f4ff;--bg-unread: rgba(25, 118, 210, .06);--border-color: #e0e0e0;--border-light: #eeeeee;--shadow: rgba(0, 0, 0, .15)}.tab-btn:not(.active) .tab-badge{background:var(--error)}.read-badge{background:var(--text-muted)}.notification-panel{position:fixed;top:0;right:-360px;width:360px;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)}.notification-panel.open{right:var(--ma-ai-panel-width, 0px)}.notifications-list{flex:1;overflow-y:auto}.notification-item{display:flex;align-items:flex-start;gap:0;border-bottom:1px solid var(--border-light);cursor:pointer;background:var(--bg-primary);transition:background-color .15s;position:relative}.notification-item:hover{background:var(--bg-hover)}.notification-item.unread{background:var(--bg-unread)}.notif-accent{width:3px;align-self:stretch;flex-shrink:0;background:transparent;border-radius:0 2px 2px 0;opacity:.3}.notification-item.unread .notif-accent{opacity:1}.notif-accent.type-info{background:var(--info-color)}.notif-accent.type-success{background:var(--success)}.notif-accent.type-warning{background:var(--warning-color)}.notif-accent.type-error{background:var(--error)}.notif-type-icon{flex-shrink:0;width:26px;height:26px;border-radius:7px;display:flex;align-items:center;justify-content:center;align-self:center;margin-left:10px}.notif-type-icon.type-info{color:var(--info-color);background:var(--info-bg)}.notif-type-icon.type-success{color:var(--success);background:var(--success-bg)}.notif-type-icon.type-warning{color:var(--warning-color);background:var(--warning-bg)}.notif-type-icon.type-error{color:var(--error);background:var(--error-bg)}.notification-content{flex:1;min-width:0;padding:12px 8px 12px 12px}.notification-title{font-weight:600;color:var(--text-primary);font-size:13.5px;margin-bottom:3px;line-height:1.35}.notification-message{color:var(--text-secondary);font-size:12px;line-height:1.45;margin-bottom:7px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.notification-meta{display:flex;justify-content:space-between;font-size:11px;color:var(--text-muted)}.app-name{font-weight:600;color:var(--primary)}.icon-btn{background:none;border:none;cursor:pointer;width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;flex-shrink:0;align-self:center;margin-right:8px;transition:color .15s,background-color .15s;color:var(--text-muted)}.read-btn:hover{color:var(--success);background:#43a0471a}.delete-btn:hover{color:var(--error);background:#f443361a}.panel-footer{padding:10px 14px;border-top:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0}.footer-actions{display:flex;gap:8px}.footer-actions .action-btn{flex:1}.action-btn{display:flex;align-items:center;justify-content:center;gap:6px;width:100%;padding:8px 12px;background:var(--primary);color:#fff;border:none;border-radius:8px;cursor:pointer;font-size:12.5px;font-weight:600;transition:background-color .18s,transform .12s}.action-btn:hover{background:var(--primary-hover);transform:translateY(-1px)}.delete-all-btn{background:var(--error)}.delete-all-btn:hover{background:var(--error-hover)}.modal-overlay{position:fixed;inset:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:1060;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.modal-container{background:var(--bg-primary);border-radius:14px;width:92%;max-width:860px;max-height:88vh;display:flex;flex-direction:column;box-shadow:0 16px 48px #00000040;animation:modal-in .2s cubic-bezier(.16,1,.3,1)}@keyframes modal-in{0%{opacity:0;transform:scale(.94) translateY(8px)}to{opacity:1;transform:scale(1) translateY(0)}}.modal-header{display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid var(--border-color);background:var(--bg-secondary);border-radius:14px 14px 0 0;border-top:3px solid transparent}.modal-header.modal-type-info{border-top-color:var(--info-color)}.modal-header.modal-type-success{border-top-color:var(--success)}.modal-header.modal-type-warning{border-top-color:var(--warning-color)}.modal-header.modal-type-error{border-top-color:var(--error)}.modal-header-left{display:flex;align-items:center;gap:10px;min-width:0}.modal-type-icon{flex-shrink:0;width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center}.modal-type-info .modal-type-icon{color:var(--info-color);background:var(--info-bg)}.modal-type-success .modal-type-icon{color:var(--success);background:var(--success-bg)}.modal-type-warning .modal-type-icon{color:var(--warning-color);background:var(--warning-bg)}.modal-type-error .modal-type-icon{color:var(--error);background:var(--error-bg)}.modal-header h3{margin:0;font-size:15px;font-weight:700;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.modal-meta{display:flex;justify-content:space-between;padding:8px 20px;font-size:11.5px;color:var(--text-muted);border-bottom:1px solid var(--border-light)}.modal-body{padding:20px;overflow-y:auto;flex:1;color:var(--text-primary);font-size:14px;line-height:1.65}.modal-footer{padding:12px 20px;border-top:1px solid var(--border-color);background:var(--bg-secondary);border-radius:0 0 14px 14px;display:flex;justify-content:flex-end;gap:8px}.modal-footer .action-btn{width:auto;padding:8px 24px}.modal-footer .see-details-btn{background:var(--info-bg);color:var(--info-color);border:1px solid var(--info-color)}.modal-footer .see-details-btn:hover{opacity:.85}@media(max-width:600px){.notification-panel{width:100%;right:-100%}.modal-container{width:95%;max-height:90vh}}\n"] }]
1234
- }], ctorParameters: () => [], propDecorators: { notificationRead: [{ type: i0.Output, args: ["notificationRead"] }], themeClass: [{
1235
- type: HostBinding,
1236
- args: ['class']
1237
- }] } });
1238
-
1239
- class MaApprovalService {
1240
- apiBase = '';
1241
- http;
1242
- config = null;
1243
- constructor() { }
1244
- init(config, httpClient) {
1245
- this.config = config;
1246
- this.http = httpClient;
1247
- this.apiBase = config.apiBaseUrl.replace(/\/$/, '');
1248
- }
1249
- get opts() {
1250
- return { withCredentials: this.config?.withCredentials ?? true };
1251
- }
1252
- // ====================== Dashboard ======================
1253
- getDashboard() {
1254
- return this.http.get(`${this.apiBase}/approval/dashboard`, this.opts);
1255
- }
1256
- // ====================== Pending & My Requests ======================
1257
- getPendingApprovals(page = 1, pageSize = 20) {
1258
- return this.http.get(`${this.apiBase}/approval/pending?page=${page}&pageSize=${pageSize}`, this.opts);
1259
- }
1260
- getMyRequests(page = 1, pageSize = 20, status) {
1261
- let url = `${this.apiBase}/approval/my-requests?page=${page}&pageSize=${pageSize}`;
1262
- if (status !== undefined)
1263
- url += `&status=${status}`;
1264
- return this.http.get(url, this.opts);
1191
+ markToolStatus(callId, status, summary) {
1192
+ this.messages.update(arr => arr.map(m => ({
1193
+ ...m,
1194
+ toolEvents: m.toolEvents?.map(t => t.id === callId ? { ...t, status, summary: summary ?? t.summary } : t)
1195
+ })));
1265
1196
  }
1266
- // ====================== Document ======================
1267
- getDocument(id) {
1268
- return this.http.get(`${this.apiBase}/approval/documents/${id}`, this.opts);
1197
+ finalizeAssistant(extra) {
1198
+ const id = this.currentAssistantId;
1199
+ if (!id)
1200
+ return;
1201
+ this.messages.update(arr => arr.map(m => m.id === id
1202
+ ? { ...m, pending: false, text: extra ? (m.text + (m.text ? '\n' : '') + extra) : m.text }
1203
+ : m));
1204
+ this.currentAssistantId = null;
1269
1205
  }
1270
- getDocumentHistory(id) {
1271
- return this.http.get(`${this.apiBase}/approval/documents/${id}/history`, this.opts);
1206
+ verbOf(name) {
1207
+ return name.split('_')[0] ?? name;
1272
1208
  }
1273
- getDocumentContentUrl(id) {
1274
- return `${this.apiBase}/approval/documents/${id}/content`;
1209
+ summarize(s) {
1210
+ const flat = s.replace(/\s+/g, ' ').trim();
1211
+ return flat.length <= 80 ? flat : flat.slice(0, 80) + '…';
1275
1212
  }
1276
- getDocumentThumbnailUrl(id) {
1277
- return `${this.apiBase}/approval/documents/${id}/thumbnail`;
1213
+ uid() {
1214
+ return Math.random().toString(36).slice(2) + Date.now().toString(36);
1278
1215
  }
1279
- // ====================== Actions ======================
1280
- approve(documentId, comment) {
1281
- const body = { comment };
1282
- return this.http.post(`${this.apiBase}/approval/documents/${documentId}/approve`, body, this.opts);
1216
+ // ── conversation history (DB-backed via MesAuth.Api) ─────────────────────
1217
+ /** GET /ai/conversations — list this user's past conversations (most recent first). */
1218
+ async listConversations(take = 50) {
1219
+ return this.apiGet(`/ai/conversations?take=${take}`) ?? [];
1283
1220
  }
1284
- reject(documentId, comment) {
1285
- const body = { comment };
1286
- return this.http.post(`${this.apiBase}/approval/documents/${documentId}/reject`, body, this.opts);
1221
+ /** GET /ai/conversations/{id} — fetch and hydrate a past conversation into the panel. */
1222
+ async resumeConversation(id) {
1223
+ const detail = await this.apiGet(`/ai/conversations/${encodeURIComponent(id)}`);
1224
+ if (!detail)
1225
+ return false;
1226
+ this.cancel();
1227
+ this.sessionId = detail.id;
1228
+ this.messages.set(this.hydrateBubbles(detail.messages));
1229
+ this.pendingApproval.set(null);
1230
+ this.lastError.set(null);
1231
+ this.alwaysApproved.clear();
1232
+ return true;
1287
1233
  }
1288
- delegate(documentId, toUserId, reason) {
1289
- const body = { toUserId, reason };
1290
- return this.http.post(`${this.apiBase}/approval/documents/${documentId}/delegate`, body, this.opts);
1234
+ /** DELETE /ai/conversations/{id} */
1235
+ async deleteConversation(id) {
1236
+ return this.apiSend('DELETE', `/ai/conversations/${encodeURIComponent(id)}`);
1291
1237
  }
1292
- cancel(documentId, reason) {
1293
- let url = `${this.apiBase}/approval/documents/${documentId}`;
1294
- if (reason)
1295
- url += `?reason=${encodeURIComponent(reason)}`;
1296
- return this.http.delete(url, this.opts);
1238
+ // ── user lessons (system-prompt injections) ──────────────────────────────
1239
+ async listLessons() {
1240
+ return this.apiGet(`/ai/lessons`) ?? [];
1297
1241
  }
1298
- // ====================== Templates ======================
1299
- getTemplates(appId) {
1300
- let url = `${this.apiBase}/approval/templates`;
1301
- if (appId)
1302
- url += `?appId=${encodeURIComponent(appId)}`;
1303
- return this.http.get(url, this.opts);
1242
+ async createLesson(text) {
1243
+ return this.apiSend('POST', `/ai/lessons`, { text });
1304
1244
  }
1305
- getTemplate(id) {
1306
- return this.http.get(`${this.apiBase}/approval/templates/${id}`, this.opts);
1245
+ async updateLesson(id, patch) {
1246
+ return this.apiSend('PUT', `/ai/lessons/${encodeURIComponent(id)}`, patch);
1307
1247
  }
1308
- createTemplate(request) {
1309
- return this.http.post(`${this.apiBase}/approval/templates`, request, this.opts);
1248
+ async deleteLesson(id) {
1249
+ return this.apiSend('DELETE', `/ai/lessons/${encodeURIComponent(id)}`);
1310
1250
  }
1311
- updateTemplate(id, request) {
1312
- return this.http.put(`${this.apiBase}/approval/templates/${id}`, request, this.opts);
1251
+ // ── private HTTP plumbing (mirrors runStream's credentials handling) ─────
1252
+ baseUrl() {
1253
+ const config = this.auth.getConfig();
1254
+ const base = config?.apiBaseUrl?.replace(/\/$/, '') ?? '';
1255
+ return base ? base : null;
1313
1256
  }
1314
- deleteTemplate(id) {
1315
- return this.http.delete(`${this.apiBase}/approval/templates/${id}`, this.opts);
1257
+ async apiGet(path) {
1258
+ const base = this.baseUrl();
1259
+ if (!base)
1260
+ return null;
1261
+ const config = this.auth.getConfig();
1262
+ try {
1263
+ const res = await fetch(`${base}${path}`, {
1264
+ method: 'GET',
1265
+ headers: { 'Accept': 'application/json' },
1266
+ credentials: config?.withCredentials !== false ? 'include' : 'same-origin',
1267
+ });
1268
+ if (!res.ok)
1269
+ return null;
1270
+ return await res.json();
1271
+ }
1272
+ catch {
1273
+ return null;
1274
+ }
1316
1275
  }
1317
- previewRole(orgCode, level) {
1318
- return this.http.get(`${this.apiBase}/approval/roles/preview?orgCode=${encodeURIComponent(orgCode)}&level=${encodeURIComponent(level)}`, this.opts);
1276
+ async apiSend(method, path, body) {
1277
+ const base = this.baseUrl();
1278
+ if (!base)
1279
+ return method === 'DELETE' ? false : null;
1280
+ const config = this.auth.getConfig();
1281
+ try {
1282
+ const res = await fetch(`${base}${path}`, {
1283
+ method,
1284
+ headers: body !== undefined
1285
+ ? { 'Accept': 'application/json', 'Content-Type': 'application/json' }
1286
+ : { 'Accept': 'application/json' },
1287
+ credentials: config?.withCredentials !== false ? 'include' : 'same-origin',
1288
+ body: body !== undefined ? JSON.stringify(body) : undefined,
1289
+ });
1290
+ if (!res.ok)
1291
+ return method === 'DELETE' ? false : null;
1292
+ if (method === 'DELETE')
1293
+ return true;
1294
+ const text = await res.text();
1295
+ return text ? JSON.parse(text) : null;
1296
+ }
1297
+ catch {
1298
+ return method === 'DELETE' ? false : null;
1299
+ }
1319
1300
  }
1320
- // ====================== Create (used by ma-arv-container) ======================
1321
- createApproval(request) {
1322
- return this.http.post(`${this.apiBase}/approval/documents`, request, this.opts);
1301
+ /**
1302
+ * Rebuild ChatBubble[] from a stored transcript. Tool messages don't appear as their
1303
+ * own bubbles — they were emitted as chips on the preceding assistant bubble — but we
1304
+ * reconstruct that linkage best-effort so the resumed view roughly matches what the
1305
+ * user originally saw.
1306
+ */
1307
+ hydrateBubbles(messages) {
1308
+ const bubbles = [];
1309
+ let lastAssistant = null;
1310
+ for (const m of messages) {
1311
+ if (m.role === 'user') {
1312
+ const b = { id: this.uid(), role: 'user', text: m.content ?? '' };
1313
+ bubbles.push(b);
1314
+ lastAssistant = null;
1315
+ }
1316
+ else if (m.role === 'assistant') {
1317
+ const toolEvents = (m.toolCalls ?? []).map(tc => ({
1318
+ id: tc.id,
1319
+ name: tc.name,
1320
+ side: (tc.side === 'client' ? 'client' : 'server'),
1321
+ status: 'ok',
1322
+ args: tc.args,
1323
+ }));
1324
+ const b = {
1325
+ id: this.uid(),
1326
+ role: 'assistant',
1327
+ text: m.content ?? '',
1328
+ toolEvents: toolEvents.length > 0 ? toolEvents : undefined,
1329
+ };
1330
+ bubbles.push(b);
1331
+ lastAssistant = b;
1332
+ }
1333
+ else if (m.role === 'tool' && lastAssistant) {
1334
+ const ev = lastAssistant.toolEvents?.find(e => e.id === (m.toolCallId ?? ''));
1335
+ if (ev) {
1336
+ ev.status = m.toolIsError ? 'error' : 'ok';
1337
+ const flat = (m.content ?? '').replace(/\s+/g, ' ').trim();
1338
+ ev.summary = flat.length <= 80 ? flat : flat.slice(0, 80) + '…';
1339
+ }
1340
+ }
1341
+ }
1342
+ return bubbles;
1323
1343
  }
1324
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaApprovalService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1325
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaApprovalService });
1344
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1345
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiService, providedIn: 'root' });
1326
1346
  }
1327
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaApprovalService, decorators: [{
1328
- type: Injectable
1329
- }], ctorParameters: () => [] });
1330
-
1331
- // ====================== Enums ======================
1332
- var ApprovalStepMode;
1333
- (function (ApprovalStepMode) {
1334
- ApprovalStepMode[ApprovalStepMode["Sequential"] = 0] = "Sequential";
1335
- ApprovalStepMode[ApprovalStepMode["Parallel"] = 1] = "Parallel";
1336
- })(ApprovalStepMode || (ApprovalStepMode = {}));
1337
- var ApprovalDocumentStatus;
1338
- (function (ApprovalDocumentStatus) {
1339
- ApprovalDocumentStatus[ApprovalDocumentStatus["Draft"] = 0] = "Draft";
1340
- ApprovalDocumentStatus[ApprovalDocumentStatus["Pending"] = 1] = "Pending";
1341
- ApprovalDocumentStatus[ApprovalDocumentStatus["Approved"] = 2] = "Approved";
1342
- ApprovalDocumentStatus[ApprovalDocumentStatus["Rejected"] = 3] = "Rejected";
1343
- ApprovalDocumentStatus[ApprovalDocumentStatus["Cancelled"] = 4] = "Cancelled";
1344
- ApprovalDocumentStatus[ApprovalDocumentStatus["Expired"] = 5] = "Expired";
1345
- })(ApprovalDocumentStatus || (ApprovalDocumentStatus = {}));
1346
- var ApprovalStepStatus;
1347
- (function (ApprovalStepStatus) {
1348
- ApprovalStepStatus[ApprovalStepStatus["Waiting"] = 0] = "Waiting";
1349
- ApprovalStepStatus[ApprovalStepStatus["Active"] = 1] = "Active";
1350
- ApprovalStepStatus[ApprovalStepStatus["Approved"] = 2] = "Approved";
1351
- ApprovalStepStatus[ApprovalStepStatus["Rejected"] = 3] = "Rejected";
1352
- ApprovalStepStatus[ApprovalStepStatus["Delegated"] = 4] = "Delegated";
1353
- ApprovalStepStatus[ApprovalStepStatus["Expired"] = 5] = "Expired";
1354
- ApprovalStepStatus[ApprovalStepStatus["Skipped"] = 6] = "Skipped";
1355
- })(ApprovalStepStatus || (ApprovalStepStatus = {}));
1356
- var ApprovalActionType;
1357
- (function (ApprovalActionType) {
1358
- ApprovalActionType[ApprovalActionType["Created"] = 0] = "Created";
1359
- ApprovalActionType[ApprovalActionType["Submitted"] = 1] = "Submitted";
1360
- ApprovalActionType[ApprovalActionType["Approved"] = 2] = "Approved";
1361
- ApprovalActionType[ApprovalActionType["Rejected"] = 3] = "Rejected";
1362
- ApprovalActionType[ApprovalActionType["Delegated"] = 4] = "Delegated";
1363
- ApprovalActionType[ApprovalActionType["Cancelled"] = 5] = "Cancelled";
1364
- ApprovalActionType[ApprovalActionType["Commented"] = 6] = "Commented";
1365
- ApprovalActionType[ApprovalActionType["Expired"] = 7] = "Expired";
1366
- ApprovalActionType[ApprovalActionType["StepAdvanced"] = 8] = "StepAdvanced";
1367
- })(ApprovalActionType || (ApprovalActionType = {}));
1347
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiService, decorators: [{
1348
+ type: Injectable,
1349
+ args: [{ providedIn: 'root' }]
1350
+ }] });
1368
1351
 
1369
- class MaApprovalPanelComponent {
1370
- approvalActioned = output();
1371
- isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
1352
+ /**
1353
+ * Modal editor for the signed-in user's AI "lessons" — long-lived instructions
1354
+ * injected into the LLM system prompt every turn. Opened from the user profile
1355
+ * dropdown via a dedicated button.
1356
+ *
1357
+ * Library-only styling (no @angular/forms — we use [value] + (input) per
1358
+ * mesauth-angular conventions).
1359
+ */
1360
+ class MaAiLessonsEditorComponent {
1361
+ ai = inject(MaAiService);
1362
+ themeService = inject(ThemeService);
1363
+ lessons = signal([], ...(ngDevMode ? [{ debugName: "lessons" }] : /* istanbul ignore next */ []));
1372
1364
  loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
1373
- activeTab = signal('processing', ...(ngDevMode ? [{ debugName: "activeTab" }] : /* istanbul ignore next */ []));
1374
- processingItems = signal([], ...(ngDevMode ? [{ debugName: "processingItems" }] : /* istanbul ignore next */ []));
1375
- approvedItems = signal([], ...(ngDevMode ? [{ debugName: "approvedItems" }] : /* istanbul ignore next */ []));
1376
- rejectedItems = signal([], ...(ngDevMode ? [{ debugName: "rejectedItems" }] : /* istanbul ignore next */ []));
1377
- mesAuth = inject(MesAuthService);
1378
- http = inject(HttpClient);
1379
- router = inject(Router);
1380
- host = inject(ElementRef);
1381
- approvalSvc = null;
1382
- constructor() {
1383
- const config = this.mesAuth.getConfig();
1384
- if (config) {
1385
- this.approvalSvc = new MaApprovalService();
1386
- this.approvalSvc.init(config, this.http);
1365
+ saving = signal(false, ...(ngDevMode ? [{ debugName: "saving" }] : /* istanbul ignore next */ []));
1366
+ draft = signal('', ...(ngDevMode ? [{ debugName: "draft" }] : /* istanbul ignore next */ []));
1367
+ error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
1368
+ cancel = output();
1369
+ // Tracks the latest in-progress text edit per lesson so blur commits the right value.
1370
+ pendingText = new Map();
1371
+ get themeClass() {
1372
+ return `theme-${this.themeService.currentTheme()}`;
1373
+ }
1374
+ async ngOnInit() {
1375
+ this.loading.set(true);
1376
+ try {
1377
+ const list = await this.ai.listLessons();
1378
+ this.lessons.set(list ?? []);
1379
+ }
1380
+ finally {
1381
+ this.loading.set(false);
1387
1382
  }
1388
- const approvalEvent = toSignal(this.mesAuth.approvalEvents$);
1389
- effect(() => {
1390
- approvalEvent(); // track SignalR approval events
1391
- if (this.isOpen())
1392
- this.loadCurrentTab();
1393
- });
1394
- // Close when the user clicks outside the panel (mirrors the old backdrop UX
1395
- // now that the backdrop has been removed).
1396
- effect((onCleanup) => {
1397
- if (!this.isOpen())
1398
- return;
1399
- const onDocClick = (ev) => {
1400
- const panel = this.host.nativeElement.querySelector('.approval-panel');
1401
- if (panel && !panel.contains(ev.target))
1402
- this.close();
1403
- };
1404
- // Defer to skip the same click that opened the panel.
1405
- const tid = setTimeout(() => document.addEventListener('mousedown', onDocClick), 0);
1406
- onCleanup(() => {
1407
- clearTimeout(tid);
1408
- document.removeEventListener('mousedown', onDocClick);
1409
- });
1410
- });
1411
1383
  }
1412
- open() {
1413
- this.isOpen.set(true);
1414
- this.loadAllTabs();
1384
+ onBackdropClick(ev) {
1385
+ if (ev.target === ev.currentTarget)
1386
+ this.cancel.emit();
1415
1387
  }
1416
- close() {
1417
- this.isOpen.set(false);
1388
+ onDraftInput(ev) {
1389
+ this.draft.set(ev.target.value);
1418
1390
  }
1419
- toggle() {
1420
- if (this.isOpen())
1421
- this.close();
1422
- else
1423
- this.open();
1391
+ async add() {
1392
+ const text = this.draft().trim();
1393
+ if (!text)
1394
+ return;
1395
+ this.saving.set(true);
1396
+ this.error.set(null);
1397
+ try {
1398
+ const created = await this.ai.createLesson(text);
1399
+ if (!created) {
1400
+ this.error.set('Failed to save preference.');
1401
+ return;
1402
+ }
1403
+ this.lessons.update(arr => [created, ...arr]);
1404
+ this.draft.set('');
1405
+ }
1406
+ finally {
1407
+ this.saving.set(false);
1408
+ }
1424
1409
  }
1425
- switchTab(tab) {
1426
- this.activeTab.set(tab);
1427
- this.loadCurrentTab();
1410
+ onTextInput(lesson, ev) {
1411
+ this.pendingText.set(lesson.id, ev.target.value);
1428
1412
  }
1429
- loadAllTabs() {
1430
- this.loading.set(true);
1431
- if (!this.approvalSvc) {
1432
- this.loading.set(false);
1413
+ async commitText(lesson) {
1414
+ const next = this.pendingText.get(lesson.id);
1415
+ this.pendingText.delete(lesson.id);
1416
+ if (next === undefined)
1433
1417
  return;
1434
- }
1435
- let pending = 3;
1436
- const done = () => { if (--pending === 0)
1437
- this.loading.set(false); };
1438
- this.approvalSvc.getPendingApprovals(1, 100).subscribe({
1439
- next: r => { this.processingItems.set(r.items); done(); },
1440
- error: () => done()
1418
+ const trimmed = next.trim();
1419
+ if (!trimmed || trimmed === lesson.text)
1420
+ return;
1421
+ const updated = await this.ai.updateLesson(lesson.id, { text: trimmed });
1422
+ if (updated) {
1423
+ this.lessons.update(arr => arr.map(l => l.id === lesson.id ? updated : l));
1424
+ }
1425
+ }
1426
+ async toggle(lesson, ev) {
1427
+ const enabled = ev.target.checked;
1428
+ const updated = await this.ai.updateLesson(lesson.id, { enabled });
1429
+ if (updated) {
1430
+ this.lessons.update(arr => arr.map(l => l.id === lesson.id ? updated : l));
1431
+ }
1432
+ }
1433
+ async remove(lesson) {
1434
+ const ok = await this.ai.deleteLesson(lesson.id);
1435
+ if (ok) {
1436
+ this.lessons.update(arr => arr.filter(l => l.id !== lesson.id));
1437
+ }
1438
+ }
1439
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiLessonsEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1440
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: MaAiLessonsEditorComponent, isStandalone: true, selector: "ma-ai-lessons-editor", outputs: { cancel: "cancel" }, host: { properties: { "class": "this.themeClass" } }, ngImport: i0, template: `
1441
+ <div class="lessons-backdrop" (click)="onBackdropClick($event)">
1442
+ <div class="lessons-modal" role="dialog" aria-modal="true" aria-labelledby="lessonsTitle">
1443
+ <div class="lessons-head">
1444
+ <div>
1445
+ <h3 id="lessonsTitle">AI preferences</h3>
1446
+ <p class="lessons-sub">
1447
+ Long-lived instructions the AI will follow in every conversation.
1448
+ Each row is injected as a bullet point into the system prompt.
1449
+ </p>
1450
+ </div>
1451
+ <button class="lessons-close" (click)="cancel.emit()" aria-label="Close">
1452
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24"
1453
+ fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
1454
+ <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
1455
+ </svg>
1456
+ </button>
1457
+ </div>
1458
+
1459
+ <div class="lessons-body">
1460
+ @if (loading()) {
1461
+ <div class="lessons-empty">Loading…</div>
1462
+ } @else if (lessons().length === 0) {
1463
+ <div class="lessons-empty">No preferences yet. Add one below or ask the AI to "remember" something.</div>
1464
+ } @else {
1465
+ @for (l of lessons(); track l.id) {
1466
+ <div class="lesson-row" [class.disabled]="!l.enabled">
1467
+ <label class="lesson-toggle" [title]="l.enabled ? 'Disable' : 'Enable'">
1468
+ <input type="checkbox" [checked]="l.enabled" (change)="toggle(l, $event)" />
1469
+ <span class="lesson-toggle-slider"></span>
1470
+ </label>
1471
+ <textarea class="lesson-text"
1472
+ rows="2"
1473
+ [value]="l.text"
1474
+ (input)="onTextInput(l, $event)"
1475
+ (blur)="commitText(l)"></textarea>
1476
+ <span class="lesson-source" [class.llm]="l.source === 'llm'">
1477
+ {{ l.source === 'llm' ? 'AI' : 'You' }}
1478
+ </span>
1479
+ <button class="lesson-del" (click)="remove(l)" title="Delete" aria-label="Delete">
1480
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24"
1481
+ fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1482
+ <polyline points="3 6 5 6 21 6"/>
1483
+ <path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
1484
+ </svg>
1485
+ </button>
1486
+ </div>
1487
+ }
1488
+ }
1489
+ </div>
1490
+
1491
+ <div class="lessons-add">
1492
+ <textarea class="lesson-text"
1493
+ rows="2"
1494
+ placeholder="e.g. Always reply in Vietnamese."
1495
+ [value]="draft()"
1496
+ (input)="onDraftInput($event)"></textarea>
1497
+ <button class="lesson-add-btn" (click)="add()" [disabled]="!draft().trim() || saving()">
1498
+ Add
1499
+ </button>
1500
+ </div>
1501
+
1502
+ @if (error()) { <div class="lesson-error">{{ error() }}</div> }
1503
+ </div>
1504
+ </div>
1505
+ `, isInline: true, styles: [":host{--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;--border-color: #e0e0e0;--shadow: rgba(0,0,0,.15)}:host(.theme-dark){--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;--border-color: #383850;--shadow: rgba(0,0,0,.4)}.lessons-backdrop{position:fixed;inset:0;background:#00000073;display:flex;align-items:center;justify-content:center;z-index:1060;padding:20px}.lessons-modal{background:var(--bg-primary);color:var(--text-primary);width:100%;max-width:640px;max-height:86vh;border-radius:12px;box-shadow:0 18px 60px var(--shadow);display:flex;flex-direction:column;overflow:hidden;border:1px solid var(--border-color)}.lessons-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;padding:18px 20px 14px;border-bottom:1px solid var(--border-color)}.lessons-head h3{margin:0 0 4px;font-size:16px;font-weight:600}.lessons-sub{margin:0;font-size:12.5px;color:var(--text-secondary);line-height:1.5}.lessons-close{background:none;border:none;cursor:pointer;color:var(--text-muted);padding:4px;border-radius:6px;display:flex;align-items:center;justify-content:center}.lessons-close:hover{color:var(--text-primary);background:var(--bg-hover)}.lessons-body{flex:1;overflow-y:auto;padding:14px 20px;display:flex;flex-direction:column;gap:10px}.lessons-empty{padding:20px 4px;font-size:13px;color:var(--text-muted);text-align:center}.lesson-row{display:grid;grid-template-columns:36px 1fr auto 28px;align-items:center;gap:10px;background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:8px;padding:10px}.lesson-row.disabled .lesson-text{opacity:.55}.lesson-toggle{position:relative;display:inline-block;width:32px;height:18px;cursor:pointer}.lesson-toggle input{opacity:0;width:0;height:0}.lesson-toggle-slider{position:absolute;inset:0;background:var(--border-color);border-radius:999px;transition:background .15s}.lesson-toggle-slider:before{content:\"\";position:absolute;top:2px;left:2px;width:14px;height:14px;background:#fff;border-radius:50%;transition:transform .15s}.lesson-toggle input:checked+.lesson-toggle-slider{background:var(--primary)}.lesson-toggle input:checked+.lesson-toggle-slider:before{transform:translate(14px)}.lesson-text{width:100%;resize:vertical;min-height:36px;padding:8px 10px;font-size:13px;line-height:1.5;background:var(--bg-primary);color:var(--text-primary);border:1px solid var(--border-color);border-radius:6px;font-family:inherit}.lesson-text:focus{outline:none;border-color:var(--primary)}.lesson-source{font-size:10.5px;padding:2px 8px;border-radius:999px;background:var(--bg-hover);color:var(--text-secondary);white-space:nowrap}.lesson-source.llm{background:var(--primary);color:#fff}.lesson-del{background:none;border:none;cursor:pointer;color:var(--text-muted);padding:4px;border-radius:4px;display:flex;align-items:center;justify-content:center}.lesson-del:hover{color:var(--error);background:var(--bg-hover)}.lessons-add{display:grid;grid-template-columns:1fr auto;gap:10px;padding:12px 20px 16px;border-top:1px solid var(--border-color);background:var(--bg-primary)}.lesson-add-btn{background:var(--primary);color:#fff;border:none;border-radius:6px;padding:0 18px;cursor:pointer;font-size:13px;font-weight:600}.lesson-add-btn:hover:not(:disabled){background:var(--primary-strong)}.lesson-add-btn:disabled{opacity:.5;cursor:not-allowed}.lesson-error{padding:8px 20px;font-size:12px;color:var(--error);border-top:1px solid var(--border-color);background:var(--bg-secondary)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1506
+ }
1507
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiLessonsEditorComponent, decorators: [{
1508
+ type: Component,
1509
+ args: [{ selector: 'ma-ai-lessons-editor', changeDetection: ChangeDetectionStrategy.OnPush, template: `
1510
+ <div class="lessons-backdrop" (click)="onBackdropClick($event)">
1511
+ <div class="lessons-modal" role="dialog" aria-modal="true" aria-labelledby="lessonsTitle">
1512
+ <div class="lessons-head">
1513
+ <div>
1514
+ <h3 id="lessonsTitle">AI preferences</h3>
1515
+ <p class="lessons-sub">
1516
+ Long-lived instructions the AI will follow in every conversation.
1517
+ Each row is injected as a bullet point into the system prompt.
1518
+ </p>
1519
+ </div>
1520
+ <button class="lessons-close" (click)="cancel.emit()" aria-label="Close">
1521
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24"
1522
+ fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
1523
+ <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
1524
+ </svg>
1525
+ </button>
1526
+ </div>
1527
+
1528
+ <div class="lessons-body">
1529
+ @if (loading()) {
1530
+ <div class="lessons-empty">Loading…</div>
1531
+ } @else if (lessons().length === 0) {
1532
+ <div class="lessons-empty">No preferences yet. Add one below or ask the AI to "remember" something.</div>
1533
+ } @else {
1534
+ @for (l of lessons(); track l.id) {
1535
+ <div class="lesson-row" [class.disabled]="!l.enabled">
1536
+ <label class="lesson-toggle" [title]="l.enabled ? 'Disable' : 'Enable'">
1537
+ <input type="checkbox" [checked]="l.enabled" (change)="toggle(l, $event)" />
1538
+ <span class="lesson-toggle-slider"></span>
1539
+ </label>
1540
+ <textarea class="lesson-text"
1541
+ rows="2"
1542
+ [value]="l.text"
1543
+ (input)="onTextInput(l, $event)"
1544
+ (blur)="commitText(l)"></textarea>
1545
+ <span class="lesson-source" [class.llm]="l.source === 'llm'">
1546
+ {{ l.source === 'llm' ? 'AI' : 'You' }}
1547
+ </span>
1548
+ <button class="lesson-del" (click)="remove(l)" title="Delete" aria-label="Delete">
1549
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24"
1550
+ fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1551
+ <polyline points="3 6 5 6 21 6"/>
1552
+ <path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
1553
+ </svg>
1554
+ </button>
1555
+ </div>
1556
+ }
1557
+ }
1558
+ </div>
1559
+
1560
+ <div class="lessons-add">
1561
+ <textarea class="lesson-text"
1562
+ rows="2"
1563
+ placeholder="e.g. Always reply in Vietnamese."
1564
+ [value]="draft()"
1565
+ (input)="onDraftInput($event)"></textarea>
1566
+ <button class="lesson-add-btn" (click)="add()" [disabled]="!draft().trim() || saving()">
1567
+ Add
1568
+ </button>
1569
+ </div>
1570
+
1571
+ @if (error()) { <div class="lesson-error">{{ error() }}</div> }
1572
+ </div>
1573
+ </div>
1574
+ `, styles: [":host{--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;--border-color: #e0e0e0;--shadow: rgba(0,0,0,.15)}:host(.theme-dark){--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;--border-color: #383850;--shadow: rgba(0,0,0,.4)}.lessons-backdrop{position:fixed;inset:0;background:#00000073;display:flex;align-items:center;justify-content:center;z-index:1060;padding:20px}.lessons-modal{background:var(--bg-primary);color:var(--text-primary);width:100%;max-width:640px;max-height:86vh;border-radius:12px;box-shadow:0 18px 60px var(--shadow);display:flex;flex-direction:column;overflow:hidden;border:1px solid var(--border-color)}.lessons-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;padding:18px 20px 14px;border-bottom:1px solid var(--border-color)}.lessons-head h3{margin:0 0 4px;font-size:16px;font-weight:600}.lessons-sub{margin:0;font-size:12.5px;color:var(--text-secondary);line-height:1.5}.lessons-close{background:none;border:none;cursor:pointer;color:var(--text-muted);padding:4px;border-radius:6px;display:flex;align-items:center;justify-content:center}.lessons-close:hover{color:var(--text-primary);background:var(--bg-hover)}.lessons-body{flex:1;overflow-y:auto;padding:14px 20px;display:flex;flex-direction:column;gap:10px}.lessons-empty{padding:20px 4px;font-size:13px;color:var(--text-muted);text-align:center}.lesson-row{display:grid;grid-template-columns:36px 1fr auto 28px;align-items:center;gap:10px;background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:8px;padding:10px}.lesson-row.disabled .lesson-text{opacity:.55}.lesson-toggle{position:relative;display:inline-block;width:32px;height:18px;cursor:pointer}.lesson-toggle input{opacity:0;width:0;height:0}.lesson-toggle-slider{position:absolute;inset:0;background:var(--border-color);border-radius:999px;transition:background .15s}.lesson-toggle-slider:before{content:\"\";position:absolute;top:2px;left:2px;width:14px;height:14px;background:#fff;border-radius:50%;transition:transform .15s}.lesson-toggle input:checked+.lesson-toggle-slider{background:var(--primary)}.lesson-toggle input:checked+.lesson-toggle-slider:before{transform:translate(14px)}.lesson-text{width:100%;resize:vertical;min-height:36px;padding:8px 10px;font-size:13px;line-height:1.5;background:var(--bg-primary);color:var(--text-primary);border:1px solid var(--border-color);border-radius:6px;font-family:inherit}.lesson-text:focus{outline:none;border-color:var(--primary)}.lesson-source{font-size:10.5px;padding:2px 8px;border-radius:999px;background:var(--bg-hover);color:var(--text-secondary);white-space:nowrap}.lesson-source.llm{background:var(--primary);color:#fff}.lesson-del{background:none;border:none;cursor:pointer;color:var(--text-muted);padding:4px;border-radius:4px;display:flex;align-items:center;justify-content:center}.lesson-del:hover{color:var(--error);background:var(--bg-hover)}.lessons-add{display:grid;grid-template-columns:1fr auto;gap:10px;padding:12px 20px 16px;border-top:1px solid var(--border-color);background:var(--bg-primary)}.lesson-add-btn{background:var(--primary);color:#fff;border:none;border-radius:6px;padding:0 18px;cursor:pointer;font-size:13px;font-weight:600}.lesson-add-btn:hover:not(:disabled){background:var(--primary-strong)}.lesson-add-btn:disabled{opacity:.5;cursor:not-allowed}.lesson-error{padding:8px 20px;font-size:12px;color:var(--error);border-top:1px solid var(--border-color);background:var(--bg-secondary)}\n"] }]
1575
+ }], propDecorators: { cancel: [{ type: i0.Output, args: ["cancel"] }], themeClass: [{
1576
+ type: HostBinding,
1577
+ args: ['class']
1578
+ }] } });
1579
+
1580
+ class UserProfileComponent {
1581
+ inputAvatarShape = input('circle', ...(ngDevMode ? [{ debugName: "inputAvatarShape" }] : /* istanbul ignore next */ []));
1582
+ showBell = input(true, ...(ngDevMode ? [{ debugName: "showBell" }] : /* istanbul ignore next */ []));
1583
+ showApproval = input(true, ...(ngDevMode ? [{ debugName: "showApproval" }] : /* istanbul ignore next */ []));
1584
+ showName = input(false, ...(ngDevMode ? [{ debugName: "showName" }] : /* istanbul ignore next */ []));
1585
+ showAi = input(true, ...(ngDevMode ? [{ debugName: "showAi" }] : /* istanbul ignore next */ []));
1586
+ showSignature = input([false, false], ...(ngDevMode ? [{ debugName: "showSignature" }] : /* istanbul ignore next */ []));
1587
+ signatureHeight = input(40, ...(ngDevMode ? [{ debugName: "signatureHeight" }] : /* istanbul ignore next */ []));
1588
+ notificationClick = output();
1589
+ approvalClick = output();
1590
+ aiClick = output();
1591
+ get themeClass() {
1592
+ return `theme-${this.themeService.currentTheme()}`;
1593
+ }
1594
+ currentUser = signal(null, ...(ngDevMode ? [{ debugName: "currentUser" }] : /* istanbul ignore next */ []));
1595
+ unreadCount = signal(0, ...(ngDevMode ? [{ debugName: "unreadCount" }] : /* istanbul ignore next */ []));
1596
+ pendingApprovalCount = signal(0, ...(ngDevMode ? [{ debugName: "pendingApprovalCount" }] : /* istanbul ignore next */ []));
1597
+ dropdownOpen = signal(false, ...(ngDevMode ? [{ debugName: "dropdownOpen" }] : /* istanbul ignore next */ []));
1598
+ avatarRefresh = signal(Date.now(), ...(ngDevMode ? [{ debugName: "avatarRefresh" }] : /* istanbul ignore next */ []));
1599
+ lessonsOpen = signal(false, ...(ngDevMode ? [{ debugName: "lessonsOpen" }] : /* istanbul ignore next */ []));
1600
+ // Avatar style derived from per-user preferences
1601
+ navAvatarSize = computed(() => this.currentUser()?.avatarSize ?? 'md', ...(ngDevMode ? [{ debugName: "navAvatarSize" }] : /* istanbul ignore next */ []));
1602
+ // Dropdown is always one step larger than the nav avatar
1603
+ dropAvatarSize = computed(() => {
1604
+ const s = this.navAvatarSize();
1605
+ return s === 'sm' ? 'md' : s === 'md' ? 'lg' : 'xl';
1606
+ }, ...(ngDevMode ? [{ debugName: "dropAvatarSize" }] : /* istanbul ignore next */ []));
1607
+ avatarShape = computed(() => this.currentUser()?.avatarShape ?? this.inputAvatarShape(), ...(ngDevMode ? [{ debugName: "avatarShape" }] : /* istanbul ignore next */ []));
1608
+ avatarFrame = computed(() => this.currentUser()?.avatarFrame ?? null, ...(ngDevMode ? [{ debugName: "avatarFrame" }] : /* istanbul ignore next */ []));
1609
+ avatarRatio = computed(() => this.currentUser()?.avatarRatio ?? 'ar-11', ...(ngDevMode ? [{ debugName: "avatarRatio" }] : /* istanbul ignore next */ []));
1610
+ givenStyle = computed(() => this.currentUser()?.givenColor || 'indigo', ...(ngDevMode ? [{ debugName: "givenStyle" }] : /* istanbul ignore next */ []));
1611
+ signatureBroken = signal(false, ...(ngDevMode ? [{ debugName: "signatureBroken" }] : /* istanbul ignore next */ []));
1612
+ signatureUrl = computed(() => {
1613
+ if (!this.showSignature().some(s => s) || this.signatureBroken())
1614
+ return null;
1615
+ const baseRaw = this.authService.getConfig()?.apiBaseUrl ?? '';
1616
+ const base = baseRaw.replace(/\/$/, '');
1617
+ const u = this.currentUser();
1618
+ const id = u?.userId ?? u?.id;
1619
+ if (!id || !base)
1620
+ return null;
1621
+ return `${base}/auth/${id}/signature`;
1622
+ }, ...(ngDevMode ? [{ debugName: "signatureUrl" }] : /* istanbul ignore next */ []));
1623
+ authService = inject(MesAuthService);
1624
+ router = inject(Router);
1625
+ themeService = inject(ThemeService);
1626
+ http = inject(HttpClient);
1627
+ constructor() {
1628
+ const currentUserSig = toSignal(this.authService.currentUser$, { initialValue: null });
1629
+ const approvalEvent = toSignal(this.authService.approvalEvents$);
1630
+ const notification = toSignal(this.authService.notifications$);
1631
+ effect(() => {
1632
+ const user = currentUserSig();
1633
+ this.currentUser.set(user);
1634
+ this.avatarRefresh.set(Date.now());
1635
+ if (!user) {
1636
+ this.unreadCount.set(0);
1637
+ this.pendingApprovalCount.set(0);
1638
+ return;
1639
+ }
1640
+ this.loadUnreadCount();
1641
+ this.loadPendingApprovalCount();
1441
1642
  });
1442
- this.approvalSvc.getMyRequests(1, 10, ApprovalDocumentStatus.Approved).subscribe({
1443
- next: r => { this.approvedItems.set(r.items); done(); },
1444
- error: () => done()
1643
+ effect(() => {
1644
+ approvalEvent(); // track SignalR approval events
1645
+ if (currentUserSig())
1646
+ this.loadPendingApprovalCount();
1445
1647
  });
1446
- this.approvalSvc.getMyRequests(1, 10, ApprovalDocumentStatus.Rejected).subscribe({
1447
- next: r => { this.rejectedItems.set(r.items); done(); },
1448
- error: () => done()
1648
+ effect(() => {
1649
+ notification(); // track SignalR notification events
1650
+ if (currentUserSig())
1651
+ this.loadUnreadCount();
1449
1652
  });
1450
1653
  }
1451
- loadCurrentTab() {
1452
- if (!this.approvalSvc)
1654
+ loadUnreadCount() {
1655
+ this.authService.getUnreadCount().subscribe({
1656
+ next: (response) => this.unreadCount.set(response.unreadCount || 0),
1657
+ error: () => { }
1658
+ });
1659
+ }
1660
+ loadPendingApprovalCount() {
1661
+ const config = this.authService.getConfig();
1662
+ if (!config)
1453
1663
  return;
1454
- if (this.activeTab() === 'processing') {
1455
- this.approvalSvc.getPendingApprovals(1, 100).subscribe({
1456
- next: r => this.processingItems.set(r.items),
1457
- error: () => { }
1458
- });
1664
+ const url = `${config.apiBaseUrl.replace(/\/$/, '')}/approval/dashboard`;
1665
+ this.http.get(url, { withCredentials: config.withCredentials ?? true }).subscribe({
1666
+ next: (r) => this.pendingApprovalCount.set(r?.pendingCount ?? 0),
1667
+ error: () => { }
1668
+ });
1669
+ }
1670
+ onApprovalClick() {
1671
+ this.approvalClick.emit();
1672
+ }
1673
+ onAiClick() {
1674
+ this.aiClick.emit();
1675
+ }
1676
+ getAvatarUrl(user) {
1677
+ // Use the refresh signal to force update
1678
+ const refresh = this.avatarRefresh();
1679
+ const config = this.authService.getConfig();
1680
+ const baseUrl = config?.apiBaseUrl || '';
1681
+ if (user.avatarPath) {
1682
+ if (user.avatarPath.startsWith('http://') || user.avatarPath.startsWith('https://')) {
1683
+ return user.avatarPath;
1684
+ }
1685
+ return `${baseUrl.replace(/\/$/, '')}${user.avatarPath}?t=${refresh}`;
1459
1686
  }
1460
- else if (this.activeTab() === 'approved') {
1461
- this.approvalSvc.getMyRequests(1, 10, ApprovalDocumentStatus.Approved).subscribe({
1462
- next: r => this.approvedItems.set(r.items),
1463
- error: () => { }
1464
- });
1687
+ const userId = user.userId;
1688
+ if (userId && baseUrl) {
1689
+ return `${baseUrl.replace(/\/$/, '')}/auth/${userId}/avatar?t=${refresh}`;
1690
+ }
1691
+ const displayName = user.userName || user.userId || 'User';
1692
+ return `https://ui-avatars.com/api/?name=${encodeURIComponent(displayName)}&background=1976d2&color=fff`;
1693
+ }
1694
+ getLastNameInitial(user) {
1695
+ const fullName = user.fullName || user.userName || 'U';
1696
+ const parts = fullName.split(' ');
1697
+ const lastPart = parts[parts.length - 1];
1698
+ return lastPart.charAt(0).toUpperCase();
1699
+ }
1700
+ toggleDropdown() {
1701
+ this.dropdownOpen.set(!this.dropdownOpen());
1702
+ }
1703
+ closeDropdown() {
1704
+ this.dropdownOpen.set(false);
1705
+ }
1706
+ onDocumentClick(event) {
1707
+ const target = event.target;
1708
+ const clickedInside = target.closest('.user-menu-wrapper');
1709
+ if (!clickedInside) {
1710
+ this.dropdownOpen.set(false);
1711
+ return;
1712
+ }
1713
+ if (target.closest('.ma-user-menu-btn')) {
1714
+ this.dropdownOpen.set(false);
1715
+ }
1716
+ }
1717
+ onLogin() {
1718
+ const config = this.authService.getConfig();
1719
+ const baseUrl = config?.userBaseUrl || '';
1720
+ const returnUrl = encodeURIComponent(this.router.url);
1721
+ window.location.href = `${baseUrl}/login?returnUrl=${returnUrl}`;
1722
+ }
1723
+ onViewProfile() {
1724
+ const config = this.authService.getConfig();
1725
+ const baseUrl = config?.userBaseUrl || '';
1726
+ const currentUrl = window.location.href;
1727
+ this.openInNewTabIfSameOrigin(currentUrl, `${baseUrl}/profile`);
1728
+ this.dropdownOpen.set(false);
1729
+ }
1730
+ openInNewTabIfSameOrigin(currentUrl, destinationUrl) {
1731
+ // Check if current page URL starts with the destination URL
1732
+ if (!destinationUrl.startsWith(currentUrl)) {
1733
+ window.open(destinationUrl, "_blank", "noopener,noreferrer");
1465
1734
  }
1466
1735
  else {
1467
- this.approvalSvc.getMyRequests(1, 10, ApprovalDocumentStatus.Rejected).subscribe({
1468
- next: r => this.rejectedItems.set(r.items),
1469
- error: () => { }
1470
- });
1736
+ // Optional: redirect in same tab
1737
+ window.location.href = destinationUrl;
1471
1738
  }
1472
1739
  }
1473
- navigateToDetail(id) {
1474
- this.close();
1475
- this.router.navigate(['/auth/approval/detail', id]);
1476
- this.approvalActioned.emit();
1740
+ onLogout() {
1741
+ this.authService.logout().subscribe({
1742
+ next: () => {
1743
+ this.dropdownOpen.set(false);
1744
+ const config = this.authService.getConfig();
1745
+ const baseUrl = config?.userBaseUrl || '';
1746
+ const returnUrl = encodeURIComponent(window.location.href);
1747
+ window.location.href = `${baseUrl}/login?returnUrl=${returnUrl}`;
1748
+ },
1749
+ error: () => {
1750
+ const config = this.authService.getConfig();
1751
+ const baseUrl = config?.userBaseUrl || '';
1752
+ window.location.href = `${baseUrl}/login`;
1753
+ }
1754
+ });
1477
1755
  }
1478
- showMore(status) {
1479
- this.close();
1480
- this.router.navigate(['/auth/approval/my-requests'], { queryParams: { status } });
1756
+ onNotificationClick() {
1757
+ this.notificationClick.emit();
1481
1758
  }
1482
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaApprovalPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1483
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: MaApprovalPanelComponent, isStandalone: true, selector: "ma-approval-panel", outputs: { approvalActioned: "approvalActioned" }, ngImport: i0, template: "<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-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:var(--ma-ai-panel-width, 0px)}.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"], dependencies: [{ kind: "pipe", type: DatePipe, name: "date" }] });
1759
+ openLessons() {
1760
+ this.lessonsOpen.set(true);
1761
+ this.dropdownOpen.set(false);
1762
+ }
1763
+ closeLessons() {
1764
+ this.lessonsOpen.set(false);
1765
+ }
1766
+ onSigErr(_ev) {
1767
+ this.signatureBroken.set(true);
1768
+ }
1769
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: UserProfileComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1770
+ 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 @if (showAi()) {\n <button class=\"icon-action\" (click)=\"openLessons()\" title=\"AI preferences\" aria-label=\"AI preferences\">\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=\"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 </button>\n }\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 @if (lessonsOpen()) {\n <ma-ai-lessons-editor (cancel)=\"closeLessons()\"></ma-ai-lessons-editor>\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"] }, { kind: "component", type: MaAiLessonsEditorComponent, selector: "ma-ai-lessons-editor", outputs: ["cancel"] }] });
1484
1771
  }
1485
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaApprovalPanelComponent, decorators: [{
1772
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: UserProfileComponent, decorators: [{
1486
1773
  type: Component,
1487
- args: [{ selector: 'ma-approval-panel', imports: [DatePipe], template: "<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-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:var(--ma-ai-panel-width, 0px)}.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"] }]
1488
- }], ctorParameters: () => [], propDecorators: { approvalActioned: [{ type: i0.Output, args: ["approvalActioned"] }] } });
1774
+ args: [{ selector: 'ma-user-profile', imports: [MaAvatarComponent, MaAiButtonComponent, MaAiLessonsEditorComponent], 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 @if (showAi()) {\n <button class=\"icon-action\" (click)=\"openLessons()\" title=\"AI preferences\" aria-label=\"AI preferences\">\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=\"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 </button>\n }\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 @if (lessonsOpen()) {\n <ma-ai-lessons-editor (cancel)=\"closeLessons()\"></ma-ai-lessons-editor>\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"] }]
1775
+ }], 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: [{
1776
+ type: HostBinding,
1777
+ args: ['class']
1778
+ }], onDocumentClick: [{
1779
+ type: HostListener,
1780
+ args: ['document:click', ['$event']]
1781
+ }] } });
1489
1782
 
1490
- /**
1491
- * Resolves the full set of client tools available to the AI: built-ins + consumer-registered.
1492
- * Handlers are invoked through `runTool(name, args)` which automatically threads the Injector
1493
- * so consumer handlers can use inject() patterns.
1494
- */
1495
- class MaAiToolsRegistry {
1496
- config = inject(MES_AUTH_AI_CONFIG, { optional: true }) ?? DEFAULT_AI_CONFIG;
1497
- injector = inject(Injector);
1498
- /** Built-in client tools shipped by the library. */
1499
- builtIn = [
1500
- {
1501
- name: 'navigate',
1502
- description: 'Navigate the user to a path within the application. Use relative paths starting with /.',
1503
- parameters: {
1504
- type: 'object',
1505
- properties: {
1506
- path: { type: 'string', description: 'Path to navigate to, e.g. "/auth/users"' }
1507
- },
1508
- required: ['path']
1509
- },
1510
- readOnly: true,
1511
- handler: (args) => {
1512
- const router = this.injector.get(Router, null);
1513
- if (!router)
1514
- return 'Router is not available in this app.';
1515
- router.navigateByUrl(args.path);
1516
- return `Navigated to ${args.path}`;
1517
- }
1518
- },
1519
- {
1520
- name: 'toggle_theme',
1521
- description: 'Toggle between light and dark theme.',
1522
- parameters: { type: 'object', properties: {} },
1523
- readOnly: true,
1524
- handler: () => {
1525
- const theme = this.injector.get(ThemeService);
1526
- const next = theme.currentTheme() === 'light' ? 'dark' : 'light';
1527
- theme.setFixTheme(next);
1528
- return `Theme switched to ${next}.`;
1529
- }
1530
- },
1531
- {
1532
- name: 'show_toast',
1533
- description: 'Show a small toast notification at the top of the screen.',
1534
- parameters: {
1535
- type: 'object',
1536
- properties: {
1537
- message: { type: 'string', description: 'Toast body text' },
1538
- title: { type: 'string', description: 'Optional title' },
1539
- severity: { type: 'string', enum: ['info', 'success', 'warning', 'error'], description: 'Toast severity' }
1540
- },
1541
- required: ['message']
1542
- },
1543
- readOnly: true,
1544
- handler: (args) => {
1545
- const toast = this.injector.get(ToastService);
1546
- toast.show(args.message, args.title, args.severity ?? 'info');
1547
- return 'Toast shown.';
1548
- }
1549
- },
1550
- {
1551
- name: 'who_am_i_client',
1552
- description: 'Get the currently signed-in user as seen by the browser (id, name, roles loaded so far).',
1553
- parameters: { type: 'object', properties: {} },
1554
- readOnly: true,
1555
- handler: () => {
1556
- const auth = this.injector.get(MesAuthService);
1557
- const u = auth.currentUser;
1558
- if (!u)
1559
- return 'No user signed in.';
1560
- return JSON.stringify({
1561
- id: u.userId ?? u.id,
1562
- userName: u.userName,
1563
- fullName: u.fullName,
1564
- department: u.department,
1565
- position: u.position,
1566
- email: u.email
1567
- });
1568
- }
1569
- },
1570
- {
1571
- name: 'reload_page',
1572
- description: 'Reload the current browser page. Use as a last resort if state seems stuck.',
1573
- parameters: { type: 'object', properties: {} },
1574
- readOnly: false,
1575
- handler: () => {
1576
- window.location.reload();
1577
- return 'Reloading page…';
1578
- }
1579
- }
1580
- ];
1581
- /** All tools, deduplicated by name (consumer wins on conflict). */
1582
- list() {
1583
- const fromConfig = this.config.tools ?? [];
1584
- const byName = new Map();
1585
- for (const t of this.builtIn)
1586
- byName.set(t.name, t);
1587
- for (const t of fromConfig)
1588
- byName.set(t.name, t);
1589
- return Array.from(byName.values());
1590
- }
1591
- resolve(name) {
1592
- return this.list().find(t => t.name === name);
1783
+ class ToastContainerComponent {
1784
+ get themeClass() {
1785
+ return `theme-${this.themeService.currentTheme()}`;
1593
1786
  }
1594
- /** Run a registered client tool and serialize its result for posting back to the agent loop. */
1595
- async runTool(name, args) {
1596
- const tool = this.resolve(name);
1597
- if (!tool)
1598
- return { ok: false, result: `Unknown client tool: ${name}` };
1599
- try {
1600
- const raw = await Promise.resolve(tool.handler(args ?? {}));
1601
- const result = typeof raw === 'string' ? raw : JSON.stringify(raw ?? null);
1602
- return { ok: true, result };
1603
- }
1604
- catch (err) {
1605
- return { ok: false, result: err?.message ?? String(err) };
1606
- }
1787
+ toastService = inject(ToastService);
1788
+ themeService = inject(ThemeService);
1789
+ toasts = this.toastService.toasts;
1790
+ close(id) {
1791
+ this.toastService.remove(id);
1607
1792
  }
1608
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiToolsRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1609
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiToolsRegistry, providedIn: 'root' });
1793
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: ToastContainerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1794
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: ToastContainerComponent, isStandalone: true, selector: "ma-toast-container", host: { properties: { "class": "this.themeClass" } }, ngImport: i0, template: "<div class=\"toast-container\">\n @for (toast of toasts(); track toast.id) {\n <div class=\"toast toast-{{ toast.type }}\">\n\n <!-- Type icon -->\n <div class=\"toast-icon\">\n @if (toast.type === 'info') {\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=\"M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9\"/><path d=\"M13.73 21a2 2 0 0 1-3.46 0\"/>\n </svg>\n }\n @if (toast.type === 'success') {\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=\"M22 11.08V12a10 10 0 1 1-5.93-9.14\"/><polyline points=\"22 4 12 14.01 9 11.01\"/>\n </svg>\n }\n @if (toast.type === 'warning') {\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=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/><line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"/><line x1=\"12\" y1=\"17\" x2=\"12.01\" y2=\"17\"/>\n </svg>\n }\n @if (toast.type === 'error') {\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 <circle cx=\"12\" cy=\"12\" r=\"10\"/><line x1=\"15\" y1=\"9\" x2=\"9\" y2=\"15\"/><line x1=\"9\" y1=\"9\" x2=\"15\" y2=\"15\"/>\n </svg>\n }\n </div>\n\n <!-- Content -->\n <div class=\"toast-content\">\n @if (toast.title) {\n <div class=\"toast-title\">{{ toast.title }}</div>\n }\n <div class=\"toast-message\" [innerHTML]=\"toast.message\"></div>\n </div>\n\n <!-- Close -->\n <button class=\"toast-close\" (click)=\"close(toast.id)\" aria-label=\"Close\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" 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\n <!-- Auto-dismiss progress bar \u2014 hidden when duration <= 0 (persistent toast) -->\n @if (toast.duration == null || toast.duration > 0) {\n <div class=\"toast-progress\" [style.animation-duration]=\"(toast.duration ?? 5000) + 'ms'\"></div>\n }\n </div>\n }\n</div>\n", styles: [".toast-container{position:fixed;top:20px;right:20px;z-index:9999;pointer-events:none;display:flex;flex-direction:column;gap:10px}.toast{position:relative;display:flex;align-items:flex-start;gap:11px;padding:13px 13px 16px 16px;border-radius:12px;background:var(--bg-primary);border:1px solid var(--border-color);box-shadow:0 8px 28px var(--shadow-lg),0 2px 8px var(--shadow);pointer-events:auto;min-width:300px;max-width:420px;overflow:hidden;animation:toast-in .35s cubic-bezier(.16,1,.3,1)}@keyframes toast-in{0%{opacity:0;transform:translate(36px) scale(.96)}to{opacity:1;transform:translate(0) scale(1)}}.toast:before{content:\"\";position:absolute;left:0;top:0;bottom:0;width:4px;border-radius:12px 0 0 12px}.toast-info:before{background:var(--info-color)}.toast-success:before{background:var(--success-color)}.toast-warning:before{background:var(--warning-color)}.toast-error:before{background:var(--error-color)}.toast-icon{flex-shrink:0;width:34px;height:34px;border-radius:9px;display:flex;align-items:center;justify-content:center;margin-left:2px}.toast-info .toast-icon{color:var(--info-color);background:var(--info-bg)}.toast-success .toast-icon{color:var(--success-color);background:var(--success-bg)}.toast-warning .toast-icon{color:var(--warning-color);background:var(--warning-bg)}.toast-error .toast-icon{color:var(--error-color);background:var(--error-bg)}.toast-content{flex:1;min-width:0;padding-top:1px}.toast-title{font-weight:700;font-size:13.5px;margin-bottom:3px;line-height:1.3}.toast-info .toast-title{color:var(--info-color)}.toast-success .toast-title{color:var(--success-color)}.toast-warning .toast-title{color:var(--warning-color)}.toast-error .toast-title{color:var(--error-color)}.toast-message{font-size:12.5px;line-height:1.45;color:var(--text-secondary)}.toast-message h3{margin:0 0 4px;font-size:13.5px;color:inherit}.toast-close{background:none;border:none;cursor:pointer;color:var(--text-secondary);width:26px;height:26px;border-radius:6px;display:flex;align-items:center;justify-content:center;flex-shrink:0;padding:0;transition:color .15s,background-color .15s}.toast-close:hover{color:var(--text-primary);background:var(--border-color)}.toast-progress{position:absolute;bottom:0;left:4px;right:0;height:3px;border-radius:0 0 12px;animation:toast-progress linear forwards;opacity:.7}.toast-info .toast-progress{background:var(--info-color)}.toast-success .toast-progress{background:var(--success-color)}.toast-warning .toast-progress{background:var(--warning-color)}.toast-error .toast-progress{background:var(--error-color)}@keyframes toast-progress{0%{width:calc(100% - 4px)}to{width:0}}@media(max-width:600px){.toast-container{top:10px;right:10px;left:10px}.toast{min-width:auto;max-width:100%}}\n"] });
1610
1795
  }
1611
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiToolsRegistry, decorators: [{
1612
- type: Injectable,
1613
- args: [{ providedIn: 'root' }]
1614
- }] });
1796
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: ToastContainerComponent, decorators: [{
1797
+ type: Component,
1798
+ args: [{ selector: 'ma-toast-container', imports: [], template: "<div class=\"toast-container\">\n @for (toast of toasts(); track toast.id) {\n <div class=\"toast toast-{{ toast.type }}\">\n\n <!-- Type icon -->\n <div class=\"toast-icon\">\n @if (toast.type === 'info') {\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=\"M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9\"/><path d=\"M13.73 21a2 2 0 0 1-3.46 0\"/>\n </svg>\n }\n @if (toast.type === 'success') {\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=\"M22 11.08V12a10 10 0 1 1-5.93-9.14\"/><polyline points=\"22 4 12 14.01 9 11.01\"/>\n </svg>\n }\n @if (toast.type === 'warning') {\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=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/><line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"/><line x1=\"12\" y1=\"17\" x2=\"12.01\" y2=\"17\"/>\n </svg>\n }\n @if (toast.type === 'error') {\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 <circle cx=\"12\" cy=\"12\" r=\"10\"/><line x1=\"15\" y1=\"9\" x2=\"9\" y2=\"15\"/><line x1=\"9\" y1=\"9\" x2=\"15\" y2=\"15\"/>\n </svg>\n }\n </div>\n\n <!-- Content -->\n <div class=\"toast-content\">\n @if (toast.title) {\n <div class=\"toast-title\">{{ toast.title }}</div>\n }\n <div class=\"toast-message\" [innerHTML]=\"toast.message\"></div>\n </div>\n\n <!-- Close -->\n <button class=\"toast-close\" (click)=\"close(toast.id)\" aria-label=\"Close\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" 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\n <!-- Auto-dismiss progress bar \u2014 hidden when duration <= 0 (persistent toast) -->\n @if (toast.duration == null || toast.duration > 0) {\n <div class=\"toast-progress\" [style.animation-duration]=\"(toast.duration ?? 5000) + 'ms'\"></div>\n }\n </div>\n }\n</div>\n", styles: [".toast-container{position:fixed;top:20px;right:20px;z-index:9999;pointer-events:none;display:flex;flex-direction:column;gap:10px}.toast{position:relative;display:flex;align-items:flex-start;gap:11px;padding:13px 13px 16px 16px;border-radius:12px;background:var(--bg-primary);border:1px solid var(--border-color);box-shadow:0 8px 28px var(--shadow-lg),0 2px 8px var(--shadow);pointer-events:auto;min-width:300px;max-width:420px;overflow:hidden;animation:toast-in .35s cubic-bezier(.16,1,.3,1)}@keyframes toast-in{0%{opacity:0;transform:translate(36px) scale(.96)}to{opacity:1;transform:translate(0) scale(1)}}.toast:before{content:\"\";position:absolute;left:0;top:0;bottom:0;width:4px;border-radius:12px 0 0 12px}.toast-info:before{background:var(--info-color)}.toast-success:before{background:var(--success-color)}.toast-warning:before{background:var(--warning-color)}.toast-error:before{background:var(--error-color)}.toast-icon{flex-shrink:0;width:34px;height:34px;border-radius:9px;display:flex;align-items:center;justify-content:center;margin-left:2px}.toast-info .toast-icon{color:var(--info-color);background:var(--info-bg)}.toast-success .toast-icon{color:var(--success-color);background:var(--success-bg)}.toast-warning .toast-icon{color:var(--warning-color);background:var(--warning-bg)}.toast-error .toast-icon{color:var(--error-color);background:var(--error-bg)}.toast-content{flex:1;min-width:0;padding-top:1px}.toast-title{font-weight:700;font-size:13.5px;margin-bottom:3px;line-height:1.3}.toast-info .toast-title{color:var(--info-color)}.toast-success .toast-title{color:var(--success-color)}.toast-warning .toast-title{color:var(--warning-color)}.toast-error .toast-title{color:var(--error-color)}.toast-message{font-size:12.5px;line-height:1.45;color:var(--text-secondary)}.toast-message h3{margin:0 0 4px;font-size:13.5px;color:inherit}.toast-close{background:none;border:none;cursor:pointer;color:var(--text-secondary);width:26px;height:26px;border-radius:6px;display:flex;align-items:center;justify-content:center;flex-shrink:0;padding:0;transition:color .15s,background-color .15s}.toast-close:hover{color:var(--text-primary);background:var(--border-color)}.toast-progress{position:absolute;bottom:0;left:4px;right:0;height:3px;border-radius:0 0 12px;animation:toast-progress linear forwards;opacity:.7}.toast-info .toast-progress{background:var(--info-color)}.toast-success .toast-progress{background:var(--success-color)}.toast-warning .toast-progress{background:var(--warning-color)}.toast-error .toast-progress{background:var(--error-color)}@keyframes toast-progress{0%{width:calc(100% - 4px)}to{width:0}}@media(max-width:600px){.toast-container{top:10px;right:10px;left:10px}.toast{min-width:auto;max-width:100%}}\n"] }]
1799
+ }], propDecorators: { themeClass: [{
1800
+ type: HostBinding,
1801
+ args: ['class']
1802
+ }] } });
1615
1803
 
1616
- /**
1617
- * Drives the chat panel.
1618
- *
1619
- * Uses `fetch` + `ReadableStream` (not `EventSource`, which can't POST a body) to talk to
1620
- * `/ai/chat` SSE. State lives in signals so the panel can re-render reactively.
1621
- */
1622
- class MaAiService {
1623
- auth = inject(MesAuthService);
1624
- aiConfig = inject(MES_AUTH_AI_CONFIG, { optional: true }) ?? DEFAULT_AI_CONFIG;
1625
- tools = inject(MaAiToolsRegistry);
1626
- messages = signal([], ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
1627
- streaming = signal(false, ...(ngDevMode ? [{ debugName: "streaming" }] : /* istanbul ignore next */ []));
1628
- /** When non-null, a write-class client tool is awaiting user confirmation. */
1629
- pendingApproval = signal(null, ...(ngDevMode ? [{ debugName: "pendingApproval" }] : /* istanbul ignore next */ []));
1630
- lastError = signal(null, ...(ngDevMode ? [{ debugName: "lastError" }] : /* istanbul ignore next */ []));
1631
- sessionId = null;
1632
- /** Verbs the user already chose "always approve" for in this session. */
1633
- alwaysApproved = new Set();
1634
- abortController = null;
1635
- /** ID of the current assistant bubble we're streaming tokens into. */
1636
- currentAssistantId = null;
1637
- get enabled() {
1638
- return this.aiConfig.enabled !== false;
1804
+ class NotificationPanelComponent {
1805
+ notificationRead = output();
1806
+ get themeClass() {
1807
+ return `theme-${this.themeService.currentTheme()}`;
1639
1808
  }
1640
- /** Start a brand-new conversation. */
1641
- resetSession() {
1642
- this.cancel();
1643
- this.sessionId = null;
1644
- this.messages.set([]);
1645
- this.pendingApproval.set(null);
1646
- this.lastError.set(null);
1647
- this.alwaysApproved.clear();
1809
+ isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
1810
+ activeTab = signal('unread', ...(ngDevMode ? [{ debugName: "activeTab" }] : /* istanbul ignore next */ []));
1811
+ notifications = signal([], ...(ngDevMode ? [{ debugName: "notifications" }] : /* istanbul ignore next */ []));
1812
+ selectedNotification = signal(null, ...(ngDevMode ? [{ debugName: "selectedNotification" }] : /* istanbul ignore next */ []));
1813
+ selectedNotificationHtml = signal(null, ...(ngDevMode ? [{ debugName: "selectedNotificationHtml" }] : /* istanbul ignore next */ []));
1814
+ selectedNotificationDate = signal('', ...(ngDevMode ? [{ debugName: "selectedNotificationDate" }] : /* istanbul ignore next */ []));
1815
+ // Stable time-ago strings keyed by notification id - refreshed every 30s via signal
1816
+ dateLabels = signal(new Map(), ...(ngDevMode ? [{ debugName: "dateLabels" }] : /* istanbul ignore next */ []));
1817
+ unreadNotifications = computed(() => this.notifications().filter(n => !n.isRead), ...(ngDevMode ? [{ debugName: "unreadNotifications" }] : /* istanbul ignore next */ []));
1818
+ readNotifications = computed(() => this.notifications().filter(n => n.isRead), ...(ngDevMode ? [{ debugName: "readNotifications" }] : /* istanbul ignore next */ []));
1819
+ currentNotifications = computed(() => this.activeTab() === 'unread' ? this.unreadNotifications() : this.readNotifications(), ...(ngDevMode ? [{ debugName: "currentNotifications" }] : /* istanbul ignore next */ []));
1820
+ dateTimer = null;
1821
+ // Normalize type to string - API may return integer or string
1822
+ // Backend enum: Info=0, Success=1, Warning=2, Error=3
1823
+ typeOf(notification) {
1824
+ const t = notification.type;
1825
+ if (t === 0 || t === 'Info')
1826
+ return 'Info';
1827
+ if (t === 1 || t === 'Success')
1828
+ return 'Success';
1829
+ if (t === 2 || t === 'Warning')
1830
+ return 'Warning';
1831
+ if (t === 3 || t === 'Error')
1832
+ return 'Error';
1833
+ return 'Info';
1648
1834
  }
1649
- /** Cancel the in-flight stream (if any). The session id is preserved so the user can resume. */
1650
- cancel() {
1651
- if (this.abortController) {
1652
- this.abortController.abort();
1653
- this.abortController = null;
1654
- }
1655
- this.streaming.set(false);
1656
- this.currentAssistantId = null;
1657
- // If a client-tool call was awaiting the user, clearing it lets them start over.
1658
- this.pendingApproval.set(null);
1835
+ toastType(type) {
1836
+ const t = this.typeOf({ type });
1837
+ return t.toLowerCase();
1659
1838
  }
1660
- /** User typed and submitted a message. */
1661
- async send(text, currentRoute) {
1662
- if (!text.trim())
1663
- return;
1664
- this.lastError.set(null);
1665
- this.appendBubble({ id: this.uid(), role: 'user', text });
1666
- await this.runStream({ message: text }, currentRoute);
1839
+ sanitizer = inject(DomSanitizer);
1840
+ authService = inject(MesAuthService);
1841
+ toastService = inject(ToastService);
1842
+ themeService = inject(ThemeService);
1843
+ host = inject(ElementRef);
1844
+ constructor() {
1845
+ this.loadNotifications();
1846
+ // Refresh time-ago labels every 30s - signal mutation triggers CD in zoneless
1847
+ this.dateTimer = setInterval(() => this.refreshDateLabels(), 30000);
1848
+ const latestNotification = toSignal(this.authService.notifications$);
1849
+ effect(() => {
1850
+ const notification = latestNotification();
1851
+ if (!notification)
1852
+ return;
1853
+ this.toastService.show(notification.message || '', notification.title, this.toastType(notification.type), 5000);
1854
+ this.loadNotifications();
1855
+ });
1856
+ // Close when the user clicks outside the panel - matches ma-approval-panel UX.
1857
+ effect((onCleanup) => {
1858
+ if (!this.isOpen())
1859
+ return;
1860
+ const onDocClick = (ev) => {
1861
+ const panel = this.host.nativeElement.querySelector('.notification-panel');
1862
+ if (panel && !panel.contains(ev.target))
1863
+ this.close();
1864
+ };
1865
+ // Defer to skip the same click that opened the panel.
1866
+ const tid = setTimeout(() => document.addEventListener('mousedown', onDocClick), 0);
1867
+ onCleanup(() => {
1868
+ clearTimeout(tid);
1869
+ document.removeEventListener('mousedown', onDocClick);
1870
+ });
1871
+ });
1667
1872
  }
1668
- /**
1669
- * Approve (or decline) a pending client tool call. If approved (and alwaysApprove),
1670
- * remember the verb so we skip the prompt next time in this session.
1671
- */
1672
- async resolvePendingApproval(decision) {
1673
- const pending = this.pendingApproval();
1674
- if (!pending)
1675
- return;
1676
- this.pendingApproval.set(null);
1677
- if (decision === 'decline') {
1678
- await this.continueWithToolResult(pending.callId, false, 'User declined this tool call.');
1679
- this.markToolStatus(pending.callId, 'declined');
1680
- return;
1681
- }
1682
- if (decision === 'always') {
1683
- this.alwaysApproved.add(this.verbOf(pending.name));
1873
+ ngOnDestroy() {
1874
+ if (this.dateTimer !== null) {
1875
+ clearInterval(this.dateTimer);
1876
+ this.dateTimer = null;
1684
1877
  }
1685
- await this.executeClientTool(pending.callId, pending.name, pending.args);
1686
1878
  }
1687
- // ── private ────────────────────────────────────────────────────────────────
1688
- async runStream(payload, currentRoute) {
1689
- if (!this.enabled) {
1690
- this.lastError.set('AI assistant is disabled.');
1691
- return;
1692
- }
1693
- const config = this.auth.getConfig();
1694
- const base = config?.apiBaseUrl?.replace(/\/$/, '') ?? '';
1695
- if (!base) {
1696
- this.lastError.set('MesAuth is not configured.');
1697
- return;
1698
- }
1699
- // Start a fresh assistant bubble that we stream tokens into.
1700
- const assistantId = this.uid();
1701
- this.currentAssistantId = assistantId;
1702
- this.appendBubble({ id: assistantId, role: 'assistant', text: '', pending: true, toolEvents: [] });
1703
- this.streaming.set(true);
1704
- this.abortController = new AbortController();
1705
- try {
1706
- const res = await fetch(`${base}/ai/chat`, {
1707
- method: 'POST',
1708
- headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream' },
1709
- credentials: config?.withCredentials !== false ? 'include' : 'same-origin',
1710
- body: JSON.stringify({
1711
- sessionId: this.sessionId,
1712
- message: payload.message,
1713
- toolResult: payload.toolResult,
1714
- clientTools: this.tools.list().map(t => ({
1715
- name: t.name,
1716
- description: t.description,
1717
- parameters: t.parameters,
1718
- readOnly: t.readOnly === true
1719
- })),
1720
- context: { currentRoute, appName: this.aiConfig.appName }
1721
- }),
1722
- signal: this.abortController.signal
1879
+ loadNotifications() {
1880
+ this.authService.getNotifications(1, 50, true).subscribe({
1881
+ next: (response) => {
1882
+ this.notifications.set(response.items || []);
1883
+ this.refreshDateLabels();
1884
+ },
1885
+ error: () => { }
1886
+ });
1887
+ }
1888
+ open() {
1889
+ this.isOpen.set(true);
1890
+ this.activeTab.set('unread');
1891
+ }
1892
+ close() {
1893
+ this.isOpen.set(false);
1894
+ }
1895
+ switchTab(tab) {
1896
+ this.activeTab.set(tab);
1897
+ }
1898
+ openDetails(notification) {
1899
+ this.selectedNotification.set(notification);
1900
+ const html = notification.messageHtml || notification.message || '';
1901
+ this.selectedNotificationHtml.set(this.sanitizer.bypassSecurityTrustHtml(html));
1902
+ this.selectedNotificationDate.set(this.formatDate(notification.createdAt));
1903
+ if (!notification.isRead) {
1904
+ this.authService.markAsRead(notification.id).subscribe({
1905
+ next: () => {
1906
+ this.notifications.update(notifs => notifs.map(n => n.id === notification.id ? { ...n, isRead: true } : n));
1907
+ this.notificationRead.emit();
1908
+ },
1909
+ error: () => { }
1723
1910
  });
1724
- if (!res.ok || !res.body) {
1725
- const detail = await res.text().catch(() => '');
1726
- throw new Error(`AI request failed (${res.status}). ${detail}`);
1727
- }
1728
- await this.readSse(res.body);
1729
1911
  }
1730
- catch (err) {
1731
- if (err?.name === 'AbortError') {
1732
- // user cancelled
1733
- }
1734
- else {
1735
- this.lastError.set(err?.message ?? String(err));
1736
- this.finalizeAssistant(`[error: ${err?.message ?? err}]`);
1737
- }
1912
+ }
1913
+ closeDetails() {
1914
+ this.selectedNotification.set(null);
1915
+ this.selectedNotificationHtml.set(null);
1916
+ this.selectedNotificationDate.set('');
1917
+ }
1918
+ openUrl() {
1919
+ const url = this.selectedNotification()?.url?.trim();
1920
+ if (url) {
1921
+ window.open(url, '_blank', 'noopener,noreferrer');
1738
1922
  }
1739
- finally {
1740
- this.streaming.set(false);
1741
- this.abortController = null;
1923
+ }
1924
+ markAsRead(notificationId, event) {
1925
+ if (event)
1926
+ event.stopPropagation();
1927
+ this.authService.markAsRead(notificationId).subscribe({
1928
+ next: () => {
1929
+ this.notifications.update(notifs => notifs.map(n => n.id === notificationId ? { ...n, isRead: true } : n));
1930
+ this.notificationRead.emit();
1931
+ },
1932
+ error: () => { }
1933
+ });
1934
+ }
1935
+ markAllAsRead() {
1936
+ this.authService.markAllAsRead().subscribe({
1937
+ next: () => {
1938
+ this.notifications.update(notifs => notifs.map(n => ({ ...n, isRead: true })));
1939
+ this.notificationRead.emit();
1940
+ },
1941
+ error: () => { }
1942
+ });
1943
+ }
1944
+ deleteAllRead() {
1945
+ const readIds = this.notifications().filter(n => n.isRead).map(n => n.id);
1946
+ const deletePromises = readIds.map(id => firstValueFrom(this.authService.deleteNotification(id)));
1947
+ Promise.all(deletePromises).then(() => {
1948
+ this.notifications.update(notifs => notifs.filter(n => !n.isRead));
1949
+ }).catch(() => {
1950
+ this.loadNotifications();
1951
+ });
1952
+ }
1953
+ deleteAllUnread() {
1954
+ const unreadIds = this.notifications().filter(n => !n.isRead).map(n => n.id);
1955
+ const deletePromises = unreadIds.map(id => firstValueFrom(this.authService.deleteNotification(id)));
1956
+ Promise.all(deletePromises).then(() => {
1957
+ this.notifications.update(notifs => notifs.filter(n => n.isRead));
1958
+ this.notificationRead.emit();
1959
+ }).catch(() => {
1960
+ this.loadNotifications();
1961
+ });
1962
+ }
1963
+ delete(notificationId, event) {
1964
+ event.stopPropagation();
1965
+ const wasUnread = this.notifications().some(n => n.id === notificationId && !n.isRead);
1966
+ this.authService.deleteNotification(notificationId).subscribe({
1967
+ next: () => {
1968
+ this.notifications.update(notifs => notifs.filter(n => n.id !== notificationId));
1969
+ if (wasUnread)
1970
+ this.notificationRead.emit();
1971
+ },
1972
+ error: () => { }
1973
+ });
1974
+ }
1975
+ formatDate(dateString) {
1976
+ return this.computeTimeAgo(dateString, new Date());
1977
+ }
1978
+ // Pure computation - takes now as param so it never calls new Date() internally
1979
+ computeTimeAgo(dateString, now) {
1980
+ const normalizedDateString = this.parseUtcDate(dateString);
1981
+ const date = new Date(normalizedDateString);
1982
+ if (isNaN(date.getTime()))
1983
+ return 'Invalid date';
1984
+ const diffMs = now.getTime() - date.getTime();
1985
+ const diffMins = Math.floor(diffMs / 60000);
1986
+ const diffHours = Math.floor(diffMs / 3600000);
1987
+ const diffDays = Math.floor(diffMs / 86400000);
1988
+ if (diffMins < 1)
1989
+ return 'Now';
1990
+ if (diffMins < 60)
1991
+ return `${diffMins}m ago`;
1992
+ if (diffHours < 24)
1993
+ return `${diffHours}h ago`;
1994
+ if (diffDays < 7)
1995
+ return `${diffDays}d ago`;
1996
+ return date.toLocaleDateString();
1997
+ }
1998
+ // Rebuild dateLabels map and store as a new signal value so CD is triggered
1999
+ refreshDateLabels() {
2000
+ const now = new Date();
2001
+ const newLabels = new Map();
2002
+ for (const n of this.notifications()) {
2003
+ newLabels.set(n.id, this.computeTimeAgo(n.createdAt, now));
1742
2004
  }
2005
+ this.dateLabels.set(newLabels);
1743
2006
  }
1744
- async readSse(body) {
1745
- const reader = body.getReader();
1746
- const decoder = new TextDecoder();
1747
- let buffer = '';
1748
- while (true) {
1749
- const { value, done } = await reader.read();
1750
- if (done)
1751
- break;
1752
- buffer += decoder.decode(value, { stream: true });
1753
- // SSE events are separated by a blank line.
1754
- let idx;
1755
- while ((idx = buffer.indexOf('\n\n')) !== -1) {
1756
- const rawEvent = buffer.slice(0, idx);
1757
- buffer = buffer.slice(idx + 2);
1758
- const dataLine = rawEvent.split('\n').find(l => l.startsWith('data:'));
1759
- if (!dataLine)
1760
- continue;
1761
- const json = dataLine.slice(5).trim();
1762
- if (!json)
1763
- continue;
1764
- let ev;
1765
- try {
1766
- ev = JSON.parse(json);
1767
- }
1768
- catch {
1769
- continue;
1770
- }
1771
- await this.handleEvent(ev);
1772
- }
2007
+ // Parse date string from server (stored in UTC but without 'Z' suffix or 'T' separator)
2008
+ parseUtcDate(dateStr) {
2009
+ let normalized = dateStr.includes('T') ? dateStr : dateStr.replace(' ', 'T');
2010
+ if (!normalized.endsWith('Z') && !normalized.includes('+') && !normalized.includes('-', 10)) {
2011
+ normalized += 'Z';
2012
+ }
2013
+ return normalized;
2014
+ }
2015
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: NotificationPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2016
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: NotificationPanelComponent, isStandalone: true, selector: "ma-notification-panel", outputs: { notificationRead: "notificationRead" }, host: { properties: { "class": "this.themeClass" } }, ngImport: i0, template: "<div class=\"notification-panel\" [class.open]=\"isOpen()\">\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=\"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 <h3>Notifications</h3>\n </div>\n <button class=\"close-btn\" (click)=\"close()\" title=\"Close\" aria-label=\"Close notifications\">\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() === 'unread'\" (click)=\"switchTab('unread')\">\n Unread\n @if (unreadNotifications().length > 0) {\n <span class=\"tab-badge\">{{ unreadNotifications().length }}</span>\n }\n </button>\n <button class=\"tab-btn\" [class.active]=\"activeTab() === 'read'\" (click)=\"switchTab('read')\">\n Read\n @if (readNotifications().length > 0) {\n <span class=\"tab-badge read-badge\">{{ readNotifications().length }}</span>\n }\n </button>\n </div>\n\n <!-- Notifications List -->\n <div class=\"notifications-list\">\n @if (currentNotifications().length > 0) {\n @for (notification of currentNotifications(); track notification.id) {\n <div\n class=\"notification-item\"\n [class.unread]=\"!notification.isRead\"\n (click)=\"openDetails(notification)\"\n >\n @let t = typeOf(notification);\n <div class=\"notif-accent\"\n [class.type-info]=\"t === 'Info'\"\n [class.type-success]=\"t === 'Success'\"\n [class.type-warning]=\"t === 'Warning'\"\n [class.type-error]=\"t === 'Error'\"></div>\n <div class=\"notif-type-icon\"\n [class.type-info]=\"t === 'Info'\"\n [class.type-success]=\"t === 'Success'\"\n [class.type-warning]=\"t === 'Warning'\"\n [class.type-error]=\"t === 'Error'\">\n @if (t === 'Info') {\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" 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\"/><path d=\"M13.73 21a2 2 0 0 1-3.46 0\"/>\n </svg>\n }\n @if (t === 'Success') {\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"20 6 9 17 4 12\"/>\n </svg>\n }\n @if (t === 'Warning') {\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/><line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"/><line x1=\"12\" y1=\"17\" x2=\"12.01\" y2=\"17\"/>\n </svg>\n }\n @if (t === 'Error') {\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"12\" cy=\"12\" r=\"10\"/><line x1=\"15\" y1=\"9\" x2=\"9\" y2=\"15\"/><line x1=\"9\" y1=\"9\" x2=\"15\" y2=\"15\"/>\n </svg>\n }\n </div>\n <div class=\"notification-content\">\n <div class=\"notification-title\">{{ notification.title }}</div>\n <div class=\"notification-message\">{{ notification.message }}</div>\n <div class=\"notification-meta\">\n <span class=\"app-name\">{{ notification.sourceAppName }}</span>\n <span class=\"time\">{{ dateLabels().get(notification.id) }}</span>\n </div>\n </div>\n @if (!notification.isRead) {\n <button class=\"icon-btn read-btn\" (click)=\"markAsRead(notification.id, $event)\" title=\"Mark as read\" aria-label=\"Mark as read\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"20 6 9 17 4 12\"/>\n </svg>\n </button>\n }\n @if (notification.isRead) {\n <button class=\"icon-btn delete-btn\" (click)=\"delete(notification.id, $event)\" title=\"Delete\" aria-label=\"Delete notification\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"3 6 5 6 21 6\"/><path d=\"M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6\"/><path d=\"M10 11v6\"/><path d=\"M14 11v6\"/><path d=\"M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2\"/>\n </svg>\n </button>\n }\n </div>\n }\n } @else {\n <div class=\"empty-state\">\n <svg class=\"empty-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"44\" height=\"44\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" 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 <p>No {{ activeTab() }} notifications</p>\n </div>\n }\n </div>\n\n <!-- Footer Actions -->\n @if (currentNotifications().length > 0) {\n <div class=\"panel-footer\">\n @if (activeTab() === 'unread') {\n <div class=\"footer-actions\">\n @if (unreadNotifications().length > 0) {\n <button class=\"action-btn\" (click)=\"markAllAsRead()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"/></svg>\n Mark all read\n </button>\n <button class=\"action-btn delete-all-btn\" (click)=\"deleteAllUnread()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"3 6 5 6 21 6\"/><path d=\"M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6\"/></svg>\n Delete all\n </button>\n }\n </div>\n }\n @if (activeTab() === 'read' && readNotifications().length > 0) {\n <button class=\"action-btn delete-all-btn\" (click)=\"deleteAllRead()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"3 6 5 6 21 6\"/><path d=\"M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6\"/></svg>\n Delete all\n </button>\n }\n </div>\n }\n</div>\n\n<!-- Details Modal -->\n@if (selectedNotification()) {\n <div class=\"modal-overlay\" (click)=\"closeDetails()\">\n <div class=\"modal-container\" (click)=\"$event.stopPropagation()\">\n <div class=\"modal-header\"\n [class.modal-type-info]=\"typeOf(selectedNotification()!) === 'Info'\"\n [class.modal-type-success]=\"typeOf(selectedNotification()!) === 'Success'\"\n [class.modal-type-warning]=\"typeOf(selectedNotification()!) === 'Warning'\"\n [class.modal-type-error]=\"typeOf(selectedNotification()!) === 'Error'\">\n <div class=\"modal-header-left\">\n <div class=\"modal-type-icon\">\n @if (typeOf(selectedNotification()!) === 'Info') {\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=\"M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9\"/><path d=\"M13.73 21a2 2 0 0 1-3.46 0\"/>\n </svg>\n }\n @if (typeOf(selectedNotification()!) === 'Success') {\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=\"M22 11.08V12a10 10 0 1 1-5.93-9.14\"/><polyline points=\"22 4 12 14.01 9 11.01\"/>\n </svg>\n }\n @if (typeOf(selectedNotification()!) === 'Warning') {\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=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/><line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"/>\n </svg>\n }\n @if (typeOf(selectedNotification()!) === 'Error') {\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 <circle cx=\"12\" cy=\"12\" r=\"10\"/><line x1=\"15\" y1=\"9\" x2=\"9\" y2=\"15\"/><line x1=\"9\" y1=\"9\" x2=\"15\" y2=\"15\"/>\n </svg>\n }\n </div>\n <h3>{{ selectedNotification()!.title }}</h3>\n </div>\n <button class=\"close-btn\" (click)=\"closeDetails()\" title=\"Close\" aria-label=\"Close notification detail\">\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 <div class=\"modal-meta\">\n <span class=\"app-name\">{{ selectedNotification()!.sourceAppName }}</span>\n <span class=\"time\">{{ selectedNotificationDate() }}</span>\n </div>\n <div class=\"modal-body\" [innerHTML]=\"selectedNotificationHtml()\"></div>\n <div class=\"modal-footer\">\n @if (selectedNotification()?.url?.trim()) {\n <button class=\"action-btn see-details-btn\" (click)=\"openUrl()\">See Details</button>\n }\n <button class=\"action-btn\" (click)=\"closeDetails()\">Close</button>\n </div>\n </div>\n </div>\n}\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{display:block;position:relative;--primary: #1976d2;--primary-hover: #1565c0;--success: #43a047;--error: #f44336;--error-hover: #d32f2f;--info-color: #2196f3;--info-bg: rgba(33, 150, 243, .1);--success-bg: rgba(67, 160, 71, .1);--warning-color: #f57c00;--warning-bg: rgba(245, 124, 0, .1);--error-bg: rgba(244, 67, 54, .1);--text-primary: #212121;--text-secondary: #616161;--text-muted: #9e9e9e;--bg-primary: #ffffff;--bg-secondary: #f8f9fa;--bg-hover: #f0f4ff;--bg-unread: rgba(25, 118, 210, .06);--border-color: #e0e0e0;--border-light: #eeeeee;--shadow: rgba(0, 0, 0, .15)}.tab-btn:not(.active) .tab-badge{background:var(--error)}.read-badge{background:var(--text-muted)}.notification-panel{position:fixed;top:0;right:-360px;width:360px;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)}.notification-panel.open{right:var(--ma-ai-panel-width, 0px)}.notifications-list{flex:1;overflow-y:auto}.notification-item{display:flex;align-items:flex-start;gap:0;border-bottom:1px solid var(--border-light);cursor:pointer;background:var(--bg-primary);transition:background-color .15s;position:relative}.notification-item:hover{background:var(--bg-hover)}.notification-item.unread{background:var(--bg-unread)}.notif-accent{width:3px;align-self:stretch;flex-shrink:0;background:transparent;border-radius:0 2px 2px 0;opacity:.3}.notification-item.unread .notif-accent{opacity:1}.notif-accent.type-info{background:var(--info-color)}.notif-accent.type-success{background:var(--success)}.notif-accent.type-warning{background:var(--warning-color)}.notif-accent.type-error{background:var(--error)}.notif-type-icon{flex-shrink:0;width:26px;height:26px;border-radius:7px;display:flex;align-items:center;justify-content:center;align-self:center;margin-left:10px}.notif-type-icon.type-info{color:var(--info-color);background:var(--info-bg)}.notif-type-icon.type-success{color:var(--success);background:var(--success-bg)}.notif-type-icon.type-warning{color:var(--warning-color);background:var(--warning-bg)}.notif-type-icon.type-error{color:var(--error);background:var(--error-bg)}.notification-content{flex:1;min-width:0;padding:12px 8px 12px 12px}.notification-title{font-weight:600;color:var(--text-primary);font-size:13.5px;margin-bottom:3px;line-height:1.35}.notification-message{color:var(--text-secondary);font-size:12px;line-height:1.45;margin-bottom:7px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.notification-meta{display:flex;justify-content:space-between;font-size:11px;color:var(--text-muted)}.app-name{font-weight:600;color:var(--primary)}.icon-btn{background:none;border:none;cursor:pointer;width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;flex-shrink:0;align-self:center;margin-right:8px;transition:color .15s,background-color .15s;color:var(--text-muted)}.read-btn:hover{color:var(--success);background:#43a0471a}.delete-btn:hover{color:var(--error);background:#f443361a}.panel-footer{padding:10px 14px;border-top:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0}.footer-actions{display:flex;gap:8px}.footer-actions .action-btn{flex:1}.action-btn{display:flex;align-items:center;justify-content:center;gap:6px;width:100%;padding:8px 12px;background:var(--primary);color:#fff;border:none;border-radius:8px;cursor:pointer;font-size:12.5px;font-weight:600;transition:background-color .18s,transform .12s}.action-btn:hover{background:var(--primary-hover);transform:translateY(-1px)}.delete-all-btn{background:var(--error)}.delete-all-btn:hover{background:var(--error-hover)}.modal-overlay{position:fixed;inset:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:1060;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.modal-container{background:var(--bg-primary);border-radius:14px;width:92%;max-width:860px;max-height:88vh;display:flex;flex-direction:column;box-shadow:0 16px 48px #00000040;animation:modal-in .2s cubic-bezier(.16,1,.3,1)}@keyframes modal-in{0%{opacity:0;transform:scale(.94) translateY(8px)}to{opacity:1;transform:scale(1) translateY(0)}}.modal-header{display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid var(--border-color);background:var(--bg-secondary);border-radius:14px 14px 0 0;border-top:3px solid transparent}.modal-header.modal-type-info{border-top-color:var(--info-color)}.modal-header.modal-type-success{border-top-color:var(--success)}.modal-header.modal-type-warning{border-top-color:var(--warning-color)}.modal-header.modal-type-error{border-top-color:var(--error)}.modal-header-left{display:flex;align-items:center;gap:10px;min-width:0}.modal-type-icon{flex-shrink:0;width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center}.modal-type-info .modal-type-icon{color:var(--info-color);background:var(--info-bg)}.modal-type-success .modal-type-icon{color:var(--success);background:var(--success-bg)}.modal-type-warning .modal-type-icon{color:var(--warning-color);background:var(--warning-bg)}.modal-type-error .modal-type-icon{color:var(--error);background:var(--error-bg)}.modal-header h3{margin:0;font-size:15px;font-weight:700;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.modal-meta{display:flex;justify-content:space-between;padding:8px 20px;font-size:11.5px;color:var(--text-muted);border-bottom:1px solid var(--border-light)}.modal-body{padding:20px;overflow-y:auto;flex:1;color:var(--text-primary);font-size:14px;line-height:1.65}.modal-footer{padding:12px 20px;border-top:1px solid var(--border-color);background:var(--bg-secondary);border-radius:0 0 14px 14px;display:flex;justify-content:flex-end;gap:8px}.modal-footer .action-btn{width:auto;padding:8px 24px}.modal-footer .see-details-btn{background:var(--info-bg);color:var(--info-color);border:1px solid var(--info-color)}.modal-footer .see-details-btn:hover{opacity:.85}@media(max-width:600px){.notification-panel{width:100%;right:-100%}.modal-container{width:95%;max-height:90vh}}\n"] });
2017
+ }
2018
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: NotificationPanelComponent, decorators: [{
2019
+ type: Component,
2020
+ args: [{ selector: 'ma-notification-panel', imports: [], template: "<div class=\"notification-panel\" [class.open]=\"isOpen()\">\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=\"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 <h3>Notifications</h3>\n </div>\n <button class=\"close-btn\" (click)=\"close()\" title=\"Close\" aria-label=\"Close notifications\">\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() === 'unread'\" (click)=\"switchTab('unread')\">\n Unread\n @if (unreadNotifications().length > 0) {\n <span class=\"tab-badge\">{{ unreadNotifications().length }}</span>\n }\n </button>\n <button class=\"tab-btn\" [class.active]=\"activeTab() === 'read'\" (click)=\"switchTab('read')\">\n Read\n @if (readNotifications().length > 0) {\n <span class=\"tab-badge read-badge\">{{ readNotifications().length }}</span>\n }\n </button>\n </div>\n\n <!-- Notifications List -->\n <div class=\"notifications-list\">\n @if (currentNotifications().length > 0) {\n @for (notification of currentNotifications(); track notification.id) {\n <div\n class=\"notification-item\"\n [class.unread]=\"!notification.isRead\"\n (click)=\"openDetails(notification)\"\n >\n @let t = typeOf(notification);\n <div class=\"notif-accent\"\n [class.type-info]=\"t === 'Info'\"\n [class.type-success]=\"t === 'Success'\"\n [class.type-warning]=\"t === 'Warning'\"\n [class.type-error]=\"t === 'Error'\"></div>\n <div class=\"notif-type-icon\"\n [class.type-info]=\"t === 'Info'\"\n [class.type-success]=\"t === 'Success'\"\n [class.type-warning]=\"t === 'Warning'\"\n [class.type-error]=\"t === 'Error'\">\n @if (t === 'Info') {\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" 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\"/><path d=\"M13.73 21a2 2 0 0 1-3.46 0\"/>\n </svg>\n }\n @if (t === 'Success') {\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"20 6 9 17 4 12\"/>\n </svg>\n }\n @if (t === 'Warning') {\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/><line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"/><line x1=\"12\" y1=\"17\" x2=\"12.01\" y2=\"17\"/>\n </svg>\n }\n @if (t === 'Error') {\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"12\" cy=\"12\" r=\"10\"/><line x1=\"15\" y1=\"9\" x2=\"9\" y2=\"15\"/><line x1=\"9\" y1=\"9\" x2=\"15\" y2=\"15\"/>\n </svg>\n }\n </div>\n <div class=\"notification-content\">\n <div class=\"notification-title\">{{ notification.title }}</div>\n <div class=\"notification-message\">{{ notification.message }}</div>\n <div class=\"notification-meta\">\n <span class=\"app-name\">{{ notification.sourceAppName }}</span>\n <span class=\"time\">{{ dateLabels().get(notification.id) }}</span>\n </div>\n </div>\n @if (!notification.isRead) {\n <button class=\"icon-btn read-btn\" (click)=\"markAsRead(notification.id, $event)\" title=\"Mark as read\" aria-label=\"Mark as read\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"20 6 9 17 4 12\"/>\n </svg>\n </button>\n }\n @if (notification.isRead) {\n <button class=\"icon-btn delete-btn\" (click)=\"delete(notification.id, $event)\" title=\"Delete\" aria-label=\"Delete notification\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"3 6 5 6 21 6\"/><path d=\"M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6\"/><path d=\"M10 11v6\"/><path d=\"M14 11v6\"/><path d=\"M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2\"/>\n </svg>\n </button>\n }\n </div>\n }\n } @else {\n <div class=\"empty-state\">\n <svg class=\"empty-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"44\" height=\"44\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" 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 <p>No {{ activeTab() }} notifications</p>\n </div>\n }\n </div>\n\n <!-- Footer Actions -->\n @if (currentNotifications().length > 0) {\n <div class=\"panel-footer\">\n @if (activeTab() === 'unread') {\n <div class=\"footer-actions\">\n @if (unreadNotifications().length > 0) {\n <button class=\"action-btn\" (click)=\"markAllAsRead()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"/></svg>\n Mark all read\n </button>\n <button class=\"action-btn delete-all-btn\" (click)=\"deleteAllUnread()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"3 6 5 6 21 6\"/><path d=\"M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6\"/></svg>\n Delete all\n </button>\n }\n </div>\n }\n @if (activeTab() === 'read' && readNotifications().length > 0) {\n <button class=\"action-btn delete-all-btn\" (click)=\"deleteAllRead()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"3 6 5 6 21 6\"/><path d=\"M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6\"/></svg>\n Delete all\n </button>\n }\n </div>\n }\n</div>\n\n<!-- Details Modal -->\n@if (selectedNotification()) {\n <div class=\"modal-overlay\" (click)=\"closeDetails()\">\n <div class=\"modal-container\" (click)=\"$event.stopPropagation()\">\n <div class=\"modal-header\"\n [class.modal-type-info]=\"typeOf(selectedNotification()!) === 'Info'\"\n [class.modal-type-success]=\"typeOf(selectedNotification()!) === 'Success'\"\n [class.modal-type-warning]=\"typeOf(selectedNotification()!) === 'Warning'\"\n [class.modal-type-error]=\"typeOf(selectedNotification()!) === 'Error'\">\n <div class=\"modal-header-left\">\n <div class=\"modal-type-icon\">\n @if (typeOf(selectedNotification()!) === 'Info') {\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=\"M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9\"/><path d=\"M13.73 21a2 2 0 0 1-3.46 0\"/>\n </svg>\n }\n @if (typeOf(selectedNotification()!) === 'Success') {\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=\"M22 11.08V12a10 10 0 1 1-5.93-9.14\"/><polyline points=\"22 4 12 14.01 9 11.01\"/>\n </svg>\n }\n @if (typeOf(selectedNotification()!) === 'Warning') {\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=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/><line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"/>\n </svg>\n }\n @if (typeOf(selectedNotification()!) === 'Error') {\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 <circle cx=\"12\" cy=\"12\" r=\"10\"/><line x1=\"15\" y1=\"9\" x2=\"9\" y2=\"15\"/><line x1=\"9\" y1=\"9\" x2=\"15\" y2=\"15\"/>\n </svg>\n }\n </div>\n <h3>{{ selectedNotification()!.title }}</h3>\n </div>\n <button class=\"close-btn\" (click)=\"closeDetails()\" title=\"Close\" aria-label=\"Close notification detail\">\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 <div class=\"modal-meta\">\n <span class=\"app-name\">{{ selectedNotification()!.sourceAppName }}</span>\n <span class=\"time\">{{ selectedNotificationDate() }}</span>\n </div>\n <div class=\"modal-body\" [innerHTML]=\"selectedNotificationHtml()\"></div>\n <div class=\"modal-footer\">\n @if (selectedNotification()?.url?.trim()) {\n <button class=\"action-btn see-details-btn\" (click)=\"openUrl()\">See Details</button>\n }\n <button class=\"action-btn\" (click)=\"closeDetails()\">Close</button>\n </div>\n </div>\n </div>\n}\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{display:block;position:relative;--primary: #1976d2;--primary-hover: #1565c0;--success: #43a047;--error: #f44336;--error-hover: #d32f2f;--info-color: #2196f3;--info-bg: rgba(33, 150, 243, .1);--success-bg: rgba(67, 160, 71, .1);--warning-color: #f57c00;--warning-bg: rgba(245, 124, 0, .1);--error-bg: rgba(244, 67, 54, .1);--text-primary: #212121;--text-secondary: #616161;--text-muted: #9e9e9e;--bg-primary: #ffffff;--bg-secondary: #f8f9fa;--bg-hover: #f0f4ff;--bg-unread: rgba(25, 118, 210, .06);--border-color: #e0e0e0;--border-light: #eeeeee;--shadow: rgba(0, 0, 0, .15)}.tab-btn:not(.active) .tab-badge{background:var(--error)}.read-badge{background:var(--text-muted)}.notification-panel{position:fixed;top:0;right:-360px;width:360px;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)}.notification-panel.open{right:var(--ma-ai-panel-width, 0px)}.notifications-list{flex:1;overflow-y:auto}.notification-item{display:flex;align-items:flex-start;gap:0;border-bottom:1px solid var(--border-light);cursor:pointer;background:var(--bg-primary);transition:background-color .15s;position:relative}.notification-item:hover{background:var(--bg-hover)}.notification-item.unread{background:var(--bg-unread)}.notif-accent{width:3px;align-self:stretch;flex-shrink:0;background:transparent;border-radius:0 2px 2px 0;opacity:.3}.notification-item.unread .notif-accent{opacity:1}.notif-accent.type-info{background:var(--info-color)}.notif-accent.type-success{background:var(--success)}.notif-accent.type-warning{background:var(--warning-color)}.notif-accent.type-error{background:var(--error)}.notif-type-icon{flex-shrink:0;width:26px;height:26px;border-radius:7px;display:flex;align-items:center;justify-content:center;align-self:center;margin-left:10px}.notif-type-icon.type-info{color:var(--info-color);background:var(--info-bg)}.notif-type-icon.type-success{color:var(--success);background:var(--success-bg)}.notif-type-icon.type-warning{color:var(--warning-color);background:var(--warning-bg)}.notif-type-icon.type-error{color:var(--error);background:var(--error-bg)}.notification-content{flex:1;min-width:0;padding:12px 8px 12px 12px}.notification-title{font-weight:600;color:var(--text-primary);font-size:13.5px;margin-bottom:3px;line-height:1.35}.notification-message{color:var(--text-secondary);font-size:12px;line-height:1.45;margin-bottom:7px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.notification-meta{display:flex;justify-content:space-between;font-size:11px;color:var(--text-muted)}.app-name{font-weight:600;color:var(--primary)}.icon-btn{background:none;border:none;cursor:pointer;width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;flex-shrink:0;align-self:center;margin-right:8px;transition:color .15s,background-color .15s;color:var(--text-muted)}.read-btn:hover{color:var(--success);background:#43a0471a}.delete-btn:hover{color:var(--error);background:#f443361a}.panel-footer{padding:10px 14px;border-top:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0}.footer-actions{display:flex;gap:8px}.footer-actions .action-btn{flex:1}.action-btn{display:flex;align-items:center;justify-content:center;gap:6px;width:100%;padding:8px 12px;background:var(--primary);color:#fff;border:none;border-radius:8px;cursor:pointer;font-size:12.5px;font-weight:600;transition:background-color .18s,transform .12s}.action-btn:hover{background:var(--primary-hover);transform:translateY(-1px)}.delete-all-btn{background:var(--error)}.delete-all-btn:hover{background:var(--error-hover)}.modal-overlay{position:fixed;inset:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:1060;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.modal-container{background:var(--bg-primary);border-radius:14px;width:92%;max-width:860px;max-height:88vh;display:flex;flex-direction:column;box-shadow:0 16px 48px #00000040;animation:modal-in .2s cubic-bezier(.16,1,.3,1)}@keyframes modal-in{0%{opacity:0;transform:scale(.94) translateY(8px)}to{opacity:1;transform:scale(1) translateY(0)}}.modal-header{display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid var(--border-color);background:var(--bg-secondary);border-radius:14px 14px 0 0;border-top:3px solid transparent}.modal-header.modal-type-info{border-top-color:var(--info-color)}.modal-header.modal-type-success{border-top-color:var(--success)}.modal-header.modal-type-warning{border-top-color:var(--warning-color)}.modal-header.modal-type-error{border-top-color:var(--error)}.modal-header-left{display:flex;align-items:center;gap:10px;min-width:0}.modal-type-icon{flex-shrink:0;width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center}.modal-type-info .modal-type-icon{color:var(--info-color);background:var(--info-bg)}.modal-type-success .modal-type-icon{color:var(--success);background:var(--success-bg)}.modal-type-warning .modal-type-icon{color:var(--warning-color);background:var(--warning-bg)}.modal-type-error .modal-type-icon{color:var(--error);background:var(--error-bg)}.modal-header h3{margin:0;font-size:15px;font-weight:700;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.modal-meta{display:flex;justify-content:space-between;padding:8px 20px;font-size:11.5px;color:var(--text-muted);border-bottom:1px solid var(--border-light)}.modal-body{padding:20px;overflow-y:auto;flex:1;color:var(--text-primary);font-size:14px;line-height:1.65}.modal-footer{padding:12px 20px;border-top:1px solid var(--border-color);background:var(--bg-secondary);border-radius:0 0 14px 14px;display:flex;justify-content:flex-end;gap:8px}.modal-footer .action-btn{width:auto;padding:8px 24px}.modal-footer .see-details-btn{background:var(--info-bg);color:var(--info-color);border:1px solid var(--info-color)}.modal-footer .see-details-btn:hover{opacity:.85}@media(max-width:600px){.notification-panel{width:100%;right:-100%}.modal-container{width:95%;max-height:90vh}}\n"] }]
2021
+ }], ctorParameters: () => [], propDecorators: { notificationRead: [{ type: i0.Output, args: ["notificationRead"] }], themeClass: [{
2022
+ type: HostBinding,
2023
+ args: ['class']
2024
+ }] } });
2025
+
2026
+ class MaApprovalService {
2027
+ apiBase = '';
2028
+ http;
2029
+ config = null;
2030
+ constructor() { }
2031
+ init(config, httpClient) {
2032
+ this.config = config;
2033
+ this.http = httpClient;
2034
+ this.apiBase = config.apiBaseUrl.replace(/\/$/, '');
2035
+ }
2036
+ get opts() {
2037
+ return { withCredentials: this.config?.withCredentials ?? true };
2038
+ }
2039
+ // ====================== Dashboard ======================
2040
+ getDashboard() {
2041
+ return this.http.get(`${this.apiBase}/approval/dashboard`, this.opts);
2042
+ }
2043
+ // ====================== Pending & My Requests ======================
2044
+ getPendingApprovals(page = 1, pageSize = 20) {
2045
+ return this.http.get(`${this.apiBase}/approval/pending?page=${page}&pageSize=${pageSize}`, this.opts);
2046
+ }
2047
+ getMyRequests(page = 1, pageSize = 20, status) {
2048
+ let url = `${this.apiBase}/approval/my-requests?page=${page}&pageSize=${pageSize}`;
2049
+ if (status !== undefined)
2050
+ url += `&status=${status}`;
2051
+ return this.http.get(url, this.opts);
2052
+ }
2053
+ // ====================== Document ======================
2054
+ getDocument(id) {
2055
+ return this.http.get(`${this.apiBase}/approval/documents/${id}`, this.opts);
2056
+ }
2057
+ getDocumentHistory(id) {
2058
+ return this.http.get(`${this.apiBase}/approval/documents/${id}/history`, this.opts);
2059
+ }
2060
+ getDocumentContentUrl(id) {
2061
+ return `${this.apiBase}/approval/documents/${id}/content`;
2062
+ }
2063
+ getDocumentThumbnailUrl(id) {
2064
+ return `${this.apiBase}/approval/documents/${id}/thumbnail`;
2065
+ }
2066
+ // ====================== Actions ======================
2067
+ approve(documentId, comment) {
2068
+ const body = { comment };
2069
+ return this.http.post(`${this.apiBase}/approval/documents/${documentId}/approve`, body, this.opts);
2070
+ }
2071
+ reject(documentId, comment) {
2072
+ const body = { comment };
2073
+ return this.http.post(`${this.apiBase}/approval/documents/${documentId}/reject`, body, this.opts);
2074
+ }
2075
+ delegate(documentId, toUserId, reason) {
2076
+ const body = { toUserId, reason };
2077
+ return this.http.post(`${this.apiBase}/approval/documents/${documentId}/delegate`, body, this.opts);
2078
+ }
2079
+ cancel(documentId, reason) {
2080
+ let url = `${this.apiBase}/approval/documents/${documentId}`;
2081
+ if (reason)
2082
+ url += `?reason=${encodeURIComponent(reason)}`;
2083
+ return this.http.delete(url, this.opts);
2084
+ }
2085
+ // ====================== Templates ======================
2086
+ getTemplates(appId) {
2087
+ let url = `${this.apiBase}/approval/templates`;
2088
+ if (appId)
2089
+ url += `?appId=${encodeURIComponent(appId)}`;
2090
+ return this.http.get(url, this.opts);
2091
+ }
2092
+ getTemplate(id) {
2093
+ return this.http.get(`${this.apiBase}/approval/templates/${id}`, this.opts);
2094
+ }
2095
+ createTemplate(request) {
2096
+ return this.http.post(`${this.apiBase}/approval/templates`, request, this.opts);
2097
+ }
2098
+ updateTemplate(id, request) {
2099
+ return this.http.put(`${this.apiBase}/approval/templates/${id}`, request, this.opts);
2100
+ }
2101
+ deleteTemplate(id) {
2102
+ return this.http.delete(`${this.apiBase}/approval/templates/${id}`, this.opts);
2103
+ }
2104
+ previewRole(orgCode, level) {
2105
+ return this.http.get(`${this.apiBase}/approval/roles/preview?orgCode=${encodeURIComponent(orgCode)}&level=${encodeURIComponent(level)}`, this.opts);
2106
+ }
2107
+ // ====================== Create (used by ma-arv-container) ======================
2108
+ createApproval(request) {
2109
+ return this.http.post(`${this.apiBase}/approval/documents`, request, this.opts);
2110
+ }
2111
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaApprovalService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2112
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaApprovalService });
2113
+ }
2114
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaApprovalService, decorators: [{
2115
+ type: Injectable
2116
+ }], ctorParameters: () => [] });
2117
+
2118
+ // ====================== Enums ======================
2119
+ var ApprovalStepMode;
2120
+ (function (ApprovalStepMode) {
2121
+ ApprovalStepMode[ApprovalStepMode["Sequential"] = 0] = "Sequential";
2122
+ ApprovalStepMode[ApprovalStepMode["Parallel"] = 1] = "Parallel";
2123
+ })(ApprovalStepMode || (ApprovalStepMode = {}));
2124
+ var ApprovalDocumentStatus;
2125
+ (function (ApprovalDocumentStatus) {
2126
+ ApprovalDocumentStatus[ApprovalDocumentStatus["Draft"] = 0] = "Draft";
2127
+ ApprovalDocumentStatus[ApprovalDocumentStatus["Pending"] = 1] = "Pending";
2128
+ ApprovalDocumentStatus[ApprovalDocumentStatus["Approved"] = 2] = "Approved";
2129
+ ApprovalDocumentStatus[ApprovalDocumentStatus["Rejected"] = 3] = "Rejected";
2130
+ ApprovalDocumentStatus[ApprovalDocumentStatus["Cancelled"] = 4] = "Cancelled";
2131
+ ApprovalDocumentStatus[ApprovalDocumentStatus["Expired"] = 5] = "Expired";
2132
+ })(ApprovalDocumentStatus || (ApprovalDocumentStatus = {}));
2133
+ var ApprovalStepStatus;
2134
+ (function (ApprovalStepStatus) {
2135
+ ApprovalStepStatus[ApprovalStepStatus["Waiting"] = 0] = "Waiting";
2136
+ ApprovalStepStatus[ApprovalStepStatus["Active"] = 1] = "Active";
2137
+ ApprovalStepStatus[ApprovalStepStatus["Approved"] = 2] = "Approved";
2138
+ ApprovalStepStatus[ApprovalStepStatus["Rejected"] = 3] = "Rejected";
2139
+ ApprovalStepStatus[ApprovalStepStatus["Delegated"] = 4] = "Delegated";
2140
+ ApprovalStepStatus[ApprovalStepStatus["Expired"] = 5] = "Expired";
2141
+ ApprovalStepStatus[ApprovalStepStatus["Skipped"] = 6] = "Skipped";
2142
+ })(ApprovalStepStatus || (ApprovalStepStatus = {}));
2143
+ var ApprovalActionType;
2144
+ (function (ApprovalActionType) {
2145
+ ApprovalActionType[ApprovalActionType["Created"] = 0] = "Created";
2146
+ ApprovalActionType[ApprovalActionType["Submitted"] = 1] = "Submitted";
2147
+ ApprovalActionType[ApprovalActionType["Approved"] = 2] = "Approved";
2148
+ ApprovalActionType[ApprovalActionType["Rejected"] = 3] = "Rejected";
2149
+ ApprovalActionType[ApprovalActionType["Delegated"] = 4] = "Delegated";
2150
+ ApprovalActionType[ApprovalActionType["Cancelled"] = 5] = "Cancelled";
2151
+ ApprovalActionType[ApprovalActionType["Commented"] = 6] = "Commented";
2152
+ ApprovalActionType[ApprovalActionType["Expired"] = 7] = "Expired";
2153
+ ApprovalActionType[ApprovalActionType["StepAdvanced"] = 8] = "StepAdvanced";
2154
+ })(ApprovalActionType || (ApprovalActionType = {}));
2155
+
2156
+ class MaApprovalPanelComponent {
2157
+ approvalActioned = output();
2158
+ isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
2159
+ loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
2160
+ activeTab = signal('processing', ...(ngDevMode ? [{ debugName: "activeTab" }] : /* istanbul ignore next */ []));
2161
+ processingItems = signal([], ...(ngDevMode ? [{ debugName: "processingItems" }] : /* istanbul ignore next */ []));
2162
+ approvedItems = signal([], ...(ngDevMode ? [{ debugName: "approvedItems" }] : /* istanbul ignore next */ []));
2163
+ rejectedItems = signal([], ...(ngDevMode ? [{ debugName: "rejectedItems" }] : /* istanbul ignore next */ []));
2164
+ mesAuth = inject(MesAuthService);
2165
+ http = inject(HttpClient);
2166
+ router = inject(Router);
2167
+ host = inject(ElementRef);
2168
+ approvalSvc = null;
2169
+ constructor() {
2170
+ const config = this.mesAuth.getConfig();
2171
+ if (config) {
2172
+ this.approvalSvc = new MaApprovalService();
2173
+ this.approvalSvc.init(config, this.http);
1773
2174
  }
2175
+ const approvalEvent = toSignal(this.mesAuth.approvalEvents$);
2176
+ effect(() => {
2177
+ approvalEvent(); // track SignalR approval events
2178
+ if (this.isOpen())
2179
+ this.loadCurrentTab();
2180
+ });
2181
+ // Close when the user clicks outside the panel (mirrors the old backdrop UX
2182
+ // now that the backdrop has been removed).
2183
+ effect((onCleanup) => {
2184
+ if (!this.isOpen())
2185
+ return;
2186
+ const onDocClick = (ev) => {
2187
+ const panel = this.host.nativeElement.querySelector('.approval-panel');
2188
+ if (panel && !panel.contains(ev.target))
2189
+ this.close();
2190
+ };
2191
+ // Defer to skip the same click that opened the panel.
2192
+ const tid = setTimeout(() => document.addEventListener('mousedown', onDocClick), 0);
2193
+ onCleanup(() => {
2194
+ clearTimeout(tid);
2195
+ document.removeEventListener('mousedown', onDocClick);
2196
+ });
2197
+ });
1774
2198
  }
1775
- async handleEvent(ev) {
1776
- switch (ev.type) {
1777
- case 'session':
1778
- this.sessionId = ev.sessionId;
1779
- break;
1780
- case 'token':
1781
- this.appendToken(ev.text);
1782
- break;
1783
- case 'tool_call_started':
1784
- this.appendToolEvent({
1785
- id: ev.id,
1786
- name: ev.name,
1787
- side: ev.side,
1788
- status: 'running',
1789
- args: ev.args,
1790
- readOnly: ev.readOnly
1791
- });
1792
- break;
1793
- case 'tool_call_completed':
1794
- this.markToolStatus(ev.id, ev.ok ? 'ok' : 'error', ev.summary);
1795
- break;
1796
- case 'client_tool_call':
1797
- this.appendToolEvent({
1798
- id: ev.id,
1799
- name: ev.name,
1800
- side: 'client',
1801
- status: ev.readOnly ? 'running' : 'awaiting-approval',
1802
- args: ev.args,
1803
- readOnly: ev.readOnly
1804
- });
1805
- if (ev.readOnly || this.alwaysApproved.has(this.verbOf(ev.name))) {
1806
- // Auto-run; the next /ai/chat call will deliver the result.
1807
- await this.executeClientTool(ev.id, ev.name, ev.args);
1808
- }
1809
- else {
1810
- this.pendingApproval.set({ callId: ev.id, name: ev.name, args: ev.args, side: 'client' });
1811
- }
1812
- break;
1813
- case 'error':
1814
- this.lastError.set(ev.message);
1815
- this.finalizeAssistant(`[error: ${ev.message}]`);
1816
- break;
1817
- case 'done':
1818
- this.finalizeAssistant();
1819
- break;
1820
- }
2199
+ open() {
2200
+ this.isOpen.set(true);
2201
+ this.loadAllTabs();
1821
2202
  }
1822
- async executeClientTool(callId, name, args) {
1823
- this.markToolStatus(callId, 'running');
1824
- const { ok, result } = await this.tools.runTool(name, args);
1825
- this.markToolStatus(callId, ok ? 'ok' : 'error', this.summarize(result));
1826
- await this.continueWithToolResult(callId, ok, result);
2203
+ close() {
2204
+ this.isOpen.set(false);
1827
2205
  }
1828
- async continueWithToolResult(callId, ok, result) {
1829
- // Open a new SSE stream that delivers the tool result.
1830
- await this.runStream({ toolResult: { callId, ok, result } });
2206
+ toggle() {
2207
+ if (this.isOpen())
2208
+ this.close();
2209
+ else
2210
+ this.open();
1831
2211
  }
1832
- // ── bubble helpers ────────────────────────────────────────────────────────
1833
- appendBubble(b) {
1834
- this.messages.update(arr => [...arr, b]);
2212
+ switchTab(tab) {
2213
+ this.activeTab.set(tab);
2214
+ this.loadCurrentTab();
1835
2215
  }
1836
- appendToken(text) {
1837
- if (!this.currentAssistantId) {
1838
- const id = this.uid();
1839
- this.currentAssistantId = id;
1840
- this.appendBubble({ id, role: 'assistant', text, pending: true, toolEvents: [] });
2216
+ loadAllTabs() {
2217
+ this.loading.set(true);
2218
+ if (!this.approvalSvc) {
2219
+ this.loading.set(false);
1841
2220
  return;
1842
2221
  }
1843
- const id = this.currentAssistantId;
1844
- this.messages.update(arr => arr.map(m => m.id === id ? { ...m, text: m.text + text } : m));
2222
+ let pending = 3;
2223
+ const done = () => { if (--pending === 0)
2224
+ this.loading.set(false); };
2225
+ this.approvalSvc.getPendingApprovals(1, 100).subscribe({
2226
+ next: r => { this.processingItems.set(r.items); done(); },
2227
+ error: () => done()
2228
+ });
2229
+ this.approvalSvc.getMyRequests(1, 10, ApprovalDocumentStatus.Approved).subscribe({
2230
+ next: r => { this.approvedItems.set(r.items); done(); },
2231
+ error: () => done()
2232
+ });
2233
+ this.approvalSvc.getMyRequests(1, 10, ApprovalDocumentStatus.Rejected).subscribe({
2234
+ next: r => { this.rejectedItems.set(r.items); done(); },
2235
+ error: () => done()
2236
+ });
1845
2237
  }
1846
- appendToolEvent(ev) {
1847
- if (!this.currentAssistantId) {
1848
- const id = this.uid();
1849
- this.currentAssistantId = id;
1850
- this.appendBubble({ id, role: 'assistant', text: '', pending: true, toolEvents: [ev] });
2238
+ loadCurrentTab() {
2239
+ if (!this.approvalSvc)
1851
2240
  return;
2241
+ if (this.activeTab() === 'processing') {
2242
+ this.approvalSvc.getPendingApprovals(1, 100).subscribe({
2243
+ next: r => this.processingItems.set(r.items),
2244
+ error: () => { }
2245
+ });
2246
+ }
2247
+ else if (this.activeTab() === 'approved') {
2248
+ this.approvalSvc.getMyRequests(1, 10, ApprovalDocumentStatus.Approved).subscribe({
2249
+ next: r => this.approvedItems.set(r.items),
2250
+ error: () => { }
2251
+ });
2252
+ }
2253
+ else {
2254
+ this.approvalSvc.getMyRequests(1, 10, ApprovalDocumentStatus.Rejected).subscribe({
2255
+ next: r => this.rejectedItems.set(r.items),
2256
+ error: () => { }
2257
+ });
1852
2258
  }
1853
- const id = this.currentAssistantId;
1854
- this.messages.update(arr => arr.map(m => m.id === id
1855
- ? { ...m, toolEvents: [...(m.toolEvents ?? []), ev] }
1856
- : m));
1857
- }
1858
- markToolStatus(callId, status, summary) {
1859
- this.messages.update(arr => arr.map(m => ({
1860
- ...m,
1861
- toolEvents: m.toolEvents?.map(t => t.id === callId ? { ...t, status, summary: summary ?? t.summary } : t)
1862
- })));
1863
- }
1864
- finalizeAssistant(extra) {
1865
- const id = this.currentAssistantId;
1866
- if (!id)
1867
- return;
1868
- this.messages.update(arr => arr.map(m => m.id === id
1869
- ? { ...m, pending: false, text: extra ? (m.text + (m.text ? '\n' : '') + extra) : m.text }
1870
- : m));
1871
- this.currentAssistantId = null;
1872
- }
1873
- verbOf(name) {
1874
- return name.split('_')[0] ?? name;
1875
2259
  }
1876
- summarize(s) {
1877
- const flat = s.replace(/\s+/g, ' ').trim();
1878
- return flat.length <= 80 ? flat : flat.slice(0, 80) + '…';
2260
+ navigateToDetail(id) {
2261
+ this.close();
2262
+ this.router.navigate(['/auth/approval/detail', id]);
2263
+ this.approvalActioned.emit();
1879
2264
  }
1880
- uid() {
1881
- return Math.random().toString(36).slice(2) + Date.now().toString(36);
2265
+ showMore(status) {
2266
+ this.close();
2267
+ this.router.navigate(['/auth/approval/my-requests'], { queryParams: { status } });
1882
2268
  }
1883
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1884
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiService, providedIn: 'root' });
2269
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaApprovalPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2270
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: MaApprovalPanelComponent, isStandalone: true, selector: "ma-approval-panel", outputs: { approvalActioned: "approvalActioned" }, ngImport: i0, template: "<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-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:var(--ma-ai-panel-width, 0px)}.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"], dependencies: [{ kind: "pipe", type: DatePipe, name: "date" }] });
1885
2271
  }
1886
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiService, decorators: [{
1887
- type: Injectable,
1888
- args: [{ providedIn: 'root' }]
1889
- }] });
2272
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaApprovalPanelComponent, decorators: [{
2273
+ type: Component,
2274
+ args: [{ selector: 'ma-approval-panel', imports: [DatePipe], template: "<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-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:var(--ma-ai-panel-width, 0px)}.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"] }]
2275
+ }], ctorParameters: () => [], propDecorators: { approvalActioned: [{ type: i0.Output, args: ["approvalActioned"] }] } });
1890
2276
 
1891
2277
  /**
1892
2278
  * Tiny hand-rolled GitHub-flavoured Markdown renderer for AI chat bubbles.
@@ -2064,6 +2450,153 @@ function sanitizeUrl(url) {
2064
2450
  return null;
2065
2451
  }
2066
2452
 
2453
+ /**
2454
+ * Drop-down list of the signed-in user's recent AI conversations.
2455
+ * Lives inside the chat panel header (toggled by the History button).
2456
+ */
2457
+ class MaAiHistoryListComponent {
2458
+ ai = inject(MaAiService);
2459
+ conversations = signal([], ...(ngDevMode ? [{ debugName: "conversations" }] : /* istanbul ignore next */ []));
2460
+ loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
2461
+ /** Emitted after the user picks a row and the panel finishes hydrating. */
2462
+ resumed = output();
2463
+ ngOnInit() {
2464
+ this.refresh();
2465
+ }
2466
+ async refresh() {
2467
+ this.loading.set(true);
2468
+ try {
2469
+ const list = await this.ai.listConversations(50);
2470
+ this.conversations.set(list ?? []);
2471
+ }
2472
+ finally {
2473
+ this.loading.set(false);
2474
+ }
2475
+ }
2476
+ async pick(c) {
2477
+ const ok = await this.ai.resumeConversation(c.id);
2478
+ if (ok)
2479
+ this.resumed.emit(c.id);
2480
+ }
2481
+ async remove(c) {
2482
+ const ok = await this.ai.deleteConversation(c.id);
2483
+ if (ok) {
2484
+ this.conversations.update(arr => arr.filter(x => x.id !== c.id));
2485
+ }
2486
+ }
2487
+ relative(iso) {
2488
+ const t = Date.parse(iso);
2489
+ if (!isFinite(t))
2490
+ return '';
2491
+ const ms = Date.now() - t;
2492
+ const m = Math.floor(ms / 60_000);
2493
+ if (m < 1)
2494
+ return 'just now';
2495
+ if (m < 60)
2496
+ return `${m}m ago`;
2497
+ const h = Math.floor(m / 60);
2498
+ if (h < 24)
2499
+ return `${h}h ago`;
2500
+ const d = Math.floor(h / 24);
2501
+ if (d < 7)
2502
+ return `${d}d ago`;
2503
+ try {
2504
+ return new Date(t).toLocaleDateString();
2505
+ }
2506
+ catch {
2507
+ return '';
2508
+ }
2509
+ }
2510
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiHistoryListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2511
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: MaAiHistoryListComponent, isStandalone: true, selector: "ma-ai-history-list", outputs: { resumed: "resumed" }, ngImport: i0, template: `
2512
+ <div class="history-root">
2513
+ <div class="history-head">
2514
+ <span class="history-title">Recent conversations</span>
2515
+ <button class="history-refresh" (click)="refresh()" title="Refresh" aria-label="Refresh">
2516
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24"
2517
+ fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2518
+ <path d="M21 12a9 9 0 1 1-3-6.7L21 8"/><path d="M21 3v5h-5"/>
2519
+ </svg>
2520
+ </button>
2521
+ </div>
2522
+
2523
+ @if (loading()) {
2524
+ <div class="history-empty">Loading…</div>
2525
+ } @else if (conversations().length === 0) {
2526
+ <div class="history-empty">No conversations yet.</div>
2527
+ } @else {
2528
+ <ul class="history-list">
2529
+ @for (c of conversations(); track c.id) {
2530
+ <li class="history-row">
2531
+ <button class="history-pick" (click)="pick(c)" [title]="c.title ?? '(untitled)'">
2532
+ <span class="history-row-title">{{ c.title || '(untitled)' }}</span>
2533
+ <span class="history-row-meta">
2534
+ <span>{{ relative(c.updatedAt) }}</span>
2535
+ @if (c.appName) { <span>· {{ c.appName }}</span> }
2536
+ <span>· {{ c.messageCount }} msg</span>
2537
+ </span>
2538
+ </button>
2539
+ <button class="history-del" (click)="remove(c)" title="Delete" aria-label="Delete">
2540
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24"
2541
+ fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2542
+ <polyline points="3 6 5 6 21 6"/>
2543
+ <path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
2544
+ <path d="M10 11v6"/><path d="M14 11v6"/>
2545
+ </svg>
2546
+ </button>
2547
+ </li>
2548
+ }
2549
+ </ul>
2550
+ }
2551
+ </div>
2552
+ `, isInline: true, styles: [":host{display:block}.history-root{border-bottom:1px solid var(--border-color, #383850);background:var(--bg-secondary, #27273a);max-height:280px;overflow-y:auto}.history-head{display:flex;align-items:center;justify-content:space-between;padding:8px 14px;font-size:12px;color:var(--text-muted, #9e9e9e);text-transform:uppercase;letter-spacing:.04em}.history-refresh{background:none;border:none;cursor:pointer;color:var(--text-muted, #9e9e9e);padding:4px;border-radius:4px;display:flex;align-items:center;justify-content:center}.history-refresh:hover{color:var(--text-primary, #e0e0e0);background:var(--bg-hover, #2a2d4a)}.history-empty{padding:12px 14px;font-size:12.5px;color:var(--text-muted, #9e9e9e)}.history-list{list-style:none;margin:0;padding:0}.history-row{display:flex;align-items:stretch;border-top:1px solid var(--border-color, #383850)}.history-pick{flex:1;background:none;border:none;cursor:pointer;text-align:left;padding:8px 14px;color:var(--text-primary, #e0e0e0);display:flex;flex-direction:column;gap:2px;min-width:0}.history-pick:hover{background:var(--bg-hover, #2a2d4a)}.history-row-title{font-size:13px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.history-row-meta{font-size:11px;color:var(--text-muted, #9e9e9e);display:flex;gap:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.history-del{background:none;border:none;cursor:pointer;color:var(--text-muted, #9e9e9e);padding:0 12px;display:flex;align-items:center;justify-content:center}.history-del:hover{color:var(--error, #ef5350);background:var(--bg-hover, #2a2d4a)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2553
+ }
2554
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiHistoryListComponent, decorators: [{
2555
+ type: Component,
2556
+ args: [{ selector: 'ma-ai-history-list', changeDetection: ChangeDetectionStrategy.OnPush, template: `
2557
+ <div class="history-root">
2558
+ <div class="history-head">
2559
+ <span class="history-title">Recent conversations</span>
2560
+ <button class="history-refresh" (click)="refresh()" title="Refresh" aria-label="Refresh">
2561
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24"
2562
+ fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2563
+ <path d="M21 12a9 9 0 1 1-3-6.7L21 8"/><path d="M21 3v5h-5"/>
2564
+ </svg>
2565
+ </button>
2566
+ </div>
2567
+
2568
+ @if (loading()) {
2569
+ <div class="history-empty">Loading…</div>
2570
+ } @else if (conversations().length === 0) {
2571
+ <div class="history-empty">No conversations yet.</div>
2572
+ } @else {
2573
+ <ul class="history-list">
2574
+ @for (c of conversations(); track c.id) {
2575
+ <li class="history-row">
2576
+ <button class="history-pick" (click)="pick(c)" [title]="c.title ?? '(untitled)'">
2577
+ <span class="history-row-title">{{ c.title || '(untitled)' }}</span>
2578
+ <span class="history-row-meta">
2579
+ <span>{{ relative(c.updatedAt) }}</span>
2580
+ @if (c.appName) { <span>· {{ c.appName }}</span> }
2581
+ <span>· {{ c.messageCount }} msg</span>
2582
+ </span>
2583
+ </button>
2584
+ <button class="history-del" (click)="remove(c)" title="Delete" aria-label="Delete">
2585
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24"
2586
+ fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2587
+ <polyline points="3 6 5 6 21 6"/>
2588
+ <path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
2589
+ <path d="M10 11v6"/><path d="M14 11v6"/>
2590
+ </svg>
2591
+ </button>
2592
+ </li>
2593
+ }
2594
+ </ul>
2595
+ }
2596
+ </div>
2597
+ `, styles: [":host{display:block}.history-root{border-bottom:1px solid var(--border-color, #383850);background:var(--bg-secondary, #27273a);max-height:280px;overflow-y:auto}.history-head{display:flex;align-items:center;justify-content:space-between;padding:8px 14px;font-size:12px;color:var(--text-muted, #9e9e9e);text-transform:uppercase;letter-spacing:.04em}.history-refresh{background:none;border:none;cursor:pointer;color:var(--text-muted, #9e9e9e);padding:4px;border-radius:4px;display:flex;align-items:center;justify-content:center}.history-refresh:hover{color:var(--text-primary, #e0e0e0);background:var(--bg-hover, #2a2d4a)}.history-empty{padding:12px 14px;font-size:12.5px;color:var(--text-muted, #9e9e9e)}.history-list{list-style:none;margin:0;padding:0}.history-row{display:flex;align-items:stretch;border-top:1px solid var(--border-color, #383850)}.history-pick{flex:1;background:none;border:none;cursor:pointer;text-align:left;padding:8px 14px;color:var(--text-primary, #e0e0e0);display:flex;flex-direction:column;gap:2px;min-width:0}.history-pick:hover{background:var(--bg-hover, #2a2d4a)}.history-row-title{font-size:13px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.history-row-meta{font-size:11px;color:var(--text-muted, #9e9e9e);display:flex;gap:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.history-del{background:none;border:none;cursor:pointer;color:var(--text-muted, #9e9e9e);padding:0 12px;display:flex;align-items:center;justify-content:center}.history-del:hover{color:var(--error, #ef5350);background:var(--bg-hover, #2a2d4a)}\n"] }]
2598
+ }], propDecorators: { resumed: [{ type: i0.Output, args: ["resumed"] }] } });
2599
+
2067
2600
  const STORAGE_KEY = 'ma-ai-panel-width';
2068
2601
  const MIN_WIDTH = 320;
2069
2602
  const MAX_WIDTH = 800;
@@ -2107,6 +2640,7 @@ class MaAiChatPanelComponent {
2107
2640
  isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
2108
2641
  width = signal(this.loadWidth(), ...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
2109
2642
  draft = signal('', ...(ngDevMode ? [{ debugName: "draft" }] : /* istanbul ignore next */ []));
2643
+ historyOpen = signal(false, ...(ngDevMode ? [{ debugName: "historyOpen" }] : /* istanbul ignore next */ []));
2110
2644
  ai = inject(MaAiService);
2111
2645
  router = inject(Router, { optional: true });
2112
2646
  themeService = inject(ThemeService);
@@ -2182,6 +2716,14 @@ class MaAiChatPanelComponent {
2182
2716
  }
2183
2717
  newConversation() {
2184
2718
  this.ai.resetSession();
2719
+ this.historyOpen.set(false);
2720
+ }
2721
+ toggleHistory() {
2722
+ this.historyOpen.update(v => !v);
2723
+ }
2724
+ onHistoryResumed(_id) {
2725
+ this.historyOpen.set(false);
2726
+ queueMicrotask(() => this.textArea()?.nativeElement.focus());
2185
2727
  }
2186
2728
  send() {
2187
2729
  const text = this.draft().trim();
@@ -2256,11 +2798,11 @@ class MaAiChatPanelComponent {
2256
2798
  el.scrollTop = el.scrollHeight;
2257
2799
  }
2258
2800
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiChatPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2259
- 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\n <!-- Footer with hints (lifts the composer off the absolute bottom of the viewport) -->\n <div class=\"ai-footer\">\n <span class=\"hint\">\n <kbd>Enter</kbd> send \u00B7 <kbd>Shift</kbd>+<kbd>Enter</kbd> new line\n </span>\n <span class=\"hint dim\">AI can make mistakes &mdash; verify important info</span>\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)}.ai-footer{flex-shrink:0;display:flex;flex-direction:column;align-items:center;gap:2px;padding:6px 12px 10px;background:var(--bg-secondary);border-top:1px solid var(--border-color);font-size:11px;color:var(--text-secondary);line-height:1.4;text-align:center}.ai-footer .hint{display:inline-block}.ai-footer .hint.dim{color:var(--text-muted);font-size:10.5px}.ai-footer kbd{display:inline-block;padding:0 5px;margin:0 1px;border:1px solid var(--border-color);border-bottom-width:2px;border-radius:4px;background:var(--bg-primary);color:var(--text-primary);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10px;line-height:1.4}.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" }] });
2801
+ 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=\"M12 5v14\"/><path d=\"M5 12h14\"/>\n </svg>\n </button>\n <button class=\"icon-btn\" (click)=\"toggleHistory()\" [class.active]=\"historyOpen()\" title=\"History\" aria-label=\"History\">\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 <circle cx=\"12\" cy=\"12\" r=\"10\"/><polyline points=\"12 6 12 12 16 14\"/>\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 @if (historyOpen()) {\n <ma-ai-history-list (resumed)=\"onHistoryResumed($event)\"></ma-ai-history-list>\n }\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\n <!-- Footer with hints (lifts the composer off the absolute bottom of the viewport) -->\n <div class=\"ai-footer\">\n <span class=\"hint\">\n <kbd>Enter</kbd> send \u00B7 <kbd>Shift</kbd>+<kbd>Enter</kbd> new line\n </span>\n <span class=\"hint dim\">AI can make mistakes &mdash; verify important info</span>\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)}.ai-footer{flex-shrink:0;display:flex;flex-direction:column;align-items:center;gap:2px;padding:6px 12px 10px;background:var(--bg-secondary);border-top:1px solid var(--border-color);font-size:11px;color:var(--text-secondary);line-height:1.4;text-align:center}.ai-footer .hint{display:inline-block}.ai-footer .hint.dim{color:var(--text-muted);font-size:10.5px}.ai-footer kbd{display:inline-block;padding:0 5px;margin:0 1px;border:1px solid var(--border-color);border-bottom-width:2px;border-radius:4px;background:var(--bg-primary);color:var(--text-primary);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10px;line-height:1.4}.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: "component", type: MaAiHistoryListComponent, selector: "ma-ai-history-list", outputs: ["resumed"] }, { kind: "pipe", type: JsonPipe, name: "json" }, { kind: "pipe", type: MaAiMarkdownPipe, name: "maAiMarkdown" }] });
2260
2802
  }
2261
2803
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiChatPanelComponent, decorators: [{
2262
2804
  type: Component,
2263
- 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\n <!-- Footer with hints (lifts the composer off the absolute bottom of the viewport) -->\n <div class=\"ai-footer\">\n <span class=\"hint\">\n <kbd>Enter</kbd> send \u00B7 <kbd>Shift</kbd>+<kbd>Enter</kbd> new line\n </span>\n <span class=\"hint dim\">AI can make mistakes &mdash; verify important info</span>\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)}.ai-footer{flex-shrink:0;display:flex;flex-direction:column;align-items:center;gap:2px;padding:6px 12px 10px;background:var(--bg-secondary);border-top:1px solid var(--border-color);font-size:11px;color:var(--text-secondary);line-height:1.4;text-align:center}.ai-footer .hint{display:inline-block}.ai-footer .hint.dim{color:var(--text-muted);font-size:10.5px}.ai-footer kbd{display:inline-block;padding:0 5px;margin:0 1px;border:1px solid var(--border-color);border-bottom-width:2px;border-radius:4px;background:var(--bg-primary);color:var(--text-primary);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10px;line-height:1.4}.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"] }]
2805
+ args: [{ selector: 'ma-ai-chat-panel', imports: [JsonPipe, MaAiMarkdownPipe, MaAiHistoryListComponent], 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=\"M12 5v14\"/><path d=\"M5 12h14\"/>\n </svg>\n </button>\n <button class=\"icon-btn\" (click)=\"toggleHistory()\" [class.active]=\"historyOpen()\" title=\"History\" aria-label=\"History\">\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 <circle cx=\"12\" cy=\"12\" r=\"10\"/><polyline points=\"12 6 12 12 16 14\"/>\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 @if (historyOpen()) {\n <ma-ai-history-list (resumed)=\"onHistoryResumed($event)\"></ma-ai-history-list>\n }\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\n <!-- Footer with hints (lifts the composer off the absolute bottom of the viewport) -->\n <div class=\"ai-footer\">\n <span class=\"hint\">\n <kbd>Enter</kbd> send \u00B7 <kbd>Shift</kbd>+<kbd>Enter</kbd> new line\n </span>\n <span class=\"hint dim\">AI can make mistakes &mdash; verify important info</span>\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)}.ai-footer{flex-shrink:0;display:flex;flex-direction:column;align-items:center;gap:2px;padding:6px 12px 10px;background:var(--bg-secondary);border-top:1px solid var(--border-color);font-size:11px;color:var(--text-secondary);line-height:1.4;text-align:center}.ai-footer .hint{display:inline-block}.ai-footer .hint.dim{color:var(--text-muted);font-size:10.5px}.ai-footer kbd{display:inline-block;padding:0 5px;margin:0 1px;border:1px solid var(--border-color);border-bottom-width:2px;border-radius:4px;background:var(--bg-primary);color:var(--text-primary);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10px;line-height:1.4}.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"] }]
2264
2806
  }], ctorParameters: () => [], propDecorators: { scrollContainer: [{ type: i0.ViewChild, args: ['scrollContainer', { isSignal: true }] }], textArea: [{ type: i0.ViewChild, args: ['textArea', { isSignal: true }] }], themeClass: [{
2265
2807
  type: HostBinding,
2266
2808
  args: ['class']
@@ -3345,5 +3887,5 @@ async function runReturnViaPostMessageIfRequested(authService) {
3345
3887
  * Generated bundle index. Do not edit.
3346
3888
  */
3347
3889
 
3348
- 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 };
3890
+ export { ALL_ACTIONS, AVATAR_FRAMES, ApprovalActionType, ApprovalDocumentStatus, ApprovalStepMode, ApprovalStepStatus, DEFAULT_AI_CONFIG, MES_AUTH_AI_CONFIG, MES_AUTH_CONFIG, MaAiButtonComponent, MaAiChatPanelComponent, MaAiHistoryListComponent, MaAiLessonsEditorComponent, 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 };
3349
3891
  //# sourceMappingURL=mesauth-angular.mjs.map