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