mesauth-angular 1.21.0 → 1.23.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/README.md +5 -0
- package/fesm2022/mesauth-angular.mjs +1585 -979
- package/fesm2022/mesauth-angular.mjs.map +1 -1
- package/package.json +1 -1
- package/types/mesauth-angular.d.ts +148 -5
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { signal, Injectable, InjectionToken, makeEnvironmentProviders, provideAppInitializer, inject, NgModule, afterNextRender, input, booleanAttribute, computed, HostBinding, Component, output, effect, HostListener,
|
|
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
4
|
import { catchError, of, Subject, EMPTY, timer, throwError, firstValueFrom, distinctUntilChanged, switchMap as switchMap$1, forkJoin } from 'rxjs';
|
|
5
5
|
import { HttpClient, HttpResponse } from '@angular/common/http';
|
|
@@ -10,7 +10,7 @@ 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.23.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.
|
|
@@ -704,7 +704,8 @@ const DEFAULT_AI_CONFIG = {
|
|
|
704
704
|
enabled: true,
|
|
705
705
|
tools: [],
|
|
706
706
|
systemPromptExtensions: [],
|
|
707
|
-
appName: undefined
|
|
707
|
+
appName: undefined,
|
|
708
|
+
panelMode: 'static'
|
|
708
709
|
};
|
|
709
710
|
/**
|
|
710
711
|
* Provide AI assistant configuration to mesauth-angular.
|
|
@@ -771,201 +772,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImpo
|
|
|
771
772
|
`, styles: [":host{display:inline-flex}.ai-btn{background:none;border:none;cursor:pointer;color:var(--cui-body-color, #6e6e9a);width:36px;height:36px;border-radius:8px;display:inline-flex;align-items:center;justify-content:center;transition:background .15s,color .15s}.ai-btn:hover{background:var(--cui-tertiary-bg, rgba(120,120,200,.12));color:#7c3aed}\n"] }]
|
|
772
773
|
}], propDecorators: { clicked: [{ type: i0.Output, args: ["clicked"] }] } });
|
|
773
774
|
|
|
774
|
-
class UserProfileComponent {
|
|
775
|
-
inputAvatarShape = input('circle', ...(ngDevMode ? [{ debugName: "inputAvatarShape" }] : /* istanbul ignore next */ []));
|
|
776
|
-
showBell = input(true, ...(ngDevMode ? [{ debugName: "showBell" }] : /* istanbul ignore next */ []));
|
|
777
|
-
showApproval = input(true, ...(ngDevMode ? [{ debugName: "showApproval" }] : /* istanbul ignore next */ []));
|
|
778
|
-
showName = input(false, ...(ngDevMode ? [{ debugName: "showName" }] : /* istanbul ignore next */ []));
|
|
779
|
-
showAi = input(true, ...(ngDevMode ? [{ debugName: "showAi" }] : /* istanbul ignore next */ []));
|
|
780
|
-
showSignature = input([false, false], ...(ngDevMode ? [{ debugName: "showSignature" }] : /* istanbul ignore next */ []));
|
|
781
|
-
signatureHeight = input(40, ...(ngDevMode ? [{ debugName: "signatureHeight" }] : /* istanbul ignore next */ []));
|
|
782
|
-
notificationClick = output();
|
|
783
|
-
approvalClick = output();
|
|
784
|
-
aiClick = output();
|
|
785
|
-
get themeClass() {
|
|
786
|
-
return `theme-${this.themeService.currentTheme()}`;
|
|
787
|
-
}
|
|
788
|
-
currentUser = signal(null, ...(ngDevMode ? [{ debugName: "currentUser" }] : /* istanbul ignore next */ []));
|
|
789
|
-
unreadCount = signal(0, ...(ngDevMode ? [{ debugName: "unreadCount" }] : /* istanbul ignore next */ []));
|
|
790
|
-
pendingApprovalCount = signal(0, ...(ngDevMode ? [{ debugName: "pendingApprovalCount" }] : /* istanbul ignore next */ []));
|
|
791
|
-
dropdownOpen = signal(false, ...(ngDevMode ? [{ debugName: "dropdownOpen" }] : /* istanbul ignore next */ []));
|
|
792
|
-
avatarRefresh = signal(Date.now(), ...(ngDevMode ? [{ debugName: "avatarRefresh" }] : /* istanbul ignore next */ []));
|
|
793
|
-
// Avatar style derived from per-user preferences
|
|
794
|
-
navAvatarSize = computed(() => this.currentUser()?.avatarSize ?? 'md', ...(ngDevMode ? [{ debugName: "navAvatarSize" }] : /* istanbul ignore next */ []));
|
|
795
|
-
// Dropdown is always one step larger than the nav avatar
|
|
796
|
-
dropAvatarSize = computed(() => {
|
|
797
|
-
const s = this.navAvatarSize();
|
|
798
|
-
return s === 'sm' ? 'md' : s === 'md' ? 'lg' : 'xl';
|
|
799
|
-
}, ...(ngDevMode ? [{ debugName: "dropAvatarSize" }] : /* istanbul ignore next */ []));
|
|
800
|
-
avatarShape = computed(() => this.currentUser()?.avatarShape ?? this.inputAvatarShape(), ...(ngDevMode ? [{ debugName: "avatarShape" }] : /* istanbul ignore next */ []));
|
|
801
|
-
avatarFrame = computed(() => this.currentUser()?.avatarFrame ?? null, ...(ngDevMode ? [{ debugName: "avatarFrame" }] : /* istanbul ignore next */ []));
|
|
802
|
-
avatarRatio = computed(() => this.currentUser()?.avatarRatio ?? 'ar-11', ...(ngDevMode ? [{ debugName: "avatarRatio" }] : /* istanbul ignore next */ []));
|
|
803
|
-
givenStyle = computed(() => this.currentUser()?.givenColor || 'indigo', ...(ngDevMode ? [{ debugName: "givenStyle" }] : /* istanbul ignore next */ []));
|
|
804
|
-
signatureBroken = signal(false, ...(ngDevMode ? [{ debugName: "signatureBroken" }] : /* istanbul ignore next */ []));
|
|
805
|
-
signatureUrl = computed(() => {
|
|
806
|
-
if (!this.showSignature().some(s => s) || this.signatureBroken())
|
|
807
|
-
return null;
|
|
808
|
-
const baseRaw = this.authService.getConfig()?.apiBaseUrl ?? '';
|
|
809
|
-
const base = baseRaw.replace(/\/$/, '');
|
|
810
|
-
const u = this.currentUser();
|
|
811
|
-
const id = u?.userId ?? u?.id;
|
|
812
|
-
if (!id || !base)
|
|
813
|
-
return null;
|
|
814
|
-
return `${base}/auth/${id}/signature`;
|
|
815
|
-
}, ...(ngDevMode ? [{ debugName: "signatureUrl" }] : /* istanbul ignore next */ []));
|
|
816
|
-
authService = inject(MesAuthService);
|
|
817
|
-
router = inject(Router);
|
|
818
|
-
themeService = inject(ThemeService);
|
|
819
|
-
http = inject(HttpClient);
|
|
820
|
-
constructor() {
|
|
821
|
-
const currentUserSig = toSignal(this.authService.currentUser$, { initialValue: null });
|
|
822
|
-
const approvalEvent = toSignal(this.authService.approvalEvents$);
|
|
823
|
-
const notification = toSignal(this.authService.notifications$);
|
|
824
|
-
effect(() => {
|
|
825
|
-
const user = currentUserSig();
|
|
826
|
-
this.currentUser.set(user);
|
|
827
|
-
this.avatarRefresh.set(Date.now());
|
|
828
|
-
if (!user) {
|
|
829
|
-
this.unreadCount.set(0);
|
|
830
|
-
this.pendingApprovalCount.set(0);
|
|
831
|
-
return;
|
|
832
|
-
}
|
|
833
|
-
this.loadUnreadCount();
|
|
834
|
-
this.loadPendingApprovalCount();
|
|
835
|
-
});
|
|
836
|
-
effect(() => {
|
|
837
|
-
approvalEvent(); // track SignalR approval events
|
|
838
|
-
if (currentUserSig())
|
|
839
|
-
this.loadPendingApprovalCount();
|
|
840
|
-
});
|
|
841
|
-
effect(() => {
|
|
842
|
-
notification(); // track SignalR notification events
|
|
843
|
-
if (currentUserSig())
|
|
844
|
-
this.loadUnreadCount();
|
|
845
|
-
});
|
|
846
|
-
}
|
|
847
|
-
loadUnreadCount() {
|
|
848
|
-
this.authService.getUnreadCount().subscribe({
|
|
849
|
-
next: (response) => this.unreadCount.set(response.unreadCount || 0),
|
|
850
|
-
error: () => { }
|
|
851
|
-
});
|
|
852
|
-
}
|
|
853
|
-
loadPendingApprovalCount() {
|
|
854
|
-
const config = this.authService.getConfig();
|
|
855
|
-
if (!config)
|
|
856
|
-
return;
|
|
857
|
-
const url = `${config.apiBaseUrl.replace(/\/$/, '')}/approval/dashboard`;
|
|
858
|
-
this.http.get(url, { withCredentials: config.withCredentials ?? true }).subscribe({
|
|
859
|
-
next: (r) => this.pendingApprovalCount.set(r?.pendingCount ?? 0),
|
|
860
|
-
error: () => { }
|
|
861
|
-
});
|
|
862
|
-
}
|
|
863
|
-
onApprovalClick() {
|
|
864
|
-
this.approvalClick.emit();
|
|
865
|
-
}
|
|
866
|
-
onAiClick() {
|
|
867
|
-
this.aiClick.emit();
|
|
868
|
-
}
|
|
869
|
-
getAvatarUrl(user) {
|
|
870
|
-
// Use the refresh signal to force update
|
|
871
|
-
const refresh = this.avatarRefresh();
|
|
872
|
-
const config = this.authService.getConfig();
|
|
873
|
-
const baseUrl = config?.apiBaseUrl || '';
|
|
874
|
-
if (user.avatarPath) {
|
|
875
|
-
if (user.avatarPath.startsWith('http://') || user.avatarPath.startsWith('https://')) {
|
|
876
|
-
return user.avatarPath;
|
|
877
|
-
}
|
|
878
|
-
return `${baseUrl.replace(/\/$/, '')}${user.avatarPath}?t=${refresh}`;
|
|
879
|
-
}
|
|
880
|
-
const userId = user.userId;
|
|
881
|
-
if (userId && baseUrl) {
|
|
882
|
-
return `${baseUrl.replace(/\/$/, '')}/auth/${userId}/avatar?t=${refresh}`;
|
|
883
|
-
}
|
|
884
|
-
const displayName = user.userName || user.userId || 'User';
|
|
885
|
-
return `https://ui-avatars.com/api/?name=${encodeURIComponent(displayName)}&background=1976d2&color=fff`;
|
|
886
|
-
}
|
|
887
|
-
getLastNameInitial(user) {
|
|
888
|
-
const fullName = user.fullName || user.userName || 'U';
|
|
889
|
-
const parts = fullName.split(' ');
|
|
890
|
-
const lastPart = parts[parts.length - 1];
|
|
891
|
-
return lastPart.charAt(0).toUpperCase();
|
|
892
|
-
}
|
|
893
|
-
toggleDropdown() {
|
|
894
|
-
this.dropdownOpen.set(!this.dropdownOpen());
|
|
895
|
-
}
|
|
896
|
-
closeDropdown() {
|
|
897
|
-
this.dropdownOpen.set(false);
|
|
898
|
-
}
|
|
899
|
-
onDocumentClick(event) {
|
|
900
|
-
const target = event.target;
|
|
901
|
-
const clickedInside = target.closest('.user-menu-wrapper');
|
|
902
|
-
if (!clickedInside) {
|
|
903
|
-
this.dropdownOpen.set(false);
|
|
904
|
-
return;
|
|
905
|
-
}
|
|
906
|
-
if (target.closest('.ma-user-menu-btn')) {
|
|
907
|
-
this.dropdownOpen.set(false);
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
onLogin() {
|
|
911
|
-
const config = this.authService.getConfig();
|
|
912
|
-
const baseUrl = config?.userBaseUrl || '';
|
|
913
|
-
const returnUrl = encodeURIComponent(this.router.url);
|
|
914
|
-
window.location.href = `${baseUrl}/login?returnUrl=${returnUrl}`;
|
|
915
|
-
}
|
|
916
|
-
onViewProfile() {
|
|
917
|
-
const config = this.authService.getConfig();
|
|
918
|
-
const baseUrl = config?.userBaseUrl || '';
|
|
919
|
-
const currentUrl = window.location.href;
|
|
920
|
-
this.openInNewTabIfSameOrigin(currentUrl, `${baseUrl}/profile`);
|
|
921
|
-
this.dropdownOpen.set(false);
|
|
922
|
-
}
|
|
923
|
-
openInNewTabIfSameOrigin(currentUrl, destinationUrl) {
|
|
924
|
-
// Check if current page URL starts with the destination URL
|
|
925
|
-
if (!destinationUrl.startsWith(currentUrl)) {
|
|
926
|
-
window.open(destinationUrl, "_blank", "noopener,noreferrer");
|
|
927
|
-
}
|
|
928
|
-
else {
|
|
929
|
-
// Optional: redirect in same tab
|
|
930
|
-
window.location.href = destinationUrl;
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
onLogout() {
|
|
934
|
-
this.authService.logout().subscribe({
|
|
935
|
-
next: () => {
|
|
936
|
-
this.dropdownOpen.set(false);
|
|
937
|
-
const config = this.authService.getConfig();
|
|
938
|
-
const baseUrl = config?.userBaseUrl || '';
|
|
939
|
-
const returnUrl = encodeURIComponent(window.location.href);
|
|
940
|
-
window.location.href = `${baseUrl}/login?returnUrl=${returnUrl}`;
|
|
941
|
-
},
|
|
942
|
-
error: () => {
|
|
943
|
-
const config = this.authService.getConfig();
|
|
944
|
-
const baseUrl = config?.userBaseUrl || '';
|
|
945
|
-
window.location.href = `${baseUrl}/login`;
|
|
946
|
-
}
|
|
947
|
-
});
|
|
948
|
-
}
|
|
949
|
-
onNotificationClick() {
|
|
950
|
-
this.notificationClick.emit();
|
|
951
|
-
}
|
|
952
|
-
onSigErr(_ev) {
|
|
953
|
-
this.signatureBroken.set(true);
|
|
954
|
-
}
|
|
955
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: UserProfileComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
956
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: UserProfileComponent, isStandalone: true, selector: "ma-user-profile", inputs: { inputAvatarShape: { classPropertyName: "inputAvatarShape", publicName: "inputAvatarShape", isSignal: true, isRequired: false, transformFunction: null }, showBell: { classPropertyName: "showBell", publicName: "showBell", isSignal: true, isRequired: false, transformFunction: null }, showApproval: { classPropertyName: "showApproval", publicName: "showApproval", isSignal: true, isRequired: false, transformFunction: null }, showName: { classPropertyName: "showName", publicName: "showName", isSignal: true, isRequired: false, transformFunction: null }, showAi: { classPropertyName: "showAi", publicName: "showAi", isSignal: true, isRequired: false, transformFunction: null }, showSignature: { classPropertyName: "showSignature", publicName: "showSignature", isSignal: true, isRequired: false, transformFunction: null }, signatureHeight: { classPropertyName: "signatureHeight", publicName: "signatureHeight", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { notificationClick: "notificationClick", approvalClick: "approvalClick", aiClick: "aiClick" }, host: { listeners: { "document:click": "onDocumentClick($event)" }, properties: { "class": "this.themeClass" } }, ngImport: i0, template: "<div class=\"user-profile-container\">\n @if (!currentUser()) {\n <!-- Not logged in -->\n <button class=\"login-btn\" (click)=\"onLogin()\">Login</button>\n } @else {\n <!-- Logged in -->\n <div class=\"user-header\">\n <!-- Ask AI -->\n @if (showAi()) {\n <ma-ai-button (clicked)=\"onAiClick()\"></ma-ai-button>\n }\n\n <!-- Notification Bell -->\n @if (showBell()) {\n <button class=\"notification-btn\" [class.has-unread]=\"unreadCount() > 0\" (click)=\"onNotificationClick()\" title=\"Notifications\" aria-label=\"Notifications\">\n <svg class=\"bell-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9\"/>\n <path d=\"M13.73 21a2 2 0 0 1-3.46 0\"/>\n </svg>\n @if (unreadCount() > 0) {\n <span class=\"badge\">{{ unreadCount() > 99 ? '99+' : unreadCount() }}</span>\n }\n </button>\n }\n \n\n <!-- Approval Button -->\n @if (showApproval()) {\n <button class=\"notification-btn\" [class.has-unread]=\"pendingApprovalCount() > 0\" (click)=\"onApprovalClick()\" title=\"Approvals\" aria-label=\"Approvals\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M9 11l3 3L22 4\"/>\n <path d=\"M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11\"/>\n </svg>\n @if (pendingApprovalCount() > 0) {\n <span class=\"badge\">{{ pendingApprovalCount() > 99 ? '99+' : pendingApprovalCount() }}</span>\n }\n </button>\n } \n\n <!-- User Avatar + Dropdown -->\n <div class=\"user-menu-wrapper\">\n <button class=\"user-menu-btn\" (click)=\"toggleDropdown()\" [attr.aria-label]=\"'User menu for ' + (currentUser().fullName || currentUser().userName)\" aria-haspopup=\"true\" [attr.aria-expanded]=\"dropdownOpen()\">\n <ma-avatar\n [src]=\"getAvatarUrl(currentUser())\"\n [alt]=\"currentUser().fullName || currentUser().userName || ''\"\n [initials]=\"getLastNameInitial(currentUser())\"\n [size]=\"navAvatarSize()\"\n [shape]=\"avatarShape()\"\n [ratio]=\"avatarRatio()\"\n [frame]=\"avatarFrame()\" \n [ring]=\"true\"\n [ringActive]=\"dropdownOpen()\" /> \n </button>\n\n @if (dropdownOpen()) {\n <div class=\"mes-dropdown-menu\">\n <!-- User info header -->\n <div class=\"mes-dropdown-header\">\n <div class=\"dropdown-avatar-wrap\">\n <ma-avatar\n [src]=\"getAvatarUrl(currentUser())\"\n [alt]=\"currentUser().fullName || currentUser().userName || ''\"\n [initials]=\"getLastNameInitial(currentUser())\"\n [size]=\"dropAvatarSize()\"\n [shape]=\"avatarShape()\"\n [ratio]=\"avatarRatio()\"\n [frame]=\"avatarFrame()\"\n [scale]=\"1.5\" /> \n </div> \n <div class=\"dropdown-info-col\">\n <div class=\"dropdown-user-info\">\n <span class=\"dropdown-user-name\">{{ currentUser().fullName || currentUser().userName }}</span> \n <span class=\"dropdown-user-sub\">\n @if (currentUser().position || currentUser().department) {\n {{ currentUser().position || currentUser().department }} \n }\n @if (currentUser().givenTitle) {\n <span class=\"given-title-badge given-title\"\n [class]=\"'given-color-' + givenStyle()\">{{ currentUser().givenTitle }}</span>\n }\n </span>\n </div>\n <div class=\"dropdown-user-actions\">\n @if (showSignature()[1] && signatureUrl(); as sigUrl) {\n <img class=\"ma-ux-signature\"\n [src]=\"sigUrl\"\n [style.height.px]=\"signatureHeight()\"\n (error)=\"onSigErr($event)\"\n alt=\"signature\" />\n }\n <button class=\"icon-action profile-link\" (click)=\"onViewProfile()\" title=\"View Profile\" aria-label=\"View Profile\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2\"/><circle cx=\"12\" cy=\"7\" r=\"4\"/>\n </svg>\n </button>\n <button class=\"icon-action logout-item\" (click)=\"onLogout()\" title=\"Logout\" aria-label=\"Logout\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\"/><polyline points=\"16 17 21 12 16 7\"/><line x1=\"21\" y1=\"12\" x2=\"9\" y2=\"12\"/>\n </svg>\n </button>\n </div>\n </div>\n </div>\n\n <div class=\"mes-dropdown-items\">\n <ng-content></ng-content>\n </div>\n </div>\n }\n </div>\n\n @if (showName()){\n <div class=\"mes-user-header\">\n {{currentUser().fullName || currentUser().userName}}\n </div>\n }\n @if (showSignature()[0] && signatureUrl(); as sigUrl) {\n <img class=\"ma-ux-signature\"\n [src]=\"sigUrl\"\n [style.height.px]=\"signatureHeight()\"\n (error)=\"onSigErr($event)\"\n alt=\"signature\" />\n }\n </div>\n }\n</div>\n", styles: [".user-profile-container{display:flex;align-items:center;gap:4px}.login-btn{padding:7px 18px;background-color:var(--primary-color);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:500;font-size:13px;letter-spacing:.2px;transition:background-color .2s,transform .15s}.login-btn:hover{background-color:var(--primary-hover);transform:translateY(-1px)}.user-header{display:flex;align-items:center;gap:4px}.mes-user-header{margin-left:10px;font-weight:600;font-size:14px;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.notification-btn{position:relative;background:none;border:none;cursor:pointer;padding:8px;border-radius:10px;color:var(--text-secondary);display:flex;align-items:center;justify-content:center;transition:color .2s,background-color .2s}.notification-btn:hover{background-color:var(--primary-light);color:var(--primary-color)}.notification-btn.has-unread{color:var(--primary-color)}.bell-icon{display:block;transition:transform .35s cubic-bezier(.34,1.56,.64,1)}.notification-btn:hover .bell-icon{transform:rotate(-20deg) scale(1.15)}.badge{position:absolute;top:2px;right:2px;background-color:var(--error-color);color:#fff;border-radius:10px;min-width:17px;height:17px;padding:0 4px;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;line-height:1;box-shadow:0 0 0 2px var(--bg-primary);animation:badge-pop .25s cubic-bezier(.34,1.56,.64,1)}@keyframes badge-pop{0%{transform:scale(0)}to{transform:scale(1)}}.user-menu-wrapper{position:relative}.user-menu-btn{background:none;border:none;cursor:pointer;padding:4px;display:flex;align-items:center;justify-content:center;transition:transform .2s}.user-menu-btn:hover{transform:scale(1.06)}.mes-dropdown-menu{position:absolute;top:calc(100% + 10px);right:0;background:var(--bg-primary);border:1px solid var(--border-color);border-radius:14px;box-shadow:0 8px 32px var(--shadow-lg),0 2px 8px var(--shadow);min-width:220px;z-index:1000;overflow:hidden;animation:dropdown-in .16s cubic-bezier(.16,1,.3,1)}@keyframes dropdown-in{0%{opacity:0;transform:translateY(-8px) scale(.96)}to{opacity:1;transform:translateY(0) scale(1)}}.mes-dropdown-header{display:flex;align-items:center;gap:12px;padding:16px;background:var(--bg-secondary)}.dropdown-avatar-wrap{flex-shrink:0}.mes-dropdown-header .dropdown-avatar-wrap{margin-right:5px}.dropdown-user-info{display:flex;flex-direction:column;gap:3px;min-width:0}.dropdown-user-name{font-weight:600;font-size:14px;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:5px;min-width:180px}.dropdown-user-sub{font-size:11px;color:var(--primary-color);font-weight:500;white-space:nowrap;text-overflow:ellipsis;margin-bottom:12px}.given-title{float:inline-end}.mes-dropdown-divider{height:1px;background:var(--border-color)}.dropdown-info-col{display:flex;flex-direction:column;gap:6px;min-width:0;flex:1}.dropdown-user-actions{display:flex;align-items:center;justify-content:flex-end;gap:4px}.icon-action{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border:none;background:none;border-radius:8px;cursor:pointer;color:var(--text-secondary);transition:background-color .15s,color .15s,transform .15s}.icon-action:hover{transform:translateY(-1px)}.icon-action.profile-link{color:var(--primary-color)}.icon-action.profile-link:hover{background-color:var(--primary-light)}.icon-action.logout-item{color:var(--error-color)}.icon-action.logout-item:hover{background-color:var(--error-light)}.mes-dropdown-items:not(:empty){border-top:1px solid var(--border-color)}.ma-ux-signature{object-fit:contain;max-width:160px;vertical-align:middle;background:transparent}\n"], dependencies: [{ kind: "component", type: MaAvatarComponent, selector: "ma-avatar", inputs: ["src", "alt", "initials", "size", "shape", "frame", "ratio", "scale", "ring", "ringActive"] }, { kind: "component", type: MaAiButtonComponent, selector: "ma-ai-button", outputs: ["clicked"] }] });
|
|
957
|
-
}
|
|
958
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: UserProfileComponent, decorators: [{
|
|
959
|
-
type: Component,
|
|
960
|
-
args: [{ selector: 'ma-user-profile', imports: [MaAvatarComponent, MaAiButtonComponent], template: "<div class=\"user-profile-container\">\n @if (!currentUser()) {\n <!-- Not logged in -->\n <button class=\"login-btn\" (click)=\"onLogin()\">Login</button>\n } @else {\n <!-- Logged in -->\n <div class=\"user-header\">\n <!-- Ask AI -->\n @if (showAi()) {\n <ma-ai-button (clicked)=\"onAiClick()\"></ma-ai-button>\n }\n\n <!-- Notification Bell -->\n @if (showBell()) {\n <button class=\"notification-btn\" [class.has-unread]=\"unreadCount() > 0\" (click)=\"onNotificationClick()\" title=\"Notifications\" aria-label=\"Notifications\">\n <svg class=\"bell-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9\"/>\n <path d=\"M13.73 21a2 2 0 0 1-3.46 0\"/>\n </svg>\n @if (unreadCount() > 0) {\n <span class=\"badge\">{{ unreadCount() > 99 ? '99+' : unreadCount() }}</span>\n }\n </button>\n }\n \n\n <!-- Approval Button -->\n @if (showApproval()) {\n <button class=\"notification-btn\" [class.has-unread]=\"pendingApprovalCount() > 0\" (click)=\"onApprovalClick()\" title=\"Approvals\" aria-label=\"Approvals\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M9 11l3 3L22 4\"/>\n <path d=\"M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11\"/>\n </svg>\n @if (pendingApprovalCount() > 0) {\n <span class=\"badge\">{{ pendingApprovalCount() > 99 ? '99+' : pendingApprovalCount() }}</span>\n }\n </button>\n } \n\n <!-- User Avatar + Dropdown -->\n <div class=\"user-menu-wrapper\">\n <button class=\"user-menu-btn\" (click)=\"toggleDropdown()\" [attr.aria-label]=\"'User menu for ' + (currentUser().fullName || currentUser().userName)\" aria-haspopup=\"true\" [attr.aria-expanded]=\"dropdownOpen()\">\n <ma-avatar\n [src]=\"getAvatarUrl(currentUser())\"\n [alt]=\"currentUser().fullName || currentUser().userName || ''\"\n [initials]=\"getLastNameInitial(currentUser())\"\n [size]=\"navAvatarSize()\"\n [shape]=\"avatarShape()\"\n [ratio]=\"avatarRatio()\"\n [frame]=\"avatarFrame()\" \n [ring]=\"true\"\n [ringActive]=\"dropdownOpen()\" /> \n </button>\n\n @if (dropdownOpen()) {\n <div class=\"mes-dropdown-menu\">\n <!-- User info header -->\n <div class=\"mes-dropdown-header\">\n <div class=\"dropdown-avatar-wrap\">\n <ma-avatar\n [src]=\"getAvatarUrl(currentUser())\"\n [alt]=\"currentUser().fullName || currentUser().userName || ''\"\n [initials]=\"getLastNameInitial(currentUser())\"\n [size]=\"dropAvatarSize()\"\n [shape]=\"avatarShape()\"\n [ratio]=\"avatarRatio()\"\n [frame]=\"avatarFrame()\"\n [scale]=\"1.5\" /> \n </div> \n <div class=\"dropdown-info-col\">\n <div class=\"dropdown-user-info\">\n <span class=\"dropdown-user-name\">{{ currentUser().fullName || currentUser().userName }}</span> \n <span class=\"dropdown-user-sub\">\n @if (currentUser().position || currentUser().department) {\n {{ currentUser().position || currentUser().department }} \n }\n @if (currentUser().givenTitle) {\n <span class=\"given-title-badge given-title\"\n [class]=\"'given-color-' + givenStyle()\">{{ currentUser().givenTitle }}</span>\n }\n </span>\n </div>\n <div class=\"dropdown-user-actions\">\n @if (showSignature()[1] && signatureUrl(); as sigUrl) {\n <img class=\"ma-ux-signature\"\n [src]=\"sigUrl\"\n [style.height.px]=\"signatureHeight()\"\n (error)=\"onSigErr($event)\"\n alt=\"signature\" />\n }\n <button class=\"icon-action profile-link\" (click)=\"onViewProfile()\" title=\"View Profile\" aria-label=\"View Profile\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2\"/><circle cx=\"12\" cy=\"7\" r=\"4\"/>\n </svg>\n </button>\n <button class=\"icon-action logout-item\" (click)=\"onLogout()\" title=\"Logout\" aria-label=\"Logout\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\"/><polyline points=\"16 17 21 12 16 7\"/><line x1=\"21\" y1=\"12\" x2=\"9\" y2=\"12\"/>\n </svg>\n </button>\n </div>\n </div>\n </div>\n\n <div class=\"mes-dropdown-items\">\n <ng-content></ng-content>\n </div>\n </div>\n }\n </div>\n\n @if (showName()){\n <div class=\"mes-user-header\">\n {{currentUser().fullName || currentUser().userName}}\n </div>\n }\n @if (showSignature()[0] && signatureUrl(); as sigUrl) {\n <img class=\"ma-ux-signature\"\n [src]=\"sigUrl\"\n [style.height.px]=\"signatureHeight()\"\n (error)=\"onSigErr($event)\"\n alt=\"signature\" />\n }\n </div>\n }\n</div>\n", styles: [".user-profile-container{display:flex;align-items:center;gap:4px}.login-btn{padding:7px 18px;background-color:var(--primary-color);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:500;font-size:13px;letter-spacing:.2px;transition:background-color .2s,transform .15s}.login-btn:hover{background-color:var(--primary-hover);transform:translateY(-1px)}.user-header{display:flex;align-items:center;gap:4px}.mes-user-header{margin-left:10px;font-weight:600;font-size:14px;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.notification-btn{position:relative;background:none;border:none;cursor:pointer;padding:8px;border-radius:10px;color:var(--text-secondary);display:flex;align-items:center;justify-content:center;transition:color .2s,background-color .2s}.notification-btn:hover{background-color:var(--primary-light);color:var(--primary-color)}.notification-btn.has-unread{color:var(--primary-color)}.bell-icon{display:block;transition:transform .35s cubic-bezier(.34,1.56,.64,1)}.notification-btn:hover .bell-icon{transform:rotate(-20deg) scale(1.15)}.badge{position:absolute;top:2px;right:2px;background-color:var(--error-color);color:#fff;border-radius:10px;min-width:17px;height:17px;padding:0 4px;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;line-height:1;box-shadow:0 0 0 2px var(--bg-primary);animation:badge-pop .25s cubic-bezier(.34,1.56,.64,1)}@keyframes badge-pop{0%{transform:scale(0)}to{transform:scale(1)}}.user-menu-wrapper{position:relative}.user-menu-btn{background:none;border:none;cursor:pointer;padding:4px;display:flex;align-items:center;justify-content:center;transition:transform .2s}.user-menu-btn:hover{transform:scale(1.06)}.mes-dropdown-menu{position:absolute;top:calc(100% + 10px);right:0;background:var(--bg-primary);border:1px solid var(--border-color);border-radius:14px;box-shadow:0 8px 32px var(--shadow-lg),0 2px 8px var(--shadow);min-width:220px;z-index:1000;overflow:hidden;animation:dropdown-in .16s cubic-bezier(.16,1,.3,1)}@keyframes dropdown-in{0%{opacity:0;transform:translateY(-8px) scale(.96)}to{opacity:1;transform:translateY(0) scale(1)}}.mes-dropdown-header{display:flex;align-items:center;gap:12px;padding:16px;background:var(--bg-secondary)}.dropdown-avatar-wrap{flex-shrink:0}.mes-dropdown-header .dropdown-avatar-wrap{margin-right:5px}.dropdown-user-info{display:flex;flex-direction:column;gap:3px;min-width:0}.dropdown-user-name{font-weight:600;font-size:14px;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:5px;min-width:180px}.dropdown-user-sub{font-size:11px;color:var(--primary-color);font-weight:500;white-space:nowrap;text-overflow:ellipsis;margin-bottom:12px}.given-title{float:inline-end}.mes-dropdown-divider{height:1px;background:var(--border-color)}.dropdown-info-col{display:flex;flex-direction:column;gap:6px;min-width:0;flex:1}.dropdown-user-actions{display:flex;align-items:center;justify-content:flex-end;gap:4px}.icon-action{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border:none;background:none;border-radius:8px;cursor:pointer;color:var(--text-secondary);transition:background-color .15s,color .15s,transform .15s}.icon-action:hover{transform:translateY(-1px)}.icon-action.profile-link{color:var(--primary-color)}.icon-action.profile-link:hover{background-color:var(--primary-light)}.icon-action.logout-item{color:var(--error-color)}.icon-action.logout-item:hover{background-color:var(--error-light)}.mes-dropdown-items:not(:empty){border-top:1px solid var(--border-color)}.ma-ux-signature{object-fit:contain;max-width:160px;vertical-align:middle;background:transparent}\n"] }]
|
|
961
|
-
}], ctorParameters: () => [], propDecorators: { inputAvatarShape: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputAvatarShape", required: false }] }], showBell: [{ type: i0.Input, args: [{ isSignal: true, alias: "showBell", required: false }] }], showApproval: [{ type: i0.Input, args: [{ isSignal: true, alias: "showApproval", required: false }] }], showName: [{ type: i0.Input, args: [{ isSignal: true, alias: "showName", required: false }] }], showAi: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAi", required: false }] }], showSignature: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSignature", required: false }] }], signatureHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "signatureHeight", required: false }] }], notificationClick: [{ type: i0.Output, args: ["notificationClick"] }], approvalClick: [{ type: i0.Output, args: ["approvalClick"] }], aiClick: [{ type: i0.Output, args: ["aiClick"] }], themeClass: [{
|
|
962
|
-
type: HostBinding,
|
|
963
|
-
args: ['class']
|
|
964
|
-
}], onDocumentClick: [{
|
|
965
|
-
type: HostListener,
|
|
966
|
-
args: ['document:click', ['$event']]
|
|
967
|
-
}] } });
|
|
968
|
-
|
|
969
775
|
class ToastService {
|
|
970
776
|
_toasts = signal([], ...(ngDevMode ? [{ debugName: "_toasts" }] : /* istanbul ignore next */ []));
|
|
971
777
|
toasts = this._toasts.asReadonly();
|
|
@@ -992,865 +798,1459 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImpo
|
|
|
992
798
|
args: [{ providedIn: 'root' }]
|
|
993
799
|
}] });
|
|
994
800
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
}] } });
|
|
1015
|
-
|
|
1016
|
-
class NotificationPanelComponent {
|
|
1017
|
-
notificationRead = output();
|
|
1018
|
-
get themeClass() {
|
|
1019
|
-
return `theme-${this.themeService.currentTheme()}`;
|
|
1020
|
-
}
|
|
1021
|
-
isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
|
|
1022
|
-
activeTab = signal('unread', ...(ngDevMode ? [{ debugName: "activeTab" }] : /* istanbul ignore next */ []));
|
|
1023
|
-
notifications = signal([], ...(ngDevMode ? [{ debugName: "notifications" }] : /* istanbul ignore next */ []));
|
|
1024
|
-
selectedNotification = signal(null, ...(ngDevMode ? [{ debugName: "selectedNotification" }] : /* istanbul ignore next */ []));
|
|
1025
|
-
selectedNotificationHtml = signal(null, ...(ngDevMode ? [{ debugName: "selectedNotificationHtml" }] : /* istanbul ignore next */ []));
|
|
1026
|
-
selectedNotificationDate = signal('', ...(ngDevMode ? [{ debugName: "selectedNotificationDate" }] : /* istanbul ignore next */ []));
|
|
1027
|
-
// Stable time-ago strings keyed by notification id - refreshed every 30s via signal
|
|
1028
|
-
dateLabels = signal(new Map(), ...(ngDevMode ? [{ debugName: "dateLabels" }] : /* istanbul ignore next */ []));
|
|
1029
|
-
unreadNotifications = computed(() => this.notifications().filter(n => !n.isRead), ...(ngDevMode ? [{ debugName: "unreadNotifications" }] : /* istanbul ignore next */ []));
|
|
1030
|
-
readNotifications = computed(() => this.notifications().filter(n => n.isRead), ...(ngDevMode ? [{ debugName: "readNotifications" }] : /* istanbul ignore next */ []));
|
|
1031
|
-
currentNotifications = computed(() => this.activeTab() === 'unread' ? this.unreadNotifications() : this.readNotifications(), ...(ngDevMode ? [{ debugName: "currentNotifications" }] : /* istanbul ignore next */ []));
|
|
1032
|
-
dateTimer = null;
|
|
1033
|
-
// Normalize type to string - API may return integer or string
|
|
1034
|
-
// Backend enum: Info=0, Success=1, Warning=2, Error=3
|
|
1035
|
-
typeOf(notification) {
|
|
1036
|
-
const t = notification.type;
|
|
1037
|
-
if (t === 0 || t === 'Info')
|
|
1038
|
-
return 'Info';
|
|
1039
|
-
if (t === 1 || t === 'Success')
|
|
1040
|
-
return 'Success';
|
|
1041
|
-
if (t === 2 || t === 'Warning')
|
|
1042
|
-
return 'Warning';
|
|
1043
|
-
if (t === 3 || t === 'Error')
|
|
1044
|
-
return 'Error';
|
|
1045
|
-
return 'Info';
|
|
1046
|
-
}
|
|
1047
|
-
toastType(type) {
|
|
1048
|
-
const t = this.typeOf({ type });
|
|
1049
|
-
return t.toLowerCase();
|
|
1050
|
-
}
|
|
1051
|
-
sanitizer = inject(DomSanitizer);
|
|
1052
|
-
authService = inject(MesAuthService);
|
|
1053
|
-
toastService = inject(ToastService);
|
|
1054
|
-
themeService = inject(ThemeService);
|
|
1055
|
-
constructor() {
|
|
1056
|
-
this.loadNotifications();
|
|
1057
|
-
// Refresh time-ago labels every 30s - signal mutation triggers CD in zoneless
|
|
1058
|
-
this.dateTimer = setInterval(() => this.refreshDateLabels(), 30000);
|
|
1059
|
-
const latestNotification = toSignal(this.authService.notifications$);
|
|
1060
|
-
effect(() => {
|
|
1061
|
-
const notification = latestNotification();
|
|
1062
|
-
if (!notification)
|
|
1063
|
-
return;
|
|
1064
|
-
this.toastService.show(notification.message || '', notification.title, this.toastType(notification.type), 5000);
|
|
1065
|
-
this.loadNotifications();
|
|
1066
|
-
});
|
|
1067
|
-
}
|
|
1068
|
-
ngOnDestroy() {
|
|
1069
|
-
if (this.dateTimer !== null) {
|
|
1070
|
-
clearInterval(this.dateTimer);
|
|
1071
|
-
this.dateTimer = null;
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
loadNotifications() {
|
|
1075
|
-
this.authService.getNotifications(1, 50, true).subscribe({
|
|
1076
|
-
next: (response) => {
|
|
1077
|
-
this.notifications.set(response.items || []);
|
|
1078
|
-
this.refreshDateLabels();
|
|
801
|
+
/**
|
|
802
|
+
* Resolves the full set of client tools available to the AI: built-ins + consumer-registered.
|
|
803
|
+
* Handlers are invoked through `runTool(name, args)` which automatically threads the Injector
|
|
804
|
+
* so consumer handlers can use inject() patterns.
|
|
805
|
+
*/
|
|
806
|
+
class MaAiToolsRegistry {
|
|
807
|
+
config = inject(MES_AUTH_AI_CONFIG, { optional: true }) ?? DEFAULT_AI_CONFIG;
|
|
808
|
+
injector = inject(Injector);
|
|
809
|
+
/** Built-in client tools shipped by the library. */
|
|
810
|
+
builtIn = [
|
|
811
|
+
{
|
|
812
|
+
name: 'navigate',
|
|
813
|
+
description: 'Navigate the user to a path within the application. Use relative paths starting with /.',
|
|
814
|
+
parameters: {
|
|
815
|
+
type: 'object',
|
|
816
|
+
properties: {
|
|
817
|
+
path: { type: 'string', description: 'Path to navigate to, e.g. "/auth/users"' }
|
|
818
|
+
},
|
|
819
|
+
required: ['path']
|
|
1079
820
|
},
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
821
|
+
readOnly: true,
|
|
822
|
+
handler: (args) => {
|
|
823
|
+
const router = this.injector.get(Router, null);
|
|
824
|
+
if (!router)
|
|
825
|
+
return 'Router is not available in this app.';
|
|
826
|
+
router.navigateByUrl(args.path);
|
|
827
|
+
return `Navigated to ${args.path}`;
|
|
828
|
+
}
|
|
829
|
+
},
|
|
830
|
+
{
|
|
831
|
+
name: 'toggle_theme',
|
|
832
|
+
description: 'Toggle between light and dark theme.',
|
|
833
|
+
parameters: { type: 'object', properties: {} },
|
|
834
|
+
readOnly: true,
|
|
835
|
+
handler: () => {
|
|
836
|
+
const theme = this.injector.get(ThemeService);
|
|
837
|
+
const next = theme.currentTheme() === 'light' ? 'dark' : 'light';
|
|
838
|
+
theme.setFixTheme(next);
|
|
839
|
+
return `Theme switched to ${next}.`;
|
|
840
|
+
}
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
name: 'show_toast',
|
|
844
|
+
description: 'Show a small toast notification at the top of the screen.',
|
|
845
|
+
parameters: {
|
|
846
|
+
type: 'object',
|
|
847
|
+
properties: {
|
|
848
|
+
message: { type: 'string', description: 'Toast body text' },
|
|
849
|
+
title: { type: 'string', description: 'Optional title' },
|
|
850
|
+
severity: { type: 'string', enum: ['info', 'success', 'warning', 'error'], description: 'Toast severity' }
|
|
1103
851
|
},
|
|
1104
|
-
|
|
1105
|
-
}
|
|
852
|
+
required: ['message']
|
|
853
|
+
},
|
|
854
|
+
readOnly: true,
|
|
855
|
+
handler: (args) => {
|
|
856
|
+
const toast = this.injector.get(ToastService);
|
|
857
|
+
toast.show(args.message, args.title, args.severity ?? 'info');
|
|
858
|
+
return 'Toast shown.';
|
|
859
|
+
}
|
|
860
|
+
},
|
|
861
|
+
{
|
|
862
|
+
name: 'who_am_i_client',
|
|
863
|
+
description: 'Get the currently signed-in user as seen by the browser (id, name, roles loaded so far).',
|
|
864
|
+
parameters: { type: 'object', properties: {} },
|
|
865
|
+
readOnly: true,
|
|
866
|
+
handler: () => {
|
|
867
|
+
const auth = this.injector.get(MesAuthService);
|
|
868
|
+
const u = auth.currentUser;
|
|
869
|
+
if (!u)
|
|
870
|
+
return 'No user signed in.';
|
|
871
|
+
return JSON.stringify({
|
|
872
|
+
id: u.userId ?? u.id,
|
|
873
|
+
userName: u.userName,
|
|
874
|
+
fullName: u.fullName,
|
|
875
|
+
department: u.department,
|
|
876
|
+
position: u.position,
|
|
877
|
+
email: u.email
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
},
|
|
881
|
+
{
|
|
882
|
+
name: 'reload_page',
|
|
883
|
+
description: 'Reload the current browser page. Use as a last resort if state seems stuck.',
|
|
884
|
+
parameters: { type: 'object', properties: {} },
|
|
885
|
+
readOnly: false,
|
|
886
|
+
handler: () => {
|
|
887
|
+
window.location.reload();
|
|
888
|
+
return 'Reloading page…';
|
|
889
|
+
}
|
|
1106
890
|
}
|
|
891
|
+
];
|
|
892
|
+
/** All tools, deduplicated by name (consumer wins on conflict). */
|
|
893
|
+
list() {
|
|
894
|
+
const fromConfig = this.config.tools ?? [];
|
|
895
|
+
const byName = new Map();
|
|
896
|
+
for (const t of this.builtIn)
|
|
897
|
+
byName.set(t.name, t);
|
|
898
|
+
for (const t of fromConfig)
|
|
899
|
+
byName.set(t.name, t);
|
|
900
|
+
return Array.from(byName.values());
|
|
1107
901
|
}
|
|
1108
|
-
|
|
1109
|
-
this.
|
|
1110
|
-
this.selectedNotificationHtml.set(null);
|
|
1111
|
-
this.selectedNotificationDate.set('');
|
|
902
|
+
resolve(name) {
|
|
903
|
+
return this.list().find(t => t.name === name);
|
|
1112
904
|
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
905
|
+
/** Run a registered client tool and serialize its result for posting back to the agent loop. */
|
|
906
|
+
async runTool(name, args) {
|
|
907
|
+
const tool = this.resolve(name);
|
|
908
|
+
if (!tool)
|
|
909
|
+
return { ok: false, result: `Unknown client tool: ${name}` };
|
|
910
|
+
try {
|
|
911
|
+
const raw = await Promise.resolve(tool.handler(args ?? {}));
|
|
912
|
+
const result = typeof raw === 'string' ? raw : JSON.stringify(raw ?? null);
|
|
913
|
+
return { ok: true, result };
|
|
914
|
+
}
|
|
915
|
+
catch (err) {
|
|
916
|
+
return { ok: false, result: err?.message ?? String(err) };
|
|
1117
917
|
}
|
|
1118
918
|
}
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
919
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiToolsRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
920
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiToolsRegistry, providedIn: 'root' });
|
|
921
|
+
}
|
|
922
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiToolsRegistry, decorators: [{
|
|
923
|
+
type: Injectable,
|
|
924
|
+
args: [{ providedIn: 'root' }]
|
|
925
|
+
}] });
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Drives the chat panel.
|
|
929
|
+
*
|
|
930
|
+
* Uses `fetch` + `ReadableStream` (not `EventSource`, which can't POST a body) to talk to
|
|
931
|
+
* `/ai/chat` SSE. State lives in signals so the panel can re-render reactively.
|
|
932
|
+
*/
|
|
933
|
+
class MaAiService {
|
|
934
|
+
auth = inject(MesAuthService);
|
|
935
|
+
aiConfig = inject(MES_AUTH_AI_CONFIG, { optional: true }) ?? DEFAULT_AI_CONFIG;
|
|
936
|
+
tools = inject(MaAiToolsRegistry);
|
|
937
|
+
messages = signal([], ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
|
|
938
|
+
streaming = signal(false, ...(ngDevMode ? [{ debugName: "streaming" }] : /* istanbul ignore next */ []));
|
|
939
|
+
/** When non-null, a write-class client tool is awaiting user confirmation. */
|
|
940
|
+
pendingApproval = signal(null, ...(ngDevMode ? [{ debugName: "pendingApproval" }] : /* istanbul ignore next */ []));
|
|
941
|
+
lastError = signal(null, ...(ngDevMode ? [{ debugName: "lastError" }] : /* istanbul ignore next */ []));
|
|
942
|
+
sessionId = null;
|
|
943
|
+
/** Verbs the user already chose "always approve" for in this session. */
|
|
944
|
+
alwaysApproved = new Set();
|
|
945
|
+
abortController = null;
|
|
946
|
+
/** ID of the current assistant bubble we're streaming tokens into. */
|
|
947
|
+
currentAssistantId = null;
|
|
948
|
+
get enabled() {
|
|
949
|
+
return this.aiConfig.enabled !== false;
|
|
1129
950
|
}
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
951
|
+
/** Start a brand-new conversation. */
|
|
952
|
+
resetSession() {
|
|
953
|
+
this.cancel();
|
|
954
|
+
this.sessionId = null;
|
|
955
|
+
this.messages.set([]);
|
|
956
|
+
this.pendingApproval.set(null);
|
|
957
|
+
this.lastError.set(null);
|
|
958
|
+
this.alwaysApproved.clear();
|
|
1138
959
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
this.
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
|
|
960
|
+
/** Cancel the in-flight stream (if any). The session id is preserved so the user can resume. */
|
|
961
|
+
cancel() {
|
|
962
|
+
if (this.abortController) {
|
|
963
|
+
this.abortController.abort();
|
|
964
|
+
this.abortController = null;
|
|
965
|
+
}
|
|
966
|
+
this.streaming.set(false);
|
|
967
|
+
this.currentAssistantId = null;
|
|
968
|
+
// If a client-tool call was awaiting the user, clearing it lets them start over.
|
|
969
|
+
this.pendingApproval.set(null);
|
|
1147
970
|
}
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
this.loadNotifications();
|
|
1156
|
-
});
|
|
971
|
+
/** User typed and submitted a message. */
|
|
972
|
+
async send(text, currentRoute) {
|
|
973
|
+
if (!text.trim())
|
|
974
|
+
return;
|
|
975
|
+
this.lastError.set(null);
|
|
976
|
+
this.appendBubble({ id: this.uid(), role: 'user', text });
|
|
977
|
+
await this.runStream({ message: text }, currentRoute);
|
|
1157
978
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
979
|
+
/**
|
|
980
|
+
* Approve (or decline) a pending client tool call. If approved (and alwaysApprove),
|
|
981
|
+
* remember the verb so we skip the prompt next time in this session.
|
|
982
|
+
*/
|
|
983
|
+
async resolvePendingApproval(decision) {
|
|
984
|
+
const pending = this.pendingApproval();
|
|
985
|
+
if (!pending)
|
|
986
|
+
return;
|
|
987
|
+
this.pendingApproval.set(null);
|
|
988
|
+
if (decision === 'decline') {
|
|
989
|
+
await this.continueWithToolResult(pending.callId, false, 'User declined this tool call.');
|
|
990
|
+
this.markToolStatus(pending.callId, 'declined');
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
if (decision === 'always') {
|
|
994
|
+
this.alwaysApproved.add(this.verbOf(pending.name));
|
|
995
|
+
}
|
|
996
|
+
await this.executeClientTool(pending.callId, pending.name, pending.args);
|
|
1172
997
|
}
|
|
1173
|
-
//
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
const
|
|
1180
|
-
const
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
998
|
+
// ── private ────────────────────────────────────────────────────────────────
|
|
999
|
+
async runStream(payload, currentRoute) {
|
|
1000
|
+
if (!this.enabled) {
|
|
1001
|
+
this.lastError.set('AI assistant is disabled.');
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
const config = this.auth.getConfig();
|
|
1005
|
+
const base = config?.apiBaseUrl?.replace(/\/$/, '') ?? '';
|
|
1006
|
+
if (!base) {
|
|
1007
|
+
this.lastError.set('MesAuth is not configured.');
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
// Start a fresh assistant bubble that we stream tokens into.
|
|
1011
|
+
const assistantId = this.uid();
|
|
1012
|
+
this.currentAssistantId = assistantId;
|
|
1013
|
+
this.appendBubble({ id: assistantId, role: 'assistant', text: '', pending: true, toolEvents: [] });
|
|
1014
|
+
this.streaming.set(true);
|
|
1015
|
+
this.abortController = new AbortController();
|
|
1016
|
+
try {
|
|
1017
|
+
const res = await fetch(`${base}/ai/chat`, {
|
|
1018
|
+
method: 'POST',
|
|
1019
|
+
headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream' },
|
|
1020
|
+
credentials: config?.withCredentials !== false ? 'include' : 'same-origin',
|
|
1021
|
+
body: JSON.stringify({
|
|
1022
|
+
sessionId: this.sessionId,
|
|
1023
|
+
message: payload.message,
|
|
1024
|
+
toolResult: payload.toolResult,
|
|
1025
|
+
clientTools: this.tools.list().map(t => ({
|
|
1026
|
+
name: t.name,
|
|
1027
|
+
description: t.description,
|
|
1028
|
+
parameters: t.parameters,
|
|
1029
|
+
readOnly: t.readOnly === true
|
|
1030
|
+
})),
|
|
1031
|
+
context: { currentRoute, appName: this.aiConfig.appName }
|
|
1032
|
+
}),
|
|
1033
|
+
signal: this.abortController.signal
|
|
1034
|
+
});
|
|
1035
|
+
if (!res.ok || !res.body) {
|
|
1036
|
+
const detail = await res.text().catch(() => '');
|
|
1037
|
+
throw new Error(`AI request failed (${res.status}). ${detail}`);
|
|
1038
|
+
}
|
|
1039
|
+
await this.readSse(res.body);
|
|
1040
|
+
}
|
|
1041
|
+
catch (err) {
|
|
1042
|
+
if (err?.name === 'AbortError') {
|
|
1043
|
+
// user cancelled
|
|
1044
|
+
}
|
|
1045
|
+
else {
|
|
1046
|
+
this.lastError.set(err?.message ?? String(err));
|
|
1047
|
+
this.finalizeAssistant(`[error: ${err?.message ?? err}]`);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
finally {
|
|
1051
|
+
this.streaming.set(false);
|
|
1052
|
+
this.abortController = null;
|
|
1053
|
+
}
|
|
1192
1054
|
}
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
const
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1055
|
+
async readSse(body) {
|
|
1056
|
+
const reader = body.getReader();
|
|
1057
|
+
const decoder = new TextDecoder();
|
|
1058
|
+
let buffer = '';
|
|
1059
|
+
while (true) {
|
|
1060
|
+
const { value, done } = await reader.read();
|
|
1061
|
+
if (done)
|
|
1062
|
+
break;
|
|
1063
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1064
|
+
// SSE events are separated by a blank line.
|
|
1065
|
+
let idx;
|
|
1066
|
+
while ((idx = buffer.indexOf('\n\n')) !== -1) {
|
|
1067
|
+
const rawEvent = buffer.slice(0, idx);
|
|
1068
|
+
buffer = buffer.slice(idx + 2);
|
|
1069
|
+
const dataLine = rawEvent.split('\n').find(l => l.startsWith('data:'));
|
|
1070
|
+
if (!dataLine)
|
|
1071
|
+
continue;
|
|
1072
|
+
const json = dataLine.slice(5).trim();
|
|
1073
|
+
if (!json)
|
|
1074
|
+
continue;
|
|
1075
|
+
let ev;
|
|
1076
|
+
try {
|
|
1077
|
+
ev = JSON.parse(json);
|
|
1078
|
+
}
|
|
1079
|
+
catch {
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
await this.handleEvent(ev);
|
|
1083
|
+
}
|
|
1199
1084
|
}
|
|
1200
|
-
this.dateLabels.set(newLabels);
|
|
1201
1085
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1086
|
+
async handleEvent(ev) {
|
|
1087
|
+
switch (ev.type) {
|
|
1088
|
+
case 'session':
|
|
1089
|
+
this.sessionId = ev.sessionId;
|
|
1090
|
+
break;
|
|
1091
|
+
case 'token':
|
|
1092
|
+
this.appendToken(ev.text);
|
|
1093
|
+
break;
|
|
1094
|
+
case 'tool_call_started':
|
|
1095
|
+
this.appendToolEvent({
|
|
1096
|
+
id: ev.id,
|
|
1097
|
+
name: ev.name,
|
|
1098
|
+
side: ev.side,
|
|
1099
|
+
status: 'running',
|
|
1100
|
+
args: ev.args,
|
|
1101
|
+
readOnly: ev.readOnly
|
|
1102
|
+
});
|
|
1103
|
+
break;
|
|
1104
|
+
case 'tool_call_completed':
|
|
1105
|
+
this.markToolStatus(ev.id, ev.ok ? 'ok' : 'error', ev.summary);
|
|
1106
|
+
break;
|
|
1107
|
+
case 'client_tool_call':
|
|
1108
|
+
this.appendToolEvent({
|
|
1109
|
+
id: ev.id,
|
|
1110
|
+
name: ev.name,
|
|
1111
|
+
side: 'client',
|
|
1112
|
+
status: ev.readOnly ? 'running' : 'awaiting-approval',
|
|
1113
|
+
args: ev.args,
|
|
1114
|
+
readOnly: ev.readOnly
|
|
1115
|
+
});
|
|
1116
|
+
if (ev.readOnly || this.alwaysApproved.has(this.verbOf(ev.name))) {
|
|
1117
|
+
// Auto-run; the next /ai/chat call will deliver the result.
|
|
1118
|
+
await this.executeClientTool(ev.id, ev.name, ev.args);
|
|
1119
|
+
}
|
|
1120
|
+
else {
|
|
1121
|
+
this.pendingApproval.set({ callId: ev.id, name: ev.name, args: ev.args, side: 'client' });
|
|
1122
|
+
}
|
|
1123
|
+
break;
|
|
1124
|
+
case 'error':
|
|
1125
|
+
this.lastError.set(ev.message);
|
|
1126
|
+
this.finalizeAssistant(`[error: ${ev.message}]`);
|
|
1127
|
+
break;
|
|
1128
|
+
case 'done':
|
|
1129
|
+
this.finalizeAssistant();
|
|
1130
|
+
break;
|
|
1207
1131
|
}
|
|
1208
|
-
return normalized;
|
|
1209
1132
|
}
|
|
1210
|
-
|
|
1211
|
-
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:0}.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"] });
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
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:0}.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"] }]
|
|
1216
|
-
}], ctorParameters: () => [], propDecorators: { notificationRead: [{ type: i0.Output, args: ["notificationRead"] }], themeClass: [{
|
|
1217
|
-
type: HostBinding,
|
|
1218
|
-
args: ['class']
|
|
1219
|
-
}] } });
|
|
1220
|
-
|
|
1221
|
-
class MaApprovalService {
|
|
1222
|
-
apiBase = '';
|
|
1223
|
-
http;
|
|
1224
|
-
config = null;
|
|
1225
|
-
constructor() { }
|
|
1226
|
-
init(config, httpClient) {
|
|
1227
|
-
this.config = config;
|
|
1228
|
-
this.http = httpClient;
|
|
1229
|
-
this.apiBase = config.apiBaseUrl.replace(/\/$/, '');
|
|
1133
|
+
async executeClientTool(callId, name, args) {
|
|
1134
|
+
this.markToolStatus(callId, 'running');
|
|
1135
|
+
const { ok, result } = await this.tools.runTool(name, args);
|
|
1136
|
+
this.markToolStatus(callId, ok ? 'ok' : 'error', this.summarize(result));
|
|
1137
|
+
await this.continueWithToolResult(callId, ok, result);
|
|
1230
1138
|
}
|
|
1231
|
-
|
|
1232
|
-
|
|
1139
|
+
async continueWithToolResult(callId, ok, result) {
|
|
1140
|
+
// Open a new SSE stream that delivers the tool result.
|
|
1141
|
+
await this.runStream({ toolResult: { callId, ok, result } });
|
|
1233
1142
|
}
|
|
1234
|
-
//
|
|
1235
|
-
|
|
1236
|
-
|
|
1143
|
+
// ── bubble helpers ────────────────────────────────────────────────────────
|
|
1144
|
+
appendBubble(b) {
|
|
1145
|
+
this.messages.update(arr => [...arr, b]);
|
|
1237
1146
|
}
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1147
|
+
appendToken(text) {
|
|
1148
|
+
if (!this.currentAssistantId) {
|
|
1149
|
+
const id = this.uid();
|
|
1150
|
+
this.currentAssistantId = id;
|
|
1151
|
+
this.appendBubble({ id, role: 'assistant', text, pending: true, toolEvents: [] });
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
const id = this.currentAssistantId;
|
|
1155
|
+
this.messages.update(arr => arr.map(m => m.id === id ? { ...m, text: m.text + text } : m));
|
|
1241
1156
|
}
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1157
|
+
appendToolEvent(ev) {
|
|
1158
|
+
if (!this.currentAssistantId) {
|
|
1159
|
+
const id = this.uid();
|
|
1160
|
+
this.currentAssistantId = id;
|
|
1161
|
+
this.appendBubble({ id, role: 'assistant', text: '', pending: true, toolEvents: [ev] });
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
const id = this.currentAssistantId;
|
|
1165
|
+
this.messages.update(arr => arr.map(m => m.id === id
|
|
1166
|
+
? { ...m, toolEvents: [...(m.toolEvents ?? []), ev] }
|
|
1167
|
+
: m));
|
|
1247
1168
|
}
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1169
|
+
markToolStatus(callId, status, summary) {
|
|
1170
|
+
this.messages.update(arr => arr.map(m => ({
|
|
1171
|
+
...m,
|
|
1172
|
+
toolEvents: m.toolEvents?.map(t => t.id === callId ? { ...t, status, summary: summary ?? t.summary } : t)
|
|
1173
|
+
})));
|
|
1251
1174
|
}
|
|
1252
|
-
|
|
1253
|
-
|
|
1175
|
+
finalizeAssistant(extra) {
|
|
1176
|
+
const id = this.currentAssistantId;
|
|
1177
|
+
if (!id)
|
|
1178
|
+
return;
|
|
1179
|
+
this.messages.update(arr => arr.map(m => m.id === id
|
|
1180
|
+
? { ...m, pending: false, text: extra ? (m.text + (m.text ? '\n' : '') + extra) : m.text }
|
|
1181
|
+
: m));
|
|
1182
|
+
this.currentAssistantId = null;
|
|
1254
1183
|
}
|
|
1255
|
-
|
|
1256
|
-
return
|
|
1184
|
+
verbOf(name) {
|
|
1185
|
+
return name.split('_')[0] ?? name;
|
|
1257
1186
|
}
|
|
1258
|
-
|
|
1259
|
-
|
|
1187
|
+
summarize(s) {
|
|
1188
|
+
const flat = s.replace(/\s+/g, ' ').trim();
|
|
1189
|
+
return flat.length <= 80 ? flat : flat.slice(0, 80) + '…';
|
|
1260
1190
|
}
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
const body = { comment };
|
|
1264
|
-
return this.http.post(`${this.apiBase}/approval/documents/${documentId}/approve`, body, this.opts);
|
|
1191
|
+
uid() {
|
|
1192
|
+
return Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
1265
1193
|
}
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1194
|
+
// ── conversation history (DB-backed via MesAuth.Api) ─────────────────────
|
|
1195
|
+
/** GET /ai/conversations — list this user's past conversations (most recent first). */
|
|
1196
|
+
async listConversations(take = 50) {
|
|
1197
|
+
return this.apiGet(`/ai/conversations?take=${take}`) ?? [];
|
|
1269
1198
|
}
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1199
|
+
/** GET /ai/conversations/{id} — fetch and hydrate a past conversation into the panel. */
|
|
1200
|
+
async resumeConversation(id) {
|
|
1201
|
+
const detail = await this.apiGet(`/ai/conversations/${encodeURIComponent(id)}`);
|
|
1202
|
+
if (!detail)
|
|
1203
|
+
return false;
|
|
1204
|
+
this.cancel();
|
|
1205
|
+
this.sessionId = detail.id;
|
|
1206
|
+
this.messages.set(this.hydrateBubbles(detail.messages));
|
|
1207
|
+
this.pendingApproval.set(null);
|
|
1208
|
+
this.lastError.set(null);
|
|
1209
|
+
this.alwaysApproved.clear();
|
|
1210
|
+
return true;
|
|
1273
1211
|
}
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
url += `?reason=${encodeURIComponent(reason)}`;
|
|
1278
|
-
return this.http.delete(url, this.opts);
|
|
1212
|
+
/** DELETE /ai/conversations/{id} */
|
|
1213
|
+
async deleteConversation(id) {
|
|
1214
|
+
return this.apiSend('DELETE', `/ai/conversations/${encodeURIComponent(id)}`);
|
|
1279
1215
|
}
|
|
1280
|
-
//
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
if (appId)
|
|
1284
|
-
url += `?appId=${encodeURIComponent(appId)}`;
|
|
1285
|
-
return this.http.get(url, this.opts);
|
|
1216
|
+
// ── user lessons (system-prompt injections) ──────────────────────────────
|
|
1217
|
+
async listLessons() {
|
|
1218
|
+
return this.apiGet(`/ai/lessons`) ?? [];
|
|
1286
1219
|
}
|
|
1287
|
-
|
|
1288
|
-
return this.
|
|
1220
|
+
async createLesson(text) {
|
|
1221
|
+
return this.apiSend('POST', `/ai/lessons`, { text });
|
|
1289
1222
|
}
|
|
1290
|
-
|
|
1291
|
-
return this.
|
|
1223
|
+
async updateLesson(id, patch) {
|
|
1224
|
+
return this.apiSend('PUT', `/ai/lessons/${encodeURIComponent(id)}`, patch);
|
|
1292
1225
|
}
|
|
1293
|
-
|
|
1294
|
-
return this.
|
|
1226
|
+
async deleteLesson(id) {
|
|
1227
|
+
return this.apiSend('DELETE', `/ai/lessons/${encodeURIComponent(id)}`);
|
|
1295
1228
|
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1229
|
+
// ── private HTTP plumbing (mirrors runStream's credentials handling) ─────
|
|
1230
|
+
baseUrl() {
|
|
1231
|
+
const config = this.auth.getConfig();
|
|
1232
|
+
const base = config?.apiBaseUrl?.replace(/\/$/, '') ?? '';
|
|
1233
|
+
return base ? base : null;
|
|
1298
1234
|
}
|
|
1299
|
-
|
|
1300
|
-
|
|
1235
|
+
async apiGet(path) {
|
|
1236
|
+
const base = this.baseUrl();
|
|
1237
|
+
if (!base)
|
|
1238
|
+
return null;
|
|
1239
|
+
const config = this.auth.getConfig();
|
|
1240
|
+
try {
|
|
1241
|
+
const res = await fetch(`${base}${path}`, {
|
|
1242
|
+
method: 'GET',
|
|
1243
|
+
headers: { 'Accept': 'application/json' },
|
|
1244
|
+
credentials: config?.withCredentials !== false ? 'include' : 'same-origin',
|
|
1245
|
+
});
|
|
1246
|
+
if (!res.ok)
|
|
1247
|
+
return null;
|
|
1248
|
+
return await res.json();
|
|
1249
|
+
}
|
|
1250
|
+
catch {
|
|
1251
|
+
return null;
|
|
1252
|
+
}
|
|
1301
1253
|
}
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1254
|
+
async apiSend(method, path, body) {
|
|
1255
|
+
const base = this.baseUrl();
|
|
1256
|
+
if (!base)
|
|
1257
|
+
return method === 'DELETE' ? false : null;
|
|
1258
|
+
const config = this.auth.getConfig();
|
|
1259
|
+
try {
|
|
1260
|
+
const res = await fetch(`${base}${path}`, {
|
|
1261
|
+
method,
|
|
1262
|
+
headers: body !== undefined
|
|
1263
|
+
? { 'Accept': 'application/json', 'Content-Type': 'application/json' }
|
|
1264
|
+
: { 'Accept': 'application/json' },
|
|
1265
|
+
credentials: config?.withCredentials !== false ? 'include' : 'same-origin',
|
|
1266
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
1267
|
+
});
|
|
1268
|
+
if (!res.ok)
|
|
1269
|
+
return method === 'DELETE' ? false : null;
|
|
1270
|
+
if (method === 'DELETE')
|
|
1271
|
+
return true;
|
|
1272
|
+
const text = await res.text();
|
|
1273
|
+
return text ? JSON.parse(text) : null;
|
|
1274
|
+
}
|
|
1275
|
+
catch {
|
|
1276
|
+
return method === 'DELETE' ? false : null;
|
|
1277
|
+
}
|
|
1305
1278
|
}
|
|
1306
|
-
|
|
1307
|
-
|
|
1279
|
+
/**
|
|
1280
|
+
* Rebuild ChatBubble[] from a stored transcript. Tool messages don't appear as their
|
|
1281
|
+
* own bubbles — they were emitted as chips on the preceding assistant bubble — but we
|
|
1282
|
+
* reconstruct that linkage best-effort so the resumed view roughly matches what the
|
|
1283
|
+
* user originally saw.
|
|
1284
|
+
*/
|
|
1285
|
+
hydrateBubbles(messages) {
|
|
1286
|
+
const bubbles = [];
|
|
1287
|
+
let lastAssistant = null;
|
|
1288
|
+
for (const m of messages) {
|
|
1289
|
+
if (m.role === 'user') {
|
|
1290
|
+
const b = { id: this.uid(), role: 'user', text: m.content ?? '' };
|
|
1291
|
+
bubbles.push(b);
|
|
1292
|
+
lastAssistant = null;
|
|
1293
|
+
}
|
|
1294
|
+
else if (m.role === 'assistant') {
|
|
1295
|
+
const toolEvents = (m.toolCalls ?? []).map(tc => ({
|
|
1296
|
+
id: tc.id,
|
|
1297
|
+
name: tc.name,
|
|
1298
|
+
side: (tc.side === 'client' ? 'client' : 'server'),
|
|
1299
|
+
status: 'ok',
|
|
1300
|
+
args: tc.args,
|
|
1301
|
+
}));
|
|
1302
|
+
const b = {
|
|
1303
|
+
id: this.uid(),
|
|
1304
|
+
role: 'assistant',
|
|
1305
|
+
text: m.content ?? '',
|
|
1306
|
+
toolEvents: toolEvents.length > 0 ? toolEvents : undefined,
|
|
1307
|
+
};
|
|
1308
|
+
bubbles.push(b);
|
|
1309
|
+
lastAssistant = b;
|
|
1310
|
+
}
|
|
1311
|
+
else if (m.role === 'tool' && lastAssistant) {
|
|
1312
|
+
const ev = lastAssistant.toolEvents?.find(e => e.id === (m.toolCallId ?? ''));
|
|
1313
|
+
if (ev) {
|
|
1314
|
+
ev.status = m.toolIsError ? 'error' : 'ok';
|
|
1315
|
+
const flat = (m.content ?? '').replace(/\s+/g, ' ').trim();
|
|
1316
|
+
ev.summary = flat.length <= 80 ? flat : flat.slice(0, 80) + '…';
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
return bubbles;
|
|
1321
|
+
}
|
|
1322
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1323
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiService, providedIn: 'root' });
|
|
1308
1324
|
}
|
|
1309
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type:
|
|
1310
|
-
type: Injectable
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
// ====================== Enums ======================
|
|
1314
|
-
var ApprovalStepMode;
|
|
1315
|
-
(function (ApprovalStepMode) {
|
|
1316
|
-
ApprovalStepMode[ApprovalStepMode["Sequential"] = 0] = "Sequential";
|
|
1317
|
-
ApprovalStepMode[ApprovalStepMode["Parallel"] = 1] = "Parallel";
|
|
1318
|
-
})(ApprovalStepMode || (ApprovalStepMode = {}));
|
|
1319
|
-
var ApprovalDocumentStatus;
|
|
1320
|
-
(function (ApprovalDocumentStatus) {
|
|
1321
|
-
ApprovalDocumentStatus[ApprovalDocumentStatus["Draft"] = 0] = "Draft";
|
|
1322
|
-
ApprovalDocumentStatus[ApprovalDocumentStatus["Pending"] = 1] = "Pending";
|
|
1323
|
-
ApprovalDocumentStatus[ApprovalDocumentStatus["Approved"] = 2] = "Approved";
|
|
1324
|
-
ApprovalDocumentStatus[ApprovalDocumentStatus["Rejected"] = 3] = "Rejected";
|
|
1325
|
-
ApprovalDocumentStatus[ApprovalDocumentStatus["Cancelled"] = 4] = "Cancelled";
|
|
1326
|
-
ApprovalDocumentStatus[ApprovalDocumentStatus["Expired"] = 5] = "Expired";
|
|
1327
|
-
})(ApprovalDocumentStatus || (ApprovalDocumentStatus = {}));
|
|
1328
|
-
var ApprovalStepStatus;
|
|
1329
|
-
(function (ApprovalStepStatus) {
|
|
1330
|
-
ApprovalStepStatus[ApprovalStepStatus["Waiting"] = 0] = "Waiting";
|
|
1331
|
-
ApprovalStepStatus[ApprovalStepStatus["Active"] = 1] = "Active";
|
|
1332
|
-
ApprovalStepStatus[ApprovalStepStatus["Approved"] = 2] = "Approved";
|
|
1333
|
-
ApprovalStepStatus[ApprovalStepStatus["Rejected"] = 3] = "Rejected";
|
|
1334
|
-
ApprovalStepStatus[ApprovalStepStatus["Delegated"] = 4] = "Delegated";
|
|
1335
|
-
ApprovalStepStatus[ApprovalStepStatus["Expired"] = 5] = "Expired";
|
|
1336
|
-
ApprovalStepStatus[ApprovalStepStatus["Skipped"] = 6] = "Skipped";
|
|
1337
|
-
})(ApprovalStepStatus || (ApprovalStepStatus = {}));
|
|
1338
|
-
var ApprovalActionType;
|
|
1339
|
-
(function (ApprovalActionType) {
|
|
1340
|
-
ApprovalActionType[ApprovalActionType["Created"] = 0] = "Created";
|
|
1341
|
-
ApprovalActionType[ApprovalActionType["Submitted"] = 1] = "Submitted";
|
|
1342
|
-
ApprovalActionType[ApprovalActionType["Approved"] = 2] = "Approved";
|
|
1343
|
-
ApprovalActionType[ApprovalActionType["Rejected"] = 3] = "Rejected";
|
|
1344
|
-
ApprovalActionType[ApprovalActionType["Delegated"] = 4] = "Delegated";
|
|
1345
|
-
ApprovalActionType[ApprovalActionType["Cancelled"] = 5] = "Cancelled";
|
|
1346
|
-
ApprovalActionType[ApprovalActionType["Commented"] = 6] = "Commented";
|
|
1347
|
-
ApprovalActionType[ApprovalActionType["Expired"] = 7] = "Expired";
|
|
1348
|
-
ApprovalActionType[ApprovalActionType["StepAdvanced"] = 8] = "StepAdvanced";
|
|
1349
|
-
})(ApprovalActionType || (ApprovalActionType = {}));
|
|
1325
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiService, decorators: [{
|
|
1326
|
+
type: Injectable,
|
|
1327
|
+
args: [{ providedIn: 'root' }]
|
|
1328
|
+
}] });
|
|
1350
1329
|
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1330
|
+
/**
|
|
1331
|
+
* Modal editor for the signed-in user's AI "lessons" — long-lived instructions
|
|
1332
|
+
* injected into the LLM system prompt every turn. Opened from the user profile
|
|
1333
|
+
* dropdown via a dedicated button.
|
|
1334
|
+
*
|
|
1335
|
+
* Library-only styling (no @angular/forms — we use [value] + (input) per
|
|
1336
|
+
* mesauth-angular conventions).
|
|
1337
|
+
*/
|
|
1338
|
+
class MaAiLessonsEditorComponent {
|
|
1339
|
+
ai = inject(MaAiService);
|
|
1340
|
+
themeService = inject(ThemeService);
|
|
1341
|
+
lessons = signal([], ...(ngDevMode ? [{ debugName: "lessons" }] : /* istanbul ignore next */ []));
|
|
1354
1342
|
loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
constructor() {
|
|
1364
|
-
const config = this.mesAuth.getConfig();
|
|
1365
|
-
if (config) {
|
|
1366
|
-
this.approvalSvc = new MaApprovalService();
|
|
1367
|
-
this.approvalSvc.init(config, this.http);
|
|
1368
|
-
}
|
|
1369
|
-
const approvalEvent = toSignal(this.mesAuth.approvalEvents$);
|
|
1370
|
-
effect(() => {
|
|
1371
|
-
approvalEvent(); // track SignalR approval events
|
|
1372
|
-
if (this.isOpen())
|
|
1373
|
-
this.loadCurrentTab();
|
|
1374
|
-
});
|
|
1375
|
-
}
|
|
1376
|
-
open() {
|
|
1377
|
-
this.isOpen.set(true);
|
|
1378
|
-
this.loadAllTabs();
|
|
1379
|
-
}
|
|
1380
|
-
close() {
|
|
1381
|
-
this.isOpen.set(false);
|
|
1382
|
-
}
|
|
1383
|
-
toggle() {
|
|
1384
|
-
if (this.isOpen())
|
|
1385
|
-
this.close();
|
|
1386
|
-
else
|
|
1387
|
-
this.open();
|
|
1388
|
-
}
|
|
1389
|
-
switchTab(tab) {
|
|
1390
|
-
this.activeTab.set(tab);
|
|
1391
|
-
this.loadCurrentTab();
|
|
1343
|
+
saving = signal(false, ...(ngDevMode ? [{ debugName: "saving" }] : /* istanbul ignore next */ []));
|
|
1344
|
+
draft = signal('', ...(ngDevMode ? [{ debugName: "draft" }] : /* istanbul ignore next */ []));
|
|
1345
|
+
error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
1346
|
+
cancel = output();
|
|
1347
|
+
// Tracks the latest in-progress text edit per lesson so blur commits the right value.
|
|
1348
|
+
pendingText = new Map();
|
|
1349
|
+
get themeClass() {
|
|
1350
|
+
return `theme-${this.themeService.currentTheme()}`;
|
|
1392
1351
|
}
|
|
1393
|
-
|
|
1352
|
+
async ngOnInit() {
|
|
1394
1353
|
this.loading.set(true);
|
|
1395
|
-
|
|
1354
|
+
try {
|
|
1355
|
+
const list = await this.ai.listLessons();
|
|
1356
|
+
this.lessons.set(list ?? []);
|
|
1357
|
+
}
|
|
1358
|
+
finally {
|
|
1396
1359
|
this.loading.set(false);
|
|
1397
|
-
return;
|
|
1398
1360
|
}
|
|
1399
|
-
let pending = 3;
|
|
1400
|
-
const done = () => { if (--pending === 0)
|
|
1401
|
-
this.loading.set(false); };
|
|
1402
|
-
this.approvalSvc.getPendingApprovals(1, 100).subscribe({
|
|
1403
|
-
next: r => { this.processingItems.set(r.items); done(); },
|
|
1404
|
-
error: () => done()
|
|
1405
|
-
});
|
|
1406
|
-
this.approvalSvc.getMyRequests(1, 10, ApprovalDocumentStatus.Approved).subscribe({
|
|
1407
|
-
next: r => { this.approvedItems.set(r.items); done(); },
|
|
1408
|
-
error: () => done()
|
|
1409
|
-
});
|
|
1410
|
-
this.approvalSvc.getMyRequests(1, 10, ApprovalDocumentStatus.Rejected).subscribe({
|
|
1411
|
-
next: r => { this.rejectedItems.set(r.items); done(); },
|
|
1412
|
-
error: () => done()
|
|
1413
|
-
});
|
|
1414
1361
|
}
|
|
1415
|
-
|
|
1416
|
-
if (
|
|
1362
|
+
onBackdropClick(ev) {
|
|
1363
|
+
if (ev.target === ev.currentTarget)
|
|
1364
|
+
this.cancel.emit();
|
|
1365
|
+
}
|
|
1366
|
+
onDraftInput(ev) {
|
|
1367
|
+
this.draft.set(ev.target.value);
|
|
1368
|
+
}
|
|
1369
|
+
async add() {
|
|
1370
|
+
const text = this.draft().trim();
|
|
1371
|
+
if (!text)
|
|
1417
1372
|
return;
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
});
|
|
1373
|
+
this.saving.set(true);
|
|
1374
|
+
this.error.set(null);
|
|
1375
|
+
try {
|
|
1376
|
+
const created = await this.ai.createLesson(text);
|
|
1377
|
+
if (!created) {
|
|
1378
|
+
this.error.set('Failed to save preference.');
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
this.lessons.update(arr => [created, ...arr]);
|
|
1382
|
+
this.draft.set('');
|
|
1429
1383
|
}
|
|
1430
|
-
|
|
1431
|
-
this.
|
|
1432
|
-
next: r => this.rejectedItems.set(r.items),
|
|
1433
|
-
error: () => { }
|
|
1434
|
-
});
|
|
1384
|
+
finally {
|
|
1385
|
+
this.saving.set(false);
|
|
1435
1386
|
}
|
|
1436
1387
|
}
|
|
1437
|
-
|
|
1438
|
-
this.
|
|
1439
|
-
this.router.navigate(['/auth/approval/detail', id]);
|
|
1440
|
-
this.approvalActioned.emit();
|
|
1441
|
-
}
|
|
1442
|
-
showMore(status) {
|
|
1443
|
-
this.close();
|
|
1444
|
-
this.router.navigate(['/auth/approval/my-requests'], { queryParams: { status } });
|
|
1388
|
+
onTextInput(lesson, ev) {
|
|
1389
|
+
this.pendingText.set(lesson.id, ev.target.value);
|
|
1445
1390
|
}
|
|
1446
|
-
|
|
1447
|
-
|
|
1391
|
+
async commitText(lesson) {
|
|
1392
|
+
const next = this.pendingText.get(lesson.id);
|
|
1393
|
+
this.pendingText.delete(lesson.id);
|
|
1394
|
+
if (next === undefined)
|
|
1395
|
+
return;
|
|
1396
|
+
const trimmed = next.trim();
|
|
1397
|
+
if (!trimmed || trimmed === lesson.text)
|
|
1398
|
+
return;
|
|
1399
|
+
const updated = await this.ai.updateLesson(lesson.id, { text: trimmed });
|
|
1400
|
+
if (updated) {
|
|
1401
|
+
this.lessons.update(arr => arr.map(l => l.id === lesson.id ? updated : l));
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
async toggle(lesson, ev) {
|
|
1405
|
+
const enabled = ev.target.checked;
|
|
1406
|
+
const updated = await this.ai.updateLesson(lesson.id, { enabled });
|
|
1407
|
+
if (updated) {
|
|
1408
|
+
this.lessons.update(arr => arr.map(l => l.id === lesson.id ? updated : l));
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
async remove(lesson) {
|
|
1412
|
+
const ok = await this.ai.deleteLesson(lesson.id);
|
|
1413
|
+
if (ok) {
|
|
1414
|
+
this.lessons.update(arr => arr.filter(l => l.id !== lesson.id));
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiLessonsEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1418
|
+
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: `
|
|
1419
|
+
<div class="lessons-backdrop" (click)="onBackdropClick($event)">
|
|
1420
|
+
<div class="lessons-modal" role="dialog" aria-modal="true" aria-labelledby="lessonsTitle">
|
|
1421
|
+
<div class="lessons-head">
|
|
1422
|
+
<div>
|
|
1423
|
+
<h3 id="lessonsTitle">AI preferences</h3>
|
|
1424
|
+
<p class="lessons-sub">
|
|
1425
|
+
Long-lived instructions the AI will follow in every conversation.
|
|
1426
|
+
Each row is injected as a bullet point into the system prompt.
|
|
1427
|
+
</p>
|
|
1428
|
+
</div>
|
|
1429
|
+
<button class="lessons-close" (click)="cancel.emit()" aria-label="Close">
|
|
1430
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24"
|
|
1431
|
+
fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
1432
|
+
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
|
1433
|
+
</svg>
|
|
1434
|
+
</button>
|
|
1435
|
+
</div>
|
|
1436
|
+
|
|
1437
|
+
<div class="lessons-body">
|
|
1438
|
+
@if (loading()) {
|
|
1439
|
+
<div class="lessons-empty">Loading…</div>
|
|
1440
|
+
} @else if (lessons().length === 0) {
|
|
1441
|
+
<div class="lessons-empty">No preferences yet. Add one below or ask the AI to "remember" something.</div>
|
|
1442
|
+
} @else {
|
|
1443
|
+
@for (l of lessons(); track l.id) {
|
|
1444
|
+
<div class="lesson-row" [class.disabled]="!l.enabled">
|
|
1445
|
+
<label class="lesson-toggle" [title]="l.enabled ? 'Disable' : 'Enable'">
|
|
1446
|
+
<input type="checkbox" [checked]="l.enabled" (change)="toggle(l, $event)" />
|
|
1447
|
+
<span class="lesson-toggle-slider"></span>
|
|
1448
|
+
</label>
|
|
1449
|
+
<textarea class="lesson-text"
|
|
1450
|
+
rows="2"
|
|
1451
|
+
[value]="l.text"
|
|
1452
|
+
(input)="onTextInput(l, $event)"
|
|
1453
|
+
(blur)="commitText(l)"></textarea>
|
|
1454
|
+
<span class="lesson-source" [class.llm]="l.source === 'llm'">
|
|
1455
|
+
{{ l.source === 'llm' ? 'AI' : 'You' }}
|
|
1456
|
+
</span>
|
|
1457
|
+
<button class="lesson-del" (click)="remove(l)" title="Delete" aria-label="Delete">
|
|
1458
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24"
|
|
1459
|
+
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1460
|
+
<polyline points="3 6 5 6 21 6"/>
|
|
1461
|
+
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
|
|
1462
|
+
</svg>
|
|
1463
|
+
</button>
|
|
1464
|
+
</div>
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
</div>
|
|
1468
|
+
|
|
1469
|
+
<div class="lessons-add">
|
|
1470
|
+
<textarea class="lesson-text"
|
|
1471
|
+
rows="2"
|
|
1472
|
+
placeholder="e.g. Always reply in Vietnamese."
|
|
1473
|
+
[value]="draft()"
|
|
1474
|
+
(input)="onDraftInput($event)"></textarea>
|
|
1475
|
+
<button class="lesson-add-btn" (click)="add()" [disabled]="!draft().trim() || saving()">
|
|
1476
|
+
Add
|
|
1477
|
+
</button>
|
|
1478
|
+
</div>
|
|
1479
|
+
|
|
1480
|
+
@if (error()) { <div class="lesson-error">{{ error() }}</div> }
|
|
1481
|
+
</div>
|
|
1482
|
+
</div>
|
|
1483
|
+
`, 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 });
|
|
1448
1484
|
}
|
|
1449
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type:
|
|
1485
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiLessonsEditorComponent, decorators: [{
|
|
1450
1486
|
type: Component,
|
|
1451
|
-
args: [{ selector: 'ma-
|
|
1452
|
-
|
|
1487
|
+
args: [{ selector: 'ma-ai-lessons-editor', changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
1488
|
+
<div class="lessons-backdrop" (click)="onBackdropClick($event)">
|
|
1489
|
+
<div class="lessons-modal" role="dialog" aria-modal="true" aria-labelledby="lessonsTitle">
|
|
1490
|
+
<div class="lessons-head">
|
|
1491
|
+
<div>
|
|
1492
|
+
<h3 id="lessonsTitle">AI preferences</h3>
|
|
1493
|
+
<p class="lessons-sub">
|
|
1494
|
+
Long-lived instructions the AI will follow in every conversation.
|
|
1495
|
+
Each row is injected as a bullet point into the system prompt.
|
|
1496
|
+
</p>
|
|
1497
|
+
</div>
|
|
1498
|
+
<button class="lessons-close" (click)="cancel.emit()" aria-label="Close">
|
|
1499
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24"
|
|
1500
|
+
fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
1501
|
+
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
|
1502
|
+
</svg>
|
|
1503
|
+
</button>
|
|
1504
|
+
</div>
|
|
1453
1505
|
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1506
|
+
<div class="lessons-body">
|
|
1507
|
+
@if (loading()) {
|
|
1508
|
+
<div class="lessons-empty">Loading…</div>
|
|
1509
|
+
} @else if (lessons().length === 0) {
|
|
1510
|
+
<div class="lessons-empty">No preferences yet. Add one below or ask the AI to "remember" something.</div>
|
|
1511
|
+
} @else {
|
|
1512
|
+
@for (l of lessons(); track l.id) {
|
|
1513
|
+
<div class="lesson-row" [class.disabled]="!l.enabled">
|
|
1514
|
+
<label class="lesson-toggle" [title]="l.enabled ? 'Disable' : 'Enable'">
|
|
1515
|
+
<input type="checkbox" [checked]="l.enabled" (change)="toggle(l, $event)" />
|
|
1516
|
+
<span class="lesson-toggle-slider"></span>
|
|
1517
|
+
</label>
|
|
1518
|
+
<textarea class="lesson-text"
|
|
1519
|
+
rows="2"
|
|
1520
|
+
[value]="l.text"
|
|
1521
|
+
(input)="onTextInput(l, $event)"
|
|
1522
|
+
(blur)="commitText(l)"></textarea>
|
|
1523
|
+
<span class="lesson-source" [class.llm]="l.source === 'llm'">
|
|
1524
|
+
{{ l.source === 'llm' ? 'AI' : 'You' }}
|
|
1525
|
+
</span>
|
|
1526
|
+
<button class="lesson-del" (click)="remove(l)" title="Delete" aria-label="Delete">
|
|
1527
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24"
|
|
1528
|
+
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1529
|
+
<polyline points="3 6 5 6 21 6"/>
|
|
1530
|
+
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
|
|
1531
|
+
</svg>
|
|
1532
|
+
</button>
|
|
1533
|
+
</div>
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
</div>
|
|
1537
|
+
|
|
1538
|
+
<div class="lessons-add">
|
|
1539
|
+
<textarea class="lesson-text"
|
|
1540
|
+
rows="2"
|
|
1541
|
+
placeholder="e.g. Always reply in Vietnamese."
|
|
1542
|
+
[value]="draft()"
|
|
1543
|
+
(input)="onDraftInput($event)"></textarea>
|
|
1544
|
+
<button class="lesson-add-btn" (click)="add()" [disabled]="!draft().trim() || saving()">
|
|
1545
|
+
Add
|
|
1546
|
+
</button>
|
|
1547
|
+
</div>
|
|
1548
|
+
|
|
1549
|
+
@if (error()) { <div class="lesson-error">{{ error() }}</div> }
|
|
1550
|
+
</div>
|
|
1551
|
+
</div>
|
|
1552
|
+
`, 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"] }]
|
|
1553
|
+
}], propDecorators: { cancel: [{ type: i0.Output, args: ["cancel"] }], themeClass: [{
|
|
1554
|
+
type: HostBinding,
|
|
1555
|
+
args: ['class']
|
|
1556
|
+
}] } });
|
|
1557
|
+
|
|
1558
|
+
class UserProfileComponent {
|
|
1559
|
+
inputAvatarShape = input('circle', ...(ngDevMode ? [{ debugName: "inputAvatarShape" }] : /* istanbul ignore next */ []));
|
|
1560
|
+
showBell = input(true, ...(ngDevMode ? [{ debugName: "showBell" }] : /* istanbul ignore next */ []));
|
|
1561
|
+
showApproval = input(true, ...(ngDevMode ? [{ debugName: "showApproval" }] : /* istanbul ignore next */ []));
|
|
1562
|
+
showName = input(false, ...(ngDevMode ? [{ debugName: "showName" }] : /* istanbul ignore next */ []));
|
|
1563
|
+
showAi = input(true, ...(ngDevMode ? [{ debugName: "showAi" }] : /* istanbul ignore next */ []));
|
|
1564
|
+
showSignature = input([false, false], ...(ngDevMode ? [{ debugName: "showSignature" }] : /* istanbul ignore next */ []));
|
|
1565
|
+
signatureHeight = input(40, ...(ngDevMode ? [{ debugName: "signatureHeight" }] : /* istanbul ignore next */ []));
|
|
1566
|
+
notificationClick = output();
|
|
1567
|
+
approvalClick = output();
|
|
1568
|
+
aiClick = output();
|
|
1569
|
+
get themeClass() {
|
|
1570
|
+
return `theme-${this.themeService.currentTheme()}`;
|
|
1571
|
+
}
|
|
1572
|
+
currentUser = signal(null, ...(ngDevMode ? [{ debugName: "currentUser" }] : /* istanbul ignore next */ []));
|
|
1573
|
+
unreadCount = signal(0, ...(ngDevMode ? [{ debugName: "unreadCount" }] : /* istanbul ignore next */ []));
|
|
1574
|
+
pendingApprovalCount = signal(0, ...(ngDevMode ? [{ debugName: "pendingApprovalCount" }] : /* istanbul ignore next */ []));
|
|
1575
|
+
dropdownOpen = signal(false, ...(ngDevMode ? [{ debugName: "dropdownOpen" }] : /* istanbul ignore next */ []));
|
|
1576
|
+
avatarRefresh = signal(Date.now(), ...(ngDevMode ? [{ debugName: "avatarRefresh" }] : /* istanbul ignore next */ []));
|
|
1577
|
+
lessonsOpen = signal(false, ...(ngDevMode ? [{ debugName: "lessonsOpen" }] : /* istanbul ignore next */ []));
|
|
1578
|
+
// Avatar style derived from per-user preferences
|
|
1579
|
+
navAvatarSize = computed(() => this.currentUser()?.avatarSize ?? 'md', ...(ngDevMode ? [{ debugName: "navAvatarSize" }] : /* istanbul ignore next */ []));
|
|
1580
|
+
// Dropdown is always one step larger than the nav avatar
|
|
1581
|
+
dropAvatarSize = computed(() => {
|
|
1582
|
+
const s = this.navAvatarSize();
|
|
1583
|
+
return s === 'sm' ? 'md' : s === 'md' ? 'lg' : 'xl';
|
|
1584
|
+
}, ...(ngDevMode ? [{ debugName: "dropAvatarSize" }] : /* istanbul ignore next */ []));
|
|
1585
|
+
avatarShape = computed(() => this.currentUser()?.avatarShape ?? this.inputAvatarShape(), ...(ngDevMode ? [{ debugName: "avatarShape" }] : /* istanbul ignore next */ []));
|
|
1586
|
+
avatarFrame = computed(() => this.currentUser()?.avatarFrame ?? null, ...(ngDevMode ? [{ debugName: "avatarFrame" }] : /* istanbul ignore next */ []));
|
|
1587
|
+
avatarRatio = computed(() => this.currentUser()?.avatarRatio ?? 'ar-11', ...(ngDevMode ? [{ debugName: "avatarRatio" }] : /* istanbul ignore next */ []));
|
|
1588
|
+
givenStyle = computed(() => this.currentUser()?.givenColor || 'indigo', ...(ngDevMode ? [{ debugName: "givenStyle" }] : /* istanbul ignore next */ []));
|
|
1589
|
+
signatureBroken = signal(false, ...(ngDevMode ? [{ debugName: "signatureBroken" }] : /* istanbul ignore next */ []));
|
|
1590
|
+
signatureUrl = computed(() => {
|
|
1591
|
+
if (!this.showSignature().some(s => s) || this.signatureBroken())
|
|
1592
|
+
return null;
|
|
1593
|
+
const baseRaw = this.authService.getConfig()?.apiBaseUrl ?? '';
|
|
1594
|
+
const base = baseRaw.replace(/\/$/, '');
|
|
1595
|
+
const u = this.currentUser();
|
|
1596
|
+
const id = u?.userId ?? u?.id;
|
|
1597
|
+
if (!id || !base)
|
|
1598
|
+
return null;
|
|
1599
|
+
return `${base}/auth/${id}/signature`;
|
|
1600
|
+
}, ...(ngDevMode ? [{ debugName: "signatureUrl" }] : /* istanbul ignore next */ []));
|
|
1601
|
+
authService = inject(MesAuthService);
|
|
1602
|
+
router = inject(Router);
|
|
1603
|
+
themeService = inject(ThemeService);
|
|
1604
|
+
http = inject(HttpClient);
|
|
1605
|
+
constructor() {
|
|
1606
|
+
const currentUserSig = toSignal(this.authService.currentUser$, { initialValue: null });
|
|
1607
|
+
const approvalEvent = toSignal(this.authService.approvalEvents$);
|
|
1608
|
+
const notification = toSignal(this.authService.notifications$);
|
|
1609
|
+
effect(() => {
|
|
1610
|
+
const user = currentUserSig();
|
|
1611
|
+
this.currentUser.set(user);
|
|
1612
|
+
this.avatarRefresh.set(Date.now());
|
|
1613
|
+
if (!user) {
|
|
1614
|
+
this.unreadCount.set(0);
|
|
1615
|
+
this.pendingApprovalCount.set(0);
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
this.loadUnreadCount();
|
|
1619
|
+
this.loadPendingApprovalCount();
|
|
1620
|
+
});
|
|
1621
|
+
effect(() => {
|
|
1622
|
+
approvalEvent(); // track SignalR approval events
|
|
1623
|
+
if (currentUserSig())
|
|
1624
|
+
this.loadPendingApprovalCount();
|
|
1625
|
+
});
|
|
1626
|
+
effect(() => {
|
|
1627
|
+
notification(); // track SignalR notification events
|
|
1628
|
+
if (currentUserSig())
|
|
1629
|
+
this.loadUnreadCount();
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
loadUnreadCount() {
|
|
1633
|
+
this.authService.getUnreadCount().subscribe({
|
|
1634
|
+
next: (response) => this.unreadCount.set(response.unreadCount || 0),
|
|
1635
|
+
error: () => { }
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
loadPendingApprovalCount() {
|
|
1639
|
+
const config = this.authService.getConfig();
|
|
1640
|
+
if (!config)
|
|
1641
|
+
return;
|
|
1642
|
+
const url = `${config.apiBaseUrl.replace(/\/$/, '')}/approval/dashboard`;
|
|
1643
|
+
this.http.get(url, { withCredentials: config.withCredentials ?? true }).subscribe({
|
|
1644
|
+
next: (r) => this.pendingApprovalCount.set(r?.pendingCount ?? 0),
|
|
1645
|
+
error: () => { }
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
onApprovalClick() {
|
|
1649
|
+
this.approvalClick.emit();
|
|
1650
|
+
}
|
|
1651
|
+
onAiClick() {
|
|
1652
|
+
this.aiClick.emit();
|
|
1653
|
+
}
|
|
1654
|
+
getAvatarUrl(user) {
|
|
1655
|
+
// Use the refresh signal to force update
|
|
1656
|
+
const refresh = this.avatarRefresh();
|
|
1657
|
+
const config = this.authService.getConfig();
|
|
1658
|
+
const baseUrl = config?.apiBaseUrl || '';
|
|
1659
|
+
if (user.avatarPath) {
|
|
1660
|
+
if (user.avatarPath.startsWith('http://') || user.avatarPath.startsWith('https://')) {
|
|
1661
|
+
return user.avatarPath;
|
|
1662
|
+
}
|
|
1663
|
+
return `${baseUrl.replace(/\/$/, '')}${user.avatarPath}?t=${refresh}`;
|
|
1664
|
+
}
|
|
1665
|
+
const userId = user.userId;
|
|
1666
|
+
if (userId && baseUrl) {
|
|
1667
|
+
return `${baseUrl.replace(/\/$/, '')}/auth/${userId}/avatar?t=${refresh}`;
|
|
1668
|
+
}
|
|
1669
|
+
const displayName = user.userName || user.userId || 'User';
|
|
1670
|
+
return `https://ui-avatars.com/api/?name=${encodeURIComponent(displayName)}&background=1976d2&color=fff`;
|
|
1671
|
+
}
|
|
1672
|
+
getLastNameInitial(user) {
|
|
1673
|
+
const fullName = user.fullName || user.userName || 'U';
|
|
1674
|
+
const parts = fullName.split(' ');
|
|
1675
|
+
const lastPart = parts[parts.length - 1];
|
|
1676
|
+
return lastPart.charAt(0).toUpperCase();
|
|
1677
|
+
}
|
|
1678
|
+
toggleDropdown() {
|
|
1679
|
+
this.dropdownOpen.set(!this.dropdownOpen());
|
|
1680
|
+
}
|
|
1681
|
+
closeDropdown() {
|
|
1682
|
+
this.dropdownOpen.set(false);
|
|
1683
|
+
}
|
|
1684
|
+
onDocumentClick(event) {
|
|
1685
|
+
const target = event.target;
|
|
1686
|
+
const clickedInside = target.closest('.user-menu-wrapper');
|
|
1687
|
+
if (!clickedInside) {
|
|
1688
|
+
this.dropdownOpen.set(false);
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
if (target.closest('.ma-user-menu-btn')) {
|
|
1692
|
+
this.dropdownOpen.set(false);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
onLogin() {
|
|
1696
|
+
const config = this.authService.getConfig();
|
|
1697
|
+
const baseUrl = config?.userBaseUrl || '';
|
|
1698
|
+
const returnUrl = encodeURIComponent(this.router.url);
|
|
1699
|
+
window.location.href = `${baseUrl}/login?returnUrl=${returnUrl}`;
|
|
1700
|
+
}
|
|
1701
|
+
onViewProfile() {
|
|
1702
|
+
const config = this.authService.getConfig();
|
|
1703
|
+
const baseUrl = config?.userBaseUrl || '';
|
|
1704
|
+
const currentUrl = window.location.href;
|
|
1705
|
+
this.openInNewTabIfSameOrigin(currentUrl, `${baseUrl}/profile`);
|
|
1706
|
+
this.dropdownOpen.set(false);
|
|
1707
|
+
}
|
|
1708
|
+
openInNewTabIfSameOrigin(currentUrl, destinationUrl) {
|
|
1709
|
+
// Check if current page URL starts with the destination URL
|
|
1710
|
+
if (!destinationUrl.startsWith(currentUrl)) {
|
|
1711
|
+
window.open(destinationUrl, "_blank", "noopener,noreferrer");
|
|
1712
|
+
}
|
|
1713
|
+
else {
|
|
1714
|
+
// Optional: redirect in same tab
|
|
1715
|
+
window.location.href = destinationUrl;
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
onLogout() {
|
|
1719
|
+
this.authService.logout().subscribe({
|
|
1720
|
+
next: () => {
|
|
1721
|
+
this.dropdownOpen.set(false);
|
|
1722
|
+
const config = this.authService.getConfig();
|
|
1723
|
+
const baseUrl = config?.userBaseUrl || '';
|
|
1724
|
+
const returnUrl = encodeURIComponent(window.location.href);
|
|
1725
|
+
window.location.href = `${baseUrl}/login?returnUrl=${returnUrl}`;
|
|
1726
|
+
},
|
|
1727
|
+
error: () => {
|
|
1728
|
+
const config = this.authService.getConfig();
|
|
1729
|
+
const baseUrl = config?.userBaseUrl || '';
|
|
1730
|
+
window.location.href = `${baseUrl}/login`;
|
|
1731
|
+
}
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
onNotificationClick() {
|
|
1735
|
+
this.notificationClick.emit();
|
|
1736
|
+
}
|
|
1737
|
+
openLessons() {
|
|
1738
|
+
this.lessonsOpen.set(true);
|
|
1739
|
+
this.dropdownOpen.set(false);
|
|
1740
|
+
}
|
|
1741
|
+
closeLessons() {
|
|
1742
|
+
this.lessonsOpen.set(false);
|
|
1743
|
+
}
|
|
1744
|
+
onSigErr(_ev) {
|
|
1745
|
+
this.signatureBroken.set(true);
|
|
1746
|
+
}
|
|
1747
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: UserProfileComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1748
|
+
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"] }] });
|
|
1749
|
+
}
|
|
1750
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: UserProfileComponent, decorators: [{
|
|
1751
|
+
type: Component,
|
|
1752
|
+
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"] }]
|
|
1753
|
+
}], 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: [{
|
|
1754
|
+
type: HostBinding,
|
|
1755
|
+
args: ['class']
|
|
1756
|
+
}], onDocumentClick: [{
|
|
1757
|
+
type: HostListener,
|
|
1758
|
+
args: ['document:click', ['$event']]
|
|
1759
|
+
}] } });
|
|
1760
|
+
|
|
1761
|
+
class ToastContainerComponent {
|
|
1762
|
+
get themeClass() {
|
|
1763
|
+
return `theme-${this.themeService.currentTheme()}`;
|
|
1764
|
+
}
|
|
1765
|
+
toastService = inject(ToastService);
|
|
1766
|
+
themeService = inject(ThemeService);
|
|
1767
|
+
toasts = this.toastService.toasts;
|
|
1768
|
+
close(id) {
|
|
1769
|
+
this.toastService.remove(id);
|
|
1770
|
+
}
|
|
1771
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: ToastContainerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1772
|
+
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"] });
|
|
1773
|
+
}
|
|
1774
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: ToastContainerComponent, decorators: [{
|
|
1775
|
+
type: Component,
|
|
1776
|
+
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"] }]
|
|
1777
|
+
}], propDecorators: { themeClass: [{
|
|
1778
|
+
type: HostBinding,
|
|
1779
|
+
args: ['class']
|
|
1780
|
+
}] } });
|
|
1781
|
+
|
|
1782
|
+
class NotificationPanelComponent {
|
|
1783
|
+
notificationRead = output();
|
|
1784
|
+
get themeClass() {
|
|
1785
|
+
return `theme-${this.themeService.currentTheme()}`;
|
|
1786
|
+
}
|
|
1787
|
+
isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
|
|
1788
|
+
activeTab = signal('unread', ...(ngDevMode ? [{ debugName: "activeTab" }] : /* istanbul ignore next */ []));
|
|
1789
|
+
notifications = signal([], ...(ngDevMode ? [{ debugName: "notifications" }] : /* istanbul ignore next */ []));
|
|
1790
|
+
selectedNotification = signal(null, ...(ngDevMode ? [{ debugName: "selectedNotification" }] : /* istanbul ignore next */ []));
|
|
1791
|
+
selectedNotificationHtml = signal(null, ...(ngDevMode ? [{ debugName: "selectedNotificationHtml" }] : /* istanbul ignore next */ []));
|
|
1792
|
+
selectedNotificationDate = signal('', ...(ngDevMode ? [{ debugName: "selectedNotificationDate" }] : /* istanbul ignore next */ []));
|
|
1793
|
+
// Stable time-ago strings keyed by notification id - refreshed every 30s via signal
|
|
1794
|
+
dateLabels = signal(new Map(), ...(ngDevMode ? [{ debugName: "dateLabels" }] : /* istanbul ignore next */ []));
|
|
1795
|
+
unreadNotifications = computed(() => this.notifications().filter(n => !n.isRead), ...(ngDevMode ? [{ debugName: "unreadNotifications" }] : /* istanbul ignore next */ []));
|
|
1796
|
+
readNotifications = computed(() => this.notifications().filter(n => n.isRead), ...(ngDevMode ? [{ debugName: "readNotifications" }] : /* istanbul ignore next */ []));
|
|
1797
|
+
currentNotifications = computed(() => this.activeTab() === 'unread' ? this.unreadNotifications() : this.readNotifications(), ...(ngDevMode ? [{ debugName: "currentNotifications" }] : /* istanbul ignore next */ []));
|
|
1798
|
+
dateTimer = null;
|
|
1799
|
+
// Normalize type to string - API may return integer or string
|
|
1800
|
+
// Backend enum: Info=0, Success=1, Warning=2, Error=3
|
|
1801
|
+
typeOf(notification) {
|
|
1802
|
+
const t = notification.type;
|
|
1803
|
+
if (t === 0 || t === 'Info')
|
|
1804
|
+
return 'Info';
|
|
1805
|
+
if (t === 1 || t === 'Success')
|
|
1806
|
+
return 'Success';
|
|
1807
|
+
if (t === 2 || t === 'Warning')
|
|
1808
|
+
return 'Warning';
|
|
1809
|
+
if (t === 3 || t === 'Error')
|
|
1810
|
+
return 'Error';
|
|
1811
|
+
return 'Info';
|
|
1812
|
+
}
|
|
1813
|
+
toastType(type) {
|
|
1814
|
+
const t = this.typeOf({ type });
|
|
1815
|
+
return t.toLowerCase();
|
|
1816
|
+
}
|
|
1817
|
+
sanitizer = inject(DomSanitizer);
|
|
1818
|
+
authService = inject(MesAuthService);
|
|
1819
|
+
toastService = inject(ToastService);
|
|
1820
|
+
themeService = inject(ThemeService);
|
|
1821
|
+
host = inject(ElementRef);
|
|
1822
|
+
constructor() {
|
|
1823
|
+
this.loadNotifications();
|
|
1824
|
+
// Refresh time-ago labels every 30s - signal mutation triggers CD in zoneless
|
|
1825
|
+
this.dateTimer = setInterval(() => this.refreshDateLabels(), 30000);
|
|
1826
|
+
const latestNotification = toSignal(this.authService.notifications$);
|
|
1827
|
+
effect(() => {
|
|
1828
|
+
const notification = latestNotification();
|
|
1829
|
+
if (!notification)
|
|
1830
|
+
return;
|
|
1831
|
+
this.toastService.show(notification.message || '', notification.title, this.toastType(notification.type), 5000);
|
|
1832
|
+
this.loadNotifications();
|
|
1833
|
+
});
|
|
1834
|
+
// Close when the user clicks outside the panel - matches ma-approval-panel UX.
|
|
1835
|
+
effect((onCleanup) => {
|
|
1836
|
+
if (!this.isOpen())
|
|
1837
|
+
return;
|
|
1838
|
+
const onDocClick = (ev) => {
|
|
1839
|
+
const panel = this.host.nativeElement.querySelector('.notification-panel');
|
|
1840
|
+
if (panel && !panel.contains(ev.target))
|
|
1841
|
+
this.close();
|
|
1842
|
+
};
|
|
1843
|
+
// Defer to skip the same click that opened the panel.
|
|
1844
|
+
const tid = setTimeout(() => document.addEventListener('mousedown', onDocClick), 0);
|
|
1845
|
+
onCleanup(() => {
|
|
1846
|
+
clearTimeout(tid);
|
|
1847
|
+
document.removeEventListener('mousedown', onDocClick);
|
|
1848
|
+
});
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
ngOnDestroy() {
|
|
1852
|
+
if (this.dateTimer !== null) {
|
|
1853
|
+
clearInterval(this.dateTimer);
|
|
1854
|
+
this.dateTimer = null;
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
loadNotifications() {
|
|
1858
|
+
this.authService.getNotifications(1, 50, true).subscribe({
|
|
1859
|
+
next: (response) => {
|
|
1860
|
+
this.notifications.set(response.items || []);
|
|
1861
|
+
this.refreshDateLabels();
|
|
1473
1862
|
},
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
description: 'Show a small toast notification at the top of the screen.',
|
|
1498
|
-
parameters: {
|
|
1499
|
-
type: 'object',
|
|
1500
|
-
properties: {
|
|
1501
|
-
message: { type: 'string', description: 'Toast body text' },
|
|
1502
|
-
title: { type: 'string', description: 'Optional title' },
|
|
1503
|
-
severity: { type: 'string', enum: ['info', 'success', 'warning', 'error'], description: 'Toast severity' }
|
|
1863
|
+
error: () => { }
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
open() {
|
|
1867
|
+
this.isOpen.set(true);
|
|
1868
|
+
this.activeTab.set('unread');
|
|
1869
|
+
}
|
|
1870
|
+
close() {
|
|
1871
|
+
this.isOpen.set(false);
|
|
1872
|
+
}
|
|
1873
|
+
switchTab(tab) {
|
|
1874
|
+
this.activeTab.set(tab);
|
|
1875
|
+
}
|
|
1876
|
+
openDetails(notification) {
|
|
1877
|
+
this.selectedNotification.set(notification);
|
|
1878
|
+
const html = notification.messageHtml || notification.message || '';
|
|
1879
|
+
this.selectedNotificationHtml.set(this.sanitizer.bypassSecurityTrustHtml(html));
|
|
1880
|
+
this.selectedNotificationDate.set(this.formatDate(notification.createdAt));
|
|
1881
|
+
if (!notification.isRead) {
|
|
1882
|
+
this.authService.markAsRead(notification.id).subscribe({
|
|
1883
|
+
next: () => {
|
|
1884
|
+
this.notifications.update(notifs => notifs.map(n => n.id === notification.id ? { ...n, isRead: true } : n));
|
|
1885
|
+
this.notificationRead.emit();
|
|
1504
1886
|
},
|
|
1505
|
-
|
|
1887
|
+
error: () => { }
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
closeDetails() {
|
|
1892
|
+
this.selectedNotification.set(null);
|
|
1893
|
+
this.selectedNotificationHtml.set(null);
|
|
1894
|
+
this.selectedNotificationDate.set('');
|
|
1895
|
+
}
|
|
1896
|
+
openUrl() {
|
|
1897
|
+
const url = this.selectedNotification()?.url?.trim();
|
|
1898
|
+
if (url) {
|
|
1899
|
+
window.open(url, '_blank', 'noopener,noreferrer');
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
markAsRead(notificationId, event) {
|
|
1903
|
+
if (event)
|
|
1904
|
+
event.stopPropagation();
|
|
1905
|
+
this.authService.markAsRead(notificationId).subscribe({
|
|
1906
|
+
next: () => {
|
|
1907
|
+
this.notifications.update(notifs => notifs.map(n => n.id === notificationId ? { ...n, isRead: true } : n));
|
|
1908
|
+
this.notificationRead.emit();
|
|
1506
1909
|
},
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
{
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1910
|
+
error: () => { }
|
|
1911
|
+
});
|
|
1912
|
+
}
|
|
1913
|
+
markAllAsRead() {
|
|
1914
|
+
this.authService.markAllAsRead().subscribe({
|
|
1915
|
+
next: () => {
|
|
1916
|
+
this.notifications.update(notifs => notifs.map(n => ({ ...n, isRead: true })));
|
|
1917
|
+
this.notificationRead.emit();
|
|
1918
|
+
},
|
|
1919
|
+
error: () => { }
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
deleteAllRead() {
|
|
1923
|
+
const readIds = this.notifications().filter(n => n.isRead).map(n => n.id);
|
|
1924
|
+
const deletePromises = readIds.map(id => firstValueFrom(this.authService.deleteNotification(id)));
|
|
1925
|
+
Promise.all(deletePromises).then(() => {
|
|
1926
|
+
this.notifications.update(notifs => notifs.filter(n => !n.isRead));
|
|
1927
|
+
}).catch(() => {
|
|
1928
|
+
this.loadNotifications();
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
deleteAllUnread() {
|
|
1932
|
+
const unreadIds = this.notifications().filter(n => !n.isRead).map(n => n.id);
|
|
1933
|
+
const deletePromises = unreadIds.map(id => firstValueFrom(this.authService.deleteNotification(id)));
|
|
1934
|
+
Promise.all(deletePromises).then(() => {
|
|
1935
|
+
this.notifications.update(notifs => notifs.filter(n => n.isRead));
|
|
1936
|
+
this.notificationRead.emit();
|
|
1937
|
+
}).catch(() => {
|
|
1938
|
+
this.loadNotifications();
|
|
1939
|
+
});
|
|
1940
|
+
}
|
|
1941
|
+
delete(notificationId, event) {
|
|
1942
|
+
event.stopPropagation();
|
|
1943
|
+
const wasUnread = this.notifications().some(n => n.id === notificationId && !n.isRead);
|
|
1944
|
+
this.authService.deleteNotification(notificationId).subscribe({
|
|
1945
|
+
next: () => {
|
|
1946
|
+
this.notifications.update(notifs => notifs.filter(n => n.id !== notificationId));
|
|
1947
|
+
if (wasUnread)
|
|
1948
|
+
this.notificationRead.emit();
|
|
1949
|
+
},
|
|
1950
|
+
error: () => { }
|
|
1951
|
+
});
|
|
1952
|
+
}
|
|
1953
|
+
formatDate(dateString) {
|
|
1954
|
+
return this.computeTimeAgo(dateString, new Date());
|
|
1955
|
+
}
|
|
1956
|
+
// Pure computation - takes now as param so it never calls new Date() internally
|
|
1957
|
+
computeTimeAgo(dateString, now) {
|
|
1958
|
+
const normalizedDateString = this.parseUtcDate(dateString);
|
|
1959
|
+
const date = new Date(normalizedDateString);
|
|
1960
|
+
if (isNaN(date.getTime()))
|
|
1961
|
+
return 'Invalid date';
|
|
1962
|
+
const diffMs = now.getTime() - date.getTime();
|
|
1963
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
1964
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
1965
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
1966
|
+
if (diffMins < 1)
|
|
1967
|
+
return 'Now';
|
|
1968
|
+
if (diffMins < 60)
|
|
1969
|
+
return `${diffMins}m ago`;
|
|
1970
|
+
if (diffHours < 24)
|
|
1971
|
+
return `${diffHours}h ago`;
|
|
1972
|
+
if (diffDays < 7)
|
|
1973
|
+
return `${diffDays}d ago`;
|
|
1974
|
+
return date.toLocaleDateString();
|
|
1975
|
+
}
|
|
1976
|
+
// Rebuild dateLabels map and store as a new signal value so CD is triggered
|
|
1977
|
+
refreshDateLabels() {
|
|
1978
|
+
const now = new Date();
|
|
1979
|
+
const newLabels = new Map();
|
|
1980
|
+
for (const n of this.notifications()) {
|
|
1981
|
+
newLabels.set(n.id, this.computeTimeAgo(n.createdAt, now));
|
|
1543
1982
|
}
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1983
|
+
this.dateLabels.set(newLabels);
|
|
1984
|
+
}
|
|
1985
|
+
// Parse date string from server (stored in UTC but without 'Z' suffix or 'T' separator)
|
|
1986
|
+
parseUtcDate(dateStr) {
|
|
1987
|
+
let normalized = dateStr.includes('T') ? dateStr : dateStr.replace(' ', 'T');
|
|
1988
|
+
if (!normalized.endsWith('Z') && !normalized.includes('+') && !normalized.includes('-', 10)) {
|
|
1989
|
+
normalized += 'Z';
|
|
1990
|
+
}
|
|
1991
|
+
return normalized;
|
|
1992
|
+
}
|
|
1993
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: NotificationPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1994
|
+
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"] });
|
|
1995
|
+
}
|
|
1996
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: NotificationPanelComponent, decorators: [{
|
|
1997
|
+
type: Component,
|
|
1998
|
+
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"] }]
|
|
1999
|
+
}], ctorParameters: () => [], propDecorators: { notificationRead: [{ type: i0.Output, args: ["notificationRead"] }], themeClass: [{
|
|
2000
|
+
type: HostBinding,
|
|
2001
|
+
args: ['class']
|
|
2002
|
+
}] } });
|
|
2003
|
+
|
|
2004
|
+
class MaApprovalService {
|
|
2005
|
+
apiBase = '';
|
|
2006
|
+
http;
|
|
2007
|
+
config = null;
|
|
2008
|
+
constructor() { }
|
|
2009
|
+
init(config, httpClient) {
|
|
2010
|
+
this.config = config;
|
|
2011
|
+
this.http = httpClient;
|
|
2012
|
+
this.apiBase = config.apiBaseUrl.replace(/\/$/, '');
|
|
2013
|
+
}
|
|
2014
|
+
get opts() {
|
|
2015
|
+
return { withCredentials: this.config?.withCredentials ?? true };
|
|
2016
|
+
}
|
|
2017
|
+
// ====================== Dashboard ======================
|
|
2018
|
+
getDashboard() {
|
|
2019
|
+
return this.http.get(`${this.apiBase}/approval/dashboard`, this.opts);
|
|
2020
|
+
}
|
|
2021
|
+
// ====================== Pending & My Requests ======================
|
|
2022
|
+
getPendingApprovals(page = 1, pageSize = 20) {
|
|
2023
|
+
return this.http.get(`${this.apiBase}/approval/pending?page=${page}&pageSize=${pageSize}`, this.opts);
|
|
1554
2024
|
}
|
|
1555
|
-
|
|
1556
|
-
|
|
2025
|
+
getMyRequests(page = 1, pageSize = 20, status) {
|
|
2026
|
+
let url = `${this.apiBase}/approval/my-requests?page=${page}&pageSize=${pageSize}`;
|
|
2027
|
+
if (status !== undefined)
|
|
2028
|
+
url += `&status=${status}`;
|
|
2029
|
+
return this.http.get(url, this.opts);
|
|
1557
2030
|
}
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
if (!tool)
|
|
1562
|
-
return { ok: false, result: `Unknown client tool: ${name}` };
|
|
1563
|
-
try {
|
|
1564
|
-
const raw = await Promise.resolve(tool.handler(args ?? {}));
|
|
1565
|
-
const result = typeof raw === 'string' ? raw : JSON.stringify(raw ?? null);
|
|
1566
|
-
return { ok: true, result };
|
|
1567
|
-
}
|
|
1568
|
-
catch (err) {
|
|
1569
|
-
return { ok: false, result: err?.message ?? String(err) };
|
|
1570
|
-
}
|
|
2031
|
+
// ====================== Document ======================
|
|
2032
|
+
getDocument(id) {
|
|
2033
|
+
return this.http.get(`${this.apiBase}/approval/documents/${id}`, this.opts);
|
|
1571
2034
|
}
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
}
|
|
1575
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiToolsRegistry, decorators: [{
|
|
1576
|
-
type: Injectable,
|
|
1577
|
-
args: [{ providedIn: 'root' }]
|
|
1578
|
-
}] });
|
|
1579
|
-
|
|
1580
|
-
/**
|
|
1581
|
-
* Drives the chat panel.
|
|
1582
|
-
*
|
|
1583
|
-
* Uses `fetch` + `ReadableStream` (not `EventSource`, which can't POST a body) to talk to
|
|
1584
|
-
* `/ai/chat` SSE. State lives in signals so the panel can re-render reactively.
|
|
1585
|
-
*/
|
|
1586
|
-
class MaAiService {
|
|
1587
|
-
auth = inject(MesAuthService);
|
|
1588
|
-
aiConfig = inject(MES_AUTH_AI_CONFIG, { optional: true }) ?? DEFAULT_AI_CONFIG;
|
|
1589
|
-
tools = inject(MaAiToolsRegistry);
|
|
1590
|
-
messages = signal([], ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
|
|
1591
|
-
streaming = signal(false, ...(ngDevMode ? [{ debugName: "streaming" }] : /* istanbul ignore next */ []));
|
|
1592
|
-
/** When non-null, a write-class client tool is awaiting user confirmation. */
|
|
1593
|
-
pendingApproval = signal(null, ...(ngDevMode ? [{ debugName: "pendingApproval" }] : /* istanbul ignore next */ []));
|
|
1594
|
-
lastError = signal(null, ...(ngDevMode ? [{ debugName: "lastError" }] : /* istanbul ignore next */ []));
|
|
1595
|
-
sessionId = null;
|
|
1596
|
-
/** Verbs the user already chose "always approve" for in this session. */
|
|
1597
|
-
alwaysApproved = new Set();
|
|
1598
|
-
abortController = null;
|
|
1599
|
-
/** ID of the current assistant bubble we're streaming tokens into. */
|
|
1600
|
-
currentAssistantId = null;
|
|
1601
|
-
get enabled() {
|
|
1602
|
-
return this.aiConfig.enabled !== false;
|
|
2035
|
+
getDocumentHistory(id) {
|
|
2036
|
+
return this.http.get(`${this.apiBase}/approval/documents/${id}/history`, this.opts);
|
|
1603
2037
|
}
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
this.cancel();
|
|
1607
|
-
this.sessionId = null;
|
|
1608
|
-
this.messages.set([]);
|
|
1609
|
-
this.pendingApproval.set(null);
|
|
1610
|
-
this.lastError.set(null);
|
|
1611
|
-
this.alwaysApproved.clear();
|
|
2038
|
+
getDocumentContentUrl(id) {
|
|
2039
|
+
return `${this.apiBase}/approval/documents/${id}/content`;
|
|
1612
2040
|
}
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
if (this.abortController) {
|
|
1616
|
-
this.abortController.abort();
|
|
1617
|
-
this.abortController = null;
|
|
1618
|
-
}
|
|
1619
|
-
this.streaming.set(false);
|
|
1620
|
-
this.currentAssistantId = null;
|
|
1621
|
-
// If a client-tool call was awaiting the user, clearing it lets them start over.
|
|
1622
|
-
this.pendingApproval.set(null);
|
|
2041
|
+
getDocumentThumbnailUrl(id) {
|
|
2042
|
+
return `${this.apiBase}/approval/documents/${id}/thumbnail`;
|
|
1623
2043
|
}
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
this.lastError.set(null);
|
|
1629
|
-
this.appendBubble({ id: this.uid(), role: 'user', text });
|
|
1630
|
-
await this.runStream({ message: text }, currentRoute);
|
|
2044
|
+
// ====================== Actions ======================
|
|
2045
|
+
approve(documentId, comment) {
|
|
2046
|
+
const body = { comment };
|
|
2047
|
+
return this.http.post(`${this.apiBase}/approval/documents/${documentId}/approve`, body, this.opts);
|
|
1631
2048
|
}
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
*/
|
|
1636
|
-
async resolvePendingApproval(decision) {
|
|
1637
|
-
const pending = this.pendingApproval();
|
|
1638
|
-
if (!pending)
|
|
1639
|
-
return;
|
|
1640
|
-
this.pendingApproval.set(null);
|
|
1641
|
-
if (decision === 'decline') {
|
|
1642
|
-
await this.continueWithToolResult(pending.callId, false, 'User declined this tool call.');
|
|
1643
|
-
this.markToolStatus(pending.callId, 'declined');
|
|
1644
|
-
return;
|
|
1645
|
-
}
|
|
1646
|
-
if (decision === 'always') {
|
|
1647
|
-
this.alwaysApproved.add(this.verbOf(pending.name));
|
|
1648
|
-
}
|
|
1649
|
-
await this.executeClientTool(pending.callId, pending.name, pending.args);
|
|
2049
|
+
reject(documentId, comment) {
|
|
2050
|
+
const body = { comment };
|
|
2051
|
+
return this.http.post(`${this.apiBase}/approval/documents/${documentId}/reject`, body, this.opts);
|
|
1650
2052
|
}
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
this.lastError.set('AI assistant is disabled.');
|
|
1655
|
-
return;
|
|
1656
|
-
}
|
|
1657
|
-
const config = this.auth.getConfig();
|
|
1658
|
-
const base = config?.apiBaseUrl?.replace(/\/$/, '') ?? '';
|
|
1659
|
-
if (!base) {
|
|
1660
|
-
this.lastError.set('MesAuth is not configured.');
|
|
1661
|
-
return;
|
|
1662
|
-
}
|
|
1663
|
-
// Start a fresh assistant bubble that we stream tokens into.
|
|
1664
|
-
const assistantId = this.uid();
|
|
1665
|
-
this.currentAssistantId = assistantId;
|
|
1666
|
-
this.appendBubble({ id: assistantId, role: 'assistant', text: '', pending: true, toolEvents: [] });
|
|
1667
|
-
this.streaming.set(true);
|
|
1668
|
-
this.abortController = new AbortController();
|
|
1669
|
-
try {
|
|
1670
|
-
const res = await fetch(`${base}/ai/chat`, {
|
|
1671
|
-
method: 'POST',
|
|
1672
|
-
headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream' },
|
|
1673
|
-
credentials: config?.withCredentials !== false ? 'include' : 'same-origin',
|
|
1674
|
-
body: JSON.stringify({
|
|
1675
|
-
sessionId: this.sessionId,
|
|
1676
|
-
message: payload.message,
|
|
1677
|
-
toolResult: payload.toolResult,
|
|
1678
|
-
clientTools: this.tools.list().map(t => ({
|
|
1679
|
-
name: t.name,
|
|
1680
|
-
description: t.description,
|
|
1681
|
-
parameters: t.parameters,
|
|
1682
|
-
readOnly: t.readOnly === true
|
|
1683
|
-
})),
|
|
1684
|
-
context: { currentRoute, appName: this.aiConfig.appName }
|
|
1685
|
-
}),
|
|
1686
|
-
signal: this.abortController.signal
|
|
1687
|
-
});
|
|
1688
|
-
if (!res.ok || !res.body) {
|
|
1689
|
-
const detail = await res.text().catch(() => '');
|
|
1690
|
-
throw new Error(`AI request failed (${res.status}). ${detail}`);
|
|
1691
|
-
}
|
|
1692
|
-
await this.readSse(res.body);
|
|
1693
|
-
}
|
|
1694
|
-
catch (err) {
|
|
1695
|
-
if (err?.name === 'AbortError') {
|
|
1696
|
-
// user cancelled
|
|
1697
|
-
}
|
|
1698
|
-
else {
|
|
1699
|
-
this.lastError.set(err?.message ?? String(err));
|
|
1700
|
-
this.finalizeAssistant(`[error: ${err?.message ?? err}]`);
|
|
1701
|
-
}
|
|
1702
|
-
}
|
|
1703
|
-
finally {
|
|
1704
|
-
this.streaming.set(false);
|
|
1705
|
-
this.abortController = null;
|
|
1706
|
-
}
|
|
2053
|
+
delegate(documentId, toUserId, reason) {
|
|
2054
|
+
const body = { toUserId, reason };
|
|
2055
|
+
return this.http.post(`${this.apiBase}/approval/documents/${documentId}/delegate`, body, this.opts);
|
|
1707
2056
|
}
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
2057
|
+
cancel(documentId, reason) {
|
|
2058
|
+
let url = `${this.apiBase}/approval/documents/${documentId}`;
|
|
2059
|
+
if (reason)
|
|
2060
|
+
url += `?reason=${encodeURIComponent(reason)}`;
|
|
2061
|
+
return this.http.delete(url, this.opts);
|
|
2062
|
+
}
|
|
2063
|
+
// ====================== Templates ======================
|
|
2064
|
+
getTemplates(appId) {
|
|
2065
|
+
let url = `${this.apiBase}/approval/templates`;
|
|
2066
|
+
if (appId)
|
|
2067
|
+
url += `?appId=${encodeURIComponent(appId)}`;
|
|
2068
|
+
return this.http.get(url, this.opts);
|
|
2069
|
+
}
|
|
2070
|
+
getTemplate(id) {
|
|
2071
|
+
return this.http.get(`${this.apiBase}/approval/templates/${id}`, this.opts);
|
|
2072
|
+
}
|
|
2073
|
+
createTemplate(request) {
|
|
2074
|
+
return this.http.post(`${this.apiBase}/approval/templates`, request, this.opts);
|
|
2075
|
+
}
|
|
2076
|
+
updateTemplate(id, request) {
|
|
2077
|
+
return this.http.put(`${this.apiBase}/approval/templates/${id}`, request, this.opts);
|
|
2078
|
+
}
|
|
2079
|
+
deleteTemplate(id) {
|
|
2080
|
+
return this.http.delete(`${this.apiBase}/approval/templates/${id}`, this.opts);
|
|
2081
|
+
}
|
|
2082
|
+
previewRole(orgCode, level) {
|
|
2083
|
+
return this.http.get(`${this.apiBase}/approval/roles/preview?orgCode=${encodeURIComponent(orgCode)}&level=${encodeURIComponent(level)}`, this.opts);
|
|
2084
|
+
}
|
|
2085
|
+
// ====================== Create (used by ma-arv-container) ======================
|
|
2086
|
+
createApproval(request) {
|
|
2087
|
+
return this.http.post(`${this.apiBase}/approval/documents`, request, this.opts);
|
|
2088
|
+
}
|
|
2089
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaApprovalService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
2090
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaApprovalService });
|
|
2091
|
+
}
|
|
2092
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaApprovalService, decorators: [{
|
|
2093
|
+
type: Injectable
|
|
2094
|
+
}], ctorParameters: () => [] });
|
|
2095
|
+
|
|
2096
|
+
// ====================== Enums ======================
|
|
2097
|
+
var ApprovalStepMode;
|
|
2098
|
+
(function (ApprovalStepMode) {
|
|
2099
|
+
ApprovalStepMode[ApprovalStepMode["Sequential"] = 0] = "Sequential";
|
|
2100
|
+
ApprovalStepMode[ApprovalStepMode["Parallel"] = 1] = "Parallel";
|
|
2101
|
+
})(ApprovalStepMode || (ApprovalStepMode = {}));
|
|
2102
|
+
var ApprovalDocumentStatus;
|
|
2103
|
+
(function (ApprovalDocumentStatus) {
|
|
2104
|
+
ApprovalDocumentStatus[ApprovalDocumentStatus["Draft"] = 0] = "Draft";
|
|
2105
|
+
ApprovalDocumentStatus[ApprovalDocumentStatus["Pending"] = 1] = "Pending";
|
|
2106
|
+
ApprovalDocumentStatus[ApprovalDocumentStatus["Approved"] = 2] = "Approved";
|
|
2107
|
+
ApprovalDocumentStatus[ApprovalDocumentStatus["Rejected"] = 3] = "Rejected";
|
|
2108
|
+
ApprovalDocumentStatus[ApprovalDocumentStatus["Cancelled"] = 4] = "Cancelled";
|
|
2109
|
+
ApprovalDocumentStatus[ApprovalDocumentStatus["Expired"] = 5] = "Expired";
|
|
2110
|
+
})(ApprovalDocumentStatus || (ApprovalDocumentStatus = {}));
|
|
2111
|
+
var ApprovalStepStatus;
|
|
2112
|
+
(function (ApprovalStepStatus) {
|
|
2113
|
+
ApprovalStepStatus[ApprovalStepStatus["Waiting"] = 0] = "Waiting";
|
|
2114
|
+
ApprovalStepStatus[ApprovalStepStatus["Active"] = 1] = "Active";
|
|
2115
|
+
ApprovalStepStatus[ApprovalStepStatus["Approved"] = 2] = "Approved";
|
|
2116
|
+
ApprovalStepStatus[ApprovalStepStatus["Rejected"] = 3] = "Rejected";
|
|
2117
|
+
ApprovalStepStatus[ApprovalStepStatus["Delegated"] = 4] = "Delegated";
|
|
2118
|
+
ApprovalStepStatus[ApprovalStepStatus["Expired"] = 5] = "Expired";
|
|
2119
|
+
ApprovalStepStatus[ApprovalStepStatus["Skipped"] = 6] = "Skipped";
|
|
2120
|
+
})(ApprovalStepStatus || (ApprovalStepStatus = {}));
|
|
2121
|
+
var ApprovalActionType;
|
|
2122
|
+
(function (ApprovalActionType) {
|
|
2123
|
+
ApprovalActionType[ApprovalActionType["Created"] = 0] = "Created";
|
|
2124
|
+
ApprovalActionType[ApprovalActionType["Submitted"] = 1] = "Submitted";
|
|
2125
|
+
ApprovalActionType[ApprovalActionType["Approved"] = 2] = "Approved";
|
|
2126
|
+
ApprovalActionType[ApprovalActionType["Rejected"] = 3] = "Rejected";
|
|
2127
|
+
ApprovalActionType[ApprovalActionType["Delegated"] = 4] = "Delegated";
|
|
2128
|
+
ApprovalActionType[ApprovalActionType["Cancelled"] = 5] = "Cancelled";
|
|
2129
|
+
ApprovalActionType[ApprovalActionType["Commented"] = 6] = "Commented";
|
|
2130
|
+
ApprovalActionType[ApprovalActionType["Expired"] = 7] = "Expired";
|
|
2131
|
+
ApprovalActionType[ApprovalActionType["StepAdvanced"] = 8] = "StepAdvanced";
|
|
2132
|
+
})(ApprovalActionType || (ApprovalActionType = {}));
|
|
2133
|
+
|
|
2134
|
+
class MaApprovalPanelComponent {
|
|
2135
|
+
approvalActioned = output();
|
|
2136
|
+
isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
|
|
2137
|
+
loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
2138
|
+
activeTab = signal('processing', ...(ngDevMode ? [{ debugName: "activeTab" }] : /* istanbul ignore next */ []));
|
|
2139
|
+
processingItems = signal([], ...(ngDevMode ? [{ debugName: "processingItems" }] : /* istanbul ignore next */ []));
|
|
2140
|
+
approvedItems = signal([], ...(ngDevMode ? [{ debugName: "approvedItems" }] : /* istanbul ignore next */ []));
|
|
2141
|
+
rejectedItems = signal([], ...(ngDevMode ? [{ debugName: "rejectedItems" }] : /* istanbul ignore next */ []));
|
|
2142
|
+
mesAuth = inject(MesAuthService);
|
|
2143
|
+
http = inject(HttpClient);
|
|
2144
|
+
router = inject(Router);
|
|
2145
|
+
host = inject(ElementRef);
|
|
2146
|
+
approvalSvc = null;
|
|
2147
|
+
constructor() {
|
|
2148
|
+
const config = this.mesAuth.getConfig();
|
|
2149
|
+
if (config) {
|
|
2150
|
+
this.approvalSvc = new MaApprovalService();
|
|
2151
|
+
this.approvalSvc.init(config, this.http);
|
|
1737
2152
|
}
|
|
2153
|
+
const approvalEvent = toSignal(this.mesAuth.approvalEvents$);
|
|
2154
|
+
effect(() => {
|
|
2155
|
+
approvalEvent(); // track SignalR approval events
|
|
2156
|
+
if (this.isOpen())
|
|
2157
|
+
this.loadCurrentTab();
|
|
2158
|
+
});
|
|
2159
|
+
// Close when the user clicks outside the panel (mirrors the old backdrop UX
|
|
2160
|
+
// now that the backdrop has been removed).
|
|
2161
|
+
effect((onCleanup) => {
|
|
2162
|
+
if (!this.isOpen())
|
|
2163
|
+
return;
|
|
2164
|
+
const onDocClick = (ev) => {
|
|
2165
|
+
const panel = this.host.nativeElement.querySelector('.approval-panel');
|
|
2166
|
+
if (panel && !panel.contains(ev.target))
|
|
2167
|
+
this.close();
|
|
2168
|
+
};
|
|
2169
|
+
// Defer to skip the same click that opened the panel.
|
|
2170
|
+
const tid = setTimeout(() => document.addEventListener('mousedown', onDocClick), 0);
|
|
2171
|
+
onCleanup(() => {
|
|
2172
|
+
clearTimeout(tid);
|
|
2173
|
+
document.removeEventListener('mousedown', onDocClick);
|
|
2174
|
+
});
|
|
2175
|
+
});
|
|
1738
2176
|
}
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
this.sessionId = ev.sessionId;
|
|
1743
|
-
break;
|
|
1744
|
-
case 'token':
|
|
1745
|
-
this.appendToken(ev.text);
|
|
1746
|
-
break;
|
|
1747
|
-
case 'tool_call_started':
|
|
1748
|
-
this.appendToolEvent({
|
|
1749
|
-
id: ev.id,
|
|
1750
|
-
name: ev.name,
|
|
1751
|
-
side: ev.side,
|
|
1752
|
-
status: 'running',
|
|
1753
|
-
args: ev.args,
|
|
1754
|
-
readOnly: ev.readOnly
|
|
1755
|
-
});
|
|
1756
|
-
break;
|
|
1757
|
-
case 'tool_call_completed':
|
|
1758
|
-
this.markToolStatus(ev.id, ev.ok ? 'ok' : 'error', ev.summary);
|
|
1759
|
-
break;
|
|
1760
|
-
case 'client_tool_call':
|
|
1761
|
-
this.appendToolEvent({
|
|
1762
|
-
id: ev.id,
|
|
1763
|
-
name: ev.name,
|
|
1764
|
-
side: 'client',
|
|
1765
|
-
status: ev.readOnly ? 'running' : 'awaiting-approval',
|
|
1766
|
-
args: ev.args,
|
|
1767
|
-
readOnly: ev.readOnly
|
|
1768
|
-
});
|
|
1769
|
-
if (ev.readOnly || this.alwaysApproved.has(this.verbOf(ev.name))) {
|
|
1770
|
-
// Auto-run; the next /ai/chat call will deliver the result.
|
|
1771
|
-
await this.executeClientTool(ev.id, ev.name, ev.args);
|
|
1772
|
-
}
|
|
1773
|
-
else {
|
|
1774
|
-
this.pendingApproval.set({ callId: ev.id, name: ev.name, args: ev.args, side: 'client' });
|
|
1775
|
-
}
|
|
1776
|
-
break;
|
|
1777
|
-
case 'error':
|
|
1778
|
-
this.lastError.set(ev.message);
|
|
1779
|
-
this.finalizeAssistant(`[error: ${ev.message}]`);
|
|
1780
|
-
break;
|
|
1781
|
-
case 'done':
|
|
1782
|
-
this.finalizeAssistant();
|
|
1783
|
-
break;
|
|
1784
|
-
}
|
|
2177
|
+
open() {
|
|
2178
|
+
this.isOpen.set(true);
|
|
2179
|
+
this.loadAllTabs();
|
|
1785
2180
|
}
|
|
1786
|
-
|
|
1787
|
-
this.
|
|
1788
|
-
const { ok, result } = await this.tools.runTool(name, args);
|
|
1789
|
-
this.markToolStatus(callId, ok ? 'ok' : 'error', this.summarize(result));
|
|
1790
|
-
await this.continueWithToolResult(callId, ok, result);
|
|
2181
|
+
close() {
|
|
2182
|
+
this.isOpen.set(false);
|
|
1791
2183
|
}
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
2184
|
+
toggle() {
|
|
2185
|
+
if (this.isOpen())
|
|
2186
|
+
this.close();
|
|
2187
|
+
else
|
|
2188
|
+
this.open();
|
|
1795
2189
|
}
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
this.
|
|
2190
|
+
switchTab(tab) {
|
|
2191
|
+
this.activeTab.set(tab);
|
|
2192
|
+
this.loadCurrentTab();
|
|
1799
2193
|
}
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
this.
|
|
1804
|
-
this.appendBubble({ id, role: 'assistant', text, pending: true, toolEvents: [] });
|
|
2194
|
+
loadAllTabs() {
|
|
2195
|
+
this.loading.set(true);
|
|
2196
|
+
if (!this.approvalSvc) {
|
|
2197
|
+
this.loading.set(false);
|
|
1805
2198
|
return;
|
|
1806
2199
|
}
|
|
1807
|
-
|
|
1808
|
-
|
|
2200
|
+
let pending = 3;
|
|
2201
|
+
const done = () => { if (--pending === 0)
|
|
2202
|
+
this.loading.set(false); };
|
|
2203
|
+
this.approvalSvc.getPendingApprovals(1, 100).subscribe({
|
|
2204
|
+
next: r => { this.processingItems.set(r.items); done(); },
|
|
2205
|
+
error: () => done()
|
|
2206
|
+
});
|
|
2207
|
+
this.approvalSvc.getMyRequests(1, 10, ApprovalDocumentStatus.Approved).subscribe({
|
|
2208
|
+
next: r => { this.approvedItems.set(r.items); done(); },
|
|
2209
|
+
error: () => done()
|
|
2210
|
+
});
|
|
2211
|
+
this.approvalSvc.getMyRequests(1, 10, ApprovalDocumentStatus.Rejected).subscribe({
|
|
2212
|
+
next: r => { this.rejectedItems.set(r.items); done(); },
|
|
2213
|
+
error: () => done()
|
|
2214
|
+
});
|
|
1809
2215
|
}
|
|
1810
|
-
|
|
1811
|
-
if (!this.
|
|
1812
|
-
const id = this.uid();
|
|
1813
|
-
this.currentAssistantId = id;
|
|
1814
|
-
this.appendBubble({ id, role: 'assistant', text: '', pending: true, toolEvents: [ev] });
|
|
2216
|
+
loadCurrentTab() {
|
|
2217
|
+
if (!this.approvalSvc)
|
|
1815
2218
|
return;
|
|
2219
|
+
if (this.activeTab() === 'processing') {
|
|
2220
|
+
this.approvalSvc.getPendingApprovals(1, 100).subscribe({
|
|
2221
|
+
next: r => this.processingItems.set(r.items),
|
|
2222
|
+
error: () => { }
|
|
2223
|
+
});
|
|
2224
|
+
}
|
|
2225
|
+
else if (this.activeTab() === 'approved') {
|
|
2226
|
+
this.approvalSvc.getMyRequests(1, 10, ApprovalDocumentStatus.Approved).subscribe({
|
|
2227
|
+
next: r => this.approvedItems.set(r.items),
|
|
2228
|
+
error: () => { }
|
|
2229
|
+
});
|
|
2230
|
+
}
|
|
2231
|
+
else {
|
|
2232
|
+
this.approvalSvc.getMyRequests(1, 10, ApprovalDocumentStatus.Rejected).subscribe({
|
|
2233
|
+
next: r => this.rejectedItems.set(r.items),
|
|
2234
|
+
error: () => { }
|
|
2235
|
+
});
|
|
1816
2236
|
}
|
|
1817
|
-
const id = this.currentAssistantId;
|
|
1818
|
-
this.messages.update(arr => arr.map(m => m.id === id
|
|
1819
|
-
? { ...m, toolEvents: [...(m.toolEvents ?? []), ev] }
|
|
1820
|
-
: m));
|
|
1821
|
-
}
|
|
1822
|
-
markToolStatus(callId, status, summary) {
|
|
1823
|
-
this.messages.update(arr => arr.map(m => ({
|
|
1824
|
-
...m,
|
|
1825
|
-
toolEvents: m.toolEvents?.map(t => t.id === callId ? { ...t, status, summary: summary ?? t.summary } : t)
|
|
1826
|
-
})));
|
|
1827
|
-
}
|
|
1828
|
-
finalizeAssistant(extra) {
|
|
1829
|
-
const id = this.currentAssistantId;
|
|
1830
|
-
if (!id)
|
|
1831
|
-
return;
|
|
1832
|
-
this.messages.update(arr => arr.map(m => m.id === id
|
|
1833
|
-
? { ...m, pending: false, text: extra ? (m.text + (m.text ? '\n' : '') + extra) : m.text }
|
|
1834
|
-
: m));
|
|
1835
|
-
this.currentAssistantId = null;
|
|
1836
|
-
}
|
|
1837
|
-
verbOf(name) {
|
|
1838
|
-
return name.split('_')[0] ?? name;
|
|
1839
2237
|
}
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
2238
|
+
navigateToDetail(id) {
|
|
2239
|
+
this.close();
|
|
2240
|
+
this.router.navigate(['/auth/approval/detail', id]);
|
|
2241
|
+
this.approvalActioned.emit();
|
|
1843
2242
|
}
|
|
1844
|
-
|
|
1845
|
-
|
|
2243
|
+
showMore(status) {
|
|
2244
|
+
this.close();
|
|
2245
|
+
this.router.navigate(['/auth/approval/my-requests'], { queryParams: { status } });
|
|
1846
2246
|
}
|
|
1847
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type:
|
|
1848
|
-
static
|
|
2247
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaApprovalPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2248
|
+
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" }] });
|
|
1849
2249
|
}
|
|
1850
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type:
|
|
1851
|
-
type:
|
|
1852
|
-
args: [{
|
|
1853
|
-
}] });
|
|
2250
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaApprovalPanelComponent, decorators: [{
|
|
2251
|
+
type: Component,
|
|
2252
|
+
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"] }]
|
|
2253
|
+
}], ctorParameters: () => [], propDecorators: { approvalActioned: [{ type: i0.Output, args: ["approvalActioned"] }] } });
|
|
1854
2254
|
|
|
1855
2255
|
/**
|
|
1856
2256
|
* Tiny hand-rolled GitHub-flavoured Markdown renderer for AI chat bubbles.
|
|
@@ -2028,10 +2428,187 @@ function sanitizeUrl(url) {
|
|
|
2028
2428
|
return null;
|
|
2029
2429
|
}
|
|
2030
2430
|
|
|
2431
|
+
/**
|
|
2432
|
+
* Drop-down list of the signed-in user's recent AI conversations.
|
|
2433
|
+
* Lives inside the chat panel header (toggled by the History button).
|
|
2434
|
+
*/
|
|
2435
|
+
class MaAiHistoryListComponent {
|
|
2436
|
+
ai = inject(MaAiService);
|
|
2437
|
+
conversations = signal([], ...(ngDevMode ? [{ debugName: "conversations" }] : /* istanbul ignore next */ []));
|
|
2438
|
+
loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
2439
|
+
/** Emitted after the user picks a row and the panel finishes hydrating. */
|
|
2440
|
+
resumed = output();
|
|
2441
|
+
ngOnInit() {
|
|
2442
|
+
this.refresh();
|
|
2443
|
+
}
|
|
2444
|
+
async refresh() {
|
|
2445
|
+
this.loading.set(true);
|
|
2446
|
+
try {
|
|
2447
|
+
const list = await this.ai.listConversations(50);
|
|
2448
|
+
this.conversations.set(list ?? []);
|
|
2449
|
+
}
|
|
2450
|
+
finally {
|
|
2451
|
+
this.loading.set(false);
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
async pick(c) {
|
|
2455
|
+
const ok = await this.ai.resumeConversation(c.id);
|
|
2456
|
+
if (ok)
|
|
2457
|
+
this.resumed.emit(c.id);
|
|
2458
|
+
}
|
|
2459
|
+
async remove(c) {
|
|
2460
|
+
const ok = await this.ai.deleteConversation(c.id);
|
|
2461
|
+
if (ok) {
|
|
2462
|
+
this.conversations.update(arr => arr.filter(x => x.id !== c.id));
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
relative(iso) {
|
|
2466
|
+
const t = Date.parse(iso);
|
|
2467
|
+
if (!isFinite(t))
|
|
2468
|
+
return '';
|
|
2469
|
+
const ms = Date.now() - t;
|
|
2470
|
+
const m = Math.floor(ms / 60_000);
|
|
2471
|
+
if (m < 1)
|
|
2472
|
+
return 'just now';
|
|
2473
|
+
if (m < 60)
|
|
2474
|
+
return `${m}m ago`;
|
|
2475
|
+
const h = Math.floor(m / 60);
|
|
2476
|
+
if (h < 24)
|
|
2477
|
+
return `${h}h ago`;
|
|
2478
|
+
const d = Math.floor(h / 24);
|
|
2479
|
+
if (d < 7)
|
|
2480
|
+
return `${d}d ago`;
|
|
2481
|
+
try {
|
|
2482
|
+
return new Date(t).toLocaleDateString();
|
|
2483
|
+
}
|
|
2484
|
+
catch {
|
|
2485
|
+
return '';
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiHistoryListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2489
|
+
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: `
|
|
2490
|
+
<div class="history-root">
|
|
2491
|
+
<div class="history-head">
|
|
2492
|
+
<span class="history-title">Recent conversations</span>
|
|
2493
|
+
<button class="history-refresh" (click)="refresh()" title="Refresh" aria-label="Refresh">
|
|
2494
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24"
|
|
2495
|
+
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
2496
|
+
<path d="M21 12a9 9 0 1 1-3-6.7L21 8"/><path d="M21 3v5h-5"/>
|
|
2497
|
+
</svg>
|
|
2498
|
+
</button>
|
|
2499
|
+
</div>
|
|
2500
|
+
|
|
2501
|
+
@if (loading()) {
|
|
2502
|
+
<div class="history-empty">Loading…</div>
|
|
2503
|
+
} @else if (conversations().length === 0) {
|
|
2504
|
+
<div class="history-empty">No conversations yet.</div>
|
|
2505
|
+
} @else {
|
|
2506
|
+
<ul class="history-list">
|
|
2507
|
+
@for (c of conversations(); track c.id) {
|
|
2508
|
+
<li class="history-row">
|
|
2509
|
+
<button class="history-pick" (click)="pick(c)" [title]="c.title ?? '(untitled)'">
|
|
2510
|
+
<span class="history-row-title">{{ c.title || '(untitled)' }}</span>
|
|
2511
|
+
<span class="history-row-meta">
|
|
2512
|
+
<span>{{ relative(c.updatedAt) }}</span>
|
|
2513
|
+
@if (c.appName) { <span>· {{ c.appName }}</span> }
|
|
2514
|
+
<span>· {{ c.messageCount }} msg</span>
|
|
2515
|
+
</span>
|
|
2516
|
+
</button>
|
|
2517
|
+
<button class="history-del" (click)="remove(c)" title="Delete" aria-label="Delete">
|
|
2518
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24"
|
|
2519
|
+
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
2520
|
+
<polyline points="3 6 5 6 21 6"/>
|
|
2521
|
+
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
|
|
2522
|
+
<path d="M10 11v6"/><path d="M14 11v6"/>
|
|
2523
|
+
</svg>
|
|
2524
|
+
</button>
|
|
2525
|
+
</li>
|
|
2526
|
+
}
|
|
2527
|
+
</ul>
|
|
2528
|
+
}
|
|
2529
|
+
</div>
|
|
2530
|
+
`, 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 });
|
|
2531
|
+
}
|
|
2532
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiHistoryListComponent, decorators: [{
|
|
2533
|
+
type: Component,
|
|
2534
|
+
args: [{ selector: 'ma-ai-history-list', changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
2535
|
+
<div class="history-root">
|
|
2536
|
+
<div class="history-head">
|
|
2537
|
+
<span class="history-title">Recent conversations</span>
|
|
2538
|
+
<button class="history-refresh" (click)="refresh()" title="Refresh" aria-label="Refresh">
|
|
2539
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24"
|
|
2540
|
+
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
2541
|
+
<path d="M21 12a9 9 0 1 1-3-6.7L21 8"/><path d="M21 3v5h-5"/>
|
|
2542
|
+
</svg>
|
|
2543
|
+
</button>
|
|
2544
|
+
</div>
|
|
2545
|
+
|
|
2546
|
+
@if (loading()) {
|
|
2547
|
+
<div class="history-empty">Loading…</div>
|
|
2548
|
+
} @else if (conversations().length === 0) {
|
|
2549
|
+
<div class="history-empty">No conversations yet.</div>
|
|
2550
|
+
} @else {
|
|
2551
|
+
<ul class="history-list">
|
|
2552
|
+
@for (c of conversations(); track c.id) {
|
|
2553
|
+
<li class="history-row">
|
|
2554
|
+
<button class="history-pick" (click)="pick(c)" [title]="c.title ?? '(untitled)'">
|
|
2555
|
+
<span class="history-row-title">{{ c.title || '(untitled)' }}</span>
|
|
2556
|
+
<span class="history-row-meta">
|
|
2557
|
+
<span>{{ relative(c.updatedAt) }}</span>
|
|
2558
|
+
@if (c.appName) { <span>· {{ c.appName }}</span> }
|
|
2559
|
+
<span>· {{ c.messageCount }} msg</span>
|
|
2560
|
+
</span>
|
|
2561
|
+
</button>
|
|
2562
|
+
<button class="history-del" (click)="remove(c)" title="Delete" aria-label="Delete">
|
|
2563
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24"
|
|
2564
|
+
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
2565
|
+
<polyline points="3 6 5 6 21 6"/>
|
|
2566
|
+
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
|
|
2567
|
+
<path d="M10 11v6"/><path d="M14 11v6"/>
|
|
2568
|
+
</svg>
|
|
2569
|
+
</button>
|
|
2570
|
+
</li>
|
|
2571
|
+
}
|
|
2572
|
+
</ul>
|
|
2573
|
+
}
|
|
2574
|
+
</div>
|
|
2575
|
+
`, 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"] }]
|
|
2576
|
+
}], propDecorators: { resumed: [{ type: i0.Output, args: ["resumed"] }] } });
|
|
2577
|
+
|
|
2031
2578
|
const STORAGE_KEY = 'ma-ai-panel-width';
|
|
2032
2579
|
const MIN_WIDTH = 320;
|
|
2033
2580
|
const MAX_WIDTH = 800;
|
|
2034
2581
|
const DEFAULT_WIDTH = 420;
|
|
2582
|
+
const LAYOUT_STYLE_ID = 'ma-ai-panel-layout-style';
|
|
2583
|
+
const LAYOUT_CSS_VAR = '--ma-ai-panel-width';
|
|
2584
|
+
let layoutStyleInstalled = false;
|
|
2585
|
+
/**
|
|
2586
|
+
* Inject (once per page) a global stylesheet that lets the host app's <body> reflow
|
|
2587
|
+
* when the AI panel opens. Driven by the `--ma-ai-panel-width` custom property which
|
|
2588
|
+
* the panel writes on every open/close/resize.
|
|
2589
|
+
*/
|
|
2590
|
+
function ensureGlobalLayoutStyle() {
|
|
2591
|
+
if (layoutStyleInstalled)
|
|
2592
|
+
return;
|
|
2593
|
+
if (typeof document === 'undefined')
|
|
2594
|
+
return;
|
|
2595
|
+
if (document.getElementById(LAYOUT_STYLE_ID)) {
|
|
2596
|
+
layoutStyleInstalled = true;
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2599
|
+
const style = document.createElement('style');
|
|
2600
|
+
style.id = LAYOUT_STYLE_ID;
|
|
2601
|
+
style.textContent = `
|
|
2602
|
+
:root { ${LAYOUT_CSS_VAR}: 0px; }
|
|
2603
|
+
body {
|
|
2604
|
+
padding-inline-end: var(${LAYOUT_CSS_VAR}, 0px);
|
|
2605
|
+
transition: padding-inline-end 300ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
2606
|
+
box-sizing: border-box;
|
|
2607
|
+
}
|
|
2608
|
+
`;
|
|
2609
|
+
document.head.appendChild(style);
|
|
2610
|
+
layoutStyleInstalled = true;
|
|
2611
|
+
}
|
|
2035
2612
|
/**
|
|
2036
2613
|
* Right-side, resizable chat drawer. Open/close via the public open/close/toggle methods.
|
|
2037
2614
|
* Uses panel-shared.css for the header/empty-state styling so it looks like the approval
|
|
@@ -2041,10 +2618,13 @@ class MaAiChatPanelComponent {
|
|
|
2041
2618
|
isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
|
|
2042
2619
|
width = signal(this.loadWidth(), ...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
|
|
2043
2620
|
draft = signal('', ...(ngDevMode ? [{ debugName: "draft" }] : /* istanbul ignore next */ []));
|
|
2621
|
+
historyOpen = signal(false, ...(ngDevMode ? [{ debugName: "historyOpen" }] : /* istanbul ignore next */ []));
|
|
2044
2622
|
ai = inject(MaAiService);
|
|
2045
2623
|
router = inject(Router, { optional: true });
|
|
2046
2624
|
themeService = inject(ThemeService);
|
|
2047
2625
|
uiConfig = inject(MaUiConfigService);
|
|
2626
|
+
aiConfig = inject(MES_AUTH_AI_CONFIG, { optional: true }) ?? DEFAULT_AI_CONFIG;
|
|
2627
|
+
panelMode = this.aiConfig.panelMode ?? 'static';
|
|
2048
2628
|
/** Suggested prompts shown in the empty state — manifest overridable. */
|
|
2049
2629
|
quickPrompts = computed(() => {
|
|
2050
2630
|
const fromManifest = this.uiConfig.quickPrompts();
|
|
@@ -2077,10 +2657,28 @@ class MaAiChatPanelComponent {
|
|
|
2077
2657
|
this.streaming();
|
|
2078
2658
|
queueMicrotask(() => this.scrollToBottom());
|
|
2079
2659
|
});
|
|
2660
|
+
// Static mode: install the global push-layout stylesheet and keep the CSS
|
|
2661
|
+
// variable in sync with open/close + live drag-resize.
|
|
2662
|
+
if (this.panelMode === 'static') {
|
|
2663
|
+
ensureGlobalLayoutStyle();
|
|
2664
|
+
effect(() => {
|
|
2665
|
+
const px = this.isOpen() ? this.width() : 0;
|
|
2666
|
+
this.setLayoutVar(px);
|
|
2667
|
+
});
|
|
2668
|
+
}
|
|
2080
2669
|
}
|
|
2081
2670
|
ngAfterViewInit() {
|
|
2082
2671
|
// pointer listeners attached on demand during drag
|
|
2083
2672
|
}
|
|
2673
|
+
ngOnDestroy() {
|
|
2674
|
+
if (this.panelMode === 'static')
|
|
2675
|
+
this.setLayoutVar(0);
|
|
2676
|
+
}
|
|
2677
|
+
setLayoutVar(px) {
|
|
2678
|
+
if (typeof document === 'undefined')
|
|
2679
|
+
return;
|
|
2680
|
+
document.documentElement.style.setProperty(LAYOUT_CSS_VAR, `${px}px`);
|
|
2681
|
+
}
|
|
2084
2682
|
open() {
|
|
2085
2683
|
this.isOpen.set(true);
|
|
2086
2684
|
queueMicrotask(() => this.textArea()?.nativeElement.focus());
|
|
@@ -2096,6 +2694,14 @@ class MaAiChatPanelComponent {
|
|
|
2096
2694
|
}
|
|
2097
2695
|
newConversation() {
|
|
2098
2696
|
this.ai.resetSession();
|
|
2697
|
+
this.historyOpen.set(false);
|
|
2698
|
+
}
|
|
2699
|
+
toggleHistory() {
|
|
2700
|
+
this.historyOpen.update(v => !v);
|
|
2701
|
+
}
|
|
2702
|
+
onHistoryResumed(_id) {
|
|
2703
|
+
this.historyOpen.set(false);
|
|
2704
|
+
queueMicrotask(() => this.textArea()?.nativeElement.focus());
|
|
2099
2705
|
}
|
|
2100
2706
|
send() {
|
|
2101
2707
|
const text = this.draft().trim();
|
|
@@ -2170,11 +2776,11 @@ class MaAiChatPanelComponent {
|
|
|
2170
2776
|
el.scrollTop = el.scrollHeight;
|
|
2171
2777
|
}
|
|
2172
2778
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiChatPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2173
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: MaAiChatPanelComponent, isStandalone: true, selector: "ma-ai-chat-panel", host: { properties: { "class": "this.themeClass" } }, viewQueries: [{ propertyName: "scrollContainer", first: true, predicate: ["scrollContainer"], descendants: true, isSignal: true }, { propertyName: "textArea", first: true, predicate: ["textArea"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"ai-panel\" [class.open]=\"isOpen()\" [style.width.px]=\"width()\">\n\n <!-- Resize handle (left edge) -->\n <div class=\"ai-resize-handle\"\n (pointerdown)=\"startResize($event)\"\n (pointermove)=\"onResizeMove($event)\"\n (pointerup)=\"endResize($event)\"\n (pointercancel)=\"endResize($event)\"\n aria-label=\"Resize panel\"\n title=\"Drag to resize\"></div>\n\n <!-- Header -->\n <div class=\"panel-header\">\n <div class=\"panel-header-left\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\"\n fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M12 2 L13.5 7.5 L19 9 L13.5 10.5 L12 16 L10.5 10.5 L5 9 L10.5 7.5 Z\"/>\n <path d=\"M19 16 L19.8 18.2 L22 19 L19.8 19.8 L19 22 L18.2 19.8 L16 19 L18.2 18.2 Z\"/>\n </svg>\n <h3>AI Assistant</h3>\n </div>\n <div class=\"header-actions\">\n <button class=\"icon-btn\" (click)=\"newConversation()\" title=\"New conversation\" aria-label=\"New conversation\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\"\n fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M21 12a9 9 0 1 1-3-6.7L21 8\"/><path d=\"M21 3v5h-5\"/>\n </svg>\n </button>\n <button class=\"close-btn\" (click)=\"close()\" aria-label=\"Close\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\"\n fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"/><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"/>\n </svg>\n </button>\n </div>\n </div>\n\n <!-- Conversation -->\n <div class=\"panel-content\" #scrollContainer>\n @if (!hasMessages()) {\n <div class=\"empty-state\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"36\" height=\"36\" viewBox=\"0 0 24 24\"\n fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\" opacity=\"0.5\">\n <path d=\"M12 2 L13.5 7.5 L19 9 L13.5 10.5 L12 16 L10.5 10.5 L5 9 L10.5 7.5 Z\"/>\n </svg>\n <p>Ask anything about your account, approvals, notifications, or how to use the app.</p>\n <div class=\"quick-prompts\">\n @for (q of quickPrompts(); track q) {\n <button class=\"quick-prompt\" (click)=\"draft.set(q)\">{{ q }}</button>\n }\n </div>\n </div>\n }\n\n @for (m of messages(); track m.id) {\n <div class=\"bubble\" [class.user]=\"m.role === 'user'\" [class.assistant]=\"m.role === 'assistant'\">\n @if (m.role === 'assistant' && m.toolEvents && m.toolEvents.length > 0) {\n <div class=\"tool-strip\">\n @for (t of m.toolEvents; track t.id) {\n <div class=\"tool-chip\" [class.ok]=\"t.status === 'ok'\" [class.err]=\"t.status === 'error'\"\n [class.running]=\"t.status === 'running'\" [class.await]=\"t.status === 'awaiting-approval'\"\n [class.declined]=\"t.status === 'declined'\"\n [title]=\"t.summary ?? ''\">\n <span class=\"dot\"></span>\n <span class=\"tool-name\">{{ t.side === 'server' ? '\u2699' : '\u2318' }} {{ t.name }}</span>\n @if (t.status === 'running') { <span class=\"muted\">\u2026</span> }\n @if (t.status === 'ok' && t.summary) { <span class=\"muted\">\u00B7 {{ t.summary }}</span> }\n @if (t.status === 'error') { <span class=\"muted\">\u00B7 failed</span> }\n @if (t.status === 'awaiting-approval') { <span class=\"muted\">\u00B7 awaiting approval</span> }\n @if (t.status === 'declined') { <span class=\"muted\">\u00B7 declined</span> }\n </div>\n }\n </div>\n }\n @if (m.text) {\n @if (m.role === 'assistant') {\n <div class=\"bubble-text md-body\" [innerHTML]=\"m.text | maAiMarkdown\"></div>\n } @else {\n <div class=\"bubble-text\">{{ m.text }}</div>\n }\n }\n @if (m.pending && !m.text) {\n <div class=\"thinking\"><span class=\"spinner-small\"></span> Thinking\u2026</div>\n }\n </div>\n }\n </div>\n\n <!-- Pending approval card -->\n @if (pendingApproval(); as p) {\n <div class=\"approval-card\">\n <div class=\"approval-card-head\">\n <strong>Approve action?</strong>\n <code class=\"tool-name\">{{ p.name }}</code>\n </div>\n @if (p.args && (p.args | json) !== '{}') {\n <pre class=\"approval-args\">{{ p.args | json }}</pre>\n }\n <div class=\"approval-actions\">\n <button class=\"btn primary\" (click)=\"approve()\">Approve</button>\n <button class=\"btn\" (click)=\"alwaysApprove()\">Always</button>\n <button class=\"btn danger\" (click)=\"decline()\">Decline</button>\n </div>\n </div>\n }\n\n <!-- Composer -->\n <div class=\"composer\">\n @if (lastError()) {\n <div class=\"composer-error\">{{ lastError() }}</div>\n }\n <div class=\"composer-row\">\n <textarea #textArea\n rows=\"1\"\n placeholder=\"Ask the AI\u2026\"\n [value]=\"draft()\"\n (input)=\"onTextareaInput($event)\"\n (keydown)=\"onTextareaKey($event)\"\n [disabled]=\"streaming()\"></textarea>\n @if (streaming()) {\n <button class=\"send-btn cancel\" (click)=\"cancel()\" title=\"Cancel\" aria-label=\"Cancel\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><rect x=\"6\" y=\"6\" width=\"12\" height=\"12\"/></svg>\n </button>\n } @else {\n <button class=\"send-btn\" (click)=\"send()\" [disabled]=\"!draft().trim()\" title=\"Send\" aria-label=\"Send\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"22\" y1=\"2\" x2=\"11\" y2=\"13\"/><polygon points=\"22 2 15 22 11 13 2 9 22 2\"/></svg>\n </button>\n }\n </div>\n </div>\n</div>\n", styles: [".panel-header{display:flex;justify-content:space-between;align-items:center;padding:16px 18px;border-bottom:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0}.panel-header-left{display:flex;align-items:center;gap:9px;color:var(--primary)}.panel-header h3{margin:0;font-size:16px;font-weight:700;color:var(--text-primary)}.close-btn{background:none;border:none;cursor:pointer;color:var(--text-muted);width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:background .15s,color .15s}.close-btn:hover{background:var(--bg-hover);color:var(--text-primary)}.tabs{display:flex;border-bottom:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0}.tab-btn{flex:1;display:flex;align-items:center;justify-content:center;gap:6px;padding:11px 8px;background:none;border:none;border-bottom:2px solid transparent;color:var(--text-muted);cursor:pointer;font-size:13px;font-weight:500;transition:color .15s,border-color .15s}.tab-btn.active{color:var(--primary);border-bottom-color:var(--primary)}.tab-btn:hover:not(.active){color:var(--text-secondary)}.tab-badge{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;background:var(--primary);color:#fff;font-size:11px;font-weight:700;border-radius:9px}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 20px;gap:12px;color:var(--text-muted)}.empty-state p{margin:0;font-size:13px}.loading-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px;gap:12px;color:var(--text-muted);font-size:13px}.spinner{width:24px;height:24px;border:2px solid var(--border-color);border-top-color:var(--primary);border-radius:50%;animation:spin .7s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}\n", ":host{--primary: #c4b5fd;--primary-strong: #a78bfa;--success: #66bb6a;--error: #ef5350;--text-primary: #e0e0e0;--text-secondary: #b0b0b0;--text-muted: #757575;--bg-primary: #1e1e2e;--bg-secondary: #27273a;--bg-hover: #2a2d4a;--bg-bubble-user: #312e81;--bg-bubble-assistant: #232336;--border-color: #383850;--shadow: rgba(0,0,0,.4)}:host(.theme-light){--primary: #7c3aed;--primary-strong: #6d28d9;--success: #2e7d32;--error: #c62828;--text-primary: #212121;--text-secondary: #616161;--text-muted: #9e9e9e;--bg-primary: #ffffff;--bg-secondary: #f5f3ff;--bg-hover: #ede9fe;--bg-bubble-user: #ede9fe;--bg-bubble-assistant: #f5f5f5;--border-color: #e0e0e0;--shadow: rgba(0,0,0,.15)}.ai-panel{position:fixed;top:0;right:0;height:100vh;background:var(--bg-primary);box-shadow:-4px 0 24px var(--shadow);display:flex;flex-direction:column;z-index:1040;transform:translate(100%);transition:transform .3s cubic-bezier(.16,1,.3,1);will-change:transform,width;pointer-events:none}.ai-panel.open{transform:translate(0);pointer-events:auto}.ai-resize-handle{position:absolute;top:0;left:-4px;width:8px;height:100%;cursor:ew-resize;z-index:1;background:transparent}.ai-resize-handle:hover{background:linear-gradient(to right,transparent,rgba(124,58,237,.25),transparent)}.header-actions{display:flex;align-items:center;gap:4px}.icon-btn{background:none;border:none;cursor:pointer;color:var(--text-muted);width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:background .15s,color .15s}.icon-btn:hover{background:var(--bg-hover);color:var(--text-primary)}.panel-content{flex:1;overflow-y:auto;padding:12px 14px;display:flex;flex-direction:column;gap:10px}.quick-prompts{display:flex;flex-wrap:wrap;gap:6px;justify-content:center;margin-top:8px}.quick-prompt{background:var(--bg-secondary);border:1px solid var(--border-color);color:var(--text-secondary);padding:6px 10px;border-radius:14px;font-size:12px;cursor:pointer;transition:background .15s,color .15s}.quick-prompt:hover{background:var(--bg-hover);color:var(--text-primary)}.bubble{max-width:90%;padding:10px 12px;border-radius:10px;font-size:13.5px;line-height:1.5;word-wrap:break-word;white-space:pre-wrap}.bubble.user{align-self:flex-end;background:var(--bg-bubble-user);color:var(--text-primary)}.bubble.assistant{align-self:flex-start;background:var(--bg-bubble-assistant);color:var(--text-primary)}.bubble-text{display:block}.thinking{display:flex;align-items:center;gap:6px;color:var(--text-muted);font-size:12.5px}.spinner-small{width:12px;height:12px;border:2px solid var(--border-color);border-top-color:var(--primary);border-radius:50%;animation:spin .7s linear infinite;display:inline-block}@keyframes spin{to{transform:rotate(360deg)}}.tool-strip{display:flex;flex-direction:column;gap:4px;margin-bottom:6px}.tool-chip{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border-radius:12px;background:var(--bg-hover);font-size:11.5px;color:var(--text-secondary);border:1px solid var(--border-color);align-self:flex-start}.tool-chip .dot{width:6px;height:6px;border-radius:50%;background:var(--text-muted)}.tool-chip.running .dot{background:var(--primary);animation:pulse 1s ease-in-out infinite}.tool-chip.ok .dot{background:var(--success)}.tool-chip.err .dot{background:var(--error)}.tool-chip.await .dot{background:#f59e0b}.tool-chip.declined .dot{background:var(--text-muted)}.tool-chip .tool-name{font-weight:600;color:var(--text-primary)}.tool-chip .muted{color:var(--text-muted)}@keyframes pulse{50%{opacity:.4}}.approval-card{flex-shrink:0;margin:8px 14px;padding:12px 14px;border:1px solid var(--primary);border-radius:10px;background:var(--bg-secondary)}.approval-card-head{display:flex;align-items:center;gap:10px;margin-bottom:8px;color:var(--text-primary)}.approval-card-head code{background:var(--bg-hover);padding:2px 6px;border-radius:6px;font-size:12px;color:var(--primary)}.approval-args{background:var(--bg-primary);border:1px solid var(--border-color);border-radius:6px;padding:8px;font-size:11.5px;max-height:100px;overflow:auto;margin:0 0 8px;color:var(--text-secondary)}.approval-actions{display:flex;gap:6px;flex-wrap:wrap}.btn{flex:1;padding:6px 10px;font-size:12.5px;font-weight:600;border-radius:6px;border:1px solid var(--border-color);background:var(--bg-primary);color:var(--text-primary);cursor:pointer;transition:background .15s,border-color .15s}.btn:hover{background:var(--bg-hover)}.btn.primary{background:var(--primary);color:#fff;border-color:var(--primary)}.btn.primary:hover{background:var(--primary-strong)}.btn.danger{color:var(--error)}.btn.danger:hover{background:#ef53501a}.composer{flex-shrink:0;border-top:1px solid var(--border-color);padding:10px 12px;background:var(--bg-secondary)}.composer-error{color:var(--error);font-size:12px;margin-bottom:6px}.composer-row{display:flex;align-items:flex-end;gap:8px}.composer textarea{flex:1;resize:none;padding:8px 10px;border-radius:8px;border:1px solid var(--border-color);background:var(--bg-primary);color:var(--text-primary);font-size:13px;font-family:inherit;outline:none;min-height:36px;max-height:160px;line-height:1.4}.composer textarea:focus{border-color:var(--primary)}.composer textarea:disabled{opacity:.6}.send-btn{width:36px;height:36px;flex-shrink:0;display:flex;align-items:center;justify-content:center;background:var(--primary);color:#fff;border:none;border-radius:8px;cursor:pointer;transition:background .15s}.send-btn:hover:not(:disabled){background:var(--primary-strong)}.send-btn:disabled{opacity:.5;cursor:not-allowed}.send-btn.cancel{background:var(--error)}.send-btn.cancel:hover{filter:brightness(1.1)}.md-body{font-size:13.5px;line-height:1.55}.md-body .md-p{margin:0 0 8px}.md-body .md-p:last-child{margin-bottom:0}.md-body .md-h1,.md-body .md-h2,.md-body .md-h3,.md-body .md-h4,.md-body .md-h5,.md-body .md-h6{margin:12px 0 6px;font-weight:700;color:var(--text-primary);line-height:1.3}.md-body .md-h1{font-size:17px}.md-body .md-h2{font-size:15.5px}.md-body .md-h3{font-size:14.5px}.md-body .md-h4,.md-body .md-h5,.md-body .md-h6{font-size:13.5px}.md-body strong{font-weight:700}.md-body em{font-style:italic}.md-body .md-link{color:var(--primary);text-decoration:underline;text-underline-offset:2px}.md-body .md-link:hover{color:var(--primary-strong)}.md-body .md-list{margin:0 0 8px;padding-left:22px}.md-body .md-list li{margin:2px 0}.md-body .md-list li::marker{color:var(--text-muted)}.md-body .md-quote{margin:0 0 8px;padding:6px 10px;border-left:3px solid var(--primary);background:var(--bg-hover);color:var(--text-secondary);border-radius:0 6px 6px 0}.md-body .md-code{background:var(--bg-hover);border:1px solid var(--border-color);border-radius:4px;padding:1px 5px;font-size:12px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:var(--primary)}.md-body .md-pre{background:var(--bg-primary);border:1px solid var(--border-color);border-radius:8px;padding:10px 12px;margin:0 0 8px;overflow-x:auto;font-size:12px;line-height:1.5}.md-body .md-pre code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:var(--text-primary);background:none;border:none;padding:0}.md-body .md-table-wrap{margin:0 0 8px;overflow-x:auto;border:1px solid var(--border-color);border-radius:8px}.md-body .md-table{border-collapse:collapse;width:100%;font-size:12.5px}.md-body .md-table th,.md-body .md-table td{padding:6px 10px;border-bottom:1px solid var(--border-color);vertical-align:top;text-align:left}.md-body .md-table th{background:var(--bg-secondary);color:var(--text-primary);font-weight:600}.md-body .md-table tr:last-child td{border-bottom:none}.md-body .md-table tbody tr:hover{background:var(--bg-hover)}\n"], dependencies: [{ kind: "pipe", type: JsonPipe, name: "json" }, { kind: "pipe", type: MaAiMarkdownPipe, name: "maAiMarkdown" }] });
|
|
2779
|
+
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" }] });
|
|
2174
2780
|
}
|
|
2175
2781
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiChatPanelComponent, decorators: [{
|
|
2176
2782
|
type: Component,
|
|
2177
|
-
args: [{ selector: 'ma-ai-chat-panel', imports: [JsonPipe, MaAiMarkdownPipe], template: "<div class=\"ai-panel\" [class.open]=\"isOpen()\" [style.width.px]=\"width()\">\n\n <!-- Resize handle (left edge) -->\n <div class=\"ai-resize-handle\"\n (pointerdown)=\"startResize($event)\"\n (pointermove)=\"onResizeMove($event)\"\n (pointerup)=\"endResize($event)\"\n (pointercancel)=\"endResize($event)\"\n aria-label=\"Resize panel\"\n title=\"Drag to resize\"></div>\n\n <!-- Header -->\n <div class=\"panel-header\">\n <div class=\"panel-header-left\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\"\n fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M12 2 L13.5 7.5 L19 9 L13.5 10.5 L12 16 L10.5 10.5 L5 9 L10.5 7.5 Z\"/>\n <path d=\"M19 16 L19.8 18.2 L22 19 L19.8 19.8 L19 22 L18.2 19.8 L16 19 L18.2 18.2 Z\"/>\n </svg>\n <h3>AI Assistant</h3>\n </div>\n <div class=\"header-actions\">\n <button class=\"icon-btn\" (click)=\"newConversation()\" title=\"New conversation\" aria-label=\"New conversation\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\"\n fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M21 12a9 9 0 1 1-3-6.7L21 8\"/><path d=\"M21 3v5h-5\"/>\n </svg>\n </button>\n <button class=\"close-btn\" (click)=\"close()\" aria-label=\"Close\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\"\n fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"/><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"/>\n </svg>\n </button>\n </div>\n </div>\n\n <!-- Conversation -->\n <div class=\"panel-content\" #scrollContainer>\n @if (!hasMessages()) {\n <div class=\"empty-state\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"36\" height=\"36\" viewBox=\"0 0 24 24\"\n fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\" opacity=\"0.5\">\n <path d=\"M12 2 L13.5 7.5 L19 9 L13.5 10.5 L12 16 L10.5 10.5 L5 9 L10.5 7.5 Z\"/>\n </svg>\n <p>Ask anything about your account, approvals, notifications, or how to use the app.</p>\n <div class=\"quick-prompts\">\n @for (q of quickPrompts(); track q) {\n <button class=\"quick-prompt\" (click)=\"draft.set(q)\">{{ q }}</button>\n }\n </div>\n </div>\n }\n\n @for (m of messages(); track m.id) {\n <div class=\"bubble\" [class.user]=\"m.role === 'user'\" [class.assistant]=\"m.role === 'assistant'\">\n @if (m.role === 'assistant' && m.toolEvents && m.toolEvents.length > 0) {\n <div class=\"tool-strip\">\n @for (t of m.toolEvents; track t.id) {\n <div class=\"tool-chip\" [class.ok]=\"t.status === 'ok'\" [class.err]=\"t.status === 'error'\"\n [class.running]=\"t.status === 'running'\" [class.await]=\"t.status === 'awaiting-approval'\"\n [class.declined]=\"t.status === 'declined'\"\n [title]=\"t.summary ?? ''\">\n <span class=\"dot\"></span>\n <span class=\"tool-name\">{{ t.side === 'server' ? '\u2699' : '\u2318' }} {{ t.name }}</span>\n @if (t.status === 'running') { <span class=\"muted\">\u2026</span> }\n @if (t.status === 'ok' && t.summary) { <span class=\"muted\">\u00B7 {{ t.summary }}</span> }\n @if (t.status === 'error') { <span class=\"muted\">\u00B7 failed</span> }\n @if (t.status === 'awaiting-approval') { <span class=\"muted\">\u00B7 awaiting approval</span> }\n @if (t.status === 'declined') { <span class=\"muted\">\u00B7 declined</span> }\n </div>\n }\n </div>\n }\n @if (m.text) {\n @if (m.role === 'assistant') {\n <div class=\"bubble-text md-body\" [innerHTML]=\"m.text | maAiMarkdown\"></div>\n } @else {\n <div class=\"bubble-text\">{{ m.text }}</div>\n }\n }\n @if (m.pending && !m.text) {\n <div class=\"thinking\"><span class=\"spinner-small\"></span> Thinking\u2026</div>\n }\n </div>\n }\n </div>\n\n <!-- Pending approval card -->\n @if (pendingApproval(); as p) {\n <div class=\"approval-card\">\n <div class=\"approval-card-head\">\n <strong>Approve action?</strong>\n <code class=\"tool-name\">{{ p.name }}</code>\n </div>\n @if (p.args && (p.args | json) !== '{}') {\n <pre class=\"approval-args\">{{ p.args | json }}</pre>\n }\n <div class=\"approval-actions\">\n <button class=\"btn primary\" (click)=\"approve()\">Approve</button>\n <button class=\"btn\" (click)=\"alwaysApprove()\">Always</button>\n <button class=\"btn danger\" (click)=\"decline()\">Decline</button>\n </div>\n </div>\n }\n\n <!-- Composer -->\n <div class=\"composer\">\n @if (lastError()) {\n <div class=\"composer-error\">{{ lastError() }}</div>\n }\n <div class=\"composer-row\">\n <textarea #textArea\n rows=\"1\"\n placeholder=\"Ask the AI\u2026\"\n [value]=\"draft()\"\n (input)=\"onTextareaInput($event)\"\n (keydown)=\"onTextareaKey($event)\"\n [disabled]=\"streaming()\"></textarea>\n @if (streaming()) {\n <button class=\"send-btn cancel\" (click)=\"cancel()\" title=\"Cancel\" aria-label=\"Cancel\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><rect x=\"6\" y=\"6\" width=\"12\" height=\"12\"/></svg>\n </button>\n } @else {\n <button class=\"send-btn\" (click)=\"send()\" [disabled]=\"!draft().trim()\" title=\"Send\" aria-label=\"Send\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"22\" y1=\"2\" x2=\"11\" y2=\"13\"/><polygon points=\"22 2 15 22 11 13 2 9 22 2\"/></svg>\n </button>\n }\n </div>\n </div>\n</div>\n", styles: [".panel-header{display:flex;justify-content:space-between;align-items:center;padding:16px 18px;border-bottom:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0}.panel-header-left{display:flex;align-items:center;gap:9px;color:var(--primary)}.panel-header h3{margin:0;font-size:16px;font-weight:700;color:var(--text-primary)}.close-btn{background:none;border:none;cursor:pointer;color:var(--text-muted);width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:background .15s,color .15s}.close-btn:hover{background:var(--bg-hover);color:var(--text-primary)}.tabs{display:flex;border-bottom:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0}.tab-btn{flex:1;display:flex;align-items:center;justify-content:center;gap:6px;padding:11px 8px;background:none;border:none;border-bottom:2px solid transparent;color:var(--text-muted);cursor:pointer;font-size:13px;font-weight:500;transition:color .15s,border-color .15s}.tab-btn.active{color:var(--primary);border-bottom-color:var(--primary)}.tab-btn:hover:not(.active){color:var(--text-secondary)}.tab-badge{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;background:var(--primary);color:#fff;font-size:11px;font-weight:700;border-radius:9px}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 20px;gap:12px;color:var(--text-muted)}.empty-state p{margin:0;font-size:13px}.loading-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px;gap:12px;color:var(--text-muted);font-size:13px}.spinner{width:24px;height:24px;border:2px solid var(--border-color);border-top-color:var(--primary);border-radius:50%;animation:spin .7s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}\n", ":host{--primary: #c4b5fd;--primary-strong: #a78bfa;--success: #66bb6a;--error: #ef5350;--text-primary: #e0e0e0;--text-secondary: #b0b0b0;--text-muted: #757575;--bg-primary: #1e1e2e;--bg-secondary: #27273a;--bg-hover: #2a2d4a;--bg-bubble-user: #312e81;--bg-bubble-assistant: #232336;--border-color: #383850;--shadow: rgba(0,0,0,.4)}:host(.theme-light){--primary: #7c3aed;--primary-strong: #6d28d9;--success: #2e7d32;--error: #c62828;--text-primary: #212121;--text-secondary: #616161;--text-muted: #9e9e9e;--bg-primary: #ffffff;--bg-secondary: #f5f3ff;--bg-hover: #ede9fe;--bg-bubble-user: #ede9fe;--bg-bubble-assistant: #f5f5f5;--border-color: #e0e0e0;--shadow: rgba(0,0,0,.15)}.ai-panel{position:fixed;top:0;right:0;height:100vh;background:var(--bg-primary);box-shadow:-4px 0 24px var(--shadow);display:flex;flex-direction:column;z-index:1040;transform:translate(100%);transition:transform .3s cubic-bezier(.16,1,.3,1);will-change:transform,width;pointer-events:none}.ai-panel.open{transform:translate(0);pointer-events:auto}.ai-resize-handle{position:absolute;top:0;left:-4px;width:8px;height:100%;cursor:ew-resize;z-index:1;background:transparent}.ai-resize-handle:hover{background:linear-gradient(to right,transparent,rgba(124,58,237,.25),transparent)}.header-actions{display:flex;align-items:center;gap:4px}.icon-btn{background:none;border:none;cursor:pointer;color:var(--text-muted);width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:background .15s,color .15s}.icon-btn:hover{background:var(--bg-hover);color:var(--text-primary)}.panel-content{flex:1;overflow-y:auto;padding:12px 14px;display:flex;flex-direction:column;gap:10px}.quick-prompts{display:flex;flex-wrap:wrap;gap:6px;justify-content:center;margin-top:8px}.quick-prompt{background:var(--bg-secondary);border:1px solid var(--border-color);color:var(--text-secondary);padding:6px 10px;border-radius:14px;font-size:12px;cursor:pointer;transition:background .15s,color .15s}.quick-prompt:hover{background:var(--bg-hover);color:var(--text-primary)}.bubble{max-width:90%;padding:10px 12px;border-radius:10px;font-size:13.5px;line-height:1.5;word-wrap:break-word;white-space:pre-wrap}.bubble.user{align-self:flex-end;background:var(--bg-bubble-user);color:var(--text-primary)}.bubble.assistant{align-self:flex-start;background:var(--bg-bubble-assistant);color:var(--text-primary)}.bubble-text{display:block}.thinking{display:flex;align-items:center;gap:6px;color:var(--text-muted);font-size:12.5px}.spinner-small{width:12px;height:12px;border:2px solid var(--border-color);border-top-color:var(--primary);border-radius:50%;animation:spin .7s linear infinite;display:inline-block}@keyframes spin{to{transform:rotate(360deg)}}.tool-strip{display:flex;flex-direction:column;gap:4px;margin-bottom:6px}.tool-chip{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border-radius:12px;background:var(--bg-hover);font-size:11.5px;color:var(--text-secondary);border:1px solid var(--border-color);align-self:flex-start}.tool-chip .dot{width:6px;height:6px;border-radius:50%;background:var(--text-muted)}.tool-chip.running .dot{background:var(--primary);animation:pulse 1s ease-in-out infinite}.tool-chip.ok .dot{background:var(--success)}.tool-chip.err .dot{background:var(--error)}.tool-chip.await .dot{background:#f59e0b}.tool-chip.declined .dot{background:var(--text-muted)}.tool-chip .tool-name{font-weight:600;color:var(--text-primary)}.tool-chip .muted{color:var(--text-muted)}@keyframes pulse{50%{opacity:.4}}.approval-card{flex-shrink:0;margin:8px 14px;padding:12px 14px;border:1px solid var(--primary);border-radius:10px;background:var(--bg-secondary)}.approval-card-head{display:flex;align-items:center;gap:10px;margin-bottom:8px;color:var(--text-primary)}.approval-card-head code{background:var(--bg-hover);padding:2px 6px;border-radius:6px;font-size:12px;color:var(--primary)}.approval-args{background:var(--bg-primary);border:1px solid var(--border-color);border-radius:6px;padding:8px;font-size:11.5px;max-height:100px;overflow:auto;margin:0 0 8px;color:var(--text-secondary)}.approval-actions{display:flex;gap:6px;flex-wrap:wrap}.btn{flex:1;padding:6px 10px;font-size:12.5px;font-weight:600;border-radius:6px;border:1px solid var(--border-color);background:var(--bg-primary);color:var(--text-primary);cursor:pointer;transition:background .15s,border-color .15s}.btn:hover{background:var(--bg-hover)}.btn.primary{background:var(--primary);color:#fff;border-color:var(--primary)}.btn.primary:hover{background:var(--primary-strong)}.btn.danger{color:var(--error)}.btn.danger:hover{background:#ef53501a}.composer{flex-shrink:0;border-top:1px solid var(--border-color);padding:10px 12px;background:var(--bg-secondary)}.composer-error{color:var(--error);font-size:12px;margin-bottom:6px}.composer-row{display:flex;align-items:flex-end;gap:8px}.composer textarea{flex:1;resize:none;padding:8px 10px;border-radius:8px;border:1px solid var(--border-color);background:var(--bg-primary);color:var(--text-primary);font-size:13px;font-family:inherit;outline:none;min-height:36px;max-height:160px;line-height:1.4}.composer textarea:focus{border-color:var(--primary)}.composer textarea:disabled{opacity:.6}.send-btn{width:36px;height:36px;flex-shrink:0;display:flex;align-items:center;justify-content:center;background:var(--primary);color:#fff;border:none;border-radius:8px;cursor:pointer;transition:background .15s}.send-btn:hover:not(:disabled){background:var(--primary-strong)}.send-btn:disabled{opacity:.5;cursor:not-allowed}.send-btn.cancel{background:var(--error)}.send-btn.cancel:hover{filter:brightness(1.1)}.md-body{font-size:13.5px;line-height:1.55}.md-body .md-p{margin:0 0 8px}.md-body .md-p:last-child{margin-bottom:0}.md-body .md-h1,.md-body .md-h2,.md-body .md-h3,.md-body .md-h4,.md-body .md-h5,.md-body .md-h6{margin:12px 0 6px;font-weight:700;color:var(--text-primary);line-height:1.3}.md-body .md-h1{font-size:17px}.md-body .md-h2{font-size:15.5px}.md-body .md-h3{font-size:14.5px}.md-body .md-h4,.md-body .md-h5,.md-body .md-h6{font-size:13.5px}.md-body strong{font-weight:700}.md-body em{font-style:italic}.md-body .md-link{color:var(--primary);text-decoration:underline;text-underline-offset:2px}.md-body .md-link:hover{color:var(--primary-strong)}.md-body .md-list{margin:0 0 8px;padding-left:22px}.md-body .md-list li{margin:2px 0}.md-body .md-list li::marker{color:var(--text-muted)}.md-body .md-quote{margin:0 0 8px;padding:6px 10px;border-left:3px solid var(--primary);background:var(--bg-hover);color:var(--text-secondary);border-radius:0 6px 6px 0}.md-body .md-code{background:var(--bg-hover);border:1px solid var(--border-color);border-radius:4px;padding:1px 5px;font-size:12px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:var(--primary)}.md-body .md-pre{background:var(--bg-primary);border:1px solid var(--border-color);border-radius:8px;padding:10px 12px;margin:0 0 8px;overflow-x:auto;font-size:12px;line-height:1.5}.md-body .md-pre code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:var(--text-primary);background:none;border:none;padding:0}.md-body .md-table-wrap{margin:0 0 8px;overflow-x:auto;border:1px solid var(--border-color);border-radius:8px}.md-body .md-table{border-collapse:collapse;width:100%;font-size:12.5px}.md-body .md-table th,.md-body .md-table td{padding:6px 10px;border-bottom:1px solid var(--border-color);vertical-align:top;text-align:left}.md-body .md-table th{background:var(--bg-secondary);color:var(--text-primary);font-weight:600}.md-body .md-table tr:last-child td{border-bottom:none}.md-body .md-table tbody tr:hover{background:var(--bg-hover)}\n"] }]
|
|
2783
|
+
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"] }]
|
|
2178
2784
|
}], ctorParameters: () => [], propDecorators: { scrollContainer: [{ type: i0.ViewChild, args: ['scrollContainer', { isSignal: true }] }], textArea: [{ type: i0.ViewChild, args: ['textArea', { isSignal: true }] }], themeClass: [{
|
|
2179
2785
|
type: HostBinding,
|
|
2180
2786
|
args: ['class']
|
|
@@ -3259,5 +3865,5 @@ async function runReturnViaPostMessageIfRequested(authService) {
|
|
|
3259
3865
|
* Generated bundle index. Do not edit.
|
|
3260
3866
|
*/
|
|
3261
3867
|
|
|
3262
|
-
export { ALL_ACTIONS, AVATAR_FRAMES, ApprovalActionType, ApprovalDocumentStatus, ApprovalStepMode, ApprovalStepStatus, DEFAULT_AI_CONFIG, MES_AUTH_AI_CONFIG, MES_AUTH_CONFIG, MaAiButtonComponent, MaAiChatPanelComponent, MaAiMarkdownPipe, MaAiService, MaAiToolsRegistry, MaApprovalPanelComponent, MaApprovalService, MaArvContainerComponent, MaAvatarComponent, MaIconComponent, MaThemeDirective, MaUiConfigService, MaUserComponent, MaUserMenuColor, MaUserMenuComponent, MaUserXComponent, MesAuthModule, MesAuthService, NotificationBadgeComponent, NotificationPanelComponent, NotificationType, PACKAGE_VERSION, ThemeService, ToastContainerComponent, ToastService, UserProfileComponent, extractXMaPerm, mesAuthInterceptor, provideMesAuth, provideMesAuthAi, renderMarkdown, runReturnViaPostMessageIfRequested, runSsoCheckHandshake, withXMaPerm, xMaResource };
|
|
3868
|
+
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 };
|
|
3263
3869
|
//# sourceMappingURL=mesauth-angular.mjs.map
|