@noatgnu/cupcake-core 1.3.9 → 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,16 +1,17 @@
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';
11
11
  import { CommonModule } 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
+ import { toSignal } from '@angular/core/rxjs-interop';
14
15
 
15
16
  var ResourceType;
16
17
  (function (ResourceType) {
@@ -253,6 +254,79 @@ function handle401Error(request, next, http, router, config) {
253
254
  }
254
255
  }
255
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
+
256
330
  const CUPCAKE_CORE_CONFIG = new InjectionToken('CUPCAKE_CORE_CONFIG');
257
331
  class AuthService {
258
332
  http = inject(HttpClient);
@@ -377,31 +451,31 @@ class AuthService {
377
451
  handleORCIDCallback(code, state, rememberMe = false) {
378
452
  const params = new URLSearchParams({ code, state, remember_me: rememberMe.toString() });
379
453
  return this.http.get(`${this.apiUrl}/auth/orcid/callback/?${params}`)
380
- .pipe(tap(response => this.setAuthData(response)));
454
+ .pipe(tap$1(response => this.setAuthData(response)));
381
455
  }
382
456
  exchangeORCIDToken(accessToken, orcidId, rememberMe = false) {
383
457
  return this.http.post(`${this.apiUrl}/auth/orcid/token/`, {
384
458
  access_token: accessToken,
385
459
  orcid_id: orcidId,
386
460
  remember_me: rememberMe
387
- }).pipe(tap(response => this.setAuthData(response)));
461
+ }).pipe(tap$1(response => this.setAuthData(response)));
388
462
  }
389
463
  login(username, password, rememberMe = false) {
390
464
  return this.http.post(`${this.apiUrl}/auth/login/`, {
391
465
  username,
392
466
  password,
393
467
  remember_me: rememberMe
394
- }).pipe(tap(response => this.setAuthData(response)));
468
+ }).pipe(tap$1(response => this.setAuthData(response)));
395
469
  }
396
470
  logout() {
397
471
  const refreshToken = this.getRefreshToken();
398
472
  const payload = refreshToken ? { refresh: refreshToken } : {};
399
473
  return this.http.post(`${this.apiUrl}/auth/logout/`, payload)
400
- .pipe(tap(() => this.clearAuthData()));
474
+ .pipe(tap$1(() => this.clearAuthData()));
401
475
  }
