@offscript/sdk 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/adapters/nuxt.cjs +3 -3
- package/dist/adapters/nuxt.js +2 -2
- package/dist/adapters/vanilla.cjs +5 -5
- package/dist/adapters/vanilla.js +3 -3
- package/dist/adapters/vue.cjs +3 -3
- package/dist/adapters/vue.js +2 -2
- package/dist/{chunk-MBYNQSKA.js → chunk-HECPMGDC.js} +3 -3
- package/dist/{chunk-MBYNQSKA.js.map → chunk-HECPMGDC.js.map} +1 -1
- package/dist/{chunk-3NX2AGXM.cjs → chunk-I6JPZAQA.cjs} +738 -31
- package/dist/chunk-I6JPZAQA.cjs.map +1 -0
- package/dist/{chunk-3OSCH7J2.js → chunk-IBEPWI7S.js} +729 -22
- package/dist/chunk-IBEPWI7S.js.map +1 -0
- package/dist/{chunk-B6GO5FE5.cjs → chunk-KQVK5EJR.cjs} +4 -3
- package/dist/chunk-KQVK5EJR.cjs.map +1 -0
- package/dist/{chunk-HAPNSHFG.cjs → chunk-TBPD2LMI.cjs} +4 -4
- package/dist/{chunk-HAPNSHFG.cjs.map → chunk-TBPD2LMI.cjs.map} +1 -1
- package/dist/{chunk-LFXHB4PN.js → chunk-WUFW7TEL.js} +4 -3
- package/dist/chunk-WUFW7TEL.js.map +1 -0
- package/dist/core/OffscriptSDK.d.ts +19 -0
- package/dist/core/SessionManager.d.ts +1 -0
- package/dist/index.cjs +25 -25
- package/dist/index.js +3 -3
- package/dist/playwright.cjs +13 -13
- package/dist/playwright.js +1 -1
- package/dist/privacy/DataSanitizer.d.ts +1 -0
- package/dist/recorders/ContextRecorder.d.ts +1 -1
- package/dist/types/config.d.ts +11 -0
- package/dist/types/events.d.ts +2 -0
- package/dist/types/session.d.ts +2 -2
- package/dist/ui/RecordingControls.d.ts +60 -0
- package/docs/api-contract.md +1 -1
- package/package.json +1 -1
- package/dist/chunk-3NX2AGXM.cjs.map +0 -1
- package/dist/chunk-3OSCH7J2.js.map +0 -1
- package/dist/chunk-B6GO5FE5.cjs.map +0 -1
- package/dist/chunk-LFXHB4PN.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildPlaywrightContract, SDK_VERSION, SCHEMA_VERSION, SelectorGenerator, ACTION_SCHEMA_VERSION } from './chunk-
|
|
1
|
+
import { buildPlaywrightContract, SDK_VERSION, SCHEMA_VERSION, SelectorGenerator, ACTION_SCHEMA_VERSION } from './chunk-WUFW7TEL.js';
|
|
2
2
|
|
|
3
3
|
// src/types/config.ts
|
|
4
4
|
var DEFAULT_RECORDING = {
|
|
@@ -18,7 +18,7 @@ var DEFAULT_RECORDING = {
|
|
|
18
18
|
stableAttributes: ["name", "data-id", "aria-label"]
|
|
19
19
|
};
|
|
20
20
|
var DEFAULT_PRIVACY = {
|
|
21
|
-
maskSensitiveInputs:
|
|
21
|
+
maskSensitiveInputs: false,
|
|
22
22
|
maskAllText: false,
|
|
23
23
|
captureRequestBodies: false,
|
|
24
24
|
captureResponseBodies: false,
|
|
@@ -30,6 +30,7 @@ var DEFAULT_PRIVACY = {
|
|
|
30
30
|
ignoreUrls: [],
|
|
31
31
|
redactHeaders: [],
|
|
32
32
|
redactKeys: [],
|
|
33
|
+
captureSensitiveValues: false,
|
|
33
34
|
maxBodyBytes: 16 * 1024,
|
|
34
35
|
textContentTypes: [/^text\//i, /json/i, /xml/i, /x-www-form-urlencoded/i, /javascript/i],
|
|
35
36
|
localRetentionMs: 24 * 36e5
|
|
@@ -159,13 +160,15 @@ var DataSanitizer = class {
|
|
|
159
160
|
this.cfg = cfg;
|
|
160
161
|
this.headerSet = /* @__PURE__ */ new Set([...BASE_SENSITIVE_HEADERS, ...cfg.redactHeaders.map((h) => h.toLowerCase())]);
|
|
161
162
|
this.extraKeys = new Set(cfg.redactKeys.map((k) => k.toLowerCase()));
|
|
163
|
+
this.open = cfg.captureSensitiveValues === true;
|
|
162
164
|
}
|
|
163
165
|
isSensitiveKey(key) {
|
|
166
|
+
if (this.open) return false;
|
|
164
167
|
return BASE_SENSITIVE_KEY_PATTERN.test(key) || this.extraKeys.has(key.toLowerCase());
|
|
165
168
|
}
|
|
166
169
|
/** Mascara padrões sensíveis em texto livre (JWT, Bearer, CPF, CNPJ, cartões). */
|
|
167
170
|
scrubString(input) {
|
|
168
|
-
if (!input) return input;
|
|
171
|
+
if (this.open || !input) return input;
|
|
169
172
|
let s = input.replace(JWT, REDACTED).replace(BEARER, (_m, scheme) => `${scheme} ${REDACTED}`);
|
|
170
173
|
s = s.replace(CNPJ, REDACTED).replace(CPF, REDACTED);
|
|
171
174
|
s = s.replace(CARD, (m) => {
|
|
@@ -175,6 +178,7 @@ var DataSanitizer = class {
|
|
|
175
178
|
return s;
|
|
176
179
|
}
|
|
177
180
|
sanitizeHeaders(headers) {
|
|
181
|
+
if (this.open) return { ...headers };
|
|
178
182
|
const out = {};
|
|
179
183
|
for (const [k, v] of Object.entries(headers)) {
|
|
180
184
|
out[k] = this.headerSet.has(k.toLowerCase()) || this.isSensitiveKey(k) ? REDACTED : this.scrubString(v);
|
|
@@ -183,6 +187,7 @@ var DataSanitizer = class {
|
|
|
183
187
|
}
|
|
184
188
|
/** Redige query params sensíveis e credenciais embutidas (user:pass@). */
|
|
185
189
|
sanitizeUrl(raw, base) {
|
|
190
|
+
if (this.open) return raw;
|
|
186
191
|
try {
|
|
187
192
|
const u = new URL(raw, base ?? (typeof location !== "undefined" ? location.href : void 0));
|
|
188
193
|
if (u.username || u.password) {
|
|
@@ -201,7 +206,7 @@ var DataSanitizer = class {
|
|
|
201
206
|
}
|
|
202
207
|
/** Redige query/fragmento de um valor de atributo de URL preservando a forma original (relativa ou absoluta). */
|
|
203
208
|
redactUrlAttribute(value) {
|
|
204
|
-
if (typeof value !== "string" || !/[?#@]/.test(value)) return value;
|
|
209
|
+
if (this.open || typeof value !== "string" || !/[?#@]/.test(value)) return value;
|
|
205
210
|
const hashAt = value.indexOf("#");
|
|
206
211
|
const hash = hashAt >= 0 ? value.slice(hashAt) : "";
|
|
207
212
|
const noHash = hashAt >= 0 ? value.slice(0, hashAt) : value;
|
|
@@ -958,6 +963,7 @@ function guard(sink, name, fn) {
|
|
|
958
963
|
|
|
959
964
|
// src/recorders/ActionRecorder.ts
|
|
960
965
|
var KEYS = /* @__PURE__ */ new Set(["Enter", "Escape", "Tab", "Backspace", "Delete", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown"]);
|
|
966
|
+
var MODIFIERS = /* @__PURE__ */ new Set(["Control", "Shift", "Alt", "Meta"]);
|
|
961
967
|
var INTERACTIVE = "a,button,input,select,textarea,summary,[role=button],[role=link],[role=menuitem],[role=tab],[role=option],[data-testid],[data-test],[data-cy]";
|
|
962
968
|
var INPUT_IDLE_MS = 700;
|
|
963
969
|
var DRAG_MIN_PX = 8;
|
|
@@ -1083,7 +1089,7 @@ var ActionRecorder = class {
|
|
|
1083
1089
|
onInput(e) {
|
|
1084
1090
|
const el = this.target(e);
|
|
1085
1091
|
if (!el || !(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) return;
|
|
1086
|
-
if (el instanceof HTMLInputElement && ["checkbox", "radio", "file"
|
|
1092
|
+
if (el instanceof HTMLInputElement && ["checkbox", "radio", "file"].includes(el.type)) return;
|
|
1087
1093
|
if (this.pendingInput && this.pendingInput.el !== el) this.flushInput();
|
|
1088
1094
|
this.pendingInput ?? (this.pendingInput = { el, inputType: e.inputType ?? "input", startedAt: Date.now() });
|
|
1089
1095
|
if (this.inputTimer) clearTimeout(this.inputTimer);
|
|
@@ -1101,8 +1107,9 @@ var ActionRecorder = class {
|
|
|
1101
1107
|
const policy = this.sink.config.privacy;
|
|
1102
1108
|
const allowed = policy.allowedInputSelectors.join(",");
|
|
1103
1109
|
const isPassword = el instanceof HTMLInputElement && el.type === "password";
|
|
1104
|
-
|
|
1105
|
-
|
|
1110
|
+
const listed = Boolean(allowed) && safe(() => el.matches(allowed));
|
|
1111
|
+
if (policy.captureSensitiveValues || !isPassword && (!policy.maskSensitiveInputs || listed)) {
|
|
1112
|
+
payload.value = policy.captureSensitiveValues ? value : this.sink.sanitizer.scrubString(value);
|
|
1106
1113
|
payload.valueCaptured = true;
|
|
1107
1114
|
} else {
|
|
1108
1115
|
payload.valueCaptured = false;
|
|
@@ -1130,13 +1137,15 @@ var ActionRecorder = class {
|
|
|
1130
1137
|
}
|
|
1131
1138
|
}
|
|
1132
1139
|
onKey(e) {
|
|
1133
|
-
const el = this.target(e);
|
|
1140
|
+
const el = this.target(e) ?? document.body;
|
|
1134
1141
|
if (!el) return;
|
|
1135
1142
|
const modified = e.ctrlKey || e.metaKey || e.altKey;
|
|
1136
|
-
const
|
|
1137
|
-
|
|
1143
|
+
const isModifier = MODIFIERS.has(e.key);
|
|
1144
|
+
const relevant = KEYS.has(e.key) || isModifier || /^F\d{1,2}$/.test(e.key) || modified && e.key.length === 1;
|
|
1145
|
+
if (!relevant || isModifier && e.repeat) return;
|
|
1138
1146
|
if (e.key === "Enter" || e.key === "Tab") this.flushInput();
|
|
1139
|
-
|
|
1147
|
+
const modifiers = mods(e).filter((name) => name !== e.key);
|
|
1148
|
+
this.emit({ type: "keydown", payload: { key: e.key, code: e.code, modifiers, repeat: e.repeat } }, el);
|
|
1140
1149
|
}
|
|
1141
1150
|
onFocus(type, e) {
|
|
1142
1151
|
const el = this.target(e);
|
|
@@ -1269,7 +1278,7 @@ function parseBrowser(ua) {
|
|
|
1269
1278
|
}
|
|
1270
1279
|
return {};
|
|
1271
1280
|
}
|
|
1272
|
-
function collectContext(id, reason, appVersion) {
|
|
1281
|
+
function collectContext(id, reason, appVersion, captureSensitiveValues = false) {
|
|
1273
1282
|
const nav = navigator;
|
|
1274
1283
|
const ua = nav.userAgent ?? "";
|
|
1275
1284
|
let browser = parseBrowser(ua);
|
|
@@ -1297,7 +1306,8 @@ function collectContext(id, reason, appVersion) {
|
|
|
1297
1306
|
testCaseId: id.testCaseId,
|
|
1298
1307
|
executionId: id.executionId,
|
|
1299
1308
|
sdkVersion: SDK_VERSION,
|
|
1300
|
-
appVersion
|
|
1309
|
+
appVersion,
|
|
1310
|
+
captureSensitiveValues
|
|
1301
1311
|
};
|
|
1302
1312
|
}
|
|
1303
1313
|
var ContextRecorder = class {
|
|
@@ -1316,7 +1326,7 @@ var ContextRecorder = class {
|
|
|
1316
1326
|
}
|
|
1317
1327
|
record(reason) {
|
|
1318
1328
|
guard(this.sink, this.name, () => {
|
|
1319
|
-
const data = collectContext(this.identity(), reason, this.sink.config.appVersion);
|
|
1329
|
+
const data = collectContext(this.identity(), reason, this.sink.config.appVersion, this.sink.config.privacy.captureSensitiveValues);
|
|
1320
1330
|
data.url = this.sink.sanitizer.sanitizeUrl(data.url);
|
|
1321
1331
|
if (data.referrer) data.referrer = this.sink.sanitizer.sanitizeUrl(data.referrer);
|
|
1322
1332
|
this.sink.emit("context", data, { priority: "normal" });
|
|
@@ -1870,9 +1880,10 @@ var RRWebRecorder = class {
|
|
|
1870
1880
|
emit: (event) => this.onEvent(event),
|
|
1871
1881
|
blockSelector: block || void 0,
|
|
1872
1882
|
maskTextSelector: maskText || void 0,
|
|
1873
|
-
maskAllInputs: privacy.maskSensitiveInputs,
|
|
1874
|
-
maskInputOptions: privacy.maskSensitiveInputs ? { password: true, email: true, tel: true, text: true, textarea: true, search: true, url: true, number: true, select: true, date: true, "datetime-local": true, month: true, time: true, week: true } : { password: true },
|
|
1883
|
+
maskAllInputs: privacy.maskSensitiveInputs && !privacy.captureSensitiveValues,
|
|
1884
|
+
maskInputOptions: privacy.captureSensitiveValues ? { password: false } : privacy.maskSensitiveInputs ? { password: true, email: true, tel: true, text: true, textarea: true, search: true, url: true, number: true, select: true, date: true, "datetime-local": true, month: true, time: true, week: true } : { password: true },
|
|
1875
1885
|
maskInputFn: (text, el) => {
|
|
1886
|
+
if (privacy.captureSensitiveValues) return text;
|
|
1876
1887
|
if (el?.type === "password") return "*".repeat(text.length);
|
|
1877
1888
|
if (allowed && safe(() => el.matches(allowed))) return text;
|
|
1878
1889
|
return "*".repeat(text.length);
|
|
@@ -2036,6 +2047,9 @@ var SessionManager = class {
|
|
|
2036
2047
|
get currentToken() {
|
|
2037
2048
|
return this.token;
|
|
2038
2049
|
}
|
|
2050
|
+
hasTokenAvailable() {
|
|
2051
|
+
return Boolean(this.token || readQueryToken());
|
|
2052
|
+
}
|
|
2039
2053
|
// ---- persistência -------------------------------------------------------
|
|
2040
2054
|
loadPersisted() {
|
|
2041
2055
|
const raw = safe(() => sessionStorage.getItem(this.storageKey));
|
|
@@ -2104,9 +2118,11 @@ var SessionManager = class {
|
|
|
2104
2118
|
// ---- ciclo de vida ------------------------------------------------------
|
|
2105
2119
|
/** Cria (ou obtém, de forma idempotente) a sessão no backend. */
|
|
2106
2120
|
async create(opts, context) {
|
|
2107
|
-
|
|
2121
|
+
const queryToken = consumeQueryToken();
|
|
2122
|
+
const sessionToken = opts?.sessionToken || this.token || queryToken;
|
|
2123
|
+
if (!sessionToken || !opts.executionId || !opts.testCaseId)
|
|
2108
2124
|
throw new AuthError("sessionToken, executionId e testCaseId s\xE3o obrigat\xF3rios");
|
|
2109
|
-
this.token =
|
|
2125
|
+
this.token = sessionToken;
|
|
2110
2126
|
const tabId = await resolveTabId(this.cfg.storagePrefix, this.loadPersisted()?.tabId);
|
|
2111
2127
|
const res = await this.api.createSession({
|
|
2112
2128
|
platformId: this.cfg.platformId,
|
|
@@ -2204,6 +2220,27 @@ var SessionManager = class {
|
|
|
2204
2220
|
this.seq = 0;
|
|
2205
2221
|
}
|
|
2206
2222
|
};
|
|
2223
|
+
function consumeQueryToken() {
|
|
2224
|
+
if (typeof window === "undefined") return "";
|
|
2225
|
+
try {
|
|
2226
|
+
const token = readQueryToken();
|
|
2227
|
+
if (!token) return "";
|
|
2228
|
+
const url = new URL(window.location.href);
|
|
2229
|
+
url.searchParams.delete("offscript_token");
|
|
2230
|
+
window.history.replaceState(window.history.state, typeof document === "undefined" ? "" : document.title, `${url.pathname}${url.search}${url.hash}`);
|
|
2231
|
+
return token;
|
|
2232
|
+
} catch {
|
|
2233
|
+
return "";
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
function readQueryToken() {
|
|
2237
|
+
if (typeof window === "undefined") return "";
|
|
2238
|
+
try {
|
|
2239
|
+
return new URL(window.location.href).searchParams.get("offscript_token")?.trim() ?? "";
|
|
2240
|
+
} catch {
|
|
2241
|
+
return "";
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2207
2244
|
var heldLocks = [];
|
|
2208
2245
|
async function resolveTabId(prefix, previous) {
|
|
2209
2246
|
const candidate = previous ?? uid("tab_");
|
|
@@ -2262,10 +2299,628 @@ function resolveConfig(input) {
|
|
|
2262
2299
|
autoResume: input.autoResume ?? true,
|
|
2263
2300
|
heartbeatIntervalMs: input.heartbeatIntervalMs ?? 3e4,
|
|
2264
2301
|
storagePrefix: input.storagePrefix ?? "offscript",
|
|
2265
|
-
debug: input.debug ?? false
|
|
2302
|
+
debug: input.debug ?? false,
|
|
2303
|
+
controls: input.controls ?? true
|
|
2266
2304
|
};
|
|
2267
2305
|
}
|
|
2268
2306
|
|
|
2307
|
+
// src/ui/RecordingControls.ts
|
|
2308
|
+
var HOST_ATTR = "data-offscript-console";
|
|
2309
|
+
var EYE_SEQUENCE = [
|
|
2310
|
+
{ pose: "open", duration: 2600 },
|
|
2311
|
+
{ pose: "blink", duration: 160 },
|
|
2312
|
+
{ pose: "open", duration: 1800 },
|
|
2313
|
+
{ pose: "left", duration: 700 },
|
|
2314
|
+
{ pose: "open", duration: 1500 },
|
|
2315
|
+
{ pose: "wink", duration: 480 },
|
|
2316
|
+
{ pose: "open", duration: 1900 },
|
|
2317
|
+
{ pose: "right", duration: 700 },
|
|
2318
|
+
{ pose: "open", duration: 1600 },
|
|
2319
|
+
{ pose: "up", duration: 640 },
|
|
2320
|
+
{ pose: "happy", duration: 720 }
|
|
2321
|
+
];
|
|
2322
|
+
var EYE_PATHS = {
|
|
2323
|
+
open: ["M59.5 79.5v15", "M93 79.5v15"],
|
|
2324
|
+
blink: ["M52 89q7.5 5 15 0", "M85.5 89q7.5 5 15 0"],
|
|
2325
|
+
wink: ["M52 89q7.5 5 15 0", "M93 79.5v15"],
|
|
2326
|
+
happy: ["M52 91q7.5-12 15 0", "M85.5 91q7.5-12 15 0"]
|
|
2327
|
+
};
|
|
2328
|
+
var RecordingControls = class {
|
|
2329
|
+
constructor(sdk) {
|
|
2330
|
+
this.sdk = sdk;
|
|
2331
|
+
this.eyeTimer = 0;
|
|
2332
|
+
this.placeFrame = 0;
|
|
2333
|
+
this.eyeIndex = 0;
|
|
2334
|
+
this.busy = false;
|
|
2335
|
+
this.expanded = true;
|
|
2336
|
+
this.privacyOpen = true;
|
|
2337
|
+
this.failure = "";
|
|
2338
|
+
this.onVisibility = () => {
|
|
2339
|
+
window.clearTimeout(this.eyeTimer);
|
|
2340
|
+
if (!document.hidden) this.scheduleEye();
|
|
2341
|
+
};
|
|
2342
|
+
this.onViewport = () => {
|
|
2343
|
+
this.placeConfirm();
|
|
2344
|
+
this.schedulePlacePrivacy();
|
|
2345
|
+
};
|
|
2346
|
+
}
|
|
2347
|
+
mount() {
|
|
2348
|
+
if (this.host || typeof document === "undefined") return;
|
|
2349
|
+
const host = document.createElement("div");
|
|
2350
|
+
host.setAttribute(HOST_ATTR, "");
|
|
2351
|
+
host.setAttribute("data-offscript-ignore", "");
|
|
2352
|
+
host.className = "offscript-ignore";
|
|
2353
|
+
host.style.cssText = "position:fixed;left:0;top:0;width:0;height:0;margin:0;padding:0;border:0;overflow:visible;z-index:2147483646;pointer-events:none;";
|
|
2354
|
+
const shadow = host.attachShadow({ mode: "closed" });
|
|
2355
|
+
shadow.innerHTML = TEMPLATE;
|
|
2356
|
+
this.bind(shadow);
|
|
2357
|
+
this.host = host;
|
|
2358
|
+
this.shadow = shadow;
|
|
2359
|
+
this.note = shadow.querySelector("[data-note]") ?? void 0;
|
|
2360
|
+
this.place();
|
|
2361
|
+
this.watchDom();
|
|
2362
|
+
this.unsubscribe = this.sdk.on("statusChange", () => this.render());
|
|
2363
|
+
window.addEventListener("resize", this.onViewport);
|
|
2364
|
+
window.visualViewport?.addEventListener("resize", this.onViewport);
|
|
2365
|
+
document.fonts?.ready.then(() => {
|
|
2366
|
+
if (this.host) this.onViewport();
|
|
2367
|
+
});
|
|
2368
|
+
this.watchDock();
|
|
2369
|
+
this.render();
|
|
2370
|
+
this.startEyes();
|
|
2371
|
+
}
|
|
2372
|
+
unmount() {
|
|
2373
|
+
this.unsubscribe?.();
|
|
2374
|
+
this.unsubscribe = void 0;
|
|
2375
|
+
this.observer?.disconnect();
|
|
2376
|
+
this.observer = void 0;
|
|
2377
|
+
this.dockWatch?.disconnect();
|
|
2378
|
+
this.dockWatch = void 0;
|
|
2379
|
+
window.cancelAnimationFrame(this.placeFrame);
|
|
2380
|
+
this.placeFrame = 0;
|
|
2381
|
+
window.clearTimeout(this.eyeTimer);
|
|
2382
|
+
window.removeEventListener("resize", this.onViewport);
|
|
2383
|
+
window.visualViewport?.removeEventListener("resize", this.onViewport);
|
|
2384
|
+
document.removeEventListener("visibilitychange", this.onVisibility);
|
|
2385
|
+
this.host?.remove();
|
|
2386
|
+
this.host = void 0;
|
|
2387
|
+
this.shadow = void 0;
|
|
2388
|
+
}
|
|
2389
|
+
place() {
|
|
2390
|
+
const host = this.host;
|
|
2391
|
+
const parent = document.body ?? document.documentElement;
|
|
2392
|
+
if (!host || !parent || host.parentNode === parent) return;
|
|
2393
|
+
parent.appendChild(host);
|
|
2394
|
+
}
|
|
2395
|
+
watchDom() {
|
|
2396
|
+
if (typeof MutationObserver === "undefined") return;
|
|
2397
|
+
this.observer?.disconnect();
|
|
2398
|
+
this.observer = new MutationObserver(() => {
|
|
2399
|
+
this.place();
|
|
2400
|
+
if (document.body) this.observer?.observe(document.body, { childList: true });
|
|
2401
|
+
});
|
|
2402
|
+
this.observer.observe(document.documentElement, { childList: true });
|
|
2403
|
+
if (document.body) this.observer.observe(document.body, { childList: true });
|
|
2404
|
+
}
|
|
2405
|
+
watchDock() {
|
|
2406
|
+
const dock = this.shadow?.querySelector(".dock");
|
|
2407
|
+
if (!(dock instanceof HTMLElement) || typeof ResizeObserver === "undefined") return;
|
|
2408
|
+
this.dockWatch?.disconnect();
|
|
2409
|
+
this.dockWatch = new ResizeObserver(() => this.placePrivacy());
|
|
2410
|
+
this.dockWatch.observe(dock);
|
|
2411
|
+
}
|
|
2412
|
+
bind(shadow) {
|
|
2413
|
+
const stop = (event) => event.stopPropagation();
|
|
2414
|
+
shadow.addEventListener("click", (event) => {
|
|
2415
|
+
const actionNode = event.composedPath().find((node) => node instanceof Element && node.hasAttribute("data-action"));
|
|
2416
|
+
const action = actionNode instanceof Element ? actionNode.getAttribute("data-action") : null;
|
|
2417
|
+
if (action === "open") {
|
|
2418
|
+
this.expanded = true;
|
|
2419
|
+
this.render();
|
|
2420
|
+
} else if (action === "close") {
|
|
2421
|
+
this.expanded = false;
|
|
2422
|
+
this.render();
|
|
2423
|
+
} else if (action === "record") void this.run(() => this.sdk.startArmedRecording());
|
|
2424
|
+
else if (action === "pause") void this.run(() => this.sdk.pauseRecording());
|
|
2425
|
+
else if (action === "resume") void this.run(() => this.sdk.resumeRecording());
|
|
2426
|
+
else if (action === "stop") void this.run(() => this.sdk.stopRecording());
|
|
2427
|
+
else if (action === "new") this.openConfirm();
|
|
2428
|
+
else if (action === "cancel") this.closeConfirm();
|
|
2429
|
+
else if (action === "confirm") {
|
|
2430
|
+
this.closeConfirm();
|
|
2431
|
+
void this.run(() => this.sdk.startNewSession());
|
|
2432
|
+
} else if (action === "privacy-show") {
|
|
2433
|
+
this.privacyOpen = true;
|
|
2434
|
+
this.render();
|
|
2435
|
+
} else if (action === "privacy-hide") {
|
|
2436
|
+
this.privacyOpen = false;
|
|
2437
|
+
this.render();
|
|
2438
|
+
}
|
|
2439
|
+
}, true);
|
|
2440
|
+
for (const type of ["click", "dblclick", "auxclick", "contextmenu", "pointerdown", "pointerup", "mousedown", "mouseup", "keydown", "keyup", "keypress", "input", "change", "mouseover", "mouseout"]) {
|
|
2441
|
+
shadow.addEventListener(type, stop);
|
|
2442
|
+
}
|
|
2443
|
+
shadow.querySelector("[data-confirm]")?.addEventListener("keydown", (event) => {
|
|
2444
|
+
if (event instanceof KeyboardEvent && event.key === "Escape") this.closeConfirm();
|
|
2445
|
+
});
|
|
2446
|
+
shadow.querySelector("[data-privacy]")?.addEventListener("keydown", (event) => {
|
|
2447
|
+
if (event instanceof KeyboardEvent && event.key === "Escape") {
|
|
2448
|
+
this.privacyOpen = false;
|
|
2449
|
+
this.render();
|
|
2450
|
+
}
|
|
2451
|
+
});
|
|
2452
|
+
}
|
|
2453
|
+
openConfirm() {
|
|
2454
|
+
const bubble = this.shadow?.querySelector("[data-confirm]");
|
|
2455
|
+
if (!(bubble instanceof HTMLElement) || this.busy || !bubble.hidden) return;
|
|
2456
|
+
bubble.hidden = false;
|
|
2457
|
+
this.placeConfirm();
|
|
2458
|
+
this.paintPrivacy();
|
|
2459
|
+
const cancel = bubble.querySelector("[data-action='cancel']");
|
|
2460
|
+
if (cancel instanceof HTMLElement) cancel.focus();
|
|
2461
|
+
}
|
|
2462
|
+
placeConfirm() {
|
|
2463
|
+
const bubble = this.shadow?.querySelector("[data-confirm]");
|
|
2464
|
+
const card = bubble?.querySelector(".bubble");
|
|
2465
|
+
const button = this.shadow?.querySelector("[data-action='new']");
|
|
2466
|
+
if (!(bubble instanceof HTMLElement) || !(card instanceof HTMLElement) || !(button instanceof HTMLElement)) return;
|
|
2467
|
+
const buttonBox = button.getBoundingClientRect();
|
|
2468
|
+
const buttonCenter = buttonBox.left + buttonBox.width / 2;
|
|
2469
|
+
const tailHalf = 4.5;
|
|
2470
|
+
const margin = 12;
|
|
2471
|
+
const viewport = document.documentElement.clientWidth;
|
|
2472
|
+
let rightEdge = buttonCenter + 22 + tailHalf;
|
|
2473
|
+
rightEdge = Math.min(viewport - margin, Math.max(margin + card.offsetWidth, rightEdge));
|
|
2474
|
+
bubble.style.right = `${Math.round(viewport - rightEdge)}px`;
|
|
2475
|
+
const tail = Math.min(Math.max(12, rightEdge - buttonCenter - tailHalf), Math.max(12, card.offsetWidth - 18));
|
|
2476
|
+
card.style.setProperty("--tail", `${Math.round(tail)}px`);
|
|
2477
|
+
}
|
|
2478
|
+
closeConfirm() {
|
|
2479
|
+
const bubble = this.shadow?.querySelector("[data-confirm]");
|
|
2480
|
+
if (bubble instanceof HTMLElement) bubble.hidden = true;
|
|
2481
|
+
this.paintPrivacy();
|
|
2482
|
+
}
|
|
2483
|
+
async run(task) {
|
|
2484
|
+
if (this.busy) return;
|
|
2485
|
+
this.busy = true;
|
|
2486
|
+
this.failure = "";
|
|
2487
|
+
this.render();
|
|
2488
|
+
try {
|
|
2489
|
+
await task();
|
|
2490
|
+
} catch {
|
|
2491
|
+
this.failure = "N\xE3o foi poss\xEDvel concluir";
|
|
2492
|
+
} finally {
|
|
2493
|
+
this.busy = false;
|
|
2494
|
+
this.render();
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
render() {
|
|
2498
|
+
this.place();
|
|
2499
|
+
const status = this.sdk.getState().status;
|
|
2500
|
+
const shell = this.shadow?.querySelector("[data-shell]");
|
|
2501
|
+
shell?.setAttribute("data-open", this.expanded ? "true" : "false");
|
|
2502
|
+
shell?.setAttribute("data-state", status);
|
|
2503
|
+
if (this.note) this.note.textContent = this.failure;
|
|
2504
|
+
const live = status === "recording" || status === "paused";
|
|
2505
|
+
if (!live) this.closeConfirm();
|
|
2506
|
+
this.paintPrivacy();
|
|
2507
|
+
this.setHidden("record", live || status === "initializing" || status === "stopping");
|
|
2508
|
+
this.setHidden("pause", status !== "recording");
|
|
2509
|
+
this.setHidden("resume", status !== "paused");
|
|
2510
|
+
this.setHidden("new", !live);
|
|
2511
|
+
this.setHidden("stop", !live);
|
|
2512
|
+
this.setEnabled("record", !this.busy && !live && status !== "initializing" && status !== "stopping");
|
|
2513
|
+
this.setEnabled("pause", !this.busy && status === "recording");
|
|
2514
|
+
this.setEnabled("resume", !this.busy && status === "paused");
|
|
2515
|
+
this.setEnabled("stop", !this.busy && live);
|
|
2516
|
+
this.setEnabled("new", !this.busy && live);
|
|
2517
|
+
}
|
|
2518
|
+
paintPrivacy() {
|
|
2519
|
+
const flag = this.sdk.capturesSensitiveValues();
|
|
2520
|
+
const confirm = this.shadow?.querySelector("[data-confirm]");
|
|
2521
|
+
const confirmOpen = confirm instanceof HTMLElement && !confirm.hidden;
|
|
2522
|
+
const bubble = this.shadow?.querySelector("[data-privacy]");
|
|
2523
|
+
const show = flag && this.privacyOpen && this.expanded && !confirmOpen;
|
|
2524
|
+
if (bubble instanceof HTMLElement) {
|
|
2525
|
+
bubble.hidden = !show;
|
|
2526
|
+
if (!show) this.clearPrivacyPlace(bubble);
|
|
2527
|
+
}
|
|
2528
|
+
if (show) this.schedulePlacePrivacy();
|
|
2529
|
+
this.setHidden("privacy-show", !flag || this.privacyOpen || !this.expanded);
|
|
2530
|
+
}
|
|
2531
|
+
schedulePlacePrivacy() {
|
|
2532
|
+
this.placePrivacy();
|
|
2533
|
+
window.cancelAnimationFrame(this.placeFrame);
|
|
2534
|
+
this.placeFrame = window.requestAnimationFrame(() => {
|
|
2535
|
+
this.placePrivacy();
|
|
2536
|
+
this.placeFrame = window.requestAnimationFrame(() => {
|
|
2537
|
+
this.placeFrame = 0;
|
|
2538
|
+
this.placePrivacy();
|
|
2539
|
+
});
|
|
2540
|
+
});
|
|
2541
|
+
}
|
|
2542
|
+
clearPrivacyPlace(wrap) {
|
|
2543
|
+
wrap.style.position = "";
|
|
2544
|
+
wrap.style.transform = "";
|
|
2545
|
+
wrap.style.width = "";
|
|
2546
|
+
wrap.style.maxWidth = "";
|
|
2547
|
+
wrap.style.left = "";
|
|
2548
|
+
wrap.style.right = "";
|
|
2549
|
+
wrap.style.top = "";
|
|
2550
|
+
wrap.style.bottom = "";
|
|
2551
|
+
}
|
|
2552
|
+
placePrivacy() {
|
|
2553
|
+
const wrap = this.shadow?.querySelector("[data-privacy]");
|
|
2554
|
+
const card = wrap?.querySelector(".bubble");
|
|
2555
|
+
const face = this.shadow?.querySelector(".face");
|
|
2556
|
+
if (!(wrap instanceof HTMLElement) || !(card instanceof HTMLElement) || !(face instanceof HTMLElement) || wrap.hidden) return;
|
|
2557
|
+
const box = face.getBoundingClientRect();
|
|
2558
|
+
if (box.width < 8 || box.height < 8 || box.top < 1 && box.left < 1) return;
|
|
2559
|
+
const margin = 12;
|
|
2560
|
+
const viewport = document.documentElement.clientWidth;
|
|
2561
|
+
const maxWidth = Math.min(264, viewport - margin * 2);
|
|
2562
|
+
wrap.style.position = "fixed";
|
|
2563
|
+
wrap.style.transform = "none";
|
|
2564
|
+
wrap.style.width = `${maxWidth}px`;
|
|
2565
|
+
wrap.style.maxWidth = `${maxWidth}px`;
|
|
2566
|
+
wrap.style.left = "auto";
|
|
2567
|
+
wrap.style.bottom = "auto";
|
|
2568
|
+
const faceCenter = box.left + box.width / 2;
|
|
2569
|
+
const tailHalf = 5;
|
|
2570
|
+
let rightEdge = faceCenter + 12 + tailHalf;
|
|
2571
|
+
rightEdge = Math.min(viewport - margin, Math.max(margin + maxWidth, rightEdge));
|
|
2572
|
+
wrap.style.right = `${Math.round(viewport - rightEdge)}px`;
|
|
2573
|
+
wrap.style.top = `${Math.round(Math.max(margin, box.top - 10 - wrap.offsetHeight))}px`;
|
|
2574
|
+
const tail = Math.min(Math.max(12, rightEdge - faceCenter - tailHalf), Math.max(12, maxWidth - 18));
|
|
2575
|
+
card.style.setProperty("--tail", `${Math.round(tail)}px`);
|
|
2576
|
+
}
|
|
2577
|
+
setEnabled(action, enabled) {
|
|
2578
|
+
const button = this.shadow?.querySelector(`[data-action='${action}']`);
|
|
2579
|
+
if (button instanceof HTMLButtonElement) button.disabled = !enabled;
|
|
2580
|
+
}
|
|
2581
|
+
setHidden(action, hidden) {
|
|
2582
|
+
const button = this.shadow?.querySelector(`[data-action='${action}']`);
|
|
2583
|
+
if (button instanceof HTMLButtonElement) button.hidden = hidden;
|
|
2584
|
+
}
|
|
2585
|
+
startEyes() {
|
|
2586
|
+
const media = window.matchMedia?.("(prefers-reduced-motion: reduce)");
|
|
2587
|
+
if (!media || media.matches) return;
|
|
2588
|
+
document.addEventListener("visibilitychange", this.onVisibility);
|
|
2589
|
+
this.scheduleEye();
|
|
2590
|
+
}
|
|
2591
|
+
scheduleEye() {
|
|
2592
|
+
const beat = EYE_SEQUENCE[this.eyeIndex % EYE_SEQUENCE.length];
|
|
2593
|
+
this.applyPose(beat.pose);
|
|
2594
|
+
this.eyeIndex = (this.eyeIndex + 1) % EYE_SEQUENCE.length;
|
|
2595
|
+
this.eyeTimer = window.setTimeout(() => this.scheduleEye(), beat.duration);
|
|
2596
|
+
}
|
|
2597
|
+
applyPose(pose) {
|
|
2598
|
+
const [left, right] = EYE_PATHS[pose] ?? EYE_PATHS.open;
|
|
2599
|
+
this.shadow?.querySelectorAll("[data-eye='left']").forEach((node) => node.setAttribute("d", left));
|
|
2600
|
+
this.shadow?.querySelectorAll("[data-eye='right']").forEach((node) => node.setAttribute("d", right));
|
|
2601
|
+
this.shadow?.querySelectorAll("[data-eyes]").forEach((node) => node.setAttribute("data-pose", pose));
|
|
2602
|
+
}
|
|
2603
|
+
};
|
|
2604
|
+
var ICON = {
|
|
2605
|
+
pause: `<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5 3.5v9M11 3.5v9"/></svg>`,
|
|
2606
|
+
play: `<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M6 3.8 12.2 8 6 12.2z" fill="currentColor" stroke="none"/></svg>`,
|
|
2607
|
+
close: `<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4 4l8 8M12 4 4 12"/></svg>`,
|
|
2608
|
+
speak: `<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3.5 3.5h9a1.5 1.5 0 0 1 1.5 1.5v5a1.5 1.5 0 0 1-1.5 1.5H7.2L4.5 13.5v-2H3.5A1.5 1.5 0 0 1 2 10V5a1.5 1.5 0 0 1 1.5-1.5Z"/></svg>`
|
|
2609
|
+
};
|
|
2610
|
+
function mark(gradientId) {
|
|
2611
|
+
return `<svg class="mark" viewBox="14 9 138 126" fill="none" aria-hidden="true">
|
|
2612
|
+
<defs>
|
|
2613
|
+
<linearGradient id="${gradientId}" x1="112" y1="41" x2="143" y2="17" gradientUnits="userSpaceOnUse">
|
|
2614
|
+
<stop offset="0" stop-color="#753eff"></stop>
|
|
2615
|
+
<stop offset="1" stop-color="#1772ff"></stop>
|
|
2616
|
+
</linearGradient>
|
|
2617
|
+
</defs>
|
|
2618
|
+
<path d="M72 23C88 23 101 26 109 32L112 40C122 50 128 64 128 80C128 109 104 131 72 131C41 131 16 108 16 80C16 46 40 23 72 23Z" fill="#eeeef7"></path>
|
|
2619
|
+
<path d="M112 41C119 37 123 27 131 22C135 19 139 18 143 17" stroke="url(#${gradientId})" stroke-width="8" stroke-linecap="round"></path>
|
|
2620
|
+
<g class="eyes" data-eyes data-pose="open" stroke="#10182e" stroke-width="7" stroke-linecap="round">
|
|
2621
|
+
<path data-eye="left" d="M59.5 79.5v15"></path>
|
|
2622
|
+
<path data-eye="right" d="M93 79.5v15"></path>
|
|
2623
|
+
</g>
|
|
2624
|
+
</svg>`;
|
|
2625
|
+
}
|
|
2626
|
+
var TEMPLATE = `
|
|
2627
|
+
<style>
|
|
2628
|
+
:host { color-scheme: dark; }
|
|
2629
|
+
button {
|
|
2630
|
+
display: grid;
|
|
2631
|
+
place-items: center;
|
|
2632
|
+
width: 32px;
|
|
2633
|
+
height: 32px;
|
|
2634
|
+
margin: 0;
|
|
2635
|
+
padding: 0;
|
|
2636
|
+
border: 1px solid #3a4458;
|
|
2637
|
+
border-radius: 999px;
|
|
2638
|
+
background: transparent;
|
|
2639
|
+
color: #f4f3fc;
|
|
2640
|
+
cursor: pointer;
|
|
2641
|
+
}
|
|
2642
|
+
button svg {
|
|
2643
|
+
width: 15px;
|
|
2644
|
+
height: 15px;
|
|
2645
|
+
fill: none;
|
|
2646
|
+
stroke: currentColor;
|
|
2647
|
+
stroke-width: 1.7;
|
|
2648
|
+
stroke-linecap: round;
|
|
2649
|
+
stroke-linejoin: round;
|
|
2650
|
+
}
|
|
2651
|
+
button:hover:not(:disabled) { border-color: #a596ff; color: #d0c8ff; }
|
|
2652
|
+
button:focus-visible { outline: 2px solid #a596ff; outline-offset: 2px; }
|
|
2653
|
+
button:disabled { cursor: not-allowed; opacity: 0.4; }
|
|
2654
|
+
button[hidden] { display: none; }
|
|
2655
|
+
button.text {
|
|
2656
|
+
width: auto;
|
|
2657
|
+
padding: 0 12px;
|
|
2658
|
+
font: 600 12px/1 "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
|
|
2659
|
+
}
|
|
2660
|
+
button.record {
|
|
2661
|
+
border: 0;
|
|
2662
|
+
background: linear-gradient(100deg, #6847ff, #1673ff);
|
|
2663
|
+
color: #fff;
|
|
2664
|
+
}
|
|
2665
|
+
button.finish {
|
|
2666
|
+
border: 0;
|
|
2667
|
+
background: #e5485f;
|
|
2668
|
+
color: #fff;
|
|
2669
|
+
}
|
|
2670
|
+
button.record:hover:not(:disabled),
|
|
2671
|
+
button.finish:hover:not(:disabled) { color: #fff; filter: brightness(1.08); }
|
|
2672
|
+
button.primary {
|
|
2673
|
+
border: 0;
|
|
2674
|
+
background: linear-gradient(100deg, #6847ff, #1673ff);
|
|
2675
|
+
color: #fff;
|
|
2676
|
+
}
|
|
2677
|
+
button.primary:hover:not(:disabled) { color: #fff; filter: brightness(1.08); }
|
|
2678
|
+
.launcher, .dock {
|
|
2679
|
+
position: fixed;
|
|
2680
|
+
right: 16px;
|
|
2681
|
+
bottom: 16px;
|
|
2682
|
+
z-index: 1;
|
|
2683
|
+
border: 1px solid #232b3b;
|
|
2684
|
+
background: #151b27;
|
|
2685
|
+
box-shadow: 0 18px 40px rgba(3, 5, 12, 0.5);
|
|
2686
|
+
pointer-events: auto;
|
|
2687
|
+
font-family: "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
|
|
2688
|
+
}
|
|
2689
|
+
.launcher {
|
|
2690
|
+
width: 56px;
|
|
2691
|
+
height: 56px;
|
|
2692
|
+
padding: 8px;
|
|
2693
|
+
border-radius: 999px;
|
|
2694
|
+
cursor: pointer;
|
|
2695
|
+
}
|
|
2696
|
+
.launcher .mark { width: 100%; height: 100%; }
|
|
2697
|
+
.launcher::after {
|
|
2698
|
+
content: "";
|
|
2699
|
+
position: absolute;
|
|
2700
|
+
right: 1px;
|
|
2701
|
+
bottom: 1px;
|
|
2702
|
+
width: 10px;
|
|
2703
|
+
height: 10px;
|
|
2704
|
+
border-radius: 999px;
|
|
2705
|
+
background: #929bb1;
|
|
2706
|
+
box-shadow: 0 0 0 2px #151b27;
|
|
2707
|
+
}
|
|
2708
|
+
[data-state="recording"] .launcher::after { background: #1673ff; }
|
|
2709
|
+
[data-state="paused"] .launcher::after { background: #d0c8ff; }
|
|
2710
|
+
[data-state="error"] .launcher::after { background: #e5485f; }
|
|
2711
|
+
.dock {
|
|
2712
|
+
display: flex;
|
|
2713
|
+
align-items: center;
|
|
2714
|
+
gap: 10px;
|
|
2715
|
+
max-width: calc(100vw - 32px);
|
|
2716
|
+
padding: 6px 6px 6px 12px;
|
|
2717
|
+
border-radius: 999px;
|
|
2718
|
+
color: #f4f3fc;
|
|
2719
|
+
overflow: visible;
|
|
2720
|
+
}
|
|
2721
|
+
[data-open="false"] .dock, [data-open="true"] .launcher { display: none; }
|
|
2722
|
+
.brand { display: flex; align-items: center; gap: 8px; min-width: 0; overflow: visible; }
|
|
2723
|
+
.face { position: relative; flex: none; width: 28px; height: 28px; overflow: visible; }
|
|
2724
|
+
.dock .mark { width: 28px; height: 28px; }
|
|
2725
|
+
.word {
|
|
2726
|
+
color: #f4f3fc;
|
|
2727
|
+
font-size: 14px;
|
|
2728
|
+
font-weight: 600;
|
|
2729
|
+
letter-spacing: -0.03em;
|
|
2730
|
+
line-height: 1;
|
|
2731
|
+
}
|
|
2732
|
+
.actions { display: flex; align-items: center; gap: 6px; }
|
|
2733
|
+
.note {
|
|
2734
|
+
position: fixed;
|
|
2735
|
+
right: 16px;
|
|
2736
|
+
bottom: 78px;
|
|
2737
|
+
z-index: 1;
|
|
2738
|
+
margin: 0;
|
|
2739
|
+
padding: 4px 10px;
|
|
2740
|
+
border-radius: 999px;
|
|
2741
|
+
background: #151b27;
|
|
2742
|
+
color: #ff9aa8;
|
|
2743
|
+
font: 600 11px/1.3 "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
|
|
2744
|
+
pointer-events: none;
|
|
2745
|
+
}
|
|
2746
|
+
.note:empty { display: none; }
|
|
2747
|
+
[data-shell]:has(.note:not(:empty)) .privacy { bottom: calc(100% + 48px); }
|
|
2748
|
+
.eyes {
|
|
2749
|
+
transform-box: view-box;
|
|
2750
|
+
transform-origin: 0 0;
|
|
2751
|
+
transition: transform 280ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
2752
|
+
}
|
|
2753
|
+
.eyes[data-pose="left"] { transform: translate(-4.5%, 0.8%); }
|
|
2754
|
+
.eyes[data-pose="right"] { transform: translate(4.5%, 0.8%); }
|
|
2755
|
+
.eyes[data-pose="up"] { transform: translate(0, -3.5%); }
|
|
2756
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2757
|
+
.eyes { transition: none; }
|
|
2758
|
+
.eyes[data-pose] { transform: none; }
|
|
2759
|
+
.speak:not([hidden]) .bubble { animation: none; }
|
|
2760
|
+
}
|
|
2761
|
+
.speak {
|
|
2762
|
+
position: fixed;
|
|
2763
|
+
right: 16px;
|
|
2764
|
+
bottom: 76px;
|
|
2765
|
+
z-index: 2;
|
|
2766
|
+
width: max-content;
|
|
2767
|
+
max-width: min(16rem, calc(100vw - 32px));
|
|
2768
|
+
pointer-events: auto;
|
|
2769
|
+
font-family: "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
|
|
2770
|
+
}
|
|
2771
|
+
.speak[hidden] { display: none; }
|
|
2772
|
+
.bubble {
|
|
2773
|
+
position: relative;
|
|
2774
|
+
padding: 0.6rem 0.7rem 0.55rem;
|
|
2775
|
+
border: 1px solid #3d3590;
|
|
2776
|
+
border-radius: 1rem;
|
|
2777
|
+
background: #1b1a3f;
|
|
2778
|
+
color: #c7cadc;
|
|
2779
|
+
box-shadow: 0 16px 40px rgba(3, 5, 12, 0.45);
|
|
2780
|
+
animation: bubble-in 320ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
2781
|
+
}
|
|
2782
|
+
.bubble::after {
|
|
2783
|
+
content: "";
|
|
2784
|
+
position: absolute;
|
|
2785
|
+
right: var(--tail, 22px);
|
|
2786
|
+
bottom: -5px;
|
|
2787
|
+
width: 9px;
|
|
2788
|
+
height: 9px;
|
|
2789
|
+
border-right: 1px solid #3d3590;
|
|
2790
|
+
border-bottom: 1px solid #3d3590;
|
|
2791
|
+
background: #1b1a3f;
|
|
2792
|
+
transform: rotate(45deg);
|
|
2793
|
+
}
|
|
2794
|
+
.bubble .mark {
|
|
2795
|
+
display: block;
|
|
2796
|
+
width: 22px;
|
|
2797
|
+
height: 22px;
|
|
2798
|
+
margin: 0 0 0.35rem;
|
|
2799
|
+
}
|
|
2800
|
+
@keyframes bubble-in {
|
|
2801
|
+
from { opacity: 0; transform: translateY(8px) scale(0.96); }
|
|
2802
|
+
to { opacity: 1; transform: none; }
|
|
2803
|
+
}
|
|
2804
|
+
.bubble p {
|
|
2805
|
+
margin: 0;
|
|
2806
|
+
color: #c7cadc;
|
|
2807
|
+
font-size: 0.8125rem;
|
|
2808
|
+
font-weight: 400;
|
|
2809
|
+
line-height: 1.45;
|
|
2810
|
+
}
|
|
2811
|
+
.privacy {
|
|
2812
|
+
position: absolute;
|
|
2813
|
+
left: auto;
|
|
2814
|
+
right: 50%;
|
|
2815
|
+
bottom: calc(100% + 18px);
|
|
2816
|
+
z-index: 3;
|
|
2817
|
+
width: min(16.5rem, calc(100vw - 24px));
|
|
2818
|
+
max-width: min(16.5rem, calc(100vw - 24px));
|
|
2819
|
+
pointer-events: auto;
|
|
2820
|
+
font-family: "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
|
|
2821
|
+
transform: translateX(20px);
|
|
2822
|
+
}
|
|
2823
|
+
.privacy[hidden] { display: none; }
|
|
2824
|
+
.privacy .bubble {
|
|
2825
|
+
padding: 0.75rem 2.15rem 0.75rem 0.9rem;
|
|
2826
|
+
border-radius: 16px;
|
|
2827
|
+
}
|
|
2828
|
+
.privacy .bubble::after {
|
|
2829
|
+
right: var(--tail, 12px);
|
|
2830
|
+
bottom: -5px;
|
|
2831
|
+
width: 10px;
|
|
2832
|
+
height: 10px;
|
|
2833
|
+
}
|
|
2834
|
+
@media (max-width: 560px) {
|
|
2835
|
+
.word { display: none; }
|
|
2836
|
+
.privacy {
|
|
2837
|
+
position: fixed;
|
|
2838
|
+
left: 12px;
|
|
2839
|
+
right: 12px;
|
|
2840
|
+
bottom: 76px;
|
|
2841
|
+
width: auto;
|
|
2842
|
+
max-width: none;
|
|
2843
|
+
transform: none;
|
|
2844
|
+
}
|
|
2845
|
+
.privacy .bubble {
|
|
2846
|
+
padding: 0.65rem 1.85rem 0.6rem 0.75rem;
|
|
2847
|
+
}
|
|
2848
|
+
.privacy .bubble p { font-size: 0.75rem; }
|
|
2849
|
+
}
|
|
2850
|
+
.privacy button.hide {
|
|
2851
|
+
position: absolute;
|
|
2852
|
+
top: 0.4rem;
|
|
2853
|
+
right: 0.4rem;
|
|
2854
|
+
width: 24px;
|
|
2855
|
+
height: 24px;
|
|
2856
|
+
border: 0;
|
|
2857
|
+
background: transparent;
|
|
2858
|
+
color: #c7cadc;
|
|
2859
|
+
}
|
|
2860
|
+
.privacy button.hide:hover:not(:disabled) { color: #f4f3fc; }
|
|
2861
|
+
.choices { display: flex; gap: 6px; margin-top: 0.7rem; }
|
|
2862
|
+
.speak button.choice {
|
|
2863
|
+
width: auto;
|
|
2864
|
+
height: auto;
|
|
2865
|
+
min-height: 1.75rem;
|
|
2866
|
+
padding: 0 0.7rem;
|
|
2867
|
+
border-radius: 999px;
|
|
2868
|
+
font: 600 0.75rem/1 "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
|
|
2869
|
+
}
|
|
2870
|
+
.speak button.secondary {
|
|
2871
|
+
border: 1px solid #3a4458;
|
|
2872
|
+
background: transparent;
|
|
2873
|
+
color: #f4f3fc;
|
|
2874
|
+
}
|
|
2875
|
+
.speak button.secondary:hover:not(:disabled) { border-color: #a596ff; color: #d0c8ff; }
|
|
2876
|
+
.speak button.danger {
|
|
2877
|
+
border: 0;
|
|
2878
|
+
background: #e5485f;
|
|
2879
|
+
color: #fff;
|
|
2880
|
+
}
|
|
2881
|
+
.speak button.danger:hover:not(:disabled) { color: #fff; background: color-mix(in srgb, #e5485f 88%, #fff); }
|
|
2882
|
+
</style>
|
|
2883
|
+
<div data-shell data-open="true" data-state="recording">
|
|
2884
|
+
<button type="button" class="launcher" data-action="open" aria-label="Abrir controles da grava\xE7\xE3o">
|
|
2885
|
+
${mark("offscript-trail-launcher")}
|
|
2886
|
+
</button>
|
|
2887
|
+
<div class="dock" role="toolbar" aria-label="Controles da grava\xE7\xE3o">
|
|
2888
|
+
<div class="brand">
|
|
2889
|
+
<span class="face">
|
|
2890
|
+
${mark("offscript-trail-dock")}
|
|
2891
|
+
<div class="privacy" data-privacy hidden>
|
|
2892
|
+
<div class="bubble" role="status">
|
|
2893
|
+
<button type="button" class="hide" data-action="privacy-hide" aria-label="Fechar aviso">${ICON.close}</button>
|
|
2894
|
+
<p>Tudo vis\xEDvel na tela ser\xE1 gravado, inclusive o que for digitado \u2014 senha e dado sens\xEDvel tamb\xE9m.</p>
|
|
2895
|
+
</div>
|
|
2896
|
+
</div>
|
|
2897
|
+
</span>
|
|
2898
|
+
<span class="word">Offscript</span>
|
|
2899
|
+
</div>
|
|
2900
|
+
<div class="actions">
|
|
2901
|
+
<button type="button" data-action="privacy-show" aria-label="Abrir aviso" title="Tudo vis\xEDvel na tela ser\xE1 gravado, inclusive o que for digitado" hidden>${ICON.speak}</button>
|
|
2902
|
+
<button type="button" class="text record" data-action="record">Iniciar</button>
|
|
2903
|
+
<button type="button" data-action="pause" aria-label="Pausar" title="Pausar">${ICON.pause}</button>
|
|
2904
|
+
<button type="button" data-action="resume" aria-label="Retomar" title="Retomar">${ICON.play}</button>
|
|
2905
|
+
<button type="button" class="text" data-action="new" title="Descarta o que foi gravado e volta ao in\xEDcio">Nova sess\xE3o</button>
|
|
2906
|
+
<button type="button" class="text finish" data-action="stop" title="Encerra a coleta e envia o que foi gravado">Finalizar</button>
|
|
2907
|
+
<button type="button" data-action="close" aria-label="Fechar" title="Fechar">${ICON.close}</button>
|
|
2908
|
+
</div>
|
|
2909
|
+
</div>
|
|
2910
|
+
<div class="speak" data-confirm hidden>
|
|
2911
|
+
<div class="bubble" role="dialog" aria-labelledby="offscript-confirm-title">
|
|
2912
|
+
${mark("offscript-trail-speak")}
|
|
2913
|
+
<p id="offscript-confirm-title">Descartar esta sess\xE3o? O que foi gravado ser\xE1 ignorado.</p>
|
|
2914
|
+
<div class="choices">
|
|
2915
|
+
<button type="button" class="choice secondary" data-action="cancel">Cancelar</button>
|
|
2916
|
+
<button type="button" class="choice danger" data-action="confirm">Descartar</button>
|
|
2917
|
+
</div>
|
|
2918
|
+
</div>
|
|
2919
|
+
</div>
|
|
2920
|
+
<p class="note" data-note></p>
|
|
2921
|
+
</div>
|
|
2922
|
+
`;
|
|
2923
|
+
|
|
2269
2924
|
// src/core/OffscriptSDK.ts
|
|
2270
2925
|
var MAX_LOG = 5e3;
|
|
2271
2926
|
var Offscript = class {
|
|
@@ -2288,6 +2943,9 @@ var Offscript = class {
|
|
|
2288
2943
|
getState() {
|
|
2289
2944
|
return { status: this.status, session: this.sessions.session, pending: this.transport?.pending ?? 0, lastError: this.lastError };
|
|
2290
2945
|
}
|
|
2946
|
+
capturesSensitiveValues() {
|
|
2947
|
+
return this.config.privacy.captureSensitiveValues === true;
|
|
2948
|
+
}
|
|
2291
2949
|
getMetrics() {
|
|
2292
2950
|
return { emitted: this.metrics.emitted, dropped: this.metrics.dropped, avgEmitMs: this.metrics.emitted ? this.metrics.emitMs / this.metrics.emitted : 0, bufferKind: this.transport?.bufferKind };
|
|
2293
2951
|
}
|
|
@@ -2299,6 +2957,7 @@ var Offscript = class {
|
|
|
2299
2957
|
}
|
|
2300
2958
|
async doInit() {
|
|
2301
2959
|
if (typeof window === "undefined") throw new OffscriptError("NOT_SUPPORTED", "O SDK requer um ambiente de navegador");
|
|
2960
|
+
this.ensureControls();
|
|
2302
2961
|
this.setStatus("initializing");
|
|
2303
2962
|
try {
|
|
2304
2963
|
const buffer = await openBuffer(`${this.config.storagePrefix}-${this.config.platformId}`, this.config.transport.persist);
|
|
@@ -2331,9 +2990,10 @@ var Offscript = class {
|
|
|
2331
2990
|
throw new InvalidStateError(`J\xE1 existe grava\xE7\xE3o ativa para a execu\xE7\xE3o ${cur.executionId}. Encerre-a antes.`);
|
|
2332
2991
|
}
|
|
2333
2992
|
if (this.status === "stopping") throw new InvalidStateError("Grava\xE7\xE3o em encerramento");
|
|
2993
|
+
this.lastStart = { testCaseId: opts.testCaseId, executionId: opts.executionId, sessionToken: opts.sessionToken };
|
|
2334
2994
|
this.setStatus("initializing");
|
|
2335
2995
|
try {
|
|
2336
|
-
const ctx = collectContext({ platformId: this.config.platformId, testCaseId: opts.testCaseId, executionId: opts.executionId }, "initial", this.config.appVersion);
|
|
2996
|
+
const ctx = collectContext({ platformId: this.config.platformId, testCaseId: opts.testCaseId, executionId: opts.executionId }, "initial", this.config.appVersion, this.config.privacy.captureSensitiveValues);
|
|
2337
2997
|
await this.sessions.create(opts, { ...ctx, url: this.sanitizer.sanitizeUrl(ctx.url), referrer: void 0, title: void 0 });
|
|
2338
2998
|
const s = this.sessions.session;
|
|
2339
2999
|
await this.transport.start({ sessionId: s.sessionId, executionId: s.executionId, tabId: s.tabId });
|
|
@@ -2368,6 +3028,11 @@ var Offscript = class {
|
|
|
2368
3028
|
if (state.status === "paused") this.recorders.forEach((r) => r.pause?.());
|
|
2369
3029
|
this.actions?.recordPageLoad(safe(() => performance.getEntriesByType("navigation")[0]?.type), document.referrer);
|
|
2370
3030
|
this.sessions.startHeartbeat((r) => this.onHeartbeat(r.status), (e) => void this.onFatal(e), () => this.transport?.pending ?? 0, () => this.status);
|
|
3031
|
+
const token = this.sessions.currentToken;
|
|
3032
|
+
if (token) {
|
|
3033
|
+
this.lastStart = { testCaseId: s.testCaseId, executionId: s.executionId, sessionToken: token };
|
|
3034
|
+
this.armed ?? (this.armed = { testCaseId: s.testCaseId, sessionToken: token });
|
|
3035
|
+
}
|
|
2371
3036
|
this.bus.emit("sessionStart", s);
|
|
2372
3037
|
} catch (e) {
|
|
2373
3038
|
await this.teardown();
|
|
@@ -2377,6 +3042,38 @@ var Offscript = class {
|
|
|
2377
3042
|
if (!(e instanceof SessionExpiredError || e instanceof SessionDisabledError || e instanceof AuthError)) throw e;
|
|
2378
3043
|
}
|
|
2379
3044
|
}
|
|
3045
|
+
/** Guarda caso de teste e token para o botão de gravar, sem abrir a sessão. */
|
|
3046
|
+
armRecording(opts) {
|
|
3047
|
+
this.armed = { testCaseId: opts.testCaseId, sessionToken: opts.sessionToken };
|
|
3048
|
+
}
|
|
3049
|
+
/** Abre a coleta com os dados armados, ou com os da última sessão. */
|
|
3050
|
+
startArmedRecording() {
|
|
3051
|
+
if ((this.status === "recording" || this.status === "paused") && this.sessions.session) return Promise.resolve(this.sessions.session);
|
|
3052
|
+
return this.startRecording(this.freshStart());
|
|
3053
|
+
}
|
|
3054
|
+
/** Descarta a coleta atual e volta ao estado inicial, sem abrir outra. */
|
|
3055
|
+
async startNewSession() {
|
|
3056
|
+
if (this.status !== "recording" && this.status !== "paused") return;
|
|
3057
|
+
await this.discardRecording();
|
|
3058
|
+
}
|
|
3059
|
+
freshStart() {
|
|
3060
|
+
const base = this.armed ?? (this.lastStart ? { testCaseId: this.lastStart.testCaseId, sessionToken: this.lastStart.sessionToken } : void 0);
|
|
3061
|
+
if (!base?.testCaseId || !base.sessionToken) throw new InvalidStateError("N\xE3o h\xE1 dados para iniciar a grava\xE7\xE3o");
|
|
3062
|
+
return { testCaseId: base.testCaseId, sessionToken: base.sessionToken, executionId: uid("exec_") };
|
|
3063
|
+
}
|
|
3064
|
+
async discardRecording() {
|
|
3065
|
+
const sessionId = this.sessions.session?.sessionId ?? "";
|
|
3066
|
+
this.accepting = false;
|
|
3067
|
+
this.stopRecorders();
|
|
3068
|
+
this.sessions.stopHeartbeat();
|
|
3069
|
+
this.transport?.stop();
|
|
3070
|
+
if (sessionId) await this.transport?.clearSession(sessionId);
|
|
3071
|
+
this.lifecycle?.uninstall();
|
|
3072
|
+
this.sessions.clearPersisted();
|
|
3073
|
+
this.sessions.reset();
|
|
3074
|
+
this.setStatus("idle");
|
|
3075
|
+
if (sessionId) this.bus.emit("sessionDiscard", { sessionId });
|
|
3076
|
+
}
|
|
2380
3077
|
async pauseRecording() {
|
|
2381
3078
|
if (this.status === "paused") return;
|
|
2382
3079
|
if (this.status !== "recording") throw new InvalidStateError(`N\xE3o \xE9 poss\xEDvel pausar no estado ${this.status}`);
|
|
@@ -2453,6 +3150,8 @@ var Offscript = class {
|
|
|
2453
3150
|
}
|
|
2454
3151
|
this.destroyed = true;
|
|
2455
3152
|
await this.teardown();
|
|
3153
|
+
this.controls?.unmount();
|
|
3154
|
+
this.controls = void 0;
|
|
2456
3155
|
this.sessions.reset();
|
|
2457
3156
|
this.transport?.close();
|
|
2458
3157
|
this.bus.clear();
|
|
@@ -2578,6 +3277,7 @@ var Offscript = class {
|
|
|
2578
3277
|
this.transport?.setOnline(typeof navigator === "undefined" ? true : navigator.onLine !== false);
|
|
2579
3278
|
}
|
|
2580
3279
|
async startRecorders() {
|
|
3280
|
+
this.ensureControls();
|
|
2581
3281
|
const r = this.config.recording;
|
|
2582
3282
|
const list = [];
|
|
2583
3283
|
if (r.rrweb) list.push(this.rrweb = new RRWebRecorder(this));
|
|
@@ -2600,6 +3300,13 @@ var Offscript = class {
|
|
|
2600
3300
|
for (const r of this.recorders.splice(0).reverse()) safe(() => r.stop());
|
|
2601
3301
|
this.rrweb = this.actions = this.context = void 0;
|
|
2602
3302
|
}
|
|
3303
|
+
ensureControls() {
|
|
3304
|
+
if (!this.config.controls || typeof document === "undefined" || !this.sessions.hasTokenAvailable()) return;
|
|
3305
|
+
safe(() => {
|
|
3306
|
+
this.controls ?? (this.controls = new RecordingControls(this));
|
|
3307
|
+
this.controls.mount();
|
|
3308
|
+
});
|
|
3309
|
+
}
|
|
2603
3310
|
async teardown() {
|
|
2604
3311
|
this.accepting = false;
|
|
2605
3312
|
this.stopRecorders();
|
|
@@ -2654,5 +3361,5 @@ var Offscript = class {
|
|
|
2654
3361
|
Offscript.version = SDK_VERSION;
|
|
2655
3362
|
|
|
2656
3363
|
export { AuthError, ConfigError, DEFAULT_PRIVACY, DEFAULT_RECORDING, DEFAULT_TRANSPORT, DataSanitizer, InvalidStateError, Offscript, OffscriptError, SessionDisabledError, SessionExpiredError, TransportError };
|
|
2657
|
-
//# sourceMappingURL=chunk-
|
|
2658
|
-
//# sourceMappingURL=chunk-
|
|
3364
|
+
//# sourceMappingURL=chunk-IBEPWI7S.js.map
|
|
3365
|
+
//# sourceMappingURL=chunk-IBEPWI7S.js.map
|