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.
- package/esm2022/lib/action-list/action-list.component.mjs +161 -161
- package/esm2022/lib/debug-panel/debug-panel.component.mjs +385 -385
- package/esm2022/lib/services/rrweb-recorder.service.mjs +32 -21
- package/esm2022/lib/session-replay/session-replay.component.mjs +85 -85
- package/esm2022/lib/settings-dialog/settings-dialog.component.mjs +75 -75
- package/esm2022/lib/test-preview/test-preview.component.mjs +59 -59
- package/fesm2022/angular-debug-recorder.mjs +791 -780
- package/fesm2022/angular-debug-recorder.mjs.map +1 -1
- package/lib/services/rrweb-recorder.service.d.ts +5 -4
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
437
|
+
this._eventsArray = [];
|
|
438
|
+
this._eventCount.set(0);
|
|
430
439
|
this._isRecording.set(true);
|
|
431
440
|
this.stopFn = record({
|
|
432
441
|
emit: (event) => {
|
|
433
|
-
|
|
434
|
-
|
|
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
|
|
438
|
-
// on TextNodes/CommentNodes which
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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"><{{ action.element?.tagName }}></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
|
|
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"><{{ action.element?.tagName }}></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
|
|
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"><{{ action.element?.tagName }}></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
|
|
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"><{{ action.element?.tagName }}></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 <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 <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 <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 <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.
|
|
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.
|
|
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.
|
|
1353
|
-
<span class="tab-badge">{{ rrweb.
|
|
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.
|
|
1549
|
-
<span class="tab-badge">{{ rrweb.
|
|
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
|
|