@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.
@@ -1,10 +1,10 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, InjectionToken, Injectable, signal, computed, Component, effect, ChangeDetectorRef, NgModule } from '@angular/core';
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, tap as tap$1, switchMap as switchMap$1 } from 'rxjs/operators';
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$1(() => {
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$1(msg => console.log('Filtering message:', msg.type, 'looking for:', type)), switchMap$1(message => message.type === type ? [message] : EMPTY));
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$1(config => {
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$1(updatedConfig => {
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
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: SiteConfigService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
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