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.
- package/README.md +24 -0
- package/esm2022/angular-debug-recorder.mjs +5 -0
- package/esm2022/lib/action-list/action-list.component.mjs +220 -0
- package/esm2022/lib/debug-panel/debug-panel.component.mjs +516 -0
- package/esm2022/lib/models/recorded-action.model.mjs +2 -0
- package/esm2022/lib/services/ai-generator.service.mjs +61 -0
- package/esm2022/lib/services/recorder.service.mjs +354 -0
- package/esm2022/lib/services/rrweb-recorder.service.mjs +108 -0
- package/esm2022/lib/session-replay/session-replay.component.mjs +166 -0
- package/esm2022/lib/settings-dialog/settings-dialog.component.mjs +105 -0
- package/esm2022/lib/test-preview/test-preview.component.mjs +120 -0
- package/esm2022/public-api.mjs +17 -0
- package/fesm2022/angular-debug-recorder.mjs +1631 -0
- package/fesm2022/angular-debug-recorder.mjs.map +1 -0
- package/index.d.ts +5 -0
- package/lib/action-list/action-list.component.d.ts +21 -0
- package/lib/debug-panel/debug-panel.component.d.ts +38 -0
- package/lib/models/recorded-action.model.d.ts +45 -0
- package/lib/services/ai-generator.service.d.ts +20 -0
- package/lib/services/recorder.service.d.ts +46 -0
- package/lib/services/rrweb-recorder.service.d.ts +27 -0
- package/lib/session-replay/session-replay.component.d.ts +19 -0
- package/lib/settings-dialog/settings-dialog.component.d.ts +11 -0
- package/lib/test-preview/test-preview.component.d.ts +13 -0
- package/package.json +37 -0
- package/public-api.d.ts +9 -0
|
@@ -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
|
|
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>
|
|
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
|
|
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>
|
|
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, '&')
|
|
752
|
+
.replace(/</g, '<')
|
|
753
|
+
.replace(/>/g, '>')
|
|
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 <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 <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
|