@noatgnu/cupcake-core 1.3.10 → 1.3.12
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/noatgnu-cupcake-core.mjs +210 -21
- package/fesm2022/noatgnu-cupcake-core.mjs.map +1 -1
- package/index.d.ts +59 -5
- package/package.json +1 -1
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject,
|
|
2
|
+
import { inject, Injectable, InjectionToken, signal, computed, Component, effect, Inject, ChangeDetectorRef, NgModule } from '@angular/core';
|
|
3
3
|
import * as i1 from '@angular/common/http';
|
|
4
|
-
import { HttpClient, HttpParams, provideHttpClient, withInterceptors, HttpClientModule } from '@angular/common/http';
|
|
5
|
-
import { BehaviorSubject, catchError, throwError, switchMap, filter, take, map, tap, Subject, timer, EMPTY, interval, debounceTime, distinctUntilChanged } from 'rxjs';
|
|
4
|
+
import { HttpClient, HttpResponse, HttpParams, provideHttpClient, withInterceptors, HttpClientModule } from '@angular/common/http';
|
|
5
|
+
import { BehaviorSubject, catchError, throwError, switchMap, filter, take, map, tap as tap$1, Subject, timer, EMPTY, interval, debounceTime, distinctUntilChanged } from 'rxjs';
|
|
6
6
|
import { Router, ActivatedRoute, RouterModule } from '@angular/router';
|
|
7
|
-
import { map as map$1, takeUntil,
|
|
7
|
+
import { tap, map as map$1, takeUntil, switchMap as switchMap$1 } from 'rxjs/operators';
|
|
8
8
|
import * as i1$1 from '@angular/forms';
|
|
9
9
|
import { FormBuilder, Validators, ReactiveFormsModule, FormsModule, NonNullableFormBuilder } from '@angular/forms';
|
|
10
10
|
import * as i2 from '@angular/common';
|
|
11
|
-
import { CommonModule } from '@angular/common';
|
|
11
|
+
import { CommonModule, DOCUMENT } from '@angular/common';
|
|
12
12
|
import * as i2$1 from '@ng-bootstrap/ng-bootstrap';
|
|
13
13
|
import { NgbAlert, NgbModal, NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
|
14
14
|
import { toSignal } from '@angular/core/rxjs-interop';
|
|
@@ -254,6 +254,79 @@ function handle401Error(request, next, http, router, config) {
|
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
class DemoModeService {
|
|
258
|
+
demoModeSubject = new BehaviorSubject({
|
|
259
|
+
isActive: false,
|
|
260
|
+
cleanupIntervalMinutes: 15
|
|
261
|
+
});
|
|
262
|
+
demoMode$ = this.demoModeSubject.asObservable();
|
|
263
|
+
setDemoMode(isActive, cleanupInterval = 15) {
|
|
264
|
+
const currentInfo = this.demoModeSubject.value;
|
|
265
|
+
if (isActive !== currentInfo.isActive) {
|
|
266
|
+
this.demoModeSubject.next({
|
|
267
|
+
isActive,
|
|
268
|
+
cleanupIntervalMinutes: cleanupInterval,
|
|
269
|
+
lastDetected: new Date()
|
|
270
|
+
});
|
|
271
|
+
if (isActive) {
|
|
272
|
+
localStorage.setItem('demo_mode_active', 'true');
|
|
273
|
+
localStorage.setItem('demo_mode_cleanup_interval', cleanupInterval.toString());
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
localStorage.removeItem('demo_mode_active');
|
|
277
|
+
localStorage.removeItem('demo_mode_cleanup_interval');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
isDemoMode() {
|
|
282
|
+
return this.demoModeSubject.value.isActive;
|
|
283
|
+
}
|
|
284
|
+
getDemoModeInfo() {
|
|
285
|
+
return this.demoModeSubject.value;
|
|
286
|
+
}
|
|
287
|
+
checkLocalStorage() {
|
|
288
|
+
const isDemoActive = localStorage.getItem('demo_mode_active') === 'true';
|
|
289
|
+
const cleanupInterval = parseInt(localStorage.getItem('demo_mode_cleanup_interval') || '15', 10);
|
|
290
|
+
if (isDemoActive) {
|
|
291
|
+
this.setDemoMode(true, cleanupInterval);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: DemoModeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
295
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: DemoModeService, providedIn: 'root' });
|
|
296
|
+
}
|
|
297
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: DemoModeService, decorators: [{
|
|
298
|
+
type: Injectable,
|
|
299
|
+
args: [{
|
|
300
|
+
providedIn: 'root'
|
|
301
|
+
}]
|
|
302
|
+
}] });
|
|
303
|
+
|
|
304
|
+
class DemoModeInterceptor {
|
|
305
|
+
demoModeService;
|
|
306
|
+
constructor(demoModeService) {
|
|
307
|
+
this.demoModeService = demoModeService;
|
|
308
|
+
}
|
|
309
|
+
intercept(req, next) {
|
|
310
|
+
return next.handle(req).pipe(tap(event => {
|
|
311
|
+
if (event instanceof HttpResponse) {
|
|
312
|
+
const demoModeHeader = event.headers.get('X-Demo-Mode');
|
|
313
|
+
if (demoModeHeader === 'true') {
|
|
314
|
+
const cleanupInterval = parseInt(event.headers.get('X-Demo-Cleanup-Interval') || '15', 10);
|
|
315
|
+
this.demoModeService.setDemoMode(true, cleanupInterval);
|
|
316
|
+
}
|
|
317
|
+
else if (demoModeHeader === 'false') {
|
|
318
|
+
this.demoModeService.setDemoMode(false);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}));
|
|
322
|
+
}
|
|
323
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: DemoModeInterceptor, deps: [{ token: DemoModeService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
324
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: DemoModeInterceptor });
|
|
325
|
+
}
|
|
326
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: DemoModeInterceptor, decorators: [{
|
|
327
|
+
type: Injectable
|
|
328
|
+
}], ctorParameters: () => [{ type: DemoModeService }] });
|
|
329
|
+
|
|
257
330
|
const CUPCAKE_CORE_CONFIG = new InjectionToken('CUPCAKE_CORE_CONFIG');
|
|
258
331
|
class AuthService {
|
|
259
332
|
http = inject(HttpClient);
|
|
@@ -378,31 +451,31 @@ class AuthService {
|
|
|
378
451
|
handleORCIDCallback(code, state, rememberMe = false) {
|
|
379
452
|
const params = new URLSearchParams({ code, state, remember_me: rememberMe.toString() });
|
|
380
453
|
return this.http.get(`${this.apiUrl}/auth/orcid/callback/?${params}`)
|
|
381
|
-
.pipe(tap(response => this.setAuthData(response)));
|
|
454
|
+
.pipe(tap$1(response => this.setAuthData(response)));
|
|
382
455
|
}
|
|
383
456
|
exchangeORCIDToken(accessToken, orcidId, rememberMe = false) {
|
|
384
457
|
return this.http.post(`${this.apiUrl}/auth/orcid/token/`, {
|
|
385
458
|
access_token: accessToken,
|
|
386
459
|
orcid_id: orcidId,
|
|
387
460
|
remember_me: rememberMe
|
|
388
|
-
}).pipe(tap(response => this.setAuthData(response)));
|
|
461
|
+
}).pipe(tap$1(response => this.setAuthData(response)));
|
|
389
462
|
}
|
|
390
463
|
login(username, password, rememberMe = false) {
|
|
391
464
|
return this.http.post(`${this.apiUrl}/auth/login/`, {
|
|
392
465
|
username,
|
|
393
466
|
password,
|
|
394
467
|
remember_me: rememberMe
|
|
395
|
-
}).pipe(tap(response => this.setAuthData(response)));
|
|
468
|
+
}).pipe(tap$1(response => this.setAuthData(response)));
|
|
396
469
|
}
|
|
397
470
|
logout() {
|
|
398
471
|
const refreshToken = this.getRefreshToken();
|
|
399
472
|
const payload = refreshToken ? { refresh: refreshToken } : {};
|
|
400
473
|
return this.http.post(`${this.apiUrl}/auth/logout/`, payload)
|
|
401
|
-
.pipe(tap(() => this.clearAuthData()));
|
|
474
|
+
.pipe(tap$1(() => this.clearAuthData()));
|
|
402
475
|
}
|
|
403
476
|
checkAuthStatus() {
|
|
404
477
|
return this.http.get(`${this.apiUrl}/auth/status/`)
|
|
405
|
-
.pipe(tap(status => {
|
|
478
|
+
.pipe(tap$1(status => {
|
|
406
479
|
if (status.authenticated && status.user) {
|
|
407
480
|
this.currentUserSubject.next(status.user);
|
|
408
481
|
this.isAuthenticatedSubject.next(true);
|
|
@@ -414,7 +487,7 @@ class AuthService {
|
|
|
414
487
|
}
|
|
415
488
|
fetchUserProfile() {
|
|
416
489
|
return this.http.get(`${this.apiUrl}/auth/profile/`)
|
|
417
|
-
.pipe(map(response => this.convertUserFromSnakeToCamel(response.user)), tap(user => {
|
|
490
|
+
.pipe(map(response => this.convertUserFromSnakeToCamel(response.user)), tap$1(user => {
|
|
418
491
|
this.currentUserSubject.next(user);
|
|
419
492
|
this.isAuthenticatedSubject.next(true);
|
|
420
493
|
}));
|
|
@@ -449,7 +522,7 @@ class AuthService {
|
|
|
449
522
|
}
|
|
450
523
|
return this.http.post(`${this.apiUrl}/auth/token/refresh/`, {
|
|
451
524
|
refresh: refreshToken
|
|
452
|
-
}).pipe(tap((response) => {
|
|
525
|
+
}).pipe(tap$1((response) => {
|
|
453
526
|
localStorage.setItem('ccvAccessToken', response.access);
|
|
454
527
|
this.isAuthenticatedSubject.next(true);
|
|
455
528
|
const user = this.getUserFromToken();
|
|
@@ -474,7 +547,7 @@ class AuthService {
|
|
|
474
547
|
}
|
|
475
548
|
return this.http.post(`${this.apiUrl}/auth/token/refresh/`, {
|
|
476
549
|
refresh: refreshToken
|
|
477
|
-
}).pipe(tap(response => {
|
|
550
|
+
}).pipe(tap$1(response => {
|
|
478
551
|
localStorage.setItem('ccvAccessToken', response.access);
|
|
479
552
|
this.isAuthenticatedSubject.next(true);
|
|
480
553
|
this.fetchUserProfile().subscribe({
|
|
@@ -1353,7 +1426,7 @@ class WebSocketService {
|
|
|
1353
1426
|
this.reconnectAttempts++;
|
|
1354
1427
|
const delay = this.config.reconnectInterval || 5000;
|
|
1355
1428
|
console.log(`WebSocket reconnection attempt ${this.reconnectAttempts} in ${delay}ms`);
|
|
1356
|
-
timer(delay).pipe(takeUntil(this.destroy$), tap
|
|
1429
|
+
timer(delay).pipe(takeUntil(this.destroy$), tap(() => {
|
|
1357
1430
|
if (this.reconnectAttempts <= (this.config.maxReconnectAttempts || 5)) {
|
|
1358
1431
|
this.connect();
|
|
1359
1432
|
}
|
|
@@ -1364,7 +1437,7 @@ class WebSocketService {
|
|
|
1364
1437
|
})).subscribe();
|
|
1365
1438
|
}
|
|
1366
1439
|
filterMessages(type) {
|
|
1367
|
-
return this.messages$.pipe(tap
|
|
1440
|
+
return this.messages$.pipe(tap(msg => console.log('Filtering message:', msg.type, 'looking for:', type)), switchMap$1(message => message.type === type ? [message] : EMPTY));
|
|
1368
1441
|
}
|
|
1369
1442
|
getNotifications() {
|
|
1370
1443
|
return this.filterMessages('notification');
|
|
@@ -1723,6 +1796,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImpor
|
|
|
1723
1796
|
}], ctorParameters: () => [{ type: ToastService }] });
|
|
1724
1797
|
|
|
1725
1798
|
class SiteConfigService extends BaseApiService {
|
|
1799
|
+
demoModeService;
|
|
1726
1800
|
defaultConfig = {
|
|
1727
1801
|
siteName: 'CUPCAKE Vanilla',
|
|
1728
1802
|
showPoweredBy: true,
|
|
@@ -1732,14 +1806,28 @@ class SiteConfigService extends BaseApiService {
|
|
|
1732
1806
|
bookingDeletionWindowMinutes: 30,
|
|
1733
1807
|
whisperCppModel: '/app/whisper.cpp/models/ggml-medium.bin',
|
|
1734
1808
|
uiFeatures: {},
|
|
1809
|
+
uiFeaturesWithDefaults: {
|
|
1810
|
+
show_metadata_tables: true,
|
|
1811
|
+
show_instruments: true,
|
|
1812
|
+
show_sessions: true,
|
|
1813
|
+
show_protocols: true,
|
|
1814
|
+
show_messages: true,
|
|
1815
|
+
show_notifications: true,
|
|
1816
|
+
show_storage: true,
|
|
1817
|
+
show_webrtc: true,
|
|
1818
|
+
show_billing: true
|
|
1819
|
+
},
|
|
1735
1820
|
installedApps: {},
|
|
1821
|
+
maxUploadSize: 104857600,
|
|
1822
|
+
maxChunkedUploadSize: 2147483648,
|
|
1736
1823
|
createdAt: new Date().toISOString(),
|
|
1737
1824
|
updatedAt: new Date().toISOString()
|
|
1738
1825
|
};
|
|
1739
1826
|
configSubject = new BehaviorSubject(this.defaultConfig);
|
|
1740
1827
|
config$ = this.configSubject.asObservable();
|
|
1741
|
-
constructor() {
|
|
1828
|
+
constructor(demoModeService) {
|
|
1742
1829
|
super();
|
|
1830
|
+
this.demoModeService = demoModeService;
|
|
1743
1831
|
this.loadConfig();
|
|
1744
1832
|
this.startPeriodicRefresh();
|
|
1745
1833
|
}
|
|
@@ -1749,6 +1837,7 @@ class SiteConfigService extends BaseApiService {
|
|
|
1749
1837
|
next: (config) => {
|
|
1750
1838
|
this.configSubject.next({ ...this.defaultConfig, ...config });
|
|
1751
1839
|
localStorage.setItem('site_config', JSON.stringify(config));
|
|
1840
|
+
this.handleDemoMode(config);
|
|
1752
1841
|
},
|
|
1753
1842
|
error: () => { }
|
|
1754
1843
|
});
|
|
@@ -1760,6 +1849,7 @@ class SiteConfigService extends BaseApiService {
|
|
|
1760
1849
|
try {
|
|
1761
1850
|
const config = JSON.parse(savedConfig);
|
|
1762
1851
|
this.configSubject.next({ ...this.defaultConfig, ...config });
|
|
1852
|
+
this.handleDemoMode(config);
|
|
1763
1853
|
}
|
|
1764
1854
|
catch (error) {
|
|
1765
1855
|
// Invalid config, use defaults
|
|
@@ -1769,6 +1859,7 @@ class SiteConfigService extends BaseApiService {
|
|
|
1769
1859
|
next: (config) => {
|
|
1770
1860
|
this.configSubject.next({ ...this.defaultConfig, ...config });
|
|
1771
1861
|
localStorage.setItem('site_config', JSON.stringify(config));
|
|
1862
|
+
this.handleDemoMode(config);
|
|
1772
1863
|
},
|
|
1773
1864
|
error: () => {
|
|
1774
1865
|
// Continue with current config
|
|
@@ -1782,13 +1873,14 @@ class SiteConfigService extends BaseApiService {
|
|
|
1782
1873
|
return this.get(`${this.apiUrl}/site-config/public/`);
|
|
1783
1874
|
}
|
|
1784
1875
|
getCurrentConfig() {
|
|
1785
|
-
return this.get(`${this.apiUrl}/site-config/current/`).pipe(tap
|
|
1876
|
+
return this.get(`${this.apiUrl}/site-config/current/`).pipe(tap(config => {
|
|
1786
1877
|
this.configSubject.next({ ...this.defaultConfig, ...config });
|
|
1787
1878
|
localStorage.setItem('site_config', JSON.stringify(config));
|
|
1879
|
+
this.handleDemoMode(config);
|
|
1788
1880
|
}));
|
|
1789
1881
|
}
|
|
1790
1882
|
updateConfig(config) {
|
|
1791
|
-
return this.put(`${this.apiUrl}/site-config/update_config/`, config).pipe(tap
|
|
1883
|
+
return this.put(`${this.apiUrl}/site-config/update_config/`, config).pipe(tap(updatedConfig => {
|
|
1792
1884
|
this.configSubject.next({ ...this.defaultConfig, ...updatedConfig });
|
|
1793
1885
|
localStorage.setItem('site_config', JSON.stringify(updatedConfig));
|
|
1794
1886
|
}));
|
|
@@ -1821,7 +1913,27 @@ class SiteConfigService extends BaseApiService {
|
|
|
1821
1913
|
getWorkerStatus() {
|
|
1822
1914
|
return this.get(`${this.apiUrl}/site-config/worker_status/`);
|
|
1823
1915
|
}
|
|
1824
|
-
|
|
1916
|
+
getMaxUploadSize() {
|
|
1917
|
+
return this.configSubject.value.maxUploadSize || 104857600;
|
|
1918
|
+
}
|
|
1919
|
+
getMaxChunkedUploadSize() {
|
|
1920
|
+
return this.configSubject.value.maxChunkedUploadSize || 2147483648;
|
|
1921
|
+
}
|
|
1922
|
+
getMaxUploadSizeMB() {
|
|
1923
|
+
return Math.floor(this.getMaxUploadSize() / 1048576);
|
|
1924
|
+
}
|
|
1925
|
+
getMaxChunkedUploadSizeMB() {
|
|
1926
|
+
return Math.floor(this.getMaxChunkedUploadSize() / 1048576);
|
|
1927
|
+
}
|
|
1928
|
+
handleDemoMode(config) {
|
|
1929
|
+
if (config.demoMode) {
|
|
1930
|
+
this.demoModeService.setDemoMode(true, config.demoCleanupIntervalMinutes || 15);
|
|
1931
|
+
}
|
|
1932
|
+
else {
|
|
1933
|
+
this.demoModeService.setDemoMode(false);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: SiteConfigService, deps: [{ token: DemoModeService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1825
1937
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: SiteConfigService, providedIn: 'root' });
|
|
1826
1938
|
}
|
|
1827
1939
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: SiteConfigService, decorators: [{
|
|
@@ -1829,7 +1941,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImpor
|
|
|
1829
1941
|
args: [{
|
|
1830
1942
|
providedIn: 'root'
|
|
1831
1943
|
}]
|
|
1832
|
-
}], ctorParameters: () => [] });
|
|
1944
|
+
}], ctorParameters: () => [{ type: DemoModeService }] });
|
|
1833
1945
|
|
|
1834
1946
|
class ThemeService {
|
|
1835
1947
|
THEME_KEY = 'cupcake-theme';
|
|
@@ -3604,6 +3716,83 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImpor
|
|
|
3604
3716
|
args: [{ selector: 'ccc-site-config', standalone: true, imports: [CommonModule, ReactiveFormsModule, NgbAlert], template: "<div class=\"site-config-container\">\r\n <!-- Header Section -->\r\n <nav class=\"navbar navbar-expand-lg bg-body-tertiary shadow-sm\">\r\n <div class=\"container-fluid\">\r\n <div class=\"d-flex align-items-center justify-content-between w-100\">\r\n <div class=\"d-flex align-items-center\">\r\n <h4 class=\"navbar-brand m-0 fw-bold\">\r\n <i class=\"bi bi-gear me-2\"></i>Site Configuration\r\n </h4>\r\n </div>\r\n <div class=\"d-flex gap-2\">\r\n <button \r\n type=\"submit\" \r\n form=\"configForm\"\r\n class=\"btn btn-primary btn-sm\"\r\n [disabled]=\"configForm.invalid || loading()\">\r\n @if (loading()) {\r\n <span class=\"spinner-border spinner-border-sm me-1\" aria-hidden=\"true\"></span>\r\n } @else {\r\n <i class=\"bi bi-check-lg me-1\"></i>\r\n }\r\n Save Configuration\r\n </button>\r\n \r\n <button \r\n type=\"button\" \r\n class=\"btn btn-outline-secondary btn-sm\"\r\n (click)=\"resetForm()\"\r\n [disabled]=\"loading()\">\r\n <i class=\"bi bi-arrow-clockwise me-1\"></i>\r\n Reset\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n </nav>\r\n\r\n <div class=\"config-content p-4\">\r\n <div class=\"row\">\r\n <div class=\"col-lg-8\">\r\n <!-- Site Branding Card -->\r\n <div class=\"card mb-4\">\r\n <div class=\"card-header\">\r\n <h5 class=\"mb-0\">\r\n <i class=\"bi bi-brush me-2\"></i>Site Branding\r\n </h5>\r\n </div>\r\n <div class=\"card-body\">\r\n <!-- Success Alert -->\r\n @if (success()) {\r\n <ngb-alert type=\"success\" (closed)=\"clearSuccess()\" [dismissible]=\"true\">\r\n <i class=\"bi bi-check-circle me-2\"></i>{{ success() }}\r\n </ngb-alert>\r\n }\r\n\r\n <!-- Error Alert -->\r\n @if (error()) {\r\n <ngb-alert type=\"danger\" (closed)=\"clearError()\" [dismissible]=\"true\">\r\n <i class=\"bi bi-exclamation-triangle me-2\"></i>{{ error() }}\r\n </ngb-alert>\r\n }\r\n\r\n <form id=\"configForm\" [formGroup]=\"configForm\" (ngSubmit)=\"onSubmit()\">\r\n <!-- Site Name -->\r\n <div class=\"mb-3\">\r\n <label for=\"site_name\" class=\"form-label\">\r\n <i class=\"bi bi-building me-1\"></i>\r\n Site Name <span class=\"text-danger\">*</span>\r\n </label>\r\n <input \r\n type=\"text\" \r\n class=\"form-control\" \r\n id=\"site_name\"\r\n formControlName=\"siteName\"\r\n [class.is-invalid]=\"configForm.get('siteName')?.invalid && configForm.get('siteName')?.touched\"\r\n placeholder=\"Enter your site name\">\r\n @if (configForm.get('siteName')?.invalid && configForm.get('siteName')?.touched) {\r\n <div class=\"invalid-feedback\">\r\n @if (configForm.get('siteName')?.errors?.['required']) {\r\n <span>Site name is required</span>\r\n }\r\n @if (configForm.get('siteName')?.errors?.['minlength']) {\r\n <span>Site name must be at least 1 character</span>\r\n }\r\n @if (configForm.get('siteName')?.errors?.['maxlength']) {\r\n <span>Site name must be less than 255 characters</span>\r\n }\r\n </div>\r\n }\r\n <div class=\"form-text\">\r\n This will appear in the navigation bar and login page\r\n </div>\r\n </div>\r\n\r\n <!-- Logo URL -->\r\n <div class=\"mb-3\">\r\n <label for=\"logo_url\" class=\"form-label\">\r\n <i class=\"bi bi-image me-1\"></i>\r\n Logo URL\r\n </label>\r\n <input \r\n type=\"url\" \r\n class=\"form-control\" \r\n id=\"logo_url\"\r\n formControlName=\"logoUrl\"\r\n placeholder=\"https://example.com/logo.png\">\r\n <div class=\"form-text\">\r\n Optional: URL to your organization's logo\r\n </div>\r\n </div>\r\n\r\n <!-- Logo File Upload -->\r\n <div class=\"mb-3\">\r\n <label for=\"logo_file\" class=\"form-label\">\r\n <i class=\"bi bi-upload me-1\"></i>\r\n Upload Logo File\r\n </label>\r\n <input \r\n type=\"file\" \r\n class=\"form-control\" \r\n id=\"logo_file\"\r\n accept=\"image/*\"\r\n (change)=\"onLogoFileSelected($event)\">\r\n <div class=\"form-text\">\r\n Upload a logo file (overrides logo URL). Supported: JPEG, PNG, GIF, SVG. Max 5MB.\r\n </div>\r\n @if (selectedLogoFile()) {\r\n <div class=\"mt-2\">\r\n <div class=\"alert alert-info py-2\">\r\n <i class=\"bi bi-file-image me-1\"></i>\r\n Selected: {{ selectedLogoFile()?.name }}\r\n <button type=\"button\" class=\"btn btn-sm btn-outline-danger ms-2\" (click)=\"clearLogoFile()\">\r\n <i class=\"bi bi-x\"></i>\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Primary Color -->\r\n <div class=\"mb-3\">\r\n <label for=\"primary_color\" class=\"form-label\">\r\n <i class=\"bi bi-palette me-1\"></i>\r\n Primary Color\r\n </label>\r\n <div class=\"d-flex gap-2 align-items-center\">\r\n <input\r\n type=\"color\"\r\n id=\"primary_color\"\r\n class=\"form-control form-control-color\"\r\n formControlName=\"primaryColor\"\r\n style=\"width: 60px; height: 40px;\">\r\n <input\r\n type=\"text\"\r\n class=\"form-control\"\r\n formControlName=\"primaryColor\"\r\n placeholder=\"#1976d2\"\r\n [class.is-invalid]=\"configForm.get('primaryColor')?.invalid && configForm.get('primaryColor')?.touched\">\r\n </div>\r\n @if (configForm.get('primaryColor')?.invalid && configForm.get('primaryColor')?.touched) {\r\n <div class=\"invalid-feedback\">\r\n @if (configForm.get('primaryColor')?.errors?.['pattern']) {\r\n <span>Please enter a valid hex color (e.g., #1976d2)</span>\r\n }\r\n </div>\r\n }\r\n <div class=\"form-text\">\r\n Main theme color for navigation and buttons\r\n </div>\r\n </div>\r\n\r\n <!-- Show Powered By -->\r\n <div class=\"mb-4\">\r\n <div class=\"form-check\">\r\n <input \r\n class=\"form-check-input\" \r\n type=\"checkbox\" \r\n id=\"show_powered_by\"\r\n formControlName=\"showPoweredBy\">\r\n <label class=\"form-check-label\" for=\"show_powered_by\">\r\n <i class=\"bi bi-lightning-charge me-1\"></i>\r\n Show \"Powered by CUPCAKE Vanilla\"\r\n </label>\r\n </div>\r\n <div class=\"form-text\">\r\n Display a small footer credit (appears on hover in bottom-right corner)\r\n </div>\r\n </div>\r\n\r\n <!-- Authentication Settings Section -->\r\n <div class=\"mb-4\">\r\n <h6 class=\"text-muted border-bottom pb-2\">\r\n <i class=\"bi bi-shield-lock me-2\"></i>Authentication Settings\r\n </h6>\r\n \r\n <!-- Allow User Registration -->\r\n <div class=\"mb-3\">\r\n <div class=\"form-check\">\r\n <input \r\n class=\"form-check-input\" \r\n type=\"checkbox\" \r\n id=\"allow_user_registration\"\r\n formControlName=\"allowUserRegistration\">\r\n <label class=\"form-check-label\" for=\"allow_user_registration\">\r\n <i class=\"bi bi-person-plus me-1\"></i>\r\n Allow Public User Registration\r\n </label>\r\n </div>\r\n <div class=\"form-text\">\r\n When enabled, users can register new accounts without admin approval. \r\n The registration API endpoint will be accessible to the public.\r\n </div>\r\n </div>\r\n\r\n <!-- Enable ORCID Login -->\r\n <div class=\"mb-3\">\r\n <div class=\"form-check\">\r\n <input\r\n class=\"form-check-input\"\r\n type=\"checkbox\"\r\n id=\"enable_orcid_login\"\r\n formControlName=\"enableOrcidLogin\">\r\n <label class=\"form-check-label\" for=\"enable_orcid_login\">\r\n <i class=\"bi bi-person-badge me-1\"></i>\r\n Enable ORCID OAuth Login\r\n </label>\r\n </div>\r\n <div class=\"form-text\">\r\n Allow users to log in using their ORCID account.\r\n Requires ORCID OAuth configuration in server settings.\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- Transcription Configuration Section -->\r\n <div class=\"mb-4\">\r\n <h6 class=\"text-muted border-bottom pb-2\">\r\n <i class=\"bi bi-mic me-2\"></i>Audio/Video Transcription\r\n </h6>\r\n\r\n <!-- Whisper Model Selection -->\r\n <div class=\"mb-3\">\r\n <label for=\"whisper_model\" class=\"form-label\">\r\n <i class=\"bi bi-cpu me-1\"></i>\r\n Default Whisper.cpp Model\r\n </label>\r\n <div class=\"input-group\">\r\n <select\r\n class=\"form-select\"\r\n id=\"whisper_model\"\r\n formControlName=\"whisperCppModel\">\r\n @if (loadingModels()) {\r\n <option disabled selected>Loading available models...</option>\r\n } @else {\r\n @for (model of availableModels(); track model.path) {\r\n <option [value]=\"model.path\">\r\n {{ model.name }} ({{ model.size }}) - {{ model.description }}\r\n </option>\r\n }\r\n @if (availableModels().length === 0) {\r\n <option disabled selected>No models found. Click refresh to scan.</option>\r\n }\r\n }\r\n </select>\r\n <button\r\n type=\"button\"\r\n class=\"btn btn-outline-secondary\"\r\n (click)=\"refreshAvailableModels()\"\r\n [disabled]=\"refreshingModels() || loadingModels()\">\r\n @if (refreshingModels()) {\r\n <span class=\"spinner-border spinner-border-sm me-1\" aria-hidden=\"true\"></span>\r\n } @else {\r\n <i class=\"bi bi-arrow-clockwise me-1\"></i>\r\n }\r\n Scan Models\r\n </button>\r\n </div>\r\n <div class=\"form-text\">\r\n Model used for audio/video transcription. Larger models are more accurate but slower.\r\n The transcribe worker scans its filesystem to detect available models.\r\n </div>\r\n @if (availableModels().length > 0) {\r\n <div class=\"mt-2\">\r\n <small class=\"text-muted\">\r\n <i class=\"bi bi-info-circle me-1\"></i>\r\n Found {{ availableModels().length }} model(s). Last scanned by transcribe worker on startup.\r\n </small>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n\r\n </form>\r\n\r\n <!-- Worker Status Section (Superuser Only) -->\r\n @if (isSuperuser()) {\r\n <div class=\"card mb-4\">\r\n <div class=\"card-header d-flex justify-content-between align-items-center\">\r\n <h6 class=\"mb-0\">\r\n <i class=\"bi bi-hdd-network me-2\"></i>Worker Status\r\n </h6>\r\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\"\r\n (click)=\"loadWorkerStatus()\"\r\n [disabled]=\"loadingWorkerStatus()\">\r\n @if (loadingWorkerStatus()) {\r\n <span class=\"spinner-border spinner-border-sm me-1\"></span>\r\n } @else {\r\n <i class=\"bi bi-arrow-clockwise me-1\"></i>\r\n }\r\n Refresh\r\n </button>\r\n </div>\r\n <div class=\"card-body\">\r\n @if (loadingWorkerStatus()) {\r\n <div class=\"text-center py-3\">\r\n <div class=\"spinner-border text-primary\" role=\"status\">\r\n <span class=\"visually-hidden\">Loading...</span>\r\n </div>\r\n </div>\r\n } @else if (workerStatus()) {\r\n <!-- Workers -->\r\n <div class=\"mb-3\">\r\n <h6 class=\"text-muted mb-2\">\r\n <i class=\"bi bi-server me-1\"></i>\r\n Workers ({{ workerStatus().workerCount }})\r\n </h6>\r\n @if (workerStatus().workers.length === 0) {\r\n <div class=\"alert alert-warning mb-0\">\r\n <i class=\"bi bi-exclamation-triangle me-2\"></i>\r\n No workers found!\r\n </div>\r\n } @else {\r\n @for (worker of workerStatus().workers; track worker.name) {\r\n <div class=\"card mb-2\">\r\n <div class=\"card-body p-2\">\r\n <div class=\"d-flex justify-content-between align-items-start\">\r\n <div class=\"flex-grow-1\">\r\n <div class=\"d-flex align-items-center mb-1\">\r\n @if (worker.state === 'idle') {\r\n <span class=\"badge bg-success me-2\">Idle</span>\r\n } @else if (worker.state === 'busy') {\r\n <span class=\"badge bg-warning me-2\">Busy</span>\r\n } @else {\r\n <span class=\"badge bg-danger me-2\">{{ worker.state }}</span>\r\n }\r\n <small class=\"text-muted\">{{ worker.hostname }} (PID: {{ worker.pid }})</small>\r\n </div>\r\n <div class=\"small text-muted\">\r\n <i class=\"bi bi-list-ul me-1\"></i>\r\n Queues: {{ worker.queues.join(', ') }}\r\n </div>\r\n <div class=\"small text-muted\">\r\n <i class=\"bi bi-check-circle me-1\"></i>\r\n {{ worker.successfulJobCount }} successful, {{ worker.failedJobCount }} failed\r\n </div>\r\n @if (worker.currentJob) {\r\n <div class=\"small mt-1\">\r\n <i class=\"bi bi-hourglass-split me-1\"></i>\r\n <strong>Current:</strong> {{ worker.currentJob.funcName }}\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n }\r\n </div>\r\n\r\n <!-- Queues -->\r\n <div>\r\n <h6 class=\"text-muted mb-2\">\r\n <i class=\"bi bi-list-task me-1\"></i>\r\n Queue Statistics\r\n </h6>\r\n <div class=\"table-responsive\">\r\n <table class=\"table table-sm mb-0\">\r\n <thead>\r\n <tr>\r\n <th>Queue</th>\r\n <th>Queued</th>\r\n <th>Started</th>\r\n <th>Failed</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (queue of Object.keys(workerStatus().queues); track queue) {\r\n <tr>\r\n <td>\r\n <span class=\"badge bg-secondary\">{{ queue }}</span>\r\n </td>\r\n <td>{{ workerStatus().queues[queue].count }}</td>\r\n <td>{{ workerStatus().queues[queue].startedCount }}</td>\r\n <td>\r\n @if (workerStatus().queues[queue].failedCount > 0) {\r\n <span class=\"text-danger\">{{ workerStatus().queues[queue].failedCount }}</span>\r\n } @else {\r\n {{ workerStatus().queues[queue].failedCount }}\r\n }\r\n </td>\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n </div>\r\n </div>\r\n } @else {\r\n <div class=\"text-center py-3\">\r\n <p class=\"text-muted mb-2\">\r\n <i class=\"bi bi-info-circle me-1\"></i>\r\n Click refresh to load worker status\r\n </p>\r\n <small class=\"text-muted\">Only available to superusers</small>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n\r\n </div>\r\n </div>\r\n\r\n </div>\r\n\r\n <div class=\"col-lg-4\">\r\n <!-- Preview Panel -->\r\n <div class=\"card mb-4\">\r\n <div class=\"card-header\">\r\n <h5 class=\"mb-0\">\r\n <i class=\"bi bi-eye me-2\"></i>Preview\r\n </h5>\r\n </div>\r\n <div class=\"card-body\">\r\n <div class=\"preview-navbar mb-3\" \r\n [style.background]=\"'linear-gradient(135deg, ' + (configForm.get('primaryColor')?.value || '#1976d2') + ' 0%, ' + getDarkerColor(configForm.get('primaryColor')?.value || '#1976d2') + ' 100%)'\">\r\n <div class=\"preview-nav\">\r\n <span class=\"preview-brand\">\r\n @if (configForm.get('logoUrl')?.value) {\r\n <img [src]=\"configForm.get('logoUrl')?.value\" alt=\"Logo\" style=\"height: 20px; width: auto;\" class=\"me-2\">\r\n } @else {\r\n <i class=\"bi bi-flask me-2\"></i>\r\n }\r\n {{ configForm.get('siteName')?.value || 'Your Site Name' }}\r\n </span>\r\n </div>\r\n </div>\r\n \r\n <div class=\"preview-login-card\">\r\n <h6 class=\"text-center mb-3\">\r\n @if (configForm.get('logoUrl')?.value) {\r\n <img [src]=\"configForm.get('logoUrl')?.value\" alt=\"Logo\" style=\"height: 24px; width: auto;\" class=\"me-2\">\r\n } @else {\r\n <i class=\"bi bi-flask me-2\"></i>\r\n }\r\n {{ configForm.get('siteName')?.value || 'Your Site Name' }}\r\n </h6>\r\n @if (configForm.get('enableOrcidLogin')?.value) {\r\n <div class=\"preview-button mb-2\"\r\n [style.background]=\"'linear-gradient(135deg, ' + (configForm.get('primaryColor')?.value || '#1976d2') + ' 0%, ' + getDarkerColor(configForm.get('primaryColor')?.value || '#1976d2') + ' 100%)'\">\r\n <i class=\"bi bi-person-badge me-2\"></i>\r\n Login with ORCID\r\n </div>\r\n }\r\n <div class=\"preview-button mb-2\"\r\n [style.background]=\"'linear-gradient(135deg, ' + (configForm.get('primaryColor')?.value || '#1976d2') + ' 0%, ' + getDarkerColor(configForm.get('primaryColor')?.value || '#1976d2') + ' 100%)'\">\r\n <i class=\"bi bi-box-arrow-in-right me-2\"></i>\r\n Regular Login\r\n </div>\r\n @if (configForm.get('allowUserRegistration')?.value) {\r\n <div class=\"text-center mb-2\">\r\n <small class=\"text-muted\">\r\n <i class=\"bi bi-person-plus me-1\"></i>\r\n Don't have an account? <a href=\"#\" class=\"text-decoration-none\">Register here</a>\r\n </small>\r\n </div>\r\n }\r\n <div class=\"text-center\">\r\n <small class=\"text-muted\">\r\n {{ configForm.get('siteName')?.value || 'Your Site Name' }} - Scientific Metadata Management\r\n </small>\r\n </div>\r\n </div>\r\n\r\n <div class=\"mt-3\">\r\n <h6 class=\"text-muted\">Authentication Status:</h6>\r\n <ul class=\"list-unstyled small text-muted mb-0\">\r\n <li>\r\n <i class=\"bi bi-check-circle-fill text-success me-1\"></i>\r\n Regular login: Always enabled\r\n </li>\r\n <li>\r\n @if (configForm.get('enableOrcidLogin')?.value) {\r\n <i class=\"bi bi-check-circle-fill text-success me-1\"></i>\r\n ORCID login: Enabled\r\n } @else {\r\n <i class=\"bi bi-x-circle-fill text-danger me-1\"></i>\r\n ORCID login: Disabled\r\n }\r\n </li>\r\n <li>\r\n @if (configForm.get('allowUserRegistration')?.value) {\r\n <i class=\"bi bi-check-circle-fill text-success me-1\"></i>\r\n Public registration: Enabled\r\n } @else {\r\n <i class=\"bi bi-x-circle-fill text-danger me-1\"></i>\r\n Public registration: Disabled\r\n }\r\n </li>\r\n </ul>\r\n </div>\r\n\r\n @if (configForm.get('showPoweredBy')?.value) {\r\n <div class=\"mt-3\">\r\n <small class=\"text-muted\">\r\n <i class=\"bi bi-lightning-charge me-1\"></i>\r\n Footer: \"Powered by CUPCAKE Vanilla\" will appear on hover\r\n </small>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n</div>" }]
|
|
3605
3717
|
}], ctorParameters: () => [] });
|
|
3606
3718
|
|
|
3719
|
+
class DemoModeBannerComponent {
|
|
3720
|
+
demoModeService;
|
|
3721
|
+
document;
|
|
3722
|
+
destroy$ = new Subject();
|
|
3723
|
+
isDemoMode = false;
|
|
3724
|
+
demoModeInfo = null;
|
|
3725
|
+
isCollapsed = false;
|
|
3726
|
+
isDismissed = false;
|
|
3727
|
+
constructor(demoModeService, document) {
|
|
3728
|
+
this.demoModeService = demoModeService;
|
|
3729
|
+
this.document = document;
|
|
3730
|
+
}
|
|
3731
|
+
ngOnInit() {
|
|
3732
|
+
this.demoModeService.demoMode$
|
|
3733
|
+
.pipe(takeUntil(this.destroy$))
|
|
3734
|
+
.subscribe(info => {
|
|
3735
|
+
this.isDemoMode = info.isActive;
|
|
3736
|
+
this.demoModeInfo = info;
|
|
3737
|
+
const dismissed = localStorage.getItem('demo_banner_dismissed');
|
|
3738
|
+
this.isDismissed = dismissed === 'true';
|
|
3739
|
+
this.updateBodyClasses();
|
|
3740
|
+
});
|
|
3741
|
+
const collapsed = localStorage.getItem('demo_banner_collapsed');
|
|
3742
|
+
if (collapsed === 'true') {
|
|
3743
|
+
this.isCollapsed = true;
|
|
3744
|
+
}
|
|
3745
|
+
}
|
|
3746
|
+
ngOnDestroy() {
|
|
3747
|
+
this.document.body.classList.remove('demo-mode-active');
|
|
3748
|
+
this.document.body.classList.remove('demo-mode-collapsed');
|
|
3749
|
+
this.destroy$.next();
|
|
3750
|
+
this.destroy$.complete();
|
|
3751
|
+
}
|
|
3752
|
+
toggleCollapse() {
|
|
3753
|
+
this.isCollapsed = !this.isCollapsed;
|
|
3754
|
+
localStorage.setItem('demo_banner_collapsed', this.isCollapsed.toString());
|
|
3755
|
+
this.updateBodyClasses();
|
|
3756
|
+
}
|
|
3757
|
+
dismissBanner() {
|
|
3758
|
+
this.isDismissed = true;
|
|
3759
|
+
localStorage.setItem('demo_banner_dismissed', 'true');
|
|
3760
|
+
this.updateBodyClasses();
|
|
3761
|
+
}
|
|
3762
|
+
updateBodyClasses() {
|
|
3763
|
+
if (this.isDemoMode && !this.isDismissed) {
|
|
3764
|
+
this.document.body.classList.add('demo-mode-active');
|
|
3765
|
+
if (this.isCollapsed) {
|
|
3766
|
+
this.document.body.classList.add('demo-mode-collapsed');
|
|
3767
|
+
}
|
|
3768
|
+
else {
|
|
3769
|
+
this.document.body.classList.remove('demo-mode-collapsed');
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
else {
|
|
3773
|
+
this.document.body.classList.remove('demo-mode-active');
|
|
3774
|
+
this.document.body.classList.remove('demo-mode-collapsed');
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3777
|
+
getMinutesRemaining() {
|
|
3778
|
+
if (!this.demoModeInfo?.lastDetected) {
|
|
3779
|
+
return this.demoModeInfo?.cleanupIntervalMinutes || 15;
|
|
3780
|
+
}
|
|
3781
|
+
const elapsed = (Date.now() - this.demoModeInfo.lastDetected.getTime()) / 1000 / 60;
|
|
3782
|
+
const remaining = this.demoModeInfo.cleanupIntervalMinutes - elapsed;
|
|
3783
|
+
return Math.max(0, Math.floor(remaining));
|
|
3784
|
+
}
|
|
3785
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: DemoModeBannerComponent, deps: [{ token: DemoModeService }, { token: DOCUMENT }], target: i0.ɵɵFactoryTarget.Component });
|
|
3786
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.7", type: DemoModeBannerComponent, isStandalone: true, selector: "cupcake-demo-mode-banner", ngImport: i0, template: "<div class=\"demo-mode-banner\" *ngIf=\"isDemoMode && !isDismissed\" [class.collapsed]=\"isCollapsed\">\r\n <div class=\"banner-content\">\r\n <div class=\"banner-header\">\r\n <div class=\"banner-icon\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n <circle cx=\"12\" cy=\"12\" r=\"10\"></circle>\r\n <line x1=\"12\" y1=\"8\" x2=\"12\" y2=\"12\"></line>\r\n <line x1=\"12\" y1=\"16\" x2=\"12.01\" y2=\"16\"></line>\r\n </svg>\r\n </div>\r\n <div class=\"banner-title\">\r\n <strong>Demo Mode Active</strong>\r\n </div>\r\n <div class=\"banner-actions\">\r\n <button class=\"collapse-btn\" (click)=\"toggleCollapse()\" type=\"button\" title=\"Collapse banner\">\r\n <svg *ngIf=\"!isCollapsed\" 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\">\r\n <polyline points=\"6 9 12 15 18 9\"></polyline>\r\n </svg>\r\n <svg *ngIf=\"isCollapsed\" 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\">\r\n <polyline points=\"18 15 12 9 6 15\"></polyline>\r\n </svg>\r\n </button>\r\n <button class=\"dismiss-btn\" (click)=\"dismissBanner()\" type=\"button\" title=\"Dismiss banner\">\r\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\">\r\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"></line>\r\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"></line>\r\n </svg>\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <div class=\"banner-details\" *ngIf=\"!isCollapsed\">\r\n <p class=\"banner-message\">\r\n This is a demonstration environment. All data will be automatically reset every\r\n <strong>{{ demoModeInfo?.cleanupIntervalMinutes }} minutes</strong>.\r\n </p>\r\n <div class=\"banner-info\">\r\n <div class=\"info-item\">\r\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\">\r\n <circle cx=\"12\" cy=\"12\" r=\"10\"></circle>\r\n <polyline points=\"12 6 12 12 16 14\"></polyline>\r\n </svg>\r\n Next reset: ~{{ getMinutesRemaining() }} minutes\r\n </div>\r\n <div class=\"info-item\">\r\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\">\r\n <path d=\"M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9\"></path>\r\n <path d=\"M13.73 21a2 2 0 0 1-3.46 0\"></path>\r\n </svg>\r\n Transcription features are disabled\r\n </div>\r\n <div class=\"info-item\">\r\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\">\r\n <rect x=\"3\" y=\"11\" width=\"18\" height=\"11\" rx=\"2\" ry=\"2\"></rect>\r\n <path d=\"M7 11V7a5 5 0 0 1 10 0v4\"></path>\r\n </svg>\r\n Read-only for non-demo users\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n</div>\r\n", styles: [".demo-mode-banner{position:fixed;top:0;left:0;right:0;background:linear-gradient(135deg,#ff6b35,#f7931e);color:#fff;box-shadow:0 2px 8px #0003;z-index:9999;transition:all .3s ease}.demo-mode-banner.collapsed .banner-details{display:none}.demo-mode-banner .banner-content{max-width:1200px;margin:0 auto;padding:12px 24px}.demo-mode-banner .banner-header{display:flex;align-items:center;gap:12px}.demo-mode-banner .banner-header .banner-icon{display:flex;align-items:center;justify-content:center;width:32px;height:32px;background:#fff3;border-radius:50%}.demo-mode-banner .banner-header .banner-icon svg{width:20px;height:20px}.demo-mode-banner .banner-header .banner-title{flex:1;font-size:16px;line-height:1.5}.demo-mode-banner .banner-header .banner-title strong{font-weight:600}.demo-mode-banner .banner-header .banner-actions{display:flex;align-items:center;gap:4px}.demo-mode-banner .banner-header .collapse-btn,.demo-mode-banner .banner-header .dismiss-btn{background:transparent;border:none;color:#fff;cursor:pointer;padding:4px;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:background-color .2s ease}.demo-mode-banner .banner-header .collapse-btn:hover,.demo-mode-banner .banner-header .dismiss-btn:hover{background:#ffffff1a}.demo-mode-banner .banner-header .collapse-btn:focus,.demo-mode-banner .banner-header .dismiss-btn:focus{outline:2px solid rgba(255,255,255,.5);outline-offset:2px}.demo-mode-banner .banner-header .dismiss-btn:hover{background:#fff3}.demo-mode-banner .banner-details{margin-top:12px;padding-top:12px;border-top:1px solid rgba(255,255,255,.2)}.demo-mode-banner .banner-details .banner-message{margin:0 0 12px;font-size:14px;line-height:1.6}.demo-mode-banner .banner-details .banner-message strong{font-weight:600;text-decoration:underline}.demo-mode-banner .banner-details .banner-info{display:flex;flex-wrap:wrap;gap:16px;font-size:13px}.demo-mode-banner .banner-details .banner-info .info-item{display:flex;align-items:center;gap:6px;background:#ffffff1a;padding:6px 12px;border-radius:4px}.demo-mode-banner .banner-details .banner-info .info-item svg{flex-shrink:0}@media (max-width: 768px){.demo-mode-banner .banner-content{padding:10px 16px}.demo-mode-banner .banner-header .banner-title{font-size:14px}.demo-mode-banner .banner-details .banner-info{flex-direction:column;gap:8px}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
3787
|
+
}
|
|
3788
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: DemoModeBannerComponent, decorators: [{
|
|
3789
|
+
type: Component,
|
|
3790
|
+
args: [{ selector: 'cupcake-demo-mode-banner', standalone: true, imports: [CommonModule], template: "<div class=\"demo-mode-banner\" *ngIf=\"isDemoMode && !isDismissed\" [class.collapsed]=\"isCollapsed\">\r\n <div class=\"banner-content\">\r\n <div class=\"banner-header\">\r\n <div class=\"banner-icon\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\r\n <circle cx=\"12\" cy=\"12\" r=\"10\"></circle>\r\n <line x1=\"12\" y1=\"8\" x2=\"12\" y2=\"12\"></line>\r\n <line x1=\"12\" y1=\"16\" x2=\"12.01\" y2=\"16\"></line>\r\n </svg>\r\n </div>\r\n <div class=\"banner-title\">\r\n <strong>Demo Mode Active</strong>\r\n </div>\r\n <div class=\"banner-actions\">\r\n <button class=\"collapse-btn\" (click)=\"toggleCollapse()\" type=\"button\" title=\"Collapse banner\">\r\n <svg *ngIf=\"!isCollapsed\" 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\">\r\n <polyline points=\"6 9 12 15 18 9\"></polyline>\r\n </svg>\r\n <svg *ngIf=\"isCollapsed\" 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\">\r\n <polyline points=\"18 15 12 9 6 15\"></polyline>\r\n </svg>\r\n </button>\r\n <button class=\"dismiss-btn\" (click)=\"dismissBanner()\" type=\"button\" title=\"Dismiss banner\">\r\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\">\r\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"></line>\r\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"></line>\r\n </svg>\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <div class=\"banner-details\" *ngIf=\"!isCollapsed\">\r\n <p class=\"banner-message\">\r\n This is a demonstration environment. All data will be automatically reset every\r\n <strong>{{ demoModeInfo?.cleanupIntervalMinutes }} minutes</strong>.\r\n </p>\r\n <div class=\"banner-info\">\r\n <div class=\"info-item\">\r\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\">\r\n <circle cx=\"12\" cy=\"12\" r=\"10\"></circle>\r\n <polyline points=\"12 6 12 12 16 14\"></polyline>\r\n </svg>\r\n Next reset: ~{{ getMinutesRemaining() }} minutes\r\n </div>\r\n <div class=\"info-item\">\r\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\">\r\n <path d=\"M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9\"></path>\r\n <path d=\"M13.73 21a2 2 0 0 1-3.46 0\"></path>\r\n </svg>\r\n Transcription features are disabled\r\n </div>\r\n <div class=\"info-item\">\r\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\">\r\n <rect x=\"3\" y=\"11\" width=\"18\" height=\"11\" rx=\"2\" ry=\"2\"></rect>\r\n <path d=\"M7 11V7a5 5 0 0 1 10 0v4\"></path>\r\n </svg>\r\n Read-only for non-demo users\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n</div>\r\n", styles: [".demo-mode-banner{position:fixed;top:0;left:0;right:0;background:linear-gradient(135deg,#ff6b35,#f7931e);color:#fff;box-shadow:0 2px 8px #0003;z-index:9999;transition:all .3s ease}.demo-mode-banner.collapsed .banner-details{display:none}.demo-mode-banner .banner-content{max-width:1200px;margin:0 auto;padding:12px 24px}.demo-mode-banner .banner-header{display:flex;align-items:center;gap:12px}.demo-mode-banner .banner-header .banner-icon{display:flex;align-items:center;justify-content:center;width:32px;height:32px;background:#fff3;border-radius:50%}.demo-mode-banner .banner-header .banner-icon svg{width:20px;height:20px}.demo-mode-banner .banner-header .banner-title{flex:1;font-size:16px;line-height:1.5}.demo-mode-banner .banner-header .banner-title strong{font-weight:600}.demo-mode-banner .banner-header .banner-actions{display:flex;align-items:center;gap:4px}.demo-mode-banner .banner-header .collapse-btn,.demo-mode-banner .banner-header .dismiss-btn{background:transparent;border:none;color:#fff;cursor:pointer;padding:4px;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:background-color .2s ease}.demo-mode-banner .banner-header .collapse-btn:hover,.demo-mode-banner .banner-header .dismiss-btn:hover{background:#ffffff1a}.demo-mode-banner .banner-header .collapse-btn:focus,.demo-mode-banner .banner-header .dismiss-btn:focus{outline:2px solid rgba(255,255,255,.5);outline-offset:2px}.demo-mode-banner .banner-header .dismiss-btn:hover{background:#fff3}.demo-mode-banner .banner-details{margin-top:12px;padding-top:12px;border-top:1px solid rgba(255,255,255,.2)}.demo-mode-banner .banner-details .banner-message{margin:0 0 12px;font-size:14px;line-height:1.6}.demo-mode-banner .banner-details .banner-message strong{font-weight:600;text-decoration:underline}.demo-mode-banner .banner-details .banner-info{display:flex;flex-wrap:wrap;gap:16px;font-size:13px}.demo-mode-banner .banner-details .banner-info .info-item{display:flex;align-items:center;gap:6px;background:#ffffff1a;padding:6px 12px;border-radius:4px}.demo-mode-banner .banner-details .banner-info .info-item svg{flex-shrink:0}@media (max-width: 768px){.demo-mode-banner .banner-content{padding:10px 16px}.demo-mode-banner .banner-header .banner-title{font-size:14px}.demo-mode-banner .banner-details .banner-info{flex-direction:column;gap:8px}}\n"] }]
|
|
3791
|
+
}], ctorParameters: () => [{ type: DemoModeService }, { type: Document, decorators: [{
|
|
3792
|
+
type: Inject,
|
|
3793
|
+
args: [DOCUMENT]
|
|
3794
|
+
}] }] });
|
|
3795
|
+
|
|
3607
3796
|
class ToastContainerComponent {
|
|
3608
3797
|
toastService = inject(ToastService);
|
|
3609
3798
|
cdr = inject(ChangeDetectorRef);
|
|
@@ -3732,5 +3921,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImpor
|
|
|
3732
3921
|
* Generated bundle index. Do not edit.
|
|
3733
3922
|
*/
|
|
3734
3923
|
|
|
3735
|
-
export { AdminWebSocketService, AnnotationType, ApiService, AsyncTaskMonitorService, AuthService, BaseApiService, CUPCAKE_CORE_CONFIG, CupcakeCoreModule, InvitationStatus, InvitationStatusLabels, LabGroupService, LabGroupsComponent, LoginComponent, NotificationService, PoweredByFooterComponent, RegisterComponent, ResourceRole, ResourceRoleLabels, ResourceService, ResourceType, ResourceTypeLabels, ResourceVisibility, ResourceVisibilityLabels, SiteConfigComponent, SiteConfigService, TASK_STATUS_COLORS, TASK_STATUS_LABELS, TASK_TYPE_LABELS, TaskStatus, TaskType, ThemeService, ToastContainerComponent, ToastService, UserManagementComponent, UserManagementService, UserProfileComponent, WEBSOCKET_ENDPOINT, WEBSOCKET_ENDPOINTS, WebSocketConfigService, WebSocketEndpoints, WebSocketService, adminGuard, authGuard, authInterceptor, resetRefreshState };
|
|
3924
|
+
export { AdminWebSocketService, AnnotationType, ApiService, AsyncTaskMonitorService, AuthService, BaseApiService, CUPCAKE_CORE_CONFIG, CupcakeCoreModule, DemoModeBannerComponent, DemoModeInterceptor, DemoModeService, InvitationStatus, InvitationStatusLabels, LabGroupService, LabGroupsComponent, LoginComponent, NotificationService, PoweredByFooterComponent, RegisterComponent, ResourceRole, ResourceRoleLabels, ResourceService, ResourceType, ResourceTypeLabels, ResourceVisibility, ResourceVisibilityLabels, SiteConfigComponent, SiteConfigService, TASK_STATUS_COLORS, TASK_STATUS_LABELS, TASK_TYPE_LABELS, TaskStatus, TaskType, ThemeService, ToastContainerComponent, ToastService, UserManagementComponent, UserManagementService, UserProfileComponent, WEBSOCKET_ENDPOINT, WEBSOCKET_ENDPOINTS, WebSocketConfigService, WebSocketEndpoints, WebSocketService, adminGuard, authGuard, authInterceptor, resetRefreshState };
|
|
3736
3925
|
//# sourceMappingURL=noatgnu-cupcake-core.mjs.map
|