@triagly/sdk 1.3.0 → 1.4.0-beta.2

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/dist/index.js CHANGED
@@ -4,6 +4,953 @@
4
4
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Triagly = {}));
5
5
  })(this, (function (exports) { 'use strict';
6
6
 
7
+ // Screenshot Capture Module
8
+ // Dynamically loads html2canvas and captures the page
9
+ // CDN URLs for html2canvas with fallbacks
10
+ const HTML2CANVAS_CDN_URLS = [
11
+ 'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js',
12
+ 'https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js',
13
+ 'https://unpkg.com/html2canvas@1.4.1/dist/html2canvas.min.js',
14
+ ];
15
+ class ScreenshotCapture {
16
+ constructor(config = {}) {
17
+ this.html2canvasLoaded = false;
18
+ this.loadPromise = null;
19
+ this.quality = config.quality ?? 0.8;
20
+ this.maxWidth = config.maxWidth ?? 1920;
21
+ this.options = config.html2canvasOptions ?? {};
22
+ this.customCdnUrl = config.html2canvasCdnUrl;
23
+ }
24
+ /**
25
+ * Dynamically load html2canvas from CDN with fallbacks
26
+ * Returns true if loaded successfully, false otherwise
27
+ */
28
+ async loadHtml2Canvas() {
29
+ // Return cached result if already attempted
30
+ if (this.html2canvasLoaded && window.html2canvas) {
31
+ return true;
32
+ }
33
+ // Return existing promise if load is in progress
34
+ if (this.loadPromise) {
35
+ return this.loadPromise;
36
+ }
37
+ // Check if already available in window (user may have loaded it)
38
+ if (window.html2canvas) {
39
+ this.html2canvasLoaded = true;
40
+ return true;
41
+ }
42
+ // Build list of CDNs to try (custom URL first if provided)
43
+ const cdnUrls = this.customCdnUrl
44
+ ? [this.customCdnUrl, ...HTML2CANVAS_CDN_URLS]
45
+ : HTML2CANVAS_CDN_URLS;
46
+ // Try loading from CDNs with fallback
47
+ this.loadPromise = this.tryLoadFromCdns(cdnUrls);
48
+ return this.loadPromise;
49
+ }
50
+ /**
51
+ * Try loading html2canvas from multiple CDN URLs
52
+ */
53
+ async tryLoadFromCdns(urls) {
54
+ for (const url of urls) {
55
+ const success = await this.loadScriptFromUrl(url);
56
+ if (success && window.html2canvas) {
57
+ this.html2canvasLoaded = true;
58
+ return true;
59
+ }
60
+ }
61
+ console.error('[Triagly] Failed to load html2canvas from all CDN sources');
62
+ return false;
63
+ }
64
+ /**
65
+ * Load a script from a specific URL
66
+ */
67
+ loadScriptFromUrl(url) {
68
+ return new Promise((resolve) => {
69
+ const script = document.createElement('script');
70
+ script.src = url;
71
+ script.async = true;
72
+ const cleanup = () => {
73
+ script.onload = null;
74
+ script.onerror = null;
75
+ };
76
+ script.onload = () => {
77
+ cleanup();
78
+ resolve(true);
79
+ };
80
+ script.onerror = () => {
81
+ cleanup();
82
+ // Remove failed script from DOM
83
+ script.remove();
84
+ resolve(false);
85
+ };
86
+ document.head.appendChild(script);
87
+ });
88
+ }
89
+ /**
90
+ * Capture screenshot of the current page
91
+ * Returns base64 data URL of the screenshot
92
+ */
93
+ async capture() {
94
+ const loaded = await this.loadHtml2Canvas();
95
+ if (!loaded || !window.html2canvas) {
96
+ throw new Error('Screenshot capture requires html2canvas. ' +
97
+ 'Include <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script> ' +
98
+ 'or ensure network access to CDN.');
99
+ }
100
+ // Hide the Triagly widget elements during capture
101
+ const widgetElements = document.querySelectorAll('.triagly-overlay, .triagly-button, #triagly-button');
102
+ const originalVisibility = new Map();
103
+ widgetElements.forEach((el) => {
104
+ const htmlEl = el;
105
+ originalVisibility.set(el, htmlEl.style.visibility);
106
+ htmlEl.style.visibility = 'hidden';
107
+ });
108
+ try {
109
+ const canvas = await window.html2canvas(document.body, {
110
+ useCORS: true,
111
+ allowTaint: false,
112
+ logging: false,
113
+ backgroundColor: null,
114
+ scale: Math.min(window.devicePixelRatio || 1, 2), // Cap at 2x for performance
115
+ ...this.options,
116
+ });
117
+ // Resize if needed
118
+ const resized = this.resizeCanvas(canvas);
119
+ // Convert to JPEG data URL
120
+ return resized.toDataURL('image/jpeg', this.quality);
121
+ }
122
+ finally {
123
+ // Restore widget visibility
124
+ widgetElements.forEach((el) => {
125
+ const htmlEl = el;
126
+ htmlEl.style.visibility = originalVisibility.get(el) || '';
127
+ });
128
+ }
129
+ }
130
+ /**
131
+ * Resize canvas if it exceeds maxWidth
132
+ */
133
+ resizeCanvas(canvas) {
134
+ if (canvas.width <= this.maxWidth) {
135
+ return canvas;
136
+ }
137
+ const ratio = this.maxWidth / canvas.width;
138
+ const resized = document.createElement('canvas');
139
+ resized.width = this.maxWidth;
140
+ resized.height = Math.round(canvas.height * ratio);
141
+ const ctx = resized.getContext('2d');
142
+ if (ctx) {
143
+ // Use high-quality image smoothing
144
+ ctx.imageSmoothingEnabled = true;
145
+ ctx.imageSmoothingQuality = 'high';
146
+ ctx.drawImage(canvas, 0, 0, resized.width, resized.height);
147
+ }
148
+ return resized;
149
+ }
150
+ /**
151
+ * Get approximate file size of a data URL in bytes
152
+ */
153
+ static getDataUrlSize(dataUrl) {
154
+ // Remove data URL prefix to get base64 content
155
+ const base64 = dataUrl.split(',')[1] || '';
156
+ // Base64 encodes 3 bytes into 4 characters
157
+ return Math.round((base64.length * 3) / 4);
158
+ }
159
+ }
160
+
161
+ // Annotation Editor Module
162
+ // Canvas-based annotation overlay for marking up screenshots
163
+ class AnnotationEditor {
164
+ constructor(imageData, onComplete, onCancel) {
165
+ this.overlay = null;
166
+ this.canvas = null;
167
+ this.ctx = null;
168
+ this.image = null;
169
+ this.annotations = [];
170
+ this.currentTool = 'highlight';
171
+ this.currentColor = '#ff0000';
172
+ this.strokeWidth = 3;
173
+ this.isDrawing = false;
174
+ this.currentAnnotation = null;
175
+ this.scale = 1;
176
+ this.offsetX = 0;
177
+ this.offsetY = 0;
178
+ this.pendingTextPoint = null;
179
+ this.handleKeyDown = (e) => {
180
+ if (e.key === 'Escape') {
181
+ this.close(true);
182
+ }
183
+ };
184
+ this.imageData = imageData;
185
+ this.onComplete = onComplete;
186
+ this.onCancel = onCancel;
187
+ }
188
+ /**
189
+ * Open the annotation editor
190
+ */
191
+ async open() {
192
+ // Load the image first
193
+ this.image = await this.loadImage(this.imageData);
194
+ // Create the overlay UI
195
+ this.createOverlay();
196
+ // Set up the canvas
197
+ this.setupCanvas();
198
+ // Draw the image
199
+ this.render();
200
+ }
201
+ /**
202
+ * Load image from data URL
203
+ */
204
+ loadImage(dataUrl) {
205
+ return new Promise((resolve, reject) => {
206
+ const img = new Image();
207
+ img.onload = () => resolve(img);
208
+ img.onerror = () => reject(new Error('Failed to load image'));
209
+ img.src = dataUrl;
210
+ });
211
+ }
212
+ /**
213
+ * Create the fullscreen overlay UI
214
+ */
215
+ createOverlay() {
216
+ this.overlay = document.createElement('div');
217
+ this.overlay.className = 'triagly-annotation-overlay';
218
+ this.overlay.innerHTML = `
219
+ <div class="triagly-annotation-header">
220
+ <div class="triagly-annotation-tools">
221
+ <button type="button" class="triagly-tool-btn active" data-tool="highlight" title="Highlight">
222
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
223
+ <rect x="3" y="3" width="18" height="18" rx="2" opacity="0.3" fill="currentColor"/>
224
+ <rect x="3" y="3" width="18" height="18" rx="2"/>
225
+ </svg>
226
+ </button>
227
+ <button type="button" class="triagly-tool-btn" data-tool="arrow" title="Arrow">
228
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
229
+ <line x1="5" y1="19" x2="19" y2="5"/>
230
+ <polyline points="10,5 19,5 19,14"/>
231
+ </svg>
232
+ </button>
233
+ <button type="button" class="triagly-tool-btn" data-tool="freehand" title="Draw">
234
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
235
+ <path d="M12 19l7-7 3 3-7 7-3-3z"/>
236
+ <path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/>
237
+ <path d="M2 2l7.586 7.586"/>
238
+ </svg>
239
+ </button>
240
+ <button type="button" class="triagly-tool-btn" data-tool="text" title="Text">
241
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
242
+ <polyline points="4 7 4 4 20 4 20 7"/>
243
+ <line x1="9" y1="20" x2="15" y2="20"/>
244
+ <line x1="12" y1="4" x2="12" y2="20"/>
245
+ </svg>
246
+ </button>
247
+ <div class="triagly-tool-divider"></div>
248
+ <input type="color" class="triagly-color-picker" value="#ff0000" title="Color">
249
+ </div>
250
+ <div class="triagly-annotation-actions">
251
+ <button type="button" class="triagly-annotation-btn triagly-btn-clear" title="Clear All">
252
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
253
+ <polyline points="3 6 5 6 21 6"/>
254
+ <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
255
+ </svg>
256
+ Clear
257
+ </button>
258
+ <button type="button" class="triagly-annotation-btn triagly-btn-cancel">Cancel</button>
259
+ <button type="button" class="triagly-annotation-btn triagly-btn-done">Done</button>
260
+ </div>
261
+ </div>
262
+ <div class="triagly-annotation-canvas-container">
263
+ <canvas class="triagly-annotation-canvas"></canvas>
264
+ </div>
265
+ <div class="triagly-text-modal" id="triagly-text-modal" style="display: none;">
266
+ <div class="triagly-text-modal-content">
267
+ <h3>Add Text Annotation</h3>
268
+ <input type="text" id="triagly-text-input" placeholder="Enter text..." maxlength="200" autofocus />
269
+ <div class="triagly-text-modal-actions">
270
+ <button type="button" class="triagly-annotation-btn triagly-btn-cancel" id="triagly-text-cancel">Cancel</button>
271
+ <button type="button" class="triagly-annotation-btn triagly-btn-done" id="triagly-text-confirm">Add Text</button>
272
+ </div>
273
+ </div>
274
+ </div>
275
+ `;
276
+ // Inject styles
277
+ this.injectStyles();
278
+ // Add event listeners
279
+ this.setupEventListeners();
280
+ document.body.appendChild(this.overlay);
281
+ }
282
+ /**
283
+ * Set up the canvas with correct dimensions
284
+ */
285
+ setupCanvas() {
286
+ if (!this.overlay || !this.image)
287
+ return;
288
+ this.canvas = this.overlay.querySelector('.triagly-annotation-canvas');
289
+ const container = this.overlay.querySelector('.triagly-annotation-canvas-container');
290
+ if (!this.canvas || !container)
291
+ return;
292
+ this.ctx = this.canvas.getContext('2d');
293
+ if (!this.ctx)
294
+ return;
295
+ // Calculate scale to fit image in container
296
+ const containerRect = container.getBoundingClientRect();
297
+ const maxWidth = containerRect.width - 40; // Padding
298
+ const maxHeight = containerRect.height - 40;
299
+ const imageAspect = this.image.width / this.image.height;
300
+ const containerAspect = maxWidth / maxHeight;
301
+ let canvasWidth;
302
+ let canvasHeight;
303
+ if (imageAspect > containerAspect) {
304
+ // Image is wider - fit to width
305
+ canvasWidth = Math.min(this.image.width, maxWidth);
306
+ canvasHeight = canvasWidth / imageAspect;
307
+ }
308
+ else {
309
+ // Image is taller - fit to height
310
+ canvasHeight = Math.min(this.image.height, maxHeight);
311
+ canvasWidth = canvasHeight * imageAspect;
312
+ }
313
+ this.scale = canvasWidth / this.image.width;
314
+ this.canvas.width = canvasWidth;
315
+ this.canvas.height = canvasHeight;
316
+ this.canvas.style.width = `${canvasWidth}px`;
317
+ this.canvas.style.height = `${canvasHeight}px`;
318
+ // Calculate offset for centering
319
+ this.offsetX = (containerRect.width - canvasWidth) / 2;
320
+ this.offsetY = (containerRect.height - canvasHeight) / 2;
321
+ }
322
+ /**
323
+ * Set up event listeners
324
+ */
325
+ setupEventListeners() {
326
+ if (!this.overlay || !this.canvas)
327
+ return;
328
+ // Tool selection
329
+ const toolBtns = this.overlay.querySelectorAll('.triagly-tool-btn');
330
+ toolBtns.forEach((btn) => {
331
+ btn.addEventListener('click', (e) => {
332
+ const tool = e.currentTarget.dataset.tool;
333
+ if (tool) {
334
+ this.currentTool = tool;
335
+ toolBtns.forEach((b) => b.classList.remove('active'));
336
+ e.currentTarget.classList.add('active');
337
+ }
338
+ });
339
+ });
340
+ // Color picker
341
+ const colorPicker = this.overlay.querySelector('.triagly-color-picker');
342
+ if (colorPicker) {
343
+ colorPicker.addEventListener('change', (e) => {
344
+ this.currentColor = e.target.value;
345
+ });
346
+ }
347
+ // Clear button
348
+ const clearBtn = this.overlay.querySelector('.triagly-btn-clear');
349
+ if (clearBtn) {
350
+ clearBtn.addEventListener('click', () => {
351
+ this.annotations = [];
352
+ this.render();
353
+ });
354
+ }
355
+ // Cancel button
356
+ const cancelBtn = this.overlay.querySelector('.triagly-btn-cancel');
357
+ if (cancelBtn) {
358
+ cancelBtn.addEventListener('click', () => this.close(true));
359
+ }
360
+ // Done button
361
+ const doneBtn = this.overlay.querySelector('.triagly-btn-done');
362
+ if (doneBtn) {
363
+ doneBtn.addEventListener('click', () => this.close(false));
364
+ }
365
+ // Canvas drawing events
366
+ this.canvas.addEventListener('mousedown', (e) => this.handleMouseDown(e));
367
+ this.canvas.addEventListener('mousemove', (e) => this.handleMouseMove(e));
368
+ this.canvas.addEventListener('mouseup', () => this.handleMouseUp());
369
+ this.canvas.addEventListener('mouseleave', () => this.handleMouseUp());
370
+ // Touch support
371
+ this.canvas.addEventListener('touchstart', (e) => {
372
+ e.preventDefault();
373
+ const touch = e.touches[0];
374
+ this.handleMouseDown(touch);
375
+ });
376
+ this.canvas.addEventListener('touchmove', (e) => {
377
+ e.preventDefault();
378
+ const touch = e.touches[0];
379
+ this.handleMouseMove(touch);
380
+ });
381
+ this.canvas.addEventListener('touchend', () => this.handleMouseUp());
382
+ this.canvas.addEventListener('touchcancel', () => this.handleMouseUp());
383
+ // Text modal event listeners
384
+ const textConfirmBtn = this.overlay.querySelector('#triagly-text-confirm');
385
+ const textCancelBtn = this.overlay.querySelector('#triagly-text-cancel');
386
+ const textInput = this.overlay.querySelector('#triagly-text-input');
387
+ textConfirmBtn?.addEventListener('click', () => this.confirmTextAnnotation());
388
+ textCancelBtn?.addEventListener('click', () => this.hideTextModal());
389
+ textInput?.addEventListener('keydown', (e) => {
390
+ if (e.key === 'Enter') {
391
+ e.preventDefault();
392
+ this.confirmTextAnnotation();
393
+ }
394
+ else if (e.key === 'Escape') {
395
+ e.preventDefault();
396
+ this.hideTextModal();
397
+ }
398
+ });
399
+ // Escape key to cancel
400
+ document.addEventListener('keydown', this.handleKeyDown);
401
+ }
402
+ /**
403
+ * Get canvas coordinates from mouse event
404
+ */
405
+ getCanvasPoint(e) {
406
+ if (!this.canvas)
407
+ return { x: 0, y: 0 };
408
+ const rect = this.canvas.getBoundingClientRect();
409
+ return {
410
+ x: e.clientX - rect.left,
411
+ y: e.clientY - rect.top,
412
+ };
413
+ }
414
+ /**
415
+ * Handle mouse down - start drawing
416
+ */
417
+ handleMouseDown(e) {
418
+ const point = this.getCanvasPoint(e);
419
+ this.isDrawing = true;
420
+ if (this.currentTool === 'text') {
421
+ // For text, show custom modal input
422
+ this.pendingTextPoint = point;
423
+ this.showTextModal();
424
+ this.isDrawing = false;
425
+ return;
426
+ }
427
+ this.currentAnnotation = {
428
+ type: this.currentTool,
429
+ color: this.currentColor,
430
+ strokeWidth: this.strokeWidth,
431
+ points: [point],
432
+ };
433
+ }
434
+ /**
435
+ * Show the text input modal
436
+ */
437
+ showTextModal() {
438
+ if (!this.overlay)
439
+ return;
440
+ const modal = this.overlay.querySelector('#triagly-text-modal');
441
+ const input = this.overlay.querySelector('#triagly-text-input');
442
+ if (modal && input) {
443
+ modal.style.display = 'flex';
444
+ input.value = '';
445
+ input.focus();
446
+ }
447
+ }
448
+ /**
449
+ * Hide the text input modal
450
+ */
451
+ hideTextModal() {
452
+ if (!this.overlay)
453
+ return;
454
+ const modal = this.overlay.querySelector('#triagly-text-modal');
455
+ if (modal) {
456
+ modal.style.display = 'none';
457
+ }
458
+ this.pendingTextPoint = null;
459
+ }
460
+ /**
461
+ * Confirm text annotation from modal
462
+ */
463
+ confirmTextAnnotation() {
464
+ if (!this.overlay || !this.pendingTextPoint)
465
+ return;
466
+ const input = this.overlay.querySelector('#triagly-text-input');
467
+ const text = input?.value.trim();
468
+ if (text) {
469
+ this.annotations.push({
470
+ type: 'text',
471
+ color: this.currentColor,
472
+ strokeWidth: this.strokeWidth,
473
+ points: [this.pendingTextPoint],
474
+ text,
475
+ });
476
+ this.render();
477
+ }
478
+ this.hideTextModal();
479
+ }
480
+ /**
481
+ * Handle mouse move - continue drawing
482
+ */
483
+ handleMouseMove(e) {
484
+ if (!this.isDrawing || !this.currentAnnotation)
485
+ return;
486
+ const point = this.getCanvasPoint(e);
487
+ if (this.currentTool === 'freehand') {
488
+ // Add point to path
489
+ this.currentAnnotation.points.push(point);
490
+ }
491
+ else {
492
+ // For highlight and arrow, just update the end point
493
+ if (this.currentAnnotation.points.length === 1) {
494
+ this.currentAnnotation.points.push(point);
495
+ }
496
+ else {
497
+ this.currentAnnotation.points[1] = point;
498
+ }
499
+ }
500
+ this.render();
501
+ }
502
+ /**
503
+ * Handle mouse up - finish drawing
504
+ */
505
+ handleMouseUp() {
506
+ if (this.isDrawing && this.currentAnnotation && this.currentAnnotation.points.length > 1) {
507
+ this.annotations.push(this.currentAnnotation);
508
+ }
509
+ this.isDrawing = false;
510
+ this.currentAnnotation = null;
511
+ this.render();
512
+ }
513
+ /**
514
+ * Render the canvas with image and annotations
515
+ */
516
+ render() {
517
+ if (!this.ctx || !this.canvas || !this.image)
518
+ return;
519
+ // Clear canvas
520
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
521
+ // Draw the image
522
+ this.ctx.drawImage(this.image, 0, 0, this.canvas.width, this.canvas.height);
523
+ // Draw all annotations
524
+ for (const annotation of this.annotations) {
525
+ this.drawAnnotation(annotation);
526
+ }
527
+ // Draw current annotation being made
528
+ if (this.currentAnnotation) {
529
+ this.drawAnnotation(this.currentAnnotation);
530
+ }
531
+ }
532
+ /**
533
+ * Draw a single annotation
534
+ */
535
+ drawAnnotation(annotation) {
536
+ if (!this.ctx)
537
+ return;
538
+ this.ctx.strokeStyle = annotation.color;
539
+ this.ctx.fillStyle = annotation.color;
540
+ this.ctx.lineWidth = annotation.strokeWidth;
541
+ this.ctx.lineCap = 'round';
542
+ this.ctx.lineJoin = 'round';
543
+ switch (annotation.type) {
544
+ case 'highlight':
545
+ this.drawHighlight(annotation);
546
+ break;
547
+ case 'arrow':
548
+ this.drawArrow(annotation);
549
+ break;
550
+ case 'freehand':
551
+ this.drawFreehand(annotation);
552
+ break;
553
+ case 'text':
554
+ this.drawText(annotation);
555
+ break;
556
+ }
557
+ }
558
+ /**
559
+ * Draw a semi-transparent highlight rectangle
560
+ */
561
+ drawHighlight(annotation) {
562
+ if (!this.ctx || annotation.points.length < 2)
563
+ return;
564
+ const [start, end] = annotation.points;
565
+ const width = end.x - start.x;
566
+ const height = end.y - start.y;
567
+ // Semi-transparent fill
568
+ this.ctx.globalAlpha = 0.3;
569
+ this.ctx.fillRect(start.x, start.y, width, height);
570
+ // Solid border
571
+ this.ctx.globalAlpha = 1;
572
+ this.ctx.strokeRect(start.x, start.y, width, height);
573
+ }
574
+ /**
575
+ * Draw an arrow from start to end
576
+ */
577
+ drawArrow(annotation) {
578
+ if (!this.ctx || annotation.points.length < 2)
579
+ return;
580
+ const [start, end] = annotation.points;
581
+ const headLength = 15;
582
+ const angle = Math.atan2(end.y - start.y, end.x - start.x);
583
+ // Draw the line
584
+ this.ctx.beginPath();
585
+ this.ctx.moveTo(start.x, start.y);
586
+ this.ctx.lineTo(end.x, end.y);
587
+ this.ctx.stroke();
588
+ // Draw the arrow head
589
+ this.ctx.beginPath();
590
+ this.ctx.moveTo(end.x, end.y);
591
+ this.ctx.lineTo(end.x - headLength * Math.cos(angle - Math.PI / 6), end.y - headLength * Math.sin(angle - Math.PI / 6));
592
+ this.ctx.moveTo(end.x, end.y);
593
+ this.ctx.lineTo(end.x - headLength * Math.cos(angle + Math.PI / 6), end.y - headLength * Math.sin(angle + Math.PI / 6));
594
+ this.ctx.stroke();
595
+ }
596
+ /**
597
+ * Draw a freehand path
598
+ */
599
+ drawFreehand(annotation) {
600
+ if (!this.ctx || annotation.points.length < 2)
601
+ return;
602
+ this.ctx.beginPath();
603
+ this.ctx.moveTo(annotation.points[0].x, annotation.points[0].y);
604
+ for (let i = 1; i < annotation.points.length; i++) {
605
+ this.ctx.lineTo(annotation.points[i].x, annotation.points[i].y);
606
+ }
607
+ this.ctx.stroke();
608
+ }
609
+ /**
610
+ * Draw text annotation
611
+ */
612
+ drawText(annotation) {
613
+ if (!this.ctx || !annotation.text || annotation.points.length < 1)
614
+ return;
615
+ const point = annotation.points[0];
616
+ // Text settings
617
+ this.ctx.font = 'bold 16px sans-serif';
618
+ this.ctx.textBaseline = 'top';
619
+ // Measure text for background
620
+ const metrics = this.ctx.measureText(annotation.text);
621
+ const padding = 4;
622
+ // Draw background
623
+ this.ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
624
+ this.ctx.fillRect(point.x - padding, point.y - padding, metrics.width + padding * 2, 20 + padding * 2);
625
+ // Draw text
626
+ this.ctx.fillStyle = '#ffffff';
627
+ this.ctx.fillText(annotation.text, point.x, point.y);
628
+ }
629
+ /**
630
+ * Close the editor
631
+ */
632
+ close(cancelled) {
633
+ document.removeEventListener('keydown', this.handleKeyDown);
634
+ if (cancelled) {
635
+ this.onCancel();
636
+ }
637
+ else {
638
+ // Generate final annotated image
639
+ const annotatedImage = this.renderFinalImage();
640
+ this.onComplete(annotatedImage);
641
+ }
642
+ // Remove overlay
643
+ if (this.overlay) {
644
+ this.overlay.remove();
645
+ this.overlay = null;
646
+ }
647
+ }
648
+ /**
649
+ * Render the final annotated image at full resolution
650
+ */
651
+ renderFinalImage() {
652
+ if (!this.image)
653
+ return this.imageData;
654
+ // Create a full-resolution canvas
655
+ const canvas = document.createElement('canvas');
656
+ canvas.width = this.image.width;
657
+ canvas.height = this.image.height;
658
+ const ctx = canvas.getContext('2d');
659
+ if (!ctx)
660
+ return this.imageData;
661
+ // Draw original image
662
+ ctx.drawImage(this.image, 0, 0);
663
+ // Calculate scale factor
664
+ const scaleFactor = 1 / this.scale;
665
+ // Draw annotations at full resolution
666
+ for (const annotation of this.annotations) {
667
+ ctx.strokeStyle = annotation.color;
668
+ ctx.fillStyle = annotation.color;
669
+ ctx.lineWidth = annotation.strokeWidth * scaleFactor;
670
+ ctx.lineCap = 'round';
671
+ ctx.lineJoin = 'round';
672
+ // Scale points
673
+ const scaledPoints = annotation.points.map((p) => ({
674
+ x: p.x * scaleFactor,
675
+ y: p.y * scaleFactor,
676
+ }));
677
+ const scaledAnnotation = { ...annotation, points: scaledPoints };
678
+ switch (annotation.type) {
679
+ case 'highlight':
680
+ this.drawHighlightOnCtx(ctx, scaledAnnotation);
681
+ break;
682
+ case 'arrow':
683
+ this.drawArrowOnCtx(ctx, scaledAnnotation, scaleFactor);
684
+ break;
685
+ case 'freehand':
686
+ this.drawFreehandOnCtx(ctx, scaledAnnotation);
687
+ break;
688
+ case 'text':
689
+ this.drawTextOnCtx(ctx, scaledAnnotation, scaleFactor);
690
+ break;
691
+ }
692
+ }
693
+ return canvas.toDataURL('image/jpeg', 0.9);
694
+ }
695
+ drawHighlightOnCtx(ctx, annotation) {
696
+ if (annotation.points.length < 2)
697
+ return;
698
+ const [start, end] = annotation.points;
699
+ const width = end.x - start.x;
700
+ const height = end.y - start.y;
701
+ ctx.globalAlpha = 0.3;
702
+ ctx.fillRect(start.x, start.y, width, height);
703
+ ctx.globalAlpha = 1;
704
+ ctx.strokeRect(start.x, start.y, width, height);
705
+ }
706
+ drawArrowOnCtx(ctx, annotation, scaleFactor) {
707
+ if (annotation.points.length < 2)
708
+ return;
709
+ const [start, end] = annotation.points;
710
+ const headLength = 15 * scaleFactor;
711
+ const angle = Math.atan2(end.y - start.y, end.x - start.x);
712
+ ctx.beginPath();
713
+ ctx.moveTo(start.x, start.y);
714
+ ctx.lineTo(end.x, end.y);
715
+ ctx.stroke();
716
+ ctx.beginPath();
717
+ ctx.moveTo(end.x, end.y);
718
+ ctx.lineTo(end.x - headLength * Math.cos(angle - Math.PI / 6), end.y - headLength * Math.sin(angle - Math.PI / 6));
719
+ ctx.moveTo(end.x, end.y);
720
+ ctx.lineTo(end.x - headLength * Math.cos(angle + Math.PI / 6), end.y - headLength * Math.sin(angle + Math.PI / 6));
721
+ ctx.stroke();
722
+ }
723
+ drawFreehandOnCtx(ctx, annotation) {
724
+ if (annotation.points.length < 2)
725
+ return;
726
+ ctx.beginPath();
727
+ ctx.moveTo(annotation.points[0].x, annotation.points[0].y);
728
+ for (let i = 1; i < annotation.points.length; i++) {
729
+ ctx.lineTo(annotation.points[i].x, annotation.points[i].y);
730
+ }
731
+ ctx.stroke();
732
+ }
733
+ drawTextOnCtx(ctx, annotation, scaleFactor) {
734
+ if (!annotation.text || annotation.points.length < 1)
735
+ return;
736
+ const point = annotation.points[0];
737
+ const fontSize = 16 * scaleFactor;
738
+ ctx.font = `bold ${fontSize}px sans-serif`;
739
+ ctx.textBaseline = 'top';
740
+ const metrics = ctx.measureText(annotation.text);
741
+ const padding = 4 * scaleFactor;
742
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
743
+ ctx.fillRect(point.x - padding, point.y - padding, metrics.width + padding * 2, fontSize * 1.2 + padding * 2);
744
+ ctx.fillStyle = '#ffffff';
745
+ ctx.fillText(annotation.text, point.x, point.y);
746
+ }
747
+ /**
748
+ * Inject annotation editor styles
749
+ */
750
+ injectStyles() {
751
+ const styleId = 'triagly-annotation-styles';
752
+ if (document.getElementById(styleId))
753
+ return;
754
+ const style = document.createElement('style');
755
+ style.id = styleId;
756
+ style.textContent = `
757
+ .triagly-annotation-overlay {
758
+ position: fixed;
759
+ top: 0;
760
+ left: 0;
761
+ right: 0;
762
+ bottom: 0;
763
+ z-index: 999999;
764
+ background: rgba(0, 0, 0, 0.9);
765
+ display: flex;
766
+ flex-direction: column;
767
+ }
768
+
769
+ .triagly-annotation-header {
770
+ display: flex;
771
+ justify-content: space-between;
772
+ align-items: center;
773
+ padding: 12px 20px;
774
+ background: #1a1a1a;
775
+ border-bottom: 1px solid #333;
776
+ }
777
+
778
+ .triagly-annotation-tools {
779
+ display: flex;
780
+ align-items: center;
781
+ gap: 8px;
782
+ }
783
+
784
+ .triagly-tool-btn {
785
+ display: flex;
786
+ align-items: center;
787
+ justify-content: center;
788
+ width: 40px;
789
+ height: 40px;
790
+ border: none;
791
+ border-radius: 8px;
792
+ background: #333;
793
+ color: #fff;
794
+ cursor: pointer;
795
+ transition: all 0.2s;
796
+ }
797
+
798
+ .triagly-tool-btn:hover {
799
+ background: #444;
800
+ }
801
+
802
+ .triagly-tool-btn.active {
803
+ background: #0066cc;
804
+ }
805
+
806
+ .triagly-tool-divider {
807
+ width: 1px;
808
+ height: 24px;
809
+ background: #444;
810
+ margin: 0 8px;
811
+ }
812
+
813
+ .triagly-color-picker {
814
+ width: 40px;
815
+ height: 40px;
816
+ border: none;
817
+ border-radius: 8px;
818
+ cursor: pointer;
819
+ padding: 0;
820
+ background: none;
821
+ }
822
+
823
+ .triagly-color-picker::-webkit-color-swatch-wrapper {
824
+ padding: 4px;
825
+ }
826
+
827
+ .triagly-color-picker::-webkit-color-swatch {
828
+ border: none;
829
+ border-radius: 4px;
830
+ }
831
+
832
+ .triagly-annotation-actions {
833
+ display: flex;
834
+ gap: 8px;
835
+ }
836
+
837
+ .triagly-annotation-btn {
838
+ padding: 8px 16px;
839
+ border: none;
840
+ border-radius: 8px;
841
+ font-size: 14px;
842
+ font-weight: 500;
843
+ cursor: pointer;
844
+ transition: all 0.2s;
845
+ display: flex;
846
+ align-items: center;
847
+ gap: 6px;
848
+ }
849
+
850
+ .triagly-btn-clear {
851
+ background: transparent;
852
+ color: #999;
853
+ border: 1px solid #444;
854
+ }
855
+
856
+ .triagly-btn-clear:hover {
857
+ background: #333;
858
+ color: #fff;
859
+ }
860
+
861
+ .triagly-btn-cancel {
862
+ background: #333;
863
+ color: #fff;
864
+ }
865
+
866
+ .triagly-btn-cancel:hover {
867
+ background: #444;
868
+ }
869
+
870
+ .triagly-btn-done {
871
+ background: #0066cc;
872
+ color: #fff;
873
+ }
874
+
875
+ .triagly-btn-done:hover {
876
+ background: #0055aa;
877
+ }
878
+
879
+ .triagly-annotation-canvas-container {
880
+ flex: 1;
881
+ display: flex;
882
+ align-items: center;
883
+ justify-content: center;
884
+ overflow: hidden;
885
+ padding: 20px;
886
+ }
887
+
888
+ .triagly-annotation-canvas {
889
+ cursor: crosshair;
890
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
891
+ border-radius: 4px;
892
+ }
893
+
894
+ .triagly-text-modal {
895
+ position: absolute;
896
+ top: 0;
897
+ left: 0;
898
+ right: 0;
899
+ bottom: 0;
900
+ background: rgba(0, 0, 0, 0.7);
901
+ display: flex;
902
+ align-items: center;
903
+ justify-content: center;
904
+ z-index: 1000000;
905
+ }
906
+
907
+ .triagly-text-modal-content {
908
+ background: #1a1a1a;
909
+ border-radius: 12px;
910
+ padding: 24px;
911
+ min-width: 320px;
912
+ max-width: 400px;
913
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
914
+ }
915
+
916
+ .triagly-text-modal-content h3 {
917
+ color: #fff;
918
+ font-size: 16px;
919
+ font-weight: 600;
920
+ margin: 0 0 16px 0;
921
+ }
922
+
923
+ .triagly-text-modal-content input {
924
+ width: 100%;
925
+ padding: 12px;
926
+ border: 1px solid #444;
927
+ border-radius: 8px;
928
+ background: #2a2a2a;
929
+ color: #fff;
930
+ font-size: 14px;
931
+ outline: none;
932
+ box-sizing: border-box;
933
+ }
934
+
935
+ .triagly-text-modal-content input:focus {
936
+ border-color: #0066cc;
937
+ }
938
+
939
+ .triagly-text-modal-content input::placeholder {
940
+ color: #666;
941
+ }
942
+
943
+ .triagly-text-modal-actions {
944
+ display: flex;
945
+ gap: 8px;
946
+ justify-content: flex-end;
947
+ margin-top: 16px;
948
+ }
949
+ `;
950
+ document.head.appendChild(style);
951
+ }
952
+ }
953
+
7
954
  // Feedback Widget UI
8
955
  class FeedbackWidget {
9
956
  constructor(config) {
@@ -11,6 +958,8 @@
11
958
  this.isOpen = false;
12
959
  this.previouslyFocusedElement = null;
13
960
  this.focusableElements = [];
961
+ this.screenshotDataUrl = null;
962
+ this.screenshotCapture = null;
14
963
  this.config = config;
15
964
  }
16
965
  /**
@@ -23,6 +972,13 @@
23
972
  if (this.config.turnstileSiteKey) {
24
973
  this.loadTurnstileScript();
25
974
  }
975
+ // Initialize screenshot capture if enabled
976
+ if (this.config.enableScreenshot) {
977
+ this.screenshotCapture = new ScreenshotCapture({
978
+ quality: this.config.screenshotQuality,
979
+ maxWidth: this.config.screenshotMaxWidth,
980
+ });
981
+ }
26
982
  }
27
983
  /**
28
984
  * Load Cloudflare Turnstile script dynamically
@@ -152,6 +1108,7 @@
152
1108
  this.container.remove();
153
1109
  this.container = null;
154
1110
  this.isOpen = false;
1111
+ this.screenshotDataUrl = null;
155
1112
  // Restore focus to previously focused element
156
1113
  if (this.previouslyFocusedElement) {
157
1114
  this.previouslyFocusedElement.focus();
@@ -226,6 +1183,41 @@
226
1183
  />
227
1184
  </div>
228
1185
 
1186
+ ${this.config.enableScreenshot ? `
1187
+ <div class="triagly-field triagly-screenshot-field">
1188
+ <label>Screenshot (optional)</label>
1189
+ <div class="triagly-screenshot-controls" id="triagly-screenshot-controls">
1190
+ <button type="button" class="triagly-btn-capture" id="triagly-capture-btn">
1191
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1192
+ <rect x="3" y="3" width="18" height="18" rx="2"/>
1193
+ <circle cx="8.5" cy="8.5" r="1.5"/>
1194
+ <path d="M21 15l-5-5L5 21"/>
1195
+ </svg>
1196
+ ${this.config.screenshotButtonText || 'Capture Screenshot'}
1197
+ </button>
1198
+ </div>
1199
+ <div class="triagly-screenshot-preview" id="triagly-screenshot-preview" style="display: none;">
1200
+ <img id="triagly-screenshot-img" alt="Screenshot preview" />
1201
+ <div class="triagly-screenshot-actions">
1202
+ ${this.config.enableAnnotation !== false ? `
1203
+ <button type="button" class="triagly-btn-icon" id="triagly-annotate-btn" title="Add annotations">
1204
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1205
+ <path d="M12 19l7-7 3 3-7 7-3-3z"/>
1206
+ <path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/>
1207
+ </svg>
1208
+ </button>
1209
+ ` : ''}
1210
+ <button type="button" class="triagly-btn-icon triagly-btn-danger" id="triagly-remove-screenshot-btn" title="Remove screenshot">
1211
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1212
+ <line x1="18" y1="6" x2="6" y2="18"/>
1213
+ <line x1="6" y1="6" x2="18" y2="18"/>
1214
+ </svg>
1215
+ </button>
1216
+ </div>
1217
+ </div>
1218
+ </div>
1219
+ ` : ''}
1220
+
229
1221
  ${this.config.turnstileSiteKey ? `
230
1222
  <div class="triagly-field triagly-turnstile">
231
1223
  <div class="cf-turnstile" data-sitekey="${this.config.turnstileSiteKey}" data-theme="light"></div>
@@ -249,6 +1241,10 @@
249
1241
  e.preventDefault();
250
1242
  this.handleSubmit(form);
251
1243
  };
1244
+ // Set up screenshot event handlers
1245
+ if (this.config.enableScreenshot) {
1246
+ this.setupScreenshotHandlers(form);
1247
+ }
252
1248
  const footer = document.createElement('div');
253
1249
  footer.className = 'triagly-footer';
254
1250
  footer.innerHTML = `
@@ -303,6 +1299,80 @@
303
1299
  console.error('Triagly: Failed to render Turnstile widget:', error instanceof Error ? error.message : 'Unknown error');
304
1300
  }
305
1301
  }
1302
+ /**
1303
+ * Set up screenshot capture event handlers
1304
+ */
1305
+ setupScreenshotHandlers(form) {
1306
+ const captureBtn = form.querySelector('#triagly-capture-btn');
1307
+ const annotateBtn = form.querySelector('#triagly-annotate-btn');
1308
+ const removeBtn = form.querySelector('#triagly-remove-screenshot-btn');
1309
+ const preview = form.querySelector('#triagly-screenshot-preview');
1310
+ const controls = form.querySelector('#triagly-screenshot-controls');
1311
+ const previewImg = form.querySelector('#triagly-screenshot-img');
1312
+ // Capture screenshot
1313
+ captureBtn?.addEventListener('click', async () => {
1314
+ if (!this.screenshotCapture)
1315
+ return;
1316
+ const btn = captureBtn;
1317
+ const originalText = btn.innerHTML;
1318
+ btn.disabled = true;
1319
+ btn.innerHTML = `
1320
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" class="triagly-spin">
1321
+ <path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" opacity="0.3"/>
1322
+ <path d="M12 2v4"/>
1323
+ </svg>
1324
+ Capturing...
1325
+ `;
1326
+ try {
1327
+ const dataUrl = await this.screenshotCapture.capture();
1328
+ this.screenshotDataUrl = dataUrl;
1329
+ // Update UI
1330
+ if (previewImg)
1331
+ previewImg.src = dataUrl;
1332
+ if (preview)
1333
+ preview.style.display = 'block';
1334
+ if (controls)
1335
+ controls.style.display = 'none';
1336
+ // Call callback if provided
1337
+ if (this.config.onScreenshotCapture) {
1338
+ this.config.onScreenshotCapture(dataUrl);
1339
+ }
1340
+ }
1341
+ catch (error) {
1342
+ console.error('Triagly: Screenshot capture failed:', error);
1343
+ if (this.config.onScreenshotError) {
1344
+ this.config.onScreenshotError(error instanceof Error ? error : new Error(String(error)));
1345
+ }
1346
+ }
1347
+ finally {
1348
+ btn.disabled = false;
1349
+ btn.innerHTML = originalText;
1350
+ }
1351
+ });
1352
+ // Annotate screenshot
1353
+ annotateBtn?.addEventListener('click', () => {
1354
+ if (!this.screenshotDataUrl)
1355
+ return;
1356
+ const editor = new AnnotationEditor(this.screenshotDataUrl, (annotatedImage) => {
1357
+ this.screenshotDataUrl = annotatedImage;
1358
+ if (previewImg)
1359
+ previewImg.src = annotatedImage;
1360
+ }, () => {
1361
+ // Cancelled - do nothing
1362
+ });
1363
+ editor.open();
1364
+ });
1365
+ // Remove screenshot
1366
+ removeBtn?.addEventListener('click', () => {
1367
+ this.screenshotDataUrl = null;
1368
+ if (preview)
1369
+ preview.style.display = 'none';
1370
+ if (controls)
1371
+ controls.style.display = 'block';
1372
+ if (previewImg)
1373
+ previewImg.src = '';
1374
+ });
1375
+ }
306
1376
  /**
307
1377
  * Handle form submission
308
1378
  */
@@ -327,6 +1397,7 @@
327
1397
  reporterName: nameInput.value.trim() || undefined,
328
1398
  reporterEmail: emailInput.value.trim() || undefined,
329
1399
  turnstileToken,
1400
+ screenshot: this.screenshotDataUrl || undefined,
330
1401
  };
331
1402
  // Create a promise that waits for actual submission result
332
1403
  const submissionPromise = new Promise((resolve, reject) => {
@@ -733,6 +1804,102 @@
733
1804
  .triagly-branding strong {
734
1805
  font-weight: 600;
735
1806
  }
1807
+
1808
+ /* Screenshot styles */
1809
+ .triagly-screenshot-field {
1810
+ margin-bottom: 16px;
1811
+ }
1812
+
1813
+ .triagly-screenshot-controls {
1814
+ display: flex;
1815
+ gap: 8px;
1816
+ }
1817
+
1818
+ .triagly-btn-capture {
1819
+ display: inline-flex;
1820
+ align-items: center;
1821
+ gap: 8px;
1822
+ padding: 10px 16px;
1823
+ background: var(--triagly-btn-secondary-bg, #f3f4f6);
1824
+ color: var(--triagly-btn-secondary-text, #374151);
1825
+ border: 1px dashed var(--triagly-input-border, #d1d5db);
1826
+ border-radius: var(--triagly-input-radius, 6px);
1827
+ font-size: 14px;
1828
+ font-weight: 500;
1829
+ cursor: pointer;
1830
+ transition: all 0.2s;
1831
+ width: 100%;
1832
+ justify-content: center;
1833
+ }
1834
+
1835
+ .triagly-btn-capture:hover:not(:disabled) {
1836
+ background: var(--triagly-btn-secondary-bg-hover, #e5e7eb);
1837
+ border-style: solid;
1838
+ }
1839
+
1840
+ .triagly-btn-capture:disabled {
1841
+ opacity: 0.7;
1842
+ cursor: wait;
1843
+ }
1844
+
1845
+ .triagly-screenshot-preview {
1846
+ position: relative;
1847
+ border-radius: var(--triagly-input-radius, 6px);
1848
+ overflow: hidden;
1849
+ border: 1px solid var(--triagly-input-border, #d1d5db);
1850
+ background: var(--triagly-input-bg, #ffffff);
1851
+ }
1852
+
1853
+ .triagly-screenshot-preview img {
1854
+ display: block;
1855
+ width: 100%;
1856
+ max-height: 200px;
1857
+ object-fit: contain;
1858
+ background: #f9fafb;
1859
+ }
1860
+
1861
+ .triagly-screenshot-actions {
1862
+ position: absolute;
1863
+ top: 8px;
1864
+ right: 8px;
1865
+ display: flex;
1866
+ gap: 4px;
1867
+ }
1868
+
1869
+ .triagly-btn-icon {
1870
+ display: flex;
1871
+ align-items: center;
1872
+ justify-content: center;
1873
+ width: 28px;
1874
+ height: 28px;
1875
+ padding: 0;
1876
+ background: rgba(255, 255, 255, 0.9);
1877
+ border: 1px solid var(--triagly-input-border, #d1d5db);
1878
+ border-radius: 6px;
1879
+ cursor: pointer;
1880
+ transition: all 0.2s;
1881
+ color: #374151;
1882
+ }
1883
+
1884
+ .triagly-btn-icon:hover {
1885
+ background: #ffffff;
1886
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
1887
+ }
1888
+
1889
+ .triagly-btn-icon.triagly-btn-danger:hover {
1890
+ background: #fee2e2;
1891
+ border-color: #fca5a5;
1892
+ color: #dc2626;
1893
+ }
1894
+
1895
+ @keyframes triagly-spin {
1896
+ from { transform: rotate(0deg); }
1897
+ to { transform: rotate(360deg); }
1898
+ }
1899
+
1900
+ .triagly-spin {
1901
+ animation: triagly-spin 1s linear infinite;
1902
+ }
736
1903
  `;
737
1904
  document.head.appendChild(style);
738
1905
  }
@@ -904,6 +2071,7 @@
904
2071
  tags: data.tags,
905
2072
  reporterEmail: data.reporterEmail,
906
2073
  reporterName: data.reporterName,
2074
+ screenshot: data.screenshot,
907
2075
  turnstileToken,
908
2076
  hardenedToken,
909
2077
  };
@@ -1269,6 +2437,7 @@
1269
2437
  description: data.description,
1270
2438
  reporterEmail: data.reporterEmail,
1271
2439
  consoleLogs: this.consoleLogger?.getLogs(),
2440
+ screenshot: data.screenshot,
1272
2441
  };
1273
2442
  // Submit to API with Turnstile token if provided
1274
2443
  const response = await this.api.submitFeedback(feedbackData, metadata, data.turnstileToken);