angular-debug-recorder 1.0.0

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.
@@ -0,0 +1,1631 @@
1
+ import * as i0 from '@angular/core';
2
+ import { signal, computed, Injectable, EventEmitter, Output, Input, Component, inject, ViewChild } from '@angular/core';
3
+ import { CommonModule } from '@angular/common';
4
+ import * as i1$1 from '@angular/forms';
5
+ import { FormsModule } from '@angular/forms';
6
+ import * as i1 from '@angular/common/http';
7
+
8
+ class RecorderService {
9
+ constructor(zone) {
10
+ this.zone = zone;
11
+ this._isRecording = signal(false);
12
+ this._currentSession = signal(null);
13
+ this._sessions = signal([]);
14
+ this._isPaused = signal(false);
15
+ this.isRecording = computed(() => this._isRecording());
16
+ this.isPaused = computed(() => this._isPaused());
17
+ this.currentSession = computed(() => this._currentSession());
18
+ this.sessions = computed(() => this._sessions());
19
+ this.actionCount = computed(() => this._currentSession()?.actions.length ?? 0);
20
+ this.listeners = [];
21
+ this.lastScrollTime = 0;
22
+ }
23
+ startRecording(name, description) {
24
+ const session = {
25
+ id: this.generateId(),
26
+ name: name || `Session ${new Date().toLocaleTimeString()}`,
27
+ description,
28
+ startTime: Date.now(),
29
+ startUrl: window.location.href,
30
+ actions: [],
31
+ tags: [],
32
+ };
33
+ this._currentSession.set(session);
34
+ this._isRecording.set(true);
35
+ this._isPaused.set(false);
36
+ this.attachListeners();
37
+ this.recordNavigation('navigation', window.location.href);
38
+ }
39
+ stopRecording() {
40
+ const session = this._currentSession();
41
+ if (!session)
42
+ return null;
43
+ const completed = { ...session, endTime: Date.now() };
44
+ this._sessions.update(s => [...s, completed]);
45
+ this._currentSession.set(null);
46
+ this._isRecording.set(false);
47
+ this._isPaused.set(false);
48
+ this.detachListeners();
49
+ return completed;
50
+ }
51
+ pauseRecording() {
52
+ if (this._isRecording()) {
53
+ this._isPaused.set(!this._isPaused());
54
+ }
55
+ }
56
+ clearCurrentSession() {
57
+ this._currentSession.update(s => s ? { ...s, actions: [] } : null);
58
+ }
59
+ removeAction(actionId) {
60
+ this._currentSession.update(s => s ? { ...s, actions: s.actions.filter(a => a.id !== actionId) } : null);
61
+ }
62
+ addNote(actionId, note) {
63
+ this._currentSession.update(s => s ? {
64
+ ...s,
65
+ actions: s.actions.map(a => a.id === actionId ? { ...a, note } : a)
66
+ } : null);
67
+ }
68
+ deleteSession(sessionId) {
69
+ this._sessions.update(s => s.filter(x => x.id !== sessionId));
70
+ }
71
+ loadSession(session) {
72
+ this._currentSession.set(session);
73
+ }
74
+ // ─── Event Listeners ──────────────────────────────────────────────────────
75
+ attachListeners() {
76
+ const opts = { capture: true, passive: true };
77
+ const onClick = (e) => this.zone.run(() => this.handleClick(e));
78
+ const onDblClick = (e) => this.zone.run(() => this.handleDblClick(e));
79
+ const onInput = (e) => this.zone.run(() => this.handleInput(e));
80
+ const onChange = (e) => this.zone.run(() => this.handleChange(e));
81
+ const onSubmit = (e) => this.zone.run(() => this.handleSubmit(e));
82
+ const onKeydown = (e) => this.zone.run(() => this.handleKeydown(e));
83
+ const onScroll = (e) => this.zone.run(() => this.handleScroll(e));
84
+ document.addEventListener('click', onClick, opts);
85
+ document.addEventListener('dblclick', onDblClick, opts);
86
+ document.addEventListener('input', onInput, opts);
87
+ document.addEventListener('change', onChange, opts);
88
+ document.addEventListener('submit', onSubmit, opts);
89
+ document.addEventListener('keydown', onKeydown, opts);
90
+ document.addEventListener('scroll', onScroll, { capture: true, passive: true });
91
+ this.listeners = [
92
+ { type: 'click', fn: onClick },
93
+ { type: 'dblclick', fn: onDblClick },
94
+ { type: 'input', fn: onInput },
95
+ { type: 'change', fn: onChange },
96
+ { type: 'submit', fn: onSubmit },
97
+ { type: 'keydown', fn: onKeydown },
98
+ { type: 'scroll', fn: onScroll },
99
+ ];
100
+ }
101
+ detachListeners() {
102
+ this.listeners.forEach(({ type, fn }) => document.removeEventListener(type, fn, true));
103
+ this.listeners = [];
104
+ this.mutationObserver?.disconnect();
105
+ }
106
+ // ─── Handlers ─────────────────────────────────────────────────────────────
107
+ handleClick(e) {
108
+ if (!this.shouldRecord(e.target))
109
+ return;
110
+ const el = e.target;
111
+ const info = this.getElementInfo(el);
112
+ const selector = this.buildSelector(el);
113
+ this.addAction({
114
+ type: 'click',
115
+ selector,
116
+ selectorStrategy: this.getSelectorStrategy(el),
117
+ element: info,
118
+ description: `Click on ${this.describeElement(info)}`,
119
+ });
120
+ }
121
+ handleDblClick(e) {
122
+ if (!this.shouldRecord(e.target))
123
+ return;
124
+ const el = e.target;
125
+ const info = this.getElementInfo(el);
126
+ const selector = this.buildSelector(el);
127
+ this.addAction({
128
+ type: 'dblclick',
129
+ selector,
130
+ selectorStrategy: this.getSelectorStrategy(el),
131
+ element: info,
132
+ description: `Double-click on ${this.describeElement(info)}`,
133
+ });
134
+ }
135
+ handleInput(e) {
136
+ if (!this.shouldRecord(e.target))
137
+ return;
138
+ const el = e.target;
139
+ if (['checkbox', 'radio'].includes(el.type))
140
+ return; // handled by change
141
+ const info = this.getElementInfo(el);
142
+ const selector = this.buildSelector(el);
143
+ this.addAction({
144
+ type: 'input',
145
+ selector,
146
+ selectorStrategy: this.getSelectorStrategy(el),
147
+ element: info,
148
+ value: el.value,
149
+ description: `Type "${el.value}" in ${this.describeElement(info)}`,
150
+ });
151
+ }
152
+ handleChange(e) {
153
+ if (!this.shouldRecord(e.target))
154
+ return;
155
+ const el = e.target;
156
+ const info = this.getElementInfo(el);
157
+ const selector = this.buildSelector(el);
158
+ if (el.tagName === 'SELECT') {
159
+ this.addAction({
160
+ type: 'select',
161
+ selector,
162
+ selectorStrategy: this.getSelectorStrategy(el),
163
+ element: info,
164
+ value: el.value,
165
+ description: `Select "${el.value}" in ${this.describeElement(info)}`,
166
+ });
167
+ }
168
+ else if (el.type === 'checkbox') {
169
+ this.addAction({
170
+ type: 'click',
171
+ selector,
172
+ selectorStrategy: this.getSelectorStrategy(el),
173
+ element: info,
174
+ value: String(el.checked),
175
+ description: `${el.checked ? 'Check' : 'Uncheck'} ${this.describeElement(info)}`,
176
+ });
177
+ }
178
+ }
179
+ handleSubmit(e) {
180
+ if (!this.shouldRecord(e.target))
181
+ return;
182
+ const form = e.target;
183
+ const info = this.getElementInfo(form);
184
+ const selector = this.buildSelector(form);
185
+ this.addAction({
186
+ type: 'submit',
187
+ selector,
188
+ selectorStrategy: this.getSelectorStrategy(form),
189
+ element: info,
190
+ description: `Submit form ${info.id ? '#' + info.id : info.name ? info.name : ''}`.trim(),
191
+ });
192
+ }
193
+ handleScroll(e) {
194
+ if (!this.shouldRecord(e.target))
195
+ return;
196
+ const now = Date.now();
197
+ if (now - this.lastScrollTime < 1000)
198
+ return; // debounce 1s
199
+ this.lastScrollTime = now;
200
+ const el = e.target;
201
+ const isDoc = !el.tagName || el.tagName === 'HTML';
202
+ const scrollY = isDoc ? window.scrollY : el.scrollTop;
203
+ const scrollX = isDoc ? window.scrollX : el.scrollLeft;
204
+ const selector = el.tagName === 'HTML' || el.tagName === 'BODY'
205
+ ? 'window'
206
+ : this.buildSelector(el);
207
+ this.addAction({
208
+ type: 'scroll',
209
+ selector,
210
+ selectorStrategy: 'combined',
211
+ value: `${scrollX},${scrollY}`,
212
+ description: `Scroll to (${scrollX}, ${scrollY})`,
213
+ });
214
+ }
215
+ handleKeydown(e) {
216
+ const specialKeys = ['Enter', 'Escape', 'Tab', 'F5', 'F12'];
217
+ if (!specialKeys.includes(e.key))
218
+ return;
219
+ if (!this.shouldRecord(e.target))
220
+ return;
221
+ const el = e.target;
222
+ const selector = this.buildSelector(el);
223
+ this.addAction({
224
+ type: 'keypress',
225
+ selector,
226
+ selectorStrategy: this.getSelectorStrategy(el),
227
+ value: e.key,
228
+ description: `Press "${e.key}" on ${el.tagName.toLowerCase()}`,
229
+ });
230
+ }
231
+ recordNavigation(type, navUrl) {
232
+ const action = {
233
+ id: this.generateId(),
234
+ timestamp: Date.now(),
235
+ url: navUrl,
236
+ type,
237
+ selector: 'window',
238
+ selectorStrategy: 'combined',
239
+ description: `Navigate to ${navUrl}`,
240
+ };
241
+ this._currentSession.update(s => s ? { ...s, actions: [...s.actions, action] } : s);
242
+ }
243
+ // ─── Selector Building ────────────────────────────────────────────────────
244
+ buildSelector(el) {
245
+ // Priority: data-testid > data-cy > id > name > aria-label > combined
246
+ const testId = el.getAttribute('data-testid');
247
+ if (testId)
248
+ return `[data-testid="${testId}"]`;
249
+ const cy = el.getAttribute('data-cy');
250
+ if (cy)
251
+ return `[data-cy="${cy}"]`;
252
+ if (el.id && !el.id.includes(':'))
253
+ return `#${el.id}`;
254
+ const name = el.getAttribute('name');
255
+ if (name)
256
+ return `${el.tagName.toLowerCase()}[name="${name}"]`;
257
+ const ariaLabel = el.getAttribute('aria-label');
258
+ if (ariaLabel)
259
+ return `[aria-label="${ariaLabel}"]`;
260
+ // Class-based fallback
261
+ const relevantClasses = Array.from(el.classList)
262
+ .filter(c => !c.startsWith('ng-') && !c.startsWith('cdk-') && c.length > 0)
263
+ .slice(0, 3);
264
+ if (relevantClasses.length > 0) {
265
+ return `${el.tagName.toLowerCase()}.${relevantClasses.join('.')}`;
266
+ }
267
+ // Text content for buttons/links
268
+ if (['BUTTON', 'A'].includes(el.tagName)) {
269
+ const text = el.textContent?.trim().slice(0, 30);
270
+ if (text)
271
+ return `${el.tagName.toLowerCase()}:contains("${text}")`;
272
+ }
273
+ return el.tagName.toLowerCase();
274
+ }
275
+ getSelectorStrategy(el) {
276
+ if (el.getAttribute('data-testid'))
277
+ return 'data-testid';
278
+ if (el.getAttribute('data-cy'))
279
+ return 'data-cy';
280
+ if (el.id)
281
+ return 'id';
282
+ if (el.getAttribute('name'))
283
+ return 'name';
284
+ return 'class';
285
+ }
286
+ getElementInfo(el) {
287
+ return {
288
+ tagName: el.tagName.toLowerCase(),
289
+ id: el.id || undefined,
290
+ classes: Array.from(el.classList).filter(c => !c.startsWith('ng-')),
291
+ dataTestId: el.getAttribute('data-testid') || undefined,
292
+ dataCy: el.getAttribute('data-cy') || undefined,
293
+ name: el.getAttribute('name') || undefined,
294
+ type: el.getAttribute('type') || undefined,
295
+ placeholder: el.getAttribute('placeholder') || undefined,
296
+ text: el.textContent?.trim().slice(0, 50) || undefined,
297
+ href: el.href || undefined,
298
+ ariaLabel: el.getAttribute('aria-label') || undefined,
299
+ };
300
+ }
301
+ describeElement(info) {
302
+ if (info.dataTestId)
303
+ return `[data-testid="${info.dataTestId}"]`;
304
+ if (info.dataCy)
305
+ return `[data-cy="${info.dataCy}"]`;
306
+ if (info.id)
307
+ return `#${info.id}`;
308
+ if (info.ariaLabel)
309
+ return `"${info.ariaLabel}"`;
310
+ if (info.placeholder)
311
+ return `"${info.placeholder}" input`;
312
+ if (info.text)
313
+ return `"${info.text}"`;
314
+ return info.tagName;
315
+ }
316
+ // ─── Helpers ──────────────────────────────────────────────────────────────
317
+ shouldRecord(target) {
318
+ if (!this._isRecording() || this._isPaused())
319
+ return false;
320
+ if (!target)
321
+ return false;
322
+ // Ignore the debug panel itself
323
+ if (target.closest('[data-debug-panel]'))
324
+ return false;
325
+ return true;
326
+ }
327
+ addAction(partial) {
328
+ const action = {
329
+ id: this.generateId(),
330
+ timestamp: Date.now(),
331
+ url: window.location.href,
332
+ ...partial,
333
+ };
334
+ // Deduplicate consecutive identical inputs (keep only latest value)
335
+ if (action.type === 'input') {
336
+ this._currentSession.update(s => {
337
+ if (!s)
338
+ return s;
339
+ const last = s.actions[s.actions.length - 1];
340
+ if (last?.type === 'input' && last.selector === action.selector) {
341
+ return { ...s, actions: [...s.actions.slice(0, -1), action] };
342
+ }
343
+ return { ...s, actions: [...s.actions, action] };
344
+ });
345
+ return;
346
+ }
347
+ this._currentSession.update(s => s ? { ...s, actions: [...s.actions, action] } : s);
348
+ }
349
+ generateId() {
350
+ return Math.random().toString(36).slice(2, 11);
351
+ }
352
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: RecorderService, deps: [{ token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable }); }
353
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: RecorderService, providedIn: 'root' }); }
354
+ }
355
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: RecorderService, decorators: [{
356
+ type: Injectable,
357
+ args: [{ providedIn: 'root' }]
358
+ }], ctorParameters: () => [{ type: i0.NgZone }] });
359
+
360
+ class AiGeneratorService {
361
+ constructor(http) {
362
+ this.http = http;
363
+ this._webhookUrl = signal(localStorage.getItem('debugRecorder_webhookUrl') ?? '');
364
+ this._isGenerating = signal(false);
365
+ this._error = signal(null);
366
+ this._lastTest = signal(null);
367
+ this.webhookUrl = this._webhookUrl.asReadonly();
368
+ this.isGenerating = this._isGenerating.asReadonly();
369
+ this.error = this._error.asReadonly();
370
+ this.lastTest = this._lastTest.asReadonly();
371
+ }
372
+ setWebhookUrl(url) {
373
+ this._webhookUrl.set(url);
374
+ localStorage.setItem('debugRecorder_webhookUrl', url);
375
+ }
376
+ async generateCypressTest(session) {
377
+ const url = this._webhookUrl();
378
+ if (!url)
379
+ throw new Error('Keine Webhook-URL konfiguriert');
380
+ this._isGenerating.set(true);
381
+ this._error.set(null);
382
+ try {
383
+ const code = await this.postSession(url, session);
384
+ const test = {
385
+ code,
386
+ generatedAt: Date.now(),
387
+ model: url,
388
+ sessionId: session.id,
389
+ };
390
+ this._lastTest.set(test);
391
+ return test;
392
+ }
393
+ catch (err) {
394
+ const msg = err?.error?.message || err?.message || 'Fehler beim Senden';
395
+ this._error.set(msg);
396
+ throw err;
397
+ }
398
+ finally {
399
+ this._isGenerating.set(false);
400
+ }
401
+ }
402
+ postSession(url, session) {
403
+ return new Promise((resolve, reject) => {
404
+ this.http.post(url, session, { responseType: 'text' }).subscribe({
405
+ next: (res) => resolve(res),
406
+ error: reject,
407
+ });
408
+ });
409
+ }
410
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AiGeneratorService, deps: [{ token: i1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable }); }
411
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AiGeneratorService, providedIn: 'root' }); }
412
+ }
413
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AiGeneratorService, decorators: [{
414
+ type: Injectable,
415
+ args: [{ providedIn: 'root' }]
416
+ }], ctorParameters: () => [{ type: i1.HttpClient }] });
417
+
418
+ class RrwebRecorderService {
419
+ constructor(zone) {
420
+ this.zone = zone;
421
+ this._events = signal([]);
422
+ this._isRecording = signal(false);
423
+ this.events = this._events.asReadonly();
424
+ this.isRecording = this._isRecording.asReadonly();
425
+ }
426
+ async startRecording() {
427
+ // Dynamically import rrweb to avoid SSR issues and reduce initial bundle
428
+ const { record } = await import('rrweb');
429
+ this._events.set([]);
430
+ this._isRecording.set(true);
431
+ this.stopFn = record({
432
+ emit: (event) => {
433
+ this.zone.run(() => {
434
+ this._events.update(ev => [...ev, event]);
435
+ });
436
+ },
437
+ // Note: blockSelector is omitted — rrweb 2.0.0-alpha.4 calls node.matches()
438
+ // on TextNodes/CommentNodes which don't have that method, crashing the recorder.
439
+ maskTextSelector: 'input[type="password"]',
440
+ checkoutEveryNth: 200,
441
+ });
442
+ }
443
+ stopRecording() {
444
+ if (this.stopFn) {
445
+ this.stopFn();
446
+ this.stopFn = undefined;
447
+ }
448
+ this._isRecording.set(false);
449
+ return this._events();
450
+ }
451
+ getEvents() {
452
+ return this._events();
453
+ }
454
+ clearEvents() {
455
+ this._events.set([]);
456
+ }
457
+ hasEvents() {
458
+ return this._events().length > 0;
459
+ }
460
+ // ─── Replay ──────────────────────────────────────────────────────────────
461
+ async startReplay(container, events) {
462
+ const { Replayer } = await import('rrweb');
463
+ const eventsToReplay = events ?? this._events();
464
+ if (eventsToReplay.length === 0)
465
+ return;
466
+ // Destroy previous replayer
467
+ this.destroyReplayer();
468
+ this.replayer = new Replayer(eventsToReplay, {
469
+ root: container,
470
+ skipInactive: true,
471
+ showWarning: false,
472
+ showDebug: false,
473
+ blockClass: 'debug-panel',
474
+ });
475
+ this.replayer.play();
476
+ }
477
+ pauseReplay() {
478
+ this.replayer?.pause();
479
+ }
480
+ resumeReplay() {
481
+ this.replayer?.play();
482
+ }
483
+ destroyReplayer() {
484
+ if (this.replayer) {
485
+ try {
486
+ this.replayer.pause();
487
+ }
488
+ catch { }
489
+ this.replayer = undefined;
490
+ }
491
+ }
492
+ // ─── Export ───────────────────────────────────────────────────────────────
493
+ exportEvents() {
494
+ return JSON.stringify(this._events(), null, 2);
495
+ }
496
+ downloadEvents(filename = 'rrweb-session.json') {
497
+ const blob = new Blob([this.exportEvents()], { type: 'application/json' });
498
+ const url = URL.createObjectURL(blob);
499
+ const a = document.createElement('a');
500
+ a.href = url;
501
+ a.download = filename;
502
+ a.click();
503
+ URL.revokeObjectURL(url);
504
+ }
505
+ importEvents(json) {
506
+ try {
507
+ const events = JSON.parse(json);
508
+ this._events.set(events);
509
+ return events;
510
+ }
511
+ catch {
512
+ console.error('Invalid rrweb events JSON');
513
+ return [];
514
+ }
515
+ }
516
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: RrwebRecorderService, deps: [{ token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable }); }
517
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: RrwebRecorderService, providedIn: 'root' }); }
518
+ }
519
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: RrwebRecorderService, decorators: [{
520
+ type: Injectable,
521
+ args: [{ providedIn: 'root' }]
522
+ }], ctorParameters: () => [{ type: i0.NgZone }] });
523
+
524
+ class ActionListComponent {
525
+ constructor() {
526
+ this.session = null;
527
+ this.removeAction = new EventEmitter();
528
+ this.addNote = new EventEmitter();
529
+ this.expandedId = signal(null);
530
+ this.noteMap = {};
531
+ }
532
+ toggleExpand(id) {
533
+ this.expandedId.update(v => v === id ? null : id);
534
+ }
535
+ onRemove(id, e) {
536
+ e.stopPropagation();
537
+ this.removeAction.emit(id);
538
+ }
539
+ onAddNote(id) {
540
+ this.addNote.emit({ id, note: this.noteMap[id] ?? '' });
541
+ }
542
+ getActionIcon(type) {
543
+ const icons = {
544
+ click: '👆',
545
+ dblclick: '👆👆',
546
+ input: '⌨️',
547
+ select: '📋',
548
+ submit: '📤',
549
+ navigation: '🔗',
550
+ keypress: '⌨️',
551
+ scroll: '↕️',
552
+ hover: '🖱️',
553
+ assertion: '✅',
554
+ screenshot: '📸',
555
+ };
556
+ return icons[type] ?? '•';
557
+ }
558
+ formatTime(ts) {
559
+ return new Date(ts).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
560
+ }
561
+ formatDuration(start, end) {
562
+ const s = Math.round((end - start) / 1000);
563
+ return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`;
564
+ }
565
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ActionListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
566
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: ActionListComponent, isStandalone: true, selector: "app-action-list", inputs: { session: "session" }, outputs: { removeAction: "removeAction", addNote: "addNote" }, ngImport: i0, template: `
567
+ <div class="action-list" data-debug-panel>
568
+ @if (!session || session.actions.length === 0) {
569
+ <div class="empty-state">
570
+ <div class="empty-icon">🎬</div>
571
+ <p>Noch keine Aktionen aufgezeichnet.</p>
572
+ <p class="hint">Starte die Aufnahme und interagiere mit der App.</p>
573
+ <div class="shortcuts-hint">
574
+ <kbd>Ctrl+Shift+D</kbd> Panel &nbsp;
575
+ <kbd>Ctrl+Shift+R</kbd> Record
576
+ </div>
577
+ </div>
578
+ } @else {
579
+ <div class="list-header">
580
+ <span class="list-count">{{ session.actions.length }} Aktionen</span>
581
+ <span class="list-duration">
582
+ @if (session.endTime) {
583
+ {{ formatDuration(session.startTime, session.endTime) }}
584
+ } @else {
585
+ Live
586
+ }
587
+ </span>
588
+ </div>
589
+
590
+ @for (action of session.actions; track action.id; let i = $index) {
591
+ <div class="action-item" [class.expanded]="expandedId() === action.id">
592
+ <div class="action-row" (click)="toggleExpand(action.id)">
593
+ <span class="action-index">{{ i + 1 }}</span>
594
+ <span class="action-type-badge" [class]="'type-' + action.type">
595
+ {{ getActionIcon(action.type) }}
596
+ </span>
597
+ <div class="action-info">
598
+ <span class="action-desc">{{ action.description }}</span>
599
+ <span class="action-selector">{{ action.selector }}</span>
600
+ </div>
601
+ <span class="action-time">{{ formatTime(action.timestamp) }}</span>
602
+ <button
603
+ class="remove-btn"
604
+ data-debug-panel
605
+ title="Aktion entfernen"
606
+ (click)="onRemove(action.id, $event)"
607
+ >✕</button>
608
+ </div>
609
+
610
+ @if (expandedId() === action.id) {
611
+ <div class="action-detail" data-debug-panel>
612
+ <div class="detail-grid">
613
+ <span class="detail-label">Selector</span>
614
+ <code class="detail-value">{{ action.selector }}</code>
615
+ @if (action.value) {
616
+ <span class="detail-label">Wert</span>
617
+ <code class="detail-value">{{ action.value }}</code>
618
+ }
619
+ @if (action.element?.tagName) {
620
+ <span class="detail-label">Element</span>
621
+ <code class="detail-value">&lt;{{ action.element?.tagName }}&gt;</code>
622
+ }
623
+ <span class="detail-label">Strategie</span>
624
+ <span class="detail-value strategy-badge" [class]="'strat-' + action.selectorStrategy">
625
+ {{ action.selectorStrategy }}
626
+ </span>
627
+ <span class="detail-label">URL</span>
628
+ <code class="detail-value url-val">{{ action.url }}</code>
629
+ </div>
630
+ <div class="note-area">
631
+ <textarea
632
+ data-debug-panel
633
+ class="note-input"
634
+ [(ngModel)]="noteMap[action.id]"
635
+ placeholder="Notiz zu dieser Aktion..."
636
+ rows="2"
637
+ (blur)="onAddNote(action.id)"
638
+ ></textarea>
639
+ </div>
640
+ </div>
641
+ }
642
+ </div>
643
+ }
644
+ }
645
+ </div>
646
+ `, 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
+ }
648
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ActionListComponent, decorators: [{
649
+ type: Component,
650
+ args: [{ selector: 'app-action-list', standalone: true, imports: [CommonModule, FormsModule], template: `
651
+ <div class="action-list" data-debug-panel>
652
+ @if (!session || session.actions.length === 0) {
653
+ <div class="empty-state">
654
+ <div class="empty-icon">🎬</div>
655
+ <p>Noch keine Aktionen aufgezeichnet.</p>
656
+ <p class="hint">Starte die Aufnahme und interagiere mit der App.</p>
657
+ <div class="shortcuts-hint">
658
+ <kbd>Ctrl+Shift+D</kbd> Panel &nbsp;
659
+ <kbd>Ctrl+Shift+R</kbd> Record
660
+ </div>
661
+ </div>
662
+ } @else {
663
+ <div class="list-header">
664
+ <span class="list-count">{{ session.actions.length }} Aktionen</span>
665
+ <span class="list-duration">
666
+ @if (session.endTime) {
667
+ {{ formatDuration(session.startTime, session.endTime) }}
668
+ } @else {
669
+ Live
670
+ }
671
+ </span>
672
+ </div>
673
+
674
+ @for (action of session.actions; track action.id; let i = $index) {
675
+ <div class="action-item" [class.expanded]="expandedId() === action.id">
676
+ <div class="action-row" (click)="toggleExpand(action.id)">
677
+ <span class="action-index">{{ i + 1 }}</span>
678
+ <span class="action-type-badge" [class]="'type-' + action.type">
679
+ {{ getActionIcon(action.type) }}
680
+ </span>
681
+ <div class="action-info">
682
+ <span class="action-desc">{{ action.description }}</span>
683
+ <span class="action-selector">{{ action.selector }}</span>
684
+ </div>
685
+ <span class="action-time">{{ formatTime(action.timestamp) }}</span>
686
+ <button
687
+ class="remove-btn"
688
+ data-debug-panel
689
+ title="Aktion entfernen"
690
+ (click)="onRemove(action.id, $event)"
691
+ >✕</button>
692
+ </div>
693
+
694
+ @if (expandedId() === action.id) {
695
+ <div class="action-detail" data-debug-panel>
696
+ <div class="detail-grid">
697
+ <span class="detail-label">Selector</span>
698
+ <code class="detail-value">{{ action.selector }}</code>
699
+ @if (action.value) {
700
+ <span class="detail-label">Wert</span>
701
+ <code class="detail-value">{{ action.value }}</code>
702
+ }
703
+ @if (action.element?.tagName) {
704
+ <span class="detail-label">Element</span>
705
+ <code class="detail-value">&lt;{{ action.element?.tagName }}&gt;</code>
706
+ }
707
+ <span class="detail-label">Strategie</span>
708
+ <span class="detail-value strategy-badge" [class]="'strat-' + action.selectorStrategy">
709
+ {{ action.selectorStrategy }}
710
+ </span>
711
+ <span class="detail-label">URL</span>
712
+ <code class="detail-value url-val">{{ action.url }}</code>
713
+ </div>
714
+ <div class="note-area">
715
+ <textarea
716
+ data-debug-panel
717
+ class="note-input"
718
+ [(ngModel)]="noteMap[action.id]"
719
+ placeholder="Notiz zu dieser Aktion..."
720
+ rows="2"
721
+ (blur)="onAddNote(action.id)"
722
+ ></textarea>
723
+ </div>
724
+ </div>
725
+ }
726
+ </div>
727
+ }
728
+ }
729
+ </div>
730
+ `, 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
+ }], propDecorators: { session: [{
732
+ type: Input
733
+ }], removeAction: [{
734
+ type: Output
735
+ }], addNote: [{
736
+ type: Output
737
+ }] } });
738
+
739
+ class TestPreviewComponent {
740
+ constructor() {
741
+ this.test = null;
742
+ this.copied = signal(false);
743
+ this.highlightedCode = computed(() => {
744
+ if (!this.test)
745
+ return '';
746
+ return this.syntaxHighlight(this.test.code);
747
+ });
748
+ }
749
+ syntaxHighlight(code) {
750
+ return code
751
+ .replace(/&/g, '&amp;')
752
+ .replace(/</g, '&lt;')
753
+ .replace(/>/g, '&gt;')
754
+ // Comments
755
+ .replace(/(\/\/[^\n]*)/g, '<span class="cm">$1</span>')
756
+ // cy. commands
757
+ .replace(/\b(cy)\b/g, '<span class="cy">$1</span>')
758
+ // Keywords
759
+ .replace(/\b(describe|it|beforeEach|afterEach|before|after|context|specify|const|let|var|function|return|import|export|from|async|await|new)\b/g, '<span class="kw">$1</span>')
760
+ // Strings
761
+ .replace(/('[^']*'|"[^"]*"|`[^`]*`)/g, '<span class="str">$1</span>')
762
+ // Numbers
763
+ .replace(/\b(\d+)\b/g, '<span class="num">$1</span>');
764
+ }
765
+ async copyCode() {
766
+ if (!this.test)
767
+ return;
768
+ await navigator.clipboard.writeText(this.test.code);
769
+ this.copied.set(true);
770
+ setTimeout(() => this.copied.set(false), 2000);
771
+ }
772
+ downloadCode() {
773
+ if (!this.test)
774
+ return;
775
+ const blob = new Blob([this.test.code], { type: 'text/typescript' });
776
+ const url = URL.createObjectURL(blob);
777
+ const a = document.createElement('a');
778
+ a.href = url;
779
+ a.download = `cypress-test-${new Date().toISOString().slice(0, 10)}.cy.ts`;
780
+ a.click();
781
+ URL.revokeObjectURL(url);
782
+ }
783
+ formatDate(ts) {
784
+ return new Date(ts).toLocaleString('de-DE', {
785
+ day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit',
786
+ });
787
+ }
788
+ 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>
818
+ `, 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
+ }
820
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TestPreviewComponent, decorators: [{
821
+ 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>
851
+ `, 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
+ }], propDecorators: { test: [{
853
+ type: Input
854
+ }] } });
855
+
856
+ class SettingsDialogComponent {
857
+ constructor() {
858
+ this.close = new EventEmitter();
859
+ this.ai = inject(AiGeneratorService);
860
+ this.localUrl = this.ai.webhookUrl();
861
+ }
862
+ save() {
863
+ this.ai.setWebhookUrl(this.localUrl.trim());
864
+ this.close.emit();
865
+ }
866
+ onOverlayClick(e) {
867
+ if (e.target.classList.contains('overlay')) {
868
+ this.close.emit();
869
+ }
870
+ }
871
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SettingsDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
872
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: SettingsDialogComponent, isStandalone: true, selector: "app-settings-dialog", outputs: { close: "close" }, ngImport: i0, template: `
873
+ <div class="overlay" data-debug-panel (click)="onOverlayClick($event)">
874
+ <div class="dialog" data-debug-panel>
875
+ <div class="dialog-header">
876
+ <h2>⚙️ Einstellungen</h2>
877
+ <button class="close-btn" (click)="close.emit()">✕</button>
878
+ </div>
879
+
880
+ <div class="dialog-body">
881
+ <div class="field-group">
882
+ <label>KI Webhook-URL</label>
883
+ <input
884
+ data-debug-panel
885
+ class="field-input"
886
+ type="url"
887
+ [(ngModel)]="localUrl"
888
+ placeholder="https://deine-ki-api.de/generate"
889
+ />
890
+ <p class="field-hint">
891
+ Die aufgezeichnete Session wird als JSON per POST an diese URL gesendet.
892
+ Die Antwort wird als Cypress-Test angezeigt.
893
+ </p>
894
+ </div>
895
+
896
+ <div class="info-box">
897
+ <p>📦 POST-Body: Die Session als JSON (<code>actions</code>, <code>startUrl</code>, ...)</p>
898
+ <p>📄 Erwartete Antwort: Cypress-Test-Code als Plain-Text</p>
899
+ <p>⌨️ Shortcuts: <kbd>Ctrl+Shift+D</kbd> Panel &nbsp; <kbd>Ctrl+Shift+R</kbd> Aufnahme</p>
900
+ </div>
901
+ </div>
902
+
903
+ <div class="dialog-footer">
904
+ <button class="btn-cancel" (click)="close.emit()">Abbrechen</button>
905
+ <button class="btn-save" (click)="save()">💾 Speichern</button>
906
+ </div>
907
+ </div>
908
+ </div>
909
+ `, 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
+ }
911
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SettingsDialogComponent, decorators: [{
912
+ type: Component,
913
+ args: [{ selector: 'app-settings-dialog', standalone: true, imports: [CommonModule, FormsModule], template: `
914
+ <div class="overlay" data-debug-panel (click)="onOverlayClick($event)">
915
+ <div class="dialog" data-debug-panel>
916
+ <div class="dialog-header">
917
+ <h2>⚙️ Einstellungen</h2>
918
+ <button class="close-btn" (click)="close.emit()">✕</button>
919
+ </div>
920
+
921
+ <div class="dialog-body">
922
+ <div class="field-group">
923
+ <label>KI Webhook-URL</label>
924
+ <input
925
+ data-debug-panel
926
+ class="field-input"
927
+ type="url"
928
+ [(ngModel)]="localUrl"
929
+ placeholder="https://deine-ki-api.de/generate"
930
+ />
931
+ <p class="field-hint">
932
+ Die aufgezeichnete Session wird als JSON per POST an diese URL gesendet.
933
+ Die Antwort wird als Cypress-Test angezeigt.
934
+ </p>
935
+ </div>
936
+
937
+ <div class="info-box">
938
+ <p>📦 POST-Body: Die Session als JSON (<code>actions</code>, <code>startUrl</code>, ...)</p>
939
+ <p>📄 Erwartete Antwort: Cypress-Test-Code als Plain-Text</p>
940
+ <p>⌨️ Shortcuts: <kbd>Ctrl+Shift+D</kbd> Panel &nbsp; <kbd>Ctrl+Shift+R</kbd> Aufnahme</p>
941
+ </div>
942
+ </div>
943
+
944
+ <div class="dialog-footer">
945
+ <button class="btn-cancel" (click)="close.emit()">Abbrechen</button>
946
+ <button class="btn-save" (click)="save()">💾 Speichern</button>
947
+ </div>
948
+ </div>
949
+ </div>
950
+ `, 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
+ }], propDecorators: { close: [{
952
+ type: Output
953
+ }] } });
954
+
955
+ class SessionReplayComponent {
956
+ constructor() {
957
+ this.rrweb = inject(RrwebRecorderService);
958
+ this.overlayOpen = signal(false);
959
+ this.isPlaying = signal(false);
960
+ }
961
+ async openOverlay() {
962
+ this.overlayOpen.set(true);
963
+ this.isPlaying.set(false);
964
+ // Wait for the DOM to render the overlay
965
+ await new Promise(r => setTimeout(r, 50));
966
+ await this.startPlay();
967
+ }
968
+ closeOverlay() {
969
+ this.rrweb.destroyReplayer();
970
+ this.overlayOpen.set(false);
971
+ this.isPlaying.set(false);
972
+ }
973
+ async pauseResume() {
974
+ if (this.isPlaying()) {
975
+ this.rrweb.pauseReplay();
976
+ this.isPlaying.set(false);
977
+ }
978
+ else {
979
+ this.rrweb.resumeReplay();
980
+ this.isPlaying.set(true);
981
+ }
982
+ }
983
+ async restart() {
984
+ this.rrweb.destroyReplayer();
985
+ await new Promise(r => setTimeout(r, 50));
986
+ await this.startPlay();
987
+ }
988
+ exportSession() {
989
+ this.rrweb.downloadEvents();
990
+ }
991
+ async startPlay() {
992
+ if (!this.replayContainer)
993
+ return;
994
+ const container = this.replayContainer.nativeElement;
995
+ await this.rrweb.startReplay(container);
996
+ // Scale iframe to fill the stage container
997
+ this.scaleReplayer(container);
998
+ this.isPlaying.set(true);
999
+ }
1000
+ scaleReplayer(container) {
1001
+ // rrweb injects .replayer-wrapper with an iframe sized to the recorded viewport
1002
+ setTimeout(() => {
1003
+ const wrapper = container.querySelector('.replayer-wrapper');
1004
+ if (!wrapper)
1005
+ return;
1006
+ const iframe = wrapper.querySelector('iframe');
1007
+ const wrapW = iframe?.offsetWidth || wrapper.offsetWidth || 1280;
1008
+ const wrapH = iframe?.offsetHeight || wrapper.offsetHeight || 720;
1009
+ const stageW = container.offsetWidth;
1010
+ const stageH = container.offsetHeight;
1011
+ if (!stageW || !stageH || !wrapW || !wrapH)
1012
+ return;
1013
+ const scale = Math.min(stageW / wrapW, stageH / wrapH);
1014
+ const offsetX = (stageW - wrapW * scale) / 2;
1015
+ const offsetY = (stageH - wrapH * scale) / 2;
1016
+ wrapper.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
1017
+ }, 300);
1018
+ }
1019
+ ngOnDestroy() {
1020
+ this.rrweb.destroyReplayer();
1021
+ }
1022
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SessionReplayComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1023
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: SessionReplayComponent, isStandalone: true, selector: "app-session-replay", viewQueries: [{ propertyName: "replayContainer", first: true, predicate: ["replayContainer"], descendants: true }], ngImport: i0, template: `
1024
+ <div class="replay-panel" data-debug-panel>
1025
+ @if (!rrweb.hasEvents()) {
1026
+ <div class="replay-empty">
1027
+ <div class="replay-icon">📽️</div>
1028
+ <p>Kein Replay verfügbar.</p>
1029
+ <p class="hint">Starte eine Aufnahme — rrweb zeichnet den DOM parallel mit.</p>
1030
+ </div>
1031
+ } @else {
1032
+ <div class="replay-info">
1033
+ <span class="event-count">{{ rrweb.events().length }} Events aufgezeichnet</span>
1034
+ </div>
1035
+ <div class="replay-actions">
1036
+ <button class="replay-btn primary" (click)="openOverlay()">
1037
+ ▶ Replay abspielen
1038
+ </button>
1039
+ <button class="replay-btn" (click)="exportSession()">
1040
+ 💾 JSON exportieren
1041
+ </button>
1042
+ </div>
1043
+ <p class="replay-hint">
1044
+ Der Replay öffnet sich als Vollbild-Overlay über der aktuellen Seite.
1045
+ </p>
1046
+ }
1047
+ </div>
1048
+
1049
+ <!-- Fullscreen Replay Overlay -->
1050
+ @if (overlayOpen()) {
1051
+ <div class="replay-overlay" data-debug-panel>
1052
+ <div class="overlay-header" data-debug-panel>
1053
+ <span class="overlay-title">📽️ Session Replay</span>
1054
+ <div class="overlay-controls" data-debug-panel>
1055
+ <button class="ovl-btn" (click)="pauseResume()">
1056
+ {{ isPlaying() ? '⏸ Pause' : '▶ Play' }}
1057
+ </button>
1058
+ <button class="ovl-btn" (click)="restart()">⟳ Neustart</button>
1059
+ <button class="ovl-btn close-ovl" (click)="closeOverlay()">✕ Schließen</button>
1060
+ </div>
1061
+ </div>
1062
+ <div #replayContainer class="overlay-stage" data-debug-panel></div>
1063
+ </div>
1064
+ }
1065
+ `, 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
+ }
1067
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SessionReplayComponent, decorators: [{
1068
+ type: Component,
1069
+ args: [{ selector: 'app-session-replay', standalone: true, imports: [CommonModule], template: `
1070
+ <div class="replay-panel" data-debug-panel>
1071
+ @if (!rrweb.hasEvents()) {
1072
+ <div class="replay-empty">
1073
+ <div class="replay-icon">📽️</div>
1074
+ <p>Kein Replay verfügbar.</p>
1075
+ <p class="hint">Starte eine Aufnahme — rrweb zeichnet den DOM parallel mit.</p>
1076
+ </div>
1077
+ } @else {
1078
+ <div class="replay-info">
1079
+ <span class="event-count">{{ rrweb.events().length }} Events aufgezeichnet</span>
1080
+ </div>
1081
+ <div class="replay-actions">
1082
+ <button class="replay-btn primary" (click)="openOverlay()">
1083
+ ▶ Replay abspielen
1084
+ </button>
1085
+ <button class="replay-btn" (click)="exportSession()">
1086
+ 💾 JSON exportieren
1087
+ </button>
1088
+ </div>
1089
+ <p class="replay-hint">
1090
+ Der Replay öffnet sich als Vollbild-Overlay über der aktuellen Seite.
1091
+ </p>
1092
+ }
1093
+ </div>
1094
+
1095
+ <!-- Fullscreen Replay Overlay -->
1096
+ @if (overlayOpen()) {
1097
+ <div class="replay-overlay" data-debug-panel>
1098
+ <div class="overlay-header" data-debug-panel>
1099
+ <span class="overlay-title">📽️ Session Replay</span>
1100
+ <div class="overlay-controls" data-debug-panel>
1101
+ <button class="ovl-btn" (click)="pauseResume()">
1102
+ {{ isPlaying() ? '⏸ Pause' : '▶ Play' }}
1103
+ </button>
1104
+ <button class="ovl-btn" (click)="restart()">⟳ Neustart</button>
1105
+ <button class="ovl-btn close-ovl" (click)="closeOverlay()">✕ Schließen</button>
1106
+ </div>
1107
+ </div>
1108
+ <div #replayContainer class="overlay-stage" data-debug-panel></div>
1109
+ </div>
1110
+ }
1111
+ `, 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
+ }], propDecorators: { replayContainer: [{
1113
+ type: ViewChild,
1114
+ args: ['replayContainer']
1115
+ }] } });
1116
+
1117
+ class DebugPanelComponent {
1118
+ constructor() {
1119
+ this.recorder = inject(RecorderService);
1120
+ this.aiService = inject(AiGeneratorService);
1121
+ this.rrweb = inject(RrwebRecorderService);
1122
+ this.panelOpen = signal(false);
1123
+ this.activeTab = signal('actions');
1124
+ this.position = signal('bottom-right');
1125
+ this.panelWidth = signal(420);
1126
+ this.showSettings = signal(false);
1127
+ this.sessionName = '';
1128
+ this.sessionDesc = '';
1129
+ this.hasActions = computed(() => (this.recorder.currentSession()?.actions.length ?? 0) > 0 ||
1130
+ (this.recorder.sessions().length > 0));
1131
+ this.resizing = false;
1132
+ this.resizeStartY = 0;
1133
+ this.resizeStartH = 0;
1134
+ this.handleHotkey = (e) => {
1135
+ if (e.ctrlKey && e.shiftKey && e.key === 'D') {
1136
+ e.preventDefault();
1137
+ this.togglePanel();
1138
+ }
1139
+ if (e.ctrlKey && e.shiftKey && e.key === 'R') {
1140
+ e.preventDefault();
1141
+ if (this.recorder.isRecording()) {
1142
+ this.stopRecording();
1143
+ }
1144
+ else {
1145
+ this.startRecording();
1146
+ }
1147
+ }
1148
+ };
1149
+ }
1150
+ ngOnInit() {
1151
+ // Keyboard shortcut: Ctrl+Shift+D
1152
+ document.addEventListener('keydown', this.handleHotkey);
1153
+ }
1154
+ ngOnDestroy() {
1155
+ document.removeEventListener('keydown', this.handleHotkey);
1156
+ }
1157
+ togglePanel() {
1158
+ this.panelOpen.update(v => !v);
1159
+ if (this.panelOpen() && !this.recorder.currentSession()) {
1160
+ this.activeTab.set('actions');
1161
+ }
1162
+ }
1163
+ cyclePosition() {
1164
+ const positions = ['bottom-right', 'bottom-left', 'top-right', 'top-left'];
1165
+ const idx = positions.indexOf(this.position());
1166
+ this.position.set(positions[(idx + 1) % positions.length]);
1167
+ }
1168
+ startRecording() {
1169
+ this.recorder.startRecording(this.sessionName || undefined, this.sessionDesc || undefined);
1170
+ // Start rrweb in parallel for visual replay
1171
+ this.rrweb.startRecording();
1172
+ this.activeTab.set('actions');
1173
+ }
1174
+ stopRecording() {
1175
+ const session = this.recorder.stopRecording();
1176
+ this.rrweb.stopRecording();
1177
+ if (session) {
1178
+ this.activeTab.set('actions');
1179
+ }
1180
+ }
1181
+ async generateTest() {
1182
+ const session = this.recorder.currentSession() ?? this.recorder.sessions().at(-1);
1183
+ if (!session)
1184
+ return;
1185
+ await this.aiService.generateCypressTest(session);
1186
+ this.activeTab.set('test');
1187
+ }
1188
+ async loadAndGenerate(session) {
1189
+ await this.aiService.generateCypressTest(session);
1190
+ this.activeTab.set('test');
1191
+ }
1192
+ // Drag to reposition
1193
+ startDrag(e) {
1194
+ if (e.target.closest('button'))
1195
+ return;
1196
+ const panel = e.currentTarget.parentElement;
1197
+ const startX = e.clientX - panel.getBoundingClientRect().left;
1198
+ const startY = e.clientY - panel.getBoundingClientRect().top;
1199
+ const onMove = (ev) => {
1200
+ panel.style.left = `${ev.clientX - startX}px`;
1201
+ panel.style.top = `${ev.clientY - startY}px`;
1202
+ panel.style.right = 'auto';
1203
+ panel.style.bottom = 'auto';
1204
+ };
1205
+ const onUp = () => {
1206
+ document.removeEventListener('mousemove', onMove);
1207
+ document.removeEventListener('mouseup', onUp);
1208
+ };
1209
+ document.addEventListener('mousemove', onMove);
1210
+ document.addEventListener('mouseup', onUp);
1211
+ }
1212
+ // Resize height
1213
+ startResize(e) {
1214
+ const panel = e.currentTarget.parentElement;
1215
+ this.resizeStartY = e.clientY;
1216
+ this.resizeStartH = panel.getBoundingClientRect().height;
1217
+ const onMove = (ev) => {
1218
+ const newH = Math.max(250, this.resizeStartH + (ev.clientY - this.resizeStartY));
1219
+ panel.style.maxHeight = `${newH}px`;
1220
+ };
1221
+ const onUp = () => {
1222
+ document.removeEventListener('mousemove', onMove);
1223
+ document.removeEventListener('mouseup', onUp);
1224
+ };
1225
+ document.addEventListener('mousemove', onMove);
1226
+ document.addEventListener('mouseup', onUp);
1227
+ e.preventDefault();
1228
+ }
1229
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DebugPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1230
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: DebugPanelComponent, isStandalone: true, selector: "app-debug-panel", ngImport: i0, template: `
1231
+ <!-- Toggle FAB -->
1232
+ <button
1233
+ data-debug-panel
1234
+ class="debug-fab"
1235
+ [class.recording]="recorder.isRecording()"
1236
+ [class.pulse]="recorder.isRecording() && !recorder.isPaused()"
1237
+ (click)="togglePanel()"
1238
+ [title]="panelOpen() ? 'Debug Panel schließen' : 'Debug Panel öffnen'"
1239
+ >
1240
+ <span class="fab-icon">{{ recorder.isRecording() ? '⏺' : '🐛' }}</span>
1241
+ @if (recorder.isRecording() && recorder.actionCount() > 0) {
1242
+ <span class="fab-badge">{{ recorder.actionCount() }}</span>
1243
+ }
1244
+ </button>
1245
+
1246
+ <!-- Main Panel -->
1247
+ @if (panelOpen()) {
1248
+ <div
1249
+ data-debug-panel
1250
+ class="debug-panel"
1251
+ [class]="'pos-' + position()"
1252
+ [style.width.px]="panelWidth()"
1253
+ >
1254
+ <!-- Header -->
1255
+ <div class="panel-header" (mousedown)="startDrag($event)">
1256
+ <div class="header-left">
1257
+ <span class="panel-icon">🐛</span>
1258
+ <span class="panel-title">Debug Recorder</span>
1259
+ @if (recorder.isRecording()) {
1260
+ <span class="rec-indicator" [class.paused]="recorder.isPaused()">
1261
+ {{ recorder.isPaused() ? '⏸ PAUSED' : '⏺ REC' }}
1262
+ </span>
1263
+ }
1264
+ </div>
1265
+ <div class="header-actions">
1266
+ <button class="icon-btn" title="Einstellungen" (click)="showSettings.set(true)">⚙️</button>
1267
+ <button class="icon-btn" title="Position wechseln" (click)="cyclePosition()">📌</button>
1268
+ <button class="icon-btn" title="Schließen" (click)="togglePanel()">✕</button>
1269
+ </div>
1270
+ </div>
1271
+
1272
+ <!-- Session Name Input (only when not recording) -->
1273
+ @if (!recorder.isRecording()) {
1274
+ <div class="session-setup">
1275
+ <input
1276
+ data-debug-panel
1277
+ class="session-input"
1278
+ type="text"
1279
+ [(ngModel)]="sessionName"
1280
+ placeholder="Session-Name (optional)"
1281
+ />
1282
+ <textarea
1283
+ data-debug-panel
1284
+ class="session-desc"
1285
+ [(ngModel)]="sessionDesc"
1286
+ placeholder="Fehlerbeschreibung..."
1287
+ rows="2"
1288
+ ></textarea>
1289
+ </div>
1290
+ }
1291
+
1292
+ <!-- Recording Controls -->
1293
+ <div class="recording-controls" data-debug-panel>
1294
+ @if (!recorder.isRecording()) {
1295
+ <button class="ctrl-btn start" (click)="startRecording()">
1296
+ ▶ Aufnahme starten
1297
+ </button>
1298
+ } @else {
1299
+ <button class="ctrl-btn pause" (click)="recorder.pauseRecording()">
1300
+ {{ recorder.isPaused() ? '▶ Fortsetzen' : '⏸ Pause' }}
1301
+ </button>
1302
+ <button class="ctrl-btn stop" (click)="stopRecording()">
1303
+ ⏹ Stoppen
1304
+ </button>
1305
+ <button class="ctrl-btn clear" title="Aktionen löschen" (click)="recorder.clearCurrentSession()">
1306
+ 🗑
1307
+ </button>
1308
+ }
1309
+
1310
+ @if (hasActions()) {
1311
+ <button
1312
+ class="ctrl-btn generate"
1313
+ [disabled]="aiService.isGenerating() || !aiService.webhookUrl()"
1314
+ [title]="!aiService.webhookUrl() ? 'Webhook-URL in Einstellungen eintragen' : 'Cypress Test generieren'"
1315
+ (click)="generateTest()"
1316
+ >
1317
+ @if (aiService.isGenerating()) {
1318
+ <span class="spinner">⟳</span> Generiere...
1319
+ } @else {
1320
+ 🤖 → Cypress Test
1321
+ }
1322
+ </button>
1323
+ }
1324
+ </div>
1325
+
1326
+ <!-- Error Banner -->
1327
+ @if (aiService.error()) {
1328
+ <div class="error-banner" data-debug-panel>
1329
+ ⚠️ {{ aiService.error() }}
1330
+ </div>
1331
+ }
1332
+
1333
+ <!-- Tabs -->
1334
+ <div class="tab-bar" data-debug-panel>
1335
+ <button
1336
+ class="tab-btn"
1337
+ [class.active]="activeTab() === 'actions'"
1338
+ (click)="activeTab.set('actions')"
1339
+ >
1340
+ Aktionen
1341
+ @if (recorder.actionCount() > 0) {
1342
+ <span class="tab-badge">{{ recorder.actionCount() }}</span>
1343
+ }
1344
+ </button>
1345
+ <button
1346
+ class="tab-btn"
1347
+ [class.active]="activeTab() === 'replay'"
1348
+ (click)="activeTab.set('replay')"
1349
+ title="rrweb Session Replay"
1350
+ >
1351
+ 📽️ Replay
1352
+ @if (rrweb.events().length > 0) {
1353
+ <span class="tab-badge">{{ rrweb.events().length }}</span>
1354
+ }
1355
+ </button>
1356
+ <button
1357
+ class="tab-btn"
1358
+ [class.active]="activeTab() === 'test'"
1359
+ [disabled]="!aiService.lastTest()"
1360
+ (click)="activeTab.set('test')"
1361
+ >
1362
+ 🤖 Test
1363
+ @if (aiService.lastTest()) {
1364
+ <span class="tab-badge new">NEU</span>
1365
+ }
1366
+ </button>
1367
+ <button
1368
+ class="tab-btn"
1369
+ [class.active]="activeTab() === 'sessions'"
1370
+ [disabled]="recorder.sessions().length === 0"
1371
+ (click)="activeTab.set('sessions')"
1372
+ >
1373
+ Sessions ({{ recorder.sessions().length }})
1374
+ </button>
1375
+ </div>
1376
+
1377
+ <!-- Tab Content -->
1378
+ <div class="tab-content">
1379
+ @if (activeTab() === 'actions') {
1380
+ <app-action-list
1381
+ [session]="recorder.currentSession()"
1382
+ (removeAction)="recorder.removeAction($event)"
1383
+ (addNote)="recorder.addNote($event.id, $event.note)"
1384
+ />
1385
+ }
1386
+
1387
+ @if (activeTab() === 'replay') {
1388
+ <app-session-replay />
1389
+ }
1390
+
1391
+ @if (activeTab() === 'test') {
1392
+ <app-test-preview [test]="aiService.lastTest()" />
1393
+ }
1394
+
1395
+ @if (activeTab() === 'sessions') {
1396
+ <div class="sessions-list">
1397
+ @for (session of recorder.sessions(); track session.id) {
1398
+ <div class="session-card">
1399
+ <div class="session-card-header">
1400
+ <span class="session-name">{{ session.name }}</span>
1401
+ <span class="session-meta">{{ session.actions.length }} Aktionen</span>
1402
+ </div>
1403
+ <div class="session-card-actions">
1404
+ <button class="sm-btn" (click)="loadAndGenerate(session)">🤖 Neu generieren</button>
1405
+ <button class="sm-btn danger" (click)="recorder.deleteSession(session.id)">🗑</button>
1406
+ </div>
1407
+ </div>
1408
+ }
1409
+ </div>
1410
+ }
1411
+ </div>
1412
+
1413
+ <!-- Resize Handle -->
1414
+ <div class="resize-handle" (mousedown)="startResize($event)">⠿</div>
1415
+ </div>
1416
+ }
1417
+
1418
+ <!-- Settings Dialog -->
1419
+ @if (showSettings()) {
1420
+ <app-settings-dialog (close)="showSettings.set(false)" />
1421
+ }
1422
+ `, 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
+ }
1424
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DebugPanelComponent, decorators: [{
1425
+ type: Component,
1426
+ args: [{ selector: 'app-debug-panel', standalone: true, imports: [CommonModule, FormsModule, ActionListComponent, TestPreviewComponent, SettingsDialogComponent, SessionReplayComponent], template: `
1427
+ <!-- Toggle FAB -->
1428
+ <button
1429
+ data-debug-panel
1430
+ class="debug-fab"
1431
+ [class.recording]="recorder.isRecording()"
1432
+ [class.pulse]="recorder.isRecording() && !recorder.isPaused()"
1433
+ (click)="togglePanel()"
1434
+ [title]="panelOpen() ? 'Debug Panel schließen' : 'Debug Panel öffnen'"
1435
+ >
1436
+ <span class="fab-icon">{{ recorder.isRecording() ? '⏺' : '🐛' }}</span>
1437
+ @if (recorder.isRecording() && recorder.actionCount() > 0) {
1438
+ <span class="fab-badge">{{ recorder.actionCount() }}</span>
1439
+ }
1440
+ </button>
1441
+
1442
+ <!-- Main Panel -->
1443
+ @if (panelOpen()) {
1444
+ <div
1445
+ data-debug-panel
1446
+ class="debug-panel"
1447
+ [class]="'pos-' + position()"
1448
+ [style.width.px]="panelWidth()"
1449
+ >
1450
+ <!-- Header -->
1451
+ <div class="panel-header" (mousedown)="startDrag($event)">
1452
+ <div class="header-left">
1453
+ <span class="panel-icon">🐛</span>
1454
+ <span class="panel-title">Debug Recorder</span>
1455
+ @if (recorder.isRecording()) {
1456
+ <span class="rec-indicator" [class.paused]="recorder.isPaused()">
1457
+ {{ recorder.isPaused() ? '⏸ PAUSED' : '⏺ REC' }}
1458
+ </span>
1459
+ }
1460
+ </div>
1461
+ <div class="header-actions">
1462
+ <button class="icon-btn" title="Einstellungen" (click)="showSettings.set(true)">⚙️</button>
1463
+ <button class="icon-btn" title="Position wechseln" (click)="cyclePosition()">📌</button>
1464
+ <button class="icon-btn" title="Schließen" (click)="togglePanel()">✕</button>
1465
+ </div>
1466
+ </div>
1467
+
1468
+ <!-- Session Name Input (only when not recording) -->
1469
+ @if (!recorder.isRecording()) {
1470
+ <div class="session-setup">
1471
+ <input
1472
+ data-debug-panel
1473
+ class="session-input"
1474
+ type="text"
1475
+ [(ngModel)]="sessionName"
1476
+ placeholder="Session-Name (optional)"
1477
+ />
1478
+ <textarea
1479
+ data-debug-panel
1480
+ class="session-desc"
1481
+ [(ngModel)]="sessionDesc"
1482
+ placeholder="Fehlerbeschreibung..."
1483
+ rows="2"
1484
+ ></textarea>
1485
+ </div>
1486
+ }
1487
+
1488
+ <!-- Recording Controls -->
1489
+ <div class="recording-controls" data-debug-panel>
1490
+ @if (!recorder.isRecording()) {
1491
+ <button class="ctrl-btn start" (click)="startRecording()">
1492
+ ▶ Aufnahme starten
1493
+ </button>
1494
+ } @else {
1495
+ <button class="ctrl-btn pause" (click)="recorder.pauseRecording()">
1496
+ {{ recorder.isPaused() ? '▶ Fortsetzen' : '⏸ Pause' }}
1497
+ </button>
1498
+ <button class="ctrl-btn stop" (click)="stopRecording()">
1499
+ ⏹ Stoppen
1500
+ </button>
1501
+ <button class="ctrl-btn clear" title="Aktionen löschen" (click)="recorder.clearCurrentSession()">
1502
+ 🗑
1503
+ </button>
1504
+ }
1505
+
1506
+ @if (hasActions()) {
1507
+ <button
1508
+ class="ctrl-btn generate"
1509
+ [disabled]="aiService.isGenerating() || !aiService.webhookUrl()"
1510
+ [title]="!aiService.webhookUrl() ? 'Webhook-URL in Einstellungen eintragen' : 'Cypress Test generieren'"
1511
+ (click)="generateTest()"
1512
+ >
1513
+ @if (aiService.isGenerating()) {
1514
+ <span class="spinner">⟳</span> Generiere...
1515
+ } @else {
1516
+ 🤖 → Cypress Test
1517
+ }
1518
+ </button>
1519
+ }
1520
+ </div>
1521
+
1522
+ <!-- Error Banner -->
1523
+ @if (aiService.error()) {
1524
+ <div class="error-banner" data-debug-panel>
1525
+ ⚠️ {{ aiService.error() }}
1526
+ </div>
1527
+ }
1528
+
1529
+ <!-- Tabs -->
1530
+ <div class="tab-bar" data-debug-panel>
1531
+ <button
1532
+ class="tab-btn"
1533
+ [class.active]="activeTab() === 'actions'"
1534
+ (click)="activeTab.set('actions')"
1535
+ >
1536
+ Aktionen
1537
+ @if (recorder.actionCount() > 0) {
1538
+ <span class="tab-badge">{{ recorder.actionCount() }}</span>
1539
+ }
1540
+ </button>
1541
+ <button
1542
+ class="tab-btn"
1543
+ [class.active]="activeTab() === 'replay'"
1544
+ (click)="activeTab.set('replay')"
1545
+ title="rrweb Session Replay"
1546
+ >
1547
+ 📽️ Replay
1548
+ @if (rrweb.events().length > 0) {
1549
+ <span class="tab-badge">{{ rrweb.events().length }}</span>
1550
+ }
1551
+ </button>
1552
+ <button
1553
+ class="tab-btn"
1554
+ [class.active]="activeTab() === 'test'"
1555
+ [disabled]="!aiService.lastTest()"
1556
+ (click)="activeTab.set('test')"
1557
+ >
1558
+ 🤖 Test
1559
+ @if (aiService.lastTest()) {
1560
+ <span class="tab-badge new">NEU</span>
1561
+ }
1562
+ </button>
1563
+ <button
1564
+ class="tab-btn"
1565
+ [class.active]="activeTab() === 'sessions'"
1566
+ [disabled]="recorder.sessions().length === 0"
1567
+ (click)="activeTab.set('sessions')"
1568
+ >
1569
+ Sessions ({{ recorder.sessions().length }})
1570
+ </button>
1571
+ </div>
1572
+
1573
+ <!-- Tab Content -->
1574
+ <div class="tab-content">
1575
+ @if (activeTab() === 'actions') {
1576
+ <app-action-list
1577
+ [session]="recorder.currentSession()"
1578
+ (removeAction)="recorder.removeAction($event)"
1579
+ (addNote)="recorder.addNote($event.id, $event.note)"
1580
+ />
1581
+ }
1582
+
1583
+ @if (activeTab() === 'replay') {
1584
+ <app-session-replay />
1585
+ }
1586
+
1587
+ @if (activeTab() === 'test') {
1588
+ <app-test-preview [test]="aiService.lastTest()" />
1589
+ }
1590
+
1591
+ @if (activeTab() === 'sessions') {
1592
+ <div class="sessions-list">
1593
+ @for (session of recorder.sessions(); track session.id) {
1594
+ <div class="session-card">
1595
+ <div class="session-card-header">
1596
+ <span class="session-name">{{ session.name }}</span>
1597
+ <span class="session-meta">{{ session.actions.length }} Aktionen</span>
1598
+ </div>
1599
+ <div class="session-card-actions">
1600
+ <button class="sm-btn" (click)="loadAndGenerate(session)">🤖 Neu generieren</button>
1601
+ <button class="sm-btn danger" (click)="recorder.deleteSession(session.id)">🗑</button>
1602
+ </div>
1603
+ </div>
1604
+ }
1605
+ </div>
1606
+ }
1607
+ </div>
1608
+
1609
+ <!-- Resize Handle -->
1610
+ <div class="resize-handle" (mousedown)="startResize($event)">⠿</div>
1611
+ </div>
1612
+ }
1613
+
1614
+ <!-- Settings Dialog -->
1615
+ @if (showSettings()) {
1616
+ <app-settings-dialog (close)="showSettings.set(false)" />
1617
+ }
1618
+ `, 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
+ }] });
1620
+
1621
+ /*
1622
+ * Public API Surface of debug-recorder
1623
+ */
1624
+ // Main entry component — add this to your Angular app
1625
+
1626
+ /**
1627
+ * Generated bundle index. Do not edit.
1628
+ */
1629
+
1630
+ export { ActionListComponent, AiGeneratorService, DebugPanelComponent, RecorderService, RrwebRecorderService, SessionReplayComponent, SettingsDialogComponent, TestPreviewComponent };
1631
+ //# sourceMappingURL=angular-debug-recorder.mjs.map