mesauth-angular 1.22.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/fesm2022/mesauth-angular.mjs +1526 -1006
- package/fesm2022/mesauth-angular.mjs.map +1 -1
- package/package.json +1 -1
- package/types/mesauth-angular.d.ts +128 -4
|
@@ -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, ElementRef,
|
|
2
|
+
import { signal, Injectable, InjectionToken, makeEnvironmentProviders, provideAppInitializer, inject, NgModule, afterNextRender, input, booleanAttribute, computed, HostBinding, Component, output, Injector, ChangeDetectionStrategy, effect, HostListener, ElementRef, Pipe, viewChild, ViewChild, DestroyRef, Directive } from '@angular/core';
|
|
3
3
|
import { toObservable, toSignal, takeUntilDestroyed, rxResource } from '@angular/core/rxjs-interop';
|
|
4
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.
|
|
@@ -772,201 +772,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImpo
|
|
|
772
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"] }]
|
|
773
773
|
}], propDecorators: { clicked: [{ type: i0.Output, args: ["clicked"] }] } });
|
|
774
774
|
|
|
775
|
-
class UserProfileComponent {
|
|
776
|
-
inputAvatarShape = input('circle', ...(ngDevMode ? [{ debugName: "inputAvatarShape" }] : /* istanbul ignore next */ []));
|
|
777
|
-
showBell = input(true, ...(ngDevMode ? [{ debugName: "showBell" }] : /* istanbul ignore next */ []));
|
|
778
|
-
showApproval = input(true, ...(ngDevMode ? [{ debugName: "showApproval" }] : /* istanbul ignore next */ []));
|
|
779
|
-
showName = input(false, ...(ngDevMode ? [{ debugName: "showName" }] : /* istanbul ignore next */ []));
|
|
780
|
-
showAi = input(true, ...(ngDevMode ? [{ debugName: "showAi" }] : /* istanbul ignore next */ []));
|
|
781
|
-
showSignature = input([false, false], ...(ngDevMode ? [{ debugName: "showSignature" }] : /* istanbul ignore next */ []));
|
|
782
|
-
signatureHeight = input(40, ...(ngDevMode ? [{ debugName: "signatureHeight" }] : /* istanbul ignore next */ []));
|
|
783
|
-
notificationClick = output();
|
|
784
|
-
approvalClick = output();
|
|
785
|
-
aiClick = output();
|
|
786
|
-
get themeClass() {
|
|
787
|
-
return `theme-${this.themeService.currentTheme()}`;
|
|
788
|
-
}
|
|
789
|
-
currentUser = signal(null, ...(ngDevMode ? [{ debugName: "currentUser" }] : /* istanbul ignore next */ []));
|
|
790
|
-
unreadCount = signal(0, ...(ngDevMode ? [{ debugName: "unreadCount" }] : /* istanbul ignore next */ []));
|
|
791
|
-
pendingApprovalCount = signal(0, ...(ngDevMode ? [{ debugName: "pendingApprovalCount" }] : /* istanbul ignore next */ []));
|
|
792
|
-
dropdownOpen = signal(false, ...(ngDevMode ? [{ debugName: "dropdownOpen" }] : /* istanbul ignore next */ []));
|
|
793
|
-
avatarRefresh = signal(Date.now(), ...(ngDevMode ? [{ debugName: "avatarRefresh" }] : /* istanbul ignore next */ []));
|
|
794
|
-
// Avatar style derived from per-user preferences
|
|
795
|
-
navAvatarSize = computed(() => this.currentUser()?.avatarSize ?? 'md', ...(ngDevMode ? [{ debugName: "navAvatarSize" }] : /* istanbul ignore next */ []));
|
|
796
|
-
// Dropdown is always one step larger than the nav avatar
|
|
797
|
-
dropAvatarSize = computed(() => {
|
|
798
|
-
const s = this.navAvatarSize();
|
|
799
|
-
return s === 'sm' ? 'md' : s === 'md' ? 'lg' : 'xl';
|
|
800
|
-
}, ...(ngDevMode ? [{ debugName: "dropAvatarSize" }] : /* istanbul ignore next */ []));
|
|
801
|
-
avatarShape = computed(() => this.currentUser()?.avatarShape ?? this.inputAvatarShape(), ...(ngDevMode ? [{ debugName: "avatarShape" }] : /* istanbul ignore next */ []));
|
|
802
|
-
avatarFrame = computed(() => this.currentUser()?.avatarFrame ?? null, ...(ngDevMode ? [{ debugName: "avatarFrame" }] : /* istanbul ignore next */ []));
|
|
803
|
-
avatarRatio = computed(() => this.currentUser()?.avatarRatio ?? 'ar-11', ...(ngDevMode ? [{ debugName: "avatarRatio" }] : /* istanbul ignore next */ []));
|
|
804
|
-
givenStyle = computed(() => this.currentUser()?.givenColor || 'indigo', ...(ngDevMode ? [{ debugName: "givenStyle" }] : /* istanbul ignore next */ []));
|
|
805
|
-
signatureBroken = signal(false, ...(ngDevMode ? [{ debugName: "signatureBroken" }] : /* istanbul ignore next */ []));
|
|
806
|
-
signatureUrl = computed(() => {
|
|
807
|
-
if (!this.showSignature().some(s => s) || this.signatureBroken())
|
|
808
|
-
return null;
|
|
809
|
-
const baseRaw = this.authService.getConfig()?.apiBaseUrl ?? '';
|
|
810
|
-
const base = baseRaw.replace(/\/$/, '');
|
|
811
|
-
const u = this.currentUser();
|
|
812
|
-
const id = u?.userId ?? u?.id;
|
|
813
|
-
if (!id || !base)
|
|
814
|
-
return null;
|
|
815
|
-
return `${base}/auth/${id}/signature`;
|
|
816
|
-
}, ...(ngDevMode ? [{ debugName: "signatureUrl" }] : /* istanbul ignore next */ []));
|
|
817
|
-
authService = inject(MesAuthService);
|
|
818
|
-
router = inject(Router);
|
|
819
|
-
themeService = inject(ThemeService);
|
|
820
|
-
http = inject(HttpClient);
|
|
821
|
-
constructor() {
|
|
822
|
-
const currentUserSig = toSignal(this.authService.currentUser$, { initialValue: null });
|
|
823
|
-
const approvalEvent = toSignal(this.authService.approvalEvents$);
|
|
824
|
-
const notification = toSignal(this.authService.notifications$);
|
|
825
|
-
effect(() => {
|
|
826
|
-
const user = currentUserSig();
|
|
827
|
-
this.currentUser.set(user);
|
|
828
|
-
this.avatarRefresh.set(Date.now());
|
|
829
|
-
if (!user) {
|
|
830
|
-
this.unreadCount.set(0);
|
|
831
|
-
this.pendingApprovalCount.set(0);
|
|
832
|
-
return;
|
|
833
|
-
}
|
|
834
|
-
this.loadUnreadCount();
|
|
835
|
-
this.loadPendingApprovalCount();
|
|
836
|
-
});
|
|
837
|
-
effect(() => {
|
|
838
|
-
approvalEvent(); // track SignalR approval events
|
|
839
|
-
if (currentUserSig())
|
|
840
|
-
this.loadPendingApprovalCount();
|
|
841
|
-
});
|
|
842
|
-
effect(() => {
|
|
843
|
-
notification(); // track SignalR notification events
|
|
844
|
-
if (currentUserSig())
|
|
845
|
-
this.loadUnreadCount();
|
|
846
|
-
});
|
|
847
|
-
}
|
|
848
|
-
loadUnreadCount() {
|
|
849
|
-
this.authService.getUnreadCount().subscribe({
|
|
850
|
-
next: (response) => this.unreadCount.set(response.unreadCount || 0),
|
|
851
|
-
error: () => { }
|
|
852
|
-
});
|
|
853
|
-
}
|
|
854
|
-
loadPendingApprovalCount() {
|
|
855
|
-
const config = this.authService.getConfig();
|
|
856
|
-
if (!config)
|
|
857
|
-
return;
|
|
858
|
-
const url = `${config.apiBaseUrl.replace(/\/$/, '')}/approval/dashboard`;
|
|
859
|
-
this.http.get(url, { withCredentials: config.withCredentials ?? true }).subscribe({
|
|
860
|
-
next: (r) => this.pendingApprovalCount.set(r?.pendingCount ?? 0),
|
|
861
|
-
error: () => { }
|
|
862
|
-
});
|
|
863
|
-
}
|
|
864
|
-
onApprovalClick() {
|
|
865
|
-
this.approvalClick.emit();
|
|
866
|
-
}
|
|
867
|
-
onAiClick() {
|
|
868
|
-
this.aiClick.emit();
|
|
869
|
-
}
|
|
870
|
-
getAvatarUrl(user) {
|
|
871
|
-
// Use the refresh signal to force update
|
|
872
|
-
const refresh = this.avatarRefresh();
|
|
873
|
-
const config = this.authService.getConfig();
|
|
874
|
-
const baseUrl = config?.apiBaseUrl || '';
|
|
875
|
-
if (user.avatarPath) {
|
|
876
|
-
if (user.avatarPath.startsWith('http://') || user.avatarPath.startsWith('https://')) {
|
|
877
|
-
return user.avatarPath;
|
|
878
|
-
}
|
|
879
|
-
return `${baseUrl.replace(/\/$/, '')}${user.avatarPath}?t=${refresh}`;
|
|
880
|
-
}
|
|
881
|
-
const userId = user.userId;
|
|
882
|
-
if (userId && baseUrl) {
|
|
883
|
-
return `${baseUrl.replace(/\/$/, '')}/auth/${userId}/avatar?t=${refresh}`;
|
|
884
|
-
}
|
|
885
|
-
const displayName = user.userName || user.userId || 'User';
|
|
886
|
-
return `https://ui-avatars.com/api/?name=${encodeURIComponent(displayName)}&background=1976d2&color=fff`;
|
|
887
|
-
}
|
|
888
|
-
getLastNameInitial(user) {
|
|
889
|
-
const fullName = user.fullName || user.userName || 'U';
|
|
890
|
-
const parts = fullName.split(' ');
|
|
891
|
-
const lastPart = parts[parts.length - 1];
|
|
892
|
-
return lastPart.charAt(0).toUpperCase();
|
|
893
|
-
}
|
|
894
|
-
toggleDropdown() {
|
|
895
|
-
this.dropdownOpen.set(!this.dropdownOpen());
|
|
896
|
-
}
|
|
897
|
-
closeDropdown() {
|
|
898
|
-
this.dropdownOpen.set(false);
|
|
899
|
-
}
|
|
900
|
-
onDocumentClick(event) {
|
|
901
|
-
const target = event.target;
|
|
902
|
-
const clickedInside = target.closest('.user-menu-wrapper');
|
|
903
|
-
if (!clickedInside) {
|
|
904
|
-
this.dropdownOpen.set(false);
|
|
905
|
-
return;
|
|
906
|
-
}
|
|
907
|
-
if (target.closest('.ma-user-menu-btn')) {
|
|
908
|
-
this.dropdownOpen.set(false);
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
onLogin() {
|
|
912
|
-
const config = this.authService.getConfig();
|
|
913
|
-
const baseUrl = config?.userBaseUrl || '';
|
|
914
|
-
const returnUrl = encodeURIComponent(this.router.url);
|
|
915
|
-
window.location.href = `${baseUrl}/login?returnUrl=${returnUrl}`;
|
|
916
|
-
}
|
|
917
|
-
onViewProfile() {
|
|
918
|
-
const config = this.authService.getConfig();
|
|
919
|
-
const baseUrl = config?.userBaseUrl || '';
|
|
920
|
-
const currentUrl = window.location.href;
|
|
921
|
-
this.openInNewTabIfSameOrigin(currentUrl, `${baseUrl}/profile`);
|
|
922
|
-
this.dropdownOpen.set(false);
|
|
923
|
-
}
|
|
924
|
-
openInNewTabIfSameOrigin(currentUrl, destinationUrl) {
|
|
925
|
-
// Check if current page URL starts with the destination URL
|
|
926
|
-
if (!destinationUrl.startsWith(currentUrl)) {
|
|
927
|
-
window.open(destinationUrl, "_blank", "noopener,noreferrer");
|
|
928
|
-
}
|
|
929
|
-
else {
|
|
930
|
-
// Optional: redirect in same tab
|
|
931
|
-
window.location.href = destinationUrl;
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
onLogout() {
|
|
935
|
-
this.authService.logout().subscribe({
|
|
936
|
-
next: () => {
|
|
937
|
-
this.dropdownOpen.set(false);
|
|
938
|
-
const config = this.authService.getConfig();
|
|
939
|
-
const baseUrl = config?.userBaseUrl || '';
|
|
940
|
-
const returnUrl = encodeURIComponent(window.location.href);
|
|
941
|
-
window.location.href = `${baseUrl}/login?returnUrl=${returnUrl}`;
|
|
942
|
-
},
|
|
943
|
-
error: () => {
|
|
944
|
-
const config = this.authService.getConfig();
|
|
945
|
-
const baseUrl = config?.userBaseUrl || '';
|
|
946
|
-
window.location.href = `${baseUrl}/login`;
|
|
947
|
-
}
|
|
948
|
-
});
|
|
949
|
-
}
|
|
950
|
-
onNotificationClick() {
|
|
951
|
-
this.notificationClick.emit();
|
|
952
|
-
}
|
|
953
|
-
onSigErr(_ev) {
|
|
954
|
-
this.signatureBroken.set(true);
|
|
955
|
-
}
|
|
956
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: UserProfileComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
957
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: UserProfileComponent, isStandalone: true, selector: "ma-user-profile", inputs: { inputAvatarShape: { classPropertyName: "inputAvatarShape", publicName: "inputAvatarShape", isSignal: true, isRequired: false, transformFunction: null }, showBell: { classPropertyName: "showBell", publicName: "showBell", isSignal: true, isRequired: false, transformFunction: null }, showApproval: { classPropertyName: "showApproval", publicName: "showApproval", isSignal: true, isRequired: false, transformFunction: null }, showName: { classPropertyName: "showName", publicName: "showName", isSignal: true, isRequired: false, transformFunction: null }, showAi: { classPropertyName: "showAi", publicName: "showAi", isSignal: true, isRequired: false, transformFunction: null }, showSignature: { classPropertyName: "showSignature", publicName: "showSignature", isSignal: true, isRequired: false, transformFunction: null }, signatureHeight: { classPropertyName: "signatureHeight", publicName: "signatureHeight", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { notificationClick: "notificationClick", approvalClick: "approvalClick", aiClick: "aiClick" }, host: { listeners: { "document:click": "onDocumentClick($event)" }, properties: { "class": "this.themeClass" } }, ngImport: i0, template: "<div class=\"user-profile-container\">\n @if (!currentUser()) {\n <!-- Not logged in -->\n <button class=\"login-btn\" (click)=\"onLogin()\">Login</button>\n } @else {\n <!-- Logged in -->\n <div class=\"user-header\">\n <!-- Ask AI -->\n @if (showAi()) {\n <ma-ai-button (clicked)=\"onAiClick()\"></ma-ai-button>\n }\n\n <!-- Notification Bell -->\n @if (showBell()) {\n <button class=\"notification-btn\" [class.has-unread]=\"unreadCount() > 0\" (click)=\"onNotificationClick()\" title=\"Notifications\" aria-label=\"Notifications\">\n <svg class=\"bell-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9\"/>\n <path d=\"M13.73 21a2 2 0 0 1-3.46 0\"/>\n </svg>\n @if (unreadCount() > 0) {\n <span class=\"badge\">{{ unreadCount() > 99 ? '99+' : unreadCount() }}</span>\n }\n </button>\n }\n \n\n <!-- Approval Button -->\n @if (showApproval()) {\n <button class=\"notification-btn\" [class.has-unread]=\"pendingApprovalCount() > 0\" (click)=\"onApprovalClick()\" title=\"Approvals\" aria-label=\"Approvals\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M9 11l3 3L22 4\"/>\n <path d=\"M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11\"/>\n </svg>\n @if (pendingApprovalCount() > 0) {\n <span class=\"badge\">{{ pendingApprovalCount() > 99 ? '99+' : pendingApprovalCount() }}</span>\n }\n </button>\n } \n\n <!-- User Avatar + Dropdown -->\n <div class=\"user-menu-wrapper\">\n <button class=\"user-menu-btn\" (click)=\"toggleDropdown()\" [attr.aria-label]=\"'User menu for ' + (currentUser().fullName || currentUser().userName)\" aria-haspopup=\"true\" [attr.aria-expanded]=\"dropdownOpen()\">\n <ma-avatar\n [src]=\"getAvatarUrl(currentUser())\"\n [alt]=\"currentUser().fullName || currentUser().userName || ''\"\n [initials]=\"getLastNameInitial(currentUser())\"\n [size]=\"navAvatarSize()\"\n [shape]=\"avatarShape()\"\n [ratio]=\"avatarRatio()\"\n [frame]=\"avatarFrame()\" \n [ring]=\"true\"\n [ringActive]=\"dropdownOpen()\" /> \n </button>\n\n @if (dropdownOpen()) {\n <div class=\"mes-dropdown-menu\">\n <!-- User info header -->\n <div class=\"mes-dropdown-header\">\n <div class=\"dropdown-avatar-wrap\">\n <ma-avatar\n [src]=\"getAvatarUrl(currentUser())\"\n [alt]=\"currentUser().fullName || currentUser().userName || ''\"\n [initials]=\"getLastNameInitial(currentUser())\"\n [size]=\"dropAvatarSize()\"\n [shape]=\"avatarShape()\"\n [ratio]=\"avatarRatio()\"\n [frame]=\"avatarFrame()\"\n [scale]=\"1.5\" /> \n </div> \n <div class=\"dropdown-info-col\">\n <div class=\"dropdown-user-info\">\n <span class=\"dropdown-user-name\">{{ currentUser().fullName || currentUser().userName }}</span> \n <span class=\"dropdown-user-sub\">\n @if (currentUser().position || currentUser().department) {\n {{ currentUser().position || currentUser().department }} \n }\n @if (currentUser().givenTitle) {\n <span class=\"given-title-badge given-title\"\n [class]=\"'given-color-' + givenStyle()\">{{ currentUser().givenTitle }}</span>\n }\n </span>\n </div>\n <div class=\"dropdown-user-actions\">\n @if (showSignature()[1] && signatureUrl(); as sigUrl) {\n <img class=\"ma-ux-signature\"\n [src]=\"sigUrl\"\n [style.height.px]=\"signatureHeight()\"\n (error)=\"onSigErr($event)\"\n alt=\"signature\" />\n }\n <button class=\"icon-action profile-link\" (click)=\"onViewProfile()\" title=\"View Profile\" aria-label=\"View Profile\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2\"/><circle cx=\"12\" cy=\"7\" r=\"4\"/>\n </svg>\n </button>\n <button class=\"icon-action logout-item\" (click)=\"onLogout()\" title=\"Logout\" aria-label=\"Logout\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\"/><polyline points=\"16 17 21 12 16 7\"/><line x1=\"21\" y1=\"12\" x2=\"9\" y2=\"12\"/>\n </svg>\n </button>\n </div>\n </div>\n </div>\n\n <div class=\"mes-dropdown-items\">\n <ng-content></ng-content>\n </div>\n </div>\n }\n </div>\n\n @if (showName()){\n <div class=\"mes-user-header\">\n {{currentUser().fullName || currentUser().userName}}\n </div>\n }\n @if (showSignature()[0] && signatureUrl(); as sigUrl) {\n <img class=\"ma-ux-signature\"\n [src]=\"sigUrl\"\n [style.height.px]=\"signatureHeight()\"\n (error)=\"onSigErr($event)\"\n alt=\"signature\" />\n }\n </div>\n }\n</div>\n", styles: [".user-profile-container{display:flex;align-items:center;gap:4px}.login-btn{padding:7px 18px;background-color:var(--primary-color);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:500;font-size:13px;letter-spacing:.2px;transition:background-color .2s,transform .15s}.login-btn:hover{background-color:var(--primary-hover);transform:translateY(-1px)}.user-header{display:flex;align-items:center;gap:4px}.mes-user-header{margin-left:10px;font-weight:600;font-size:14px;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.notification-btn{position:relative;background:none;border:none;cursor:pointer;padding:8px;border-radius:10px;color:var(--text-secondary);display:flex;align-items:center;justify-content:center;transition:color .2s,background-color .2s}.notification-btn:hover{background-color:var(--primary-light);color:var(--primary-color)}.notification-btn.has-unread{color:var(--primary-color)}.bell-icon{display:block;transition:transform .35s cubic-bezier(.34,1.56,.64,1)}.notification-btn:hover .bell-icon{transform:rotate(-20deg) scale(1.15)}.badge{position:absolute;top:2px;right:2px;background-color:var(--error-color);color:#fff;border-radius:10px;min-width:17px;height:17px;padding:0 4px;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;line-height:1;box-shadow:0 0 0 2px var(--bg-primary);animation:badge-pop .25s cubic-bezier(.34,1.56,.64,1)}@keyframes badge-pop{0%{transform:scale(0)}to{transform:scale(1)}}.user-menu-wrapper{position:relative}.user-menu-btn{background:none;border:none;cursor:pointer;padding:4px;display:flex;align-items:center;justify-content:center;transition:transform .2s}.user-menu-btn:hover{transform:scale(1.06)}.mes-dropdown-menu{position:absolute;top:calc(100% + 10px);right:0;background:var(--bg-primary);border:1px solid var(--border-color);border-radius:14px;box-shadow:0 8px 32px var(--shadow-lg),0 2px 8px var(--shadow);min-width:220px;z-index:1000;overflow:hidden;animation:dropdown-in .16s cubic-bezier(.16,1,.3,1)}@keyframes dropdown-in{0%{opacity:0;transform:translateY(-8px) scale(.96)}to{opacity:1;transform:translateY(0) scale(1)}}.mes-dropdown-header{display:flex;align-items:center;gap:12px;padding:16px;background:var(--bg-secondary)}.dropdown-avatar-wrap{flex-shrink:0}.mes-dropdown-header .dropdown-avatar-wrap{margin-right:5px}.dropdown-user-info{display:flex;flex-direction:column;gap:3px;min-width:0}.dropdown-user-name{font-weight:600;font-size:14px;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:5px;min-width:180px}.dropdown-user-sub{font-size:11px;color:var(--primary-color);font-weight:500;white-space:nowrap;text-overflow:ellipsis;margin-bottom:12px}.given-title{float:inline-end}.mes-dropdown-divider{height:1px;background:var(--border-color)}.dropdown-info-col{display:flex;flex-direction:column;gap:6px;min-width:0;flex:1}.dropdown-user-actions{display:flex;align-items:center;justify-content:flex-end;gap:4px}.icon-action{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border:none;background:none;border-radius:8px;cursor:pointer;color:var(--text-secondary);transition:background-color .15s,color .15s,transform .15s}.icon-action:hover{transform:translateY(-1px)}.icon-action.profile-link{color:var(--primary-color)}.icon-action.profile-link:hover{background-color:var(--primary-light)}.icon-action.logout-item{color:var(--error-color)}.icon-action.logout-item:hover{background-color:var(--error-light)}.mes-dropdown-items:not(:empty){border-top:1px solid var(--border-color)}.ma-ux-signature{object-fit:contain;max-width:160px;vertical-align:middle;background:transparent}\n"], dependencies: [{ kind: "component", type: MaAvatarComponent, selector: "ma-avatar", inputs: ["src", "alt", "initials", "size", "shape", "frame", "ratio", "scale", "ring", "ringActive"] }, { kind: "component", type: MaAiButtonComponent, selector: "ma-ai-button", outputs: ["clicked"] }] });
|
|
958
|
-
}
|
|
959
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: UserProfileComponent, decorators: [{
|
|
960
|
-
type: Component,
|
|
961
|
-
args: [{ selector: 'ma-user-profile', imports: [MaAvatarComponent, MaAiButtonComponent], template: "<div class=\"user-profile-container\">\n @if (!currentUser()) {\n <!-- Not logged in -->\n <button class=\"login-btn\" (click)=\"onLogin()\">Login</button>\n } @else {\n <!-- Logged in -->\n <div class=\"user-header\">\n <!-- Ask AI -->\n @if (showAi()) {\n <ma-ai-button (clicked)=\"onAiClick()\"></ma-ai-button>\n }\n\n <!-- Notification Bell -->\n @if (showBell()) {\n <button class=\"notification-btn\" [class.has-unread]=\"unreadCount() > 0\" (click)=\"onNotificationClick()\" title=\"Notifications\" aria-label=\"Notifications\">\n <svg class=\"bell-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9\"/>\n <path d=\"M13.73 21a2 2 0 0 1-3.46 0\"/>\n </svg>\n @if (unreadCount() > 0) {\n <span class=\"badge\">{{ unreadCount() > 99 ? '99+' : unreadCount() }}</span>\n }\n </button>\n }\n \n\n <!-- Approval Button -->\n @if (showApproval()) {\n <button class=\"notification-btn\" [class.has-unread]=\"pendingApprovalCount() > 0\" (click)=\"onApprovalClick()\" title=\"Approvals\" aria-label=\"Approvals\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M9 11l3 3L22 4\"/>\n <path d=\"M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11\"/>\n </svg>\n @if (pendingApprovalCount() > 0) {\n <span class=\"badge\">{{ pendingApprovalCount() > 99 ? '99+' : pendingApprovalCount() }}</span>\n }\n </button>\n } \n\n <!-- User Avatar + Dropdown -->\n <div class=\"user-menu-wrapper\">\n <button class=\"user-menu-btn\" (click)=\"toggleDropdown()\" [attr.aria-label]=\"'User menu for ' + (currentUser().fullName || currentUser().userName)\" aria-haspopup=\"true\" [attr.aria-expanded]=\"dropdownOpen()\">\n <ma-avatar\n [src]=\"getAvatarUrl(currentUser())\"\n [alt]=\"currentUser().fullName || currentUser().userName || ''\"\n [initials]=\"getLastNameInitial(currentUser())\"\n [size]=\"navAvatarSize()\"\n [shape]=\"avatarShape()\"\n [ratio]=\"avatarRatio()\"\n [frame]=\"avatarFrame()\" \n [ring]=\"true\"\n [ringActive]=\"dropdownOpen()\" /> \n </button>\n\n @if (dropdownOpen()) {\n <div class=\"mes-dropdown-menu\">\n <!-- User info header -->\n <div class=\"mes-dropdown-header\">\n <div class=\"dropdown-avatar-wrap\">\n <ma-avatar\n [src]=\"getAvatarUrl(currentUser())\"\n [alt]=\"currentUser().fullName || currentUser().userName || ''\"\n [initials]=\"getLastNameInitial(currentUser())\"\n [size]=\"dropAvatarSize()\"\n [shape]=\"avatarShape()\"\n [ratio]=\"avatarRatio()\"\n [frame]=\"avatarFrame()\"\n [scale]=\"1.5\" /> \n </div> \n <div class=\"dropdown-info-col\">\n <div class=\"dropdown-user-info\">\n <span class=\"dropdown-user-name\">{{ currentUser().fullName || currentUser().userName }}</span> \n <span class=\"dropdown-user-sub\">\n @if (currentUser().position || currentUser().department) {\n {{ currentUser().position || currentUser().department }} \n }\n @if (currentUser().givenTitle) {\n <span class=\"given-title-badge given-title\"\n [class]=\"'given-color-' + givenStyle()\">{{ currentUser().givenTitle }}</span>\n }\n </span>\n </div>\n <div class=\"dropdown-user-actions\">\n @if (showSignature()[1] && signatureUrl(); as sigUrl) {\n <img class=\"ma-ux-signature\"\n [src]=\"sigUrl\"\n [style.height.px]=\"signatureHeight()\"\n (error)=\"onSigErr($event)\"\n alt=\"signature\" />\n }\n <button class=\"icon-action profile-link\" (click)=\"onViewProfile()\" title=\"View Profile\" aria-label=\"View Profile\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2\"/><circle cx=\"12\" cy=\"7\" r=\"4\"/>\n </svg>\n </button>\n <button class=\"icon-action logout-item\" (click)=\"onLogout()\" title=\"Logout\" aria-label=\"Logout\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\"/><polyline points=\"16 17 21 12 16 7\"/><line x1=\"21\" y1=\"12\" x2=\"9\" y2=\"12\"/>\n </svg>\n </button>\n </div>\n </div>\n </div>\n\n <div class=\"mes-dropdown-items\">\n <ng-content></ng-content>\n </div>\n </div>\n }\n </div>\n\n @if (showName()){\n <div class=\"mes-user-header\">\n {{currentUser().fullName || currentUser().userName}}\n </div>\n }\n @if (showSignature()[0] && signatureUrl(); as sigUrl) {\n <img class=\"ma-ux-signature\"\n [src]=\"sigUrl\"\n [style.height.px]=\"signatureHeight()\"\n (error)=\"onSigErr($event)\"\n alt=\"signature\" />\n }\n </div>\n }\n</div>\n", styles: [".user-profile-container{display:flex;align-items:center;gap:4px}.login-btn{padding:7px 18px;background-color:var(--primary-color);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:500;font-size:13px;letter-spacing:.2px;transition:background-color .2s,transform .15s}.login-btn:hover{background-color:var(--primary-hover);transform:translateY(-1px)}.user-header{display:flex;align-items:center;gap:4px}.mes-user-header{margin-left:10px;font-weight:600;font-size:14px;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.notification-btn{position:relative;background:none;border:none;cursor:pointer;padding:8px;border-radius:10px;color:var(--text-secondary);display:flex;align-items:center;justify-content:center;transition:color .2s,background-color .2s}.notification-btn:hover{background-color:var(--primary-light);color:var(--primary-color)}.notification-btn.has-unread{color:var(--primary-color)}.bell-icon{display:block;transition:transform .35s cubic-bezier(.34,1.56,.64,1)}.notification-btn:hover .bell-icon{transform:rotate(-20deg) scale(1.15)}.badge{position:absolute;top:2px;right:2px;background-color:var(--error-color);color:#fff;border-radius:10px;min-width:17px;height:17px;padding:0 4px;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;line-height:1;box-shadow:0 0 0 2px var(--bg-primary);animation:badge-pop .25s cubic-bezier(.34,1.56,.64,1)}@keyframes badge-pop{0%{transform:scale(0)}to{transform:scale(1)}}.user-menu-wrapper{position:relative}.user-menu-btn{background:none;border:none;cursor:pointer;padding:4px;display:flex;align-items:center;justify-content:center;transition:transform .2s}.user-menu-btn:hover{transform:scale(1.06)}.mes-dropdown-menu{position:absolute;top:calc(100% + 10px);right:0;background:var(--bg-primary);border:1px solid var(--border-color);border-radius:14px;box-shadow:0 8px 32px var(--shadow-lg),0 2px 8px var(--shadow);min-width:220px;z-index:1000;overflow:hidden;animation:dropdown-in .16s cubic-bezier(.16,1,.3,1)}@keyframes dropdown-in{0%{opacity:0;transform:translateY(-8px) scale(.96)}to{opacity:1;transform:translateY(0) scale(1)}}.mes-dropdown-header{display:flex;align-items:center;gap:12px;padding:16px;background:var(--bg-secondary)}.dropdown-avatar-wrap{flex-shrink:0}.mes-dropdown-header .dropdown-avatar-wrap{margin-right:5px}.dropdown-user-info{display:flex;flex-direction:column;gap:3px;min-width:0}.dropdown-user-name{font-weight:600;font-size:14px;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:5px;min-width:180px}.dropdown-user-sub{font-size:11px;color:var(--primary-color);font-weight:500;white-space:nowrap;text-overflow:ellipsis;margin-bottom:12px}.given-title{float:inline-end}.mes-dropdown-divider{height:1px;background:var(--border-color)}.dropdown-info-col{display:flex;flex-direction:column;gap:6px;min-width:0;flex:1}.dropdown-user-actions{display:flex;align-items:center;justify-content:flex-end;gap:4px}.icon-action{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border:none;background:none;border-radius:8px;cursor:pointer;color:var(--text-secondary);transition:background-color .15s,color .15s,transform .15s}.icon-action:hover{transform:translateY(-1px)}.icon-action.profile-link{color:var(--primary-color)}.icon-action.profile-link:hover{background-color:var(--primary-light)}.icon-action.logout-item{color:var(--error-color)}.icon-action.logout-item:hover{background-color:var(--error-light)}.mes-dropdown-items:not(:empty){border-top:1px solid var(--border-color)}.ma-ux-signature{object-fit:contain;max-width:160px;vertical-align:middle;background:transparent}\n"] }]
|
|
962
|
-
}], ctorParameters: () => [], propDecorators: { inputAvatarShape: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputAvatarShape", required: false }] }], showBell: [{ type: i0.Input, args: [{ isSignal: true, alias: "showBell", required: false }] }], showApproval: [{ type: i0.Input, args: [{ isSignal: true, alias: "showApproval", required: false }] }], showName: [{ type: i0.Input, args: [{ isSignal: true, alias: "showName", required: false }] }], showAi: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAi", required: false }] }], showSignature: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSignature", required: false }] }], signatureHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "signatureHeight", required: false }] }], notificationClick: [{ type: i0.Output, args: ["notificationClick"] }], approvalClick: [{ type: i0.Output, args: ["approvalClick"] }], aiClick: [{ type: i0.Output, args: ["aiClick"] }], themeClass: [{
|
|
963
|
-
type: HostBinding,
|
|
964
|
-
args: ['class']
|
|
965
|
-
}], onDocumentClick: [{
|
|
966
|
-
type: HostListener,
|
|
967
|
-
args: ['document:click', ['$event']]
|
|
968
|
-
}] } });
|
|
969
|
-
|
|
970
775
|
class ToastService {
|
|
971
776
|
_toasts = signal([], ...(ngDevMode ? [{ debugName: "_toasts" }] : /* istanbul ignore next */ []));
|
|
972
777
|
toasts = this._toasts.asReadonly();
|
|
@@ -993,900 +798,1459 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImpo
|
|
|
993
798
|
args: [{ providedIn: 'root' }]
|
|
994
799
|
}] });
|
|
995
800
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
}
|
|
1086
|
-
ngOnDestroy() {
|
|
1087
|
-
if (this.dateTimer !== null) {
|
|
1088
|
-
clearInterval(this.dateTimer);
|
|
1089
|
-
this.dateTimer = null;
|
|
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']
|
|
820
|
+
},
|
|
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' }
|
|
851
|
+
},
|
|
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
|
+
}
|
|
1090
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());
|
|
1091
901
|
}
|
|
1092
|
-
|
|
1093
|
-
this.
|
|
1094
|
-
next: (response) => {
|
|
1095
|
-
this.notifications.set(response.items || []);
|
|
1096
|
-
this.refreshDateLabels();
|
|
1097
|
-
},
|
|
1098
|
-
error: () => { }
|
|
1099
|
-
});
|
|
902
|
+
resolve(name) {
|
|
903
|
+
return this.list().find(t => t.name === name);
|
|
1100
904
|
}
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
this.
|
|
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) };
|
|
917
|
+
}
|
|
1104
918
|
}
|
|
1105
|
-
|
|
1106
|
-
|
|
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;
|
|
1107
950
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
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();
|
|
1110
959
|
}
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
if (!notification.isRead) {
|
|
1117
|
-
this.authService.markAsRead(notification.id).subscribe({
|
|
1118
|
-
next: () => {
|
|
1119
|
-
this.notifications.update(notifs => notifs.map(n => n.id === notification.id ? { ...n, isRead: true } : n));
|
|
1120
|
-
this.notificationRead.emit();
|
|
1121
|
-
},
|
|
1122
|
-
error: () => { }
|
|
1123
|
-
});
|
|
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;
|
|
1124
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);
|
|
1125
970
|
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
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);
|
|
1130
978
|
}
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
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));
|
|
1135
995
|
}
|
|
996
|
+
await this.executeClientTool(pending.callId, pending.name, pending.args);
|
|
1136
997
|
}
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
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
|
+
}
|
|
1156
1054
|
}
|
|
1157
|
-
|
|
1158
|
-
const
|
|
1159
|
-
const
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
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
|
+
}
|
|
1084
|
+
}
|
|
1165
1085
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
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;
|
|
1131
|
+
}
|
|
1175
1132
|
}
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
const
|
|
1179
|
-
this.
|
|
1180
|
-
|
|
1181
|
-
this.notifications.update(notifs => notifs.filter(n => n.id !== notificationId));
|
|
1182
|
-
if (wasUnread)
|
|
1183
|
-
this.notificationRead.emit();
|
|
1184
|
-
},
|
|
1185
|
-
error: () => { }
|
|
1186
|
-
});
|
|
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);
|
|
1187
1138
|
}
|
|
1188
|
-
|
|
1189
|
-
|
|
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 } });
|
|
1190
1142
|
}
|
|
1191
|
-
//
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
const date = new Date(normalizedDateString);
|
|
1195
|
-
if (isNaN(date.getTime()))
|
|
1196
|
-
return 'Invalid date';
|
|
1197
|
-
const diffMs = now.getTime() - date.getTime();
|
|
1198
|
-
const diffMins = Math.floor(diffMs / 60000);
|
|
1199
|
-
const diffHours = Math.floor(diffMs / 3600000);
|
|
1200
|
-
const diffDays = Math.floor(diffMs / 86400000);
|
|
1201
|
-
if (diffMins < 1)
|
|
1202
|
-
return 'Now';
|
|
1203
|
-
if (diffMins < 60)
|
|
1204
|
-
return `${diffMins}m ago`;
|
|
1205
|
-
if (diffHours < 24)
|
|
1206
|
-
return `${diffHours}h ago`;
|
|
1207
|
-
if (diffDays < 7)
|
|
1208
|
-
return `${diffDays}d ago`;
|
|
1209
|
-
return date.toLocaleDateString();
|
|
1143
|
+
// ── bubble helpers ────────────────────────────────────────────────────────
|
|
1144
|
+
appendBubble(b) {
|
|
1145
|
+
this.messages.update(arr => [...arr, b]);
|
|
1210
1146
|
}
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
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;
|
|
1217
1153
|
}
|
|
1218
|
-
this.
|
|
1154
|
+
const id = this.currentAssistantId;
|
|
1155
|
+
this.messages.update(arr => arr.map(m => m.id === id ? { ...m, text: m.text + text } : m));
|
|
1219
1156
|
}
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
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;
|
|
1225
1163
|
}
|
|
1226
|
-
|
|
1164
|
+
const id = this.currentAssistantId;
|
|
1165
|
+
this.messages.update(arr => arr.map(m => m.id === id
|
|
1166
|
+
? { ...m, toolEvents: [...(m.toolEvents ?? []), ev] }
|
|
1167
|
+
: m));
|
|
1227
1168
|
}
|
|
1228
|
-
|
|
1229
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: NotificationPanelComponent, isStandalone: true, selector: "ma-notification-panel", outputs: { notificationRead: "notificationRead" }, host: { properties: { "class": "this.themeClass" } }, ngImport: i0, template: "<div class=\"notification-panel\" [class.open]=\"isOpen()\">\n <!-- Header -->\n <div class=\"panel-header\">\n <div class=\"panel-header-left\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9\"/>\n <path d=\"M13.73 21a2 2 0 0 1-3.46 0\"/>\n </svg>\n <h3>Notifications</h3>\n </div>\n <button class=\"close-btn\" (click)=\"close()\" title=\"Close\" aria-label=\"Close notifications\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"/><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"/>\n </svg>\n </button>\n </div>\n\n <!-- Tabs -->\n <div class=\"tabs\">\n <button class=\"tab-btn\" [class.active]=\"activeTab() === 'unread'\" (click)=\"switchTab('unread')\">\n Unread\n @if (unreadNotifications().length > 0) {\n <span class=\"tab-badge\">{{ unreadNotifications().length }}</span>\n }\n </button>\n <button class=\"tab-btn\" [class.active]=\"activeTab() === 'read'\" (click)=\"switchTab('read')\">\n Read\n @if (readNotifications().length > 0) {\n <span class=\"tab-badge read-badge\">{{ readNotifications().length }}</span>\n }\n </button>\n </div>\n\n <!-- Notifications List -->\n <div class=\"notifications-list\">\n @if (currentNotifications().length > 0) {\n @for (notification of currentNotifications(); track notification.id) {\n <div\n class=\"notification-item\"\n [class.unread]=\"!notification.isRead\"\n (click)=\"openDetails(notification)\"\n >\n @let t = typeOf(notification);\n <div class=\"notif-accent\"\n [class.type-info]=\"t === 'Info'\"\n [class.type-success]=\"t === 'Success'\"\n [class.type-warning]=\"t === 'Warning'\"\n [class.type-error]=\"t === 'Error'\"></div>\n <div class=\"notif-type-icon\"\n [class.type-info]=\"t === 'Info'\"\n [class.type-success]=\"t === 'Success'\"\n [class.type-warning]=\"t === 'Warning'\"\n [class.type-error]=\"t === 'Error'\">\n @if (t === 'Info') {\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9\"/><path d=\"M13.73 21a2 2 0 0 1-3.46 0\"/>\n </svg>\n }\n @if (t === 'Success') {\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"20 6 9 17 4 12\"/>\n </svg>\n }\n @if (t === 'Warning') {\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/><line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"/><line x1=\"12\" y1=\"17\" x2=\"12.01\" y2=\"17\"/>\n </svg>\n }\n @if (t === 'Error') {\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"12\" cy=\"12\" r=\"10\"/><line x1=\"15\" y1=\"9\" x2=\"9\" y2=\"15\"/><line x1=\"9\" y1=\"9\" x2=\"15\" y2=\"15\"/>\n </svg>\n }\n </div>\n <div class=\"notification-content\">\n <div class=\"notification-title\">{{ notification.title }}</div>\n <div class=\"notification-message\">{{ notification.message }}</div>\n <div class=\"notification-meta\">\n <span class=\"app-name\">{{ notification.sourceAppName }}</span>\n <span class=\"time\">{{ dateLabels().get(notification.id) }}</span>\n </div>\n </div>\n @if (!notification.isRead) {\n <button class=\"icon-btn read-btn\" (click)=\"markAsRead(notification.id, $event)\" title=\"Mark as read\" aria-label=\"Mark as read\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"20 6 9 17 4 12\"/>\n </svg>\n </button>\n }\n @if (notification.isRead) {\n <button class=\"icon-btn delete-btn\" (click)=\"delete(notification.id, $event)\" title=\"Delete\" aria-label=\"Delete notification\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"3 6 5 6 21 6\"/><path d=\"M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6\"/><path d=\"M10 11v6\"/><path d=\"M14 11v6\"/><path d=\"M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2\"/>\n </svg>\n </button>\n }\n </div>\n }\n } @else {\n <div class=\"empty-state\">\n <svg class=\"empty-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"44\" height=\"44\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9\"/>\n <path d=\"M13.73 21a2 2 0 0 1-3.46 0\"/>\n </svg>\n <p>No {{ activeTab() }} notifications</p>\n </div>\n }\n </div>\n\n <!-- Footer Actions -->\n @if (currentNotifications().length > 0) {\n <div class=\"panel-footer\">\n @if (activeTab() === 'unread') {\n <div class=\"footer-actions\">\n @if (unreadNotifications().length > 0) {\n <button class=\"action-btn\" (click)=\"markAllAsRead()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"/></svg>\n Mark all read\n </button>\n <button class=\"action-btn delete-all-btn\" (click)=\"deleteAllUnread()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"3 6 5 6 21 6\"/><path d=\"M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6\"/></svg>\n Delete all\n </button>\n }\n </div>\n }\n @if (activeTab() === 'read' && readNotifications().length > 0) {\n <button class=\"action-btn delete-all-btn\" (click)=\"deleteAllRead()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"3 6 5 6 21 6\"/><path d=\"M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6\"/></svg>\n Delete all\n </button>\n }\n </div>\n }\n</div>\n\n<!-- Details Modal -->\n@if (selectedNotification()) {\n <div class=\"modal-overlay\" (click)=\"closeDetails()\">\n <div class=\"modal-container\" (click)=\"$event.stopPropagation()\">\n <div class=\"modal-header\"\n [class.modal-type-info]=\"typeOf(selectedNotification()!) === 'Info'\"\n [class.modal-type-success]=\"typeOf(selectedNotification()!) === 'Success'\"\n [class.modal-type-warning]=\"typeOf(selectedNotification()!) === 'Warning'\"\n [class.modal-type-error]=\"typeOf(selectedNotification()!) === 'Error'\">\n <div class=\"modal-header-left\">\n <div class=\"modal-type-icon\">\n @if (typeOf(selectedNotification()!) === 'Info') {\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9\"/><path d=\"M13.73 21a2 2 0 0 1-3.46 0\"/>\n </svg>\n }\n @if (typeOf(selectedNotification()!) === 'Success') {\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M22 11.08V12a10 10 0 1 1-5.93-9.14\"/><polyline points=\"22 4 12 14.01 9 11.01\"/>\n </svg>\n }\n @if (typeOf(selectedNotification()!) === 'Warning') {\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/><line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"/>\n </svg>\n }\n @if (typeOf(selectedNotification()!) === 'Error') {\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"12\" cy=\"12\" r=\"10\"/><line x1=\"15\" y1=\"9\" x2=\"9\" y2=\"15\"/><line x1=\"9\" y1=\"9\" x2=\"15\" y2=\"15\"/>\n </svg>\n }\n </div>\n <h3>{{ selectedNotification()!.title }}</h3>\n </div>\n <button class=\"close-btn\" (click)=\"closeDetails()\" title=\"Close\" aria-label=\"Close notification detail\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"/><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"/>\n </svg>\n </button>\n </div>\n <div class=\"modal-meta\">\n <span class=\"app-name\">{{ selectedNotification()!.sourceAppName }}</span>\n <span class=\"time\">{{ selectedNotificationDate() }}</span>\n </div>\n <div class=\"modal-body\" [innerHTML]=\"selectedNotificationHtml()\"></div>\n <div class=\"modal-footer\">\n @if (selectedNotification()?.url?.trim()) {\n <button class=\"action-btn see-details-btn\" (click)=\"openUrl()\">See Details</button>\n }\n <button class=\"action-btn\" (click)=\"closeDetails()\">Close</button>\n </div>\n </div>\n </div>\n}\n", styles: [".panel-header{display:flex;justify-content:space-between;align-items:center;padding:16px 18px;border-bottom:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0}.panel-header-left{display:flex;align-items:center;gap:9px;color:var(--primary)}.panel-header h3{margin:0;font-size:16px;font-weight:700;color:var(--text-primary)}.close-btn{background:none;border:none;cursor:pointer;color:var(--text-muted);width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:background .15s,color .15s}.close-btn:hover{background:var(--bg-hover);color:var(--text-primary)}.tabs{display:flex;border-bottom:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0}.tab-btn{flex:1;display:flex;align-items:center;justify-content:center;gap:6px;padding:11px 8px;background:none;border:none;border-bottom:2px solid transparent;color:var(--text-muted);cursor:pointer;font-size:13px;font-weight:500;transition:color .15s,border-color .15s}.tab-btn.active{color:var(--primary);border-bottom-color:var(--primary)}.tab-btn:hover:not(.active){color:var(--text-secondary)}.tab-badge{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;background:var(--primary);color:#fff;font-size:11px;font-weight:700;border-radius:9px}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 20px;gap:12px;color:var(--text-muted)}.empty-state p{margin:0;font-size:13px}.loading-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px;gap:12px;color:var(--text-muted);font-size:13px}.spinner{width:24px;height:24px;border:2px solid var(--border-color);border-top-color:var(--primary);border-radius:50%;animation:spin .7s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}\n", ":host{display:block;position:relative;--primary: #1976d2;--primary-hover: #1565c0;--success: #43a047;--error: #f44336;--error-hover: #d32f2f;--info-color: #2196f3;--info-bg: rgba(33, 150, 243, .1);--success-bg: rgba(67, 160, 71, .1);--warning-color: #f57c00;--warning-bg: rgba(245, 124, 0, .1);--error-bg: rgba(244, 67, 54, .1);--text-primary: #212121;--text-secondary: #616161;--text-muted: #9e9e9e;--bg-primary: #ffffff;--bg-secondary: #f8f9fa;--bg-hover: #f0f4ff;--bg-unread: rgba(25, 118, 210, .06);--border-color: #e0e0e0;--border-light: #eeeeee;--shadow: rgba(0, 0, 0, .15)}.tab-btn:not(.active) .tab-badge{background:var(--error)}.read-badge{background:var(--text-muted)}.notification-panel{position:fixed;top:0;right:-360px;width:360px;height:100vh;background:var(--bg-primary);box-shadow:-4px 0 24px var(--shadow);display:flex;flex-direction:column;z-index:1030;transition:right .3s cubic-bezier(.16,1,.3,1)}.notification-panel.open{right:var(--ma-ai-panel-width, 0px)}.notifications-list{flex:1;overflow-y:auto}.notification-item{display:flex;align-items:flex-start;gap:0;border-bottom:1px solid var(--border-light);cursor:pointer;background:var(--bg-primary);transition:background-color .15s;position:relative}.notification-item:hover{background:var(--bg-hover)}.notification-item.unread{background:var(--bg-unread)}.notif-accent{width:3px;align-self:stretch;flex-shrink:0;background:transparent;border-radius:0 2px 2px 0;opacity:.3}.notification-item.unread .notif-accent{opacity:1}.notif-accent.type-info{background:var(--info-color)}.notif-accent.type-success{background:var(--success)}.notif-accent.type-warning{background:var(--warning-color)}.notif-accent.type-error{background:var(--error)}.notif-type-icon{flex-shrink:0;width:26px;height:26px;border-radius:7px;display:flex;align-items:center;justify-content:center;align-self:center;margin-left:10px}.notif-type-icon.type-info{color:var(--info-color);background:var(--info-bg)}.notif-type-icon.type-success{color:var(--success);background:var(--success-bg)}.notif-type-icon.type-warning{color:var(--warning-color);background:var(--warning-bg)}.notif-type-icon.type-error{color:var(--error);background:var(--error-bg)}.notification-content{flex:1;min-width:0;padding:12px 8px 12px 12px}.notification-title{font-weight:600;color:var(--text-primary);font-size:13.5px;margin-bottom:3px;line-height:1.35}.notification-message{color:var(--text-secondary);font-size:12px;line-height:1.45;margin-bottom:7px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.notification-meta{display:flex;justify-content:space-between;font-size:11px;color:var(--text-muted)}.app-name{font-weight:600;color:var(--primary)}.icon-btn{background:none;border:none;cursor:pointer;width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;flex-shrink:0;align-self:center;margin-right:8px;transition:color .15s,background-color .15s;color:var(--text-muted)}.read-btn:hover{color:var(--success);background:#43a0471a}.delete-btn:hover{color:var(--error);background:#f443361a}.panel-footer{padding:10px 14px;border-top:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0}.footer-actions{display:flex;gap:8px}.footer-actions .action-btn{flex:1}.action-btn{display:flex;align-items:center;justify-content:center;gap:6px;width:100%;padding:8px 12px;background:var(--primary);color:#fff;border:none;border-radius:8px;cursor:pointer;font-size:12.5px;font-weight:600;transition:background-color .18s,transform .12s}.action-btn:hover{background:var(--primary-hover);transform:translateY(-1px)}.delete-all-btn{background:var(--error)}.delete-all-btn:hover{background:var(--error-hover)}.modal-overlay{position:fixed;inset:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:1060;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.modal-container{background:var(--bg-primary);border-radius:14px;width:92%;max-width:860px;max-height:88vh;display:flex;flex-direction:column;box-shadow:0 16px 48px #00000040;animation:modal-in .2s cubic-bezier(.16,1,.3,1)}@keyframes modal-in{0%{opacity:0;transform:scale(.94) translateY(8px)}to{opacity:1;transform:scale(1) translateY(0)}}.modal-header{display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid var(--border-color);background:var(--bg-secondary);border-radius:14px 14px 0 0;border-top:3px solid transparent}.modal-header.modal-type-info{border-top-color:var(--info-color)}.modal-header.modal-type-success{border-top-color:var(--success)}.modal-header.modal-type-warning{border-top-color:var(--warning-color)}.modal-header.modal-type-error{border-top-color:var(--error)}.modal-header-left{display:flex;align-items:center;gap:10px;min-width:0}.modal-type-icon{flex-shrink:0;width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center}.modal-type-info .modal-type-icon{color:var(--info-color);background:var(--info-bg)}.modal-type-success .modal-type-icon{color:var(--success);background:var(--success-bg)}.modal-type-warning .modal-type-icon{color:var(--warning-color);background:var(--warning-bg)}.modal-type-error .modal-type-icon{color:var(--error);background:var(--error-bg)}.modal-header h3{margin:0;font-size:15px;font-weight:700;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.modal-meta{display:flex;justify-content:space-between;padding:8px 20px;font-size:11.5px;color:var(--text-muted);border-bottom:1px solid var(--border-light)}.modal-body{padding:20px;overflow-y:auto;flex:1;color:var(--text-primary);font-size:14px;line-height:1.65}.modal-footer{padding:12px 20px;border-top:1px solid var(--border-color);background:var(--bg-secondary);border-radius:0 0 14px 14px;display:flex;justify-content:flex-end;gap:8px}.modal-footer .action-btn{width:auto;padding:8px 24px}.modal-footer .see-details-btn{background:var(--info-bg);color:var(--info-color);border:1px solid var(--info-color)}.modal-footer .see-details-btn:hover{opacity:.85}@media(max-width:600px){.notification-panel{width:100%;right:-100%}.modal-container{width:95%;max-height:90vh}}\n"] });
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
args: [{ selector: 'ma-notification-panel', imports: [], template: "<div class=\"notification-panel\" [class.open]=\"isOpen()\">\n <!-- Header -->\n <div class=\"panel-header\">\n <div class=\"panel-header-left\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9\"/>\n <path d=\"M13.73 21a2 2 0 0 1-3.46 0\"/>\n </svg>\n <h3>Notifications</h3>\n </div>\n <button class=\"close-btn\" (click)=\"close()\" title=\"Close\" aria-label=\"Close notifications\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"/><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"/>\n </svg>\n </button>\n </div>\n\n <!-- Tabs -->\n <div class=\"tabs\">\n <button class=\"tab-btn\" [class.active]=\"activeTab() === 'unread'\" (click)=\"switchTab('unread')\">\n Unread\n @if (unreadNotifications().length > 0) {\n <span class=\"tab-badge\">{{ unreadNotifications().length }}</span>\n }\n </button>\n <button class=\"tab-btn\" [class.active]=\"activeTab() === 'read'\" (click)=\"switchTab('read')\">\n Read\n @if (readNotifications().length > 0) {\n <span class=\"tab-badge read-badge\">{{ readNotifications().length }}</span>\n }\n </button>\n </div>\n\n <!-- Notifications List -->\n <div class=\"notifications-list\">\n @if (currentNotifications().length > 0) {\n @for (notification of currentNotifications(); track notification.id) {\n <div\n class=\"notification-item\"\n [class.unread]=\"!notification.isRead\"\n (click)=\"openDetails(notification)\"\n >\n @let t = typeOf(notification);\n <div class=\"notif-accent\"\n [class.type-info]=\"t === 'Info'\"\n [class.type-success]=\"t === 'Success'\"\n [class.type-warning]=\"t === 'Warning'\"\n [class.type-error]=\"t === 'Error'\"></div>\n <div class=\"notif-type-icon\"\n [class.type-info]=\"t === 'Info'\"\n [class.type-success]=\"t === 'Success'\"\n [class.type-warning]=\"t === 'Warning'\"\n [class.type-error]=\"t === 'Error'\">\n @if (t === 'Info') {\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9\"/><path d=\"M13.73 21a2 2 0 0 1-3.46 0\"/>\n </svg>\n }\n @if (t === 'Success') {\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"20 6 9 17 4 12\"/>\n </svg>\n }\n @if (t === 'Warning') {\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/><line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"/><line x1=\"12\" y1=\"17\" x2=\"12.01\" y2=\"17\"/>\n </svg>\n }\n @if (t === 'Error') {\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"12\" cy=\"12\" r=\"10\"/><line x1=\"15\" y1=\"9\" x2=\"9\" y2=\"15\"/><line x1=\"9\" y1=\"9\" x2=\"15\" y2=\"15\"/>\n </svg>\n }\n </div>\n <div class=\"notification-content\">\n <div class=\"notification-title\">{{ notification.title }}</div>\n <div class=\"notification-message\">{{ notification.message }}</div>\n <div class=\"notification-meta\">\n <span class=\"app-name\">{{ notification.sourceAppName }}</span>\n <span class=\"time\">{{ dateLabels().get(notification.id) }}</span>\n </div>\n </div>\n @if (!notification.isRead) {\n <button class=\"icon-btn read-btn\" (click)=\"markAsRead(notification.id, $event)\" title=\"Mark as read\" aria-label=\"Mark as read\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"20 6 9 17 4 12\"/>\n </svg>\n </button>\n }\n @if (notification.isRead) {\n <button class=\"icon-btn delete-btn\" (click)=\"delete(notification.id, $event)\" title=\"Delete\" aria-label=\"Delete notification\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"3 6 5 6 21 6\"/><path d=\"M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6\"/><path d=\"M10 11v6\"/><path d=\"M14 11v6\"/><path d=\"M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2\"/>\n </svg>\n </button>\n }\n </div>\n }\n } @else {\n <div class=\"empty-state\">\n <svg class=\"empty-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"44\" height=\"44\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9\"/>\n <path d=\"M13.73 21a2 2 0 0 1-3.46 0\"/>\n </svg>\n <p>No {{ activeTab() }} notifications</p>\n </div>\n }\n </div>\n\n <!-- Footer Actions -->\n @if (currentNotifications().length > 0) {\n <div class=\"panel-footer\">\n @if (activeTab() === 'unread') {\n <div class=\"footer-actions\">\n @if (unreadNotifications().length > 0) {\n <button class=\"action-btn\" (click)=\"markAllAsRead()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"/></svg>\n Mark all read\n </button>\n <button class=\"action-btn delete-all-btn\" (click)=\"deleteAllUnread()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"3 6 5 6 21 6\"/><path d=\"M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6\"/></svg>\n Delete all\n </button>\n }\n </div>\n }\n @if (activeTab() === 'read' && readNotifications().length > 0) {\n <button class=\"action-btn delete-all-btn\" (click)=\"deleteAllRead()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"3 6 5 6 21 6\"/><path d=\"M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6\"/></svg>\n Delete all\n </button>\n }\n </div>\n }\n</div>\n\n<!-- Details Modal -->\n@if (selectedNotification()) {\n <div class=\"modal-overlay\" (click)=\"closeDetails()\">\n <div class=\"modal-container\" (click)=\"$event.stopPropagation()\">\n <div class=\"modal-header\"\n [class.modal-type-info]=\"typeOf(selectedNotification()!) === 'Info'\"\n [class.modal-type-success]=\"typeOf(selectedNotification()!) === 'Success'\"\n [class.modal-type-warning]=\"typeOf(selectedNotification()!) === 'Warning'\"\n [class.modal-type-error]=\"typeOf(selectedNotification()!) === 'Error'\">\n <div class=\"modal-header-left\">\n <div class=\"modal-type-icon\">\n @if (typeOf(selectedNotification()!) === 'Info') {\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9\"/><path d=\"M13.73 21a2 2 0 0 1-3.46 0\"/>\n </svg>\n }\n @if (typeOf(selectedNotification()!) === 'Success') {\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M22 11.08V12a10 10 0 1 1-5.93-9.14\"/><polyline points=\"22 4 12 14.01 9 11.01\"/>\n </svg>\n }\n @if (typeOf(selectedNotification()!) === 'Warning') {\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/><line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"/>\n </svg>\n }\n @if (typeOf(selectedNotification()!) === 'Error') {\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"12\" cy=\"12\" r=\"10\"/><line x1=\"15\" y1=\"9\" x2=\"9\" y2=\"15\"/><line x1=\"9\" y1=\"9\" x2=\"15\" y2=\"15\"/>\n </svg>\n }\n </div>\n <h3>{{ selectedNotification()!.title }}</h3>\n </div>\n <button class=\"close-btn\" (click)=\"closeDetails()\" title=\"Close\" aria-label=\"Close notification detail\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"/><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"/>\n </svg>\n </button>\n </div>\n <div class=\"modal-meta\">\n <span class=\"app-name\">{{ selectedNotification()!.sourceAppName }}</span>\n <span class=\"time\">{{ selectedNotificationDate() }}</span>\n </div>\n <div class=\"modal-body\" [innerHTML]=\"selectedNotificationHtml()\"></div>\n <div class=\"modal-footer\">\n @if (selectedNotification()?.url?.trim()) {\n <button class=\"action-btn see-details-btn\" (click)=\"openUrl()\">See Details</button>\n }\n <button class=\"action-btn\" (click)=\"closeDetails()\">Close</button>\n </div>\n </div>\n </div>\n}\n", styles: [".panel-header{display:flex;justify-content:space-between;align-items:center;padding:16px 18px;border-bottom:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0}.panel-header-left{display:flex;align-items:center;gap:9px;color:var(--primary)}.panel-header h3{margin:0;font-size:16px;font-weight:700;color:var(--text-primary)}.close-btn{background:none;border:none;cursor:pointer;color:var(--text-muted);width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:background .15s,color .15s}.close-btn:hover{background:var(--bg-hover);color:var(--text-primary)}.tabs{display:flex;border-bottom:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0}.tab-btn{flex:1;display:flex;align-items:center;justify-content:center;gap:6px;padding:11px 8px;background:none;border:none;border-bottom:2px solid transparent;color:var(--text-muted);cursor:pointer;font-size:13px;font-weight:500;transition:color .15s,border-color .15s}.tab-btn.active{color:var(--primary);border-bottom-color:var(--primary)}.tab-btn:hover:not(.active){color:var(--text-secondary)}.tab-badge{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;background:var(--primary);color:#fff;font-size:11px;font-weight:700;border-radius:9px}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 20px;gap:12px;color:var(--text-muted)}.empty-state p{margin:0;font-size:13px}.loading-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px;gap:12px;color:var(--text-muted);font-size:13px}.spinner{width:24px;height:24px;border:2px solid var(--border-color);border-top-color:var(--primary);border-radius:50%;animation:spin .7s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}\n", ":host{display:block;position:relative;--primary: #1976d2;--primary-hover: #1565c0;--success: #43a047;--error: #f44336;--error-hover: #d32f2f;--info-color: #2196f3;--info-bg: rgba(33, 150, 243, .1);--success-bg: rgba(67, 160, 71, .1);--warning-color: #f57c00;--warning-bg: rgba(245, 124, 0, .1);--error-bg: rgba(244, 67, 54, .1);--text-primary: #212121;--text-secondary: #616161;--text-muted: #9e9e9e;--bg-primary: #ffffff;--bg-secondary: #f8f9fa;--bg-hover: #f0f4ff;--bg-unread: rgba(25, 118, 210, .06);--border-color: #e0e0e0;--border-light: #eeeeee;--shadow: rgba(0, 0, 0, .15)}.tab-btn:not(.active) .tab-badge{background:var(--error)}.read-badge{background:var(--text-muted)}.notification-panel{position:fixed;top:0;right:-360px;width:360px;height:100vh;background:var(--bg-primary);box-shadow:-4px 0 24px var(--shadow);display:flex;flex-direction:column;z-index:1030;transition:right .3s cubic-bezier(.16,1,.3,1)}.notification-panel.open{right:var(--ma-ai-panel-width, 0px)}.notifications-list{flex:1;overflow-y:auto}.notification-item{display:flex;align-items:flex-start;gap:0;border-bottom:1px solid var(--border-light);cursor:pointer;background:var(--bg-primary);transition:background-color .15s;position:relative}.notification-item:hover{background:var(--bg-hover)}.notification-item.unread{background:var(--bg-unread)}.notif-accent{width:3px;align-self:stretch;flex-shrink:0;background:transparent;border-radius:0 2px 2px 0;opacity:.3}.notification-item.unread .notif-accent{opacity:1}.notif-accent.type-info{background:var(--info-color)}.notif-accent.type-success{background:var(--success)}.notif-accent.type-warning{background:var(--warning-color)}.notif-accent.type-error{background:var(--error)}.notif-type-icon{flex-shrink:0;width:26px;height:26px;border-radius:7px;display:flex;align-items:center;justify-content:center;align-self:center;margin-left:10px}.notif-type-icon.type-info{color:var(--info-color);background:var(--info-bg)}.notif-type-icon.type-success{color:var(--success);background:var(--success-bg)}.notif-type-icon.type-warning{color:var(--warning-color);background:var(--warning-bg)}.notif-type-icon.type-error{color:var(--error);background:var(--error-bg)}.notification-content{flex:1;min-width:0;padding:12px 8px 12px 12px}.notification-title{font-weight:600;color:var(--text-primary);font-size:13.5px;margin-bottom:3px;line-height:1.35}.notification-message{color:var(--text-secondary);font-size:12px;line-height:1.45;margin-bottom:7px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.notification-meta{display:flex;justify-content:space-between;font-size:11px;color:var(--text-muted)}.app-name{font-weight:600;color:var(--primary)}.icon-btn{background:none;border:none;cursor:pointer;width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;flex-shrink:0;align-self:center;margin-right:8px;transition:color .15s,background-color .15s;color:var(--text-muted)}.read-btn:hover{color:var(--success);background:#43a0471a}.delete-btn:hover{color:var(--error);background:#f443361a}.panel-footer{padding:10px 14px;border-top:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0}.footer-actions{display:flex;gap:8px}.footer-actions .action-btn{flex:1}.action-btn{display:flex;align-items:center;justify-content:center;gap:6px;width:100%;padding:8px 12px;background:var(--primary);color:#fff;border:none;border-radius:8px;cursor:pointer;font-size:12.5px;font-weight:600;transition:background-color .18s,transform .12s}.action-btn:hover{background:var(--primary-hover);transform:translateY(-1px)}.delete-all-btn{background:var(--error)}.delete-all-btn:hover{background:var(--error-hover)}.modal-overlay{position:fixed;inset:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:1060;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.modal-container{background:var(--bg-primary);border-radius:14px;width:92%;max-width:860px;max-height:88vh;display:flex;flex-direction:column;box-shadow:0 16px 48px #00000040;animation:modal-in .2s cubic-bezier(.16,1,.3,1)}@keyframes modal-in{0%{opacity:0;transform:scale(.94) translateY(8px)}to{opacity:1;transform:scale(1) translateY(0)}}.modal-header{display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid var(--border-color);background:var(--bg-secondary);border-radius:14px 14px 0 0;border-top:3px solid transparent}.modal-header.modal-type-info{border-top-color:var(--info-color)}.modal-header.modal-type-success{border-top-color:var(--success)}.modal-header.modal-type-warning{border-top-color:var(--warning-color)}.modal-header.modal-type-error{border-top-color:var(--error)}.modal-header-left{display:flex;align-items:center;gap:10px;min-width:0}.modal-type-icon{flex-shrink:0;width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center}.modal-type-info .modal-type-icon{color:var(--info-color);background:var(--info-bg)}.modal-type-success .modal-type-icon{color:var(--success);background:var(--success-bg)}.modal-type-warning .modal-type-icon{color:var(--warning-color);background:var(--warning-bg)}.modal-type-error .modal-type-icon{color:var(--error);background:var(--error-bg)}.modal-header h3{margin:0;font-size:15px;font-weight:700;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.modal-meta{display:flex;justify-content:space-between;padding:8px 20px;font-size:11.5px;color:var(--text-muted);border-bottom:1px solid var(--border-light)}.modal-body{padding:20px;overflow-y:auto;flex:1;color:var(--text-primary);font-size:14px;line-height:1.65}.modal-footer{padding:12px 20px;border-top:1px solid var(--border-color);background:var(--bg-secondary);border-radius:0 0 14px 14px;display:flex;justify-content:flex-end;gap:8px}.modal-footer .action-btn{width:auto;padding:8px 24px}.modal-footer .see-details-btn{background:var(--info-bg);color:var(--info-color);border:1px solid var(--info-color)}.modal-footer .see-details-btn:hover{opacity:.85}@media(max-width:600px){.notification-panel{width:100%;right:-100%}.modal-container{width:95%;max-height:90vh}}\n"] }]
|
|
1234
|
-
}], ctorParameters: () => [], propDecorators: { notificationRead: [{ type: i0.Output, args: ["notificationRead"] }], themeClass: [{
|
|
1235
|
-
type: HostBinding,
|
|
1236
|
-
args: ['class']
|
|
1237
|
-
}] } });
|
|
1238
|
-
|
|
1239
|
-
class MaApprovalService {
|
|
1240
|
-
apiBase = '';
|
|
1241
|
-
http;
|
|
1242
|
-
config = null;
|
|
1243
|
-
constructor() { }
|
|
1244
|
-
init(config, httpClient) {
|
|
1245
|
-
this.config = config;
|
|
1246
|
-
this.http = httpClient;
|
|
1247
|
-
this.apiBase = config.apiBaseUrl.replace(/\/$/, '');
|
|
1248
|
-
}
|
|
1249
|
-
get opts() {
|
|
1250
|
-
return { withCredentials: this.config?.withCredentials ?? true };
|
|
1251
|
-
}
|
|
1252
|
-
// ====================== Dashboard ======================
|
|
1253
|
-
getDashboard() {
|
|
1254
|
-
return this.http.get(`${this.apiBase}/approval/dashboard`, this.opts);
|
|
1255
|
-
}
|
|
1256
|
-
// ====================== Pending & My Requests ======================
|
|
1257
|
-
getPendingApprovals(page = 1, pageSize = 20) {
|
|
1258
|
-
return this.http.get(`${this.apiBase}/approval/pending?page=${page}&pageSize=${pageSize}`, this.opts);
|
|
1259
|
-
}
|
|
1260
|
-
getMyRequests(page = 1, pageSize = 20, status) {
|
|
1261
|
-
let url = `${this.apiBase}/approval/my-requests?page=${page}&pageSize=${pageSize}`;
|
|
1262
|
-
if (status !== undefined)
|
|
1263
|
-
url += `&status=${status}`;
|
|
1264
|
-
return this.http.get(url, this.opts);
|
|
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
|
+
})));
|
|
1265
1174
|
}
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
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;
|
|
1269
1183
|
}
|
|
1270
|
-
|
|
1271
|
-
return
|
|
1184
|
+
verbOf(name) {
|
|
1185
|
+
return name.split('_')[0] ?? name;
|
|
1272
1186
|
}
|
|
1273
|
-
|
|
1274
|
-
|
|
1187
|
+
summarize(s) {
|
|
1188
|
+
const flat = s.replace(/\s+/g, ' ').trim();
|
|
1189
|
+
return flat.length <= 80 ? flat : flat.slice(0, 80) + '…';
|
|
1275
1190
|
}
|
|
1276
|
-
|
|
1277
|
-
return
|
|
1191
|
+
uid() {
|
|
1192
|
+
return Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
1278
1193
|
}
|
|
1279
|
-
//
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
return this.
|
|
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}`) ?? [];
|
|
1283
1198
|
}
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
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;
|
|
1287
1211
|
}
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
return this.
|
|
1212
|
+
/** DELETE /ai/conversations/{id} */
|
|
1213
|
+
async deleteConversation(id) {
|
|
1214
|
+
return this.apiSend('DELETE', `/ai/conversations/${encodeURIComponent(id)}`);
|
|
1291
1215
|
}
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
url += `?reason=${encodeURIComponent(reason)}`;
|
|
1296
|
-
return this.http.delete(url, this.opts);
|
|
1216
|
+
// ── user lessons (system-prompt injections) ──────────────────────────────
|
|
1217
|
+
async listLessons() {
|
|
1218
|
+
return this.apiGet(`/ai/lessons`) ?? [];
|
|
1297
1219
|
}
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
let url = `${this.apiBase}/approval/templates`;
|
|
1301
|
-
if (appId)
|
|
1302
|
-
url += `?appId=${encodeURIComponent(appId)}`;
|
|
1303
|
-
return this.http.get(url, this.opts);
|
|
1220
|
+
async createLesson(text) {
|
|
1221
|
+
return this.apiSend('POST', `/ai/lessons`, { text });
|
|
1304
1222
|
}
|
|
1305
|
-
|
|
1306
|
-
return this.
|
|
1223
|
+
async updateLesson(id, patch) {
|
|
1224
|
+
return this.apiSend('PUT', `/ai/lessons/${encodeURIComponent(id)}`, patch);
|
|
1307
1225
|
}
|
|
1308
|
-
|
|
1309
|
-
return this.
|
|
1226
|
+
async deleteLesson(id) {
|
|
1227
|
+
return this.apiSend('DELETE', `/ai/lessons/${encodeURIComponent(id)}`);
|
|
1310
1228
|
}
|
|
1311
|
-
|
|
1312
|
-
|
|
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;
|
|
1313
1234
|
}
|
|
1314
|
-
|
|
1315
|
-
|
|
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
|
+
}
|
|
1316
1253
|
}
|
|
1317
|
-
|
|
1318
|
-
|
|
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
|
+
}
|
|
1319
1278
|
}
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
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;
|
|
1323
1321
|
}
|
|
1324
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type:
|
|
1325
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type:
|
|
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' });
|
|
1326
1324
|
}
|
|
1327
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type:
|
|
1328
|
-
type: Injectable
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
// ====================== Enums ======================
|
|
1332
|
-
var ApprovalStepMode;
|
|
1333
|
-
(function (ApprovalStepMode) {
|
|
1334
|
-
ApprovalStepMode[ApprovalStepMode["Sequential"] = 0] = "Sequential";
|
|
1335
|
-
ApprovalStepMode[ApprovalStepMode["Parallel"] = 1] = "Parallel";
|
|
1336
|
-
})(ApprovalStepMode || (ApprovalStepMode = {}));
|
|
1337
|
-
var ApprovalDocumentStatus;
|
|
1338
|
-
(function (ApprovalDocumentStatus) {
|
|
1339
|
-
ApprovalDocumentStatus[ApprovalDocumentStatus["Draft"] = 0] = "Draft";
|
|
1340
|
-
ApprovalDocumentStatus[ApprovalDocumentStatus["Pending"] = 1] = "Pending";
|
|
1341
|
-
ApprovalDocumentStatus[ApprovalDocumentStatus["Approved"] = 2] = "Approved";
|
|
1342
|
-
ApprovalDocumentStatus[ApprovalDocumentStatus["Rejected"] = 3] = "Rejected";
|
|
1343
|
-
ApprovalDocumentStatus[ApprovalDocumentStatus["Cancelled"] = 4] = "Cancelled";
|
|
1344
|
-
ApprovalDocumentStatus[ApprovalDocumentStatus["Expired"] = 5] = "Expired";
|
|
1345
|
-
})(ApprovalDocumentStatus || (ApprovalDocumentStatus = {}));
|
|
1346
|
-
var ApprovalStepStatus;
|
|
1347
|
-
(function (ApprovalStepStatus) {
|
|
1348
|
-
ApprovalStepStatus[ApprovalStepStatus["Waiting"] = 0] = "Waiting";
|
|
1349
|
-
ApprovalStepStatus[ApprovalStepStatus["Active"] = 1] = "Active";
|
|
1350
|
-
ApprovalStepStatus[ApprovalStepStatus["Approved"] = 2] = "Approved";
|
|
1351
|
-
ApprovalStepStatus[ApprovalStepStatus["Rejected"] = 3] = "Rejected";
|
|
1352
|
-
ApprovalStepStatus[ApprovalStepStatus["Delegated"] = 4] = "Delegated";
|
|
1353
|
-
ApprovalStepStatus[ApprovalStepStatus["Expired"] = 5] = "Expired";
|
|
1354
|
-
ApprovalStepStatus[ApprovalStepStatus["Skipped"] = 6] = "Skipped";
|
|
1355
|
-
})(ApprovalStepStatus || (ApprovalStepStatus = {}));
|
|
1356
|
-
var ApprovalActionType;
|
|
1357
|
-
(function (ApprovalActionType) {
|
|
1358
|
-
ApprovalActionType[ApprovalActionType["Created"] = 0] = "Created";
|
|
1359
|
-
ApprovalActionType[ApprovalActionType["Submitted"] = 1] = "Submitted";
|
|
1360
|
-
ApprovalActionType[ApprovalActionType["Approved"] = 2] = "Approved";
|
|
1361
|
-
ApprovalActionType[ApprovalActionType["Rejected"] = 3] = "Rejected";
|
|
1362
|
-
ApprovalActionType[ApprovalActionType["Delegated"] = 4] = "Delegated";
|
|
1363
|
-
ApprovalActionType[ApprovalActionType["Cancelled"] = 5] = "Cancelled";
|
|
1364
|
-
ApprovalActionType[ApprovalActionType["Commented"] = 6] = "Commented";
|
|
1365
|
-
ApprovalActionType[ApprovalActionType["Expired"] = 7] = "Expired";
|
|
1366
|
-
ApprovalActionType[ApprovalActionType["StepAdvanced"] = 8] = "StepAdvanced";
|
|
1367
|
-
})(ApprovalActionType || (ApprovalActionType = {}));
|
|
1325
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiService, decorators: [{
|
|
1326
|
+
type: Injectable,
|
|
1327
|
+
args: [{ providedIn: 'root' }]
|
|
1328
|
+
}] });
|
|
1368
1329
|
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
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 */ []));
|
|
1372
1342
|
loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
approvalSvc = null;
|
|
1382
|
-
constructor() {
|
|
1383
|
-
const config = this.mesAuth.getConfig();
|
|
1384
|
-
if (config) {
|
|
1385
|
-
this.approvalSvc = new MaApprovalService();
|
|
1386
|
-
this.approvalSvc.init(config, this.http);
|
|
1387
|
-
}
|
|
1388
|
-
const approvalEvent = toSignal(this.mesAuth.approvalEvents$);
|
|
1389
|
-
effect(() => {
|
|
1390
|
-
approvalEvent(); // track SignalR approval events
|
|
1391
|
-
if (this.isOpen())
|
|
1392
|
-
this.loadCurrentTab();
|
|
1393
|
-
});
|
|
1394
|
-
// Close when the user clicks outside the panel (mirrors the old backdrop UX
|
|
1395
|
-
// now that the backdrop has been removed).
|
|
1396
|
-
effect((onCleanup) => {
|
|
1397
|
-
if (!this.isOpen())
|
|
1398
|
-
return;
|
|
1399
|
-
const onDocClick = (ev) => {
|
|
1400
|
-
const panel = this.host.nativeElement.querySelector('.approval-panel');
|
|
1401
|
-
if (panel && !panel.contains(ev.target))
|
|
1402
|
-
this.close();
|
|
1403
|
-
};
|
|
1404
|
-
// Defer to skip the same click that opened the panel.
|
|
1405
|
-
const tid = setTimeout(() => document.addEventListener('mousedown', onDocClick), 0);
|
|
1406
|
-
onCleanup(() => {
|
|
1407
|
-
clearTimeout(tid);
|
|
1408
|
-
document.removeEventListener('mousedown', onDocClick);
|
|
1409
|
-
});
|
|
1410
|
-
});
|
|
1411
|
-
}
|
|
1412
|
-
open() {
|
|
1413
|
-
this.isOpen.set(true);
|
|
1414
|
-
this.loadAllTabs();
|
|
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()}`;
|
|
1415
1351
|
}
|
|
1416
|
-
|
|
1417
|
-
this.
|
|
1352
|
+
async ngOnInit() {
|
|
1353
|
+
this.loading.set(true);
|
|
1354
|
+
try {
|
|
1355
|
+
const list = await this.ai.listLessons();
|
|
1356
|
+
this.lessons.set(list ?? []);
|
|
1357
|
+
}
|
|
1358
|
+
finally {
|
|
1359
|
+
this.loading.set(false);
|
|
1360
|
+
}
|
|
1418
1361
|
}
|
|
1419
|
-
|
|
1420
|
-
if (
|
|
1421
|
-
this.
|
|
1422
|
-
else
|
|
1423
|
-
this.open();
|
|
1362
|
+
onBackdropClick(ev) {
|
|
1363
|
+
if (ev.target === ev.currentTarget)
|
|
1364
|
+
this.cancel.emit();
|
|
1424
1365
|
}
|
|
1425
|
-
|
|
1426
|
-
this.
|
|
1427
|
-
this.loadCurrentTab();
|
|
1366
|
+
onDraftInput(ev) {
|
|
1367
|
+
this.draft.set(ev.target.value);
|
|
1428
1368
|
}
|
|
1429
|
-
|
|
1430
|
-
this.
|
|
1431
|
-
if (!
|
|
1432
|
-
this.loading.set(false);
|
|
1369
|
+
async add() {
|
|
1370
|
+
const text = this.draft().trim();
|
|
1371
|
+
if (!text)
|
|
1433
1372
|
return;
|
|
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('');
|
|
1383
|
+
}
|
|
1384
|
+
finally {
|
|
1385
|
+
this.saving.set(false);
|
|
1434
1386
|
}
|
|
1435
|
-
let pending = 3;
|
|
1436
|
-
const done = () => { if (--pending === 0)
|
|
1437
|
-
this.loading.set(false); };
|
|
1438
|
-
this.approvalSvc.getPendingApprovals(1, 100).subscribe({
|
|
1439
|
-
next: r => { this.processingItems.set(r.items); done(); },
|
|
1440
|
-
error: () => done()
|
|
1441
|
-
});
|
|
1442
|
-
this.approvalSvc.getMyRequests(1, 10, ApprovalDocumentStatus.Approved).subscribe({
|
|
1443
|
-
next: r => { this.approvedItems.set(r.items); done(); },
|
|
1444
|
-
error: () => done()
|
|
1445
|
-
});
|
|
1446
|
-
this.approvalSvc.getMyRequests(1, 10, ApprovalDocumentStatus.Rejected).subscribe({
|
|
1447
|
-
next: r => { this.rejectedItems.set(r.items); done(); },
|
|
1448
|
-
error: () => done()
|
|
1449
|
-
});
|
|
1450
1387
|
}
|
|
1451
|
-
|
|
1452
|
-
|
|
1388
|
+
onTextInput(lesson, ev) {
|
|
1389
|
+
this.pendingText.set(lesson.id, ev.target.value);
|
|
1390
|
+
}
|
|
1391
|
+
async commitText(lesson) {
|
|
1392
|
+
const next = this.pendingText.get(lesson.id);
|
|
1393
|
+
this.pendingText.delete(lesson.id);
|
|
1394
|
+
if (next === undefined)
|
|
1453
1395
|
return;
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
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 });
|
|
1484
|
+
}
|
|
1485
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiLessonsEditorComponent, decorators: [{
|
|
1486
|
+
type: Component,
|
|
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>
|
|
1505
|
+
|
|
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}`;
|
|
1459
1664
|
}
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
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");
|
|
1465
1712
|
}
|
|
1466
1713
|
else {
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
error: () => { }
|
|
1470
|
-
});
|
|
1714
|
+
// Optional: redirect in same tab
|
|
1715
|
+
window.location.href = destinationUrl;
|
|
1471
1716
|
}
|
|
1472
1717
|
}
|
|
1473
|
-
|
|
1474
|
-
this.
|
|
1475
|
-
|
|
1476
|
-
|
|
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
|
+
});
|
|
1477
1733
|
}
|
|
1478
|
-
|
|
1479
|
-
this.
|
|
1480
|
-
this.router.navigate(['/auth/approval/my-requests'], { queryParams: { status } });
|
|
1734
|
+
onNotificationClick() {
|
|
1735
|
+
this.notificationClick.emit();
|
|
1481
1736
|
}
|
|
1482
|
-
|
|
1483
|
-
|
|
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"] }] });
|
|
1484
1749
|
}
|
|
1485
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type:
|
|
1750
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: UserProfileComponent, decorators: [{
|
|
1486
1751
|
type: Component,
|
|
1487
|
-
args: [{ selector: 'ma-approval-panel', imports: [DatePipe], template: "<div class=\"approval-panel\" [class.open]=\"isOpen()\">\n\n <!-- Header -->\n <div class=\"panel-header\">\n <div class=\"panel-header-left\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M9 11l3 3L22 4\"/>\n <path d=\"M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11\"/>\n </svg>\n <h3>Approvals</h3>\n </div>\n <button class=\"close-btn\" (click)=\"close()\" aria-label=\"Close\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"/><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"/>\n </svg>\n </button>\n </div>\n\n <!-- Tabs -->\n <div class=\"tabs\">\n <button class=\"tab-btn\" [class.active]=\"activeTab() === 'processing'\" (click)=\"switchTab('processing')\">\n Processing\n @if (processingItems().length > 0) {\n <span class=\"tab-badge\">{{ processingItems().length }}</span>\n }\n </button>\n <button class=\"tab-btn\" [class.active]=\"activeTab() === 'approved'\" (click)=\"switchTab('approved')\">\n Approved\n </button>\n <button class=\"tab-btn\" [class.active]=\"activeTab() === 'rejected'\" (click)=\"switchTab('rejected')\">\n Rejected\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"panel-content\">\n <!-- Loading -->\n @if (loading()) {\n <div class=\"loading-state\">\n <div class=\"spinner\"></div>\n <span>Loading...</span>\n </div>\n }\n\n <!-- Processing tab -->\n @if (!loading() && activeTab() === 'processing') {\n @if (processingItems().length === 0) {\n <div class=\"empty-state\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"32\" height=\"32\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" opacity=\"0.4\"><path d=\"M9 11l3 3L22 4\"/><path d=\"M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11\"/></svg>\n <p>No pending approvals</p>\n </div>\n }\n @for (item of processingItems(); track item.id) {\n <div class=\"approval-item\" (click)=\"navigateToDetail(item.id)\">\n <div class=\"item-title\">{{ item.title }}</div>\n <div class=\"item-meta\">\n <span class=\"item-requester\">By {{ item.requestedByUserName }}</span>\n @if (item.currentStepName) {\n <span class=\"item-step\">\u00B7 {{ item.currentStepName }}</span>\n }\n </div>\n <div class=\"item-footer\">\n <span class=\"item-time\">{{ item.createdAt | date:'shortDate' }}</span>\n <span class=\"item-link\">View →</span>\n </div>\n </div>\n }\n }\n\n <!-- Approved tab -->\n @if (!loading() && activeTab() === 'approved') {\n @if (approvedItems().length === 0) {\n <div class=\"empty-state\">\n <p>No approved documents</p>\n </div>\n }\n @for (item of approvedItems(); track item.id) {\n <div class=\"approval-item approved\" (click)=\"navigateToDetail(item.id)\">\n <div class=\"item-title\">{{ item.title }}</div>\n <div class=\"item-meta\">\n <span class=\"item-requester\">By {{ item.requestedByUserName }}</span>\n </div>\n <div class=\"item-footer\">\n <span class=\"status-badge approved-badge\">Approved</span>\n <span class=\"item-time\">{{ item.completedAt | date:'shortDate' }}</span>\n <span class=\"item-link\">View →</span>\n </div>\n </div>\n }\n @if (approvedItems().length >= 10) {\n <div class=\"show-more\" (click)=\"showMore('approved')\">Show more →</div>\n }\n }\n\n <!-- Rejected tab -->\n @if (!loading() && activeTab() === 'rejected') {\n @if (rejectedItems().length === 0) {\n <div class=\"empty-state\">\n <p>No rejected documents</p>\n </div>\n }\n @for (item of rejectedItems(); track item.id) {\n <div class=\"approval-item rejected\" (click)=\"navigateToDetail(item.id)\">\n <div class=\"item-title\">{{ item.title }}</div>\n <div class=\"item-meta\">\n <span class=\"item-requester\">By {{ item.requestedByUserName }}</span>\n </div>\n <div class=\"item-footer\">\n <span class=\"status-badge rejected-badge\">Rejected</span>\n <span class=\"item-time\">{{ item.completedAt | date:'shortDate' }}</span>\n <span class=\"item-link\">View →</span>\n </div>\n </div>\n }\n @if (rejectedItems().length >= 10) {\n <div class=\"show-more\" (click)=\"showMore('rejected')\">Show more →</div>\n }\n }\n </div>\n</div>\n", styles: [".panel-header{display:flex;justify-content:space-between;align-items:center;padding:16px 18px;border-bottom:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0}.panel-header-left{display:flex;align-items:center;gap:9px;color:var(--primary)}.panel-header h3{margin:0;font-size:16px;font-weight:700;color:var(--text-primary)}.close-btn{background:none;border:none;cursor:pointer;color:var(--text-muted);width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:background .15s,color .15s}.close-btn:hover{background:var(--bg-hover);color:var(--text-primary)}.tabs{display:flex;border-bottom:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0}.tab-btn{flex:1;display:flex;align-items:center;justify-content:center;gap:6px;padding:11px 8px;background:none;border:none;border-bottom:2px solid transparent;color:var(--text-muted);cursor:pointer;font-size:13px;font-weight:500;transition:color .15s,border-color .15s}.tab-btn.active{color:var(--primary);border-bottom-color:var(--primary)}.tab-btn:hover:not(.active){color:var(--text-secondary)}.tab-badge{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;background:var(--primary);color:#fff;font-size:11px;font-weight:700;border-radius:9px}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 20px;gap:12px;color:var(--text-muted)}.empty-state p{margin:0;font-size:13px}.loading-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px;gap:12px;color:var(--text-muted);font-size:13px}.spinner{width:24px;height:24px;border:2px solid var(--border-color);border-top-color:var(--primary);border-radius:50%;animation:spin .7s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}\n", ":host{--primary: #90caf9;--success: #66bb6a;--error: #ef5350;--text-primary: #e0e0e0;--text-secondary: #b0b0b0;--text-muted: #757575;--bg-primary: #1e1e2e;--bg-secondary: #27273a;--bg-hover: #2a2d4a;--border-color: #383850;--shadow: rgba(0,0,0,.4)}:host(.theme-light){--primary: #1565c0;--success: #2e7d32;--error: #c62828;--text-primary: #212121;--text-secondary: #616161;--text-muted: #9e9e9e;--bg-primary: #ffffff;--bg-secondary: #f5f5f5;--bg-hover: #e8eaf6;--border-color: #e0e0e0;--shadow: rgba(0,0,0,.15)}.approval-panel{position:fixed;top:0;right:-380px;width:380px;height:100vh;background:var(--bg-primary);box-shadow:-4px 0 24px var(--shadow);display:flex;flex-direction:column;z-index:1030;transition:right .3s cubic-bezier(.16,1,.3,1)}.approval-panel.open{right:var(--ma-ai-panel-width, 0px)}.panel-content{flex:1;overflow-y:auto;padding:8px 0}.approval-item{padding:14px 18px;border-bottom:1px solid var(--border-color);cursor:pointer;transition:background .15s}.approval-item:hover{background:var(--bg-hover)}.approval-item:last-child{border-bottom:none}.item-title{font-size:14px;font-weight:600;color:var(--text-primary);margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.item-meta{font-size:12px;color:var(--text-muted);margin-bottom:8px;display:flex;gap:4px;flex-wrap:wrap}.item-footer{display:flex;align-items:center;gap:8px}.item-time{font-size:11px;color:var(--text-muted);margin-right:auto}.item-link{font-size:12px;color:var(--primary);font-weight:500}.status-badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600;letter-spacing:.3px}.approved-badge{background:#66bb6a26;color:var(--success)}.rejected-badge{background:#ef53501f;color:var(--error)}.show-more{text-align:center;padding:14px;font-size:13px;color:var(--primary);cursor:pointer;font-weight:500}.show-more:hover{text-decoration:underline}\n"] }]
|
|
1488
|
-
}], ctorParameters: () => [], propDecorators: {
|
|
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
|
+
}] } });
|
|
1489
1760
|
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
* so consumer handlers can use inject() patterns.
|
|
1494
|
-
*/
|
|
1495
|
-
class MaAiToolsRegistry {
|
|
1496
|
-
config = inject(MES_AUTH_AI_CONFIG, { optional: true }) ?? DEFAULT_AI_CONFIG;
|
|
1497
|
-
injector = inject(Injector);
|
|
1498
|
-
/** Built-in client tools shipped by the library. */
|
|
1499
|
-
builtIn = [
|
|
1500
|
-
{
|
|
1501
|
-
name: 'navigate',
|
|
1502
|
-
description: 'Navigate the user to a path within the application. Use relative paths starting with /.',
|
|
1503
|
-
parameters: {
|
|
1504
|
-
type: 'object',
|
|
1505
|
-
properties: {
|
|
1506
|
-
path: { type: 'string', description: 'Path to navigate to, e.g. "/auth/users"' }
|
|
1507
|
-
},
|
|
1508
|
-
required: ['path']
|
|
1509
|
-
},
|
|
1510
|
-
readOnly: true,
|
|
1511
|
-
handler: (args) => {
|
|
1512
|
-
const router = this.injector.get(Router, null);
|
|
1513
|
-
if (!router)
|
|
1514
|
-
return 'Router is not available in this app.';
|
|
1515
|
-
router.navigateByUrl(args.path);
|
|
1516
|
-
return `Navigated to ${args.path}`;
|
|
1517
|
-
}
|
|
1518
|
-
},
|
|
1519
|
-
{
|
|
1520
|
-
name: 'toggle_theme',
|
|
1521
|
-
description: 'Toggle between light and dark theme.',
|
|
1522
|
-
parameters: { type: 'object', properties: {} },
|
|
1523
|
-
readOnly: true,
|
|
1524
|
-
handler: () => {
|
|
1525
|
-
const theme = this.injector.get(ThemeService);
|
|
1526
|
-
const next = theme.currentTheme() === 'light' ? 'dark' : 'light';
|
|
1527
|
-
theme.setFixTheme(next);
|
|
1528
|
-
return `Theme switched to ${next}.`;
|
|
1529
|
-
}
|
|
1530
|
-
},
|
|
1531
|
-
{
|
|
1532
|
-
name: 'show_toast',
|
|
1533
|
-
description: 'Show a small toast notification at the top of the screen.',
|
|
1534
|
-
parameters: {
|
|
1535
|
-
type: 'object',
|
|
1536
|
-
properties: {
|
|
1537
|
-
message: { type: 'string', description: 'Toast body text' },
|
|
1538
|
-
title: { type: 'string', description: 'Optional title' },
|
|
1539
|
-
severity: { type: 'string', enum: ['info', 'success', 'warning', 'error'], description: 'Toast severity' }
|
|
1540
|
-
},
|
|
1541
|
-
required: ['message']
|
|
1542
|
-
},
|
|
1543
|
-
readOnly: true,
|
|
1544
|
-
handler: (args) => {
|
|
1545
|
-
const toast = this.injector.get(ToastService);
|
|
1546
|
-
toast.show(args.message, args.title, args.severity ?? 'info');
|
|
1547
|
-
return 'Toast shown.';
|
|
1548
|
-
}
|
|
1549
|
-
},
|
|
1550
|
-
{
|
|
1551
|
-
name: 'who_am_i_client',
|
|
1552
|
-
description: 'Get the currently signed-in user as seen by the browser (id, name, roles loaded so far).',
|
|
1553
|
-
parameters: { type: 'object', properties: {} },
|
|
1554
|
-
readOnly: true,
|
|
1555
|
-
handler: () => {
|
|
1556
|
-
const auth = this.injector.get(MesAuthService);
|
|
1557
|
-
const u = auth.currentUser;
|
|
1558
|
-
if (!u)
|
|
1559
|
-
return 'No user signed in.';
|
|
1560
|
-
return JSON.stringify({
|
|
1561
|
-
id: u.userId ?? u.id,
|
|
1562
|
-
userName: u.userName,
|
|
1563
|
-
fullName: u.fullName,
|
|
1564
|
-
department: u.department,
|
|
1565
|
-
position: u.position,
|
|
1566
|
-
email: u.email
|
|
1567
|
-
});
|
|
1568
|
-
}
|
|
1569
|
-
},
|
|
1570
|
-
{
|
|
1571
|
-
name: 'reload_page',
|
|
1572
|
-
description: 'Reload the current browser page. Use as a last resort if state seems stuck.',
|
|
1573
|
-
parameters: { type: 'object', properties: {} },
|
|
1574
|
-
readOnly: false,
|
|
1575
|
-
handler: () => {
|
|
1576
|
-
window.location.reload();
|
|
1577
|
-
return 'Reloading page…';
|
|
1578
|
-
}
|
|
1579
|
-
}
|
|
1580
|
-
];
|
|
1581
|
-
/** All tools, deduplicated by name (consumer wins on conflict). */
|
|
1582
|
-
list() {
|
|
1583
|
-
const fromConfig = this.config.tools ?? [];
|
|
1584
|
-
const byName = new Map();
|
|
1585
|
-
for (const t of this.builtIn)
|
|
1586
|
-
byName.set(t.name, t);
|
|
1587
|
-
for (const t of fromConfig)
|
|
1588
|
-
byName.set(t.name, t);
|
|
1589
|
-
return Array.from(byName.values());
|
|
1590
|
-
}
|
|
1591
|
-
resolve(name) {
|
|
1592
|
-
return this.list().find(t => t.name === name);
|
|
1761
|
+
class ToastContainerComponent {
|
|
1762
|
+
get themeClass() {
|
|
1763
|
+
return `theme-${this.themeService.currentTheme()}`;
|
|
1593
1764
|
}
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
try {
|
|
1600
|
-
const raw = await Promise.resolve(tool.handler(args ?? {}));
|
|
1601
|
-
const result = typeof raw === 'string' ? raw : JSON.stringify(raw ?? null);
|
|
1602
|
-
return { ok: true, result };
|
|
1603
|
-
}
|
|
1604
|
-
catch (err) {
|
|
1605
|
-
return { ok: false, result: err?.message ?? String(err) };
|
|
1606
|
-
}
|
|
1765
|
+
toastService = inject(ToastService);
|
|
1766
|
+
themeService = inject(ThemeService);
|
|
1767
|
+
toasts = this.toastService.toasts;
|
|
1768
|
+
close(id) {
|
|
1769
|
+
this.toastService.remove(id);
|
|
1607
1770
|
}
|
|
1608
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type:
|
|
1609
|
-
static
|
|
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"] });
|
|
1610
1773
|
}
|
|
1611
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type:
|
|
1612
|
-
type:
|
|
1613
|
-
args: [{
|
|
1614
|
-
}]
|
|
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
|
+
}] } });
|
|
1615
1781
|
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
* `/ai/chat` SSE. State lives in signals so the panel can re-render reactively.
|
|
1621
|
-
*/
|
|
1622
|
-
class MaAiService {
|
|
1623
|
-
auth = inject(MesAuthService);
|
|
1624
|
-
aiConfig = inject(MES_AUTH_AI_CONFIG, { optional: true }) ?? DEFAULT_AI_CONFIG;
|
|
1625
|
-
tools = inject(MaAiToolsRegistry);
|
|
1626
|
-
messages = signal([], ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
|
|
1627
|
-
streaming = signal(false, ...(ngDevMode ? [{ debugName: "streaming" }] : /* istanbul ignore next */ []));
|
|
1628
|
-
/** When non-null, a write-class client tool is awaiting user confirmation. */
|
|
1629
|
-
pendingApproval = signal(null, ...(ngDevMode ? [{ debugName: "pendingApproval" }] : /* istanbul ignore next */ []));
|
|
1630
|
-
lastError = signal(null, ...(ngDevMode ? [{ debugName: "lastError" }] : /* istanbul ignore next */ []));
|
|
1631
|
-
sessionId = null;
|
|
1632
|
-
/** Verbs the user already chose "always approve" for in this session. */
|
|
1633
|
-
alwaysApproved = new Set();
|
|
1634
|
-
abortController = null;
|
|
1635
|
-
/** ID of the current assistant bubble we're streaming tokens into. */
|
|
1636
|
-
currentAssistantId = null;
|
|
1637
|
-
get enabled() {
|
|
1638
|
-
return this.aiConfig.enabled !== false;
|
|
1782
|
+
class NotificationPanelComponent {
|
|
1783
|
+
notificationRead = output();
|
|
1784
|
+
get themeClass() {
|
|
1785
|
+
return `theme-${this.themeService.currentTheme()}`;
|
|
1639
1786
|
}
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
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';
|
|
1648
1812
|
}
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
this.abortController.abort();
|
|
1653
|
-
this.abortController = null;
|
|
1654
|
-
}
|
|
1655
|
-
this.streaming.set(false);
|
|
1656
|
-
this.currentAssistantId = null;
|
|
1657
|
-
// If a client-tool call was awaiting the user, clearing it lets them start over.
|
|
1658
|
-
this.pendingApproval.set(null);
|
|
1813
|
+
toastType(type) {
|
|
1814
|
+
const t = this.typeOf({ type });
|
|
1815
|
+
return t.toLowerCase();
|
|
1659
1816
|
}
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
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
|
+
});
|
|
1667
1850
|
}
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
async resolvePendingApproval(decision) {
|
|
1673
|
-
const pending = this.pendingApproval();
|
|
1674
|
-
if (!pending)
|
|
1675
|
-
return;
|
|
1676
|
-
this.pendingApproval.set(null);
|
|
1677
|
-
if (decision === 'decline') {
|
|
1678
|
-
await this.continueWithToolResult(pending.callId, false, 'User declined this tool call.');
|
|
1679
|
-
this.markToolStatus(pending.callId, 'declined');
|
|
1680
|
-
return;
|
|
1681
|
-
}
|
|
1682
|
-
if (decision === 'always') {
|
|
1683
|
-
this.alwaysApproved.add(this.verbOf(pending.name));
|
|
1851
|
+
ngOnDestroy() {
|
|
1852
|
+
if (this.dateTimer !== null) {
|
|
1853
|
+
clearInterval(this.dateTimer);
|
|
1854
|
+
this.dateTimer = null;
|
|
1684
1855
|
}
|
|
1685
|
-
await this.executeClientTool(pending.callId, pending.name, pending.args);
|
|
1686
1856
|
}
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
this.
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
this.
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
readOnly: t.readOnly === true
|
|
1719
|
-
})),
|
|
1720
|
-
context: { currentRoute, appName: this.aiConfig.appName }
|
|
1721
|
-
}),
|
|
1722
|
-
signal: this.abortController.signal
|
|
1857
|
+
loadNotifications() {
|
|
1858
|
+
this.authService.getNotifications(1, 50, true).subscribe({
|
|
1859
|
+
next: (response) => {
|
|
1860
|
+
this.notifications.set(response.items || []);
|
|
1861
|
+
this.refreshDateLabels();
|
|
1862
|
+
},
|
|
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();
|
|
1886
|
+
},
|
|
1887
|
+
error: () => { }
|
|
1723
1888
|
});
|
|
1724
|
-
if (!res.ok || !res.body) {
|
|
1725
|
-
const detail = await res.text().catch(() => '');
|
|
1726
|
-
throw new Error(`AI request failed (${res.status}). ${detail}`);
|
|
1727
|
-
}
|
|
1728
|
-
await this.readSse(res.body);
|
|
1729
1889
|
}
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
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');
|
|
1738
1900
|
}
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
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();
|
|
1909
|
+
},
|
|
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));
|
|
1742
1982
|
}
|
|
1983
|
+
this.dateLabels.set(newLabels);
|
|
1743
1984
|
}
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
// SSE events are separated by a blank line.
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
buffer = buffer.slice(idx + 2);
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
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);
|
|
2024
|
+
}
|
|
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);
|
|
2030
|
+
}
|
|
2031
|
+
// ====================== Document ======================
|
|
2032
|
+
getDocument(id) {
|
|
2033
|
+
return this.http.get(`${this.apiBase}/approval/documents/${id}`, this.opts);
|
|
2034
|
+
}
|
|
2035
|
+
getDocumentHistory(id) {
|
|
2036
|
+
return this.http.get(`${this.apiBase}/approval/documents/${id}/history`, this.opts);
|
|
2037
|
+
}
|
|
2038
|
+
getDocumentContentUrl(id) {
|
|
2039
|
+
return `${this.apiBase}/approval/documents/${id}/content`;
|
|
2040
|
+
}
|
|
2041
|
+
getDocumentThumbnailUrl(id) {
|
|
2042
|
+
return `${this.apiBase}/approval/documents/${id}/thumbnail`;
|
|
2043
|
+
}
|
|
2044
|
+
// ====================== Actions ======================
|
|
2045
|
+
approve(documentId, comment) {
|
|
2046
|
+
const body = { comment };
|
|
2047
|
+
return this.http.post(`${this.apiBase}/approval/documents/${documentId}/approve`, body, this.opts);
|
|
2048
|
+
}
|
|
2049
|
+
reject(documentId, comment) {
|
|
2050
|
+
const body = { comment };
|
|
2051
|
+
return this.http.post(`${this.apiBase}/approval/documents/${documentId}/reject`, body, this.opts);
|
|
2052
|
+
}
|
|
2053
|
+
delegate(documentId, toUserId, reason) {
|
|
2054
|
+
const body = { toUserId, reason };
|
|
2055
|
+
return this.http.post(`${this.apiBase}/approval/documents/${documentId}/delegate`, body, this.opts);
|
|
2056
|
+
}
|
|
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);
|
|
1773
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
|
+
});
|
|
1774
2176
|
}
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
this.sessionId = ev.sessionId;
|
|
1779
|
-
break;
|
|
1780
|
-
case 'token':
|
|
1781
|
-
this.appendToken(ev.text);
|
|
1782
|
-
break;
|
|
1783
|
-
case 'tool_call_started':
|
|
1784
|
-
this.appendToolEvent({
|
|
1785
|
-
id: ev.id,
|
|
1786
|
-
name: ev.name,
|
|
1787
|
-
side: ev.side,
|
|
1788
|
-
status: 'running',
|
|
1789
|
-
args: ev.args,
|
|
1790
|
-
readOnly: ev.readOnly
|
|
1791
|
-
});
|
|
1792
|
-
break;
|
|
1793
|
-
case 'tool_call_completed':
|
|
1794
|
-
this.markToolStatus(ev.id, ev.ok ? 'ok' : 'error', ev.summary);
|
|
1795
|
-
break;
|
|
1796
|
-
case 'client_tool_call':
|
|
1797
|
-
this.appendToolEvent({
|
|
1798
|
-
id: ev.id,
|
|
1799
|
-
name: ev.name,
|
|
1800
|
-
side: 'client',
|
|
1801
|
-
status: ev.readOnly ? 'running' : 'awaiting-approval',
|
|
1802
|
-
args: ev.args,
|
|
1803
|
-
readOnly: ev.readOnly
|
|
1804
|
-
});
|
|
1805
|
-
if (ev.readOnly || this.alwaysApproved.has(this.verbOf(ev.name))) {
|
|
1806
|
-
// Auto-run; the next /ai/chat call will deliver the result.
|
|
1807
|
-
await this.executeClientTool(ev.id, ev.name, ev.args);
|
|
1808
|
-
}
|
|
1809
|
-
else {
|
|
1810
|
-
this.pendingApproval.set({ callId: ev.id, name: ev.name, args: ev.args, side: 'client' });
|
|
1811
|
-
}
|
|
1812
|
-
break;
|
|
1813
|
-
case 'error':
|
|
1814
|
-
this.lastError.set(ev.message);
|
|
1815
|
-
this.finalizeAssistant(`[error: ${ev.message}]`);
|
|
1816
|
-
break;
|
|
1817
|
-
case 'done':
|
|
1818
|
-
this.finalizeAssistant();
|
|
1819
|
-
break;
|
|
1820
|
-
}
|
|
2177
|
+
open() {
|
|
2178
|
+
this.isOpen.set(true);
|
|
2179
|
+
this.loadAllTabs();
|
|
1821
2180
|
}
|
|
1822
|
-
|
|
1823
|
-
this.
|
|
1824
|
-
const { ok, result } = await this.tools.runTool(name, args);
|
|
1825
|
-
this.markToolStatus(callId, ok ? 'ok' : 'error', this.summarize(result));
|
|
1826
|
-
await this.continueWithToolResult(callId, ok, result);
|
|
2181
|
+
close() {
|
|
2182
|
+
this.isOpen.set(false);
|
|
1827
2183
|
}
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
2184
|
+
toggle() {
|
|
2185
|
+
if (this.isOpen())
|
|
2186
|
+
this.close();
|
|
2187
|
+
else
|
|
2188
|
+
this.open();
|
|
1831
2189
|
}
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
this.
|
|
2190
|
+
switchTab(tab) {
|
|
2191
|
+
this.activeTab.set(tab);
|
|
2192
|
+
this.loadCurrentTab();
|
|
1835
2193
|
}
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
this.
|
|
1840
|
-
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);
|
|
1841
2198
|
return;
|
|
1842
2199
|
}
|
|
1843
|
-
|
|
1844
|
-
|
|
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
|
+
});
|
|
1845
2215
|
}
|
|
1846
|
-
|
|
1847
|
-
if (!this.
|
|
1848
|
-
const id = this.uid();
|
|
1849
|
-
this.currentAssistantId = id;
|
|
1850
|
-
this.appendBubble({ id, role: 'assistant', text: '', pending: true, toolEvents: [ev] });
|
|
2216
|
+
loadCurrentTab() {
|
|
2217
|
+
if (!this.approvalSvc)
|
|
1851
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
|
+
});
|
|
1852
2236
|
}
|
|
1853
|
-
const id = this.currentAssistantId;
|
|
1854
|
-
this.messages.update(arr => arr.map(m => m.id === id
|
|
1855
|
-
? { ...m, toolEvents: [...(m.toolEvents ?? []), ev] }
|
|
1856
|
-
: m));
|
|
1857
|
-
}
|
|
1858
|
-
markToolStatus(callId, status, summary) {
|
|
1859
|
-
this.messages.update(arr => arr.map(m => ({
|
|
1860
|
-
...m,
|
|
1861
|
-
toolEvents: m.toolEvents?.map(t => t.id === callId ? { ...t, status, summary: summary ?? t.summary } : t)
|
|
1862
|
-
})));
|
|
1863
|
-
}
|
|
1864
|
-
finalizeAssistant(extra) {
|
|
1865
|
-
const id = this.currentAssistantId;
|
|
1866
|
-
if (!id)
|
|
1867
|
-
return;
|
|
1868
|
-
this.messages.update(arr => arr.map(m => m.id === id
|
|
1869
|
-
? { ...m, pending: false, text: extra ? (m.text + (m.text ? '\n' : '') + extra) : m.text }
|
|
1870
|
-
: m));
|
|
1871
|
-
this.currentAssistantId = null;
|
|
1872
|
-
}
|
|
1873
|
-
verbOf(name) {
|
|
1874
|
-
return name.split('_')[0] ?? name;
|
|
1875
2237
|
}
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
2238
|
+
navigateToDetail(id) {
|
|
2239
|
+
this.close();
|
|
2240
|
+
this.router.navigate(['/auth/approval/detail', id]);
|
|
2241
|
+
this.approvalActioned.emit();
|
|
1879
2242
|
}
|
|
1880
|
-
|
|
1881
|
-
|
|
2243
|
+
showMore(status) {
|
|
2244
|
+
this.close();
|
|
2245
|
+
this.router.navigate(['/auth/approval/my-requests'], { queryParams: { status } });
|
|
1882
2246
|
}
|
|
1883
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type:
|
|
1884
|
-
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" }] });
|
|
1885
2249
|
}
|
|
1886
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type:
|
|
1887
|
-
type:
|
|
1888
|
-
args: [{
|
|
1889
|
-
}] });
|
|
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"] }] } });
|
|
1890
2254
|
|
|
1891
2255
|
/**
|
|
1892
2256
|
* Tiny hand-rolled GitHub-flavoured Markdown renderer for AI chat bubbles.
|
|
@@ -2064,6 +2428,153 @@ function sanitizeUrl(url) {
|
|
|
2064
2428
|
return null;
|
|
2065
2429
|
}
|
|
2066
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
|
+
|
|
2067
2578
|
const STORAGE_KEY = 'ma-ai-panel-width';
|
|
2068
2579
|
const MIN_WIDTH = 320;
|
|
2069
2580
|
const MAX_WIDTH = 800;
|
|
@@ -2107,6 +2618,7 @@ class MaAiChatPanelComponent {
|
|
|
2107
2618
|
isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
|
|
2108
2619
|
width = signal(this.loadWidth(), ...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
|
|
2109
2620
|
draft = signal('', ...(ngDevMode ? [{ debugName: "draft" }] : /* istanbul ignore next */ []));
|
|
2621
|
+
historyOpen = signal(false, ...(ngDevMode ? [{ debugName: "historyOpen" }] : /* istanbul ignore next */ []));
|
|
2110
2622
|
ai = inject(MaAiService);
|
|
2111
2623
|
router = inject(Router, { optional: true });
|
|
2112
2624
|
themeService = inject(ThemeService);
|
|
@@ -2182,6 +2694,14 @@ class MaAiChatPanelComponent {
|
|
|
2182
2694
|
}
|
|
2183
2695
|
newConversation() {
|
|
2184
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());
|
|
2185
2705
|
}
|
|
2186
2706
|
send() {
|
|
2187
2707
|
const text = this.draft().trim();
|
|
@@ -2256,11 +2776,11 @@ class MaAiChatPanelComponent {
|
|
|
2256
2776
|
el.scrollTop = el.scrollHeight;
|
|
2257
2777
|
}
|
|
2258
2778
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiChatPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2259
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: MaAiChatPanelComponent, isStandalone: true, selector: "ma-ai-chat-panel", host: { properties: { "class": "this.themeClass" } }, viewQueries: [{ propertyName: "scrollContainer", first: true, predicate: ["scrollContainer"], descendants: true, isSignal: true }, { propertyName: "textArea", first: true, predicate: ["textArea"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"ai-panel\" [class.open]=\"isOpen()\" [style.width.px]=\"width()\">\n\n <!-- Resize handle (left edge) -->\n <div class=\"ai-resize-handle\"\n (pointerdown)=\"startResize($event)\"\n (pointermove)=\"onResizeMove($event)\"\n (pointerup)=\"endResize($event)\"\n (pointercancel)=\"endResize($event)\"\n aria-label=\"Resize panel\"\n title=\"Drag to resize\"></div>\n\n <!-- Header -->\n <div class=\"panel-header\">\n <div class=\"panel-header-left\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\"\n fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M12 2 L13.5 7.5 L19 9 L13.5 10.5 L12 16 L10.5 10.5 L5 9 L10.5 7.5 Z\"/>\n <path d=\"M19 16 L19.8 18.2 L22 19 L19.8 19.8 L19 22 L18.2 19.8 L16 19 L18.2 18.2 Z\"/>\n </svg>\n <h3>AI Assistant</h3>\n </div>\n <div class=\"header-actions\">\n <button class=\"icon-btn\" (click)=\"newConversation()\" title=\"New conversation\" aria-label=\"New conversation\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\"\n fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M21 12a9 9 0 1 1-3-6.7L21 8\"/><path d=\"M21 3v5h-5\"/>\n </svg>\n </button>\n <button class=\"close-btn\" (click)=\"close()\" aria-label=\"Close\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\"\n fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"/><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"/>\n </svg>\n </button>\n </div>\n </div>\n\n <!-- Conversation -->\n <div class=\"panel-content\" #scrollContainer>\n @if (!hasMessages()) {\n <div class=\"empty-state\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"36\" height=\"36\" viewBox=\"0 0 24 24\"\n fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\" opacity=\"0.5\">\n <path d=\"M12 2 L13.5 7.5 L19 9 L13.5 10.5 L12 16 L10.5 10.5 L5 9 L10.5 7.5 Z\"/>\n </svg>\n <p>Ask anything about your account, approvals, notifications, or how to use the app.</p>\n <div class=\"quick-prompts\">\n @for (q of quickPrompts(); track q) {\n <button class=\"quick-prompt\" (click)=\"draft.set(q)\">{{ q }}</button>\n }\n </div>\n </div>\n }\n\n @for (m of messages(); track m.id) {\n <div class=\"bubble\" [class.user]=\"m.role === 'user'\" [class.assistant]=\"m.role === 'assistant'\">\n @if (m.role === 'assistant' && m.toolEvents && m.toolEvents.length > 0) {\n <div class=\"tool-strip\">\n @for (t of m.toolEvents; track t.id) {\n <div class=\"tool-chip\" [class.ok]=\"t.status === 'ok'\" [class.err]=\"t.status === 'error'\"\n [class.running]=\"t.status === 'running'\" [class.await]=\"t.status === 'awaiting-approval'\"\n [class.declined]=\"t.status === 'declined'\"\n [title]=\"t.summary ?? ''\">\n <span class=\"dot\"></span>\n <span class=\"tool-name\">{{ t.side === 'server' ? '\u2699' : '\u2318' }} {{ t.name }}</span>\n @if (t.status === 'running') { <span class=\"muted\">\u2026</span> }\n @if (t.status === 'ok' && t.summary) { <span class=\"muted\">\u00B7 {{ t.summary }}</span> }\n @if (t.status === 'error') { <span class=\"muted\">\u00B7 failed</span> }\n @if (t.status === 'awaiting-approval') { <span class=\"muted\">\u00B7 awaiting approval</span> }\n @if (t.status === 'declined') { <span class=\"muted\">\u00B7 declined</span> }\n </div>\n }\n </div>\n }\n @if (m.text) {\n @if (m.role === 'assistant') {\n <div class=\"bubble-text md-body\" [innerHTML]=\"m.text | maAiMarkdown\"></div>\n } @else {\n <div class=\"bubble-text\">{{ m.text }}</div>\n }\n }\n @if (m.pending && !m.text) {\n <div class=\"thinking\"><span class=\"spinner-small\"></span> Thinking\u2026</div>\n }\n </div>\n }\n </div>\n\n <!-- Pending approval card -->\n @if (pendingApproval(); as p) {\n <div class=\"approval-card\">\n <div class=\"approval-card-head\">\n <strong>Approve action?</strong>\n <code class=\"tool-name\">{{ p.name }}</code>\n </div>\n @if (p.args && (p.args | json) !== '{}') {\n <pre class=\"approval-args\">{{ p.args | json }}</pre>\n }\n <div class=\"approval-actions\">\n <button class=\"btn primary\" (click)=\"approve()\">Approve</button>\n <button class=\"btn\" (click)=\"alwaysApprove()\">Always</button>\n <button class=\"btn danger\" (click)=\"decline()\">Decline</button>\n </div>\n </div>\n }\n\n <!-- Composer -->\n <div class=\"composer\">\n @if (lastError()) {\n <div class=\"composer-error\">{{ lastError() }}</div>\n }\n <div class=\"composer-row\">\n <textarea #textArea\n rows=\"1\"\n placeholder=\"Ask the AI\u2026\"\n [value]=\"draft()\"\n (input)=\"onTextareaInput($event)\"\n (keydown)=\"onTextareaKey($event)\"\n [disabled]=\"streaming()\"></textarea>\n @if (streaming()) {\n <button class=\"send-btn cancel\" (click)=\"cancel()\" title=\"Cancel\" aria-label=\"Cancel\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><rect x=\"6\" y=\"6\" width=\"12\" height=\"12\"/></svg>\n </button>\n } @else {\n <button class=\"send-btn\" (click)=\"send()\" [disabled]=\"!draft().trim()\" title=\"Send\" aria-label=\"Send\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"22\" y1=\"2\" x2=\"11\" y2=\"13\"/><polygon points=\"22 2 15 22 11 13 2 9 22 2\"/></svg>\n </button>\n }\n </div>\n </div>\n\n <!-- Footer with hints (lifts the composer off the absolute bottom of the viewport) -->\n <div class=\"ai-footer\">\n <span class=\"hint\">\n <kbd>Enter</kbd> send \u00B7 <kbd>Shift</kbd>+<kbd>Enter</kbd> new line\n </span>\n <span class=\"hint dim\">AI can make mistakes — verify important info</span>\n </div>\n</div>\n", styles: [".panel-header{display:flex;justify-content:space-between;align-items:center;padding:16px 18px;border-bottom:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0}.panel-header-left{display:flex;align-items:center;gap:9px;color:var(--primary)}.panel-header h3{margin:0;font-size:16px;font-weight:700;color:var(--text-primary)}.close-btn{background:none;border:none;cursor:pointer;color:var(--text-muted);width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:background .15s,color .15s}.close-btn:hover{background:var(--bg-hover);color:var(--text-primary)}.tabs{display:flex;border-bottom:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0}.tab-btn{flex:1;display:flex;align-items:center;justify-content:center;gap:6px;padding:11px 8px;background:none;border:none;border-bottom:2px solid transparent;color:var(--text-muted);cursor:pointer;font-size:13px;font-weight:500;transition:color .15s,border-color .15s}.tab-btn.active{color:var(--primary);border-bottom-color:var(--primary)}.tab-btn:hover:not(.active){color:var(--text-secondary)}.tab-badge{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;background:var(--primary);color:#fff;font-size:11px;font-weight:700;border-radius:9px}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 20px;gap:12px;color:var(--text-muted)}.empty-state p{margin:0;font-size:13px}.loading-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px;gap:12px;color:var(--text-muted);font-size:13px}.spinner{width:24px;height:24px;border:2px solid var(--border-color);border-top-color:var(--primary);border-radius:50%;animation:spin .7s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}\n", ":host{--primary: #c4b5fd;--primary-strong: #a78bfa;--success: #66bb6a;--error: #ef5350;--text-primary: #e0e0e0;--text-secondary: #b0b0b0;--text-muted: #757575;--bg-primary: #1e1e2e;--bg-secondary: #27273a;--bg-hover: #2a2d4a;--bg-bubble-user: #312e81;--bg-bubble-assistant: #232336;--border-color: #383850;--shadow: rgba(0,0,0,.4)}:host(.theme-light){--primary: #7c3aed;--primary-strong: #6d28d9;--success: #2e7d32;--error: #c62828;--text-primary: #212121;--text-secondary: #616161;--text-muted: #9e9e9e;--bg-primary: #ffffff;--bg-secondary: #f5f3ff;--bg-hover: #ede9fe;--bg-bubble-user: #ede9fe;--bg-bubble-assistant: #f5f5f5;--border-color: #e0e0e0;--shadow: rgba(0,0,0,.15)}.ai-panel{position:fixed;top:0;right:0;height:100vh;background:var(--bg-primary);box-shadow:-4px 0 24px var(--shadow);display:flex;flex-direction:column;z-index:1040;transform:translate(100%);transition:transform .3s cubic-bezier(.16,1,.3,1);will-change:transform,width;pointer-events:none}.ai-panel.open{transform:translate(0);pointer-events:auto}.ai-resize-handle{position:absolute;top:0;left:-4px;width:8px;height:100%;cursor:ew-resize;z-index:1;background:transparent}.ai-resize-handle:hover{background:linear-gradient(to right,transparent,rgba(124,58,237,.25),transparent)}.header-actions{display:flex;align-items:center;gap:4px}.icon-btn{background:none;border:none;cursor:pointer;color:var(--text-muted);width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:background .15s,color .15s}.icon-btn:hover{background:var(--bg-hover);color:var(--text-primary)}.panel-content{flex:1;overflow-y:auto;padding:12px 14px;display:flex;flex-direction:column;gap:10px}.quick-prompts{display:flex;flex-wrap:wrap;gap:6px;justify-content:center;margin-top:8px}.quick-prompt{background:var(--bg-secondary);border:1px solid var(--border-color);color:var(--text-secondary);padding:6px 10px;border-radius:14px;font-size:12px;cursor:pointer;transition:background .15s,color .15s}.quick-prompt:hover{background:var(--bg-hover);color:var(--text-primary)}.bubble{max-width:90%;padding:10px 12px;border-radius:10px;font-size:13.5px;line-height:1.5;word-wrap:break-word;white-space:pre-wrap}.bubble.user{align-self:flex-end;background:var(--bg-bubble-user);color:var(--text-primary)}.bubble.assistant{align-self:flex-start;background:var(--bg-bubble-assistant);color:var(--text-primary)}.bubble-text{display:block}.thinking{display:flex;align-items:center;gap:6px;color:var(--text-muted);font-size:12.5px}.spinner-small{width:12px;height:12px;border:2px solid var(--border-color);border-top-color:var(--primary);border-radius:50%;animation:spin .7s linear infinite;display:inline-block}@keyframes spin{to{transform:rotate(360deg)}}.tool-strip{display:flex;flex-direction:column;gap:4px;margin-bottom:6px}.tool-chip{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border-radius:12px;background:var(--bg-hover);font-size:11.5px;color:var(--text-secondary);border:1px solid var(--border-color);align-self:flex-start}.tool-chip .dot{width:6px;height:6px;border-radius:50%;background:var(--text-muted)}.tool-chip.running .dot{background:var(--primary);animation:pulse 1s ease-in-out infinite}.tool-chip.ok .dot{background:var(--success)}.tool-chip.err .dot{background:var(--error)}.tool-chip.await .dot{background:#f59e0b}.tool-chip.declined .dot{background:var(--text-muted)}.tool-chip .tool-name{font-weight:600;color:var(--text-primary)}.tool-chip .muted{color:var(--text-muted)}@keyframes pulse{50%{opacity:.4}}.approval-card{flex-shrink:0;margin:8px 14px;padding:12px 14px;border:1px solid var(--primary);border-radius:10px;background:var(--bg-secondary)}.approval-card-head{display:flex;align-items:center;gap:10px;margin-bottom:8px;color:var(--text-primary)}.approval-card-head code{background:var(--bg-hover);padding:2px 6px;border-radius:6px;font-size:12px;color:var(--primary)}.approval-args{background:var(--bg-primary);border:1px solid var(--border-color);border-radius:6px;padding:8px;font-size:11.5px;max-height:100px;overflow:auto;margin:0 0 8px;color:var(--text-secondary)}.approval-actions{display:flex;gap:6px;flex-wrap:wrap}.btn{flex:1;padding:6px 10px;font-size:12.5px;font-weight:600;border-radius:6px;border:1px solid var(--border-color);background:var(--bg-primary);color:var(--text-primary);cursor:pointer;transition:background .15s,border-color .15s}.btn:hover{background:var(--bg-hover)}.btn.primary{background:var(--primary);color:#fff;border-color:var(--primary)}.btn.primary:hover{background:var(--primary-strong)}.btn.danger{color:var(--error)}.btn.danger:hover{background:#ef53501a}.composer{flex-shrink:0;border-top:1px solid var(--border-color);padding:10px 12px;background:var(--bg-secondary)}.composer-error{color:var(--error);font-size:12px;margin-bottom:6px}.composer-row{display:flex;align-items:flex-end;gap:8px}.composer textarea{flex:1;resize:none;padding:8px 10px;border-radius:8px;border:1px solid var(--border-color);background:var(--bg-primary);color:var(--text-primary);font-size:13px;font-family:inherit;outline:none;min-height:36px;max-height:160px;line-height:1.4}.composer textarea:focus{border-color:var(--primary)}.composer textarea:disabled{opacity:.6}.send-btn{width:36px;height:36px;flex-shrink:0;display:flex;align-items:center;justify-content:center;background:var(--primary);color:#fff;border:none;border-radius:8px;cursor:pointer;transition:background .15s}.send-btn:hover:not(:disabled){background:var(--primary-strong)}.send-btn:disabled{opacity:.5;cursor:not-allowed}.send-btn.cancel{background:var(--error)}.send-btn.cancel:hover{filter:brightness(1.1)}.ai-footer{flex-shrink:0;display:flex;flex-direction:column;align-items:center;gap:2px;padding:6px 12px 10px;background:var(--bg-secondary);border-top:1px solid var(--border-color);font-size:11px;color:var(--text-secondary);line-height:1.4;text-align:center}.ai-footer .hint{display:inline-block}.ai-footer .hint.dim{color:var(--text-muted);font-size:10.5px}.ai-footer kbd{display:inline-block;padding:0 5px;margin:0 1px;border:1px solid var(--border-color);border-bottom-width:2px;border-radius:4px;background:var(--bg-primary);color:var(--text-primary);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10px;line-height:1.4}.md-body{font-size:13.5px;line-height:1.55}.md-body .md-p{margin:0 0 8px}.md-body .md-p:last-child{margin-bottom:0}.md-body .md-h1,.md-body .md-h2,.md-body .md-h3,.md-body .md-h4,.md-body .md-h5,.md-body .md-h6{margin:12px 0 6px;font-weight:700;color:var(--text-primary);line-height:1.3}.md-body .md-h1{font-size:17px}.md-body .md-h2{font-size:15.5px}.md-body .md-h3{font-size:14.5px}.md-body .md-h4,.md-body .md-h5,.md-body .md-h6{font-size:13.5px}.md-body strong{font-weight:700}.md-body em{font-style:italic}.md-body .md-link{color:var(--primary);text-decoration:underline;text-underline-offset:2px}.md-body .md-link:hover{color:var(--primary-strong)}.md-body .md-list{margin:0 0 8px;padding-left:22px}.md-body .md-list li{margin:2px 0}.md-body .md-list li::marker{color:var(--text-muted)}.md-body .md-quote{margin:0 0 8px;padding:6px 10px;border-left:3px solid var(--primary);background:var(--bg-hover);color:var(--text-secondary);border-radius:0 6px 6px 0}.md-body .md-code{background:var(--bg-hover);border:1px solid var(--border-color);border-radius:4px;padding:1px 5px;font-size:12px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:var(--primary)}.md-body .md-pre{background:var(--bg-primary);border:1px solid var(--border-color);border-radius:8px;padding:10px 12px;margin:0 0 8px;overflow-x:auto;font-size:12px;line-height:1.5}.md-body .md-pre code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:var(--text-primary);background:none;border:none;padding:0}.md-body .md-table-wrap{margin:0 0 8px;overflow-x:auto;border:1px solid var(--border-color);border-radius:8px}.md-body .md-table{border-collapse:collapse;width:100%;font-size:12.5px}.md-body .md-table th,.md-body .md-table td{padding:6px 10px;border-bottom:1px solid var(--border-color);vertical-align:top;text-align:left}.md-body .md-table th{background:var(--bg-secondary);color:var(--text-primary);font-weight:600}.md-body .md-table tr:last-child td{border-bottom:none}.md-body .md-table tbody tr:hover{background:var(--bg-hover)}\n"], dependencies: [{ kind: "pipe", type: JsonPipe, name: "json" }, { kind: "pipe", type: MaAiMarkdownPipe, name: "maAiMarkdown" }] });
|
|
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" }] });
|
|
2260
2780
|
}
|
|
2261
2781
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MaAiChatPanelComponent, decorators: [{
|
|
2262
2782
|
type: Component,
|
|
2263
|
-
args: [{ selector: 'ma-ai-chat-panel', imports: [JsonPipe, MaAiMarkdownPipe], template: "<div class=\"ai-panel\" [class.open]=\"isOpen()\" [style.width.px]=\"width()\">\n\n <!-- Resize handle (left edge) -->\n <div class=\"ai-resize-handle\"\n (pointerdown)=\"startResize($event)\"\n (pointermove)=\"onResizeMove($event)\"\n (pointerup)=\"endResize($event)\"\n (pointercancel)=\"endResize($event)\"\n aria-label=\"Resize panel\"\n title=\"Drag to resize\"></div>\n\n <!-- Header -->\n <div class=\"panel-header\">\n <div class=\"panel-header-left\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\"\n fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M12 2 L13.5 7.5 L19 9 L13.5 10.5 L12 16 L10.5 10.5 L5 9 L10.5 7.5 Z\"/>\n <path d=\"M19 16 L19.8 18.2 L22 19 L19.8 19.8 L19 22 L18.2 19.8 L16 19 L18.2 18.2 Z\"/>\n </svg>\n <h3>AI Assistant</h3>\n </div>\n <div class=\"header-actions\">\n <button class=\"icon-btn\" (click)=\"newConversation()\" title=\"New conversation\" aria-label=\"New conversation\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\"\n fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M21 12a9 9 0 1 1-3-6.7L21 8\"/><path d=\"M21 3v5h-5\"/>\n </svg>\n </button>\n <button class=\"close-btn\" (click)=\"close()\" aria-label=\"Close\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\"\n fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"/><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"/>\n </svg>\n </button>\n </div>\n </div>\n\n <!-- Conversation -->\n <div class=\"panel-content\" #scrollContainer>\n @if (!hasMessages()) {\n <div class=\"empty-state\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"36\" height=\"36\" viewBox=\"0 0 24 24\"\n fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\" opacity=\"0.5\">\n <path d=\"M12 2 L13.5 7.5 L19 9 L13.5 10.5 L12 16 L10.5 10.5 L5 9 L10.5 7.5 Z\"/>\n </svg>\n <p>Ask anything about your account, approvals, notifications, or how to use the app.</p>\n <div class=\"quick-prompts\">\n @for (q of quickPrompts(); track q) {\n <button class=\"quick-prompt\" (click)=\"draft.set(q)\">{{ q }}</button>\n }\n </div>\n </div>\n }\n\n @for (m of messages(); track m.id) {\n <div class=\"bubble\" [class.user]=\"m.role === 'user'\" [class.assistant]=\"m.role === 'assistant'\">\n @if (m.role === 'assistant' && m.toolEvents && m.toolEvents.length > 0) {\n <div class=\"tool-strip\">\n @for (t of m.toolEvents; track t.id) {\n <div class=\"tool-chip\" [class.ok]=\"t.status === 'ok'\" [class.err]=\"t.status === 'error'\"\n [class.running]=\"t.status === 'running'\" [class.await]=\"t.status === 'awaiting-approval'\"\n [class.declined]=\"t.status === 'declined'\"\n [title]=\"t.summary ?? ''\">\n <span class=\"dot\"></span>\n <span class=\"tool-name\">{{ t.side === 'server' ? '\u2699' : '\u2318' }} {{ t.name }}</span>\n @if (t.status === 'running') { <span class=\"muted\">\u2026</span> }\n @if (t.status === 'ok' && t.summary) { <span class=\"muted\">\u00B7 {{ t.summary }}</span> }\n @if (t.status === 'error') { <span class=\"muted\">\u00B7 failed</span> }\n @if (t.status === 'awaiting-approval') { <span class=\"muted\">\u00B7 awaiting approval</span> }\n @if (t.status === 'declined') { <span class=\"muted\">\u00B7 declined</span> }\n </div>\n }\n </div>\n }\n @if (m.text) {\n @if (m.role === 'assistant') {\n <div class=\"bubble-text md-body\" [innerHTML]=\"m.text | maAiMarkdown\"></div>\n } @else {\n <div class=\"bubble-text\">{{ m.text }}</div>\n }\n }\n @if (m.pending && !m.text) {\n <div class=\"thinking\"><span class=\"spinner-small\"></span> Thinking\u2026</div>\n }\n </div>\n }\n </div>\n\n <!-- Pending approval card -->\n @if (pendingApproval(); as p) {\n <div class=\"approval-card\">\n <div class=\"approval-card-head\">\n <strong>Approve action?</strong>\n <code class=\"tool-name\">{{ p.name }}</code>\n </div>\n @if (p.args && (p.args | json) !== '{}') {\n <pre class=\"approval-args\">{{ p.args | json }}</pre>\n }\n <div class=\"approval-actions\">\n <button class=\"btn primary\" (click)=\"approve()\">Approve</button>\n <button class=\"btn\" (click)=\"alwaysApprove()\">Always</button>\n <button class=\"btn danger\" (click)=\"decline()\">Decline</button>\n </div>\n </div>\n }\n\n <!-- Composer -->\n <div class=\"composer\">\n @if (lastError()) {\n <div class=\"composer-error\">{{ lastError() }}</div>\n }\n <div class=\"composer-row\">\n <textarea #textArea\n rows=\"1\"\n placeholder=\"Ask the AI\u2026\"\n [value]=\"draft()\"\n (input)=\"onTextareaInput($event)\"\n (keydown)=\"onTextareaKey($event)\"\n [disabled]=\"streaming()\"></textarea>\n @if (streaming()) {\n <button class=\"send-btn cancel\" (click)=\"cancel()\" title=\"Cancel\" aria-label=\"Cancel\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><rect x=\"6\" y=\"6\" width=\"12\" height=\"12\"/></svg>\n </button>\n } @else {\n <button class=\"send-btn\" (click)=\"send()\" [disabled]=\"!draft().trim()\" title=\"Send\" aria-label=\"Send\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"22\" y1=\"2\" x2=\"11\" y2=\"13\"/><polygon points=\"22 2 15 22 11 13 2 9 22 2\"/></svg>\n </button>\n }\n </div>\n </div>\n\n <!-- Footer with hints (lifts the composer off the absolute bottom of the viewport) -->\n <div class=\"ai-footer\">\n <span class=\"hint\">\n <kbd>Enter</kbd> send \u00B7 <kbd>Shift</kbd>+<kbd>Enter</kbd> new line\n </span>\n <span class=\"hint dim\">AI can make mistakes — verify important info</span>\n </div>\n</div>\n", styles: [".panel-header{display:flex;justify-content:space-between;align-items:center;padding:16px 18px;border-bottom:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0}.panel-header-left{display:flex;align-items:center;gap:9px;color:var(--primary)}.panel-header h3{margin:0;font-size:16px;font-weight:700;color:var(--text-primary)}.close-btn{background:none;border:none;cursor:pointer;color:var(--text-muted);width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:background .15s,color .15s}.close-btn:hover{background:var(--bg-hover);color:var(--text-primary)}.tabs{display:flex;border-bottom:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0}.tab-btn{flex:1;display:flex;align-items:center;justify-content:center;gap:6px;padding:11px 8px;background:none;border:none;border-bottom:2px solid transparent;color:var(--text-muted);cursor:pointer;font-size:13px;font-weight:500;transition:color .15s,border-color .15s}.tab-btn.active{color:var(--primary);border-bottom-color:var(--primary)}.tab-btn:hover:not(.active){color:var(--text-secondary)}.tab-badge{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;background:var(--primary);color:#fff;font-size:11px;font-weight:700;border-radius:9px}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 20px;gap:12px;color:var(--text-muted)}.empty-state p{margin:0;font-size:13px}.loading-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px;gap:12px;color:var(--text-muted);font-size:13px}.spinner{width:24px;height:24px;border:2px solid var(--border-color);border-top-color:var(--primary);border-radius:50%;animation:spin .7s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}\n", ":host{--primary: #c4b5fd;--primary-strong: #a78bfa;--success: #66bb6a;--error: #ef5350;--text-primary: #e0e0e0;--text-secondary: #b0b0b0;--text-muted: #757575;--bg-primary: #1e1e2e;--bg-secondary: #27273a;--bg-hover: #2a2d4a;--bg-bubble-user: #312e81;--bg-bubble-assistant: #232336;--border-color: #383850;--shadow: rgba(0,0,0,.4)}:host(.theme-light){--primary: #7c3aed;--primary-strong: #6d28d9;--success: #2e7d32;--error: #c62828;--text-primary: #212121;--text-secondary: #616161;--text-muted: #9e9e9e;--bg-primary: #ffffff;--bg-secondary: #f5f3ff;--bg-hover: #ede9fe;--bg-bubble-user: #ede9fe;--bg-bubble-assistant: #f5f5f5;--border-color: #e0e0e0;--shadow: rgba(0,0,0,.15)}.ai-panel{position:fixed;top:0;right:0;height:100vh;background:var(--bg-primary);box-shadow:-4px 0 24px var(--shadow);display:flex;flex-direction:column;z-index:1040;transform:translate(100%);transition:transform .3s cubic-bezier(.16,1,.3,1);will-change:transform,width;pointer-events:none}.ai-panel.open{transform:translate(0);pointer-events:auto}.ai-resize-handle{position:absolute;top:0;left:-4px;width:8px;height:100%;cursor:ew-resize;z-index:1;background:transparent}.ai-resize-handle:hover{background:linear-gradient(to right,transparent,rgba(124,58,237,.25),transparent)}.header-actions{display:flex;align-items:center;gap:4px}.icon-btn{background:none;border:none;cursor:pointer;color:var(--text-muted);width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:background .15s,color .15s}.icon-btn:hover{background:var(--bg-hover);color:var(--text-primary)}.panel-content{flex:1;overflow-y:auto;padding:12px 14px;display:flex;flex-direction:column;gap:10px}.quick-prompts{display:flex;flex-wrap:wrap;gap:6px;justify-content:center;margin-top:8px}.quick-prompt{background:var(--bg-secondary);border:1px solid var(--border-color);color:var(--text-secondary);padding:6px 10px;border-radius:14px;font-size:12px;cursor:pointer;transition:background .15s,color .15s}.quick-prompt:hover{background:var(--bg-hover);color:var(--text-primary)}.bubble{max-width:90%;padding:10px 12px;border-radius:10px;font-size:13.5px;line-height:1.5;word-wrap:break-word;white-space:pre-wrap}.bubble.user{align-self:flex-end;background:var(--bg-bubble-user);color:var(--text-primary)}.bubble.assistant{align-self:flex-start;background:var(--bg-bubble-assistant);color:var(--text-primary)}.bubble-text{display:block}.thinking{display:flex;align-items:center;gap:6px;color:var(--text-muted);font-size:12.5px}.spinner-small{width:12px;height:12px;border:2px solid var(--border-color);border-top-color:var(--primary);border-radius:50%;animation:spin .7s linear infinite;display:inline-block}@keyframes spin{to{transform:rotate(360deg)}}.tool-strip{display:flex;flex-direction:column;gap:4px;margin-bottom:6px}.tool-chip{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border-radius:12px;background:var(--bg-hover);font-size:11.5px;color:var(--text-secondary);border:1px solid var(--border-color);align-self:flex-start}.tool-chip .dot{width:6px;height:6px;border-radius:50%;background:var(--text-muted)}.tool-chip.running .dot{background:var(--primary);animation:pulse 1s ease-in-out infinite}.tool-chip.ok .dot{background:var(--success)}.tool-chip.err .dot{background:var(--error)}.tool-chip.await .dot{background:#f59e0b}.tool-chip.declined .dot{background:var(--text-muted)}.tool-chip .tool-name{font-weight:600;color:var(--text-primary)}.tool-chip .muted{color:var(--text-muted)}@keyframes pulse{50%{opacity:.4}}.approval-card{flex-shrink:0;margin:8px 14px;padding:12px 14px;border:1px solid var(--primary);border-radius:10px;background:var(--bg-secondary)}.approval-card-head{display:flex;align-items:center;gap:10px;margin-bottom:8px;color:var(--text-primary)}.approval-card-head code{background:var(--bg-hover);padding:2px 6px;border-radius:6px;font-size:12px;color:var(--primary)}.approval-args{background:var(--bg-primary);border:1px solid var(--border-color);border-radius:6px;padding:8px;font-size:11.5px;max-height:100px;overflow:auto;margin:0 0 8px;color:var(--text-secondary)}.approval-actions{display:flex;gap:6px;flex-wrap:wrap}.btn{flex:1;padding:6px 10px;font-size:12.5px;font-weight:600;border-radius:6px;border:1px solid var(--border-color);background:var(--bg-primary);color:var(--text-primary);cursor:pointer;transition:background .15s,border-color .15s}.btn:hover{background:var(--bg-hover)}.btn.primary{background:var(--primary);color:#fff;border-color:var(--primary)}.btn.primary:hover{background:var(--primary-strong)}.btn.danger{color:var(--error)}.btn.danger:hover{background:#ef53501a}.composer{flex-shrink:0;border-top:1px solid var(--border-color);padding:10px 12px;background:var(--bg-secondary)}.composer-error{color:var(--error);font-size:12px;margin-bottom:6px}.composer-row{display:flex;align-items:flex-end;gap:8px}.composer textarea{flex:1;resize:none;padding:8px 10px;border-radius:8px;border:1px solid var(--border-color);background:var(--bg-primary);color:var(--text-primary);font-size:13px;font-family:inherit;outline:none;min-height:36px;max-height:160px;line-height:1.4}.composer textarea:focus{border-color:var(--primary)}.composer textarea:disabled{opacity:.6}.send-btn{width:36px;height:36px;flex-shrink:0;display:flex;align-items:center;justify-content:center;background:var(--primary);color:#fff;border:none;border-radius:8px;cursor:pointer;transition:background .15s}.send-btn:hover:not(:disabled){background:var(--primary-strong)}.send-btn:disabled{opacity:.5;cursor:not-allowed}.send-btn.cancel{background:var(--error)}.send-btn.cancel:hover{filter:brightness(1.1)}.ai-footer{flex-shrink:0;display:flex;flex-direction:column;align-items:center;gap:2px;padding:6px 12px 10px;background:var(--bg-secondary);border-top:1px solid var(--border-color);font-size:11px;color:var(--text-secondary);line-height:1.4;text-align:center}.ai-footer .hint{display:inline-block}.ai-footer .hint.dim{color:var(--text-muted);font-size:10.5px}.ai-footer kbd{display:inline-block;padding:0 5px;margin:0 1px;border:1px solid var(--border-color);border-bottom-width:2px;border-radius:4px;background:var(--bg-primary);color:var(--text-primary);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10px;line-height:1.4}.md-body{font-size:13.5px;line-height:1.55}.md-body .md-p{margin:0 0 8px}.md-body .md-p:last-child{margin-bottom:0}.md-body .md-h1,.md-body .md-h2,.md-body .md-h3,.md-body .md-h4,.md-body .md-h5,.md-body .md-h6{margin:12px 0 6px;font-weight:700;color:var(--text-primary);line-height:1.3}.md-body .md-h1{font-size:17px}.md-body .md-h2{font-size:15.5px}.md-body .md-h3{font-size:14.5px}.md-body .md-h4,.md-body .md-h5,.md-body .md-h6{font-size:13.5px}.md-body strong{font-weight:700}.md-body em{font-style:italic}.md-body .md-link{color:var(--primary);text-decoration:underline;text-underline-offset:2px}.md-body .md-link:hover{color:var(--primary-strong)}.md-body .md-list{margin:0 0 8px;padding-left:22px}.md-body .md-list li{margin:2px 0}.md-body .md-list li::marker{color:var(--text-muted)}.md-body .md-quote{margin:0 0 8px;padding:6px 10px;border-left:3px solid var(--primary);background:var(--bg-hover);color:var(--text-secondary);border-radius:0 6px 6px 0}.md-body .md-code{background:var(--bg-hover);border:1px solid var(--border-color);border-radius:4px;padding:1px 5px;font-size:12px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:var(--primary)}.md-body .md-pre{background:var(--bg-primary);border:1px solid var(--border-color);border-radius:8px;padding:10px 12px;margin:0 0 8px;overflow-x:auto;font-size:12px;line-height:1.5}.md-body .md-pre code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:var(--text-primary);background:none;border:none;padding:0}.md-body .md-table-wrap{margin:0 0 8px;overflow-x:auto;border:1px solid var(--border-color);border-radius:8px}.md-body .md-table{border-collapse:collapse;width:100%;font-size:12.5px}.md-body .md-table th,.md-body .md-table td{padding:6px 10px;border-bottom:1px solid var(--border-color);vertical-align:top;text-align:left}.md-body .md-table th{background:var(--bg-secondary);color:var(--text-primary);font-weight:600}.md-body .md-table tr:last-child td{border-bottom:none}.md-body .md-table tbody tr:hover{background:var(--bg-hover)}\n"] }]
|
|
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"] }]
|
|
2264
2784
|
}], ctorParameters: () => [], propDecorators: { scrollContainer: [{ type: i0.ViewChild, args: ['scrollContainer', { isSignal: true }] }], textArea: [{ type: i0.ViewChild, args: ['textArea', { isSignal: true }] }], themeClass: [{
|
|
2265
2785
|
type: HostBinding,
|
|
2266
2786
|
args: ['class']
|
|
@@ -3345,5 +3865,5 @@ async function runReturnViaPostMessageIfRequested(authService) {
|
|
|
3345
3865
|
* Generated bundle index. Do not edit.
|
|
3346
3866
|
*/
|
|
3347
3867
|
|
|
3348
|
-
export { ALL_ACTIONS, AVATAR_FRAMES, ApprovalActionType, ApprovalDocumentStatus, ApprovalStepMode, ApprovalStepStatus, DEFAULT_AI_CONFIG, MES_AUTH_AI_CONFIG, MES_AUTH_CONFIG, MaAiButtonComponent, MaAiChatPanelComponent, MaAiMarkdownPipe, MaAiService, MaAiToolsRegistry, MaApprovalPanelComponent, MaApprovalService, MaArvContainerComponent, MaAvatarComponent, MaIconComponent, MaThemeDirective, MaUiConfigService, MaUserComponent, MaUserMenuColor, MaUserMenuComponent, MaUserXComponent, MesAuthModule, MesAuthService, NotificationBadgeComponent, NotificationPanelComponent, NotificationType, PACKAGE_VERSION, ThemeService, ToastContainerComponent, ToastService, UserProfileComponent, extractXMaPerm, mesAuthInterceptor, provideMesAuth, provideMesAuthAi, renderMarkdown, runReturnViaPostMessageIfRequested, runSsoCheckHandshake, withXMaPerm, xMaResource };
|
|
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 };
|
|
3349
3869
|
//# sourceMappingURL=mesauth-angular.mjs.map
|