quala-widget
Version:
Smart feedback widget for SaaS trial optimization
1,299 lines (1,298 loc) • 167 kB
JavaScript
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
import { useState, useRef, useCallback, useEffect, createElement } from "react";
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
const _ReactRenderer = class _ReactRenderer {
constructor() {
__publicField(this, "createRootFn", null);
__publicField(this, "legacyRender", null);
__publicField(this, "isReady", false);
__publicField(this, "debug", false);
}
static getInstance() {
if (!_ReactRenderer.instance) {
_ReactRenderer.instance = new _ReactRenderer();
}
return _ReactRenderer.instance;
}
setDebug(debug) {
this.debug = debug;
}
log(message, ...args) {
if (this.debug) {
console.log(`[Quala Renderer] ${message}`, ...args);
}
}
async initialize() {
var _a, _b;
if (this.isReady) return;
try {
const ReactDOMClient = require("react-dom/client");
if (ReactDOMClient && ReactDOMClient.createRoot) {
this.createRootFn = ReactDOMClient.createRoot;
this.isReady = true;
this.log("Using direct require for react-dom/client");
return;
}
} catch (e) {
}
try {
const ReactDOMClient = await import("./client-DFJurY5z.js").then((n) => n.c);
this.createRootFn = ReactDOMClient.createRoot || ((_a = ReactDOMClient.default) == null ? void 0 : _a.createRoot);
if (this.createRootFn) {
this.isReady = true;
this.log("Using dynamic import for react-dom/client");
return;
}
} catch (e) {
}
try {
const ReactDOM = require("react-dom");
if (ReactDOM && ReactDOM.render) {
this.legacyRender = ReactDOM;
this.isReady = true;
this.log("Using legacy ReactDOM.render");
return;
}
} catch (e) {
}
try {
const ReactDOM = await import("react-dom");
const render = ReactDOM.render || ((_b = ReactDOM.default) == null ? void 0 : _b.render);
if (render && typeof render === "function") {
this.legacyRender = ReactDOM.default || ReactDOM;
this.isReady = true;
this.log("Using dynamic import for legacy ReactDOM");
return;
}
} catch (e) {
console.error("[Quala] All React DOM loading methods failed");
}
}
createRoot(container) {
if (!this.isReady) {
throw new Error("[Quala] Renderer not ready. Call initialize() first.");
}
if (this.createRootFn) {
return this.createRootFn(container);
} else if (this.legacyRender) {
return {
render: (element) => {
this.legacyRender.render(element, container);
},
unmount: () => {
if (this.legacyRender.unmountComponentAtNode) {
this.legacyRender.unmountComponentAtNode(container);
}
}
};
} else {
throw new Error("[Quala] No React rendering method available");
}
}
isInitialized() {
return this.isReady;
}
async ensureReady() {
if (!this.isReady) {
await this.initialize();
}
}
};
__publicField(_ReactRenderer, "instance");
let ReactRenderer = _ReactRenderer;
const _TechnicalContextCollector = class _TechnicalContextCollector {
constructor(debug = false) {
__publicField(this, "consoleErrors", []);
__publicField(this, "failedRequests", []);
__publicField(this, "userActions", []);
__publicField(this, "pageStartTime", Date.now());
__publicField(this, "debug", false);
__publicField(this, "isCapturing", false);
// ✅ SIMPLIFIED: Just container-level selectors
__publicField(this, "qualaContainerSelectors", [
"[data-quala-widget]",
// Main widget containers
"[data-quala-micro-widget]",
// Micro widget containers
"[data-quala-container]",
// Generic Quala container
".quala-widget",
// CSS class for main widgets
".quala-micro-widget",
// CSS class for micro widgets
".quala-container",
// CSS class for containers
".quala-modal",
// Modal containers
".quala-overlay"
// Overlay/backdrop
]);
this.debug = debug;
}
log(message, ...args) {
if (this.debug) {
console.log(`[Quala Context] ${message}`, ...args);
}
}
/**
* ✅ SIMPLIFIED: Check if element is inside any Quala container
* Just mark top-level containers and ignore everything inside!
*/
isQualaElement(element) {
var _a;
if (!element || !(element instanceof Element)) return false;
let currentElement = element;
while (currentElement) {
if (currentElement.hasAttribute("data-quala-widget") || currentElement.hasAttribute("data-quala-micro-widget") || currentElement.hasAttribute("data-quala-container")) {
this.log("🎯 Quala element detected - inside container:", currentElement.tagName);
return true;
}
if (currentElement.className && typeof currentElement.className === "string") {
const classes = currentElement.className.split(" ");
if (classes.some(
(cls) => cls === "quala-widget" || cls === "quala-micro-widget" || cls === "quala-container" || cls === "quala-modal" || cls === "quala-overlay"
)) {
this.log("🎯 Quala element detected - inside CSS container:", currentElement.className);
return true;
}
}
if (currentElement.tagName === "DIV" || currentElement.tagName === "FORM") {
const elementText = ((_a = currentElement.textContent) == null ? void 0 : _a.toLowerCase()) || "";
if ((elementText.includes("send feedback") || elementText.includes("submit feedback")) && elementText.length < 200) {
this.log("🎯 Quala element detected - content pattern container");
return true;
}
const textarea = currentElement.querySelector('textarea[placeholder*="explain"], textarea[placeholder*="feedback"], textarea[placeholder*="improve"]');
if (textarea) {
this.log("🎯 Quala element detected - feedback form container");
return true;
}
}
currentElement = currentElement.parentElement;
}
return false;
}
/**
* Check if a URL is a Quala API endpoint
*/
isQualaRequest(url) {
try {
const urlObj = new URL(url, window.location.origin);
return urlObj.pathname.includes("/api/widget") || urlObj.hostname.includes("getquala.xyz") || urlObj.pathname.includes("quala");
} catch {
return url.includes("quala") || url.includes("/api/widget");
}
}
initialize() {
if (typeof window === "undefined" || this.isCapturing) return;
this.isCapturing = true;
this.captureConsoleErrors();
this.captureNetworkFailures();
this.captureUserActions();
this.trackPageVisibility();
this.log("Enhanced technical context capture initialized with improved Quala component filtering");
}
captureConsoleErrors() {
const originalError = console.error;
const originalWarn = console.warn;
console.error = (...args) => {
const errorString = args.join(" ").toLowerCase();
if (!errorString.includes("[quala") && !errorString.includes("quala sdk")) {
this.addConsoleError("error", args);
}
originalError.apply(console, args);
};
console.warn = (...args) => {
const warnString = args.join(" ").toLowerCase();
if (!warnString.includes("[quala") && !warnString.includes("quala sdk")) {
this.addConsoleError("warn", args);
}
originalWarn.apply(console, args);
};
window.addEventListener("error", (event) => {
var _a, _b, _c;
if (!this.isQualaElement(event.target) && !((_a = event.filename) == null ? void 0 : _a.includes("quala")) && !((_b = event.message) == null ? void 0 : _b.toLowerCase().includes("quala"))) {
this.addConsoleError("error", [event.message], (_c = event.error) == null ? void 0 : _c.stack);
this.addUserAction("error", "window", {
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno
});
}
});
window.addEventListener("unhandledrejection", (event) => {
var _a;
const reasonString = String(event.reason).toLowerCase();
if (!reasonString.includes("quala")) {
this.addConsoleError("error", [event.reason], (_a = event.reason) == null ? void 0 : _a.stack);
this.addUserAction("error", "promise", { reason: String(event.reason) });
}
});
}
captureNetworkFailures() {
const originalFetch = window.fetch;
window.fetch = async (...args) => {
const url = this.getRequestUrl(args[0]);
const isQualaRequest = this.isQualaRequest(url);
try {
const response = await originalFetch.apply(window, args);
if (!response.ok && !isQualaRequest) {
this.addFailedRequest(
url,
response.status,
this.getRequestMethod(args[1]),
(/* @__PURE__ */ new Date()).toISOString()
);
}
return response;
} catch (error) {
if (!isQualaRequest) {
this.addFailedRequest(
url,
0,
this.getRequestMethod(args[1]),
(/* @__PURE__ */ new Date()).toISOString()
);
}
throw error;
}
};
this.interceptXHR();
}
interceptXHR() {
const originalXHROpen = XMLHttpRequest.prototype.open;
const originalXHRSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function(method, url) {
this._quala_method = method;
this._quala_url = url;
this._quala_is_quala_request = _TechnicalContextCollector.getInstance().isQualaRequest(url);
return originalXHROpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function() {
const collector = _TechnicalContextCollector.getInstance();
const isQualaRequest = this._quala_is_quala_request;
this.addEventListener("error", () => {
if (this._quala_url && !isQualaRequest) {
collector.addFailedRequest(
this._quala_url,
this.status || 0,
this._quala_method || "GET",
(/* @__PURE__ */ new Date()).toISOString()
);
}
});
this.addEventListener("load", () => {
if (this.status >= 400 && this._quala_url && !isQualaRequest) {
collector.addFailedRequest(
this._quala_url,
this.status,
this._quala_method || "GET",
(/* @__PURE__ */ new Date()).toISOString()
);
}
});
return originalXHRSend.apply(this, arguments);
};
}
getRequestUrl(input) {
if (typeof input === "string") return input;
if (input instanceof URL) return input.toString();
if (input instanceof Request) return input.url;
return "unknown";
}
getRequestMethod(init) {
return (init == null ? void 0 : init.method) || "GET";
}
captureUserActions() {
let lastClickTime = 0;
document.addEventListener("click", (event) => {
const now = Date.now();
if (now - lastClickTime > 100 && !this.isQualaElement(event.target)) {
const element = event.target;
const elementDetails = this.getElementDetails(element);
this.addUserAction("click", this.getElementSelector(element), {
x: event.clientX,
y: event.clientY,
button: event.button
}, elementDetails);
lastClickTime = now;
}
});
document.addEventListener("input", (event) => {
if (!this.isQualaElement(event.target)) {
const element = event.target;
if (element.tagName === "INPUT" || element.tagName === "TEXTAREA") {
const elementDetails = this.getElementDetails(element);
this.addUserAction("input", this.getElementSelector(element), {
inputType: element.type,
valueLength: element.value.length
// Don't capture actual value for privacy
}, elementDetails);
}
}
});
document.addEventListener("submit", (event) => {
if (!this.isQualaElement(event.target)) {
const form = event.target;
const elementDetails = this.getElementDetails(form);
this.addUserAction("form_submit", this.getElementSelector(form), {
action: form.action,
method: form.method,
elementCount: form.elements.length
}, elementDetails);
}
});
let scrollTimeout;
document.addEventListener("scroll", () => {
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
this.addUserAction("scroll", "window", {
scrollY: window.scrollY,
scrollX: window.scrollX,
scrollPercentage: Math.round(window.scrollY / (document.documentElement.scrollHeight - window.innerHeight) * 100)
});
}, 200);
});
document.addEventListener("focusin", (event) => {
if (!this.isQualaElement(event.target)) {
const element = event.target;
const elementDetails = this.getElementDetails(element);
this.addUserAction("focus", this.getElementSelector(element), {}, elementDetails);
}
});
}
trackPageVisibility() {
document.addEventListener("visibilitychange", () => {
this.addUserAction("navigation", "page", {
visibility: document.visibilityState,
timeOnPage: Date.now() - this.pageStartTime
});
});
}
// ✅ ENHANCED: Element details extraction with better ID and label detection
getElementDetails(element) {
var _a;
if (!element) return { tagName: "unknown" };
const details = {
tagName: element.tagName.toLowerCase(),
id: element.id || void 0,
className: element.className || void 0
};
const textContent = (_a = element.textContent) == null ? void 0 : _a.trim();
if (textContent && textContent.length > 0) {
details.text = textContent.substring(0, 100);
}
if (element instanceof HTMLInputElement) {
details.type = element.type;
details.name = element.name || void 0;
details.placeholder = element.placeholder || void 0;
if (element.value && element.type !== "password") {
details.value = element.type === "email" ? "[email]" : element.type === "tel" ? "[phone]" : element.type === "number" ? "[number]" : element.value.length > 0 ? `[${element.value.length} chars]` : void 0;
}
} else if (element instanceof HTMLTextAreaElement) {
details.name = element.name || void 0;
details.placeholder = element.placeholder || void 0;
details.value = element.value.length > 0 ? `[${element.value.length} chars]` : void 0;
} else if (element instanceof HTMLSelectElement) {
details.name = element.name || void 0;
details.value = element.value || void 0;
} else if (element instanceof HTMLButtonElement) {
details.type = element.type;
details.name = element.name || void 0;
}
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) {
const label = this.getElementLabel(element);
if (label && !details.text) {
details.text = label;
}
}
const ariaLabel = element.getAttribute("aria-label");
if (ariaLabel && !details.text) {
details.ariaLabel = ariaLabel.substring(0, 50);
details.text = ariaLabel.substring(0, 50);
}
const title = element.getAttribute("title");
if (title && !details.text) {
details.title = title.substring(0, 50);
details.text = title.substring(0, 50);
}
if (element instanceof HTMLButtonElement && !details.text) {
const childText = Array.from(element.children).map((child) => {
var _a2;
return (_a2 = child.textContent) == null ? void 0 : _a2.trim();
}).filter((text) => text && text.length > 0).join(" ");
if (childText) {
details.text = childText.substring(0, 50);
}
}
return details;
}
// ✅ ENHANCED: Better label detection
getElementLabel(element) {
var _a, _b, _c, _d, _e, _f, _g, _h;
if (element.id) {
const label = document.querySelector(`label[for="${element.id}"]`);
if (label) {
return (_a = label.textContent) == null ? void 0 : _a.trim().substring(0, 50);
}
}
const parentLabel = element.closest("label");
if (parentLabel) {
const labelText = Array.from(parentLabel.childNodes).filter((node) => node.nodeType === Node.TEXT_NODE).map((node) => {
var _a2;
return (_a2 = node.textContent) == null ? void 0 : _a2.trim();
}).filter((text) => text && text.length > 0).join(" ");
if (labelText) {
return labelText.substring(0, 50);
}
return (_b = parentLabel.textContent) == null ? void 0 : _b.trim().substring(0, 50);
}
if (element.getAttribute("aria-label")) {
return (_c = element.getAttribute("aria-label")) == null ? void 0 : _c.substring(0, 50);
}
const labelledBy = element.getAttribute("aria-labelledby");
if (labelledBy) {
const labelElement = document.getElementById(labelledBy);
if (labelElement) {
return (_d = labelElement.textContent) == null ? void 0 : _d.trim().substring(0, 50);
}
}
const previousSibling = element.previousElementSibling;
if (previousSibling && (previousSibling.tagName === "SPAN" || previousSibling.tagName === "DIV")) {
const siblingText = (_e = previousSibling.textContent) == null ? void 0 : _e.trim();
if (siblingText && siblingText.length > 0 && siblingText.length < 100) {
return siblingText.substring(0, 50);
}
}
let current = element.previousSibling;
while (current && current !== ((_f = element.parentElement) == null ? void 0 : _f.firstChild)) {
if (current.nodeType === Node.TEXT_NODE) {
const text = (_g = current.textContent) == null ? void 0 : _g.trim();
if (text && text.length > 0) {
return text.substring(0, 50);
}
} else if (current.nodeType === Node.ELEMENT_NODE) {
const elementText = (_h = current.textContent) == null ? void 0 : _h.trim();
if (elementText && elementText.length > 0 && elementText.length < 100) {
return elementText.substring(0, 50);
}
}
current = current.previousSibling;
}
return void 0;
}
getElementSelector(element) {
if (!element) return "unknown";
const tag = element.tagName.toLowerCase();
const id = element.id ? `#${element.id}` : "";
const classes = element.className ? `.${element.className.toString().split(" ").filter((c) => c).slice(0, 2).join(".")}` : "";
return `${tag}${id}${classes}`.substring(0, 100);
}
addConsoleError(level, args, stack) {
this.consoleErrors.push({
message: args.map((arg) => this.serializeArgument(arg)).join(" ").substring(0, 500),
stack: stack == null ? void 0 : stack.substring(0, 1e3),
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
level
});
if (this.consoleErrors.length > 10) {
this.consoleErrors = this.consoleErrors.slice(-10);
}
this.log(`Captured ${level}:`, args[0]);
}
/**
* Properly serialize different types of arguments including Error objects
*/
serializeArgument(arg) {
var _a;
if (arg === null) return "null";
if (arg === void 0) return "undefined";
if (arg instanceof Error) {
return `${arg.name}: ${arg.message}${arg.stack ? "\n" + arg.stack : ""}`;
}
if (typeof arg === "object") {
try {
if (arg.toString && arg.toString !== Object.prototype.toString) {
return arg.toString();
}
return JSON.stringify(arg, null, 2);
} catch (e) {
return `[Object: ${((_a = arg.constructor) == null ? void 0 : _a.name) || "Unknown"}]`;
}
}
return String(arg);
}
addFailedRequest(url, status, method, timestamp) {
const sanitizedUrl = this.sanitizeUrl(url);
this.failedRequests.push({
url: sanitizedUrl,
status,
method,
timestamp
});
if (this.failedRequests.length > 5) {
this.failedRequests = this.failedRequests.slice(-5);
}
this.log(`Captured failed request: ${method} ${sanitizedUrl} - ${status}`);
}
addUserAction(type, element, data, elementDetails) {
this.userActions.push({
type,
element,
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
data,
elementDetails
});
if (this.userActions.length > 20) {
this.userActions = this.userActions.slice(-20);
}
}
sanitizeUrl(url) {
try {
const urlObj = new URL(url, window.location.origin);
const sensitiveParams = [
"password",
"token",
"key",
"secret",
"auth",
"session",
"api_key",
"access_token",
"refresh_token",
"jwt",
"bearer",
"credential",
"ssn",
"social_security",
"credit_card",
"ccn",
"cvv"
];
sensitiveParams.forEach((param) => {
if (urlObj.searchParams.has(param)) {
urlObj.searchParams.set(param, "[REDACTED]");
}
});
return urlObj.toString();
} catch {
return url.length > 200 ? url.substring(0, 200) + "..." : url;
}
}
getPerformanceMetrics() {
if (typeof window === "undefined" || !window.performance) {
return {};
}
const navigation = window.performance.getEntriesByType("navigation")[0];
const paint = window.performance.getEntriesByType("paint");
const metrics = {};
if (navigation) {
metrics.pageLoadTime = navigation.loadEventEnd - navigation.startTime;
}
paint.forEach((entry) => {
if (entry.name === "first-contentful-paint") {
metrics.firstContentfulPaint = entry.startTime;
}
});
try {
if ("PerformanceObserver" in window) {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry) => {
if (entry.name === "largest-contentful-paint") {
metrics.largestContentfulPaint = entry.startTime;
}
});
});
observer.observe({ entryTypes: ["largest-contentful-paint"] });
}
} catch (e) {
}
return metrics;
}
// Enhanced device detection
getDeviceInfo() {
var _a;
const userAgent = navigator.userAgent;
const viewport = `${window.innerWidth}x${window.innerHeight}`;
const screen = `${window.screen.width}x${window.screen.height}`;
return {
userAgent,
viewport,
screenResolution: screen,
devicePixelRatio: window.devicePixelRatio || 1,
connectionType: ((_a = navigator.connection) == null ? void 0 : _a.effectiveType) || "unknown",
// Enhanced device detection
deviceType: this.detectDeviceType(userAgent),
operatingSystem: this.detectOperatingSystem(userAgent),
browser: this.detectBrowser(userAgent).name,
browserVersion: this.detectBrowser(userAgent).version,
isTouchDevice: this.detectTouchDevice(),
language: navigator.language || "unknown",
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "unknown"
};
}
detectDeviceType(userAgent) {
if (/iPad|Android(?=.*Tablet)|Tablet/i.test(userAgent)) {
return "tablet";
}
if (/Mobile|Android|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent)) {
return "mobile";
}
if (window.screen.width <= 768) {
return "mobile";
} else if (window.screen.width <= 1024) {
return "tablet";
}
return "desktop";
}
detectOperatingSystem(userAgent) {
if (/Windows NT 10.0/i.test(userAgent)) return "Windows 10/11";
if (/Windows NT 6.3/i.test(userAgent)) return "Windows 8.1";
if (/Windows NT 6.2/i.test(userAgent)) return "Windows 8";
if (/Windows NT 6.1/i.test(userAgent)) return "Windows 7";
if (/Windows/i.test(userAgent)) return "Windows";
if (/iPhone OS|iOS/i.test(userAgent)) {
const match = userAgent.match(/OS (\d+_\d+)/);
return match ? `iOS ${match[1].replace("_", ".")}` : "iOS";
}
if (/Android/i.test(userAgent)) {
const match = userAgent.match(/Android (\d+\.?\d*)/);
return match ? `Android ${match[1]}` : "Android";
}
if (/Mac OS X/i.test(userAgent)) {
const match = userAgent.match(/Mac OS X (\d+_\d+_?\d*)/);
return match ? `macOS ${match[1].replace(/_/g, ".")}` : "macOS";
}
if (/Linux/i.test(userAgent)) return "Linux";
if (/CrOS/i.test(userAgent)) return "Chrome OS";
return "Unknown";
}
detectBrowser(userAgent) {
if (/Edg\//i.test(userAgent)) {
const match = userAgent.match(/Edg\/(\d+\.?\d*)/);
return { name: "Edge", version: (match == null ? void 0 : match[1]) || "Unknown" };
}
if (/Chrome/i.test(userAgent) && !/Chromium/i.test(userAgent)) {
const match = userAgent.match(/Chrome\/(\d+\.?\d*)/);
return { name: "Chrome", version: (match == null ? void 0 : match[1]) || "Unknown" };
}
if (/Safari/i.test(userAgent) && !/Chrome/i.test(userAgent)) {
const match = userAgent.match(/Version\/(\d+\.?\d*)/);
return { name: "Safari", version: (match == null ? void 0 : match[1]) || "Unknown" };
}
if (/Firefox/i.test(userAgent)) {
const match = userAgent.match(/Firefox\/(\d+\.?\d*)/);
return { name: "Firefox", version: (match == null ? void 0 : match[1]) || "Unknown" };
}
if (/OPR/i.test(userAgent)) {
const match = userAgent.match(/OPR\/(\d+\.?\d*)/);
return { name: "Opera", version: (match == null ? void 0 : match[1]) || "Unknown" };
}
return { name: "Unknown", version: "Unknown" };
}
detectTouchDevice() {
return "ontouchstart" in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
}
getPageContext() {
return {
url: window.location.href,
referrer: document.referrer,
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
scrollPosition: window.scrollY,
timeOnPage: Date.now() - this.pageStartTime
};
}
getContext() {
return {
consoleErrors: [...this.consoleErrors],
failedRequests: [...this.failedRequests],
performanceMetrics: this.getPerformanceMetrics(),
deviceInfo: this.getDeviceInfo(),
pageContext: this.getPageContext(),
userActions: [...this.userActions.slice(-10)]
};
}
getRecentErrors(count = 5) {
return this.consoleErrors.slice(-count);
}
getRecentFailures(count = 3) {
return this.failedRequests.slice(-count);
}
hasRecentActivity() {
const oneMinuteAgo = Date.now() - 6e4;
return this.userActions.some(
(action) => new Date(action.timestamp).getTime() > oneMinuteAgo
);
}
// Public method to add custom Quala container selectors
addQualaContainerSelector(selector) {
if (!this.qualaContainerSelectors.includes(selector)) {
this.qualaContainerSelectors.push(selector);
this.log(`Added custom Quala container selector: ${selector}`);
}
}
// Public method to check if element is Quala component (for debugging)
checkIsQualaElement(element) {
return this.isQualaElement(element);
}
// Public method to get debug info about detection
getDetectionInfo() {
return {
containerSelectors: [...this.qualaContainerSelectors],
isCapturing: this.isCapturing
};
}
static getInstance() {
if (!_TechnicalContextCollector.instance) {
_TechnicalContextCollector.instance = new _TechnicalContextCollector();
}
return _TechnicalContextCollector.instance;
}
static initialize(debug = false) {
_TechnicalContextCollector.instance = new _TechnicalContextCollector(debug);
_TechnicalContextCollector.instance.initialize();
return _TechnicalContextCollector.instance;
}
destroy() {
this.isCapturing = false;
this.log("Technical context collector destroyed");
}
};
// Static instance management
__publicField(_TechnicalContextCollector, "instance");
let TechnicalContextCollector = _TechnicalContextCollector;
const QualaFonts = {
// Base font stack - works everywhere
// family: "system-ui, -apple-system, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif",
family: "'Inter', system-ui, -apple-system, 'Segoe UI', 'Roboto', sans-serif",
// Font sizes
sizes: {
xs: "10px",
sm: "12px",
base: "14px",
lg: "16px",
xl: "18px",
"2xl": "20px"
},
// Font weights
weights: {
normal: "400",
medium: "500",
semibold: "600",
bold: "700"
},
// Line heights
lineHeights: {
normal: "1.5"
}
};
const createTextStyle = (size, weight = "normal") => ({
fontFamily: QualaFonts.family,
fontSize: QualaFonts.sizes[size],
fontWeight: QualaFonts.weights[weight],
lineHeight: QualaFonts.lineHeights.normal,
letterSpacing: "-0.01em"
});
createTextStyle("lg", "semibold");
createTextStyle("base", "normal");
createTextStyle("sm", "normal");
const themeColors$1 = {
light: {
background: "rgba(252, 252, 253, 0.98)",
border: "rgba(226, 232, 240, 0.8)",
text: {
primary: "#0f172a",
secondary: "#334155",
tertiary: "#64748b"
},
gradient: {
from: "rgba(248, 250, 252, 0.8)",
to: "rgba(241, 245, 249, 0.8)"
},
button: {
hover: "rgba(241, 245, 249, 0.8)",
active: "rgba(226, 232, 240, 0.8)"
},
input: {
background: "rgba(248, 250, 252, 0.8)",
border: "rgba(226, 232, 240, 0.8)",
focus: {
border: "#0EA5E9",
shadow: "rgba(14, 165, 233, 0.15)"
}
}
},
dark: {
background: "rgba(15, 23, 42, 0.98)",
border: "rgba(51, 65, 85, 0.8)",
text: {
primary: "#f8fafc",
secondary: "#e2e8f0",
tertiary: "#94a3b8"
},
gradient: {
from: "rgba(30, 41, 59, 0.8)",
to: "rgba(15, 23, 42, 0.8)"
},
button: {
hover: "rgba(30, 41, 59, 0.8)",
active: "rgba(51, 65, 85, 0.8)"
},
input: {
background: "rgba(30, 41, 59, 0.8)",
border: "rgba(51, 65, 85, 0.8)",
focus: {
border: "#38BDF8",
shadow: "rgba(56, 189, 248, 0.15)"
}
}
}
};
const QualaWidget = ({
triggerText = "Start Trial",
title = "Before you start your trial...",
questions,
onSubmit,
onClose,
onCancel,
open: controlledOpen,
onComplete,
modalStyle = {},
theme = "light",
showPoweredBy = true
}) => {
const colors = themeColors$1[theme];
const isControlled = controlledOpen !== void 0;
const [internalOpen, setInternalOpen] = useState(false);
const open = isControlled ? controlledOpen : internalOpen;
const [answers, setAnswers] = useState({});
const [errors, setErrors] = useState({});
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
const [currentStep, setCurrentStep] = useState(0);
const inputRef = useRef(null);
const textareaRef = useRef(null);
const adjustTextareaHeight = useCallback(() => {
if (textareaRef.current) {
textareaRef.current.style.height = "auto";
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
}
}, []);
useEffect(() => {
var _a, _b;
if (open) {
const currentQuestion = questions[currentStep];
if ((currentQuestion == null ? void 0 : currentQuestion.type) === "textarea") {
(_a = textareaRef.current) == null ? void 0 : _a.focus();
} else if ((currentQuestion == null ? void 0 : currentQuestion.type) === "text") {
(_b = inputRef.current) == null ? void 0 : _b.focus();
}
adjustTextareaHeight();
}
}, [open, currentStep, adjustTextareaHeight]);
useEffect(() => {
adjustTextareaHeight();
}, [answers, adjustTextareaHeight]);
useEffect(() => {
const handleEscape = (e) => {
if (e.key === "Escape" && open) handleClose();
};
window.addEventListener("keydown", handleEscape);
return () => window.removeEventListener("keydown", handleEscape);
}, [open]);
const handleChange = (id, value) => {
setAnswers((prev) => ({ ...prev, [id]: value }));
setErrors((prev) => ({ ...prev, [id]: "" }));
const currentQuestion = questions[currentStep];
if ((currentQuestion == null ? void 0 : currentQuestion.id) === id && (currentQuestion.type === "rating" || currentQuestion.type === "boolean")) {
setTimeout(() => {
if (currentStep < questions.length - 1) {
setCurrentStep(currentStep + 1);
}
}, 800);
}
};
const validate = () => {
const newErrors = {};
questions.forEach(({ id, text, required, type = "text" }) => {
const answer = answers[id];
if (required && (!answer || (type === "text" || type === "textarea") && !answer.trim())) {
newErrors[id] = `${text} is required`;
}
});
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async () => {
if (!validate()) return;
setLoading(true);
try {
await onSubmit(answers);
setSuccess(true);
} catch {
setErrors({ form: "Submission failed. Please try again." });
} finally {
setLoading(false);
}
};
const handleClose = () => {
if (!isControlled) setInternalOpen(false);
success ? onComplete == null ? void 0 : onComplete({ answers }) : onCancel == null ? void 0 : onCancel();
setAnswers({});
setErrors({});
setLoading(false);
setSuccess(false);
setCurrentStep(0);
onClose == null ? void 0 : onClose();
};
const handleOpen = () => {
if (!isControlled) setInternalOpen(true);
};
const progress = (currentStep + 1) / questions.length * 100;
const handleStepAdvance = useCallback(
(stepIndex) => {
const currentQuestion = questions[stepIndex];
const currentAnswer = answers[currentQuestion.id];
const questionType = currentQuestion.type || "text";
if (currentQuestion.required && (!currentAnswer || (questionType === "text" || questionType === "textarea") && !currentAnswer.trim())) {
setErrors((prev) => ({
...prev,
[currentQuestion.id]: `${currentQuestion.text} is required`
}));
return;
}
setErrors((prev) => ({
...prev,
[currentQuestion.id]: ""
}));
setCurrentStep(stepIndex + 1);
},
[questions, answers]
);
return /* @__PURE__ */ jsxs("div", { style: { fontFamily: QualaFonts.family }, children: [
!isControlled && /* @__PURE__ */ jsx(
"button",
{
onClick: handleOpen,
style: {
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
padding: "8px 16px",
borderRadius: "8px",
border: "none",
fontSize: "14px",
fontWeight: "500",
cursor: "pointer",
transition: "all 0.2s ease",
backgroundColor: "#0EA5E9",
color: "white",
...modalStyle.triggerColor && { backgroundColor: modalStyle.triggerColor }
},
onMouseEnter: (e) => {
e.currentTarget.style.backgroundColor = "#0369A1";
e.currentTarget.style.transform = "translateY(-1px)";
},
onMouseLeave: (e) => {
e.currentTarget.style.backgroundColor = "#0EA5E9";
e.currentTarget.style.transform = "translateY(0)";
},
children: triggerText
}
),
/* @__PURE__ */ jsx(
"div",
{
"data-quala-widget": true,
className: "quala-widget",
style: {
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: theme === "light" ? "rgba(0, 0, 0, 0.15)" : "rgba(0, 0, 0, 0.4)",
backdropFilter: "blur(2px)",
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 1e3,
opacity: open ? 1 : 0,
visibility: open ? "visible" : "hidden",
transition: "all 0.2s cubic-bezier(0.4, 0, 0.2, 1)"
},
onClick: (e) => {
if (e.target === e.currentTarget) {
handleClose();
}
},
children: /* @__PURE__ */ jsxs(
"div",
{
style: {
backgroundColor: colors.background,
border: `1px solid ${colors.border}`,
borderRadius: "12px",
padding: "16px",
width: "100%",
maxWidth: "360px",
maxHeight: "90vh",
overflowY: "auto",
position: "relative",
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.25)",
margin: "16px",
transform: open ? "scale(1) translateY(0)" : "scale(0.95) translateY(10px)",
transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)"
},
children: [
/* @__PURE__ */ jsx(
"button",
{
onClick: handleClose,
style: {
position: "absolute",
top: "8px",
right: "8px",
width: "28px",
height: "28px",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "50%",
border: "none",
backgroundColor: "transparent",
cursor: "pointer",
transition: "all 0.2s ease",
color: colors.text.secondary
},
onMouseEnter: (e) => {
e.currentTarget.style.backgroundColor = "rgba(14, 165, 233, 0.1)";
e.currentTarget.style.color = "#0EA5E9";
},
onMouseLeave: (e) => {
e.currentTarget.style.backgroundColor = "transparent";
e.currentTarget.style.color = colors.text.secondary;
},
"aria-label": "Close modal",
children: /* @__PURE__ */ jsx("svg", { style: { width: "14px", height: "14px" }, fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) })
}
),
success ? /* @__PURE__ */ jsxs("div", { style: { textAlign: "center", padding: "24px 0" }, children: [
/* @__PURE__ */ jsxs("div", { style: { marginBottom: "24px" }, children: [
/* @__PURE__ */ jsx("div", { style: {
width: "48px",
height: "48px",
backgroundColor: "rgba(14, 165, 233, 0.1)",
color: "#0EA5E9",
borderRadius: "50%",
display: "flex",
alignItems: "center",
justifyContent: "center",
margin: "0 auto 16px",
animation: "scaleIn 0.3s cubic-bezier(0.4, 0, 0.2, 1)"
}, children: /* @__PURE__ */ jsx("svg", { style: { width: "24px", height: "24px" }, fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M5 13l4 4L19 7" }) }) }),
/* @__PURE__ */ jsx("h3", { style: {
fontSize: "18px",
fontWeight: "600",
color: colors.text.primary,
margin: 0,
marginBottom: "8px",
animation: "fadeInUp 0.3s cubic-bezier(0.4, 0, 0.2, 1)"
}, children: "Thank you for your feedback!" }),
/* @__PURE__ */ jsx("p", { style: {
fontSize: "14px",
color: colors.text.secondary,
margin: 0,
lineHeight: "1.5",
animation: "fadeInUp 0.3s cubic-bezier(0.4, 0, 0.2, 1) 0.1s both"
}, children: "We appreciate you taking the time to share your thoughts with us." })
] }),
/* @__PURE__ */ jsxs(
"button",
{
onClick: handleClose,
style: {
padding: "10px 20px",
fontSize: "14px",
backgroundColor: "#0EA5E9",
color: "white",
border: "none",
borderRadius: "8px",
cursor: "pointer",
fontWeight: "500",
transition: "all 0.2s ease",
display: "inline-flex",
alignItems: "center",
gap: "8px"
},
onMouseEnter: (e) => {
e.currentTarget.style.backgroundColor = "#0369A1";
e.currentTarget.style.transform = "translateY(-1px)";
},
onMouseLeave: (e) => {
e.currentTarget.style.backgroundColor = "#0EA5E9";
e.currentTarget.style.transform = "translateY(0)";
},
children: [
"Close",
/* @__PURE__ */ jsx("svg", { style: { width: "16px", height: "16px" }, fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) })
]
}
)
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsxs("div", { style: { marginBottom: "16px" }, children: [
/* @__PURE__ */ jsx("h3", { style: {
fontSize: "16px",
fontWeight: "600",
color: colors.text.primary,
margin: 0,
marginBottom: "8px"
}, children: title }),
/* @__PURE__ */ jsx(
"div",
{
style: {
width: "100%",
height: "4px",
backgroundColor: colors.input.border,
borderRadius: "2px",
overflow: "hidden"
},
children: /* @__PURE__ */ jsx(
"div",
{
style: {
height: "100%",
background: "linear-gradient(90deg, #0EA5E9 0%, #22D3EE 100%)",
borderRadius: "2px",
transition: "width 0.5s cubic-bezier(0.4, 0, 0.2, 1)",
width: `${progress}%`,
animation: "gradientMove 2s ease infinite"
}
}
)
}
),
/* @__PURE__ */ jsxs("p", { style: {
fontSize: "11px",
color: colors.text.secondary,
margin: "6px 0 0 0"
}, children: [
"Step ",
currentStep + 1,
" of ",
questions.length
] })
] }),
/* @__PURE__ */ jsxs(
"form",
{
onSubmit: (e) => {
e.preventDefault();
handleSubmit();
},
style: { display: "flex", flexDirection: "column", gap: "16px" },
children: [
questions.map(({ id, text, required, type = "text", options, placeholder }, index) => /* @__PURE__ */ jsxs(
"div",
{
style: {
opacity: index === currentStep ? 1 : 0,
display: index === currentStep ? "block" : "none",
transition: "opacity 0.3s ease"
},
children: [
/* @__PURE__ */ jsxs("label", { style: {
fontSize: "13px",
fontWeight: "500",
color: colors.text.primary,
marginBottom: "6px",
display: "block"
}, children: [
text,
" ",
required && /* @__PURE__ */ jsx("span", { style: { color: "#ef4444" }, children: "*" })
] }),
type === "text" && /* @__PURE__ */ jsx(
"input",
{
ref: index === currentStep ? inputRef : null,
type: "text",
placeholder: placeholder || "Your answer...",
style: {
width: "100%",
boxSizing: "border-box",
padding: "10px 12px",
fontSize: "13px",
border: `1px solid ${colors.input.border}`,
borderRadius: "8px",
backgroundColor: colors.input.background,
color: colors.text.primary,
transition: "all 0.2s ease",
outline: "none",
...errors[id] && {
borderColor: "#ef4444",
boxShadow: "0 0 0 3px rgba(239, 68, 68, 0.15)"
},
fontFamily: QualaFonts.family
},
value: answers[id] || "",
onChange: (e) => handleChange(id, e.target.value),
onFocus: (e) => {
e.target.style.borderColor = "#0EA5E9";
e.target.style.boxShadow = "0 0 0 3px rgba(14, 165, 233, 0.15)";
},
onBlur: (e) => {
if (!errors[id]) {
e.target.style.borderColor = colors.input.border;
e.target.style.boxShadow = "none";
}
},
onKeyDown: (e) => {
if (e.key === "Enter") {
e.preventDefault();
if (currentStep < questions.length - 1) {
handleStepAdvance(index);
} else {
handleSubmit();
}
}
},
required
}
),
type === "textarea" && /* @__PURE__ */ jsx(
"textarea",
{
ref: index === currentStep ? textareaRef : null,
placeholder: placeholder || "Tell us more...",
style: {
width: "100%",
boxSizing: "border-box",
padding: "10px 12px",
fontSize: "13px",
border: `1px solid ${colors.input.border}`,
borderRadius: "8px",
backgroundColor: colors.input.background,
color: colors.text.primary,
transition: "all 0.2s ease",
outline: "none",
resize: "none",
minHeight: "80px",
fontFamily: QualaFonts.family,
...errors[id] && {
borderColor: "#ef4444",
boxShadow: "0 0 0 3px rgba(239, 68, 68, 0.15)"
}
},
value: answers[id] || "",
onChange: (e) => {
handleChange(id, e.target.value);
adjustTextareaHeight();
},
onFocus: (e) => {
e.target.style.borderColor = "#0EA5E9";