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.
- package/fesm2022/mesauth-angular.mjs +1600 -1058
- package/fesm2022/mesauth-angular.mjs.map +1 -1
- package/package.json +1 -1
- package/types/mesauth-angular.d.ts +140 -7
|
@@ -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,
|
|
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,
|
|
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.
|
|
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
|
-
//
|
|
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
|
|
453
|
-
*
|
|
454
|
-
*
|
|
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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
-
|
|
490
|
-
|
|
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
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
//
|
|
517
|
-
|
|
518
|
-
if (
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
-
|
|
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
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
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
|
-
|
|
1053
|
-
|
|
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
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
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
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
}
|
|
1098
|
-
|
|
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
|
-
|
|
1102
|
-
|
|
1103
|
-
this.
|
|
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
|
-
|
|
1106
|
-
|
|
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
|
-
|
|
1109
|
-
|
|
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
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
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
|
-
|
|
1127
|
-
|
|
1128
|
-
this.
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
const
|
|
1133
|
-
|
|
1134
|
-
|
|
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
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
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
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
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
|
-
|
|
1177
|
-
|
|
1178
|
-
const
|
|
1179
|
-
this.
|
|
1180
|
-
|
|
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
|
-
|
|
1189
|
-
|
|
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
|
-
//
|
|
1192
|
-
|
|
1193
|
-
|
|
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
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
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.
|
|
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
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1232
|
-
|
|
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
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
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
|
-
|
|
1271
|
-
return
|
|
1206
|
+
verbOf(name) {
|
|
1207
|
+
return name.split('_')[0] ?? name;
|
|
1272
1208
|
}
|
|
1273
|
-
|
|
1274
|
-
|
|
1209
|
+
summarize(s) {
|
|
1210
|
+
const flat = s.replace(/\s+/g, ' ').trim();
|
|
1211
|
+
return flat.length <= 80 ? flat : flat.slice(0, 80) + '…';
|
|
1275
1212
|
}
|
|
1276
|
-
|
|
1277
|
-
return
|
|
1213
|
+
uid() {
|
|
1214
|
+
return Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
1278
1215
|
}
|
|
1279
|
-
//
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
return this.
|
|
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
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
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
|
-
|
|
1289
|
-
|
|
1290
|
-
return this.
|
|
1234
|
+
/** DELETE /ai/conversations/{id} */
|
|
1235
|
+
async deleteConversation(id) {
|
|
1236
|
+
return this.apiSend('DELETE', `/ai/conversations/${encodeURIComponent(id)}`);
|
|
1291
1237
|
}
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
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
|
-
|
|
1299
|
-
|
|
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
|
-
|
|
1306
|
-
return this.
|
|
1245
|
+
async updateLesson(id, patch) {
|
|
1246
|
+
return this.apiSend('PUT', `/ai/lessons/${encodeURIComponent(id)}`, patch);
|
|
1307
1247
|
}
|
|
1308
|
-
|
|
1309
|
-
return this.
|
|
1248
|
+
async deleteLesson(id) {
|
|
1249
|
+
return this.apiSend('DELETE', `/ai/lessons/${encodeURIComponent(id)}`);
|
|
1310
1250
|
}
|
|
1311
|
-
|
|
1312
|
-
|
|
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
|
-
|
|
1315
|
-
|
|
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
|
-
|
|
1318
|
-
|
|
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
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
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:
|
|
1325
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type:
|
|
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:
|
|
1328
|
-
type: Injectable
|
|
1329
|
-
|
|
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
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
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
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
this.
|
|
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
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1384
|
+
onBackdropClick(ev) {
|
|
1385
|
+
if (ev.target === ev.currentTarget)
|
|
1386
|
+
this.cancel.emit();
|
|
1415
1387
|
}
|
|
1416
|
-
|
|
1417
|
-
this.
|
|
1388
|
+
onDraftInput(ev) {
|
|
1389
|
+
this.draft.set(ev.target.value);
|
|
1418
1390
|
}
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
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
|
-
|
|
1426
|
-
this.
|
|
1427
|
-
this.loadCurrentTab();
|
|
1410
|
+
onTextInput(lesson, ev) {
|
|
1411
|
+
this.pendingText.set(lesson.id, ev.target.value);
|
|
1428
1412
|
}
|
|
1429
|
-
|
|
1430
|
-
this.
|
|
1431
|
-
|
|
1432
|
-
|
|
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
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
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
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1643
|
+
effect(() => {
|
|
1644
|
+
approvalEvent(); // track SignalR approval events
|
|
1645
|
+
if (currentUserSig())
|
|
1646
|
+
this.loadPendingApprovalCount();
|
|
1445
1647
|
});
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1648
|
+
effect(() => {
|
|
1649
|
+
notification(); // track SignalR notification events
|
|
1650
|
+
if (currentUserSig())
|
|
1651
|
+
this.loadUnreadCount();
|
|
1449
1652
|
});
|
|
1450
1653
|
}
|
|
1451
|
-
|
|
1452
|
-
|
|
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
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
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
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
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
|
-
|
|
1468
|
-
|
|
1469
|
-
error: () => { }
|
|
1470
|
-
});
|
|
1736
|
+
// Optional: redirect in same tab
|
|
1737
|
+
window.location.href = destinationUrl;
|
|
1471
1738
|
}
|
|
1472
1739
|
}
|
|
1473
|
-
|
|
1474
|
-
this.
|
|
1475
|
-
|
|
1476
|
-
|
|
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
|
-
|
|
1479
|
-
this.
|
|
1480
|
-
this.router.navigate(['/auth/approval/my-requests'], { queryParams: { status } });
|
|
1756
|
+
onNotificationClick() {
|
|
1757
|
+
this.notificationClick.emit();
|
|
1481
1758
|
}
|
|
1482
|
-
|
|
1483
|
-
|
|
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:
|
|
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 →</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 →</span>\n </div>\n </div>\n }\n @if (approvedItems().length >= 10) {\n <div class=\"show-more\" (click)=\"showMore('approved')\">Show more →</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 →</span>\n </div>\n </div>\n }\n @if (rejectedItems().length >= 10) {\n <div class=\"show-more\" (click)=\"showMore('rejected')\">Show more →</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: {
|
|
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
|
-
|
|
1492
|
-
|
|
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
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
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:
|
|
1609
|
-
static
|
|
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:
|
|
1612
|
-
type:
|
|
1613
|
-
args: [{
|
|
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
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
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
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
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
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
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
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
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
|
-
|
|
1670
|
-
|
|
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
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
this.
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
this.
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
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
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
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
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
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
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
// SSE events are separated by a blank line.
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
buffer = buffer.slice(idx + 2);
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
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
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
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
|
-
|
|
1823
|
-
this.
|
|
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
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
2206
|
+
toggle() {
|
|
2207
|
+
if (this.isOpen())
|
|
2208
|
+
this.close();
|
|
2209
|
+
else
|
|
2210
|
+
this.open();
|
|
1831
2211
|
}
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
this.
|
|
2212
|
+
switchTab(tab) {
|
|
2213
|
+
this.activeTab.set(tab);
|
|
2214
|
+
this.loadCurrentTab();
|
|
1835
2215
|
}
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
this.
|
|
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
|
-
|
|
1844
|
-
|
|
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
|
-
|
|
1847
|
-
if (!this.
|
|
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
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
2260
|
+
navigateToDetail(id) {
|
|
2261
|
+
this.close();
|
|
2262
|
+
this.router.navigate(['/auth/approval/detail', id]);
|
|
2263
|
+
this.approvalActioned.emit();
|
|
1879
2264
|
}
|
|
1880
|
-
|
|
1881
|
-
|
|
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:
|
|
1884
|
-
static
|
|
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 →</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 →</span>\n </div>\n </div>\n }\n @if (approvedItems().length >= 10) {\n <div class=\"show-more\" (click)=\"showMore('approved')\">Show more →</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 →</span>\n </div>\n </div>\n }\n @if (rejectedItems().length >= 10) {\n <div class=\"show-more\" (click)=\"showMore('rejected')\">Show more →</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:
|
|
1887
|
-
type:
|
|
1888
|
-
args: [{
|
|
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 →</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 →</span>\n </div>\n </div>\n }\n @if (approvedItems().length >= 10) {\n <div class=\"show-more\" (click)=\"showMore('approved')\">Show more →</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 →</span>\n </div>\n </div>\n }\n @if (rejectedItems().length >= 10) {\n <div class=\"show-more\" (click)=\"showMore('rejected')\">Show more →</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 — 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 — 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 — 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 — 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
|