angular-debug-recorder 1.0.0 → 1.0.1

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.
@@ -418,24 +418,39 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
418
418
  class RrwebRecorderService {
419
419
  constructor(zone) {
420
420
  this.zone = zone;
421
- this._events = signal([]);
421
+ // Plain array — cheap push, no O(n) spread on every event
422
+ this._eventsArray = [];
423
+ // Signal only for reactive count display in templates
424
+ this._eventCount = signal(0);
422
425
  this._isRecording = signal(false);
423
- this.events = this._events.asReadonly();
426
+ this.eventCount = this._eventCount.asReadonly();
424
427
  this.isRecording = this._isRecording.asReadonly();
425
428
  }
429
+ hasEvents() {
430
+ return this._eventsArray.length > 0;
431
+ }
432
+ getEvents() {
433
+ return this._eventsArray;
434
+ }
426
435
  async startRecording() {
427
- // Dynamically import rrweb to avoid SSR issues and reduce initial bundle
428
436
  const { record } = await import('rrweb');
429
- this._events.set([]);
437
+ this._eventsArray = [];
438
+ this._eventCount.set(0);
430
439
  this._isRecording.set(true);
431
440
  this.stopFn = record({
432
441
  emit: (event) => {
433
- this.zone.run(() => {
434
- this._events.update(ev => [...ev, event]);
442
+ // Push outside Angular zone no change detection per event
443
+ this.zone.runOutsideAngular(() => {
444
+ this._eventsArray.push(event);
435
445
  });
446
+ // Update count signal only every 10 events to keep UI responsive
447
+ const len = this._eventsArray.length;
448
+ if (len === 1 || len % 10 === 0) {
449
+ this.zone.run(() => this._eventCount.set(len));
450
+ }
436
451
  },
437
- // Note: blockSelector is omitted — rrweb 2.0.0-alpha.4 calls node.matches()
438
- // on TextNodes/CommentNodes which don't have that method, crashing the recorder.
452
+ // Note: blockSelector omitted — rrweb 2.0.0-alpha.4 calls node.matches()
453
+ // on TextNodes/CommentNodes which crash the recorder.
439
454
  maskTextSelector: 'input[type="password"]',
440
455
  checkoutEveryNth: 200,
441
456
  });
@@ -446,24 +461,19 @@ class RrwebRecorderService {
446
461
  this.stopFn = undefined;
447
462
  }
448
463
  this._isRecording.set(false);
449
- return this._events();
450
- }
451
- getEvents() {
452
- return this._events();
464
+ this._eventCount.set(this._eventsArray.length);
465
+ return this._eventsArray;
453
466
  }
454
467
  clearEvents() {
455
- this._events.set([]);
456
- }
457
- hasEvents() {
458
- return this._events().length > 0;
468
+ this._eventsArray = [];
469
+ this._eventCount.set(0);
459
470
  }
460
471
  // ─── Replay ──────────────────────────────────────────────────────────────
461
472
  async startReplay(container, events) {
462
473
  const { Replayer } = await import('rrweb');
463
- const eventsToReplay = events ?? this._events();
474
+ const eventsToReplay = events ?? this._eventsArray;
464
475
  if (eventsToReplay.length === 0)
465
476
  return;
466
- // Destroy previous replayer
467
477
  this.destroyReplayer();
468
478
  this.replayer = new Replayer(eventsToReplay, {
469
479
  root: container,
@@ -491,7 +501,7 @@ class RrwebRecorderService {
491
501
  }
492
502
  // ─── Export ───────────────────────────────────────────────────────────────
493
503
  exportEvents() {
494
- return JSON.stringify(this._events(), null, 2);
504
+ return JSON.stringify(this._eventsArray, null, 2);
495
505
  }
496
506
  downloadEvents(filename = 'rrweb-session.json') {
497
507
  const blob = new Blob([this.exportEvents()], { type: 'application/json' });
@@ -505,7 +515,8 @@ class RrwebRecorderService {
505
515
  importEvents(json) {
506
516
  try {
507
517
  const events = JSON.parse(json);
508
- this._events.set(events);
518
+ this._eventsArray = events;
519
+ this._eventCount.set(events.length);
509
520
  return events;
510
521
  }
511
522
  catch {
@@ -563,170 +574,170 @@ class ActionListComponent {
563
574
  return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`;
564
575
  }
565
576
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ActionListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
566
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: ActionListComponent, isStandalone: true, selector: "app-action-list", inputs: { session: "session" }, outputs: { removeAction: "removeAction", addNote: "addNote" }, ngImport: i0, template: `
567
- <div class="action-list" data-debug-panel>
568
- @if (!session || session.actions.length === 0) {
569
- <div class="empty-state">
570
- <div class="empty-icon">🎬</div>
571
- <p>Noch keine Aktionen aufgezeichnet.</p>
572
- <p class="hint">Starte die Aufnahme und interagiere mit der App.</p>
573
- <div class="shortcuts-hint">
574
- <kbd>Ctrl+Shift+D</kbd> Panel &nbsp;
575
- <kbd>Ctrl+Shift+R</kbd> Record
576
- </div>
577
- </div>
578
- } @else {
579
- <div class="list-header">
580
- <span class="list-count">{{ session.actions.length }} Aktionen</span>
581
- <span class="list-duration">
582
- @if (session.endTime) {
583
- {{ formatDuration(session.startTime, session.endTime) }}
584
- } @else {
585
- Live
586
- }
587
- </span>
588
- </div>
589
-
590
- @for (action of session.actions; track action.id; let i = $index) {
591
- <div class="action-item" [class.expanded]="expandedId() === action.id">
592
- <div class="action-row" (click)="toggleExpand(action.id)">
593
- <span class="action-index">{{ i + 1 }}</span>
594
- <span class="action-type-badge" [class]="'type-' + action.type">
595
- {{ getActionIcon(action.type) }}
596
- </span>
597
- <div class="action-info">
598
- <span class="action-desc">{{ action.description }}</span>
599
- <span class="action-selector">{{ action.selector }}</span>
600
- </div>
601
- <span class="action-time">{{ formatTime(action.timestamp) }}</span>
602
- <button
603
- class="remove-btn"
604
- data-debug-panel
605
- title="Aktion entfernen"
606
- (click)="onRemove(action.id, $event)"
607
- >✕</button>
608
- </div>
609
-
610
- @if (expandedId() === action.id) {
611
- <div class="action-detail" data-debug-panel>
612
- <div class="detail-grid">
613
- <span class="detail-label">Selector</span>
614
- <code class="detail-value">{{ action.selector }}</code>
615
- @if (action.value) {
616
- <span class="detail-label">Wert</span>
617
- <code class="detail-value">{{ action.value }}</code>
618
- }
619
- @if (action.element?.tagName) {
620
- <span class="detail-label">Element</span>
621
- <code class="detail-value">&lt;{{ action.element?.tagName }}&gt;</code>
622
- }
623
- <span class="detail-label">Strategie</span>
624
- <span class="detail-value strategy-badge" [class]="'strat-' + action.selectorStrategy">
625
- {{ action.selectorStrategy }}
626
- </span>
627
- <span class="detail-label">URL</span>
628
- <code class="detail-value url-val">{{ action.url }}</code>
629
- </div>
630
- <div class="note-area">
631
- <textarea
632
- data-debug-panel
633
- class="note-input"
634
- [(ngModel)]="noteMap[action.id]"
635
- placeholder="Notiz zu dieser Aktion..."
636
- rows="2"
637
- (blur)="onAddNote(action.id)"
638
- ></textarea>
639
- </div>
640
- </div>
641
- }
642
- </div>
643
- }
644
- }
645
- </div>
577
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: ActionListComponent, isStandalone: true, selector: "app-action-list", inputs: { session: "session" }, outputs: { removeAction: "removeAction", addNote: "addNote" }, ngImport: i0, template: `
578
+ <div class="action-list" data-debug-panel>
579
+ @if (!session || session.actions.length === 0) {
580
+ <div class="empty-state">
581
+ <div class="empty-icon">🎬</div>
582
+ <p>Noch keine Aktionen aufgezeichnet.</p>
583
+ <p class="hint">Starte die Aufnahme und interagiere mit der App.</p>
584
+ <div class="shortcuts-hint">
585
+ <kbd>Ctrl+Shift+D</kbd> Panel &nbsp;
586
+ <kbd>Ctrl+Shift+R</kbd> Record
587
+ </div>
588
+ </div>
589
+ } @else {
590
+ <div class="list-header">
591
+ <span class="list-count">{{ session.actions.length }} Aktionen</span>
592
+ <span class="list-duration">
593
+ @if (session.endTime) {
594
+ {{ formatDuration(session.startTime, session.endTime) }}
595
+ } @else {
596
+ Live
597
+ }
598
+ </span>
599
+ </div>
600
+
601
+ @for (action of session.actions; track action.id; let i = $index) {
602
+ <div class="action-item" [class.expanded]="expandedId() === action.id">
603
+ <div class="action-row" (click)="toggleExpand(action.id)">
604
+ <span class="action-index">{{ i + 1 }}</span>
605
+ <span class="action-type-badge" [class]="'type-' + action.type">
606
+ {{ getActionIcon(action.type) }}
607
+ </span>
608
+ <div class="action-info">
609
+ <span class="action-desc">{{ action.description }}</span>
610
+ <span class="action-selector">{{ action.selector }}</span>
611
+ </div>
612
+ <span class="action-time">{{ formatTime(action.timestamp) }}</span>
613
+ <button
614
+ class="remove-btn"
615
+ data-debug-panel
616
+ title="Aktion entfernen"
617
+ (click)="onRemove(action.id, $event)"
618
+ >✕</button>
619
+ </div>
620
+
621
+ @if (expandedId() === action.id) {
622
+ <div class="action-detail" data-debug-panel>
623
+ <div class="detail-grid">
624
+ <span class="detail-label">Selector</span>
625
+ <code class="detail-value">{{ action.selector }}</code>
626
+ @if (action.value) {
627
+ <span class="detail-label">Wert</span>
628
+ <code class="detail-value">{{ action.value }}</code>
629
+ }
630
+ @if (action.element?.tagName) {
631
+ <span class="detail-label">Element</span>
632
+ <code class="detail-value">&lt;{{ action.element?.tagName }}&gt;</code>
633
+ }
634
+ <span class="detail-label">Strategie</span>
635
+ <span class="detail-value strategy-badge" [class]="'strat-' + action.selectorStrategy">
636
+ {{ action.selectorStrategy }}
637
+ </span>
638
+ <span class="detail-label">URL</span>
639
+ <code class="detail-value url-val">{{ action.url }}</code>
640
+ </div>
641
+ <div class="note-area">
642
+ <textarea
643
+ data-debug-panel
644
+ class="note-input"
645
+ [(ngModel)]="noteMap[action.id]"
646
+ placeholder="Notiz zu dieser Aktion..."
647
+ rows="2"
648
+ (blur)="onAddNote(action.id)"
649
+ ></textarea>
650
+ </div>
651
+ </div>
652
+ }
653
+ </div>
654
+ }
655
+ }
656
+ </div>
646
657
  `, isInline: true, styles: [".action-list{padding:0}.empty-state{text-align:center;padding:32px 20px;color:#64748b}.empty-icon{font-size:40px;margin-bottom:10px}.empty-state p{margin:4px 0;font-size:13px}.hint{font-size:11px;color:#475569}.shortcuts-hint{margin-top:12px;font-size:11px;color:#475569}kbd{background:#1e293b;border:1px solid #334155;color:#94a3b8;padding:2px 6px;border-radius:4px;font-size:10px}.list-header{display:flex;justify-content:space-between;padding:8px 14px;font-size:11px;color:#64748b;background:#0f172a;border-bottom:1px solid #1e293b;position:sticky;top:0}.action-item{border-bottom:1px solid #1e293b;transition:background .1s}.action-item:hover{background:#1e293b80}.action-item.expanded{background:#1e293b}.action-row{display:flex;align-items:center;padding:8px 10px;gap:8px;cursor:pointer}.action-index{color:#475569;font-size:10px;min-width:18px;text-align:right}.action-type-badge{font-size:14px;min-width:20px;text-align:center}.action-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}.action-desc{font-size:12px;color:#cbd5e1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.action-selector{font-size:10px;color:#64748b;font-family:Cascadia Code,Consolas,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.action-time{font-size:10px;color:#475569;white-space:nowrap}.remove-btn{background:none;border:none;color:#475569;cursor:pointer;font-size:12px;padding:2px 5px;border-radius:3px;opacity:0;transition:opacity .15s,color .15s}.action-row:hover .remove-btn{opacity:1}.remove-btn:hover{color:#f87171}.action-detail{padding:10px 14px;background:#0f172ab3;border-top:1px solid #1e293b}.detail-grid{display:grid;grid-template-columns:auto 1fr;gap:4px 10px;margin-bottom:8px;align-items:start}.detail-label{font-size:10px;color:#64748b;padding-top:2px;white-space:nowrap}.detail-value{font-size:11px;color:#93c5fd;font-family:Cascadia Code,Consolas,monospace;word-break:break-all}.url-val{color:#6ee7b7}.strategy-badge{font-size:10px;padding:1px 6px;border-radius:3px;font-family:monospace}.strat-data-testid,.strat-data-cy{background:#064e3b;color:#34d399}.strat-id{background:#1e3a8a;color:#93c5fd}.strat-name{background:#44337a;color:#c4b5fd}.strat-class{background:#374151;color:#9ca3af}.strat-combined{background:#292524;color:#d6d3d1}.note-area{margin-top:6px}.note-input{width:100%;box-sizing:border-box;background:#0f172a;border:1px solid #334155;color:#e2e8f0;border-radius:5px;padding:6px 8px;font-size:11px;resize:vertical}.note-input:focus{outline:none;border-color:#3b82f6}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { 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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] }); }
647
658
  }
648
659
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ActionListComponent, decorators: [{
649
660
  type: Component,
650
- args: [{ selector: 'app-action-list', standalone: true, imports: [CommonModule, FormsModule], template: `
651
- <div class="action-list" data-debug-panel>
652
- @if (!session || session.actions.length === 0) {
653
- <div class="empty-state">
654
- <div class="empty-icon">🎬</div>
655
- <p>Noch keine Aktionen aufgezeichnet.</p>
656
- <p class="hint">Starte die Aufnahme und interagiere mit der App.</p>
657
- <div class="shortcuts-hint">
658
- <kbd>Ctrl+Shift+D</kbd> Panel &nbsp;
659
- <kbd>Ctrl+Shift+R</kbd> Record
660
- </div>
661
- </div>
662
- } @else {
663
- <div class="list-header">
664
- <span class="list-count">{{ session.actions.length }} Aktionen</span>
665
- <span class="list-duration">
666
- @if (session.endTime) {
667
- {{ formatDuration(session.startTime, session.endTime) }}
668
- } @else {
669
- Live
670
- }
671
- </span>
672
- </div>
673
-
674
- @for (action of session.actions; track action.id; let i = $index) {
675
- <div class="action-item" [class.expanded]="expandedId() === action.id">
676
- <div class="action-row" (click)="toggleExpand(action.id)">
677
- <span class="action-index">{{ i + 1 }}</span>
678
- <span class="action-type-badge" [class]="'type-' + action.type">
679
- {{ getActionIcon(action.type) }}
680
- </span>
681
- <div class="action-info">
682
- <span class="action-desc">{{ action.description }}</span>
683
- <span class="action-selector">{{ action.selector }}</span>
684
- </div>
685
- <span class="action-time">{{ formatTime(action.timestamp) }}</span>
686
- <button
687
- class="remove-btn"
688
- data-debug-panel
689
- title="Aktion entfernen"
690
- (click)="onRemove(action.id, $event)"
691
- >✕</button>
692
- </div>
693
-
694
- @if (expandedId() === action.id) {
695
- <div class="action-detail" data-debug-panel>
696
- <div class="detail-grid">
697
- <span class="detail-label">Selector</span>
698
- <code class="detail-value">{{ action.selector }}</code>
699
- @if (action.value) {
700
- <span class="detail-label">Wert</span>
701
- <code class="detail-value">{{ action.value }}</code>
702
- }
703
- @if (action.element?.tagName) {
704
- <span class="detail-label">Element</span>
705
- <code class="detail-value">&lt;{{ action.element?.tagName }}&gt;</code>
706
- }
707
- <span class="detail-label">Strategie</span>
708
- <span class="detail-value strategy-badge" [class]="'strat-' + action.selectorStrategy">
709
- {{ action.selectorStrategy }}
710
- </span>
711
- <span class="detail-label">URL</span>
712
- <code class="detail-value url-val">{{ action.url }}</code>
713
- </div>
714
- <div class="note-area">
715
- <textarea
716
- data-debug-panel
717
- class="note-input"
718
- [(ngModel)]="noteMap[action.id]"
719
- placeholder="Notiz zu dieser Aktion..."
720
- rows="2"
721
- (blur)="onAddNote(action.id)"
722
- ></textarea>
723
- </div>
724
- </div>
725
- }
726
- </div>
727
- }
728
- }
729
- </div>
661
+ args: [{ selector: 'app-action-list', standalone: true, imports: [CommonModule, FormsModule], template: `
662
+ <div class="action-list" data-debug-panel>
663
+ @if (!session || session.actions.length === 0) {
664
+ <div class="empty-state">
665
+ <div class="empty-icon">🎬</div>
666
+ <p>Noch keine Aktionen aufgezeichnet.</p>
667
+ <p class="hint">Starte die Aufnahme und interagiere mit der App.</p>
668
+ <div class="shortcuts-hint">
669
+ <kbd>Ctrl+Shift+D</kbd> Panel &nbsp;
670
+ <kbd>Ctrl+Shift+R</kbd> Record
671
+ </div>
672
+ </div>
673
+ } @else {
674
+ <div class="list-header">
675
+ <span class="list-count">{{ session.actions.length }} Aktionen</span>
676
+ <span class="list-duration">
677
+ @if (session.endTime) {
678
+ {{ formatDuration(session.startTime, session.endTime) }}
679
+ } @else {
680
+ Live
681
+ }
682
+ </span>
683
+ </div>
684
+
685
+ @for (action of session.actions; track action.id; let i = $index) {
686
+ <div class="action-item" [class.expanded]="expandedId() === action.id">
687
+ <div class="action-row" (click)="toggleExpand(action.id)">
688
+ <span class="action-index">{{ i + 1 }}</span>
689
+ <span class="action-type-badge" [class]="'type-' + action.type">
690
+ {{ getActionIcon(action.type) }}
691
+ </span>
692
+ <div class="action-info">
693
+ <span class="action-desc">{{ action.description }}</span>
694
+ <span class="action-selector">{{ action.selector }}</span>
695
+ </div>
696
+ <span class="action-time">{{ formatTime(action.timestamp) }}</span>
697
+ <button
698
+ class="remove-btn"
699
+ data-debug-panel
700
+ title="Aktion entfernen"
701
+ (click)="onRemove(action.id, $event)"
702
+ >✕</button>
703
+ </div>
704
+
705
+ @if (expandedId() === action.id) {
706
+ <div class="action-detail" data-debug-panel>
707
+ <div class="detail-grid">
708
+ <span class="detail-label">Selector</span>
709
+ <code class="detail-value">{{ action.selector }}</code>
710
+ @if (action.value) {
711
+ <span class="detail-label">Wert</span>
712
+ <code class="detail-value">{{ action.value }}</code>
713
+ }
714
+ @if (action.element?.tagName) {
715
+ <span class="detail-label">Element</span>
716
+ <code class="detail-value">&lt;{{ action.element?.tagName }}&gt;</code>
717
+ }
718
+ <span class="detail-label">Strategie</span>
719
+ <span class="detail-value strategy-badge" [class]="'strat-' + action.selectorStrategy">
720
+ {{ action.selectorStrategy }}
721
+ </span>
722
+ <span class="detail-label">URL</span>
723
+ <code class="detail-value url-val">{{ action.url }}</code>
724
+ </div>
725
+ <div class="note-area">
726
+ <textarea
727
+ data-debug-panel
728
+ class="note-input"
729
+ [(ngModel)]="noteMap[action.id]"
730
+ placeholder="Notiz zu dieser Aktion..."
731
+ rows="2"
732
+ (blur)="onAddNote(action.id)"
733
+ ></textarea>
734
+ </div>
735
+ </div>
736
+ }
737
+ </div>
738
+ }
739
+ }
740
+ </div>
730
741
  `, styles: [".action-list{padding:0}.empty-state{text-align:center;padding:32px 20px;color:#64748b}.empty-icon{font-size:40px;margin-bottom:10px}.empty-state p{margin:4px 0;font-size:13px}.hint{font-size:11px;color:#475569}.shortcuts-hint{margin-top:12px;font-size:11px;color:#475569}kbd{background:#1e293b;border:1px solid #334155;color:#94a3b8;padding:2px 6px;border-radius:4px;font-size:10px}.list-header{display:flex;justify-content:space-between;padding:8px 14px;font-size:11px;color:#64748b;background:#0f172a;border-bottom:1px solid #1e293b;position:sticky;top:0}.action-item{border-bottom:1px solid #1e293b;transition:background .1s}.action-item:hover{background:#1e293b80}.action-item.expanded{background:#1e293b}.action-row{display:flex;align-items:center;padding:8px 10px;gap:8px;cursor:pointer}.action-index{color:#475569;font-size:10px;min-width:18px;text-align:right}.action-type-badge{font-size:14px;min-width:20px;text-align:center}.action-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}.action-desc{font-size:12px;color:#cbd5e1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.action-selector{font-size:10px;color:#64748b;font-family:Cascadia Code,Consolas,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.action-time{font-size:10px;color:#475569;white-space:nowrap}.remove-btn{background:none;border:none;color:#475569;cursor:pointer;font-size:12px;padding:2px 5px;border-radius:3px;opacity:0;transition:opacity .15s,color .15s}.action-row:hover .remove-btn{opacity:1}.remove-btn:hover{color:#f87171}.action-detail{padding:10px 14px;background:#0f172ab3;border-top:1px solid #1e293b}.detail-grid{display:grid;grid-template-columns:auto 1fr;gap:4px 10px;margin-bottom:8px;align-items:start}.detail-label{font-size:10px;color:#64748b;padding-top:2px;white-space:nowrap}.detail-value{font-size:11px;color:#93c5fd;font-family:Cascadia Code,Consolas,monospace;word-break:break-all}.url-val{color:#6ee7b7}.strategy-badge{font-size:10px;padding:1px 6px;border-radius:3px;font-family:monospace}.strat-data-testid,.strat-data-cy{background:#064e3b;color:#34d399}.strat-id{background:#1e3a8a;color:#93c5fd}.strat-name{background:#44337a;color:#c4b5fd}.strat-class{background:#374151;color:#9ca3af}.strat-combined{background:#292524;color:#d6d3d1}.note-area{margin-top:6px}.note-input{width:100%;box-sizing:border-box;background:#0f172a;border:1px solid #334155;color:#e2e8f0;border-radius:5px;padding:6px 8px;font-size:11px;resize:vertical}.note-input:focus{outline:none;border-color:#3b82f6}\n"] }]
731
742
  }], propDecorators: { session: [{
732
743
  type: Input
@@ -786,68 +797,68 @@ class TestPreviewComponent {
786
797
  });
787
798
  }
788
799
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TestPreviewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
789
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: TestPreviewComponent, isStandalone: true, selector: "app-test-preview", inputs: { test: "test" }, ngImport: i0, template: `
790
- <div class="test-preview" data-debug-panel>
791
- @if (!test) {
792
- <div class="empty-state">
793
- <div class="empty-icon">🤖</div>
794
- <p>Noch kein Test generiert.</p>
795
- <p class="hint">Zeichne Aktionen auf und klicke „🤖 → Cypress Test".</p>
796
- </div>
797
- } @else {
798
- <div class="test-toolbar" data-debug-panel>
799
- <div class="test-meta">
800
- <span class="model-tag">{{ test.model }}</span>
801
- <span class="gen-time">{{ formatDate(test.generatedAt) }}</span>
802
- </div>
803
- <div class="test-actions">
804
- <button class="action-btn" title="Kopieren" (click)="copyCode()">
805
- {{ copied() ? '✅ Kopiert!' : '📋 Kopieren' }}
806
- </button>
807
- <button class="action-btn" title="Herunterladen" (click)="downloadCode()">
808
- 💾 Download
809
- </button>
810
- </div>
811
- </div>
812
-
813
- <div class="code-container">
814
- <pre class="code-block"><code [innerHTML]="highlightedCode()"></code></pre>
815
- </div>
816
- }
817
- </div>
800
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: TestPreviewComponent, isStandalone: true, selector: "app-test-preview", inputs: { test: "test" }, ngImport: i0, template: `
801
+ <div class="test-preview" data-debug-panel>
802
+ @if (!test) {
803
+ <div class="empty-state">
804
+ <div class="empty-icon">🤖</div>
805
+ <p>Noch kein Test generiert.</p>
806
+ <p class="hint">Zeichne Aktionen auf und klicke „🤖 → Cypress Test".</p>
807
+ </div>
808
+ } @else {
809
+ <div class="test-toolbar" data-debug-panel>
810
+ <div class="test-meta">
811
+ <span class="model-tag">{{ test.model }}</span>
812
+ <span class="gen-time">{{ formatDate(test.generatedAt) }}</span>
813
+ </div>
814
+ <div class="test-actions">
815
+ <button class="action-btn" title="Kopieren" (click)="copyCode()">
816
+ {{ copied() ? '✅ Kopiert!' : '📋 Kopieren' }}
817
+ </button>
818
+ <button class="action-btn" title="Herunterladen" (click)="downloadCode()">
819
+ 💾 Download
820
+ </button>
821
+ </div>
822
+ </div>
823
+
824
+ <div class="code-container">
825
+ <pre class="code-block"><code [innerHTML]="highlightedCode()"></code></pre>
826
+ </div>
827
+ }
828
+ </div>
818
829
  `, isInline: true, styles: [".test-preview{height:100%;display:flex;flex-direction:column}.empty-state{text-align:center;padding:32px 20px;color:#64748b}.empty-icon{font-size:40px;margin-bottom:10px}.empty-state p{margin:4px 0;font-size:13px}.hint{font-size:11px;color:#475569}.test-toolbar{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;background:#1e293b;border-bottom:1px solid #334155;flex-shrink:0;gap:8px}.test-meta{display:flex;align-items:center;gap:8px;flex:1;min-width:0}.model-tag{background:#312e81;color:#a5b4fc;font-size:10px;padding:2px 7px;border-radius:4px;font-weight:600;white-space:nowrap}.gen-time{font-size:10px;color:#64748b;white-space:nowrap}.test-actions{display:flex;gap:6px;flex-shrink:0}.action-btn{background:#334155;border:none;color:#cbd5e1;padding:4px 10px;border-radius:5px;font-size:11px;cursor:pointer;white-space:nowrap;transition:background .15s}.action-btn:hover{background:#475569}.code-container{flex:1;overflow:auto}.code-container::-webkit-scrollbar{width:5px;height:5px}.code-container::-webkit-scrollbar-thumb{background:#334155;border-radius:3px}.code-block{margin:0;padding:14px;font-family:Cascadia Code,Consolas,Fira Code,monospace;font-size:11px;line-height:1.7;color:#e2e8f0;white-space:pre;tab-size:2}:global(.kw){color:#c084fc}:global(.str){color:#86efac}:global(.fn){color:#67e8f9}:global(.cm){color:#64748b;font-style:italic}:global(.num){color:#fb923c}:global(.cy){color:#fbbf24;font-weight:600}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] }); }
819
830
  }
820
831
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TestPreviewComponent, decorators: [{
821
832
  type: Component,
822
- args: [{ selector: 'app-test-preview', standalone: true, imports: [CommonModule], template: `
823
- <div class="test-preview" data-debug-panel>
824
- @if (!test) {
825
- <div class="empty-state">
826
- <div class="empty-icon">🤖</div>
827
- <p>Noch kein Test generiert.</p>
828
- <p class="hint">Zeichne Aktionen auf und klicke „🤖 → Cypress Test".</p>
829
- </div>
830
- } @else {
831
- <div class="test-toolbar" data-debug-panel>
832
- <div class="test-meta">
833
- <span class="model-tag">{{ test.model }}</span>
834
- <span class="gen-time">{{ formatDate(test.generatedAt) }}</span>
835
- </div>
836
- <div class="test-actions">
837
- <button class="action-btn" title="Kopieren" (click)="copyCode()">
838
- {{ copied() ? '✅ Kopiert!' : '📋 Kopieren' }}
839
- </button>
840
- <button class="action-btn" title="Herunterladen" (click)="downloadCode()">
841
- 💾 Download
842
- </button>
843
- </div>
844
- </div>
845
-
846
- <div class="code-container">
847
- <pre class="code-block"><code [innerHTML]="highlightedCode()"></code></pre>
848
- </div>
849
- }
850
- </div>
833
+ args: [{ selector: 'app-test-preview', standalone: true, imports: [CommonModule], template: `
834
+ <div class="test-preview" data-debug-panel>
835
+ @if (!test) {
836
+ <div class="empty-state">
837
+ <div class="empty-icon">🤖</div>
838
+ <p>Noch kein Test generiert.</p>
839
+ <p class="hint">Zeichne Aktionen auf und klicke „🤖 → Cypress Test".</p>
840
+ </div>
841
+ } @else {
842
+ <div class="test-toolbar" data-debug-panel>
843
+ <div class="test-meta">
844
+ <span class="model-tag">{{ test.model }}</span>
845
+ <span class="gen-time">{{ formatDate(test.generatedAt) }}</span>
846
+ </div>
847
+ <div class="test-actions">
848
+ <button class="action-btn" title="Kopieren" (click)="copyCode()">
849
+ {{ copied() ? '✅ Kopiert!' : '📋 Kopieren' }}
850
+ </button>
851
+ <button class="action-btn" title="Herunterladen" (click)="downloadCode()">
852
+ 💾 Download
853
+ </button>
854
+ </div>
855
+ </div>
856
+
857
+ <div class="code-container">
858
+ <pre class="code-block"><code [innerHTML]="highlightedCode()"></code></pre>
859
+ </div>
860
+ }
861
+ </div>
851
862
  `, styles: [".test-preview{height:100%;display:flex;flex-direction:column}.empty-state{text-align:center;padding:32px 20px;color:#64748b}.empty-icon{font-size:40px;margin-bottom:10px}.empty-state p{margin:4px 0;font-size:13px}.hint{font-size:11px;color:#475569}.test-toolbar{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;background:#1e293b;border-bottom:1px solid #334155;flex-shrink:0;gap:8px}.test-meta{display:flex;align-items:center;gap:8px;flex:1;min-width:0}.model-tag{background:#312e81;color:#a5b4fc;font-size:10px;padding:2px 7px;border-radius:4px;font-weight:600;white-space:nowrap}.gen-time{font-size:10px;color:#64748b;white-space:nowrap}.test-actions{display:flex;gap:6px;flex-shrink:0}.action-btn{background:#334155;border:none;color:#cbd5e1;padding:4px 10px;border-radius:5px;font-size:11px;cursor:pointer;white-space:nowrap;transition:background .15s}.action-btn:hover{background:#475569}.code-container{flex:1;overflow:auto}.code-container::-webkit-scrollbar{width:5px;height:5px}.code-container::-webkit-scrollbar-thumb{background:#334155;border-radius:3px}.code-block{margin:0;padding:14px;font-family:Cascadia Code,Consolas,Fira Code,monospace;font-size:11px;line-height:1.7;color:#e2e8f0;white-space:pre;tab-size:2}:global(.kw){color:#c084fc}:global(.str){color:#86efac}:global(.fn){color:#67e8f9}:global(.cm){color:#64748b;font-style:italic}:global(.num){color:#fb923c}:global(.cy){color:#fbbf24;font-weight:600}\n"] }]
852
863
  }], propDecorators: { test: [{
853
864
  type: Input
@@ -869,84 +880,84 @@ class SettingsDialogComponent {
869
880
  }
870
881
  }
871
882
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SettingsDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
872
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: SettingsDialogComponent, isStandalone: true, selector: "app-settings-dialog", outputs: { close: "close" }, ngImport: i0, template: `
873
- <div class="overlay" data-debug-panel (click)="onOverlayClick($event)">
874
- <div class="dialog" data-debug-panel>
875
- <div class="dialog-header">
876
- <h2>⚙️ Einstellungen</h2>
877
- <button class="close-btn" (click)="close.emit()">✕</button>
878
- </div>
879
-
880
- <div class="dialog-body">
881
- <div class="field-group">
882
- <label>KI Webhook-URL</label>
883
- <input
884
- data-debug-panel
885
- class="field-input"
886
- type="url"
887
- [(ngModel)]="localUrl"
888
- placeholder="https://deine-ki-api.de/generate"
889
- />
890
- <p class="field-hint">
891
- Die aufgezeichnete Session wird als JSON per POST an diese URL gesendet.
892
- Die Antwort wird als Cypress-Test angezeigt.
893
- </p>
894
- </div>
895
-
896
- <div class="info-box">
897
- <p>📦 POST-Body: Die Session als JSON (<code>actions</code>, <code>startUrl</code>, ...)</p>
898
- <p>📄 Erwartete Antwort: Cypress-Test-Code als Plain-Text</p>
899
- <p>⌨️ Shortcuts: <kbd>Ctrl+Shift+D</kbd> Panel &nbsp; <kbd>Ctrl+Shift+R</kbd> Aufnahme</p>
900
- </div>
901
- </div>
902
-
903
- <div class="dialog-footer">
904
- <button class="btn-cancel" (click)="close.emit()">Abbrechen</button>
905
- <button class="btn-save" (click)="save()">💾 Speichern</button>
906
- </div>
907
- </div>
908
- </div>
883
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: SettingsDialogComponent, isStandalone: true, selector: "app-settings-dialog", outputs: { close: "close" }, ngImport: i0, template: `
884
+ <div class="overlay" data-debug-panel (click)="onOverlayClick($event)">
885
+ <div class="dialog" data-debug-panel>
886
+ <div class="dialog-header">
887
+ <h2>⚙️ Einstellungen</h2>
888
+ <button class="close-btn" (click)="close.emit()">✕</button>
889
+ </div>
890
+
891
+ <div class="dialog-body">
892
+ <div class="field-group">
893
+ <label>KI Webhook-URL</label>
894
+ <input
895
+ data-debug-panel
896
+ class="field-input"
897
+ type="url"
898
+ [(ngModel)]="localUrl"
899
+ placeholder="https://deine-ki-api.de/generate"
900
+ />
901
+ <p class="field-hint">
902
+ Die aufgezeichnete Session wird als JSON per POST an diese URL gesendet.
903
+ Die Antwort wird als Cypress-Test angezeigt.
904
+ </p>
905
+ </div>
906
+
907
+ <div class="info-box">
908
+ <p>📦 POST-Body: Die Session als JSON (<code>actions</code>, <code>startUrl</code>, ...)</p>
909
+ <p>📄 Erwartete Antwort: Cypress-Test-Code als Plain-Text</p>
910
+ <p>⌨️ Shortcuts: <kbd>Ctrl+Shift+D</kbd> Panel &nbsp; <kbd>Ctrl+Shift+R</kbd> Aufnahme</p>
911
+ </div>
912
+ </div>
913
+
914
+ <div class="dialog-footer">
915
+ <button class="btn-cancel" (click)="close.emit()">Abbrechen</button>
916
+ <button class="btn-save" (click)="save()">💾 Speichern</button>
917
+ </div>
918
+ </div>
919
+ </div>
909
920
  `, isInline: true, styles: [".overlay{position:fixed;inset:0;background:#000000b3;z-index:100000;display:flex;align-items:center;justify-content:center;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.dialog{background:#0f172a;border:1px solid #1e3a5f;border-radius:12px;width:440px;max-width:95vw;display:flex;flex-direction:column;box-shadow:0 25px 60px #0009;overflow:hidden}.dialog-header{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;background:#1e293b;border-bottom:1px solid #334155}.dialog-header h2{margin:0;font-size:15px;color:#f1f5f9;font-weight:600}.close-btn{background:none;border:none;color:#94a3b8;cursor:pointer;font-size:16px;padding:4px;border-radius:4px}.close-btn:hover{color:#f1f5f9;background:#334155}.dialog-body{padding:18px;display:flex;flex-direction:column;gap:16px}.field-group{display:flex;flex-direction:column;gap:6px}label{font-size:12px;font-weight:600;color:#94a3b8;text-transform:uppercase;letter-spacing:.05em}.field-input{background:#1e293b;border:1px solid #334155;color:#e2e8f0;border-radius:6px;padding:10px 12px;font-size:13px;width:100%;box-sizing:border-box}.field-input:focus{outline:none;border-color:#3b82f6}.field-hint{font-size:11px;color:#64748b;margin:0;line-height:1.5}.info-box{background:#1e293b80;border:1px solid #1e3a5f;border-radius:8px;padding:12px;display:flex;flex-direction:column;gap:6px}.info-box p{margin:0;font-size:11px;color:#64748b}.info-box code{background:#1e293b;color:#34d399;padding:1px 4px;border-radius:3px;font-family:monospace}kbd{background:#1e293b;border:1px solid #334155;color:#94a3b8;padding:1px 5px;border-radius:3px;font-size:10px}.dialog-footer{display:flex;gap:8px;justify-content:flex-end;padding:14px 18px;background:#1e293b;border-top:1px solid #334155}.btn-cancel,.btn-save{padding:8px 20px;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;border:none;transition:filter .15s}.btn-cancel{background:#334155;color:#94a3b8}.btn-cancel:hover{filter:brightness(1.2)}.btn-save{background:#2563eb;color:#fff}.btn-save:hover{filter:brightness(1.1)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { 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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] }); }
910
921
  }
911
922
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SettingsDialogComponent, decorators: [{
912
923
  type: Component,
913
- args: [{ selector: 'app-settings-dialog', standalone: true, imports: [CommonModule, FormsModule], template: `
914
- <div class="overlay" data-debug-panel (click)="onOverlayClick($event)">
915
- <div class="dialog" data-debug-panel>
916
- <div class="dialog-header">
917
- <h2>⚙️ Einstellungen</h2>
918
- <button class="close-btn" (click)="close.emit()">✕</button>
919
- </div>
920
-
921
- <div class="dialog-body">
922
- <div class="field-group">
923
- <label>KI Webhook-URL</label>
924
- <input
925
- data-debug-panel
926
- class="field-input"
927
- type="url"
928
- [(ngModel)]="localUrl"
929
- placeholder="https://deine-ki-api.de/generate"
930
- />
931
- <p class="field-hint">
932
- Die aufgezeichnete Session wird als JSON per POST an diese URL gesendet.
933
- Die Antwort wird als Cypress-Test angezeigt.
934
- </p>
935
- </div>
936
-
937
- <div class="info-box">
938
- <p>📦 POST-Body: Die Session als JSON (<code>actions</code>, <code>startUrl</code>, ...)</p>
939
- <p>📄 Erwartete Antwort: Cypress-Test-Code als Plain-Text</p>
940
- <p>⌨️ Shortcuts: <kbd>Ctrl+Shift+D</kbd> Panel &nbsp; <kbd>Ctrl+Shift+R</kbd> Aufnahme</p>
941
- </div>
942
- </div>
943
-
944
- <div class="dialog-footer">
945
- <button class="btn-cancel" (click)="close.emit()">Abbrechen</button>
946
- <button class="btn-save" (click)="save()">💾 Speichern</button>
947
- </div>
948
- </div>
949
- </div>
924
+ args: [{ selector: 'app-settings-dialog', standalone: true, imports: [CommonModule, FormsModule], template: `
925
+ <div class="overlay" data-debug-panel (click)="onOverlayClick($event)">
926
+ <div class="dialog" data-debug-panel>
927
+ <div class="dialog-header">
928
+ <h2>⚙️ Einstellungen</h2>
929
+ <button class="close-btn" (click)="close.emit()">✕</button>
930
+ </div>
931
+
932
+ <div class="dialog-body">
933
+ <div class="field-group">
934
+ <label>KI Webhook-URL</label>
935
+ <input
936
+ data-debug-panel
937
+ class="field-input"
938
+ type="url"
939
+ [(ngModel)]="localUrl"
940
+ placeholder="https://deine-ki-api.de/generate"
941
+ />
942
+ <p class="field-hint">
943
+ Die aufgezeichnete Session wird als JSON per POST an diese URL gesendet.
944
+ Die Antwort wird als Cypress-Test angezeigt.
945
+ </p>
946
+ </div>
947
+
948
+ <div class="info-box">
949
+ <p>📦 POST-Body: Die Session als JSON (<code>actions</code>, <code>startUrl</code>, ...)</p>
950
+ <p>📄 Erwartete Antwort: Cypress-Test-Code als Plain-Text</p>
951
+ <p>⌨️ Shortcuts: <kbd>Ctrl+Shift+D</kbd> Panel &nbsp; <kbd>Ctrl+Shift+R</kbd> Aufnahme</p>
952
+ </div>
953
+ </div>
954
+
955
+ <div class="dialog-footer">
956
+ <button class="btn-cancel" (click)="close.emit()">Abbrechen</button>
957
+ <button class="btn-save" (click)="save()">💾 Speichern</button>
958
+ </div>
959
+ </div>
960
+ </div>
950
961
  `, styles: [".overlay{position:fixed;inset:0;background:#000000b3;z-index:100000;display:flex;align-items:center;justify-content:center;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.dialog{background:#0f172a;border:1px solid #1e3a5f;border-radius:12px;width:440px;max-width:95vw;display:flex;flex-direction:column;box-shadow:0 25px 60px #0009;overflow:hidden}.dialog-header{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;background:#1e293b;border-bottom:1px solid #334155}.dialog-header h2{margin:0;font-size:15px;color:#f1f5f9;font-weight:600}.close-btn{background:none;border:none;color:#94a3b8;cursor:pointer;font-size:16px;padding:4px;border-radius:4px}.close-btn:hover{color:#f1f5f9;background:#334155}.dialog-body{padding:18px;display:flex;flex-direction:column;gap:16px}.field-group{display:flex;flex-direction:column;gap:6px}label{font-size:12px;font-weight:600;color:#94a3b8;text-transform:uppercase;letter-spacing:.05em}.field-input{background:#1e293b;border:1px solid #334155;color:#e2e8f0;border-radius:6px;padding:10px 12px;font-size:13px;width:100%;box-sizing:border-box}.field-input:focus{outline:none;border-color:#3b82f6}.field-hint{font-size:11px;color:#64748b;margin:0;line-height:1.5}.info-box{background:#1e293b80;border:1px solid #1e3a5f;border-radius:8px;padding:12px;display:flex;flex-direction:column;gap:6px}.info-box p{margin:0;font-size:11px;color:#64748b}.info-box code{background:#1e293b;color:#34d399;padding:1px 4px;border-radius:3px;font-family:monospace}kbd{background:#1e293b;border:1px solid #334155;color:#94a3b8;padding:1px 5px;border-radius:3px;font-size:10px}.dialog-footer{display:flex;gap:8px;justify-content:flex-end;padding:14px 18px;background:#1e293b;border-top:1px solid #334155}.btn-cancel,.btn-save{padding:8px 20px;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;border:none;transition:filter .15s}.btn-cancel{background:#334155;color:#94a3b8}.btn-cancel:hover{filter:brightness(1.2)}.btn-save{background:#2563eb;color:#fff}.btn-save:hover{filter:brightness(1.1)}\n"] }]
951
962
  }], propDecorators: { close: [{
952
963
  type: Output
@@ -1020,94 +1031,94 @@ class SessionReplayComponent {
1020
1031
  this.rrweb.destroyReplayer();
1021
1032
  }
1022
1033
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SessionReplayComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1023
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: SessionReplayComponent, isStandalone: true, selector: "app-session-replay", viewQueries: [{ propertyName: "replayContainer", first: true, predicate: ["replayContainer"], descendants: true }], ngImport: i0, template: `
1024
- <div class="replay-panel" data-debug-panel>
1025
- @if (!rrweb.hasEvents()) {
1026
- <div class="replay-empty">
1027
- <div class="replay-icon">📽️</div>
1028
- <p>Kein Replay verfügbar.</p>
1029
- <p class="hint">Starte eine Aufnahme — rrweb zeichnet den DOM parallel mit.</p>
1030
- </div>
1031
- } @else {
1032
- <div class="replay-info">
1033
- <span class="event-count">{{ rrweb.events().length }} Events aufgezeichnet</span>
1034
- </div>
1035
- <div class="replay-actions">
1036
- <button class="replay-btn primary" (click)="openOverlay()">
1037
- ▶ Replay abspielen
1038
- </button>
1039
- <button class="replay-btn" (click)="exportSession()">
1040
- 💾 JSON exportieren
1041
- </button>
1042
- </div>
1043
- <p class="replay-hint">
1044
- Der Replay öffnet sich als Vollbild-Overlay über der aktuellen Seite.
1045
- </p>
1046
- }
1047
- </div>
1048
-
1049
- <!-- Fullscreen Replay Overlay -->
1050
- @if (overlayOpen()) {
1051
- <div class="replay-overlay" data-debug-panel>
1052
- <div class="overlay-header" data-debug-panel>
1053
- <span class="overlay-title">📽️ Session Replay</span>
1054
- <div class="overlay-controls" data-debug-panel>
1055
- <button class="ovl-btn" (click)="pauseResume()">
1056
- {{ isPlaying() ? '⏸ Pause' : '▶ Play' }}
1057
- </button>
1058
- <button class="ovl-btn" (click)="restart()">⟳ Neustart</button>
1059
- <button class="ovl-btn close-ovl" (click)="closeOverlay()">✕ Schließen</button>
1060
- </div>
1061
- </div>
1062
- <div #replayContainer class="overlay-stage" data-debug-panel></div>
1063
- </div>
1064
- }
1034
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: SessionReplayComponent, isStandalone: true, selector: "app-session-replay", viewQueries: [{ propertyName: "replayContainer", first: true, predicate: ["replayContainer"], descendants: true }], ngImport: i0, template: `
1035
+ <div class="replay-panel" data-debug-panel>
1036
+ @if (!rrweb.hasEvents()) {
1037
+ <div class="replay-empty">
1038
+ <div class="replay-icon">📽️</div>
1039
+ <p>Kein Replay verfügbar.</p>
1040
+ <p class="hint">Starte eine Aufnahme — rrweb zeichnet den DOM parallel mit.</p>
1041
+ </div>
1042
+ } @else {
1043
+ <div class="replay-info">
1044
+ <span class="event-count">{{ rrweb.eventCount() }} Events aufgezeichnet</span>
1045
+ </div>
1046
+ <div class="replay-actions">
1047
+ <button class="replay-btn primary" (click)="openOverlay()">
1048
+ ▶ Replay abspielen
1049
+ </button>
1050
+ <button class="replay-btn" (click)="exportSession()">
1051
+ 💾 JSON exportieren
1052
+ </button>
1053
+ </div>
1054
+ <p class="replay-hint">
1055
+ Der Replay öffnet sich als Vollbild-Overlay über der aktuellen Seite.
1056
+ </p>
1057
+ }
1058
+ </div>
1059
+
1060
+ <!-- Fullscreen Replay Overlay -->
1061
+ @if (overlayOpen()) {
1062
+ <div class="replay-overlay" data-debug-panel>
1063
+ <div class="overlay-header" data-debug-panel>
1064
+ <span class="overlay-title">📽️ Session Replay</span>
1065
+ <div class="overlay-controls" data-debug-panel>
1066
+ <button class="ovl-btn" (click)="pauseResume()">
1067
+ {{ isPlaying() ? '⏸ Pause' : '▶ Play' }}
1068
+ </button>
1069
+ <button class="ovl-btn" (click)="restart()">⟳ Neustart</button>
1070
+ <button class="ovl-btn close-ovl" (click)="closeOverlay()">✕ Schließen</button>
1071
+ </div>
1072
+ </div>
1073
+ <div #replayContainer class="overlay-stage" data-debug-panel></div>
1074
+ </div>
1075
+ }
1065
1076
  `, isInline: true, styles: [".replay-panel{padding:20px 16px;display:flex;flex-direction:column;gap:14px}.replay-empty{text-align:center;padding:20px 0;color:#64748b}.replay-icon{font-size:36px;margin-bottom:8px}.replay-empty p{margin:4px 0;font-size:13px}.hint{font-size:11px;color:#475569}.replay-info{background:#1e293b;border-radius:6px;padding:8px 12px}.event-count{font-size:12px;color:#6ee7b7;font-weight:600}.replay-actions{display:flex;gap:8px}.replay-btn{background:#334155;border:none;color:#cbd5e1;padding:8px 14px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;transition:background .15s}.replay-btn:hover{background:#475569}.replay-btn.primary{background:#1d4ed8;color:#fff}.replay-btn.primary:hover{background:#2563eb}.replay-hint{font-size:11px;color:#475569;margin:0}.replay-overlay{position:fixed;inset:0;z-index:99997;background:#000;display:flex;flex-direction:column}.overlay-header{display:flex;align-items:center;justify-content:space-between;padding:10px 16px;background:#0f172a;border-bottom:1px solid #1e293b;flex-shrink:0}.overlay-title{font-size:14px;font-weight:600;color:#f1f5f9}.overlay-controls{display:flex;gap:8px}.ovl-btn{background:#1e293b;border:1px solid #334155;color:#cbd5e1;padding:5px 12px;border-radius:5px;font-size:12px;font-weight:600;cursor:pointer;transition:background .15s}.ovl-btn:hover{background:#334155}.close-ovl{color:#fca5a5}.close-ovl:hover{background:#dc262633}.overlay-stage{flex:1;overflow:hidden;position:relative;background:#f8fafc}:host ::ng-deep .replayer-wrapper{position:absolute!important;top:0!important;left:0!important;transform-origin:top left!important}:host ::ng-deep .replayer-wrapper iframe{pointer-events:none}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] }); }
1066
1077
  }
1067
1078
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SessionReplayComponent, decorators: [{
1068
1079
  type: Component,
1069
- args: [{ selector: 'app-session-replay', standalone: true, imports: [CommonModule], template: `
1070
- <div class="replay-panel" data-debug-panel>
1071
- @if (!rrweb.hasEvents()) {
1072
- <div class="replay-empty">
1073
- <div class="replay-icon">📽️</div>
1074
- <p>Kein Replay verfügbar.</p>
1075
- <p class="hint">Starte eine Aufnahme — rrweb zeichnet den DOM parallel mit.</p>
1076
- </div>
1077
- } @else {
1078
- <div class="replay-info">
1079
- <span class="event-count">{{ rrweb.events().length }} Events aufgezeichnet</span>
1080
- </div>
1081
- <div class="replay-actions">
1082
- <button class="replay-btn primary" (click)="openOverlay()">
1083
- ▶ Replay abspielen
1084
- </button>
1085
- <button class="replay-btn" (click)="exportSession()">
1086
- 💾 JSON exportieren
1087
- </button>
1088
- </div>
1089
- <p class="replay-hint">
1090
- Der Replay öffnet sich als Vollbild-Overlay über der aktuellen Seite.
1091
- </p>
1092
- }
1093
- </div>
1094
-
1095
- <!-- Fullscreen Replay Overlay -->
1096
- @if (overlayOpen()) {
1097
- <div class="replay-overlay" data-debug-panel>
1098
- <div class="overlay-header" data-debug-panel>
1099
- <span class="overlay-title">📽️ Session Replay</span>
1100
- <div class="overlay-controls" data-debug-panel>
1101
- <button class="ovl-btn" (click)="pauseResume()">
1102
- {{ isPlaying() ? '⏸ Pause' : '▶ Play' }}
1103
- </button>
1104
- <button class="ovl-btn" (click)="restart()">⟳ Neustart</button>
1105
- <button class="ovl-btn close-ovl" (click)="closeOverlay()">✕ Schließen</button>
1106
- </div>
1107
- </div>
1108
- <div #replayContainer class="overlay-stage" data-debug-panel></div>
1109
- </div>
1110
- }
1080
+ args: [{ selector: 'app-session-replay', standalone: true, imports: [CommonModule], template: `
1081
+ <div class="replay-panel" data-debug-panel>
1082
+ @if (!rrweb.hasEvents()) {
1083
+ <div class="replay-empty">
1084
+ <div class="replay-icon">📽️</div>
1085
+ <p>Kein Replay verfügbar.</p>
1086
+ <p class="hint">Starte eine Aufnahme — rrweb zeichnet den DOM parallel mit.</p>
1087
+ </div>
1088
+ } @else {
1089
+ <div class="replay-info">
1090
+ <span class="event-count">{{ rrweb.eventCount() }} Events aufgezeichnet</span>
1091
+ </div>
1092
+ <div class="replay-actions">
1093
+ <button class="replay-btn primary" (click)="openOverlay()">
1094
+ ▶ Replay abspielen
1095
+ </button>
1096
+ <button class="replay-btn" (click)="exportSession()">
1097
+ 💾 JSON exportieren
1098
+ </button>
1099
+ </div>
1100
+ <p class="replay-hint">
1101
+ Der Replay öffnet sich als Vollbild-Overlay über der aktuellen Seite.
1102
+ </p>
1103
+ }
1104
+ </div>
1105
+
1106
+ <!-- Fullscreen Replay Overlay -->
1107
+ @if (overlayOpen()) {
1108
+ <div class="replay-overlay" data-debug-panel>
1109
+ <div class="overlay-header" data-debug-panel>
1110
+ <span class="overlay-title">📽️ Session Replay</span>
1111
+ <div class="overlay-controls" data-debug-panel>
1112
+ <button class="ovl-btn" (click)="pauseResume()">
1113
+ {{ isPlaying() ? '⏸ Pause' : '▶ Play' }}
1114
+ </button>
1115
+ <button class="ovl-btn" (click)="restart()">⟳ Neustart</button>
1116
+ <button class="ovl-btn close-ovl" (click)="closeOverlay()">✕ Schließen</button>
1117
+ </div>
1118
+ </div>
1119
+ <div #replayContainer class="overlay-stage" data-debug-panel></div>
1120
+ </div>
1121
+ }
1111
1122
  `, styles: [".replay-panel{padding:20px 16px;display:flex;flex-direction:column;gap:14px}.replay-empty{text-align:center;padding:20px 0;color:#64748b}.replay-icon{font-size:36px;margin-bottom:8px}.replay-empty p{margin:4px 0;font-size:13px}.hint{font-size:11px;color:#475569}.replay-info{background:#1e293b;border-radius:6px;padding:8px 12px}.event-count{font-size:12px;color:#6ee7b7;font-weight:600}.replay-actions{display:flex;gap:8px}.replay-btn{background:#334155;border:none;color:#cbd5e1;padding:8px 14px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;transition:background .15s}.replay-btn:hover{background:#475569}.replay-btn.primary{background:#1d4ed8;color:#fff}.replay-btn.primary:hover{background:#2563eb}.replay-hint{font-size:11px;color:#475569;margin:0}.replay-overlay{position:fixed;inset:0;z-index:99997;background:#000;display:flex;flex-direction:column}.overlay-header{display:flex;align-items:center;justify-content:space-between;padding:10px 16px;background:#0f172a;border-bottom:1px solid #1e293b;flex-shrink:0}.overlay-title{font-size:14px;font-weight:600;color:#f1f5f9}.overlay-controls{display:flex;gap:8px}.ovl-btn{background:#1e293b;border:1px solid #334155;color:#cbd5e1;padding:5px 12px;border-radius:5px;font-size:12px;font-weight:600;cursor:pointer;transition:background .15s}.ovl-btn:hover{background:#334155}.close-ovl{color:#fca5a5}.close-ovl:hover{background:#dc262633}.overlay-stage{flex:1;overflow:hidden;position:relative;background:#f8fafc}:host ::ng-deep .replayer-wrapper{position:absolute!important;top:0!important;left:0!important;transform-origin:top left!important}:host ::ng-deep .replayer-wrapper iframe{pointer-events:none}\n"] }]
1112
1123
  }], propDecorators: { replayContainer: [{
1113
1124
  type: ViewChild,
@@ -1227,394 +1238,394 @@ class DebugPanelComponent {
1227
1238
  e.preventDefault();
1228
1239
  }
1229
1240
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DebugPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1230
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: DebugPanelComponent, isStandalone: true, selector: "app-debug-panel", ngImport: i0, template: `
1231
- <!-- Toggle FAB -->
1232
- <button
1233
- data-debug-panel
1234
- class="debug-fab"
1235
- [class.recording]="recorder.isRecording()"
1236
- [class.pulse]="recorder.isRecording() && !recorder.isPaused()"
1237
- (click)="togglePanel()"
1238
- [title]="panelOpen() ? 'Debug Panel schließen' : 'Debug Panel öffnen'"
1239
- >
1240
- <span class="fab-icon">{{ recorder.isRecording() ? '⏺' : '🐛' }}</span>
1241
- @if (recorder.isRecording() && recorder.actionCount() > 0) {
1242
- <span class="fab-badge">{{ recorder.actionCount() }}</span>
1243
- }
1244
- </button>
1245
-
1246
- <!-- Main Panel -->
1247
- @if (panelOpen()) {
1248
- <div
1249
- data-debug-panel
1250
- class="debug-panel"
1251
- [class]="'pos-' + position()"
1252
- [style.width.px]="panelWidth()"
1253
- >
1254
- <!-- Header -->
1255
- <div class="panel-header" (mousedown)="startDrag($event)">
1256
- <div class="header-left">
1257
- <span class="panel-icon">🐛</span>
1258
- <span class="panel-title">Debug Recorder</span>
1259
- @if (recorder.isRecording()) {
1260
- <span class="rec-indicator" [class.paused]="recorder.isPaused()">
1261
- {{ recorder.isPaused() ? '⏸ PAUSED' : '⏺ REC' }}
1262
- </span>
1263
- }
1264
- </div>
1265
- <div class="header-actions">
1266
- <button class="icon-btn" title="Einstellungen" (click)="showSettings.set(true)">⚙️</button>
1267
- <button class="icon-btn" title="Position wechseln" (click)="cyclePosition()">📌</button>
1268
- <button class="icon-btn" title="Schließen" (click)="togglePanel()">✕</button>
1269
- </div>
1270
- </div>
1271
-
1272
- <!-- Session Name Input (only when not recording) -->
1273
- @if (!recorder.isRecording()) {
1274
- <div class="session-setup">
1275
- <input
1276
- data-debug-panel
1277
- class="session-input"
1278
- type="text"
1279
- [(ngModel)]="sessionName"
1280
- placeholder="Session-Name (optional)"
1281
- />
1282
- <textarea
1283
- data-debug-panel
1284
- class="session-desc"
1285
- [(ngModel)]="sessionDesc"
1286
- placeholder="Fehlerbeschreibung..."
1287
- rows="2"
1288
- ></textarea>
1289
- </div>
1290
- }
1291
-
1292
- <!-- Recording Controls -->
1293
- <div class="recording-controls" data-debug-panel>
1294
- @if (!recorder.isRecording()) {
1295
- <button class="ctrl-btn start" (click)="startRecording()">
1296
- ▶ Aufnahme starten
1297
- </button>
1298
- } @else {
1299
- <button class="ctrl-btn pause" (click)="recorder.pauseRecording()">
1300
- {{ recorder.isPaused() ? '▶ Fortsetzen' : '⏸ Pause' }}
1301
- </button>
1302
- <button class="ctrl-btn stop" (click)="stopRecording()">
1303
- ⏹ Stoppen
1304
- </button>
1305
- <button class="ctrl-btn clear" title="Aktionen löschen" (click)="recorder.clearCurrentSession()">
1306
- 🗑
1307
- </button>
1308
- }
1309
-
1310
- @if (hasActions()) {
1311
- <button
1312
- class="ctrl-btn generate"
1313
- [disabled]="aiService.isGenerating() || !aiService.webhookUrl()"
1314
- [title]="!aiService.webhookUrl() ? 'Webhook-URL in Einstellungen eintragen' : 'Cypress Test generieren'"
1315
- (click)="generateTest()"
1316
- >
1317
- @if (aiService.isGenerating()) {
1318
- <span class="spinner">⟳</span> Generiere...
1319
- } @else {
1320
- 🤖 → Cypress Test
1321
- }
1322
- </button>
1323
- }
1324
- </div>
1325
-
1326
- <!-- Error Banner -->
1327
- @if (aiService.error()) {
1328
- <div class="error-banner" data-debug-panel>
1329
- ⚠️ {{ aiService.error() }}
1330
- </div>
1331
- }
1332
-
1333
- <!-- Tabs -->
1334
- <div class="tab-bar" data-debug-panel>
1335
- <button
1336
- class="tab-btn"
1337
- [class.active]="activeTab() === 'actions'"
1338
- (click)="activeTab.set('actions')"
1339
- >
1340
- Aktionen
1341
- @if (recorder.actionCount() > 0) {
1342
- <span class="tab-badge">{{ recorder.actionCount() }}</span>
1343
- }
1344
- </button>
1345
- <button
1346
- class="tab-btn"
1347
- [class.active]="activeTab() === 'replay'"
1348
- (click)="activeTab.set('replay')"
1349
- title="rrweb Session Replay"
1350
- >
1351
- 📽️ Replay
1352
- @if (rrweb.events().length > 0) {
1353
- <span class="tab-badge">{{ rrweb.events().length }}</span>
1354
- }
1355
- </button>
1356
- <button
1357
- class="tab-btn"
1358
- [class.active]="activeTab() === 'test'"
1359
- [disabled]="!aiService.lastTest()"
1360
- (click)="activeTab.set('test')"
1361
- >
1362
- 🤖 Test
1363
- @if (aiService.lastTest()) {
1364
- <span class="tab-badge new">NEU</span>
1365
- }
1366
- </button>
1367
- <button
1368
- class="tab-btn"
1369
- [class.active]="activeTab() === 'sessions'"
1370
- [disabled]="recorder.sessions().length === 0"
1371
- (click)="activeTab.set('sessions')"
1372
- >
1373
- Sessions ({{ recorder.sessions().length }})
1374
- </button>
1375
- </div>
1376
-
1377
- <!-- Tab Content -->
1378
- <div class="tab-content">
1379
- @if (activeTab() === 'actions') {
1380
- <app-action-list
1381
- [session]="recorder.currentSession()"
1382
- (removeAction)="recorder.removeAction($event)"
1383
- (addNote)="recorder.addNote($event.id, $event.note)"
1384
- />
1385
- }
1386
-
1387
- @if (activeTab() === 'replay') {
1388
- <app-session-replay />
1389
- }
1390
-
1391
- @if (activeTab() === 'test') {
1392
- <app-test-preview [test]="aiService.lastTest()" />
1393
- }
1394
-
1395
- @if (activeTab() === 'sessions') {
1396
- <div class="sessions-list">
1397
- @for (session of recorder.sessions(); track session.id) {
1398
- <div class="session-card">
1399
- <div class="session-card-header">
1400
- <span class="session-name">{{ session.name }}</span>
1401
- <span class="session-meta">{{ session.actions.length }} Aktionen</span>
1402
- </div>
1403
- <div class="session-card-actions">
1404
- <button class="sm-btn" (click)="loadAndGenerate(session)">🤖 Neu generieren</button>
1405
- <button class="sm-btn danger" (click)="recorder.deleteSession(session.id)">🗑</button>
1406
- </div>
1407
- </div>
1408
- }
1409
- </div>
1410
- }
1411
- </div>
1412
-
1413
- <!-- Resize Handle -->
1414
- <div class="resize-handle" (mousedown)="startResize($event)">⠿</div>
1415
- </div>
1416
- }
1417
-
1418
- <!-- Settings Dialog -->
1419
- @if (showSettings()) {
1420
- <app-settings-dialog (close)="showSettings.set(false)" />
1421
- }
1241
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: DebugPanelComponent, isStandalone: true, selector: "app-debug-panel", ngImport: i0, template: `
1242
+ <!-- Toggle FAB -->
1243
+ <button
1244
+ data-debug-panel
1245
+ class="debug-fab"
1246
+ [class.recording]="recorder.isRecording()"
1247
+ [class.pulse]="recorder.isRecording() && !recorder.isPaused()"
1248
+ (click)="togglePanel()"
1249
+ [title]="panelOpen() ? 'Debug Panel schließen' : 'Debug Panel öffnen'"
1250
+ >
1251
+ <span class="fab-icon">{{ recorder.isRecording() ? '⏺' : '🐛' }}</span>
1252
+ @if (recorder.isRecording() && recorder.actionCount() > 0) {
1253
+ <span class="fab-badge">{{ recorder.actionCount() }}</span>
1254
+ }
1255
+ </button>
1256
+
1257
+ <!-- Main Panel -->
1258
+ @if (panelOpen()) {
1259
+ <div
1260
+ data-debug-panel
1261
+ class="debug-panel"
1262
+ [class]="'pos-' + position()"
1263
+ [style.width.px]="panelWidth()"
1264
+ >
1265
+ <!-- Header -->
1266
+ <div class="panel-header" (mousedown)="startDrag($event)">
1267
+ <div class="header-left">
1268
+ <span class="panel-icon">🐛</span>
1269
+ <span class="panel-title">Debug Recorder</span>
1270
+ @if (recorder.isRecording()) {
1271
+ <span class="rec-indicator" [class.paused]="recorder.isPaused()">
1272
+ {{ recorder.isPaused() ? '⏸ PAUSED' : '⏺ REC' }}
1273
+ </span>
1274
+ }
1275
+ </div>
1276
+ <div class="header-actions">
1277
+ <button class="icon-btn" title="Einstellungen" (click)="showSettings.set(true)">⚙️</button>
1278
+ <button class="icon-btn" title="Position wechseln" (click)="cyclePosition()">📌</button>
1279
+ <button class="icon-btn" title="Schließen" (click)="togglePanel()">✕</button>
1280
+ </div>
1281
+ </div>
1282
+
1283
+ <!-- Session Name Input (only when not recording) -->
1284
+ @if (!recorder.isRecording()) {
1285
+ <div class="session-setup">
1286
+ <input
1287
+ data-debug-panel
1288
+ class="session-input"
1289
+ type="text"
1290
+ [(ngModel)]="sessionName"
1291
+ placeholder="Session-Name (optional)"
1292
+ />
1293
+ <textarea
1294
+ data-debug-panel
1295
+ class="session-desc"
1296
+ [(ngModel)]="sessionDesc"
1297
+ placeholder="Fehlerbeschreibung..."
1298
+ rows="2"
1299
+ ></textarea>
1300
+ </div>
1301
+ }
1302
+
1303
+ <!-- Recording Controls -->
1304
+ <div class="recording-controls" data-debug-panel>
1305
+ @if (!recorder.isRecording()) {
1306
+ <button class="ctrl-btn start" (click)="startRecording()">
1307
+ ▶ Aufnahme starten
1308
+ </button>
1309
+ } @else {
1310
+ <button class="ctrl-btn pause" (click)="recorder.pauseRecording()">
1311
+ {{ recorder.isPaused() ? '▶ Fortsetzen' : '⏸ Pause' }}
1312
+ </button>
1313
+ <button class="ctrl-btn stop" (click)="stopRecording()">
1314
+ ⏹ Stoppen
1315
+ </button>
1316
+ <button class="ctrl-btn clear" title="Aktionen löschen" (click)="recorder.clearCurrentSession()">
1317
+ 🗑
1318
+ </button>
1319
+ }
1320
+
1321
+ @if (hasActions()) {
1322
+ <button
1323
+ class="ctrl-btn generate"
1324
+ [disabled]="aiService.isGenerating() || !aiService.webhookUrl()"
1325
+ [title]="!aiService.webhookUrl() ? 'Webhook-URL in Einstellungen eintragen' : 'Cypress Test generieren'"
1326
+ (click)="generateTest()"
1327
+ >
1328
+ @if (aiService.isGenerating()) {
1329
+ <span class="spinner">⟳</span> Generiere...
1330
+ } @else {
1331
+ 🤖 → Cypress Test
1332
+ }
1333
+ </button>
1334
+ }
1335
+ </div>
1336
+
1337
+ <!-- Error Banner -->
1338
+ @if (aiService.error()) {
1339
+ <div class="error-banner" data-debug-panel>
1340
+ ⚠️ {{ aiService.error() }}
1341
+ </div>
1342
+ }
1343
+
1344
+ <!-- Tabs -->
1345
+ <div class="tab-bar" data-debug-panel>
1346
+ <button
1347
+ class="tab-btn"
1348
+ [class.active]="activeTab() === 'actions'"
1349
+ (click)="activeTab.set('actions')"
1350
+ >
1351
+ Aktionen
1352
+ @if (recorder.actionCount() > 0) {
1353
+ <span class="tab-badge">{{ recorder.actionCount() }}</span>
1354
+ }
1355
+ </button>
1356
+ <button
1357
+ class="tab-btn"
1358
+ [class.active]="activeTab() === 'replay'"
1359
+ (click)="activeTab.set('replay')"
1360
+ title="rrweb Session Replay"
1361
+ >
1362
+ 📽️ Replay
1363
+ @if (rrweb.eventCount() > 0) {
1364
+ <span class="tab-badge">{{ rrweb.eventCount() }}</span>
1365
+ }
1366
+ </button>
1367
+ <button
1368
+ class="tab-btn"
1369
+ [class.active]="activeTab() === 'test'"
1370
+ [disabled]="!aiService.lastTest()"
1371
+ (click)="activeTab.set('test')"
1372
+ >
1373
+ 🤖 Test
1374
+ @if (aiService.lastTest()) {
1375
+ <span class="tab-badge new">NEU</span>
1376
+ }
1377
+ </button>
1378
+ <button
1379
+ class="tab-btn"
1380
+ [class.active]="activeTab() === 'sessions'"
1381
+ [disabled]="recorder.sessions().length === 0"
1382
+ (click)="activeTab.set('sessions')"
1383
+ >
1384
+ Sessions ({{ recorder.sessions().length }})
1385
+ </button>
1386
+ </div>
1387
+
1388
+ <!-- Tab Content -->
1389
+ <div class="tab-content">
1390
+ @if (activeTab() === 'actions') {
1391
+ <app-action-list
1392
+ [session]="recorder.currentSession()"
1393
+ (removeAction)="recorder.removeAction($event)"
1394
+ (addNote)="recorder.addNote($event.id, $event.note)"
1395
+ />
1396
+ }
1397
+
1398
+ @if (activeTab() === 'replay') {
1399
+ <app-session-replay />
1400
+ }
1401
+
1402
+ @if (activeTab() === 'test') {
1403
+ <app-test-preview [test]="aiService.lastTest()" />
1404
+ }
1405
+
1406
+ @if (activeTab() === 'sessions') {
1407
+ <div class="sessions-list">
1408
+ @for (session of recorder.sessions(); track session.id) {
1409
+ <div class="session-card">
1410
+ <div class="session-card-header">
1411
+ <span class="session-name">{{ session.name }}</span>
1412
+ <span class="session-meta">{{ session.actions.length }} Aktionen</span>
1413
+ </div>
1414
+ <div class="session-card-actions">
1415
+ <button class="sm-btn" (click)="loadAndGenerate(session)">🤖 Neu generieren</button>
1416
+ <button class="sm-btn danger" (click)="recorder.deleteSession(session.id)">🗑</button>
1417
+ </div>
1418
+ </div>
1419
+ }
1420
+ </div>
1421
+ }
1422
+ </div>
1423
+
1424
+ <!-- Resize Handle -->
1425
+ <div class="resize-handle" (mousedown)="startResize($event)">⠿</div>
1426
+ </div>
1427
+ }
1428
+
1429
+ <!-- Settings Dialog -->
1430
+ @if (showSettings()) {
1431
+ <app-settings-dialog (close)="showSettings.set(false)" />
1432
+ }
1422
1433
  `, isInline: true, styles: [".debug-fab{bottom:24px;right:24px;z-index:99999;width:52px;height:52px;border-radius:50%;border:none;background:#1e293b;color:#fff;font-size:22px;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 20px #0006;transition:transform .2s,background .2s;position:fixed}.debug-fab:hover{transform:scale(1.1);background:#334155}.debug-fab.recording{background:#dc2626}.debug-fab.pulse{animation:fabPulse 1.5s ease-in-out infinite}@keyframes fabPulse{0%,to{box-shadow:0 4px 20px #dc262666}50%{box-shadow:0 4px 30px #dc2626e6,0 0 0 8px #dc262626}}.fab-badge{position:absolute;top:-4px;right:-4px;background:#f59e0b;color:#000;font-size:10px;font-weight:700;min-width:18px;height:18px;border-radius:9px;display:flex;align-items:center;justify-content:center;padding:0 4px}.debug-panel{position:fixed;z-index:99998;width:420px;max-height:80vh;background:#0f172a;color:#e2e8f0;border-radius:12px;border:1px solid #1e3a5f;box-shadow:0 25px 60px #0009;display:flex;flex-direction:column;font-family:Segoe UI,system-ui,sans-serif;font-size:13px;overflow:hidden}.pos-bottom-right{bottom:88px;right:24px}.pos-bottom-left{bottom:88px;left:24px}.pos-top-right{top:24px;right:24px}.pos-top-left{top:24px;left:24px}.panel-header{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:#1e293b;border-bottom:1px solid #334155;cursor:move;-webkit-user-select:none;user-select:none;flex-shrink:0}.header-left{display:flex;align-items:center;gap:8px}.panel-icon{font-size:16px}.panel-title{font-weight:600;font-size:14px;color:#f1f5f9}.rec-indicator{font-size:10px;font-weight:700;color:#ef4444;background:#ef444426;padding:2px 7px;border-radius:4px;animation:blink 1s step-end infinite}.rec-indicator.paused{color:#f59e0b;background:#f59e0b26;animation:none}@keyframes blink{50%{opacity:.4}}.header-actions{display:flex;gap:4px}.icon-btn{background:none;border:none;color:#94a3b8;cursor:pointer;padding:4px;border-radius:4px;font-size:14px;line-height:1;transition:color .15s,background .15s}.icon-btn:hover{color:#f1f5f9;background:#334155}.session-setup{padding:10px 14px;border-bottom:1px solid #1e293b;display:flex;flex-direction:column;gap:6px;flex-shrink:0}.session-input,.session-desc{background:#1e293b;border:1px solid #334155;color:#e2e8f0;border-radius:6px;padding:6px 10px;font-size:12px;width:100%;box-sizing:border-box;resize:vertical}.session-input:focus,.session-desc:focus{outline:none;border-color:#3b82f6}.recording-controls{display:flex;gap:6px;padding:10px 14px;border-bottom:1px solid #1e293b;flex-wrap:wrap;flex-shrink:0}.ctrl-btn{border:none;border-radius:6px;padding:6px 14px;font-size:12px;font-weight:600;cursor:pointer;transition:filter .15s,transform .1s;display:flex;align-items:center;gap:5px}.ctrl-btn:hover:not(:disabled){filter:brightness(1.15);transform:translateY(-1px)}.ctrl-btn:active:not(:disabled){transform:translateY(0)}.ctrl-btn:disabled{opacity:.5;cursor:not-allowed}.ctrl-btn.start{background:#16a34a;color:#fff}.ctrl-btn.stop{background:#dc2626;color:#fff}.ctrl-btn.pause{background:#d97706;color:#fff}.ctrl-btn.generate{background:#7c3aed;color:#fff;flex:1;justify-content:center}.ctrl-btn.clear{background:#374151;color:#9ca3af;padding:6px 10px}.spinner{display:inline-block;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.error-banner{background:#dc262626;border-left:3px solid #dc2626;color:#fca5a5;padding:8px 14px;font-size:12px;flex-shrink:0}.tab-bar{display:flex;border-bottom:1px solid #1e293b;flex-shrink:0}.tab-btn{flex:1;background:none;border:none;color:#64748b;padding:8px 4px;font-size:12px;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:5px;border-bottom:2px solid transparent;transition:color .15s}.tab-btn:hover:not(:disabled){color:#cbd5e1}.tab-btn.active{color:#60a5fa;border-bottom-color:#3b82f6}.tab-btn:disabled{opacity:.4;cursor:not-allowed}.tab-badge{background:#334155;color:#94a3b8;font-size:10px;padding:1px 5px;border-radius:3px}.tab-badge.new{background:#7c3aed;color:#e9d5ff}.tab-content{overflow-y:auto;flex:1;min-height:0}.tab-content::-webkit-scrollbar{width:5px}.tab-content::-webkit-scrollbar-track{background:transparent}.tab-content::-webkit-scrollbar-thumb{background:#334155;border-radius:3px}.sessions-list{padding:10px 14px;display:flex;flex-direction:column;gap:8px}.session-card{background:#1e293b;border:1px solid #334155;border-radius:8px;padding:10px}.session-card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}.session-name{font-weight:600;color:#f1f5f9;font-size:13px}.session-meta{font-size:11px;color:#64748b}.session-card-actions{display:flex;gap:6px}.sm-btn{background:#334155;border:none;color:#cbd5e1;padding:4px 10px;border-radius:5px;font-size:11px;cursor:pointer;transition:background .15s}.sm-btn:hover{background:#475569}.sm-btn.danger{color:#fca5a5}.sm-btn.danger:hover{background:#dc262633}.resize-handle{text-align:center;color:#334155;cursor:ns-resize;font-size:16px;padding:2px;flex-shrink:0;background:#0f172a;-webkit-user-select:none;user-select:none}.resize-handle:hover{color:#64748b}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { 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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: ActionListComponent, selector: "app-action-list", inputs: ["session"], outputs: ["removeAction", "addNote"] }, { kind: "component", type: TestPreviewComponent, selector: "app-test-preview", inputs: ["test"] }, { kind: "component", type: SettingsDialogComponent, selector: "app-settings-dialog", outputs: ["close"] }, { kind: "component", type: SessionReplayComponent, selector: "app-session-replay" }] }); }
1423
1434
  }
1424
1435
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DebugPanelComponent, decorators: [{
1425
1436
  type: Component,
1426
- args: [{ selector: 'app-debug-panel', standalone: true, imports: [CommonModule, FormsModule, ActionListComponent, TestPreviewComponent, SettingsDialogComponent, SessionReplayComponent], template: `
1427
- <!-- Toggle FAB -->
1428
- <button
1429
- data-debug-panel
1430
- class="debug-fab"
1431
- [class.recording]="recorder.isRecording()"
1432
- [class.pulse]="recorder.isRecording() && !recorder.isPaused()"
1433
- (click)="togglePanel()"
1434
- [title]="panelOpen() ? 'Debug Panel schließen' : 'Debug Panel öffnen'"
1435
- >
1436
- <span class="fab-icon">{{ recorder.isRecording() ? '⏺' : '🐛' }}</span>
1437
- @if (recorder.isRecording() && recorder.actionCount() > 0) {
1438
- <span class="fab-badge">{{ recorder.actionCount() }}</span>
1439
- }
1440
- </button>
1441
-
1442
- <!-- Main Panel -->
1443
- @if (panelOpen()) {
1444
- <div
1445
- data-debug-panel
1446
- class="debug-panel"
1447
- [class]="'pos-' + position()"
1448
- [style.width.px]="panelWidth()"
1449
- >
1450
- <!-- Header -->
1451
- <div class="panel-header" (mousedown)="startDrag($event)">
1452
- <div class="header-left">
1453
- <span class="panel-icon">🐛</span>
1454
- <span class="panel-title">Debug Recorder</span>
1455
- @if (recorder.isRecording()) {
1456
- <span class="rec-indicator" [class.paused]="recorder.isPaused()">
1457
- {{ recorder.isPaused() ? '⏸ PAUSED' : '⏺ REC' }}
1458
- </span>
1459
- }
1460
- </div>
1461
- <div class="header-actions">
1462
- <button class="icon-btn" title="Einstellungen" (click)="showSettings.set(true)">⚙️</button>
1463
- <button class="icon-btn" title="Position wechseln" (click)="cyclePosition()">📌</button>
1464
- <button class="icon-btn" title="Schließen" (click)="togglePanel()">✕</button>
1465
- </div>
1466
- </div>
1467
-
1468
- <!-- Session Name Input (only when not recording) -->
1469
- @if (!recorder.isRecording()) {
1470
- <div class="session-setup">
1471
- <input
1472
- data-debug-panel
1473
- class="session-input"
1474
- type="text"
1475
- [(ngModel)]="sessionName"
1476
- placeholder="Session-Name (optional)"
1477
- />
1478
- <textarea
1479
- data-debug-panel
1480
- class="session-desc"
1481
- [(ngModel)]="sessionDesc"
1482
- placeholder="Fehlerbeschreibung..."
1483
- rows="2"
1484
- ></textarea>
1485
- </div>
1486
- }
1487
-
1488
- <!-- Recording Controls -->
1489
- <div class="recording-controls" data-debug-panel>
1490
- @if (!recorder.isRecording()) {
1491
- <button class="ctrl-btn start" (click)="startRecording()">
1492
- ▶ Aufnahme starten
1493
- </button>
1494
- } @else {
1495
- <button class="ctrl-btn pause" (click)="recorder.pauseRecording()">
1496
- {{ recorder.isPaused() ? '▶ Fortsetzen' : '⏸ Pause' }}
1497
- </button>
1498
- <button class="ctrl-btn stop" (click)="stopRecording()">
1499
- ⏹ Stoppen
1500
- </button>
1501
- <button class="ctrl-btn clear" title="Aktionen löschen" (click)="recorder.clearCurrentSession()">
1502
- 🗑
1503
- </button>
1504
- }
1505
-
1506
- @if (hasActions()) {
1507
- <button
1508
- class="ctrl-btn generate"
1509
- [disabled]="aiService.isGenerating() || !aiService.webhookUrl()"
1510
- [title]="!aiService.webhookUrl() ? 'Webhook-URL in Einstellungen eintragen' : 'Cypress Test generieren'"
1511
- (click)="generateTest()"
1512
- >
1513
- @if (aiService.isGenerating()) {
1514
- <span class="spinner">⟳</span> Generiere...
1515
- } @else {
1516
- 🤖 → Cypress Test
1517
- }
1518
- </button>
1519
- }
1520
- </div>
1521
-
1522
- <!-- Error Banner -->
1523
- @if (aiService.error()) {
1524
- <div class="error-banner" data-debug-panel>
1525
- ⚠️ {{ aiService.error() }}
1526
- </div>
1527
- }
1528
-
1529
- <!-- Tabs -->
1530
- <div class="tab-bar" data-debug-panel>
1531
- <button
1532
- class="tab-btn"
1533
- [class.active]="activeTab() === 'actions'"
1534
- (click)="activeTab.set('actions')"
1535
- >
1536
- Aktionen
1537
- @if (recorder.actionCount() > 0) {
1538
- <span class="tab-badge">{{ recorder.actionCount() }}</span>
1539
- }
1540
- </button>
1541
- <button
1542
- class="tab-btn"
1543
- [class.active]="activeTab() === 'replay'"
1544
- (click)="activeTab.set('replay')"
1545
- title="rrweb Session Replay"
1546
- >
1547
- 📽️ Replay
1548
- @if (rrweb.events().length > 0) {
1549
- <span class="tab-badge">{{ rrweb.events().length }}</span>
1550
- }
1551
- </button>
1552
- <button
1553
- class="tab-btn"
1554
- [class.active]="activeTab() === 'test'"
1555
- [disabled]="!aiService.lastTest()"
1556
- (click)="activeTab.set('test')"
1557
- >
1558
- 🤖 Test
1559
- @if (aiService.lastTest()) {
1560
- <span class="tab-badge new">NEU</span>
1561
- }
1562
- </button>
1563
- <button
1564
- class="tab-btn"
1565
- [class.active]="activeTab() === 'sessions'"
1566
- [disabled]="recorder.sessions().length === 0"
1567
- (click)="activeTab.set('sessions')"
1568
- >
1569
- Sessions ({{ recorder.sessions().length }})
1570
- </button>
1571
- </div>
1572
-
1573
- <!-- Tab Content -->
1574
- <div class="tab-content">
1575
- @if (activeTab() === 'actions') {
1576
- <app-action-list
1577
- [session]="recorder.currentSession()"
1578
- (removeAction)="recorder.removeAction($event)"
1579
- (addNote)="recorder.addNote($event.id, $event.note)"
1580
- />
1581
- }
1582
-
1583
- @if (activeTab() === 'replay') {
1584
- <app-session-replay />
1585
- }
1586
-
1587
- @if (activeTab() === 'test') {
1588
- <app-test-preview [test]="aiService.lastTest()" />
1589
- }
1590
-
1591
- @if (activeTab() === 'sessions') {
1592
- <div class="sessions-list">
1593
- @for (session of recorder.sessions(); track session.id) {
1594
- <div class="session-card">
1595
- <div class="session-card-header">
1596
- <span class="session-name">{{ session.name }}</span>
1597
- <span class="session-meta">{{ session.actions.length }} Aktionen</span>
1598
- </div>
1599
- <div class="session-card-actions">
1600
- <button class="sm-btn" (click)="loadAndGenerate(session)">🤖 Neu generieren</button>
1601
- <button class="sm-btn danger" (click)="recorder.deleteSession(session.id)">🗑</button>
1602
- </div>
1603
- </div>
1604
- }
1605
- </div>
1606
- }
1607
- </div>
1608
-
1609
- <!-- Resize Handle -->
1610
- <div class="resize-handle" (mousedown)="startResize($event)">⠿</div>
1611
- </div>
1612
- }
1613
-
1614
- <!-- Settings Dialog -->
1615
- @if (showSettings()) {
1616
- <app-settings-dialog (close)="showSettings.set(false)" />
1617
- }
1437
+ args: [{ selector: 'app-debug-panel', standalone: true, imports: [CommonModule, FormsModule, ActionListComponent, TestPreviewComponent, SettingsDialogComponent, SessionReplayComponent], template: `
1438
+ <!-- Toggle FAB -->
1439
+ <button
1440
+ data-debug-panel
1441
+ class="debug-fab"
1442
+ [class.recording]="recorder.isRecording()"
1443
+ [class.pulse]="recorder.isRecording() && !recorder.isPaused()"
1444
+ (click)="togglePanel()"
1445
+ [title]="panelOpen() ? 'Debug Panel schließen' : 'Debug Panel öffnen'"
1446
+ >
1447
+ <span class="fab-icon">{{ recorder.isRecording() ? '⏺' : '🐛' }}</span>
1448
+ @if (recorder.isRecording() && recorder.actionCount() > 0) {
1449
+ <span class="fab-badge">{{ recorder.actionCount() }}</span>
1450
+ }
1451
+ </button>
1452
+
1453
+ <!-- Main Panel -->
1454
+ @if (panelOpen()) {
1455
+ <div
1456
+ data-debug-panel
1457
+ class="debug-panel"
1458
+ [class]="'pos-' + position()"
1459
+ [style.width.px]="panelWidth()"
1460
+ >
1461
+ <!-- Header -->
1462
+ <div class="panel-header" (mousedown)="startDrag($event)">
1463
+ <div class="header-left">
1464
+ <span class="panel-icon">🐛</span>
1465
+ <span class="panel-title">Debug Recorder</span>
1466
+ @if (recorder.isRecording()) {
1467
+ <span class="rec-indicator" [class.paused]="recorder.isPaused()">
1468
+ {{ recorder.isPaused() ? '⏸ PAUSED' : '⏺ REC' }}
1469
+ </span>
1470
+ }
1471
+ </div>
1472
+ <div class="header-actions">
1473
+ <button class="icon-btn" title="Einstellungen" (click)="showSettings.set(true)">⚙️</button>
1474
+ <button class="icon-btn" title="Position wechseln" (click)="cyclePosition()">📌</button>
1475
+ <button class="icon-btn" title="Schließen" (click)="togglePanel()">✕</button>
1476
+ </div>
1477
+ </div>
1478
+
1479
+ <!-- Session Name Input (only when not recording) -->
1480
+ @if (!recorder.isRecording()) {
1481
+ <div class="session-setup">
1482
+ <input
1483
+ data-debug-panel
1484
+ class="session-input"
1485
+ type="text"
1486
+ [(ngModel)]="sessionName"
1487
+ placeholder="Session-Name (optional)"
1488
+ />
1489
+ <textarea
1490
+ data-debug-panel
1491
+ class="session-desc"
1492
+ [(ngModel)]="sessionDesc"
1493
+ placeholder="Fehlerbeschreibung..."
1494
+ rows="2"
1495
+ ></textarea>
1496
+ </div>
1497
+ }
1498
+
1499
+ <!-- Recording Controls -->
1500
+ <div class="recording-controls" data-debug-panel>
1501
+ @if (!recorder.isRecording()) {
1502
+ <button class="ctrl-btn start" (click)="startRecording()">
1503
+ ▶ Aufnahme starten
1504
+ </button>
1505
+ } @else {
1506
+ <button class="ctrl-btn pause" (click)="recorder.pauseRecording()">
1507
+ {{ recorder.isPaused() ? '▶ Fortsetzen' : '⏸ Pause' }}
1508
+ </button>
1509
+ <button class="ctrl-btn stop" (click)="stopRecording()">
1510
+ ⏹ Stoppen
1511
+ </button>
1512
+ <button class="ctrl-btn clear" title="Aktionen löschen" (click)="recorder.clearCurrentSession()">
1513
+ 🗑
1514
+ </button>
1515
+ }
1516
+
1517
+ @if (hasActions()) {
1518
+ <button
1519
+ class="ctrl-btn generate"
1520
+ [disabled]="aiService.isGenerating() || !aiService.webhookUrl()"
1521
+ [title]="!aiService.webhookUrl() ? 'Webhook-URL in Einstellungen eintragen' : 'Cypress Test generieren'"
1522
+ (click)="generateTest()"
1523
+ >
1524
+ @if (aiService.isGenerating()) {
1525
+ <span class="spinner">⟳</span> Generiere...
1526
+ } @else {
1527
+ 🤖 → Cypress Test
1528
+ }
1529
+ </button>
1530
+ }
1531
+ </div>
1532
+
1533
+ <!-- Error Banner -->
1534
+ @if (aiService.error()) {
1535
+ <div class="error-banner" data-debug-panel>
1536
+ ⚠️ {{ aiService.error() }}
1537
+ </div>
1538
+ }
1539
+
1540
+ <!-- Tabs -->
1541
+ <div class="tab-bar" data-debug-panel>
1542
+ <button
1543
+ class="tab-btn"
1544
+ [class.active]="activeTab() === 'actions'"
1545
+ (click)="activeTab.set('actions')"
1546
+ >
1547
+ Aktionen
1548
+ @if (recorder.actionCount() > 0) {
1549
+ <span class="tab-badge">{{ recorder.actionCount() }}</span>
1550
+ }
1551
+ </button>
1552
+ <button
1553
+ class="tab-btn"
1554
+ [class.active]="activeTab() === 'replay'"
1555
+ (click)="activeTab.set('replay')"
1556
+ title="rrweb Session Replay"
1557
+ >
1558
+ 📽️ Replay
1559
+ @if (rrweb.eventCount() > 0) {
1560
+ <span class="tab-badge">{{ rrweb.eventCount() }}</span>
1561
+ }
1562
+ </button>
1563
+ <button
1564
+ class="tab-btn"
1565
+ [class.active]="activeTab() === 'test'"
1566
+ [disabled]="!aiService.lastTest()"
1567
+ (click)="activeTab.set('test')"
1568
+ >
1569
+ 🤖 Test
1570
+ @if (aiService.lastTest()) {
1571
+ <span class="tab-badge new">NEU</span>
1572
+ }
1573
+ </button>
1574
+ <button
1575
+ class="tab-btn"
1576
+ [class.active]="activeTab() === 'sessions'"
1577
+ [disabled]="recorder.sessions().length === 0"
1578
+ (click)="activeTab.set('sessions')"
1579
+ >
1580
+ Sessions ({{ recorder.sessions().length }})
1581
+ </button>
1582
+ </div>
1583
+
1584
+ <!-- Tab Content -->
1585
+ <div class="tab-content">
1586
+ @if (activeTab() === 'actions') {
1587
+ <app-action-list
1588
+ [session]="recorder.currentSession()"
1589
+ (removeAction)="recorder.removeAction($event)"
1590
+ (addNote)="recorder.addNote($event.id, $event.note)"
1591
+ />
1592
+ }
1593
+
1594
+ @if (activeTab() === 'replay') {
1595
+ <app-session-replay />
1596
+ }
1597
+
1598
+ @if (activeTab() === 'test') {
1599
+ <app-test-preview [test]="aiService.lastTest()" />
1600
+ }
1601
+
1602
+ @if (activeTab() === 'sessions') {
1603
+ <div class="sessions-list">
1604
+ @for (session of recorder.sessions(); track session.id) {
1605
+ <div class="session-card">
1606
+ <div class="session-card-header">
1607
+ <span class="session-name">{{ session.name }}</span>
1608
+ <span class="session-meta">{{ session.actions.length }} Aktionen</span>
1609
+ </div>
1610
+ <div class="session-card-actions">
1611
+ <button class="sm-btn" (click)="loadAndGenerate(session)">🤖 Neu generieren</button>
1612
+ <button class="sm-btn danger" (click)="recorder.deleteSession(session.id)">🗑</button>
1613
+ </div>
1614
+ </div>
1615
+ }
1616
+ </div>
1617
+ }
1618
+ </div>
1619
+
1620
+ <!-- Resize Handle -->
1621
+ <div class="resize-handle" (mousedown)="startResize($event)">⠿</div>
1622
+ </div>
1623
+ }
1624
+
1625
+ <!-- Settings Dialog -->
1626
+ @if (showSettings()) {
1627
+ <app-settings-dialog (close)="showSettings.set(false)" />
1628
+ }
1618
1629
  `, styles: [".debug-fab{bottom:24px;right:24px;z-index:99999;width:52px;height:52px;border-radius:50%;border:none;background:#1e293b;color:#fff;font-size:22px;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 20px #0006;transition:transform .2s,background .2s;position:fixed}.debug-fab:hover{transform:scale(1.1);background:#334155}.debug-fab.recording{background:#dc2626}.debug-fab.pulse{animation:fabPulse 1.5s ease-in-out infinite}@keyframes fabPulse{0%,to{box-shadow:0 4px 20px #dc262666}50%{box-shadow:0 4px 30px #dc2626e6,0 0 0 8px #dc262626}}.fab-badge{position:absolute;top:-4px;right:-4px;background:#f59e0b;color:#000;font-size:10px;font-weight:700;min-width:18px;height:18px;border-radius:9px;display:flex;align-items:center;justify-content:center;padding:0 4px}.debug-panel{position:fixed;z-index:99998;width:420px;max-height:80vh;background:#0f172a;color:#e2e8f0;border-radius:12px;border:1px solid #1e3a5f;box-shadow:0 25px 60px #0009;display:flex;flex-direction:column;font-family:Segoe UI,system-ui,sans-serif;font-size:13px;overflow:hidden}.pos-bottom-right{bottom:88px;right:24px}.pos-bottom-left{bottom:88px;left:24px}.pos-top-right{top:24px;right:24px}.pos-top-left{top:24px;left:24px}.panel-header{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:#1e293b;border-bottom:1px solid #334155;cursor:move;-webkit-user-select:none;user-select:none;flex-shrink:0}.header-left{display:flex;align-items:center;gap:8px}.panel-icon{font-size:16px}.panel-title{font-weight:600;font-size:14px;color:#f1f5f9}.rec-indicator{font-size:10px;font-weight:700;color:#ef4444;background:#ef444426;padding:2px 7px;border-radius:4px;animation:blink 1s step-end infinite}.rec-indicator.paused{color:#f59e0b;background:#f59e0b26;animation:none}@keyframes blink{50%{opacity:.4}}.header-actions{display:flex;gap:4px}.icon-btn{background:none;border:none;color:#94a3b8;cursor:pointer;padding:4px;border-radius:4px;font-size:14px;line-height:1;transition:color .15s,background .15s}.icon-btn:hover{color:#f1f5f9;background:#334155}.session-setup{padding:10px 14px;border-bottom:1px solid #1e293b;display:flex;flex-direction:column;gap:6px;flex-shrink:0}.session-input,.session-desc{background:#1e293b;border:1px solid #334155;color:#e2e8f0;border-radius:6px;padding:6px 10px;font-size:12px;width:100%;box-sizing:border-box;resize:vertical}.session-input:focus,.session-desc:focus{outline:none;border-color:#3b82f6}.recording-controls{display:flex;gap:6px;padding:10px 14px;border-bottom:1px solid #1e293b;flex-wrap:wrap;flex-shrink:0}.ctrl-btn{border:none;border-radius:6px;padding:6px 14px;font-size:12px;font-weight:600;cursor:pointer;transition:filter .15s,transform .1s;display:flex;align-items:center;gap:5px}.ctrl-btn:hover:not(:disabled){filter:brightness(1.15);transform:translateY(-1px)}.ctrl-btn:active:not(:disabled){transform:translateY(0)}.ctrl-btn:disabled{opacity:.5;cursor:not-allowed}.ctrl-btn.start{background:#16a34a;color:#fff}.ctrl-btn.stop{background:#dc2626;color:#fff}.ctrl-btn.pause{background:#d97706;color:#fff}.ctrl-btn.generate{background:#7c3aed;color:#fff;flex:1;justify-content:center}.ctrl-btn.clear{background:#374151;color:#9ca3af;padding:6px 10px}.spinner{display:inline-block;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.error-banner{background:#dc262626;border-left:3px solid #dc2626;color:#fca5a5;padding:8px 14px;font-size:12px;flex-shrink:0}.tab-bar{display:flex;border-bottom:1px solid #1e293b;flex-shrink:0}.tab-btn{flex:1;background:none;border:none;color:#64748b;padding:8px 4px;font-size:12px;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:5px;border-bottom:2px solid transparent;transition:color .15s}.tab-btn:hover:not(:disabled){color:#cbd5e1}.tab-btn.active{color:#60a5fa;border-bottom-color:#3b82f6}.tab-btn:disabled{opacity:.4;cursor:not-allowed}.tab-badge{background:#334155;color:#94a3b8;font-size:10px;padding:1px 5px;border-radius:3px}.tab-badge.new{background:#7c3aed;color:#e9d5ff}.tab-content{overflow-y:auto;flex:1;min-height:0}.tab-content::-webkit-scrollbar{width:5px}.tab-content::-webkit-scrollbar-track{background:transparent}.tab-content::-webkit-scrollbar-thumb{background:#334155;border-radius:3px}.sessions-list{padding:10px 14px;display:flex;flex-direction:column;gap:8px}.session-card{background:#1e293b;border:1px solid #334155;border-radius:8px;padding:10px}.session-card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}.session-name{font-weight:600;color:#f1f5f9;font-size:13px}.session-meta{font-size:11px;color:#64748b}.session-card-actions{display:flex;gap:6px}.sm-btn{background:#334155;border:none;color:#cbd5e1;padding:4px 10px;border-radius:5px;font-size:11px;cursor:pointer;transition:background .15s}.sm-btn:hover{background:#475569}.sm-btn.danger{color:#fca5a5}.sm-btn.danger:hover{background:#dc262633}.resize-handle{text-align:center;color:#334155;cursor:ns-resize;font-size:16px;padding:2px;flex-shrink:0;background:#0f172a;-webkit-user-select:none;user-select:none}.resize-handle:hover{color:#64748b}\n"] }]
1619
1630
  }] });
1620
1631