@noatgnu/cupcake-core 1.3.10 → 1.3.11
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 +163 -20
- package/fesm2022/noatgnu-cupcake-core.mjs.map +1 -1
- package/index.d.ts +49 -5
- package/package.json +1 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject,
|
|
2
|
+
import { inject, Injectable, InjectionToken, signal, computed, Component, effect, 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';
|
|
@@ -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,26 @@ 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: {},
|
|
1736
1821
|
createdAt: new Date().toISOString(),
|
|
1737
1822
|
updatedAt: new Date().toISOString()
|
|
1738
1823
|
};
|
|
1739
1824
|
configSubject = new BehaviorSubject(this.defaultConfig);
|
|
1740
1825
|
config$ = this.configSubject.asObservable();
|
|
1741
|
-
constructor() {
|
|
1826
|
+
constructor(demoModeService) {
|
|
1742
1827
|
super();
|
|
1828
|
+
this.demoModeService = demoModeService;
|
|
1743
1829
|
this.loadConfig();
|
|
1744
1830
|
this.startPeriodicRefresh();
|
|
1745
1831
|
}
|
|
@@ -1749,6 +1835,7 @@ class SiteConfigService extends BaseApiService {
|
|
|
1749
1835
|
next: (config) => {
|
|
1750
1836
|
this.configSubject.next({ ...this.defaultConfig, ...config });
|
|
1751
1837
|
localStorage.setItem('site_config', JSON.stringify(config));
|
|
1838
|
+
this.handleDemoMode(config);
|
|
1752
1839
|
},
|
|
1753
1840
|
error: () => { }
|
|
1754
1841
|
});
|
|
@@ -1760,6 +1847,7 @@ class SiteConfigService extends BaseApiService {
|
|
|
1760
1847
|
try {
|
|
1761
1848
|
const config = JSON.parse(savedConfig);
|
|
1762
1849
|
this.configSubject.next({ ...this.defaultConfig, ...config });
|
|
1850
|
+
this.handleDemoMode(config);
|
|
1763
1851
|
}
|
|
1764
1852
|
catch (error) {
|
|
1765
1853
|
// Invalid config, use defaults
|
|
@@ -1769,6 +1857,7 @@ class SiteConfigService extends BaseApiService {
|
|
|
1769
1857
|
next: (config) => {
|
|
1770
1858
|
this.configSubject.next({ ...this.defaultConfig, ...config });
|
|
1771
1859
|
localStorage.setItem('site_config', JSON.stringify(config));
|
|
1860
|
+
this.handleDemoMode(config);
|
|
1772
1861
|
},
|
|
1773
1862
|
error: () => {
|
|
1774
1863
|
// Continue with current config
|
|
@@ -1782,13 +1871,14 @@ class SiteConfigService extends BaseApiService {
|
|
|
1782
1871
|
return this.get(`${this.apiUrl}/site-config/public/`);
|
|
1783
1872
|
}
|
|
1784
1873
|
getCurrentConfig() {
|
|
1785
|
-
return this.get(`${this.apiUrl}/site-config/current/`).pipe(tap
|
|
1874
|
+
return this.get(`${this.apiUrl}/site-config/current/`).pipe(tap(config => {
|
|
1786
1875
|
this.configSubject.next({ ...this.defaultConfig, ...config });
|
|
1787
1876
|
localStorage.setItem('site_config', JSON.stringify(config));
|
|
1877
|
+
this.handleDemoMode(config);
|
|
1788
1878
|
}));
|
|
1789
1879
|
}
|
|
1790
1880
|
updateConfig(config) {
|
|
1791
|
-
return this.put(`${this.apiUrl}/site-config/update_config/`, config).pipe(tap
|
|
1881
|
+
return this.put(`${this.apiUrl}/site-config/update_config/`, config).pipe(tap(updatedConfig => {
|
|
1792
1882
|
this.configSubject.next({ ...this.defaultConfig, ...updatedConfig });
|
|
1793
1883
|
localStorage.setItem('site_config', JSON.stringify(updatedConfig));
|
|
1794
1884
|
}));
|
|
@@ -1821,7 +1911,15 @@ class SiteConfigService extends BaseApiService {
|
|
|
1821
1911
|
getWorkerStatus() {
|
|
1822
1912
|
return this.get(`${this.apiUrl}/site-config/worker_status/`);
|
|
1823
1913
|
}
|
|
1824
|
-
|
|
1914
|
+
handleDemoMode(config) {
|
|
1915
|
+
if (config.demoMode) {
|
|
1916
|
+
this.demoModeService.setDemoMode(true, config.demoCleanupIntervalMinutes || 15);
|
|
1917
|
+
}
|
|
1918
|
+
else {
|
|
1919
|
+
this.demoModeService.setDemoMode(false);
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: SiteConfigService, deps: [{ token: DemoModeService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1825
1923
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: SiteConfigService, providedIn: 'root' });
|
|
1826
1924
|
}
|
|
1827
1925
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: SiteConfigService, decorators: [{
|
|
@@ -1829,7 +1927,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImpor
|
|
|
1829
1927
|
args: [{
|
|
1830
1928
|
providedIn: 'root'
|
|
1831
1929
|
}]
|
|
1832
|
-
}], ctorParameters: () => [] });
|
|
1930
|
+
}], ctorParameters: () => [{ type: DemoModeService }] });
|
|
1833
1931
|
|
|
1834
1932
|
class ThemeService {
|
|
1835
1933
|
THEME_KEY = 'cupcake-theme';
|
|
@@ -3604,6 +3702,51 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImpor
|
|
|
3604
3702
|
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
3703
|
}], ctorParameters: () => [] });
|
|
3606
3704
|
|
|
3705
|
+
class DemoModeBannerComponent {
|
|
3706
|
+
demoModeService;
|
|
3707
|
+
destroy$ = new Subject();
|
|
3708
|
+
isDemoMode = false;
|
|
3709
|
+
demoModeInfo = null;
|
|
3710
|
+
isCollapsed = false;
|
|
3711
|
+
constructor(demoModeService) {
|
|
3712
|
+
this.demoModeService = demoModeService;
|
|
3713
|
+
}
|
|
3714
|
+
ngOnInit() {
|
|
3715
|
+
this.demoModeService.demoMode$
|
|
3716
|
+
.pipe(takeUntil(this.destroy$))
|
|
3717
|
+
.subscribe(info => {
|
|
3718
|
+
this.isDemoMode = info.isActive;
|
|
3719
|
+
this.demoModeInfo = info;
|
|
3720
|
+
});
|
|
3721
|
+
const collapsed = localStorage.getItem('demo_banner_collapsed');
|
|
3722
|
+
if (collapsed === 'true') {
|
|
3723
|
+
this.isCollapsed = true;
|
|
3724
|
+
}
|
|
3725
|
+
}
|
|
3726
|
+
ngOnDestroy() {
|
|
3727
|
+
this.destroy$.next();
|
|
3728
|
+
this.destroy$.complete();
|
|
3729
|
+
}
|
|
3730
|
+
toggleCollapse() {
|
|
3731
|
+
this.isCollapsed = !this.isCollapsed;
|
|
3732
|
+
localStorage.setItem('demo_banner_collapsed', this.isCollapsed.toString());
|
|
3733
|
+
}
|
|
3734
|
+
getMinutesRemaining() {
|
|
3735
|
+
if (!this.demoModeInfo?.lastDetected) {
|
|
3736
|
+
return this.demoModeInfo?.cleanupIntervalMinutes || 15;
|
|
3737
|
+
}
|
|
3738
|
+
const elapsed = (Date.now() - this.demoModeInfo.lastDetected.getTime()) / 1000 / 60;
|
|
3739
|
+
const remaining = this.demoModeInfo.cleanupIntervalMinutes - elapsed;
|
|
3740
|
+
return Math.max(0, Math.floor(remaining));
|
|
3741
|
+
}
|
|
3742
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: DemoModeBannerComponent, deps: [{ token: DemoModeService }], target: i0.ɵɵFactoryTarget.Component });
|
|
3743
|
+
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\" [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 <button class=\"collapse-btn\" (click)=\"toggleCollapse()\" type=\"button\">\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 </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 .collapse-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{background:#ffffff1a}.demo-mode-banner .banner-header .collapse-btn:focus{outline:2px solid rgba(255,255,255,.5);outline-offset:2px}.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"] }] });
|
|
3744
|
+
}
|
|
3745
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: DemoModeBannerComponent, decorators: [{
|
|
3746
|
+
type: Component,
|
|
3747
|
+
args: [{ selector: 'cupcake-demo-mode-banner', standalone: true, imports: [CommonModule], template: "<div class=\"demo-mode-banner\" *ngIf=\"isDemoMode\" [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 <button class=\"collapse-btn\" (click)=\"toggleCollapse()\" type=\"button\">\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 </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 .collapse-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{background:#ffffff1a}.demo-mode-banner .banner-header .collapse-btn:focus{outline:2px solid rgba(255,255,255,.5);outline-offset:2px}.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"] }]
|
|
3748
|
+
}], ctorParameters: () => [{ type: DemoModeService }] });
|
|
3749
|
+
|
|
3607
3750
|
class ToastContainerComponent {
|
|
3608
3751
|
toastService = inject(ToastService);
|
|
3609
3752
|
cdr = inject(ChangeDetectorRef);
|
|
@@ -3732,5 +3875,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImpor
|
|
|
3732
3875
|
* Generated bundle index. Do not edit.
|
|
3733
3876
|
*/
|
|
3734
3877
|
|
|
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 };
|
|
3878
|
+
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
3879
|
//# sourceMappingURL=noatgnu-cupcake-core.mjs.map
|