402
476
  checkAuthStatus() {
403
477
  return this.http.get(`${this.apiUrl}/auth/status/`)
404
- .pipe(tap(status => {
478
+ .pipe(tap$1(status => {
405
479
  if (status.authenticated && status.user) {
406
480
  this.currentUserSubject.next(status.user);
407
481
  this.isAuthenticatedSubject.next(true);
@@ -413,7 +487,7 @@ class AuthService {
413
487
  }
414
488
  fetchUserProfile() {
415
489
  return this.http.get(`${this.apiUrl}/auth/profile/`)
416
- .pipe(map(response => this.convertUserFromSnakeToCamel(response.user)), tap(user => {
490
+ .pipe(map(response => this.convertUserFromSnakeToCamel(response.user)), tap$1(user => {
417
491
  this.currentUserSubject.next(user);
418
492
  this.isAuthenticatedSubject.next(true);
419
493
  }));
@@ -448,7 +522,7 @@ class AuthService {
448
522
  }
449
523
  return this.http.post(`${this.apiUrl}/auth/token/refresh/`, {
450
524
  refresh: refreshToken
451
- }).pipe(tap((response) => {
525
+ }).pipe(tap$1((response) => {
452
526
  localStorage.setItem('ccvAccessToken', response.access);
453
527
  this.isAuthenticatedSubject.next(true);
454
528
  const user = this.getUserFromToken();
@@ -473,7 +547,7 @@ class AuthService {
473
547
  }
474
548
  return this.http.post(`${this.apiUrl}/auth/token/refresh/`, {
475
549
  refresh: refreshToken
476
- }).pipe(tap(response => {
550
+ }).pipe(tap$1(response => {
477
551
  localStorage.setItem('ccvAccessToken', response.access);
478
552
  this.isAuthenticatedSubject.next(true);
479
553
  this.fetchUserProfile().subscribe({
@@ -1352,7 +1426,7 @@ class WebSocketService {
1352
1426
  this.reconnectAttempts++;
1353
1427
  const delay = this.config.reconnectInterval || 5000;
1354
1428
  console.log(`WebSocket reconnection attempt ${this.reconnectAttempts} in ${delay}ms`);
1355
- timer(delay).pipe(takeUntil(this.destroy$), tap$1(() => {
1429
+ timer(delay).pipe(takeUntil(this.destroy$), tap(() => {
1356
1430
  if (this.reconnectAttempts <= (this.config.maxReconnectAttempts || 5)) {
1357
1431
  this.connect();
1358
1432
  }
@@ -1363,7 +1437,7 @@ class WebSocketService {
1363
1437
  })).subscribe();
1364
1438
  }
1365
1439
  filterMessages(type) {
1366
- 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));
1367
1441
  }
1368
1442
  getNotifications() {
1369
1443
  return this.filterMessages('notification');
@@ -1722,6 +1796,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImpor
1722
1796
  }], ctorParameters: () => [{ type: ToastService }] });
1723
1797
 
1724
1798
  class SiteConfigService extends BaseApiService {
1799
+ demoModeService;
1725
1800
  defaultConfig = {
1726
1801
  siteName: 'CUPCAKE Vanilla',
1727
1802
  showPoweredBy: true,
@@ -1729,15 +1804,28 @@ class SiteConfigService extends BaseApiService {
1729
1804
  allowUserRegistration: false,
1730
1805
  enableOrcidLogin: false,
1731
1806
  bookingDeletionWindowMinutes: 30,
1807
+ whisperCppModel: '/app/whisper.cpp/models/ggml-medium.bin',
1732
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
+ },
1733
1820
  installedApps: {},
1734
1821
  createdAt: new Date().toISOString(),
1735
1822
  updatedAt: new Date().toISOString()
1736
1823
  };
1737
1824
  configSubject = new BehaviorSubject(this.defaultConfig);
1738
1825
  config$ = this.configSubject.asObservable();
1739
- constructor() {
1826
+ constructor(demoModeService) {
1740
1827
  super();
1828
+ this.demoModeService = demoModeService;
1741
1829
  this.loadConfig();
1742
1830
  this.startPeriodicRefresh();
1743
1831
  }
@@ -1747,6 +1835,7 @@ class SiteConfigService extends BaseApiService {
1747
1835
  next: (config) => {
1748
1836
  this.configSubject.next({ ...this.defaultConfig, ...config });
1749
1837
  localStorage.setItem('site_config', JSON.stringify(config));
1838
+ this.handleDemoMode(config);
1750
1839
  },
1751
1840
  error: () => { }
1752
1841
  });
@@ -1758,6 +1847,7 @@ class SiteConfigService extends BaseApiService {
1758
1847
  try {
1759
1848
  const config = JSON.parse(savedConfig);
1760
1849
  this.configSubject.next({ ...this.defaultConfig, ...config });
1850
+ this.handleDemoMode(config);
1761
1851
  }
1762
1852
  catch (error) {
1763
1853
  // Invalid config, use defaults
@@ -1767,6 +1857,7 @@ class SiteConfigService extends BaseApiService {
1767
1857
  next: (config) => {
1768
1858
  this.configSubject.next({ ...this.defaultConfig, ...config });
1769
1859
  localStorage.setItem('site_config', JSON.stringify(config));
1860
+ this.handleDemoMode(config);
1770
1861
  },
1771
1862
  error: () => {
1772
1863
  // Continue with current config
@@ -1780,13 +1871,14 @@ class SiteConfigService extends BaseApiService {
1780
1871
  return this.get(`${this.apiUrl}/site-config/public/`);
1781
1872
  }
1782
1873
  getCurrentConfig() {
1783
- return this.get(`${this.apiUrl}/site-config/current/`).pipe(tap$1(config => {
1874
+ return this.get(`${this.apiUrl}/site-config/current/`).pipe(tap(config => {
1784
1875
  this.configSubject.next({ ...this.defaultConfig, ...config });
1785
1876
  localStorage.setItem('site_config', JSON.stringify(config));
1877
+ this.handleDemoMode(config);
1786
1878
  }));
1787
1879
  }
1788
1880
  updateConfig(config) {
1789
- 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 => {
1790
1882
  this.configSubject.next({ ...this.defaultConfig, ...updatedConfig });
1791
1883
  localStorage.setItem('site_config', JSON.stringify(updatedConfig));
1792
1884
  }));
@@ -1810,7 +1902,24 @@ class SiteConfigService extends BaseApiService {
1810
1902
  isOrcidLoginEnabled() {
1811
1903
  return this.configSubject.value.enableOrcidLogin === true;
1812
1904
  }
1813
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: SiteConfigService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1905
+ getAvailableWhisperModels() {
1906
+ return this.get(`${this.apiUrl}/site-config/available_whisper_models/`);
1907
+ }
1908
+ refreshWhisperModels() {
1909
+ return this.post(`${this.apiUrl}/site-config/refresh_whisper_models/`, {});
1910
+ }
1911
+ getWorkerStatus() {
1912
+ return this.get(`${this.apiUrl}/site-config/worker_status/`);
1913
+ }
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 });
1814
1923
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: SiteConfigService, providedIn: 'root' });
1815
1924
  }
1816
1925
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: SiteConfigService, decorators: [{
@@ -1818,7 +1927,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImpor
1818
1927
  args: [{
1819
1928
  providedIn: 'root'
1820
1929
  }]
1821
- }], ctorParameters: () => [] });
1930
+ }], ctorParameters: () => [{ type: DemoModeService }] });
1822
1931
 
1823
1932
  class ThemeService {
1824
1933
  THEME_KEY = 'cupcake-theme';
@@ -3380,6 +3489,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImpor
3380
3489
  class SiteConfigComponent {
3381
3490
  fb = inject(FormBuilder);
3382
3491
  siteConfigService = inject(SiteConfigService);
3492
+ authService = inject(AuthService);
3383
3493
  configForm;
3384
3494
  // Signals for reactive state management
3385
3495
  loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
@@ -3387,12 +3497,22 @@ class SiteConfigComponent {
3387
3497
  success = signal(null, ...(ngDevMode ? [{ debugName: "success" }] : []));
3388
3498
  selectedLogoFile = signal(null, ...(ngDevMode ? [{ debugName: "selectedLogoFile" }] : []));
3389
3499
  currentConfig = signal(null, ...(ngDevMode ? [{ debugName: "currentConfig" }] : []));
3500
+ availableModels = signal([], ...(ngDevMode ? [{ debugName: "availableModels" }] : []));
3501
+ loadingModels = signal(false, ...(ngDevMode ? [{ debugName: "loadingModels" }] : []));
3502
+ refreshingModels = signal(false, ...(ngDevMode ? [{ debugName: "refreshingModels" }] : []));
3503
+ workerStatus = signal(null, ...(ngDevMode ? [{ debugName: "workerStatus" }] : []));
3504
+ loadingWorkerStatus = signal(false, ...(ngDevMode ? [{ debugName: "loadingWorkerStatus" }] : []));
3505
+ currentUser = toSignal(this.authService.currentUser$);
3506
+ Object = Object;
3390
3507
  // Computed signal for preview configuration
3391
3508
  previewConfig = computed(() => {
3392
3509
  if (!this.currentConfig())
3393
3510
  return null;
3394
3511
  return { ...this.currentConfig(), ...this.configForm.value };
3395
3512
  }, ...(ngDevMode ? [{ debugName: "previewConfig" }] : []));
3513
+ isSuperuser = computed(() => {
3514
+ return this.currentUser()?.isSuperuser === true;
3515
+ }, ...(ngDevMode ? [{ debugName: "isSuperuser" }] : []));
3396
3516
  constructor() {
3397
3517
  this.configForm = this.fb.group({
3398
3518
  siteName: ['', [Validators.required, Validators.minLength(1), Validators.maxLength(255)]],
@@ -3400,7 +3520,8 @@ class SiteConfigComponent {
3400
3520
  primaryColor: ['#1976d2', [Validators.pattern(/^#[0-9A-Fa-f]{6}$/)]],
3401
3521
  showPoweredBy: [true],
3402
3522
  allowUserRegistration: [false],
3403
- enableOrcidLogin: [false]
3523
+ enableOrcidLogin: [false],
3524
+ whisperCppModel: ['/app/whisper.cpp/models/ggml-medium.bin']
3404
3525
  });
3405
3526
  }
3406
3527
  ngOnInit() {
@@ -3414,6 +3535,50 @@ class SiteConfigComponent {
3414
3535
  this.error.set('Failed to load current configuration.');
3415
3536
  }
3416
3537
  });
3538
+ // Load available Whisper models
3539
+ this.loadAvailableModels();
3540
+ }
3541
+ loadAvailableModels() {
3542
+ this.loadingModels.set(true);
3543
+ this.siteConfigService.getAvailableWhisperModels().subscribe({
3544
+ next: (response) => {
3545
+ this.availableModels.set(response.models || []);
3546
+ this.loadingModels.set(false);
3547
+ },
3548
+ error: (error) => {
3549
+ this.loadingModels.set(false);
3550
+ console.error('Failed to load available models:', error);
3551
+ }
3552
+ });
3553
+ }
3554
+ refreshAvailableModels() {
3555
+ this.refreshingModels.set(true);
3556
+ this.siteConfigService.refreshWhisperModels().subscribe({
3557
+ next: (response) => {
3558
+ this.success.set('Model scan queued. Refreshing in 5 seconds...');
3559
+ setTimeout(() => {
3560
+ this.loadAvailableModels();
3561
+ this.refreshingModels.set(false);
3562
+ }, 5000);
3563
+ },
3564
+ error: (error) => {
3565
+ this.error.set('Failed to queue model scan.');
3566
+ this.refreshingModels.set(false);
3567
+ }
3568
+ });
3569
+ }
3570
+ loadWorkerStatus() {
3571
+ this.loadingWorkerStatus.set(true);
3572
+ this.siteConfigService.getWorkerStatus().subscribe({
3573
+ next: (response) => {
3574
+ this.workerStatus.set(response);
3575
+ this.loadingWorkerStatus.set(false);
3576
+ },
3577
+ error: (error) => {
3578
+ this.loadingWorkerStatus.set(false);
3579
+ console.error('Failed to load worker status:', error);
3580
+ }
3581
+ });
3417
3582
  }
3418
3583
  /**
3419
3584
  * Update site configuration
@@ -3530,13 +3695,58 @@ class SiteConfigComponent {
3530
3695
  }
3531
3696
  }
3532
3697
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: SiteConfigComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3533
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.7", type: SiteConfigComponent, isStandalone: true, selector: "ccc-site-config", ngImport: i0, 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 </form>\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>", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: NgbAlert, selector: "ngb-alert", inputs: ["animation", "dismissible", "type"], outputs: ["closed"], exportAs: ["ngbAlert"] }] });
3698
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.7", type: SiteConfigComponent, isStandalone: true, selector: "ccc-site-config", ngImport: i0, 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>", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: NgbAlert, selector: "ngb-alert", inputs: ["animation", "dismissible", "type"], outputs: ["closed"], exportAs: ["ngbAlert"] }] });
3534
3699
  }
3535
3700
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImport: i0, type: SiteConfigComponent, decorators: [{
3536
3701
  type: Component,
3537
- 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 </form>\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>" }]
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>" }]
3538
3703
  }], ctorParameters: () => [] });
3539
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
+
3540
3750
  class ToastContainerComponent {
3541
3751
  toastService = inject(ToastService);
3542
3752
  cdr = inject(ChangeDetectorRef);
@@ -3665,5 +3875,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.7", ngImpor
3665
3875
  * Generated bundle index. Do not edit.
3666
3876
  */
3667
3877
 
3668
- 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 };
3669
3879
  //# sourceMappingURL=noatgnu-cupcake-core.mjs.map