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