@sailfish-ai/recorder 1.7.9 → 1.7.12-alpha5

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/modal.js ADDED
@@ -0,0 +1,628 @@
1
+ import { createTriageFromRecorder } from "./graphql";
2
+ let modalEl = null;
3
+ let currentState = { mode: "lookback", description: "" };
4
+ let recordingStartTime = null;
5
+ let recordingEndTime = null;
6
+ let timerInterval = null;
7
+ let isRecording = false;
8
+ let resolveSessionId = null;
9
+ let apiKey = null;
10
+ let backendApi = null;
11
+ function isTypingInInput() {
12
+ const el = document.activeElement;
13
+ return (el instanceof HTMLInputElement ||
14
+ el instanceof HTMLTextAreaElement ||
15
+ (el instanceof HTMLElement && el.isContentEditable));
16
+ }
17
+ export function setupIssueReporting(options) {
18
+ apiKey = options.apiKey;
19
+ backendApi = options.backendApi;
20
+ resolveSessionId = options.getSessionId;
21
+ // Attach keyboard shortcuts
22
+ window.addEventListener("keydown", (e) => {
23
+ // 1. Ignore if user is typing in input
24
+ if (isTypingInInput())
25
+ return;
26
+ // 2. Normalize keys
27
+ const key = e.key.toLowerCase();
28
+ const isCmdOrCtrl = e.metaKey || e.ctrlKey;
29
+ // 3. Shortcuts for modal open
30
+ if (options.enableShortcuts) {
31
+ if (key === "o") {
32
+ e.preventDefault();
33
+ injectModalHTML("lookback");
34
+ return;
35
+ }
36
+ if (key === "c") {
37
+ e.preventDefault();
38
+ injectModalHTML("startnow");
39
+ return;
40
+ }
41
+ }
42
+ // 4. Inside modal actions
43
+ const modalOpen = !!document.getElementById("sf-report-issue-modal");
44
+ if (!modalOpen && !isRecording)
45
+ return;
46
+ // (a) Cmd/Ctrl + Enter → submit
47
+ if (isCmdOrCtrl && key === "enter" && modalOpen) {
48
+ const submitBtn = document.getElementById("sf-issue-submit-btn");
49
+ if (submitBtn && !submitBtn.disabled) {
50
+ e.preventDefault();
51
+ submitBtn.click();
52
+ }
53
+ }
54
+ // (b) Cmd/Ctrl + Esc → stop recording
55
+ if (isCmdOrCtrl && key === "escape" && isRecording) {
56
+ e.preventDefault();
57
+ stopRecording();
58
+ }
59
+ // (c) "r" to start recording (only in capture-new mode)
60
+ if (key === "r" && modalOpen && currentState.mode === "startnow") {
61
+ const recordIcon = document.getElementById("sf-record-icon");
62
+ if (recordIcon) {
63
+ e.preventDefault();
64
+ recordIcon.click();
65
+ }
66
+ }
67
+ });
68
+ }
69
+ function getSessionIdSafely() {
70
+ if (!resolveSessionId) {
71
+ throw new Error("getSessionId not defined");
72
+ }
73
+ return resolveSessionId();
74
+ }
75
+ export function openReportIssueModal() {
76
+ if (isRecording) {
77
+ stopRecording();
78
+ return;
79
+ }
80
+ injectModalHTML();
81
+ if (modalEl)
82
+ document.body.appendChild(modalEl);
83
+ }
84
+ function closeModal() {
85
+ if (modalEl?.parentNode) {
86
+ modalEl.parentNode.removeChild(modalEl);
87
+ }
88
+ modalEl = null;
89
+ if (!isRecording) {
90
+ currentState = { mode: "lookback", description: "" };
91
+ recordingStartTime = null;
92
+ recordingEndTime = null;
93
+ }
94
+ if (timerInterval)
95
+ clearInterval(timerInterval);
96
+ }
97
+ function injectModalHTML(initialMode = "lookback") {
98
+ if (modalEl) {
99
+ modalEl.remove();
100
+ modalEl = null;
101
+ }
102
+ modalEl = document.createElement("div");
103
+ modalEl.id = "sf-report-issue-modal";
104
+ const isStartNow = initialMode === "startnow";
105
+ modalEl.innerHTML = `
106
+ <div style="position:fixed; inset:0; background:rgba(0,0,0,0.4); z-index:9998;"></div>
107
+ <div style="position:fixed; top:50%; left:50%; transform:translate(-50%, -50%);
108
+ background:#fff; padding:24px; border-radius:12px;
109
+ width:424px; max-width:90%; z-index:9999;
110
+ box-shadow:0 4px 20px rgba(0,0,0,0.15); font-family:sans-serif;">
111
+
112
+ <button id="sf-modal-close-btn"
113
+ style="position:absolute; top:24px; right:24px;">
114
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
115
+ <path d="M18 6L6 18" stroke="#71717A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
116
+ <path d="M6 6L18 18" stroke="#71717A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
117
+ </svg>
118
+ </button>
119
+
120
+ <h2 style="font-size:18px; font-weight:600; margin-bottom:16px;">Report Issue</h2>
121
+
122
+ <div id="sf-issue-tabs" style="display:flex; gap:4px; margin-bottom:16px; background:#f1f5f9; padding:6px; border-radius:6px; width: fit-content;">
123
+ <button id="sf-tab-lookback" data-mode="lookback" class="issue-tab ${!isStartNow ? "active" : ""}"
124
+ style="padding:6px 12px; border:none; background:${!isStartNow ? "white" : "transparent"}; color: ${!isStartNow ? "#0F172A" : "#64748B"}; border-radius:4px; font-size:14px; cursor:pointer; font-weight:500; height:32px;">
125
+ Already occurred <span style="color:#94a3b8;">[o]</span>
126
+ </button>
127
+ <button id="sf-tab-startnow" data-mode="startnow" class="issue-tab ${isStartNow ? "active" : ""}"
128
+ style="padding:6px 12px; border:none; background:${isStartNow ? "white" : "transparent"}; color: ${isStartNow ? "#0F172A" : "#64748B"}; border-radius:4px; font-size:14px; cursor:pointer; font-weight:500; height:32px;">
129
+ Capture new <span style="color:#94a3b8;">[c]</span>
130
+ </button>
131
+ </div>
132
+
133
+ <div id="sf-issue-mode-info" style="display:flex; align-items:flex-start; gap:8px; margin-bottom:24px;">
134
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-top:4px;">
135
+ <g clip-path="url(#clip0_2477_11797)">
136
+ <path d="M6.99935 12.8333C10.221 12.8333 12.8327 10.2216 12.8327 6.99996C12.8327 3.7783 10.221 1.16663 6.99935 1.16663C3.77769 1.16663 1.16602 3.7783 1.16602 6.99996C1.16602 10.2216 3.77769 12.8333 6.99935 12.8333Z" stroke="#A1A1AA" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
137
+ <path d="M7 9.33333V7" stroke="#A1A1AA" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
138
+ <path d="M7 4.66663H7.00583" stroke="#A1A1AA" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
139
+ </g>
140
+ <defs>
141
+ <clipPath id="clip0_2477_11797">
142
+ <rect width="14" height="14" fill="white"/>
143
+ </clipPath>
144
+ </defs>
145
+ </svg>
146
+ <div style="font-size:14px;">
147
+ ${isStartNow
148
+ ? "I want to reproduce the issue right now."
149
+ : "Something already happened. Capture the past few minutes."}
150
+ </div>
151
+ </div>
152
+
153
+ <label for="sf-issue-description" style="display:block; font-size:14px; font-weight:500; margin-bottom:6px;">
154
+ What happened? (optional)
155
+ </label>
156
+ <textarea id="sf-issue-description" placeholder="Add description here"
157
+ style="width:100%; height:80px; padding:8px 12px; font-size:14px;
158
+ border:1px solid #cbd5e1; border-radius:6px; margin-bottom:20px;
159
+ resize:none; outline:none;"></textarea>
160
+
161
+ <div id="sf-lookback-timestamp-container" style="display: ${isStartNow ? "none" : "block"};">
162
+ <label for="sf-lookback-time-input" style="display:block; font-size:14px; font-weight:500; margin-bottom:6px;">
163
+ Time stamp
164
+ </label>
165
+
166
+ <div id="sf-lookback-time-input" style="display:flex; align-items:center; gap:6px; padding:8px 12px;
167
+ border:1px solid #cbd5e1; border-radius:6px; width:fit-content; font-size:14px; color:#0f172a; cursor:pointer;">
168
+ <span id="sf-lookback-time-display">00:00</span>
169
+ <svg width="16" height="16" fill="none" stroke="#0f172a" stroke-width="1.5" viewBox="0 0 24 24">
170
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6l4 2" />
171
+ <circle cx="12" cy="12" r="9" stroke-linecap="round" stroke-linejoin="round" />
172
+ </svg>
173
+ </div>
174
+
175
+ <div id="sf-lookback-dropdown" style="display:none; position:absolute; background:white; border:1px solid #e5e7eb;
176
+ border-radius:6px; padding:12px; margin-top:4px; box-shadow:0 4px 12px rgba(0,0,0,0.1); z-index:10000;">
177
+ <div style="display:flex; gap:12px;">
178
+ <div>
179
+ <label style="font-size:12px; color:#64748b;">Minutes</label>
180
+ <select id="sf-lookback-minutes" style="margin-top:4px; padding:6px; border:1px solid #cbd5e1; border-radius:4px;">
181
+ ${Array.from({ length: 60 }, (_, i) => `<option value="${i}">${i
182
+ .toString()
183
+ .padStart(2, "0")}</option>`).join("")}
184
+ </select>
185
+ </div>
186
+ <div>
187
+ <label style="font-size:12px; color:#64748b;">Seconds</label>
188
+ <select id="sf-lookback-seconds" style="margin-top:4px; padding:6px; border:1px solid #cbd5e1; border-radius:4px;">
189
+ ${Array.from({ length: 60 }, (_, i) => `<option value="${i}">${i
190
+ .toString()
191
+ .padStart(2, "0")}</option>`).join("")}
192
+ </select>
193
+ </div>
194
+ </div>
195
+ </div>
196
+
197
+ <div style="font-size:12px; color:#a1a1aa; margin-top:4px;">Enter time stamp here. Max 1 hour.</div>
198
+ </div>
199
+
200
+ <div id="sf-start-recording" style="display: ${isStartNow ? "block" : "none"};">
201
+ <label for="sf-inline-record-chip" style="display:block; font-size:14px; font-weight:500; margin-bottom:6px;">
202
+ Start recording <span style="color:#94a3b8;">[r]</span>
203
+ </label>
204
+
205
+ <div id="sf-inline-record-chip" style="display:flex; margin-top:8px; align-items:center; gap:8px;">
206
+ <div id="sf-record-icon" style="padding: 8px; border-radius: 6px; border: 1px solid #9dd3ef; cursor: pointer;">
207
+ <div style="width: 14px; height: 14px; background: #fc5555; border-radius: 50%; border: 1px solid #991b1b;"></div>
208
+ </div>
209
+ <div>
210
+ <div id="sf-inline-record-timer" style="font-family: monospace; font-weight: 500; color: #d4d4d8;">00:00</div>
211
+ <div style="font-size: 12px; color: #a1a1aa; margin-top: 2px;">Max time stamp 1 hour.</div>
212
+ </div>
213
+ </div>
214
+ </div>
215
+
216
+ <div style="display:flex; justify-content:flex-end; margin-top:16px;">
217
+ <button id="sf-issue-submit-btn"
218
+ style="background: #295DBF; color:white; border:none; padding:8px 16px;
219
+ border-radius:6px; font-size:14px; line-height: 24px; font-weight:500; cursor:not-allowed; opacity:0.4;"
220
+ disabled>
221
+ Create Triage <span style="color: #E2E8F0; margin-left:6px; font-size:10px; line-height: 20px;">[cmd+enter]</span>
222
+ </button>
223
+ </div>
224
+ </div>
225
+ `;
226
+ currentState.mode = initialMode;
227
+ document.body.appendChild(modalEl);
228
+ bindListeners();
229
+ }
230
+ function setActiveTab(mode) {
231
+ const tabLookback = modalEl?.querySelector("#sf-tab-lookback");
232
+ const tabStartnow = modalEl?.querySelector("#sf-tab-startnow");
233
+ if (mode === "lookback") {
234
+ tabLookback.style.background = "white";
235
+ tabLookback.style.color = "#0F172A";
236
+ tabStartnow.style.background = "transparent";
237
+ tabStartnow.style.color = "#64748B";
238
+ }
239
+ else {
240
+ tabStartnow.style.background = "white";
241
+ tabStartnow.style.color = "#0F172A";
242
+ tabLookback.style.background = "transparent";
243
+ tabLookback.style.color = "#64748B";
244
+ }
245
+ }
246
+ function updateModeSpecificUI(mode) {
247
+ const infoText = document.getElementById("sf-issue-mode-info");
248
+ const infoMessage = infoText?.querySelector("div");
249
+ const submitBtn = document.getElementById("sf-issue-submit-btn");
250
+ const timerText = document.getElementById("sf-inline-record-timer");
251
+ const timestampContainer = document.getElementById("sf-lookback-timestamp-container");
252
+ const startRecording = document.getElementById("sf-start-recording");
253
+ if (!infoText || !infoMessage || !submitBtn)
254
+ return;
255
+ if (mode === "startnow") {
256
+ if (startRecording)
257
+ startRecording.style.display = "block";
258
+ if (timestampContainer)
259
+ timestampContainer.style.display = "none";
260
+ infoMessage.textContent = "I want to reproduce the issue right now.";
261
+ // Disable submit if recording not done yet
262
+ const canSubmit = recordingStartTime !== null && recordingEndTime !== null;
263
+ submitBtn.disabled = !canSubmit;
264
+ submitBtn.style.opacity = canSubmit ? "1" : "0.4";
265
+ submitBtn.style.cursor = canSubmit ? "pointer" : "not-allowed";
266
+ // Show current timer value if available
267
+ if (recordingStartTime && recordingEndTime && timerText) {
268
+ const duration = Math.floor((recordingEndTime - recordingStartTime) / 1000);
269
+ const mins = String(Math.floor(duration / 60)).padStart(2, "0");
270
+ const secs = String(duration % 60).padStart(2, "0");
271
+ timerText.textContent = `${mins}:${secs}`;
272
+ timerText.style.color = "#171717";
273
+ }
274
+ else if (timerText) {
275
+ timerText.textContent = "00:00";
276
+ timerText.style.color = "#d4d4d8";
277
+ }
278
+ }
279
+ else if (mode === "lookback") {
280
+ if (startRecording)
281
+ startRecording.style.display = "none";
282
+ if (timestampContainer)
283
+ timestampContainer.style.display = "block";
284
+ infoMessage.textContent =
285
+ "Something already happened. Capture the past few minutes.";
286
+ // Enable submit only if a valid timestamp is selected
287
+ const mins = Number(document.getElementById("sf-lookback-minutes")
288
+ ?.value || "0");
289
+ const secs = Number(document.getElementById("sf-lookback-seconds")
290
+ ?.value || "0");
291
+ const canSubmit = mins > 0 || secs > 0;
292
+ submitBtn.disabled = !canSubmit;
293
+ submitBtn.style.opacity = canSubmit ? "1" : "0.4";
294
+ submitBtn.style.cursor = canSubmit ? "pointer" : "not-allowed";
295
+ }
296
+ }
297
+ function bindListeners() {
298
+ const timeInput = document.getElementById("sf-lookback-time-input");
299
+ const dropdown = document.getElementById("sf-lookback-dropdown");
300
+ const display = document.getElementById("sf-lookback-time-display");
301
+ const closeBtn = document.getElementById("sf-modal-close-btn");
302
+ const recordIcon = document.getElementById("sf-record-icon");
303
+ if (closeBtn) {
304
+ closeBtn.onclick = () => {
305
+ closeModal();
306
+ };
307
+ }
308
+ if (recordIcon) {
309
+ recordIcon.onclick = () => {
310
+ const descInput = document.getElementById("sf-issue-description");
311
+ currentState.description = descInput?.value ?? "";
312
+ startCountdownThenRecord();
313
+ };
314
+ }
315
+ timeInput?.addEventListener("click", () => {
316
+ if (!dropdown)
317
+ return;
318
+ if (dropdown.style.display === "none") {
319
+ dropdown.style.display = "block";
320
+ }
321
+ else {
322
+ dropdown.style.display = "none";
323
+ }
324
+ });
325
+ window.addEventListener("click", (e) => {
326
+ if (!dropdown)
327
+ return;
328
+ if (!dropdown.contains(e.target) &&
329
+ !timeInput?.contains(e.target)) {
330
+ dropdown.style.display = "none";
331
+ }
332
+ });
333
+ const updateTimeDisplay = () => {
334
+ const mins = Number(document.getElementById("sf-lookback-minutes")
335
+ ?.value || "0");
336
+ const secs = Number(document.getElementById("sf-lookback-seconds")
337
+ ?.value || "0");
338
+ if (display) {
339
+ display.textContent = `${mins.toString().padStart(2, "0")}:${secs
340
+ .toString()
341
+ .padStart(2, "0")}`;
342
+ }
343
+ updateModeSpecificUI("lookback"); // re-check submit button status
344
+ };
345
+ // Handle dropdown value changes
346
+ document
347
+ .getElementById("sf-lookback-minutes")
348
+ ?.addEventListener("change", updateTimeDisplay);
349
+ document
350
+ .getElementById("sf-lookback-seconds")
351
+ ?.addEventListener("change", updateTimeDisplay);
352
+ modalEl?.querySelectorAll(".issue-tab").forEach((tabBtn) => {
353
+ tabBtn.addEventListener("click", (e) => {
354
+ const mode = e.currentTarget.dataset
355
+ .mode;
356
+ currentState.mode = mode;
357
+ setActiveTab(mode);
358
+ // Optionally toggle relevant UI
359
+ updateModeSpecificUI(mode);
360
+ });
361
+ });
362
+ modalEl?.addEventListener("click", (e) => {
363
+ const target = e.target;
364
+ if (target.closest("#sf-issue-submit-btn")) {
365
+ const desc = document.getElementById("sf-issue-description")
366
+ ?.value || "";
367
+ const mode = currentState.mode;
368
+ currentState.description = desc;
369
+ let startTimestamp;
370
+ let endTimestamp;
371
+ if (mode === "startnow") {
372
+ startTimestamp = recordingStartTime ?? Date.now() - 5 * 60 * 1000;
373
+ endTimestamp = recordingEndTime ?? Date.now();
374
+ }
375
+ else {
376
+ // Parse time from time selector
377
+ const minutes = Number(document.getElementById("sf-lookback-minutes")
378
+ ?.value || "0");
379
+ const seconds = Number(document.getElementById("sf-lookback-seconds")
380
+ ?.value || "0");
381
+ const delta = (minutes * 60 + seconds) * 1000;
382
+ endTimestamp = Date.now();
383
+ startTimestamp = endTimestamp - delta;
384
+ }
385
+ closeModal();
386
+ createTriage(`${startTimestamp}`, `${endTimestamp}`, desc);
387
+ }
388
+ });
389
+ }
390
+ function startCountdownThenRecord() {
391
+ // Prevent duplicates
392
+ if (document.getElementById("sf-countdown-overlay"))
393
+ return;
394
+ const overlay = document.createElement("div");
395
+ overlay.id = "sf-countdown-overlay";
396
+ overlay.style.cssText = `
397
+ position: fixed;
398
+ inset: 0;
399
+ background: rgba(0,0,0,0.6);
400
+ z-index: 10001;
401
+ display: flex;
402
+ justify-content: center;
403
+ align-items: center;
404
+ font-size: 80px;
405
+ font-weight: bold;
406
+ color: white;
407
+ font-family: sans-serif;
408
+ `;
409
+ let count = 3;
410
+ overlay.textContent = count.toString();
411
+ document.body.appendChild(overlay);
412
+ const interval = setInterval(() => {
413
+ count--;
414
+ if (count > 0) {
415
+ overlay.textContent = count.toString();
416
+ }
417
+ else {
418
+ clearInterval(interval);
419
+ document.body.removeChild(overlay);
420
+ // Begin recording
421
+ recordingStartTime = Date.now();
422
+ isRecording = true;
423
+ closeModal();
424
+ showFloatingTimer();
425
+ }
426
+ }, 1000);
427
+ }
428
+ function showFloatingTimer() {
429
+ const timer = document.createElement("div");
430
+ timer.id = "sf-recording-indicator";
431
+ timer.style.cssText = `
432
+ position: fixed;
433
+ bottom: 16px;
434
+ right: 16px;
435
+ background: white;
436
+ border: 1px solid #cbd5e1;
437
+ border-radius: 8px;
438
+ box-shadow: 0 2px 10px rgba(0,0,0,0.15);
439
+ padding: 8px 16px;
440
+ font-family: sans-serif;
441
+ font-size: 14px;
442
+ display: flex;
443
+ align-items: center;
444
+ gap: 8px;
445
+ z-index: 10000;
446
+ width: 168px;
447
+ `;
448
+ timer.innerHTML = `
449
+ <div id="sf-stop-icon" style="width: 24px; height: 24px; background: #fc5555; border-radius: 6px; border: 1px solid #991b1b; display: flex; align-items: center; justify-content: center; cursor: pointer;">
450
+ <div style="width: 8px; height: 8px; background: white; border-radius: 2px;"></div>
451
+ </div>
452
+ <span id="sf-recording-timer">00:00</span>
453
+ <div style="font-size:10px; color:#94a3b8;">[cmd] + [esc]</div>
454
+ `;
455
+ document.body.appendChild(timer);
456
+ const timerEl = timer.querySelector("#sf-recording-timer");
457
+ if (!timerEl)
458
+ return;
459
+ timerInterval = setInterval(() => {
460
+ const delta = Date.now() - (recordingStartTime ?? Date.now());
461
+ const mins = Math.floor(delta / 60000)
462
+ .toString()
463
+ .padStart(2, "0");
464
+ const secs = Math.floor((delta % 60000) / 1000)
465
+ .toString()
466
+ .padStart(2, "0");
467
+ timerEl.textContent = `${mins}:${secs}`;
468
+ }, 1000);
469
+ const stopBtn = timer.querySelector("#sf-stop-icon");
470
+ stopBtn?.addEventListener("click", () => stopRecording());
471
+ }
472
+ function stopRecording() {
473
+ recordingEndTime = Date.now();
474
+ isRecording = false;
475
+ if (timerInterval)
476
+ clearInterval(timerInterval);
477
+ document.getElementById("sf-recording-indicator")?.remove();
478
+ reopenModalAfterStop();
479
+ }
480
+ function reopenModalAfterStop() {
481
+ injectModalHTML("startnow");
482
+ const descBox = document.getElementById("sf-issue-description");
483
+ if (descBox)
484
+ descBox.value = currentState.description;
485
+ const startNowRadio = document.querySelector('input[value="startnow"]');
486
+ if (startNowRadio)
487
+ startNowRadio.checked = true;
488
+ const chip = document.getElementById("sf-inline-record-chip");
489
+ const timerText = document.getElementById("sf-inline-record-timer");
490
+ if (chip && timerText) {
491
+ const durationSec = Math.floor(((recordingEndTime ?? 0) - (recordingStartTime ?? 0)) / 1000);
492
+ const mins = Math.floor(durationSec / 60)
493
+ .toString()
494
+ .padStart(2, "0");
495
+ const secs = Math.floor(durationSec % 60)
496
+ .toString()
497
+ .padStart(2, "0");
498
+ timerText.textContent = `${mins}:${secs}`;
499
+ timerText.style.color = "black";
500
+ chip.style.display = "flex";
501
+ }
502
+ const submitBtn = document.getElementById("sf-issue-submit-btn");
503
+ submitBtn.disabled = false;
504
+ submitBtn.style.opacity = "1";
505
+ submitBtn.style.cursor = "pointer";
506
+ }
507
+ async function createTriage(startTimestamp, endTimestamp, description) {
508
+ try {
509
+ showTriageStatusModal(true);
510
+ const response = await createTriageFromRecorder(apiKey, backendApi, getSessionIdSafely(), startTimestamp, endTimestamp, description);
511
+ const triageId = response?.data?.createTriageFromRecorder?.id;
512
+ if (triageId) {
513
+ showTriageStatusModal(false, triageId);
514
+ }
515
+ else {
516
+ console.error("No Triage ID returned from backend.");
517
+ showTriageStatusModal(false, null); // fallback behavior
518
+ }
519
+ }
520
+ catch (error) {
521
+ console.error("Error creating triage:", error);
522
+ showTriageStatusModal(false, null);
523
+ }
524
+ }
525
+ function showTriageStatusModal(isLoading, triageId) {
526
+ removeExistingModals();
527
+ const triageUrl = triageId
528
+ ? `https://app.sailfishqa.com/triage/${triageId}`
529
+ : "";
530
+ const container = document.createElement("div");
531
+ container.id = "sf-triage-status-modal";
532
+ const statusTitle = isLoading ? "Creating Triage..." : "Triage created!";
533
+ const statusSubtitle = isLoading
534
+ ? `<p style="font-size:14px; color:#64748b; line-height:20px;">This may take ~10 seconds</p>`
535
+ : "";
536
+ const spinner = isLoading
537
+ ? `<div style="display:flex; justify-content:center; align-items:center; padding: 40px 0;">
538
+ <div class="spinner" style="width:24px; height:24px; border:2px solid #295dbf; border-top:2px solid white; border-radius:50%; animation:spin 1s linear infinite;"></div>
539
+ </div>`
540
+ : "";
541
+ const copiedStatus = !isLoading
542
+ ? `<div id="sf-copied-status" style="display:none; font-size:12px; font-weight:500; color:white;
543
+ background-color:#22c55e; padding:4px 8px; border-radius:6px; white-space:nowrap; align-items:center; gap:6px;">
544
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
545
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M21 7.5L9 19.5L3 13.5L5.25 11.25L9 15L18.75 5.25L21 7.5Z" fill="white"/>
546
+ </svg>
547
+ Copied
548
+ </div>`
549
+ : "";
550
+ container.innerHTML = `
551
+ <div style="position:fixed; inset:0; background:rgba(0,0,0,0.4); z-index:9998;"></div>
552
+ <div style="position:fixed; top:50%; left:50%; transform:translate(-50%, -50%);
553
+ background:#fff; padding:24px; border-radius:12px; width:300px; max-width:90%;
554
+ z-index:9999; font-family:sans-serif; box-shadow:0 4px 20px rgba(0,0,0,0.15);">
555
+
556
+ <div style="position:absolute; top:24px; right:48px;">${copiedStatus}</div>
557
+
558
+ <button id="sf-triage-modal-close"
559
+ style="position:absolute; top:24px; right:16px; background:none; border:none; cursor:pointer;">
560
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
561
+ <path d="M18 6L6 18" stroke="#71717A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
562
+ <path d="M6 6L18 18" stroke="#71717A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
563
+ </svg>
564
+ </button>
565
+
566
+ <h2 style="font-size:18px; font-weight:600; margin-bottom:${isLoading ? 8 : 40}px; line-height: 28px;">${statusTitle}</h2>
567
+ ${statusSubtitle}
568
+ ${spinner}
569
+
570
+ <div style="display:flex; justify-content:flex-end; align-items:center; gap:8px;">
571
+ <button id="sf-copy-triage-link"
572
+ style="background:white; border:1px solid #e2e8f0; padding:8px 16px; border-radius:6px;
573
+ font-size:14px; color: #0f172a; cursor:pointer;">
574
+ Share
575
+ </button>
576
+ <button id="sf-view-triage-btn"
577
+ style="display:flex; align-items:center; gap:8px; background:#295dbf; border:none;
578
+ padding:8px 16px; border-radius:6px; font-size:14px; color:white; cursor:pointer;">
579
+ View Triage
580
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
581
+ <path d="M12 8.66667V12.6667C12 13.0203 11.8595 13.3594 11.6095 13.6095C11.3594 13.8595 11.0203 14 10.6667 14H3.33333C2.97971 14 2.64057 13.8595 2.39052 13.6095C2.14048 13.3594 2 13.0203 2 12.6667V5.33333C2 4.97971 2.14048 4.64057 2.39052 4.39052C2.64057 4.14048 2.97971 4 3.33333 4H7.33333" stroke="white" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
582
+ <path d="M10 2H14V6" stroke="white" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
583
+ <path d="M6.66602 9.33333L13.9993 2" stroke="white" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
584
+ </svg>
585
+ </button>
586
+ </div>
587
+ </div>
588
+ <style>
589
+ @keyframes spin { to { transform: rotate(360deg); } }
590
+ </style>
591
+ `;
592
+ document.body.appendChild(container);
593
+ document
594
+ .getElementById("sf-triage-modal-close")
595
+ ?.addEventListener("click", () => {
596
+ container.remove();
597
+ });
598
+ const copyBtn = document.getElementById("sf-copy-triage-link");
599
+ const viewBtn = document.getElementById("sf-view-triage-btn");
600
+ if (isLoading) {
601
+ copyBtn.disabled = true;
602
+ copyBtn.style.opacity = "0.4";
603
+ copyBtn.style.cursor = "not-allowed";
604
+ viewBtn.disabled = true;
605
+ viewBtn.style.opacity = "0.4";
606
+ viewBtn.style.cursor = "not-allowed";
607
+ }
608
+ else {
609
+ copyBtn.disabled = false;
610
+ copyBtn.addEventListener("click", () => {
611
+ navigator.clipboard.writeText(triageUrl).then(() => {
612
+ const copiedStatus = document.getElementById("sf-copied-status");
613
+ if (copiedStatus)
614
+ copiedStatus.style.display = "flex";
615
+ });
616
+ });
617
+ viewBtn.disabled = false;
618
+ viewBtn.addEventListener("click", () => {
619
+ if (triageId) {
620
+ window.open(triageUrl, "_blank");
621
+ }
622
+ });
623
+ }
624
+ }
625
+ function removeExistingModals() {
626
+ document.getElementById("sf-report-issue-modal")?.remove();
627
+ document.getElementById("sf-triage-status-modal")?.remove();
628
+ }
@@ -0,0 +1,26 @@
1
+ import { openDB } from 'idb';
2
+ const DB_NAME = 'leapsNotifyDB';
3
+ const STORE_NAME = 'notifyMessages';
4
+ const dbPromise = openDB(DB_NAME, 1, {
5
+ upgrade(db) {
6
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
7
+ db.createObjectStore(STORE_NAME, { keyPath: 'id', autoIncrement: true });
8
+ }
9
+ },
10
+ });
11
+ export async function saveNotifyMessageToIDB(message) {
12
+ const db = await dbPromise;
13
+ const tx = db.transaction(STORE_NAME, 'readwrite');
14
+ await tx.store.add({ value: message });
15
+ await tx.done;
16
+ }
17
+ export async function getAllNotifyMessages() {
18
+ const db = await dbPromise;
19
+ return await db.getAll(STORE_NAME);
20
+ }
21
+ export async function deleteNotifyMessageById(id) {
22
+ const db = await dbPromise;
23
+ const tx = db.transaction(STORE_NAME, 'readwrite');
24
+ await tx.store.delete(id);
25
+ await tx.done;
26
+ }