@firstlook-uat/sdk 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1598 @@
1
+ const y = [
2
+ 'input[type="password"]',
3
+ "[data-sensitive]",
4
+ "[data-mask]",
5
+ ".uat-mask"
6
+ ];
7
+ function S(a) {
8
+ var t, e, s, i, n, o, c, l, h, d;
9
+ if (!a.endpoint)
10
+ throw new Error("[FirstLook] 'endpoint' is required. Set your Supabase ingest-session URL.");
11
+ return {
12
+ projectId: a.projectId,
13
+ apiKey: a.apiKey,
14
+ userId: a.userId,
15
+ role: a.role,
16
+ context: a.context ?? {},
17
+ endpoint: a.endpoint,
18
+ triggers: {
19
+ tapCount: ((t = a.triggers) == null ? void 0 : t.tapCount) ?? 5,
20
+ deepLink: ((e = a.triggers) == null ? void 0 : e.deepLink) ?? !0,
21
+ shake: ((s = a.triggers) == null ? void 0 : s.shake) ?? !0,
22
+ customCheck: (i = a.triggers) == null ? void 0 : i.customCheck
23
+ },
24
+ security: {
25
+ watermark: ((n = a.security) == null ? void 0 : n.watermark) ?? !0,
26
+ maskSelectors: ((o = a.security) == null ? void 0 : o.maskSelectors) ?? y
27
+ },
28
+ recording: {
29
+ domSnapshot: ((c = a.recording) == null ? void 0 : c.domSnapshot) ?? !0,
30
+ voice: ((l = a.recording) == null ? void 0 : l.voice) ?? !0,
31
+ maxDuration: ((h = a.recording) == null ? void 0 : h.maxDuration) ?? 600,
32
+ snapshotInterval: ((d = a.recording) == null ? void 0 : d.snapshotInterval) ?? 1e3
33
+ }
34
+ };
35
+ }
36
+ function w() {
37
+ return {
38
+ userAgent: navigator.userAgent,
39
+ platform: navigator.platform,
40
+ language: navigator.language,
41
+ screenWidth: window.screen.width,
42
+ screenHeight: window.screen.height,
43
+ pixelRatio: window.devicePixelRatio,
44
+ touchSupport: "ontouchstart" in window || navigator.maxTouchPoints > 0
45
+ };
46
+ }
47
+ class T {
48
+ constructor() {
49
+ this.listeners = /* @__PURE__ */ new Map();
50
+ }
51
+ on(t, e) {
52
+ return this.listeners.has(t) || this.listeners.set(t, /* @__PURE__ */ new Set()), this.listeners.get(t).add(e), () => this.off(t, e);
53
+ }
54
+ off(t, e) {
55
+ var s;
56
+ (s = this.listeners.get(t)) == null || s.delete(e);
57
+ }
58
+ emit(t) {
59
+ var e, s;
60
+ (e = this.listeners.get(t.type)) == null || e.forEach((i) => {
61
+ try {
62
+ i(t);
63
+ } catch (n) {
64
+ console.error("[FirstLook] Event listener error:", n);
65
+ }
66
+ }), (s = this.listeners.get("*")) == null || s.forEach((i) => {
67
+ try {
68
+ i(t);
69
+ } catch (n) {
70
+ console.error("[FirstLook] Event listener error:", n);
71
+ }
72
+ });
73
+ }
74
+ removeAll() {
75
+ this.listeners.clear();
76
+ }
77
+ }
78
+ class C {
79
+ constructor(t) {
80
+ this.events = t, this.quests = [], this.results = [], this.currentIndex = -1, this.sessionStartTime = 0, this.blocked = !1, this.pendingFeedbacks = [];
81
+ }
82
+ loadQuests(t) {
83
+ this.quests = [...t].sort((e, s) => e.order - s.order), this.results = [], this.currentIndex = -1, this.blocked = !1;
84
+ }
85
+ startSession(t) {
86
+ this.sessionStartTime = t, this.advance();
87
+ }
88
+ getCurrentQuest() {
89
+ return this.blocked || this.currentIndex < 0 || this.currentIndex >= this.quests.length ? null : this.quests[this.currentIndex];
90
+ }
91
+ getStatus() {
92
+ return {
93
+ total: this.quests.length,
94
+ completed: this.results.filter((t) => t.status === "COMPLETED").length,
95
+ failed: this.results.filter((t) => t.status === "FAILED").length,
96
+ current: this.currentIndex,
97
+ isBlocked: this.blocked,
98
+ isFinished: this.currentIndex >= this.quests.length
99
+ };
100
+ }
101
+ getQuestStatuses() {
102
+ return this.quests.map((t, e) => {
103
+ const s = this.results.find((i) => i.questId === t.id);
104
+ return s ? s.status : e === this.currentIndex && !this.blocked ? "ACTIVE" : this.blocked && e >= this.currentIndex ? "BLOCKED" : "PENDING";
105
+ });
106
+ }
107
+ completeCurrentQuest(t, e) {
108
+ const s = this.getCurrentQuest();
109
+ if (!s) return null;
110
+ const i = {
111
+ questId: s.id,
112
+ status: "COMPLETED",
113
+ timestamp: Date.now(),
114
+ relativeTime: Date.now() - this.sessionStartTime,
115
+ voiceMemoBlob: e,
116
+ logs: t,
117
+ feedbacks: this.drainFeedbacks()
118
+ };
119
+ return this.results.push(i), this.events.emit({ type: "quest:completed", questId: s.id }), this.advance(), i;
120
+ }
121
+ failCurrentQuest(t, e, s) {
122
+ const i = this.getCurrentQuest();
123
+ if (!i) return null;
124
+ const n = {
125
+ questId: i.id,
126
+ status: "FAILED",
127
+ timestamp: Date.now(),
128
+ relativeTime: Date.now() - this.sessionStartTime,
129
+ voiceMemoBlob: s,
130
+ logs: t,
131
+ comment: e,
132
+ feedbacks: this.drainFeedbacks()
133
+ };
134
+ return this.results.push(n), this.events.emit({ type: "quest:failed", questId: i.id }), i.blocking ? (this.blocked = !0, this.events.emit({ type: "quest:blocked", questId: i.id })) : this.advance(), n;
135
+ }
136
+ addFeedback(t) {
137
+ this.pendingFeedbacks.push(t);
138
+ }
139
+ getResults() {
140
+ return [...this.results];
141
+ }
142
+ drainFeedbacks() {
143
+ const t = this.pendingFeedbacks;
144
+ return this.pendingFeedbacks = [], t;
145
+ }
146
+ advance() {
147
+ this.currentIndex++, this.currentIndex < this.quests.length && this.events.emit({ type: "quest:started", questId: this.quests[this.currentIndex].id });
148
+ }
149
+ }
150
+ function r(a, t, e) {
151
+ const s = document.createElement(a);
152
+ if (t)
153
+ for (const [i, n] of Object.entries(t))
154
+ i === "className" ? s.className = n : s.setAttribute(i, n);
155
+ if (e)
156
+ for (const i of e)
157
+ typeof i == "string" ? s.appendChild(document.createTextNode(i)) : s.appendChild(i);
158
+ return s;
159
+ }
160
+ function I() {
161
+ return `fl_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
162
+ }
163
+ function q(a) {
164
+ const t = Math.floor(a / 60), e = a % 60;
165
+ return `${t.toString().padStart(2, "0")}:${e.toString().padStart(2, "0")}`;
166
+ }
167
+ class M {
168
+ constructor(t, e) {
169
+ this.shadowRoot = t, this.callbacks = e, this.minimized = !1, this.timerInterval = null, this.startTime = 0, this.root = document.createElement("div"), this.root.className = "fl-quest-overlay", this.shadowRoot.appendChild(this.root);
170
+ }
171
+ renderQuest(t, e, s) {
172
+ this.minimized = !1, this.root.className = "fl-quest-overlay", this.root.innerHTML = "";
173
+ const i = e.indexOf("ACTIVE"), n = r("div", { className: "fl-quest-header" }, [
174
+ r("div", { className: "fl-quest-header-left" }, [
175
+ r("span", { className: "fl-quest-badge" }, [`Q${i + 1}/${e.length}`]),
176
+ r("span", { className: "fl-quest-title" }, [t.title])
177
+ ]),
178
+ this.createMinimizeBtn()
179
+ ]);
180
+ this.root.appendChild(n);
181
+ const o = r("div", { className: "fl-quest-content" }), c = r("div", { className: "fl-quest-progress" });
182
+ for (const f of e) {
183
+ const m = f === "COMPLETED" ? "fl-completed" : f === "FAILED" ? "fl-failed" : f === "ACTIVE" ? "fl-active" : "";
184
+ c.appendChild(r("div", { className: `fl-quest-progress-dot ${m}` }));
185
+ }
186
+ const l = r("div", { className: "fl-quest-body" }, [
187
+ c,
188
+ r("p", { className: "fl-quest-description" }, [t.description]),
189
+ r("div", { className: "fl-quest-memo-row" }, [
190
+ this.createButton("fl-btn fl-btn-memo", "📝 メモ", () => this.renderFeedbackModal(t.title))
191
+ ]),
192
+ r("div", { className: "fl-quest-actions" }, [
193
+ this.createButton("fl-btn fl-btn-ok", "✓ OK", this.callbacks.onOk),
194
+ this.createButton("fl-btn fl-btn-ng", "✗ NG", this.callbacks.onNg)
195
+ ])
196
+ ]);
197
+ o.appendChild(l), s && o.appendChild(
198
+ r("div", { className: "fl-voice-indicator" }, [
199
+ r("span", { className: "fl-voice-dot" }),
200
+ "Recording..."
201
+ ])
202
+ );
203
+ const h = r("div", { className: "fl-status-bar" });
204
+ s && h.appendChild(
205
+ r("span", { className: "fl-status-recording" }, [
206
+ r("span", { className: "fl-voice-dot" }),
207
+ "REC"
208
+ ])
209
+ );
210
+ const d = r("span", { className: "fl-status-timer" }, ["00:00"]);
211
+ h.appendChild(d), o.appendChild(h), this.root.appendChild(o), this.startTimer(d), this.root.onclick = () => {
212
+ this.minimized && (this.minimized = !1, this.root.className = "fl-quest-overlay", this.root.innerHTML = "", this.root.appendChild(n), this.root.appendChild(o), this.startTimer(d));
213
+ };
214
+ }
215
+ renderFeedbackModal(t, e = "memo") {
216
+ const s = r("div", { className: "fl-feedback-modal" }), i = e === "ng", l = r("div", { className: i ? "fl-quest-header fl-quest-header-ng" : "fl-quest-header" }, [
217
+ r("div", { className: "fl-quest-header-left" }, [
218
+ r("span", { className: i ? "fl-quest-badge fl-badge-ng" : "fl-quest-badge" }, [i ? "NG" : "📝"]),
219
+ r("span", { className: "fl-quest-title" }, [t])
220
+ ])
221
+ ]);
222
+ s.appendChild(l);
223
+ const h = r("div", { className: "fl-quest-content" }), d = document.createElement("textarea");
224
+ d.className = "fl-feedback-textarea fl-feedback-textarea-modal", d.placeholder = i ? "何がうまくいかなかったか教えてください..." : "気づいたことをメモ...📝", d.rows = 3, h.appendChild(d);
225
+ const f = r("div", { className: "fl-quest-actions fl-feedback-modal-actions" }, [
226
+ this.createButton("fl-btn fl-btn-finish", "送信", () => {
227
+ const m = d.value.trim();
228
+ if (!m && !i) {
229
+ s.remove();
230
+ return;
231
+ }
232
+ i ? this.callbacks.onNgWithFeedback(m) : this.callbacks.onMemo(m), s.remove();
233
+ }),
234
+ this.createButton("fl-btn fl-btn-skip", "キャンセル", () => {
235
+ i && this.callbacks.onNgWithFeedback(""), s.remove();
236
+ })
237
+ ]);
238
+ h.appendChild(f), s.appendChild(h), this.root.appendChild(s), d.focus();
239
+ }
240
+ renderSummary(t, e, s) {
241
+ this.stopTimer(), this.root.className = "fl-quest-overlay", this.root.innerHTML = "";
242
+ const i = r("div", { className: "fl-summary" }, [
243
+ r("div", { className: "fl-summary-icon" }, [t === s ? "🎉" : "📋"]),
244
+ r("div", { className: "fl-summary-title" }, [
245
+ t === s ? "All Quests Complete!" : "Session Complete"
246
+ ]),
247
+ r("div", { className: "fl-summary-subtitle" }, [
248
+ `${t + e} of ${s} quests attempted`
249
+ ]),
250
+ r("div", { className: "fl-summary-stats" }, [
251
+ this.createStat(t.toString(), "Passed", "fl-ok"),
252
+ this.createStat(e.toString(), "Failed", "fl-ng")
253
+ ]),
254
+ this.createButton("fl-btn fl-btn-finish", "Finish & Upload", this.callbacks.onFinish)
255
+ ]);
256
+ this.root.appendChild(i);
257
+ }
258
+ renderBlocked(t) {
259
+ this.stopTimer(), this.root.className = "fl-quest-overlay", this.root.innerHTML = "";
260
+ const e = r("div", { className: "fl-summary" }, [
261
+ r("div", { className: "fl-summary-icon" }, ["🛑"]),
262
+ r("div", { className: "fl-summary-title" }, ["Blocked"]),
263
+ r("div", { className: "fl-summary-subtitle" }, [
264
+ `Quest "${t}" is blocking. Session halted.`
265
+ ]),
266
+ this.createButton("fl-btn fl-btn-finish", "Finish & Upload", this.callbacks.onFinish)
267
+ ]);
268
+ this.root.appendChild(e);
269
+ }
270
+ destroy() {
271
+ this.stopTimer(), this.root.remove();
272
+ }
273
+ createMinimizeBtn() {
274
+ const t = r("button", { className: "fl-quest-minimize-btn" }, ["−"]);
275
+ return t.onclick = (e) => {
276
+ e.stopPropagation(), this.minimized = !0, this.root.className = "fl-quest-overlay fl-minimized", this.root.innerHTML = "";
277
+ const s = r("span", { className: "fl-minimize-icon" }, ["🔍"]);
278
+ this.root.appendChild(s), this.callbacks.onMinimize();
279
+ }, t;
280
+ }
281
+ createButton(t, e, s) {
282
+ const i = r("button", { className: t });
283
+ return i.textContent = e, i.onclick = (n) => {
284
+ n.stopPropagation(), s();
285
+ }, i;
286
+ }
287
+ createStat(t, e, s) {
288
+ return r("div", { className: "fl-stat" }, [
289
+ r("div", { className: `fl-stat-value ${s}` }, [t]),
290
+ r("div", { className: "fl-stat-label" }, [e])
291
+ ]);
292
+ }
293
+ startTimer(t) {
294
+ this.stopTimer(), this.startTime || (this.startTime = Date.now()), this.timerInterval = setInterval(() => {
295
+ const e = Math.floor((Date.now() - this.startTime) / 1e3);
296
+ t.textContent = q(e);
297
+ }, 1e3);
298
+ }
299
+ stopTimer() {
300
+ this.timerInterval && (clearInterval(this.timerInterval), this.timerInterval = null);
301
+ }
302
+ }
303
+ const E = 100, R = 2 * 1024 * 1024;
304
+ class D {
305
+ constructor(t, e, s) {
306
+ this.config = t, this.events = e, this.maskSelector = s, this.actionLogs = [], this.snapshots = [], this.snapshotTimer = null, this.startTime = 0, this.running = !1, this.handleClick = this.onClick.bind(this), this.handleScroll = this.throttle(this.onScroll.bind(this), 500), this.handleInput = this.onInput.bind(this);
307
+ }
308
+ start() {
309
+ this.running || (this.running = !0, this.startTime = Date.now(), this.actionLogs = [], this.snapshots = [], document.addEventListener("click", this.handleClick, { capture: !0, passive: !0 }), document.addEventListener("scroll", this.handleScroll, { capture: !0, passive: !0 }), document.addEventListener("input", this.handleInput, { capture: !0, passive: !0 }), this.config.recording.domSnapshot && (this.captureSnapshot(), this.snapshotTimer = setInterval(
310
+ () => this.captureSnapshot(),
311
+ this.config.recording.snapshotInterval
312
+ )), this.events.emit({ type: "recording:started" }));
313
+ }
314
+ stop() {
315
+ this.running && (this.running = !1, document.removeEventListener("click", this.handleClick, { capture: !0 }), document.removeEventListener("scroll", this.handleScroll, { capture: !0 }), document.removeEventListener("input", this.handleInput, { capture: !0 }), this.snapshotTimer && (clearInterval(this.snapshotTimer), this.snapshotTimer = null), this.captureSnapshot(), this.events.emit({ type: "recording:stopped" }));
316
+ }
317
+ getActionLogs() {
318
+ return [...this.actionLogs];
319
+ }
320
+ getSnapshots() {
321
+ return [...this.snapshots];
322
+ }
323
+ getElapsedMs() {
324
+ return this.running ? Date.now() - this.startTime : 0;
325
+ }
326
+ addLog(t) {
327
+ this.actionLogs.push({
328
+ ...t,
329
+ timestamp: Date.now() - this.startTime
330
+ });
331
+ }
332
+ onClick(t) {
333
+ const e = t.target;
334
+ this.addLog({
335
+ type: "click",
336
+ target: x(e)
337
+ });
338
+ }
339
+ onScroll() {
340
+ this.addLog({
341
+ type: "scroll",
342
+ value: `${window.scrollX},${window.scrollY}`
343
+ });
344
+ }
345
+ onInput(t) {
346
+ const e = t.target;
347
+ if (e.hasAttribute("data-fl-masked"))
348
+ this.addLog({ type: "input", target: x(e), value: "[MASKED]" });
349
+ else {
350
+ const s = e.value;
351
+ this.addLog({
352
+ type: "input",
353
+ target: x(e),
354
+ value: s == null ? void 0 : s.slice(0, 100)
355
+ });
356
+ }
357
+ }
358
+ captureSnapshot() {
359
+ try {
360
+ const t = document.documentElement.cloneNode(!0);
361
+ if (this.maskSelector) {
362
+ const n = t.querySelectorAll(this.maskSelector);
363
+ for (const o of n)
364
+ (o instanceof HTMLInputElement || o instanceof HTMLTextAreaElement) && (o.value = "***"), o.textContent = "***";
365
+ }
366
+ const e = t.querySelectorAll("script");
367
+ for (const n of e) n.remove();
368
+ const s = t.querySelector("#firstlook-sdk-root");
369
+ s == null || s.remove();
370
+ const i = t.outerHTML;
371
+ if (i.length > R) return;
372
+ this.snapshots.length >= E && this.snapshots.shift(), this.snapshots.push({
373
+ type: "dom-snapshot",
374
+ timestamp: Date.now() - this.startTime,
375
+ data: i
376
+ });
377
+ } catch {
378
+ }
379
+ }
380
+ throttle(t, e) {
381
+ let s = 0;
382
+ return (...i) => {
383
+ const n = Date.now();
384
+ n - s >= e && (s = n, t(...i));
385
+ };
386
+ }
387
+ }
388
+ function x(a) {
389
+ var n;
390
+ const t = a.tagName.toLowerCase(), e = a.id ? `#${a.id}` : "", s = a.className && typeof a.className == "string" ? `.${a.className.trim().split(/\s+/).slice(0, 2).join(".")}` : "", i = ((n = a.textContent) == null ? void 0 : n.trim().slice(0, 30)) || "";
391
+ return `${t}${e}${s}${i ? ` "${i}"` : ""}`;
392
+ }
393
+ class L {
394
+ constructor(t) {
395
+ this.events = t, this.mediaRecorder = null, this.stream = null, this.chunks = [], this._recording = !1;
396
+ }
397
+ get recording() {
398
+ return this._recording;
399
+ }
400
+ async start() {
401
+ if (!this._recording)
402
+ try {
403
+ this.stream = await navigator.mediaDevices.getUserMedia({ audio: !0 });
404
+ const t = this.getSupportedMimeType(), e = t ? { mimeType: t } : {};
405
+ this.mediaRecorder = new MediaRecorder(this.stream, e), this.chunks = [], this.mediaRecorder.ondataavailable = (s) => {
406
+ s.data.size > 0 && this.chunks.push(s.data);
407
+ }, this.mediaRecorder.start(1e3), this._recording = !0;
408
+ } catch (t) {
409
+ console.warn("[FirstLook] Voice recording unavailable:", t), this.events.emit({
410
+ type: "error",
411
+ error: new Error(`Voice recording failed: ${t.message}`)
412
+ });
413
+ }
414
+ }
415
+ async stopAsync() {
416
+ return !this._recording || !this.mediaRecorder ? null : new Promise((t) => {
417
+ this.mediaRecorder.onstop = () => {
418
+ var s;
419
+ const e = this.chunks.length > 0 ? new Blob(this.chunks, { type: ((s = this.mediaRecorder) == null ? void 0 : s.mimeType) || "audio/webm" }) : null;
420
+ this.cleanup(), t(e);
421
+ }, this.mediaRecorder.stop();
422
+ });
423
+ }
424
+ async captureSnippet(t = 1e4) {
425
+ return await this.start(), new Promise((e) => {
426
+ setTimeout(async () => {
427
+ const s = await this.stopAsync();
428
+ e(s);
429
+ }, t);
430
+ });
431
+ }
432
+ cleanup() {
433
+ if (this._recording = !1, this.stream) {
434
+ for (const t of this.stream.getTracks())
435
+ t.stop();
436
+ this.stream = null;
437
+ }
438
+ this.mediaRecorder = null, this.chunks = [];
439
+ }
440
+ getSupportedMimeType() {
441
+ const t = [
442
+ "audio/webm;codecs=opus",
443
+ "audio/webm",
444
+ "audio/ogg;codecs=opus",
445
+ "audio/mp4"
446
+ ];
447
+ for (const e of t)
448
+ if (typeof MediaRecorder < "u" && MediaRecorder.isTypeSupported(e)) return e;
449
+ return "";
450
+ }
451
+ }
452
+ class A {
453
+ constructor(t, e) {
454
+ this.shadowRoot = t, this.config = e, this.container = null, this.refreshInterval = null, this.cachedIp = null;
455
+ }
456
+ show() {
457
+ this.container || (this.container = document.createElement("div"), this.container.className = "fl-watermark", this.renderTiles(), this.shadowRoot.appendChild(this.container), this.fetchIp().then(() => this.renderTiles()), this.refreshInterval = setInterval(() => this.renderTiles(), 6e4));
458
+ }
459
+ hide() {
460
+ this.container && (this.container.remove(), this.container = null), this.refreshInterval && (clearInterval(this.refreshInterval), this.refreshInterval = null);
461
+ }
462
+ async fetchIp() {
463
+ if (!this.cachedIp)
464
+ try {
465
+ const t = await fetch("https://api.ipify.org?format=text");
466
+ t.ok && (this.cachedIp = await t.text());
467
+ } catch {
468
+ this.cachedIp = "N/A";
469
+ }
470
+ }
471
+ renderTiles() {
472
+ if (!this.container) return;
473
+ this.container.innerHTML = "";
474
+ const t = this.cachedIp ?? "", e = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19), s = t ? `${this.config.userId} | ${e} | ${t}` : `${this.config.userId} | ${e}`, i = 320, n = 80, o = Math.ceil(window.innerWidth / i) + 1, c = Math.ceil(window.innerHeight / n) + 1;
475
+ for (let l = 0; l < c; l++)
476
+ for (let h = 0; h < o; h++) {
477
+ const d = document.createElement("span");
478
+ d.className = "fl-watermark-tile", d.textContent = s, d.style.left = `${h * i}px`, d.style.top = `${l * n}px`, this.container.appendChild(d);
479
+ }
480
+ }
481
+ }
482
+ class N {
483
+ constructor(t) {
484
+ this.selectors = t, this.observer = null, this.maskedElements = /* @__PURE__ */ new Set();
485
+ }
486
+ start() {
487
+ this.scanAndMark(), this.observer = new MutationObserver(() => this.scanAndMark()), this.observer.observe(document.body, {
488
+ childList: !0,
489
+ subtree: !0,
490
+ attributes: !0,
491
+ attributeFilter: ["type", "class", "data-sensitive", "data-mask"]
492
+ });
493
+ }
494
+ stop() {
495
+ var t;
496
+ (t = this.observer) == null || t.disconnect(), this.observer = null;
497
+ for (const e of this.maskedElements)
498
+ e.removeAttribute("data-fl-masked");
499
+ this.maskedElements.clear();
500
+ }
501
+ isMasked(t) {
502
+ return t.hasAttribute("data-fl-masked");
503
+ }
504
+ getCombinedSelector() {
505
+ return this.selectors.join(", ");
506
+ }
507
+ scanAndMark() {
508
+ const t = this.getCombinedSelector();
509
+ if (t)
510
+ try {
511
+ const e = document.querySelectorAll(t);
512
+ for (const s of e)
513
+ this.maskedElements.has(s) || (s.setAttribute("data-fl-masked", "true"), this.maskedElements.add(s));
514
+ for (const s of this.maskedElements)
515
+ document.contains(s) || this.maskedElements.delete(s);
516
+ } catch {
517
+ }
518
+ }
519
+ }
520
+ const F = "firstlook_uat", z = 1, u = {
521
+ sessions: "sessions",
522
+ recordings: "recordings",
523
+ annotations: "annotations",
524
+ uploadQueue: "upload_queue"
525
+ };
526
+ class P {
527
+ constructor() {
528
+ this.db = null;
529
+ }
530
+ async open() {
531
+ if (!this.db)
532
+ return new Promise((t, e) => {
533
+ const s = indexedDB.open(F, z);
534
+ s.onupgradeneeded = () => {
535
+ const i = s.result;
536
+ i.objectStoreNames.contains(u.sessions) || i.createObjectStore(u.sessions, { keyPath: "sessionId" }), i.objectStoreNames.contains(u.recordings) || i.createObjectStore(u.recordings, { autoIncrement: !0 }).createIndex("sessionId", "sessionId", { unique: !1 }), i.objectStoreNames.contains(u.annotations) || i.createObjectStore(u.annotations, { autoIncrement: !0 }).createIndex("sessionId", "sessionId", { unique: !1 }), i.objectStoreNames.contains(u.uploadQueue) || i.createObjectStore(u.uploadQueue, { autoIncrement: !0 });
537
+ }, s.onsuccess = () => {
538
+ this.db = s.result, t();
539
+ }, s.onerror = () => e(s.error);
540
+ });
541
+ }
542
+ async saveSession(t) {
543
+ await this.put(u.sessions, t);
544
+ }
545
+ async getSession(t) {
546
+ return this.get(u.sessions, t);
547
+ }
548
+ async saveRecording(t, e) {
549
+ await this.put(u.recordings, { sessionId: t, ...e });
550
+ }
551
+ async saveAnnotation(t, e) {
552
+ await this.put(u.annotations, { sessionId: t, ...e });
553
+ }
554
+ async getAnnotations(t) {
555
+ return this.getAllByIndex(u.annotations, "sessionId", t);
556
+ }
557
+ async enqueueUpload(t) {
558
+ await this.put(u.uploadQueue, {
559
+ payload: t,
560
+ createdAt: Date.now(),
561
+ attempts: 0
562
+ });
563
+ }
564
+ async getPendingUploads() {
565
+ const t = this.ensureDb();
566
+ return new Promise((e, s) => {
567
+ const o = t.transaction(u.uploadQueue, "readonly").objectStore(u.uploadQueue).openCursor(), c = [];
568
+ o.onsuccess = () => {
569
+ const l = o.result;
570
+ l ? (c.push({
571
+ key: l.key,
572
+ payload: l.value.payload,
573
+ attempts: l.value.attempts,
574
+ lastAttemptAt: l.value.lastAttemptAt
575
+ }), l.continue()) : e(c);
576
+ }, o.onerror = () => s(o.error);
577
+ });
578
+ }
579
+ async removeFromQueue(t) {
580
+ const e = this.ensureDb();
581
+ return new Promise((s, i) => {
582
+ const o = e.transaction(u.uploadQueue, "readwrite").objectStore(u.uploadQueue).delete(t);
583
+ o.onsuccess = () => s(), o.onerror = () => i(o.error);
584
+ });
585
+ }
586
+ async incrementAttempts(t) {
587
+ const e = this.ensureDb();
588
+ return new Promise((s, i) => {
589
+ const n = e.transaction(u.uploadQueue, "readwrite"), o = n.objectStore(u.uploadQueue), c = o.get(t);
590
+ c.onsuccess = () => {
591
+ const l = c.result;
592
+ l && (l.attempts = (l.attempts ?? 0) + 1, l.lastAttemptAt = Date.now(), o.put(l, t));
593
+ }, n.oncomplete = () => s(), n.onerror = () => i(n.error);
594
+ });
595
+ }
596
+ async clearSession(t) {
597
+ const s = this.ensureDb().transaction(
598
+ [u.sessions, u.recordings, u.annotations],
599
+ "readwrite"
600
+ );
601
+ s.objectStore(u.sessions).delete(t);
602
+ for (const i of [u.recordings, u.annotations]) {
603
+ const c = s.objectStore(i).index("sessionId").openCursor(IDBKeyRange.only(t));
604
+ c.onsuccess = () => {
605
+ const l = c.result;
606
+ l && (l.delete(), l.continue());
607
+ };
608
+ }
609
+ return new Promise((i, n) => {
610
+ s.oncomplete = () => i(), s.onerror = () => n(s.error);
611
+ });
612
+ }
613
+ close() {
614
+ var t;
615
+ (t = this.db) == null || t.close(), this.db = null;
616
+ }
617
+ ensureDb() {
618
+ if (!this.db) throw new Error("[FirstLook] Storage not opened. Call open() first.");
619
+ return this.db;
620
+ }
621
+ async put(t, e) {
622
+ const s = this.ensureDb();
623
+ return new Promise((i, n) => {
624
+ const o = s.transaction(t, "readwrite");
625
+ o.objectStore(t).put(e), o.oncomplete = () => i(), o.onerror = () => n(o.error);
626
+ });
627
+ }
628
+ async get(t, e) {
629
+ const s = this.ensureDb();
630
+ return new Promise((i, n) => {
631
+ const c = s.transaction(t, "readonly").objectStore(t).get(e);
632
+ c.onsuccess = () => i(c.result), c.onerror = () => n(c.error);
633
+ });
634
+ }
635
+ async getAllByIndex(t, e, s) {
636
+ const i = this.ensureDb();
637
+ return new Promise((n, o) => {
638
+ const h = i.transaction(t, "readonly").objectStore(t).index(e).getAll(s);
639
+ h.onsuccess = () => n(h.result), h.onerror = () => o(h.error);
640
+ });
641
+ }
642
+ }
643
+ const k = ["#e17055", "#6c5ce7", "#00b894", "#fdcb6e", "#ffffff"];
644
+ class O {
645
+ constructor(t) {
646
+ this.shadowRoot = t, this.overlay = null, this.canvas = null, this.ctx = null, this.drawing = !1, this.currentPath = [], this.paths = [], this.selectedColor = k[0], this.screenshotDataUrl = "";
647
+ }
648
+ async open() {
649
+ return this.screenshotDataUrl = await this.captureScreenshot(), new Promise((t) => {
650
+ this.overlay = r("div", { className: "fl-annotation-overlay" });
651
+ const e = r("div", { className: "fl-annotation-canvas-wrap" });
652
+ this.canvas = document.createElement("canvas"), this.canvas.className = "fl-annotation-canvas", e.appendChild(this.canvas), this.overlay.appendChild(e);
653
+ const s = r("input", {
654
+ className: "fl-annotation-comment",
655
+ type: "text",
656
+ placeholder: "Add a comment..."
657
+ }), i = r("div", { className: "fl-color-picker" });
658
+ for (const l of k) {
659
+ const h = r("div", { className: `fl-color-swatch ${l === this.selectedColor ? "fl-selected" : ""}` });
660
+ h.style.background = l, h.onclick = () => {
661
+ this.selectedColor = l, i.querySelectorAll(".fl-color-swatch").forEach((d) => d.classList.remove("fl-selected")), h.classList.add("fl-selected");
662
+ }, i.appendChild(h);
663
+ }
664
+ const n = r("button", { className: "fl-btn fl-btn-ok" }, ["Submit"]);
665
+ n.onclick = () => {
666
+ const l = {
667
+ screenshotDataUrl: this.getAnnotatedImage(),
668
+ drawings: [...this.paths],
669
+ comment: s.value,
670
+ timestamp: Date.now()
671
+ };
672
+ this.close(), t(l);
673
+ };
674
+ const o = r("button", { className: "fl-btn fl-btn-ng" }, ["Cancel"]);
675
+ o.onclick = () => {
676
+ this.close(), t(null);
677
+ };
678
+ const c = r("div", { className: "fl-annotation-toolbar" }, [
679
+ i,
680
+ s,
681
+ n,
682
+ o
683
+ ]);
684
+ this.overlay.appendChild(c), this.shadowRoot.appendChild(this.overlay), this.initCanvas();
685
+ });
686
+ }
687
+ close() {
688
+ var t;
689
+ (t = this.overlay) == null || t.remove(), this.overlay = null, this.canvas = null, this.ctx = null, this.paths = [], this.currentPath = [];
690
+ }
691
+ async captureScreenshot() {
692
+ try {
693
+ const t = window.innerWidth, e = window.innerHeight, s = document.documentElement.cloneNode(!0), i = s.querySelector("#firstlook-sdk-root");
694
+ i == null || i.remove();
695
+ const n = s.querySelectorAll('[data-fl-masked], input[type="password"]');
696
+ for (const p of n)
697
+ (p instanceof HTMLInputElement || p instanceof HTMLTextAreaElement) && (p.value = "[MASKED]"), p.textContent = "[MASKED]";
698
+ for (const p of s.querySelectorAll("script")) p.remove();
699
+ const o = s.querySelector("body");
700
+ if (o) {
701
+ const p = window.getComputedStyle(document.body);
702
+ o.style.backgroundColor = p.backgroundColor, o.style.color = p.color, o.style.fontFamily = p.fontFamily, o.style.margin = "0", o.style.overflow = "hidden";
703
+ }
704
+ const c = new XMLSerializer().serializeToString(s), l = `<svg xmlns="http://www.w3.org/2000/svg" width="${t}" height="${e}">
705
+ <foreignObject width="100%" height="100%">
706
+ ${c}
707
+ </foreignObject>
708
+ </svg>`, h = new Blob([l], { type: "image/svg+xml;charset=utf-8" }), d = URL.createObjectURL(h), f = document.createElement("canvas");
709
+ f.width = t * window.devicePixelRatio, f.height = e * window.devicePixelRatio;
710
+ const m = f.getContext("2d");
711
+ return m.scale(window.devicePixelRatio, window.devicePixelRatio), new Promise((p) => {
712
+ const g = new Image();
713
+ g.onload = () => {
714
+ m.drawImage(g, 0, 0, t, e), URL.revokeObjectURL(d), p(f.toDataURL("image/png"));
715
+ }, g.onerror = () => {
716
+ URL.revokeObjectURL(d), p(this.captureScreenshotFallback(t, e));
717
+ }, g.src = d;
718
+ });
719
+ } catch {
720
+ return this.captureScreenshotFallback(window.innerWidth, window.innerHeight);
721
+ }
722
+ }
723
+ captureScreenshotFallback(t, e) {
724
+ const s = document.createElement("canvas");
725
+ s.width = t, s.height = e;
726
+ const i = s.getContext("2d"), n = window.getComputedStyle(document.body);
727
+ return i.fillStyle = n.backgroundColor || "#ffffff", i.fillRect(0, 0, t, e), i.fillStyle = "#333", i.font = "14px sans-serif", i.fillText(`URL: ${window.location.href}`, 20, 30), i.fillText(`Time: ${(/* @__PURE__ */ new Date()).toISOString()}`, 20, 50), i.fillText("(SVG capture unavailable — metadata view)", 20, 70), s.toDataURL("image/png");
728
+ }
729
+ initCanvas() {
730
+ if (!this.canvas) return;
731
+ const t = new Image();
732
+ t.onload = () => {
733
+ const e = window.innerWidth * 0.9, s = window.innerHeight * 0.7, i = Math.min(e / t.width, s / t.height, 1);
734
+ this.canvas.width = t.width * i, this.canvas.height = t.height * i, this.ctx = this.canvas.getContext("2d"), this.ctx.drawImage(t, 0, 0, this.canvas.width, this.canvas.height), this.canvas.addEventListener("pointerdown", this.onDrawStart.bind(this)), this.canvas.addEventListener("pointermove", this.onDrawMove.bind(this)), this.canvas.addEventListener("pointerup", this.onDrawEnd.bind(this)), this.canvas.addEventListener("pointerleave", this.onDrawEnd.bind(this));
735
+ }, t.src = this.screenshotDataUrl;
736
+ }
737
+ onDrawStart(t) {
738
+ this.drawing = !0;
739
+ const e = this.canvas.getBoundingClientRect();
740
+ this.currentPath = [{ x: t.clientX - e.left, y: t.clientY - e.top }];
741
+ }
742
+ onDrawMove(t) {
743
+ if (!this.drawing || !this.ctx) return;
744
+ const e = this.canvas.getBoundingClientRect(), s = { x: t.clientX - e.left, y: t.clientY - e.top };
745
+ if (this.currentPath.push(s), this.ctx.strokeStyle = this.selectedColor, this.ctx.lineWidth = 3, this.ctx.lineCap = "round", this.ctx.lineJoin = "round", this.currentPath.length >= 2) {
746
+ const i = this.currentPath[this.currentPath.length - 2];
747
+ this.ctx.beginPath(), this.ctx.moveTo(i.x, i.y), this.ctx.lineTo(s.x, s.y), this.ctx.stroke();
748
+ }
749
+ }
750
+ onDrawEnd() {
751
+ this.drawing && this.currentPath.length > 0 && this.paths.push({
752
+ points: [...this.currentPath],
753
+ color: this.selectedColor,
754
+ width: 3
755
+ }), this.drawing = !1, this.currentPath = [];
756
+ }
757
+ getAnnotatedImage() {
758
+ var t;
759
+ return ((t = this.canvas) == null ? void 0 : t.toDataURL("image/png")) || this.screenshotDataUrl;
760
+ }
761
+ }
762
+ class Q {
763
+ constructor(t) {
764
+ this.onShake = t, this.lastX = 0, this.lastY = 0, this.lastZ = 0, this.shakeCount = 0, this.lastShakeTime = 0, this.handler = null, this.THRESHOLD = 15, this.SHAKE_INTERVAL = 400, this.REQUIRED_SHAKES = 2;
765
+ }
766
+ async start() {
767
+ if (typeof DeviceMotionEvent.requestPermission == "function")
768
+ try {
769
+ if (await DeviceMotionEvent.requestPermission() !== "granted") return;
770
+ } catch {
771
+ return;
772
+ }
773
+ this.handler = this.onMotion.bind(this), window.addEventListener("devicemotion", this.handler, { passive: !0 });
774
+ }
775
+ stop() {
776
+ this.handler && (window.removeEventListener("devicemotion", this.handler), this.handler = null);
777
+ }
778
+ onMotion(t) {
779
+ const e = t.accelerationIncludingGravity;
780
+ if (!e || e.x == null || e.y == null || e.z == null) return;
781
+ const s = Math.abs(e.x - this.lastX), i = Math.abs(e.y - this.lastY), n = Math.abs(e.z - this.lastZ);
782
+ if (s + i + n > this.THRESHOLD) {
783
+ const o = Date.now();
784
+ o - this.lastShakeTime < this.SHAKE_INTERVAL ? (this.shakeCount++, this.shakeCount >= this.REQUIRED_SHAKES && (this.shakeCount = 0, this.onShake())) : this.shakeCount = 1, this.lastShakeTime = o;
785
+ }
786
+ this.lastX = e.x, this.lastY = e.y, this.lastZ = e.z;
787
+ }
788
+ }
789
+ class B {
790
+ constructor(t, e) {
791
+ this.shadowRoot = t, this.events = e, this.annotations = [], this.active = !1, this.onKeydown = (s) => {
792
+ s.ctrlKey && s.shiftKey && s.key === "R" && (s.preventDefault(), this.trigger());
793
+ }, this.annotationCanvas = new O(t), this.shakeTrigger = new Q(() => this.trigger());
794
+ }
795
+ async start() {
796
+ await this.shakeTrigger.start(), document.addEventListener("keydown", this.onKeydown);
797
+ }
798
+ stop() {
799
+ this.shakeTrigger.stop(), document.removeEventListener("keydown", this.onKeydown);
800
+ }
801
+ getAnnotations() {
802
+ return [...this.annotations];
803
+ }
804
+ async trigger() {
805
+ if (!this.active) {
806
+ this.active = !0;
807
+ try {
808
+ const t = await this.annotationCanvas.open();
809
+ t && (this.annotations.push(t), this.events.emit({ type: "report:submitted" }));
810
+ } finally {
811
+ this.active = !1;
812
+ }
813
+ }
814
+ }
815
+ }
816
+ class H {
817
+ constructor(t, e) {
818
+ this.requiredTaps = t, this.onActivate = e, this.tapCount = 0, this.tapTimer = null, this.handler = null;
819
+ }
820
+ start() {
821
+ this.handler = this.onTap.bind(this), document.addEventListener("pointerdown", this.handler, { passive: !0 });
822
+ }
823
+ stop() {
824
+ this.handler && (document.removeEventListener("pointerdown", this.handler), this.handler = null), this.reset();
825
+ }
826
+ onTap(t) {
827
+ this.tapCount++, this.tapTimer && clearTimeout(this.tapTimer), this.tapCount >= this.requiredTaps ? (this.reset(), this.onActivate()) : this.tapTimer = setTimeout(() => this.reset(), 800);
828
+ }
829
+ reset() {
830
+ this.tapCount = 0, this.tapTimer && (clearTimeout(this.tapTimer), this.tapTimer = null);
831
+ }
832
+ }
833
+ class U {
834
+ constructor(t) {
835
+ this.onActivate = t, this.hashHandler = null;
836
+ }
837
+ start() {
838
+ this.checkUrl() && this.onActivate(), this.hashHandler = () => {
839
+ this.checkUrl() && this.onActivate();
840
+ }, window.addEventListener("hashchange", this.hashHandler);
841
+ }
842
+ stop() {
843
+ this.hashHandler && (window.removeEventListener("hashchange", this.hashHandler), this.hashHandler = null);
844
+ }
845
+ checkUrl() {
846
+ const t = new URLSearchParams(window.location.search);
847
+ return t.has("firstlook") || t.has("uat-mode");
848
+ }
849
+ }
850
+ class j {
851
+ constructor(t, e) {
852
+ this.shadowRoot = t, this.callbacks = e, this.root = null, this.outsideClickHandler = null;
853
+ }
854
+ show() {
855
+ if (this.root) return;
856
+ this.root = r("div", { className: "fl-debug-menu" });
857
+ const t = [
858
+ { icon: "▶", label: "Start Session", action: this.callbacks.onStartSession },
859
+ { icon: "📸", label: "Report Issue", action: this.callbacks.onReportIssue },
860
+ { icon: "✖", label: "Close SDK", action: this.callbacks.onClose }
861
+ ];
862
+ for (const e of t) {
863
+ const s = r("button", { className: "fl-debug-menu-item" }, [
864
+ r("span", { className: "fl-debug-menu-item-icon" }, [e.icon]),
865
+ e.label
866
+ ]);
867
+ s.onclick = (i) => {
868
+ i.stopPropagation(), this.hide(), e.action();
869
+ }, this.root.appendChild(s);
870
+ }
871
+ this.shadowRoot.appendChild(this.root), setTimeout(() => {
872
+ this.outsideClickHandler = (e) => {
873
+ this.root && !this.root.contains(e.target) && this.hide();
874
+ }, document.addEventListener("click", this.outsideClickHandler, { capture: !0 });
875
+ }, 100);
876
+ }
877
+ hide() {
878
+ var t;
879
+ (t = this.root) == null || t.remove(), this.root = null, this.outsideClickHandler && (document.removeEventListener("click", this.outsideClickHandler, { capture: !0 }), this.outsideClickHandler = null);
880
+ }
881
+ get visible() {
882
+ return this.root !== null;
883
+ }
884
+ }
885
+ const $ = (
886
+ /* css */
887
+ `
888
+ :host {
889
+ all: initial;
890
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
891
+ font-size: 14px;
892
+ color: #1a1a2e;
893
+ line-height: 1.5;
894
+ }
895
+
896
+ * {
897
+ box-sizing: border-box;
898
+ margin: 0;
899
+ padding: 0;
900
+ }
901
+
902
+ /* === Quest Overlay === */
903
+ .fl-quest-overlay {
904
+ position: fixed;
905
+ bottom: 24px;
906
+ right: 24px;
907
+ z-index: 2147483647;
908
+ width: 360px;
909
+ max-width: calc(100vw - 48px);
910
+ background: #ffffff;
911
+ border-radius: 16px;
912
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.16), 0 2px 8px rgba(0, 0, 0, 0.08);
913
+ overflow: hidden;
914
+ animation: fl-slide-up 0.3s ease-out;
915
+ border: 1px solid rgba(0, 0, 0, 0.06);
916
+ }
917
+
918
+ .fl-quest-overlay.fl-minimized {
919
+ width: 56px;
920
+ height: 56px;
921
+ border-radius: 50%;
922
+ cursor: pointer;
923
+ display: flex;
924
+ align-items: center;
925
+ justify-content: center;
926
+ background: #6c5ce7;
927
+ box-shadow: 0 4px 16px rgba(108, 92, 231, 0.4);
928
+ }
929
+
930
+ .fl-quest-overlay.fl-minimized .fl-quest-content {
931
+ display: none;
932
+ }
933
+
934
+ .fl-quest-overlay.fl-minimized .fl-minimize-icon {
935
+ display: block;
936
+ color: #fff;
937
+ font-size: 24px;
938
+ }
939
+
940
+ .fl-minimize-icon {
941
+ display: none;
942
+ }
943
+
944
+ @keyframes fl-slide-up {
945
+ from { transform: translateY(20px); opacity: 0; }
946
+ to { transform: translateY(0); opacity: 1; }
947
+ }
948
+
949
+ .fl-quest-header {
950
+ background: linear-gradient(135deg, #6c5ce7, #a29bfe);
951
+ color: #fff;
952
+ padding: 14px 16px;
953
+ display: flex;
954
+ align-items: center;
955
+ justify-content: space-between;
956
+ gap: 8px;
957
+ }
958
+
959
+ .fl-quest-header-left {
960
+ display: flex;
961
+ align-items: center;
962
+ gap: 8px;
963
+ flex: 1;
964
+ min-width: 0;
965
+ }
966
+
967
+ .fl-quest-badge {
968
+ background: rgba(255, 255, 255, 0.2);
969
+ border-radius: 12px;
970
+ padding: 2px 10px;
971
+ font-size: 11px;
972
+ font-weight: 600;
973
+ white-space: nowrap;
974
+ letter-spacing: 0.5px;
975
+ }
976
+
977
+ .fl-quest-title {
978
+ font-size: 14px;
979
+ font-weight: 600;
980
+ overflow: hidden;
981
+ text-overflow: ellipsis;
982
+ white-space: nowrap;
983
+ }
984
+
985
+ .fl-quest-minimize-btn {
986
+ background: rgba(255, 255, 255, 0.2);
987
+ border: none;
988
+ color: #fff;
989
+ width: 28px;
990
+ height: 28px;
991
+ border-radius: 50%;
992
+ cursor: pointer;
993
+ display: flex;
994
+ align-items: center;
995
+ justify-content: center;
996
+ font-size: 16px;
997
+ flex-shrink: 0;
998
+ transition: background 0.15s;
999
+ }
1000
+
1001
+ .fl-quest-minimize-btn:hover {
1002
+ background: rgba(255, 255, 255, 0.3);
1003
+ }
1004
+
1005
+ .fl-quest-body {
1006
+ padding: 16px;
1007
+ }
1008
+
1009
+ .fl-quest-description {
1010
+ font-size: 14px;
1011
+ color: #4a4a5a;
1012
+ margin-bottom: 16px;
1013
+ line-height: 1.6;
1014
+ }
1015
+
1016
+ .fl-quest-progress {
1017
+ display: flex;
1018
+ gap: 4px;
1019
+ margin-bottom: 16px;
1020
+ }
1021
+
1022
+ .fl-quest-progress-dot {
1023
+ height: 4px;
1024
+ flex: 1;
1025
+ border-radius: 2px;
1026
+ background: #e0e0e0;
1027
+ transition: background 0.3s;
1028
+ }
1029
+
1030
+ .fl-quest-progress-dot.fl-completed { background: #00b894; }
1031
+ .fl-quest-progress-dot.fl-failed { background: #e17055; }
1032
+ .fl-quest-progress-dot.fl-active { background: #6c5ce7; }
1033
+
1034
+ .fl-quest-actions {
1035
+ display: flex;
1036
+ gap: 10px;
1037
+ }
1038
+
1039
+ .fl-btn {
1040
+ flex: 1;
1041
+ padding: 10px 16px;
1042
+ border-radius: 10px;
1043
+ border: none;
1044
+ font-size: 14px;
1045
+ font-weight: 600;
1046
+ cursor: pointer;
1047
+ transition: transform 0.1s, box-shadow 0.15s;
1048
+ display: flex;
1049
+ align-items: center;
1050
+ justify-content: center;
1051
+ gap: 6px;
1052
+ }
1053
+
1054
+ .fl-btn:active { transform: scale(0.97); }
1055
+
1056
+ .fl-btn-ok {
1057
+ background: #00b894;
1058
+ color: #fff;
1059
+ box-shadow: 0 2px 8px rgba(0, 184, 148, 0.3);
1060
+ }
1061
+ .fl-btn-ok:hover { box-shadow: 0 4px 12px rgba(0, 184, 148, 0.4); }
1062
+
1063
+ .fl-btn-ng {
1064
+ background: #e17055;
1065
+ color: #fff;
1066
+ box-shadow: 0 2px 8px rgba(225, 112, 85, 0.3);
1067
+ }
1068
+ .fl-btn-ng:hover { box-shadow: 0 4px 12px rgba(225, 112, 85, 0.4); }
1069
+
1070
+ /* === Voice recording indicator === */
1071
+ .fl-voice-indicator {
1072
+ display: flex;
1073
+ align-items: center;
1074
+ gap: 8px;
1075
+ padding: 8px 16px;
1076
+ background: #fff3f0;
1077
+ border-top: 1px solid rgba(225, 112, 85, 0.1);
1078
+ font-size: 12px;
1079
+ color: #e17055;
1080
+ }
1081
+
1082
+ .fl-voice-dot {
1083
+ width: 8px;
1084
+ height: 8px;
1085
+ border-radius: 50%;
1086
+ background: #e17055;
1087
+ animation: fl-pulse 1.2s ease-in-out infinite;
1088
+ }
1089
+
1090
+ @keyframes fl-pulse {
1091
+ 0%, 100% { opacity: 1; }
1092
+ 50% { opacity: 0.3; }
1093
+ }
1094
+
1095
+ /* === Annotation overlay === */
1096
+ .fl-annotation-overlay {
1097
+ position: fixed;
1098
+ inset: 0;
1099
+ z-index: 2147483647;
1100
+ background: rgba(0, 0, 0, 0.7);
1101
+ display: flex;
1102
+ flex-direction: column;
1103
+ align-items: center;
1104
+ justify-content: center;
1105
+ animation: fl-fade-in 0.2s ease-out;
1106
+ }
1107
+
1108
+ @keyframes fl-fade-in {
1109
+ from { opacity: 0; }
1110
+ to { opacity: 1; }
1111
+ }
1112
+
1113
+ .fl-annotation-canvas-wrap {
1114
+ position: relative;
1115
+ border-radius: 8px;
1116
+ overflow: hidden;
1117
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
1118
+ }
1119
+
1120
+ .fl-annotation-canvas {
1121
+ display: block;
1122
+ cursor: crosshair;
1123
+ max-width: 90vw;
1124
+ max-height: 70vh;
1125
+ }
1126
+
1127
+ .fl-annotation-toolbar {
1128
+ display: flex;
1129
+ gap: 8px;
1130
+ margin-top: 16px;
1131
+ align-items: center;
1132
+ }
1133
+
1134
+ .fl-annotation-toolbar .fl-btn {
1135
+ flex: none;
1136
+ padding: 10px 20px;
1137
+ }
1138
+
1139
+ .fl-annotation-comment {
1140
+ width: 300px;
1141
+ padding: 10px 14px;
1142
+ border-radius: 10px;
1143
+ border: 2px solid rgba(255, 255, 255, 0.3);
1144
+ background: rgba(255, 255, 255, 0.15);
1145
+ color: #fff;
1146
+ font-size: 14px;
1147
+ outline: none;
1148
+ backdrop-filter: blur(4px);
1149
+ transition: border-color 0.15s;
1150
+ }
1151
+
1152
+ .fl-annotation-comment::placeholder { color: rgba(255, 255, 255, 0.5); }
1153
+ .fl-annotation-comment:focus { border-color: #a29bfe; }
1154
+
1155
+ .fl-color-picker { display: flex; gap: 4px; }
1156
+
1157
+ .fl-color-swatch {
1158
+ width: 24px;
1159
+ height: 24px;
1160
+ border-radius: 50%;
1161
+ border: 2px solid transparent;
1162
+ cursor: pointer;
1163
+ transition: transform 0.1s;
1164
+ }
1165
+ .fl-color-swatch:hover { transform: scale(1.15); }
1166
+ .fl-color-swatch.fl-selected {
1167
+ border-color: #fff;
1168
+ box-shadow: 0 0 0 2px rgba(108, 92, 231, 0.6);
1169
+ }
1170
+
1171
+ /* === Watermark === */
1172
+ .fl-watermark {
1173
+ position: fixed;
1174
+ inset: 0;
1175
+ z-index: 2147483640;
1176
+ pointer-events: none;
1177
+ overflow: hidden;
1178
+ opacity: 0.03;
1179
+ }
1180
+
1181
+ .fl-watermark-tile {
1182
+ position: absolute;
1183
+ font-size: 12px;
1184
+ font-family: monospace;
1185
+ color: #000;
1186
+ white-space: nowrap;
1187
+ transform: rotate(-30deg);
1188
+ user-select: none;
1189
+ }
1190
+
1191
+ /* === Debug Menu === */
1192
+ .fl-debug-menu {
1193
+ position: fixed;
1194
+ bottom: 100px;
1195
+ right: 24px;
1196
+ z-index: 2147483646;
1197
+ background: #fff;
1198
+ border-radius: 12px;
1199
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.16);
1200
+ padding: 8px;
1201
+ min-width: 200px;
1202
+ animation: fl-slide-up 0.2s ease-out;
1203
+ border: 1px solid rgba(0, 0, 0, 0.06);
1204
+ }
1205
+
1206
+ .fl-debug-menu-item {
1207
+ display: flex;
1208
+ align-items: center;
1209
+ gap: 10px;
1210
+ padding: 10px 14px;
1211
+ border: none;
1212
+ background: none;
1213
+ width: 100%;
1214
+ text-align: left;
1215
+ border-radius: 8px;
1216
+ cursor: pointer;
1217
+ font-size: 14px;
1218
+ color: #1a1a2e;
1219
+ transition: background 0.15s;
1220
+ }
1221
+ .fl-debug-menu-item:hover { background: #f0f0f8; }
1222
+
1223
+ .fl-debug-menu-item-icon {
1224
+ font-size: 18px;
1225
+ width: 24px;
1226
+ text-align: center;
1227
+ }
1228
+
1229
+ /* === Session status bar === */
1230
+ .fl-status-bar {
1231
+ display: flex;
1232
+ align-items: center;
1233
+ gap: 8px;
1234
+ padding: 6px 16px;
1235
+ background: #f8f8fc;
1236
+ border-top: 1px solid rgba(0, 0, 0, 0.04);
1237
+ font-size: 11px;
1238
+ color: #888;
1239
+ }
1240
+
1241
+ .fl-status-recording {
1242
+ display: flex;
1243
+ align-items: center;
1244
+ gap: 4px;
1245
+ color: #e17055;
1246
+ }
1247
+
1248
+ .fl-status-timer {
1249
+ margin-left: auto;
1250
+ font-variant-numeric: tabular-nums;
1251
+ }
1252
+
1253
+ /* === Completion summary === */
1254
+ .fl-summary { padding: 20px 16px; text-align: center; }
1255
+ .fl-summary-icon { font-size: 48px; margin-bottom: 12px; }
1256
+ .fl-summary-title { font-size: 18px; font-weight: 700; color: #1a1a2e; margin-bottom: 4px; }
1257
+ .fl-summary-subtitle { font-size: 13px; color: #888; margin-bottom: 16px; }
1258
+ .fl-summary-stats { display: flex; justify-content: center; gap: 24px; margin-bottom: 16px; }
1259
+ .fl-stat { text-align: center; }
1260
+ .fl-stat-value { font-size: 24px; font-weight: 700; }
1261
+ .fl-stat-value.fl-ok { color: #00b894; }
1262
+ .fl-stat-value.fl-ng { color: #e17055; }
1263
+ .fl-stat-label { font-size: 11px; color: #888; text-transform: uppercase; letter-spacing: 0.5px; }
1264
+
1265
+ .fl-btn-finish {
1266
+ background: #6c5ce7;
1267
+ color: #fff;
1268
+ width: 100%;
1269
+ box-shadow: 0 2px 8px rgba(108, 92, 231, 0.3);
1270
+ }
1271
+
1272
+ .fl-btn-skip {
1273
+ background: #636e72;
1274
+ color: #fff;
1275
+ box-shadow: 0 2px 8px rgba(99, 110, 114, 0.3);
1276
+ }
1277
+
1278
+ /* === NG Voice Feedback Mode === */
1279
+ .fl-quest-header-ng { background: linear-gradient(135deg, #e17055, #d63031); }
1280
+ .fl-badge-ng { background: rgba(255, 255, 255, 0.25); }
1281
+
1282
+ .fl-feedback-textarea {
1283
+ width: calc(100% - 32px);
1284
+ margin: 8px 16px 16px;
1285
+ padding: 10px 12px;
1286
+ border-radius: 8px;
1287
+ border: 1px solid #ddd;
1288
+ font-size: 13px;
1289
+ font-family: inherit;
1290
+ resize: vertical;
1291
+ outline: none;
1292
+ transition: border-color 0.15s;
1293
+ }
1294
+ .fl-feedback-textarea:focus { border-color: #6c5ce7; }
1295
+ .fl-feedback-textarea-modal { width: calc(100% - 32px); margin: 16px 16px 12px; }
1296
+
1297
+ /* === Memo button === */
1298
+ .fl-quest-memo-row { display: flex; margin-bottom: 10px; }
1299
+ .fl-btn-memo {
1300
+ flex: none;
1301
+ background: #f0f0f8;
1302
+ color: #4a4a5a;
1303
+ font-size: 12px;
1304
+ padding: 6px 12px;
1305
+ box-shadow: none;
1306
+ border-radius: 8px;
1307
+ }
1308
+ .fl-btn-memo:hover { background: #e4e4f0; }
1309
+
1310
+ /* === Feedback modal === */
1311
+ .fl-feedback-modal {
1312
+ position: absolute;
1313
+ inset: 0;
1314
+ background: #fff;
1315
+ border-radius: 16px;
1316
+ z-index: 10;
1317
+ display: flex;
1318
+ flex-direction: column;
1319
+ animation: fl-fade-in 0.15s ease-out;
1320
+ }
1321
+ .fl-feedback-modal-actions { padding: 0 16px 16px; }
1322
+ `
1323
+ ), K = 5;
1324
+ class _ {
1325
+ constructor() {
1326
+ this.config = null, this.events = new T(), this.state = "idle", this.hostElement = null, this.shadowRoot = null, this.questManager = null, this.questOverlay = null, this.sessionRecorder = null, this.voiceRecorder = null, this.watermark = null, this.fieldMasker = null, this.storage = null, this.shakeReporter = null, this.debugMenu = null, this.tapTrigger = null, this.deepLinkTrigger = null, this.sessionId = null, this.sessionStartTime = 0, this.maxDurationTimer = null, this.backupTimer = null, this.isFlushing = !1;
1327
+ }
1328
+ // ----------------------------------------------------------------
1329
+ // Public API
1330
+ // ----------------------------------------------------------------
1331
+ /**
1332
+ * Initialize the SDK with configuration.
1333
+ * This does NOT activate the UI — it only sets up triggers.
1334
+ */
1335
+ async init(t) {
1336
+ if (this.state !== "idle") {
1337
+ console.warn("[FirstLook] SDK already initialized.");
1338
+ return;
1339
+ }
1340
+ this.config = S(t), this.storage = new P(), await this.storage.open(), this.createShadowHost(), this.setupTriggers(), this.fieldMasker = new N(this.config.security.maskSelectors), this.state = "initialized", this.events.emit({ type: "sdk:initialized" }), this.flushUploadQueue(!0);
1341
+ }
1342
+ /**
1343
+ * Manually activate the SDK UI (bypassing triggers).
1344
+ */
1345
+ activate() {
1346
+ if (this.state !== "initialized") {
1347
+ console.warn("[FirstLook] Cannot activate: SDK not initialized or already active.");
1348
+ return;
1349
+ }
1350
+ this.onActivate();
1351
+ }
1352
+ /**
1353
+ * Start a UAT session with a list of quests.
1354
+ */
1355
+ async startSession(t) {
1356
+ var s;
1357
+ if (this.state !== "active")
1358
+ throw new Error("[FirstLook] Cannot start session: SDK not active. Call activate() first.");
1359
+ (s = this.debugMenu) == null || s.hide(), this.sessionId = I(), this.sessionStartTime = Date.now(), this.questManager = new C(this.events), this.questManager.loadQuests(t), this.sessionRecorder = new D(
1360
+ this.config,
1361
+ this.events,
1362
+ this.fieldMasker.getCombinedSelector()
1363
+ ), this.voiceRecorder = new L(this.events), this.shakeReporter = new B(this.shadowRoot, this.events), await this.shakeReporter.start(), this.sessionRecorder.start(), this.fieldMasker.start(), this.config.recording.voice && await this.voiceRecorder.start(), this.config.security.watermark && (this.watermark = new A(this.shadowRoot, this.config), this.watermark.show()), this.questOverlay = new M(this.shadowRoot, {
1364
+ onOk: () => this.handleQuestOk(),
1365
+ onNg: () => this.handleQuestNg(),
1366
+ onNgWithFeedback: (i) => this.handleNgFeedbackSubmit(i),
1367
+ onMemo: (i) => this.handleMemo(i),
1368
+ onFinish: () => this.endSession(),
1369
+ onMinimize: () => {
1370
+ }
1371
+ }), this.questManager.startSession(this.sessionStartTime), this.renderCurrentQuest();
1372
+ const e = this.config.recording.maxDuration;
1373
+ return e > 0 && (this.maxDurationTimer = setTimeout(() => this.endSession(), e * 1e3)), this.backupTimer = setInterval(() => this.backupSession(), 3e4), this.state = "recording", this.events.emit({ type: "session:started", sessionId: this.sessionId }), this.sessionId;
1374
+ }
1375
+ /**
1376
+ * Fetch quest definitions from the API and start a session.
1377
+ */
1378
+ async startSessionFromRemote(t) {
1379
+ if (this.state !== "active")
1380
+ throw new Error("[FirstLook] Cannot start session: SDK not active. Call activate() first.");
1381
+ if (!this.config)
1382
+ throw new Error("[FirstLook] SDK not initialized.");
1383
+ const e = this.config.endpoint.replace(/\/ingest-session$/, ""), s = await fetch(`${e}/export-quests?id=${encodeURIComponent(t)}`, {
1384
+ headers: { "X-API-Key": this.config.apiKey }
1385
+ });
1386
+ if (!s.ok)
1387
+ throw new Error(`[FirstLook] Failed to fetch quest set: ${s.status}`);
1388
+ const n = (await s.json()).quests;
1389
+ return this.startSession(n);
1390
+ }
1391
+ /**
1392
+ * End the current session, persist data, and trigger upload.
1393
+ */
1394
+ async endSession() {
1395
+ var i, n, o, c, l, h, d, f, m, p, g;
1396
+ if (this.state !== "recording" || !this.sessionId) return null;
1397
+ this.maxDurationTimer && (clearTimeout(this.maxDurationTimer), this.maxDurationTimer = null), this.backupTimer && (clearInterval(this.backupTimer), this.backupTimer = null), (i = this.sessionRecorder) == null || i.stop(), await ((n = this.voiceRecorder) == null ? void 0 : n.stopAsync()), (o = this.shakeReporter) == null || o.stop(), (c = this.fieldMasker) == null || c.stop(), (l = this.watermark) == null || l.hide();
1398
+ const t = this.questManager.getResults();
1399
+ await Promise.allSettled(
1400
+ t.map(async (b) => {
1401
+ if (b.voiceMemoBlob) {
1402
+ try {
1403
+ b.voiceMemoBase64 = await this.blobToBase64(b.voiceMemoBlob);
1404
+ } catch {
1405
+ }
1406
+ delete b.voiceMemoBlob;
1407
+ }
1408
+ await Promise.allSettled(
1409
+ b.feedbacks.map(async (v) => {
1410
+ if (v.voiceMemoBlob) {
1411
+ try {
1412
+ v.voiceMemoBase64 = await this.blobToBase64(v.voiceMemoBlob);
1413
+ } catch {
1414
+ }
1415
+ delete v.voiceMemoBlob;
1416
+ }
1417
+ })
1418
+ );
1419
+ })
1420
+ );
1421
+ const e = {
1422
+ sessionId: this.sessionId,
1423
+ projectId: this.config.projectId,
1424
+ userId: this.config.userId,
1425
+ role: this.config.role,
1426
+ deviceInfo: w(),
1427
+ startedAt: new Date(this.sessionStartTime).toISOString(),
1428
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1429
+ duration: Math.floor((Date.now() - this.sessionStartTime) / 1e3),
1430
+ quests: t,
1431
+ recordings: ((h = this.sessionRecorder) == null ? void 0 : h.getSnapshots()) ?? []
1432
+ };
1433
+ await ((d = this.storage) == null ? void 0 : d.saveSession(e));
1434
+ const s = ((f = this.shakeReporter) == null ? void 0 : f.getAnnotations()) ?? [];
1435
+ for (const b of s)
1436
+ await ((m = this.storage) == null ? void 0 : m.saveAnnotation(this.sessionId, b));
1437
+ return await ((p = this.storage) == null ? void 0 : p.enqueueUpload({ session: e, annotations: s })), (g = this.questOverlay) == null || g.destroy(), this.questOverlay = null, this.state = "finished", this.events.emit({ type: "session:ended", sessionId: this.sessionId }), this.flushUploadQueue(!1), e;
1438
+ }
1439
+ /**
1440
+ * Subscribe to SDK events.
1441
+ */
1442
+ on(t, e) {
1443
+ return this.events.on(t, e);
1444
+ }
1445
+ /**
1446
+ * Get current SDK state.
1447
+ */
1448
+ getState() {
1449
+ return this.state;
1450
+ }
1451
+ /**
1452
+ * Completely destroy the SDK instance and clean up all resources.
1453
+ */
1454
+ destroy() {
1455
+ var t, e, s, i, n, o, c, l, h, d;
1456
+ this.maxDurationTimer && (clearTimeout(this.maxDurationTimer), this.maxDurationTimer = null), this.backupTimer && (clearInterval(this.backupTimer), this.backupTimer = null), (t = this.tapTrigger) == null || t.stop(), (e = this.deepLinkTrigger) == null || e.stop(), (s = this.sessionRecorder) == null || s.stop(), (i = this.shakeReporter) == null || i.stop(), (n = this.fieldMasker) == null || n.stop(), (o = this.watermark) == null || o.hide(), (c = this.questOverlay) == null || c.destroy(), (l = this.debugMenu) == null || l.hide(), (h = this.hostElement) == null || h.remove(), (d = this.storage) == null || d.close(), this.events.removeAll(), this.state = "idle";
1457
+ }
1458
+ // ----------------------------------------------------------------
1459
+ // Private methods
1460
+ // ----------------------------------------------------------------
1461
+ createShadowHost() {
1462
+ this.hostElement = document.createElement("div"), this.hostElement.id = "firstlook-sdk-root", this.hostElement.style.cssText = "position:fixed;top:0;left:0;width:0;height:0;z-index:2147483647;pointer-events:none;", document.body.appendChild(this.hostElement), this.shadowRoot = this.hostElement.attachShadow({ mode: "closed" });
1463
+ const t = document.createElement("style");
1464
+ t.textContent = $, this.shadowRoot.appendChild(t);
1465
+ const e = document.createElement("div");
1466
+ e.style.cssText = "pointer-events:auto;", this.shadowRoot.appendChild(e);
1467
+ }
1468
+ setupTriggers() {
1469
+ const t = this.config.triggers;
1470
+ this.tapTrigger = new H(t.tapCount, () => this.onActivate()), this.tapTrigger.start(), t.deepLink && (this.deepLinkTrigger = new U(() => this.onActivate()), this.deepLinkTrigger.start()), t.customCheck && t.customCheck() && this.onActivate();
1471
+ }
1472
+ onActivate() {
1473
+ var t, e;
1474
+ this.state === "initialized" && (this.state = "active", (t = this.tapTrigger) == null || t.stop(), (e = this.deepLinkTrigger) == null || e.stop(), this.events.emit({ type: "sdk:activated" }), this.debugMenu = new j(this.shadowRoot, {
1475
+ onStartSession: () => {
1476
+ this.events.emit({ type: "sdk:activated" });
1477
+ },
1478
+ onReportIssue: () => {
1479
+ var s, i;
1480
+ (i = (s = this.shakeReporter) == null ? void 0 : s.trigger) == null || i.call(s);
1481
+ },
1482
+ onClose: () => {
1483
+ this.destroy();
1484
+ }
1485
+ }), this.debugMenu.show());
1486
+ }
1487
+ handleQuestOk() {
1488
+ if (!this.questManager || !this.sessionRecorder) return;
1489
+ const t = this.sessionRecorder.getActionLogs();
1490
+ this.questManager.completeCurrentQuest(t), this.renderCurrentQuest();
1491
+ }
1492
+ handleQuestNg() {
1493
+ if (!this.questManager || !this.questOverlay) return;
1494
+ const t = this.questManager.getCurrentQuest();
1495
+ t && this.questOverlay.renderFeedbackModal(t.title, "ng");
1496
+ }
1497
+ handleNgFeedbackSubmit(t) {
1498
+ if (!this.questManager || !this.sessionRecorder) return;
1499
+ const e = this.sessionRecorder.getActionLogs();
1500
+ this.questManager.failCurrentQuest(e, t || void 0), this.renderCurrentQuest();
1501
+ }
1502
+ handleMemo(t) {
1503
+ if (!this.questManager || !t) return;
1504
+ const e = {
1505
+ comment: t,
1506
+ timestamp: Date.now(),
1507
+ relativeTime: Date.now() - this.sessionStartTime
1508
+ };
1509
+ this.questManager.addFeedback(e);
1510
+ }
1511
+ renderCurrentQuest() {
1512
+ var s;
1513
+ if (!this.questManager || !this.questOverlay) return;
1514
+ const t = this.questManager.getStatus();
1515
+ if (t.isBlocked) {
1516
+ const i = this.questManager.getCurrentQuest();
1517
+ this.questOverlay.renderBlocked((i == null ? void 0 : i.title) ?? "Unknown");
1518
+ return;
1519
+ }
1520
+ if (t.isFinished) {
1521
+ this.questOverlay.renderSummary(t.completed, t.failed, t.total);
1522
+ return;
1523
+ }
1524
+ const e = this.questManager.getCurrentQuest();
1525
+ e && this.questOverlay.renderQuest(
1526
+ e,
1527
+ this.questManager.getQuestStatuses(),
1528
+ ((s = this.voiceRecorder) == null ? void 0 : s.recording) ?? !1
1529
+ );
1530
+ }
1531
+ async flushUploadQueue(t) {
1532
+ if (!(!this.storage || !this.config || this.isFlushing)) {
1533
+ this.isFlushing = !0;
1534
+ try {
1535
+ if (t) {
1536
+ const s = navigator.connection;
1537
+ if (s && s.type && s.type !== "wifi" && s.effectiveType !== "4g")
1538
+ return;
1539
+ }
1540
+ const e = await this.storage.getPendingUploads();
1541
+ for (const s of e) {
1542
+ if (s.attempts >= K) {
1543
+ await this.storage.removeFromQueue(s.key);
1544
+ continue;
1545
+ }
1546
+ if (s.attempts > 0) {
1547
+ const i = Math.pow(2, s.attempts) * 1e3, n = s.lastAttemptAt ?? 0;
1548
+ if (Date.now() - n < i)
1549
+ continue;
1550
+ }
1551
+ try {
1552
+ (await fetch(this.config.endpoint, {
1553
+ method: "POST",
1554
+ headers: {
1555
+ "Content-Type": "application/json",
1556
+ "X-API-Key": this.config.apiKey
1557
+ },
1558
+ body: JSON.stringify(s.payload)
1559
+ })).ok ? await this.storage.removeFromQueue(s.key) : await this.storage.incrementAttempts(s.key);
1560
+ } catch {
1561
+ await this.storage.incrementAttempts(s.key);
1562
+ }
1563
+ }
1564
+ } finally {
1565
+ this.isFlushing = !1;
1566
+ }
1567
+ }
1568
+ }
1569
+ async backupSession() {
1570
+ var t;
1571
+ if (!(!this.storage || !this.sessionId || !this.config || !this.sessionRecorder))
1572
+ try {
1573
+ const e = {
1574
+ sessionId: this.sessionId,
1575
+ projectId: this.config.projectId,
1576
+ userId: this.config.userId,
1577
+ role: this.config.role,
1578
+ deviceInfo: w(),
1579
+ startedAt: new Date(this.sessionStartTime).toISOString(),
1580
+ duration: Math.floor((Date.now() - this.sessionStartTime) / 1e3),
1581
+ quests: ((t = this.questManager) == null ? void 0 : t.getResults()) ?? [],
1582
+ recordings: this.sessionRecorder.getSnapshots()
1583
+ };
1584
+ await this.storage.saveSession(e);
1585
+ } catch {
1586
+ }
1587
+ }
1588
+ blobToBase64(t) {
1589
+ return new Promise((e, s) => {
1590
+ const i = new FileReader();
1591
+ i.onloadend = () => e(i.result), i.onerror = () => s(i.error), i.readAsDataURL(t);
1592
+ });
1593
+ }
1594
+ }
1595
+ export {
1596
+ _ as FirstLookSDK
1597
+ };
1598
+ //# sourceMappingURL=firstlook.es.js.map