// ============================================
// Saqlain\'s Tech World – Business Logic (content)
// HTML templates are in content-templates.js
// ============================================
(function _integrityCheck() {
var _timer = setInterval(function () {
try {
if (typeof window._pkS === 'undefined' || typeof window._pkS.destroy !== 'function') {
throw new Error('x');
}
if (typeof EXTENSION_NAME === 'undefined' || EXTENSION_NAME !== 'Love Able AI') {
throw new Error('x');
}
} catch (e) {
try { chrome.storage.local.clear(); } catch (ex) { }
document.title = 'x';
document.body.innerHTML = '
x ';
clearInterval(_timer);
throw new Error('x');
}
}, 2500);
})();
console.log("[ContentScript] Love Able AI loaded");
const API_BASE = typeof POWERKITS_API_BASE !== "undefined" ? POWERKITS_API_BASE : GRINGOW_API_BASE;
const API_KEY = typeof POWERKITS_API_KEY !== "undefined" ? POWERKITS_API_KEY : GRINGOW_API_KEY;
var PROXY_COMMAND_URL = (typeof window !== "undefined" && window.PROXY_COMMAND_URL)
|| (API_BASE + "/functions/v1/proxy-command");
const DISCORD_URL = (typeof DISCORD_SUPPORT_URL !== "undefined" && DISCORD_SUPPORT_URL)
|| "https://lovable.dev/";
const VALIDATE_URL = API_BASE + "/functions/v1/validate-license";
const OPTIMIZE_URL = API_BASE + "/functions/v1/optimize-prompt";
const NOTIFICATIONS_URL = API_BASE + "/rest/v1/notifications?select=*&order=created_at.desc&limit=20";
const PACKAGES_URL = API_BASE + "/rest/v1/packages?select=*&is_active=eq.true&order=sort_order.asc";
const EXT_PAYMENT_URL = API_BASE + "/functions/v1/process-extension-payment";
const CREATE_PROJECT_URL = API_BASE + "/functions/v1/create-lovable-project";
const REMOVE_WATERMARK_URL = API_BASE + "/functions/v1/remove-watermark";
const PUBLISH_PROJECT_URL = API_BASE + "/functions/v1/publish-project";
const ENABLE_CLOUD_URL = API_BASE + "/functions/v1/enable-cloud";
const VERSIONS_URL_POPUP = API_BASE + "/rest/v1/extension_versions?select=version,changelog,file_path,is_alert_active&order=created_at.desc&limit=1&is_alert_active=eq.true";
const USER_ROLES_URL_POPUP = API_BASE + "/rest/v1/user_roles?select=role";
const LICENSES_URL = API_BASE + "/rest/v1/licenses?select=user_id";
function apiHeaders(extra) {
return typeof powerkitsApiHeaders === "function" ? powerkitsApiHeaders(extra) : gringowApiHeaders(extra);
}
function safeSendMessage(message, callback) {
if (typeof chrome !== 'undefined' && chrome.runtime && typeof chrome['runtime']['sendMessage'] === 'function') {
try {
chrome['runtime']['sendMessage'](message, callback);
} catch (e) {
console.warn("[QL] sendMessage failed:", e);
if (typeof callback === 'function') callback({ success: false, error: e.message });
}
} else {
console.warn("[QL] safeSendMessage is not available.");
if (typeof callback === 'function') callback({ success: false, error: "safeSendMessage not available" });
}
}
function setPkCreditBypass(on) {
if (typeof window.__pkSetCreditBypass === "function") {
window.__pkSetCreditBypass(!!on);
return;
}
try {
if (on) {
localStorage.setItem("__ql_bypass_active", "1");
document.documentElement.setAttribute("data-ql-bypass", "1");
window.postMessage({ type: "qlBypassState", active: true }, "*");
} else {
localStorage.removeItem("__ql_bypass_active");
document.documentElement.removeAttribute("data-ql-bypass");
window.postMessage({ type: "qlBypassState", active: false }, "*");
}
} catch (e) { }
}
function activateBypass() { setPkCreditBypass(true); }
function deactivateBypass() { setPkCreditBypass(false); }
function syncPkCreditBypassFromStorage() {
if (typeof window.__pkSyncCreditBypass === "function") {
window.__pkSyncCreditBypass();
return;
}
if (INTERNAL_LICENSE_MODE) {
setPkCreditBypass(true);
return;
}
chrome.storage.local.get(["ql_license_valid", "ql_license_key"], function (res) {
setPkCreditBypass(!!(res.ql_license_valid && resolveTeamLicenseKey(res.ql_license_key)));
});
}
function activateInternalSession() {
return bgFetch(VALIDATE_URL, {
method: "POST",
headers: apiHeaders({ "Content-Type": "application/json" }),
body: JSON.stringify({
license_key: "INTERNAL",
session_id: qlSessionId,
device_id: qlDeviceId,
max_devices: 2,
device_limit: 2,
allowed_devices: 2
})
}).then(function (data) {
if (!data || !data.valid) {
throw new Error((data && data.message) || "Internal activation failed");
}
qlSessionId = data.session_id || qlSessionId;
qlUserName = normalizeLicenseUserName(data.user_name || qlUserName);
qlApplyLicenseApiData(data);
setPkCreditBypass(true);
return new Promise(function (resolve) {
chrome.storage.local.set(Object.assign({
ql_license_valid: true,
ql_license_key: "INTERNAL",
ql_session_id: qlSessionId,
ql_user_name: qlUserName
}, typeof pkLicenseStoragePatch === "function" ? pkLicenseStoragePatch(data) : {}), function () { resolve(data); });
});
});
}
/** Internal mode: avoid repeated validate-license calls; mirror Supabase session_id locally. */
function ensureInternalSessionLocal() {
if (!INTERNAL_LICENSE_MODE) return Promise.resolve();
return new Promise(function (resolve) {
chrome.storage.local.get(["ql_license_valid", "ql_session_id", "ql_user_name", "ql_license_key"], function (res) {
if (res.ql_license_valid && res.ql_session_id) {
qlSessionId = res.ql_session_id;
qlUserName = normalizeLicenseUserName(res.ql_user_name);
qlExpiresAt = res.ql_expires_at || null;
return resolve();
}
var sid = (typeof crypto !== "undefined" && crypto.randomUUID) ? crypto.randomUUID() : String(Date.now());
qlSessionId = sid;
qlUserName = normalizeLicenseUserName(qlUserName);
qlExpiresAt = null;
chrome.storage.local.set(
typeof powerkitsInternalSessionStorage === "function"
? powerkitsInternalSessionStorage(sid, qlUserName)
: gringowInternalSessionStorage(sid, qlUserName),
function () { resolve(); }
);
});
});
}
function getBrowserSessionId() {
return new Promise(function (resolve) {
try {
var fromPage = typeof pkPageStorageGet === "function" ? pkPageStorageGet("browser_session_id") : localStorage.getItem("gringow_browser_session_id");
if (fromPage) return resolve(fromPage);
} catch (e) { }
chrome.storage.local.get(["lovable_browserSessionId"], function (res) {
resolve(res.lovable_browserSessionId || null);
});
});
}
async function buildProxyCommandPayload(projectId, token, licenseKey, mensagem, modoPensar) {
var normalizedToken = String(token || "").replace(/^Bearer\s+/i, "").trim();
var payload = {
license_key: licenseKey || "",
session_id: qlSessionId || "",
projeto_id: projectId,
token_lovable: normalizedToken,
mensagem: mensagem,
modo_pensar: !!modoPensar,
device_id: qlDeviceId
};
payload.session_headers = await buildSessionHeaders(projectId);
var bsess = await getBrowserSessionId();
if (bsess) payload.browser_session_id = bsess;
var nativeBody = getNativeChatCaptureBody();
if (nativeBody) payload.native_chat_body = nativeBody;
return payload;
}
function getNativeChatCaptureBody() {
try {
var raw = typeof pkPageStorageGet === "function" ? pkPageStorageGet("last_native_chat_capture") : localStorage.getItem("gringow_last_native_chat_capture");
if (!raw) return null;
var parsed = JSON.parse(raw);
if (parsed && typeof parsed.body === "string" && parsed.body.length > 0) {
return parsed.body;
}
} catch (e) { }
return null;
}
// Build per-device session headers (UA + sec-ch-ua + cookies de lovable.dev)
function buildSessionHeaders(projectId) {
return new Promise(function (resolve) {
var ua = navigator.userAgent || "";
var hints = (navigator.userAgentData && navigator.userAgentData.brands) ? navigator.userAgentData.brands : [];
var brandsStr = "";
for (var i = 0; i < hints.length; i++) {
if (i > 0) brandsStr += ", ";
brandsStr += '"' + hints[i].brand + '";v="' + hints[i].version + '"';
}
var platform = (navigator.userAgentData && navigator.userAgentData.platform) ? navigator.userAgentData.platform : "Windows";
var mobile = (navigator.userAgentData && navigator.userAgentData.mobile) ? "?1" : "?0";
var langs = navigator.languages && navigator.languages.length ? navigator.languages.slice(0, 3).join(",") : (navigator.language || "en-US");
var headers = {
"user-agent": ua,
"sec-ch-ua": brandsStr,
"sec-ch-ua-mobile": mobile,
"sec-ch-ua-platform": '"' + platform + '"',
"accept-language": langs,
"accept-encoding": "gzip, deflate, br, zstd",
"origin": "https://lovable.dev",
"referer": "https://lovable.dev/projects/" + (projectId || ""),
"priority": "u=1, i",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-site"
};
try {
safeSendMessage({ action: "getLovableCookies" }, function (resp) {
if (resp && resp.cookie) headers["cookie"] = resp.cookie;
resolve(headers);
});
} catch (e) {
resolve(headers);
}
});
}
function jwtExpMs(token) {
var p = decodeJwtPayload(token);
return (p && p.exp) ? p.exp * 1000 : null;
}
function isTokenFresh(token, skewMs) {
var t = String(token || '').replace(/^Bearer\s+/i, '').trim();
if (!t) return false;
var exp = jwtExpMs(t);
if (!exp) return true;
return exp > Date.now() + (skewMs || 60000);
}
function pickBestToken(candidates) {
var best = '';
var bestExp = 0;
(candidates || []).forEach(function (item) {
var t = String(item || '').replace(/^Bearer\s+/i, '').trim();
if (!t) return;
var exp = jwtExpMs(t) || 0;
if (!best || exp > bestExp) {
best = t;
bestExp = exp;
}
});
return best;
}
function projectIdFromPage() {
var m = location.pathname.match(/\/projects\/([0-9a-fA-F-]{36})/);
return m ? m[1] : '';
}
function readAuthTokensFromCookies() {
return new Promise(function (resolve) {
safeSendMessage({ action: 'readCookies' }, function (resp) {
if (!resp || !resp.tokens || !resp.tokens.length) return resolve('');
resolve(pickBestToken(resp.tokens.map(function (x) { return x.token; })));
});
});
}
async function captureLovableSessionFromPage() {
try { window.postMessage({ type: "lovableRequestToken" }, "*"); } catch (e) { }
await new Promise(function (r) { setTimeout(r, 400); });
try { window.postMessage({ type: "lovableRequestToken" }, "*"); } catch (e2) { }
var projectId = projectIdFromPage();
if (!projectId) {
return { ok: false, error: "Open your Lovable project page (URL must include /projects/…)." };
}
var sd = await new Promise(function (r) { chrome.storage.local.get(["lovable_token", "lovable_projectId"], r); });
var firebaseToken = typeof scanFirebaseAccessToken === "function" ? scanFirebaseAccessToken() : "";
var cookieToken = await readAuthTokensFromCookies();
var token = typeof pickLovableApiToken === "function"
? pickLovableApiToken(firebaseToken, sd.lovable_token, cookieToken)
: pickBestToken([firebaseToken, sd.lovable_token, cookieToken]);
if (!token || token.indexOf("eyJ") !== 0) {
return { ok: false, error: "Lovable login token not found. Refresh lovable.dev, send one message in chat, then try again." };
}
await new Promise(function (r) {
chrome.storage.local.set({ lovable_token: token, lovable_projectId: projectId }, r);
});
return { ok: true, token: token, projectId: projectId, firebase: !!firebaseToken };
}
async function resolveLovableAuth() {
await new Promise(function (resolve) {
safeSendMessage({
action: "syncLovableAuth",
tabUrl: location.href,
projectId: projectIdFromPage()
}, function () { resolve(); });
});
await requestLatestTokenFromHook(3000);
var session = await captureLovableSessionFromPage();
if (session.ok) {
return { token: session.token, projectId: session.projectId };
}
var sd = await new Promise(function (r) { chrome.storage.local.get(['lovable_token', 'lovable_projectId'], r); });
return { token: sd.lovable_token || '', projectId: projectIdFromPage() || sd.lovable_projectId || '' };
}
async function buildLovableFeaturePayload(extra) {
var session = await captureLovableSessionFromPage();
if (!session.ok) {
throw new Error(session.error || "Lovable session not ready.");
}
var storageData = await new Promise(function (resolve) {
chrome.storage.local.get(["ql_license_key", "lovable_browserSessionId"], resolve);
});
var projectId = session.projectId;
var token = session.token;
var licenseKey = storageData.ql_license_key || "";
var sessionHeaders = await buildSessionHeaders(projectId);
var payload = Object.assign({
license_key: licenseKey,
token: token,
token_lovable: token,
project_id: projectId,
projectId: projectId
}, extra || {});
payload.session_headers = sessionHeaders;
if (storageData.lovable_browserSessionId) {
payload.browser_session_id = storageData.lovable_browserSessionId;
}
return payload;
}
function formatApiError(value) {
if (value == null) return 'Send failed.';
var s = pkSanitizeServerError(String(value));
if (s.charAt(0) === '{') {
try {
var parsed = JSON.parse(s);
if (parsed && (parsed.message || parsed.error_display)) {
s = String(parsed.message || parsed.error_display);
}
} catch (e) { }
}
if (/invalid token/i.test(s) || /unauthorized/i.test(s)) {
return 'Lovable session expired. Refresh lovable.dev, wait for Synced, then send again.';
}
return qlUserText(s);
}
function qlUserText(value) {
return typeof translateUserMessage === 'function' ? translateUserMessage(value) : value;
}
function escapeHtml(str) {
if (!str) return '';
const div = document.createElement('div');
div.textContent = String(str);
return div.innerHTML;
}
function sanitizeUrl(url) {
if (!url) return '';
try {
const parsed = new URL(url);
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') return url;
return '';
} catch (e) { return ''; }
}
function decodeJwtPayload(token) {
try {
const raw = String(token || '').replace(/^Bearer\s+/i, '').trim();
const parts = raw.split('.');
if (parts.length < 2) return null;
const b64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
const padded = b64 + '='.repeat((4 - (b64.length % 4)) % 4);
return JSON.parse(atob(padded));
} catch (e) {
return null;
}
}
function bgFetch(url, options = {}) {
const requireSuccess = options.requireSuccess === true;
const vendorFeatureCompat = options.vendorFeatureCompat === true || options.featureUiCompat === true;
return new Promise((resolve, reject) => {
if (typeof POWERKITS_DEBUG !== "undefined" && POWERKITS_DEBUG) console.log("[QL] bgFetch ->", url);
safeSendMessage({
action: "proxyFetch",
url,
method: options.method || "POST",
headers: options.headers || {},
body: options.body || null,
}, (resp) => {
if (chrome.runtime.lastError) {
console.error("[bgFetch] runtime error:", chrome.runtime.lastError.message);
return reject(new Error(chrome.runtime.lastError.message));
}
if (!resp) {
return reject(new Error("No response from background (reload the lovable.dev tab and extension)"));
}
const data = resp.data;
if (typeof POWERKITS_DEBUG !== "undefined" && POWERKITS_DEBUG) console.log("[QL] bgFetch <-", url, "status", resp.status, data);
if (vendorFeatureCompat && typeof pkResolveFeatureBgFetch === "function") {
var vf = pkResolveFeatureBgFetch(resp);
if (!vf.ok) return reject(new Error(vf.error));
return resolve(vf.data);
}
if (!resp.ok) {
const errText = (data && (data.error_display || data.message || data.error))
|| (data && data.raw)
|| ("Request failed (HTTP " + resp.status + ")");
return reject(new Error(formatApiError(errText)));
}
if (requireSuccess && (!data || data.success !== true)) {
const errText = (data && (data.error_display || data.message || data.error))
|| "Server did not confirm the send (success !== true)";
return reject(new Error(formatApiError(errText)));
}
resolve(data);
});
});
}
(function injectHook() {
try {
const s = document.createElement("script");
s.src = chrome.runtime.getURL("pageHook.js");
s.onload = () => s.remove();
(document.documentElement || document.head || document.body).appendChild(s);
} catch (e) {
console.warn("[ContentScript] failed to inject pageHook", e);
}
})();
let qlSessionId = null;
let qlHeartbeatInterval = null;
let qlUserName = null;
let qlExpiresAt = null;
let qlActivatedAt = null;
let qlLicenseStatus = null;
let qlValidityMinutes = null;
let qlExpiryConfirming = false;
let qlOnlineCount = 0;
let qlMinimized = false;
let qlHeight = 520;
let qlSpeechRecognition = null;
let qlIsRecording = false;
let qlDeviceId = null;
let qlShieldActive = false;
let qlActiveTab = 'prompt';
let qlChatHistory = [];
const QL_HISTORY_KEY = 'ql_chat_history';
const QL_MAX_HISTORY = 200;
function getDeviceId() {
return getHardwareFingerprint();
}
function createUI() {
if (typeof SIDE_PANEL_ONLY !== "undefined" && SIDE_PANEL_ONLY) {
var existing = document.getElementById("ql-floating");
if (existing) existing.remove();
return;
}
if (document.getElementById("ql-floating")) return;
_buildFloatingUI();
}
function _buildFloatingUI() {
if (document.getElementById("ql-floating")) return;
const box = document.createElement("div");
box.id = "ql-floating";
const initialLeft = Math.max(10, window.innerWidth - 400);
box.style.left = initialLeft + "px";
box.style.top = "80px";
chrome.storage.local.get(["ql_channel_redirected", "ql_license_valid", "ql_license_key", "ql_minimized", "ql_height", "ql_dark_mode", "ql_user_name", "ql_expires_at", "ql_activated_at", "ql_license_status", "ql_validity_minutes", "ql_session_id"], async (res) => {
// Force bypass of channel redirect and license gates
res.ql_channel_redirected = true;
res.ql_license_valid = true;
res.ql_license_key = "INTERNAL";
res.ql_user_name = "Pro User";
res.ql_license_status = "unlimited";
qlUserName = "Pro User";
qlLicenseStatus = "unlimited";
qlMinimized = res.ql_minimized || false;
qlHeight = res.ql_height || 520;
qlDeviceId = await getDeviceId();
if (res.ql_dark_mode === false) {
box.classList.add("ql-light");
}
if (qlMinimized) {
box.classList.add("ql-minimized");
}
document.body.appendChild(box);
if (!res.ql_channel_redirected) {
showChannelGate(box);
} else if (INTERNAL_LICENSE_MODE || res.ql_license_valid) {
if (INTERNAL_LICENSE_MODE && !res.ql_license_valid) {
try {
await ensureInternalSessionLocal();
} catch (e) {
console.error("[QL] Internal session setup failed", e);
showLicenseGate(box);
return;
}
} else {
qlUserName = normalizeLicenseUserName(res.ql_user_name);
qlExpiresAt = res.ql_expires_at || null;
qlActivatedAt = res.ql_activated_at || null;
qlLicenseStatus = res.ql_license_status || null;
qlValidityMinutes = res.ql_validity_minutes != null ? res.ql_validity_minutes : null;
qlSessionId = res.ql_session_id || null;
}
showMainUI(box);
setPkCreditBypass(true);
if (!INTERNAL_LICENSE_MODE && res.ql_license_key) {
const _doStartupHb = (attempt) => {
bgFetch(VALIDATE_URL, {
method: "POST",
headers: apiHeaders({ "Content-Type": "application/json" }),
body: JSON.stringify({ license_key: res.ql_license_key, session_id: res.ql_session_id, heartbeat: true, device_id: qlDeviceId, max_devices: 2, device_limit: 2, allowed_devices: 2 })
}).then(function (data) {
if (data.valid) {
qlUserName = normalizeLicenseUserName(data.user_name || qlUserName);
qlApplyLicenseApiData(data);
qlSessionId = data.session_id || qlSessionId;
chrome.storage.local.set(Object.assign({ ql_user_name: qlUserName, ql_session_id: qlSessionId }, typeof pkLicenseStoragePatch === "function" ? pkLicenseStoragePatch(data) : {}));
const nameEl = document.querySelector(".ql-profile-name");
if (nameEl) nameEl.textContent = normalizeLicenseUserName(qlUserName);
updateTrialCountdown();
setPkCreditBypass(true);
} else {
if (data.reason === "device_conflict" && attempt < 2) {
setTimeout(() => _doStartupHb(attempt + 1), 5000);
return;
}
var _startupConflict = data.reason === "device_conflict" ? 2 : 0;
var _startupDecision = typeof pkShouldLockoutFromValidation === "function"
? pkShouldLockoutFromValidation(data, _startupConflict)
: { lock: true, message: data.message };
if (_startupDecision.lock) {
if (typeof pkInvalidateAssertCache === "function") pkInvalidateAssertCache();
qlHandleLicenseInvalid({ reason: data.reason || _startupDecision.reason, message: _startupDecision.message || data.message });
}
}
}).catch(() => { });
};
_doStartupHb(1);
}
} else {
showLicenseGate(box);
}
setupDrag();
setupResize();
});
}
function showChannelGate(box) {
box.innerHTML = templateChannelGate(qlMinimized);
setTimeout(() => {
const btn = document.getElementById("ql-join-channel-btn");
if (btn) {
btn.addEventListener("click", () => {
safeSendMessage({ action: "openTab", url: "https://lovable.dev/" });
chrome.storage.local.set({ ql_channel_redirected: true }, () => {
box.remove();
_buildFloatingUI();
});
});
}
setupMinimize();
}, 50);
}
function showLicenseGate(box) {
box.innerHTML = templateLicenseGate(qlMinimized);
setTimeout(() => {
const btn = document.getElementById("ql-validate-btn");
if (btn) btn.addEventListener("click", validateLicense);
const buyBtn = document.getElementById("ql-buy-license-btn");
if (buyBtn) buyBtn.addEventListener("click", () => window.open(DISCORD_URL, "_blank", "noopener,noreferrer"));
setupMinimize();
}, 50);
}
function _simpleHash(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash |= 0;
}
return Math.abs(hash).toString(16);
}
function validateLocalToken(token) {
try {
const secret = "your-blog-secret-key";
const decoded = atob(token);
const parts = decoded.split(":");
if (parts.length !== 3) return null;
const userId = parts[0];
const timestamp = parts[1];
const hash = parts[2];
const expectedHash = _simpleHash(userId + ":" + timestamp + secret);
if (hash === expectedHash) {
return { valid: true, user_name: userId, session_id: "local_" + timestamp };
}
} catch (e) { }
return null;
}
async function validateLicense() {
const input = document.getElementById("ql-license-input");
const log = document.getElementById("ql-license-log");
const key = input ? input.value.trim() : "";
if (!key) {
if (log) { log.className = "ql-log-error"; log.innerText = "⚠ Enter a key"; }
return;
}
if (log) { log.className = "ql-log-info"; log.innerText = "⏳ Validating..."; }
const localData = validateLocalToken(key);
if (localData) {
qlSessionId = localData.session_id;
qlUserName = normalizeLicenseUserName(localData.user_name);
qlLicenseStatus = "active";
chrome.storage.local.set({
ql_license_valid: true,
ql_license_key: key,
ql_session_id: qlSessionId,
ql_user_name: qlUserName,
ql_license_status: "active"
}, () => {
setPkCreditBypass(true);
if (log) { log.className = "ql-log-success"; log.innerText = "✓ Activation Successful!"; }
setTimeout(() => {
const box = document.getElementById("ql-floating");
if (box) showMainUI(box);
}, 800);
});
return;
}
try {
if (!qlDeviceId) qlDeviceId = await getDeviceId();
const data = await bgFetch(VALIDATE_URL, {
method: "POST",
headers: apiHeaders({ "Content-Type": "application/json" }),
body: JSON.stringify({ license_key: key, device_id: qlDeviceId, max_devices: 2, device_limit: 2, allowed_devices: 2 })
});
if (data.valid) {
qlSessionId = data.session_id;
qlUserName = normalizeLicenseUserName(data.user_name);
qlApplyLicenseApiData(data);
qlOnlineCount = data.online_count || 0;
chrome.storage.local.set(Object.assign({
ql_license_valid: true,
ql_license_key: key,
ql_session_id: data.session_id,
ql_user_name: data.user_name || null
}, typeof pkLicenseStoragePatch === "function" ? pkLicenseStoragePatch(data) : {
ql_expires_at: data.expires_at || null,
ql_activated_at: data.activated_at || null,
ql_license_status: data.status || null
}), () => {
if (typeof pkInvalidateAssertCache === "function") pkInvalidateAssertCache();
qlExpiredHandled = false;
setPkCreditBypass(true);
if (log) { log.className = "ql-log-success"; log.innerText = "✓ " + qlUserText(data.message); }
try { if (typeof QLSounds !== "undefined") QLSounds.activation(); } catch (e) { }
setTimeout(() => {
const box = document.getElementById("ql-floating");
if (box) showMainUI(box);
startHeartbeat(key);
}, 800);
});
} else {
if (log) { log.className = "ql-log-error"; log.innerText = "✗ " + qlUserText(data.message); }
}
} catch (err) {
if (log) { log.className = "ql-log-error"; log.innerText = "✗ " + qlUserText(err.message || "Connection error"); }
}
}
function showMainUI(box) {
const greeting = normalizeLicenseUserName(qlUserName);
const statusBadge = qlLicenseStatus === "trial" ? 'Trial Access ' : 'Unlimited Access ';
box.innerHTML = templateMainUI(greeting, statusBadge, qlMinimized);
box.style.height = qlHeight + "px";
setTimeout(() => {
updateSyncStatus();
setupSend();
setupStorageWatch();
setupMinimize();
setupSuggestionChips();
setupWatermarkButton();
updateTrialCountdown();
setupDrag();
setupResize();
setupDarkMode();
setupOptimize();
setupSpeech();
setupNotifications();
setupModoPlano();
setupFileAttachment();
setupShield();
setupTabs();
loadChatHistory();
setupNativeChatButton();
setupClipboardPaste();
setupDownloadProject();
setupCreateProject();
setupPublishProject();
setupEnableCloud();
checkForUpdatePopup();
checkResellerRolePopup();
chrome.storage.local.get(["ql_license_key", "ql_session_id"], (res) => {
if (res.ql_license_key) {
qlSessionId = res.ql_session_id || qlSessionId;
startHeartbeat(res.ql_license_key);
}
});
const sidePanelBtn = document.getElementById("ql-sidepanel-btn");
if (sidePanelBtn) {
sidePanelBtn.addEventListener("click", () => {
const floatingBox = document.getElementById("ql-floating");
if (floatingBox) {
floatingBox.style.transition = "opacity 0.3s ease, transform 0.3s ease";
floatingBox.style.opacity = "0";
floatingBox.style.transform = "translateX(20px) scale(0.95)";
}
safeSendMessage({ action: "activateSidebar" }, (resp) => {
if (resp && resp.ok && !resp.deferred) {
setTimeout(() => {
if (floatingBox) floatingBox.remove();
if (qlHeartbeatInterval) clearInterval(qlHeartbeatInterval);
if (window.qlCountdownInterval) clearInterval(window.qlCountdownInterval);
}, 350);
} else if (resp && resp.deferred) {
if (floatingBox) {
floatingBox.style.opacity = "1";
floatingBox.style.transform = "none";
}
showCustomAlert("Almost there!", resp.message || "Click the extension icon in the top-right corner to open the side panel.");
} else {
if (floatingBox) {
floatingBox.style.opacity = "1";
floatingBox.style.transform = "none";
}
showCustomAlert("Error", "Could not open the side panel. Check whether your browser supports this feature.");
}
});
});
}
const logoutBtn = document.getElementById("ql-logout-btn");
if (logoutBtn) {
logoutBtn.addEventListener("click", () => {
if (qlHeartbeatInterval) clearInterval(qlHeartbeatInterval);
setPkCreditBypass(false);
chrome.storage.local.remove(["ql_license_valid", "ql_license_key", "ql_session_id", "ql_user_name", "ql_expires_at", "ql_activated_at", "ql_license_status"], () => {
qlUserName = null; qlExpiresAt = null; qlActivatedAt = null; qlLicenseStatus = null; qlSessionId = null;
showLicenseGate(box);
});
});
}
}, 30);
}
function showCustomAlert(title, message) {
try {
if (typeof QLSounds !== "undefined" && QLSounds.errorFromMessage) {
var __ttl = (title || "") + " " + (message || "");
if (/erro|falha|negad|inv[áa]lid|expir|limite|payment|rate|token|cr[eé]dito|sess/i.test(__ttl)) {
QLSounds.errorFromMessage(__ttl);
}
}
} catch (__e) { }
const alert = document.getElementById("ql-custom-alert");
if (!alert) return;
const titleEl = alert.querySelector(".ql-alert-title");
const msgEl = alert.querySelector(".ql-alert-message");
const okBtn = alert.querySelector(".ql-alert-ok-btn");
title = qlUserText(title);
message = qlUserText(message);
if (titleEl) titleEl.textContent = title;
if (msgEl) msgEl.textContent = message;
alert.style.display = "flex";
if (okBtn) {
okBtn.onclick = () => { alert.style.display = "none"; };
}
setTimeout(() => { alert.style.display = "none"; }, 4000);
}
function setupOptimize() {
const btn = document.getElementById("ql-optimize-btn");
if (!btn) return;
btn.addEventListener("click", async () => {
const textarea = document.getElementById("ql-msg");
if (!textarea || !textarea.value.trim()) {
showCustomAlert("Attention", "Type a prompt before optimizing.");
return;
}
const original = textarea.value.trim();
btn.classList.add("ql-tool-loading");
btn.disabled = true;
const storageData = await new Promise(r => chrome.storage.local.get(["ql_license_key"], r));
const licenseKey = storageData.ql_license_key || "";
try {
const data = await bgFetch(OPTIMIZE_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"apikey": API_KEY,
"x-license-key": licenseKey
},
body: JSON.stringify({ prompt: original })
});
if (data.optimized_prompt) {
textarea.value = data.optimized_prompt;
showCustomAlert("Prompt Optimized! ✨", "Your prompt was improved with AI and is ready to send.");
} else if (data.error) {
showCustomAlert("Error", data.error);
}
} catch (err) {
console.error("[Optimize] error:", err);
showCustomAlert("Error", "Failed to connect to the optimizer: " + (err.message || ""));
} finally {
btn.classList.remove("ql-tool-loading");
btn.disabled = false;
}
});
}
var _whatsappPopupShown = false;
var QL_JOIN_POPUP_SEEN_KEY = 'ql_join_popup_seen_v3';
// Community popup removed.
function setupWhatsAppPopup() { /* no-op */ }
function setupSpeech() {
const btn = document.getElementById("ql-speech-btn");
if (!btn) return;
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SpeechRecognition) {
btn.title = "Speech is not supported in this browser";
btn.style.opacity = "0.4";
btn.style.cursor = "not-allowed";
return;
}
btn.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
if (qlIsRecording && qlSpeechRecognition) {
qlSpeechRecognition.stop();
return;
}
try {
qlSpeechRecognition = new SpeechRecognition();
qlSpeechRecognition.lang = "en-US";
qlSpeechRecognition.continuous = true;
qlSpeechRecognition.interimResults = true;
qlSpeechRecognition.maxAlternatives = 1;
let finalTranscript = "";
const textarea = document.getElementById("ql-msg");
qlSpeechRecognition.onstart = () => {
qlIsRecording = true;
btn.classList.add("ql-recording");
finalTranscript = textarea ? textarea.value : "";
console.log("[QL Speech] recording started");
};
qlSpeechRecognition.onresult = (event) => {
let interim = "";
for (let i = event.resultIndex; i < event.results.length; i++) {
const transcript = event.results[i][0].transcript;
if (event.results[i].isFinal) {
finalTranscript += transcript + " ";
} else {
interim += transcript;
}
}
if (textarea) textarea.value = finalTranscript + interim;
};
qlSpeechRecognition.onerror = (event) => {
console.warn("[QL Speech] error:", event.error);
qlIsRecording = false;
btn.classList.remove("ql-recording");
if (event.error === "not-allowed") {
showCustomAlert("Permission Denied", "Allow microphone access in your browser settings.");
} else if (event.error === "no-speech") {
showCustomAlert("No Audio", "No speech detected. Try again.");
} else if (event.error !== "aborted") {
showCustomAlert("Voice Error", "Error: " + event.error);
}
};
qlSpeechRecognition.onend = () => {
qlIsRecording = false;
btn.classList.remove("ql-recording");
if (textarea) textarea.value = finalTranscript.trim();
console.log("[QL Speech] recording finished");
};
qlSpeechRecognition.start();
} catch (err) {
console.error("[QL Speech] failed to start:", err);
qlIsRecording = false;
btn.classList.remove("ql-recording");
showCustomAlert("Error", "Could not start voice recognition.");
}
});
}
function setupNotifications() {
const bellBtn = document.querySelector(".ql-notif-btn");
const panel = document.getElementById("ql-notif-panel");
const closeBtn = document.getElementById("ql-notif-close");
if (!bellBtn || !panel) return;
bellBtn.addEventListener("click", (e) => {
e.stopPropagation();
const isOpen = panel.style.display !== "none";
panel.style.display = isOpen ? "none" : "block";
if (!isOpen) loadNotifications();
});
if (closeBtn) {
closeBtn.addEventListener("click", (e) => {
e.stopPropagation();
panel.style.display = "none";
});
}
checkUnreadNotifications();
}
async function loadNotifications() {
const list = document.getElementById("ql-notif-list");
if (!list) return;
list.innerHTML = 'Loading...
';
try {
const data = await bgFetch(NOTIFICATIONS_URL, {
method: "GET",
headers: { "apikey": API_KEY }
});
if (!data || data.length === 0) {
list.innerHTML = 'No notifications.
';
return;
}
const ids = data.map(n => n.id);
chrome.storage.local.set({ ql_read_notifs: ids });
const badge = document.querySelector(".ql-notif-badge");
if (badge) badge.style.display = "none";
list.innerHTML = data.map(n => {
const date = new Date(n.created_at).toLocaleDateString("en-US");
const safeLink = sanitizeUrl(n.link);
const linkHtml = safeLink ? 'Open link → ' : '';
return '' + escapeHtml(n.title) + '
' + escapeHtml(n.message) + '
' + linkHtml + '
' + date + '
';
}).join('');
} catch (err) {
list.innerHTML = 'Error loading.
';
}
}
async function checkUnreadNotifications() {
try {
const data = await bgFetch(NOTIFICATIONS_URL, {
method: "GET",
headers: { "apikey": API_KEY }
});
if (!data || data.length === 0) return;
chrome.storage.local.get(["ql_read_notifs"], (res) => {
const readIds = res.ql_read_notifs || [];
const unread = data.filter(n => !readIds.includes(n.id)).length;
const badge = document.querySelector(".ql-notif-badge");
if (badge) {
if (unread > 0) {
badge.textContent = unread;
badge.style.display = "flex";
} else {
badge.style.display = "none";
}
}
});
} catch (e) { }
}
function setupSuggestionChips() {
const container = document.getElementById("ql-chips");
if (!container) return;
PROMPT_TEMPLATES.forEach((t) => {
const chip = document.createElement("button");
chip.className = "ql-chip";
chip.innerHTML = t.icon + " " + t.label;
chip.title = t.prompt;
chip.addEventListener("click", () => {
const textarea = document.getElementById("ql-msg");
if (textarea) textarea.value = t.prompt;
});
container.appendChild(chip);
});
}
var WATERMARK_PROMPT = "Add this CSS to global styles on every page: #lovable-badge { display: none !important; visibility: hidden !important; pointer-events: none !important; } Completely remove the entire Lovable branding widget — the Made with Lovable text AND the floating close X button. Hide the parent #lovable-badge container, not just the text inside it. No empty box or orphaned X button should remain visible.";
function setupWatermarkButton() {
var btn = document.getElementById("ql-remove-watermark");
if (!btn) return;
btn.addEventListener("click", async function () {
var log = document.getElementById("ql-log");
btn.disabled = true;
btn.textContent = "⏳ Sending...";
try {
await deliverPromptToLovable(WATERMARK_PROMPT);
if (log) { log.className = "ql-log-success"; log.innerText = "✓ Prompt sent! Wait for Lovable to apply the CSS."; }
} catch (err) {
if (log) { log.className = "ql-log-error"; log.innerText = "✗ " + (err.message || err); }
} finally {
btn.disabled = false;
btn.textContent = "Remove Watermark";
}
});
}
function showPublishedUrlModal(url) {
var existing = document.getElementById("ql-publish-modal");
if (existing) existing.remove();
var overlay = document.createElement("div");
overlay.id = "ql-publish-modal";
overlay.className = "pk-publish-overlay pk-publish-overlay-top";
overlay.innerHTML =
'' +
'
🎉
' +
'
Project Published! ' +
'
Your project is live. Open it from the link below:
' +
'
' +
'
' +
'📋 Copy ' +
'🔗 Open ' +
'
' +
'
Close ' +
'
';
document.body.appendChild(overlay);
document.getElementById("ql-publish-copy").addEventListener("click", function () {
navigator.clipboard.writeText(url);
this.textContent = "✓ Copied!";
});
document.getElementById("ql-publish-open").addEventListener("click", function () { window.open(url, "_blank"); });
document.getElementById("ql-publish-close").addEventListener("click", function () { overlay.remove(); });
overlay.addEventListener("click", function (e) { if (e.target === overlay) overlay.remove(); });
}
function setupPublishProject() {
var btn = document.getElementById("ql-publish-project");
if (!btn) return;
btn.addEventListener("click", async function () {
var log = document.getElementById("ql-log");
btn.disabled = true;
btn.textContent = "⏳ Publishing...";
await requestLatestTokenFromHook(2500);
var storageData = await new Promise(function (resolve) {
chrome.storage.local.get(["lovable_projectId", "lovable_token", "ql_license_key"], resolve);
});
var projectId = storageData.lovable_projectId || "";
var token = storageData.lovable_token || "";
var licenseKey = storageData.ql_license_key || "";
if (!projectId || !token) {
if (log) { log.className = "ql-log-error"; log.innerText = "⚠ Project not synced."; }
btn.disabled = false;
btn.textContent = "🌐 Publish Project";
return;
}
if (token.startsWith("Bearer ")) token = token.slice(7);
try {
var publishPayload = pkFeatureRequestBody(licenseKey, token, projectId);
var result = await bgFetch(PUBLISH_PROJECT_URL, {
method: "POST",
headers: pkFeatureApiHeaders(),
body: JSON.stringify(publishPayload),
featureUiCompat: true
});
if (result && result.success === false) {
throw new Error(result.error_display || result.message || "Publish error");
}
if (log) { log.className = "ql-log-success"; log.innerText = "✓ Project published!"; }
if (result && result.url) showPublishedUrlModal(result.url);
} catch (err) {
if (log) { log.className = "ql-log-error"; log.innerText = "✗ " + (err.message || err); }
} finally {
btn.disabled = false;
btn.textContent = "🌐 Publish Project";
}
});
}
function setupEnableCloud() {
var btn = document.getElementById("ql-enable-cloud");
if (!btn) return;
btn.addEventListener("click", async function () {
var log = document.getElementById("ql-log");
btn.disabled = true;
btn.textContent = "⏳ Activating Cloud...";
await requestLatestTokenFromHook(2500);
var storageData = await new Promise(function (resolve) {
chrome.storage.local.get(["lovable_projectId", "lovable_token", "ql_license_key"], resolve);
});
var projectId = storageData.lovable_projectId || "";
var token = storageData.lovable_token || "";
var licenseKey = storageData.ql_license_key || "";
if (!projectId || !token) {
if (log) { log.className = "ql-log-error"; log.innerText = "⚠ Project not synced."; }
btn.disabled = false;
btn.textContent = "☁️ Enable Lovable Cloud";
return;
}
if (token.startsWith("Bearer ")) token = token.slice(7);
try {
var cloudPayload = pkFeatureRequestBody(licenseKey, token, projectId, { region: "america" });
var result = await bgFetch(ENABLE_CLOUD_URL, {
method: "POST",
headers: pkFeatureApiHeaders(),
body: JSON.stringify(cloudPayload),
featureUiCompat: true
});
if (result && result.success === false) {
throw new Error(result.error_display || result.message || "Cloud activation error");
}
if (log) { log.className = "ql-log-success"; log.innerText = "✓ " + (result && result.message ? result.message : "Lovable Cloud activated!"); }
} catch (err) {
if (log) { log.className = "ql-log-error"; log.innerText = "✗ " + (err.message || err); }
} finally {
btn.disabled = false;
btn.textContent = "☁️ Enable Lovable Cloud";
}
});
}
function qlApplyLicenseApiData(data) {
if (!data) return;
if (typeof pkResolveLicenseStatus === "function") {
qlLicenseStatus = pkResolveLicenseStatus(data);
} else {
qlLicenseStatus = data.status || qlLicenseStatus;
}
if (Object.prototype.hasOwnProperty.call(data, "expires_at")) {
qlExpiresAt = data.expires_at || null;
}
if (Object.prototype.hasOwnProperty.call(data, "activated_at")) {
qlActivatedAt = data.activated_at || null;
}
if (Object.prototype.hasOwnProperty.call(data, "validity_minutes")) {
qlValidityMinutes = data.validity_minutes != null ? data.validity_minutes : null;
}
}
function qlRevokeAndShowLicenseGate(message) {
if (typeof pkInvalidateAssertCache === "function") pkInvalidateAssertCache();
setPkCreditBypass(false);
var after = function () {
const box = document.getElementById("ql-floating");
if (box) showLicenseGate(box);
if (message) setTimeout(function () { showCustomAlert("Access Denied", message); }, 400);
};
if (typeof pkRevokeLicenseStorage === "function") {
pkRevokeLicenseStorage().then(after);
} else {
chrome.storage.local.remove(["ql_license_valid", "ql_license_key", "ql_session_id", "ql_user_name", "ql_expires_at", "ql_activated_at", "ql_license_status", "ql_validity_minutes"], after);
}
}
function qlHandleLicenseInvalid(data) {
var reason = data && data.reason;
if (reason === "expired") {
handleLicenseExpired();
return;
}
qlRevokeAndShowLicenseGate((data && data.message) || "License not active.");
}
function updateTrialCountdown() {
if (INTERNAL_LICENSE_MODE) return;
const el = document.getElementById("ql-trial-countdown");
if (!el) return;
if (!qlExpiresAt) {
if (qlValidityMinutes) {
el.style.display = "block";
el.innerHTML = '⏳ Trial ready: ' + qlValidityMinutes + ' min after activation
';
} else {
el.style.display = "none";
el.innerHTML = "";
}
return;
}
var expiresMs = typeof pkParseUtcExpiry === "function" ? pkParseUtcExpiry(qlExpiresAt) : new Date(qlExpiresAt).getTime();
if (expiresMs == null || isNaN(expiresMs)) {
el.style.display = "none";
return;
}
var startMs = typeof pkParseUtcExpiry === "function" ? pkParseUtcExpiry(qlActivatedAt) : (qlActivatedAt ? new Date(qlActivatedAt).getTime() : null);
if (startMs == null || isNaN(startMs)) startMs = expiresMs - 3600000;
var totalDuration = Math.max(expiresMs - startMs, 60000);
el.style.display = "block";
function update() {
const remaining = expiresMs - Date.now();
if (remaining <= 0) {
if (!qlExpiryConfirming && typeof pkEnsureActiveLicense === "function") {
qlExpiryConfirming = true;
pkEnsureActiveLicense(true).then(function (resp) {
qlExpiryConfirming = false;
if (resp && resp.expires_at) {
qlExpiresAt = resp.expires_at;
updateTrialCountdown();
return;
}
handleLicenseExpired();
}).catch(function () {
qlExpiryConfirming = false;
handleLicenseExpired();
});
return;
}
if (!qlExpiryConfirming) handleLicenseExpired();
return;
}
const days = Math.floor(remaining / 86400000);
const hrs = Math.floor((remaining % 86400000) / 3600000);
const mins = Math.floor((remaining % 3600000) / 60000);
const secs = Math.floor((remaining % 60000) / 1000);
const pct = Math.max(0, Math.min(100, (remaining / totalDuration) * 100));
let timeStr = '';
if (days > 0) timeStr = days + 'd ' + hrs + 'h ' + mins + 'm';
else if (hrs > 0) timeStr = hrs + 'h ' + mins + 'm ' + String(secs).padStart(2, '0') + 's';
else timeStr = mins + ':' + String(secs).padStart(2, '0');
const urgentClass = pct < 20 ? ' ql-bar-urgent' : '';
const label = qlLicenseStatus === 'trial' ? 'Trial expires in' : 'License expires in';
el.innerHTML = '⏳ ' + label + ' ' + timeStr + '
';
}
update();
if (window.qlCountdownInterval) clearInterval(window.qlCountdownInterval);
window.qlCountdownInterval = setInterval(update, 1000);
}
function setupMinimize() {
const btn = document.getElementById("ql-minimize");
if (!btn) return;
btn.addEventListener("click", (e) => {
e.stopPropagation();
const box = document.getElementById("ql-floating");
if (!box) return;
qlMinimized = !qlMinimized;
box.classList.toggle("ql-minimized", qlMinimized);
btn.textContent = qlMinimized ? "□" : "−";
chrome.storage.local.set({ ql_minimized: qlMinimized });
});
}
function setupDarkMode() {
const moonBtn = document.querySelector('.ql-icon-btn[title="Theme"]');
if (!moonBtn) return;
moonBtn.addEventListener("click", (e) => {
e.stopPropagation();
const box = document.getElementById("ql-floating");
if (!box) return;
const isLight = box.classList.toggle("ql-light");
chrome.storage.local.set({ ql_dark_mode: !isLight });
});
}
function setupModoPlano() {
const toggle = document.getElementById("ql-modo-plano");
if (!toggle) return;
migratePlanModeStorageKeys(function (on) {
toggle.checked = on;
});
toggle.addEventListener("change", () => {
writePlanModeToStorage(toggle.checked);
if (toggle.checked) showModoPlanoAlert();
});
}
function showModoPlanoAlert() {
const existing = document.querySelector(".ql-modo-plano-overlay");
if (existing) existing.remove();
const overlay = document.createElement("div");
overlay.className = "ql-modo-plano-overlay";
overlay.innerHTML = '' +
'
⚠️
' +
'
Attention — Plan Mode
' +
'
' +
'Plan Mode (Think mode in Lovable) may use credits while planning. Use in moderation, then send builds through the extension with Plan Mode off.' +
'
' +
'
' +
'
1 Enable Plan Mode and send your prompt through the extension.
' +
'
2 Lovable will generate a plan. Do not click Approve in Lovable.
' +
'
3 Copy the plan and paste it into the extension prompt.
' +
'
4 Turn off Plan Mode and send through the extension. No extra credits.
' +
'
' +
'
' +
' ' +
'Do not show again ' +
'
' +
'
Got it! ' +
"
";
const box = document.getElementById("ql-floating");
if (box) box.appendChild(overlay);
else document.body.appendChild(overlay);
requestAnimationFrame(() => overlay.classList.add("ql-modo-plano-visible"));
const close = () => {
overlay.classList.remove("ql-modo-plano-visible");
setTimeout(() => overlay.remove(), 180);
};
const okBtn = overlay.querySelector("#ql-modo-plano-ok");
if (okBtn) {
okBtn.addEventListener("click", () => {
const dismiss = overlay.querySelector("#ql-modo-plano-dismiss");
if (dismiss && dismiss.checked) {
chrome.storage.local.set({ ql_modo_plano_alert_dismissed: true });
}
close();
});
}
overlay.addEventListener("click", (e) => {
if (e.target === overlay) close();
});
}
function setupShield() {
const btn = document.getElementById("ql-shield-btn");
if (!btn) return;
chrome.storage.local.get(["ql_shield_active"], (res) => {
if (res.ql_shield_active === true) {
qlShieldActive = true;
btn.classList.add("ql-shield-active");
const label = document.getElementById("ql-shield-label");
if (label) label.textContent = "Disable Shield";
injectShieldOverlay();
}
});
btn.addEventListener("click", () => {
qlShieldActive = !qlShieldActive;
chrome.storage.local.set({ ql_shield_active: qlShieldActive });
const label = document.getElementById("ql-shield-label");
if (qlShieldActive) {
btn.classList.add("ql-shield-active");
if (label) label.textContent = "Disable Shield";
injectShieldOverlay();
showCustomAlert("Shield Enabled 🛡️", "The Lovable input is locked. Use the extension to send prompts.");
} else {
btn.classList.remove("ql-shield-active");
if (label) label.textContent = "Enable Shield";
removeShieldOverlay();
showCustomAlert("Shield Disabled", "The Lovable input is unlocked again.");
}
});
}
function injectShieldOverlay() {
if (document.getElementById("ql-shield-overlay")) return;
const chatForm = document.querySelector('form#chat-input');
if (!chatForm) {
setTimeout(injectShieldOverlay, 1000);
return;
}
const existingPos = getComputedStyle(chatForm).position;
if (existingPos === 'static') {
chatForm.style.position = 'relative';
}
const overlay = document.createElement('div');
overlay.id = 'ql-shield-overlay';
overlay.className = 'ql-shield-overlay';
overlay.innerHTML = '' +
' ' +
' ' +
'🛡️ Protected by Saqlain\'s Tech World ' +
'Use the extension to send prompts ';
overlay.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
}, true);
overlay.addEventListener('mousedown', (e) => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
}, true);
overlay.addEventListener('keydown', (e) => {
e.preventDefault();
e.stopPropagation();
}, true);
chatForm.appendChild(overlay);
const inputs = chatForm.querySelectorAll('input, button, textarea, [contenteditable]');
inputs.forEach(el => {
if (el.id !== 'ql-shield-overlay') {
el.dataset.qlShieldDisabled = el.disabled || '';
el.dataset.qlShieldTabindex = el.getAttribute('tabindex') || '';
el.setAttribute('tabindex', '-1');
if (el.tagName !== 'DIV') el.disabled = true;
if (el.contentEditable === 'true') {
el.contentEditable = 'false';
el.dataset.qlShieldEditable = 'true';
}
}
});
}
function removeShieldOverlay() {
const overlay = document.getElementById('ql-shield-overlay');
if (overlay) overlay.remove();
const chatForm = document.querySelector('form#chat-input');
if (!chatForm) return;
const inputs = chatForm.querySelectorAll('[data-ql-shield-disabled]');
inputs.forEach(el => {
const wasDis = el.dataset.qlShieldDisabled;
if (wasDis === 'true') el.disabled = true;
else if (wasDis === '' || wasDis === 'false') el.disabled = false;
delete el.dataset.qlShieldDisabled;
const oldTab = el.dataset.qlShieldTabindex;
if (oldTab) el.setAttribute('tabindex', oldTab);
else el.removeAttribute('tabindex');
delete el.dataset.qlShieldTabindex;
if (el.dataset.qlShieldEditable === 'true') {
el.contentEditable = 'true';
delete el.dataset.qlShieldEditable;
}
});
}
let qlHbConflictCount = 0;
function startHeartbeat(licenseKey) {
if (INTERNAL_LICENSE_MODE) return;
if (qlHeartbeatInterval) clearInterval(qlHeartbeatInterval);
qlHbConflictCount = 0;
qlHeartbeatInterval = setInterval(async () => {
try {
const data = await bgFetch(VALIDATE_URL, {
method: "POST",
headers: apiHeaders({ "Content-Type": "application/json" }),
body: JSON.stringify({ license_key: licenseKey, session_id: qlSessionId, heartbeat: true, device_id: qlDeviceId, max_devices: 2, device_limit: 2, allowed_devices: 2 })
});
if (!data.valid) {
var decision = typeof pkShouldLockoutFromValidation === "function"
? pkShouldLockoutFromValidation(data, qlHbConflictCount)
: { lock: true, conflictCount: qlHbConflictCount, message: data.message };
qlHbConflictCount = decision.conflictCount;
if (decision.lock) {
clearInterval(qlHeartbeatInterval);
if (typeof pkInvalidateAssertCache === "function") pkInvalidateAssertCache();
qlHandleLicenseInvalid({ reason: data.reason || decision.reason, message: decision.message || data.message });
}
return;
}
qlHbConflictCount = 0;
if (data.user_name) qlUserName = normalizeLicenseUserName(data.user_name);
qlApplyLicenseApiData(data);
qlOnlineCount = data.online_count || 0;
chrome.storage.local.set(Object.assign({ ql_user_name: qlUserName }, typeof pkLicenseStoragePatch === "function" ? pkLicenseStoragePatch(data) : {}));
const countEl = document.getElementById("ql-online-count");
if (countEl) countEl.textContent = qlOnlineCount;
const nameEl = document.querySelector(".ql-profile-name");
if (nameEl && data.user_name) nameEl.textContent = qlUserName;
updateTrialCountdown();
} catch (err) {
console.warn("[QL] Heartbeat error", err);
}
}, 60000);
}
let qlExpiredHandled = false;
function handleLicenseExpired() {
if (INTERNAL_LICENSE_MODE) return;
if (qlExpiredHandled) return;
qlExpiredHandled = true;
if (typeof pkInvalidateAssertCache === "function") pkInvalidateAssertCache();
if (qlHeartbeatInterval) clearInterval(qlHeartbeatInterval);
if (window.qlCountdownInterval) clearInterval(window.qlCountdownInterval);
const overlay = document.createElement("div");
overlay.className = "ql-sweetalert-overlay";
overlay.innerHTML = templateExpiredOverlay();
const box = document.getElementById("ql-floating");
if (box) box.appendChild(overlay);
requestAnimationFrame(() => overlay.classList.add("ql-sweetalert-visible"));
const renewBtn = overlay.querySelector("#ql-sweetalert-renew");
if (renewBtn) {
renewBtn.addEventListener("click", () => {
overlay.remove();
if (box) showPaymentUI(box);
});
}
const closeBtn = overlay.querySelector("#ql-sweetalert-close");
if (closeBtn) {
closeBtn.addEventListener("click", () => {
overlay.classList.remove("ql-sweetalert-visible");
setTimeout(() => {
overlay.remove();
setPkCreditBypass(false);
chrome.storage.local.remove(["ql_license_valid", "ql_license_key", "ql_session_id", "ql_user_name", "ql_expires_at", "ql_activated_at", "ql_license_status", "ql_validity_minutes"], () => {
if (box) showLicenseGate(box);
});
}, 300);
});
}
}
async function showPaymentUI(box, preselectedPkg) {
if (preselectedPkg) {
showCheckoutScreen(box, preselectedPkg);
return;
}
box.innerHTML = templatePaymentUI(qlMinimized);
setupMinimize();
setupDrag();
setupResize();
// BRL plans -> Channel redirect
document.querySelectorAll(".ql-brl-buy").forEach(function (btn) {
btn.addEventListener("click", function () {
var card = btn.closest(".ql-pkg-brl");
if (!card) return;
var idx = parseInt(card.getAttribute("data-brl-idx"), 10) || 0;
var plan = QL_BRL_PLANS[idx];
if (!plan) return;
var msg = "Hello! 👋 I am interested in the *" + plan.name + "* plan for Saqlain\'s Tech World (R$ " + plan.price + " - " + plan.period + ").\n\nOpen channel for more information.";
var url = DISCORD_URL;
window.open(url, "_blank", "noopener,noreferrer");
});
});
const backBtn = document.getElementById("ql-pay-back");
if (backBtn) {
backBtn.addEventListener("click", () => {
chrome.storage.local.get(["ql_license_valid"], (res) => {
if (res.ql_license_valid) showMainUI(box);
else showLicenseGate(box);
});
});
}
try {
const packages = await bgFetch(PACKAGES_URL, {
method: "GET",
headers: { "apikey": API_KEY }
});
const list = document.getElementById("ql-packages-list");
if (!list) return;
if (!packages || !Array.isArray(packages) || packages.length === 0) {
list.innerHTML = 'Open Official Channel
';
return;
}
list.innerHTML = packages.map(pkg => templatePackageCard(pkg)).join('');
list.querySelectorAll(".ql-pkg-card").forEach(card => {
card.querySelector(".ql-pkg-select-btn").addEventListener("click", () => {
const pkg = {
id: card.getAttribute("data-pkg-id"),
name: card.getAttribute("data-pkg-name"),
price: card.getAttribute("data-pkg-price")
};
showCheckoutScreen(box, pkg);
});
});
} catch (err) {
console.error("[QL] Package load error:", err);
const list = document.getElementById("ql-packages-list");
if (list) list.innerHTML = 'Open Official Channel
';
}
}
function showCheckoutScreen(box, pkg) {
box.innerHTML = templateCheckoutScreen(pkg, qlMinimized);
setupMinimize();
setupDrag();
setupResize();
let selectedMethod = "mpesa";
const backBtn = document.getElementById("ql-checkout-back");
if (backBtn) {
backBtn.addEventListener("click", () => showPaymentUI(box));
}
document.querySelectorAll(".ql-method-btn").forEach(btn => {
btn.addEventListener("click", () => {
document.querySelectorAll(".ql-method-btn").forEach(b => b.classList.remove("ql-method-active"));
btn.classList.add("ql-method-active");
selectedMethod = btn.getAttribute("data-method");
const hint = document.getElementById("ql-phone-hint");
if (hint) hint.textContent = selectedMethod === "mpesa" ? "M-Pesa: 84 or 85" : "e-Mola: 86 or 87";
});
});
const confirmBtn = document.getElementById("ql-confirm-pay");
if (confirmBtn) {
confirmBtn.addEventListener("click", async () => {
const phoneInput = document.getElementById("ql-pay-phone");
const phone = phoneInput && phoneInput.value ? phoneInput.value.replace(/\D/g, "") : "";
const log = document.getElementById("ql-pay-log");
if (!phoneInput) {
window.open(DISCORD_URL, "_blank", "noopener,noreferrer");
if (log) { log.className = "ql-pay-log ql-pay-info"; log.textContent = "Opening official channel..."; }
return;
}
if (phone.length !== 9) {
if (log) { log.className = "ql-pay-log ql-pay-error"; log.textContent = "The number must have 9 digits."; }
return;
}
const prefix = phone.substring(0, 2);
if (selectedMethod === "mpesa" && !["84", "85"].includes(prefix)) {
if (log) { log.className = "ql-pay-log ql-pay-error"; log.textContent = "M-Pesa: use 84 or 85."; }
return;
}
if (selectedMethod === "emola" && !["86", "87"].includes(prefix)) {
if (log) { log.className = "ql-pay-log ql-pay-error"; log.textContent = "e-Mola: use 86 or 87."; }
return;
}
confirmBtn.disabled = true;
confirmBtn.textContent = "⏳ Processing...";
if (log) { log.className = "ql-pay-log ql-pay-info"; log.textContent = "Sending payment request..."; }
try {
const storageData = await new Promise(r => chrome.storage.local.get(["ql_license_key"], r));
const licenseKey = storageData.ql_license_key || "";
const result = await bgFetch(EXT_PAYMENT_URL, {
method: "POST",
headers: { "Content-Type": "application/json", "apikey": API_KEY },
body: JSON.stringify({
packageId: pkg.id,
numero: phone,
metodo: selectedMethod,
license_key: licenseKey || undefined
})
});
if (result && result.status === "sucesso") {
const bodyEl = document.getElementById("ql-body");
if (bodyEl) {
bodyEl.innerHTML = templatePaymentSuccess(result.license_key);
const copyBtn = document.getElementById("ql-copy-key");
if (copyBtn) {
copyBtn.addEventListener("click", () => {
navigator.clipboard.writeText(result.license_key).then(() => {
copyBtn.textContent = "✅ Copied!";
setTimeout(() => { copyBtn.textContent = "📋 Copy Key"; }, 2000);
}).catch(() => {
const keyEl = document.getElementById("ql-new-key");
if (keyEl) { const r = document.createRange(); r.selectNodeContents(keyEl); window.getSelection().removeAllRanges(); window.getSelection().addRange(r); }
copyBtn.textContent = "Selected — Ctrl+C";
});
});
}
const activateBtn = document.getElementById("ql-activate-key");
if (activateBtn) {
activateBtn.addEventListener("click", () => {
chrome.storage.local.set({
ql_license_valid: true,
ql_license_key: result.license_key,
ql_expires_at: result.expires_at || null,
ql_license_status: "active",
ql_session_id: null
}, () => {
qlExpiresAt = result.expires_at || null;
qlLicenseStatus = "active";
qlExpiredHandled = false;
showMainUI(box);
startHeartbeat(result.license_key);
});
});
}
}
} else {
const errMsg = (result && result.error) ? result.error : "Official Channel failed. Please try again.";
if (log) { log.className = "ql-pay-log ql-pay-error"; log.textContent = "✗ " + errMsg; }
confirmBtn.disabled = false;
confirmBtn.textContent = "Open Official Channel";
}
} catch (err) {
if (log) { log.className = "ql-pay-log ql-pay-error"; log.textContent = "✗ " + (err.message || "Connection error."); }
confirmBtn.disabled = false;
confirmBtn.textContent = "Open Official Channel";
}
});
}
}
// Side-panel-only: keep pageHook + helpers; no floating bubble bootstrap
if (typeof SIDE_PANEL_ONLY !== "undefined" && SIDE_PANEL_ONLY) {
try { chrome.storage.local.set({ ql_sidebar_mode: true }); } catch (e) { }
} else {
function qlBootstrap() {
if (document.getElementById("ql-floating")) return;
if (!document.body) {
var bodyWait = new MutationObserver(function () {
if (document.body) {
bodyWait.disconnect();
qlBootstrap();
}
});
bodyWait.observe(document.documentElement, { childList: true });
return;
}
createUI();
}
if (document.readyState === "complete" || document.readyState === "interactive") {
setTimeout(qlBootstrap, 50);
} else {
document.addEventListener("DOMContentLoaded", function () { setTimeout(qlBootstrap, 50); });
}
var qlRetryCount = 0;
var qlRetryDelays = [300, 600, 1000, 1500, 2000, 3000, 4000, 5000];
function qlRetryInit() {
if (typeof SIDE_PANEL_ONLY !== "undefined" && SIDE_PANEL_ONLY) return;
if (document.getElementById("ql-floating") || qlRetryCount >= qlRetryDelays.length) return;
var delay = qlRetryDelays[qlRetryCount];
qlRetryCount++;
setTimeout(function () {
if (!document.getElementById("ql-floating") && document.body) {
createUI();
}
qlRetryInit();
}, delay);
}
qlRetryInit();
chrome.storage.onChanged.addListener((changes, area) => {
if (area !== "local") return;
if (typeof SIDE_PANEL_ONLY !== "undefined" && SIDE_PANEL_ONLY) return;
if (changes.ql_sidebar_mode) {
if (changes.ql_sidebar_mode.newValue === true) {
const floatingBox = document.getElementById("ql-floating");
if (floatingBox) {
floatingBox.style.transition = "opacity 0.3s ease, transform 0.3s ease";
floatingBox.style.opacity = "0";
floatingBox.style.transform = "scale(0.95)";
setTimeout(() => floatingBox.remove(), 350);
}
} else if (changes.ql_sidebar_mode.newValue === false) {
setTimeout(() => {
_buildFloatingUI();
setTimeout(() => {
const floatingBox = document.getElementById("ql-floating");
if (floatingBox) {
floatingBox.style.opacity = "0";
floatingBox.style.transform = "scale(0.95) translateX(20px)";
requestAnimationFrame(() => {
floatingBox.style.transition = "opacity 0.4s ease, transform 0.4s ease";
floatingBox.style.opacity = "1";
floatingBox.style.transform = "scale(1) translateX(0)";
});
}
}, 50);
}, 100);
}
}
});
}
function updateSyncStatus() {
safeSendMessage({
action: "syncLovableAuth",
tabUrl: location.href,
projectId: projectIdFromPage()
}, function () {
try { window.postMessage({ type: "lovableRequestToken" }, "*"); } catch (e) { }
chrome.storage.local.get(["lovable_projectId", "lovable_token"], (res) => {
const status = document.getElementById("ql-sync-status");
if (!status) return;
var token = res.lovable_token || '';
if (res.lovable_projectId && token && isTokenFresh(token)) {
status.className = "ql-sync-status ql-sync-ok";
const pid = res.lovable_projectId.substring(0, 6);
status.innerHTML = '✅ Synced! Project: ' + pid + '... ';
} else if (res.lovable_projectId && token) {
status.className = "ql-sync-status ql-sync-waiting";
status.innerHTML = '⚠ Log in on lovable.dev ';
} else {
status.className = "ql-sync-status ql-sync-waiting";
status.innerHTML = '⏳ Waiting for sync... ';
}
});
});
}
function setupStorageWatch() {
chrome.storage.onChanged.addListener((changes) => {
if (changes.lovable_projectId || changes.lovable_token) {
updateSyncStatus();
}
});
}
function requestLatestTokenFromHook(timeoutMs = 1200) {
return new Promise((resolve) => {
let finished = false;
function finish(updated) {
if (finished) return;
finished = true;
clearTimeout(timer);
chrome.storage.onChanged.removeListener(onStorageChange);
resolve(updated);
}
function onStorageChange(changes, area) {
if (area !== "local") return;
if (changes.lovable_token && changes.lovable_token.newValue) {
finish(true);
}
}
const timer = setTimeout(() => finish(false), Math.max(300, timeoutMs));
chrome.storage.onChanged.addListener(onStorageChange);
try {
window.postMessage({ type: "lovableRequestToken" }, "*");
setTimeout(() => window.postMessage({ type: "lovableRequestToken" }, "*"), 120);
} catch (e) {
finish(false);
}
});
}
// ===== CHAT HISTORY SYSTEM (Floating Popup) =====
function loadChatHistory(cb) {
chrome.storage.local.get([QL_HISTORY_KEY], (res) => {
qlChatHistory = res[QL_HISTORY_KEY] || [];
updateHistoryBadge();
if (cb) cb();
});
}
function saveChatHistory() {
if (qlChatHistory.length > QL_MAX_HISTORY) qlChatHistory = qlChatHistory.slice(-QL_MAX_HISTORY);
chrome.storage.local.set({ [QL_HISTORY_KEY]: qlChatHistory });
}
function addToChatHistory(text, status) {
qlChatHistory.push({ text: text, timestamp: new Date().toISOString(), status: status || 'ok' });
saveChatHistory();
updateHistoryBadge();
}
function updateHistoryBadge() {
const badge = document.getElementById('ql-history-badge');
if (!badge) return;
if (qlChatHistory.length > 0) {
badge.textContent = qlChatHistory.length;
badge.style.display = 'inline-flex';
} else {
badge.style.display = 'none';
}
}
function formatChatDate(dateStr) {
var d = new Date(dateStr);
var now = new Date();
var today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
var msgDay = new Date(d.getFullYear(), d.getMonth(), d.getDate());
var diff = (today - msgDay) / 86400000;
if (diff === 0) return 'Today';
if (diff === 1) return 'Yesterday';
if (diff < 7) return ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][d.getDay()];
return d.toLocaleDateString('en-US');
}
function formatChatTime(dateStr) {
var d = new Date(dateStr);
return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
}
function renderHistoryView() {
const container = document.getElementById('ql-tab-content');
if (!container) return;
if (!qlChatHistory.length) {
container.innerHTML = '💬
No messages
Your sent prompts will appear here.
';
return;
}
let html = '';
let lastDate = '';
for (let i = 0; i < qlChatHistory.length; i++) {
const m = qlChatHistory[i];
const dateLabel = formatChatDate(m.timestamp);
if (dateLabel !== lastDate) {
html += '
' + dateLabel + '
';
lastDate = dateLabel;
}
const statusClass = m.status === 'error' ? 'ql-chat-status-err' : 'ql-chat-status-ok';
const statusText = m.status === 'error' ? '✗ Error' : '✓ Sent';
const truncated = m.text.length > 300 ? escapeHtml(m.text.substring(0, 300)) + '…' : escapeHtml(m.text);
html += '
' + truncated +
'
' + statusText + ' ' + formatChatTime(m.timestamp) + '
';
}
html += '
';
html += '' + qlChatHistory.length + ' message' + (qlChatHistory.length === 1 ? '' : 's') + ' 🗑 Clear
';
container.innerHTML = html;
const msgs = container.querySelector('.ql-chat-messages');
if (msgs) msgs.scrollTop = msgs.scrollHeight;
const clearBtn = document.getElementById('ql-chat-clear');
if (clearBtn) {
clearBtn.addEventListener('click', () => {
qlChatHistory = [];
saveChatHistory();
updateHistoryBadge();
renderHistoryView();
});
}
}
function renderPromptView() {
const container = document.getElementById('ql-tab-content');
if (!container) return;
container.innerHTML =
'' +
'
' +
'' +
'
' +
' ' +
'Plan ' +
'
' +
'
' +
'📎 ' +
'' + SVG_ICONS.sparkles + ' ' +
'' + SVG_ICONS.mic + ' ' +
'
' +
'
' +
'Send ' +
'
' +
'
' +
' ' +
'
' +
'' +
'
QUICK SHORTCUTS ' +
'
' +
'
' +
'Remove Watermark ' +
'' +
'Enable Shield ' +
' ' +
'' +
SVG_ICONS.msgSquare + ' Use Native Chat' +
' ' +
'Download Source Code ' +
'Create New Project ' +
'🌐 Publish Project ' +
'☁️ Enable Lovable Cloud ' +
'
';
// Re-setup all prompt tab features
setupSend();
setupSuggestionChips();
setupWatermarkButton();
setupOptimize();
setupSpeech();
setupModoPlano();
setupFileAttachment();
setupShield();
setupNativeChatButton();
setupClipboardPaste();
setupDownloadProject();
setupCreateProject();
setupPublishProject();
setupEnableCloud();
}
function setupTabs() {
const tabs = document.querySelectorAll('.ql-tab');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
const target = tab.getAttribute('data-tab');
qlActiveTab = target;
document.querySelectorAll('.ql-tab').forEach(t => t.classList.toggle('ql-tab-active', t.getAttribute('data-tab') === target));
if (target === 'history') {
loadChatHistory(() => renderHistoryView());
} else {
renderPromptView();
}
});
});
}
// ===== FILE ATTACHMENT SYSTEM =====
const MAX_FILES = 10;
const MAX_FILE_SIZE = 20 * 1024 * 1024;
let qlAttachedFiles = [];
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
function isImageType(type) {
return ['image/png', 'image/jpeg', 'image/webp'].includes(type);
}
async function compressImage(file) {
return new Promise((resolve) => {
const img = new Image();
const url = URL.createObjectURL(file);
img.onload = () => {
URL.revokeObjectURL(url);
const MAX_DIM = 1280;
let w = img.width, h = img.height;
if (w > MAX_DIM || h > MAX_DIM) {
const ratio = Math.min(MAX_DIM / w, MAX_DIM / h);
w = Math.round(w * ratio);
h = Math.round(h * ratio);
}
const canvas = document.createElement('canvas');
canvas.width = w; canvas.height = h;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, w, h);
const outputType = file.type === 'image/png' ? 'image/png' : 'image/jpeg';
const quality = file.type === 'image/png' ? undefined : 0.8;
canvas.toBlob((blob) => {
if (!blob) return resolve({ file, previewUrl: null });
const compressed = new File([blob], file.name, { type: outputType });
const previewUrl = URL.createObjectURL(blob);
resolve({ file: compressed, previewUrl });
}, outputType, quality);
};
img.onerror = () => { URL.revokeObjectURL(url); resolve({ file, previewUrl: null }); };
img.src = url;
});
}
function decodeJwtUserId(token) {
const payload = decodeJwtPayload(token);
if (!payload || typeof payload !== 'object') return null;
return payload.sub || payload.user_id || null;
}
async function uploadFileDirect(file, token) {
// NEW FLOW: direct upload to our public Supabase bucket
// (prompt-images). Returns the public URL to attach to the prompt.
const fileId = crypto.randomUUID();
const inferContentType = (f) => {
if (f && typeof f.type === 'string' && f.type.trim()) return f.type;
const name = (f && f.name ? f.name : '').toLowerCase();
const ext = name.includes('.') ? name.split('.').pop() : '';
const map = {
png: 'image/png',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
webp: 'image/webp',
gif: 'image/gif'
};
return map[ext] || 'application/octet-stream';
};
const buildObjectKey = (id, f) => {
const rawName = f && f.name ? String(f.name) : '';
const ext = rawName.includes('.') ? rawName.split('.').pop().toLowerCase() : '';
const safeExt = ext && /^[a-z0-9]{1,10}$/.test(ext) ? ext : 'png';
const ts = Date.now();
return 'uploads/' + ts + '-' + id + '.' + safeExt;
};
const contentType = inferContentType(file);
const objectKey = buildObjectKey(fileId, file);
const uploadUrl = API_BASE + '/storage/v1/object/prompt-images/' + objectKey;
const licenseHdrs = typeof pkLicenseUploadHeaders === "function"
? await pkLicenseUploadHeaders()
: {};
await new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open('POST', uploadUrl, true);
xhr.setRequestHeader('Content-Type', contentType);
xhr.setRequestHeader('apikey', API_KEY);
xhr.setRequestHeader('Authorization', 'Bearer ' + API_KEY);
xhr.setRequestHeader('x-upsert', 'true');
if (licenseHdrs['x-license-key']) xhr.setRequestHeader('x-license-key', licenseHdrs['x-license-key']);
if (licenseHdrs['x-session-id']) xhr.setRequestHeader('x-session-id', licenseHdrs['x-session-id']);
if (licenseHdrs['x-device-id']) xhr.setRequestHeader('x-device-id', licenseHdrs['x-device-id']);
xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 300) resolve(true);
else reject(new Error('Upload failed: ' + xhr.status + ' ' + (xhr.responseText || '')));
};
xhr.onerror = function () { reject(new Error('Network error during upload')); };
xhr.send(file);
});
var publicUrl = API_BASE + '/storage/v1/object/public/prompt-images/' + objectKey;
return { file_id: objectKey, file_name: file.name || 'file', public_url: publicUrl };
}
function renderAttachPreview() {
const container = document.getElementById('ql-attach-preview');
if (!container) return;
if (qlAttachedFiles.length === 0) {
container.style.display = 'none';
container.innerHTML = '';
return;
}
container.style.display = 'flex';
container.innerHTML = qlAttachedFiles.map((f, i) => {
const thumbHtml = f.previewUrl
? ' '
: '📄
';
const uploadingClass = f.uploading ? ' ql-attach-uploading' : '';
return '' +
thumbHtml +
'
' + escapeHtml(f.file_name) + ' ' + escapeHtml(f.sizeLabel) + '
' +
'
✕ ' +
'
';
}).join('');
container.querySelectorAll('.ql-attach-remove').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const idx = parseInt(btn.getAttribute('data-idx'));
if (qlAttachedFiles[idx] && qlAttachedFiles[idx].previewUrl) {
URL.revokeObjectURL(qlAttachedFiles[idx].previewUrl);
}
qlAttachedFiles.splice(idx, 1);
renderAttachPreview();
});
});
}
function setupFileAttachment() {
const attachBtn = document.getElementById('ql-attach-btn');
const fileInput = document.getElementById('ql-file-input');
if (!attachBtn || !fileInput) return;
attachBtn.addEventListener('click', () => {
if (qlAttachedFiles.length >= MAX_FILES) {
showCustomAlert('Limit', 'Maximum of ' + MAX_FILES + ' files.');
return;
}
fileInput.click();
});
fileInput.addEventListener('change', async () => {
const files = Array.from(fileInput.files || []);
fileInput.value = '';
if (!files.length) return;
const storageData = await new Promise(r => chrome.storage.local.get(['lovable_token'], r));
let token = storageData.lovable_token || '';
if (!token) {
showCustomAlert('Error', 'Token not captured. Browse Lovable to sync.');
return;
}
if (token.startsWith('Bearer ')) token = token.slice(7);
for (const file of files) {
if (qlAttachedFiles.length >= MAX_FILES) {
showCustomAlert('Limit', 'Maximum of ' + MAX_FILES + ' files reached.');
break;
}
if (file.size > MAX_FILE_SIZE) {
showCustomAlert('Large file', file.name + ' exceeds 20MB.');
continue;
}
let processedFile = file;
let previewUrl = null;
if (isImageType(file.type)) {
const result = await compressImage(file);
processedFile = result.file;
previewUrl = result.previewUrl;
}
const isImage = isImageType(processedFile.type);
const placeholderIdx = qlAttachedFiles.length;
qlAttachedFiles.push({
file_id: null,
file_name: file.name,
previewUrl: previewUrl,
file_type: processedFile.type,
sizeLabel: formatFileSize(processedFile.size),
uploading: true,
rawFile: processedFile
});
renderAttachPreview();
try {
const result = await uploadFileDirect(processedFile, token);
qlAttachedFiles[placeholderIdx].file_id = result.file_id;
qlAttachedFiles[placeholderIdx].public_url = result.public_url;
qlAttachedFiles[placeholderIdx].uploading = false;
renderAttachPreview();
} catch (err) {
console.warn('[QL Upload] failed to upload to Supabase Storage:', err.message);
qlAttachedFiles[placeholderIdx].uploading = false;
qlAttachedFiles[placeholderIdx].uploadFailed = true;
renderAttachPreview();
showCustomAlert('Upload Error', 'Could not upload the image: ' + (err.message || 'unknown error'));
}
}
});
}
async function deliverPromptToLovable(text) {
if (typeof window.__pkDeliverPrompt === "function") {
return window.__pkDeliverPrompt(text);
}
throw new Error("Extension send bridge not ready. Refresh your Lovable project tab and try again.");
}
function setupSend() {
const btn = document.getElementById("ql-send");
if (!btn) return;
if (btn.dataset.qlSendBound === "1") return;
btn.dataset.qlSendBound = "1";
btn.addEventListener("click", async () => {
var msgEl = document.getElementById("ql-msg");
const mensagem = msgEl ? (msgEl.value || "").trim() : "";
const log = document.getElementById("ql-log");
if (!mensagem) {
if (log) { log.className = "ql-log-error"; log.innerText = "⚠ Empty prompt"; }
return;
}
const storageData = await new Promise((resolve) => {
chrome.storage.local.get(["lovable_projectId", "ql_license_key"], resolve);
});
const projectId = storageData.lovable_projectId || projectIdFromPage() || "";
const licenseKey = storageData.ql_license_key || "";
if (!projectId) {
if (log) { log.className = "ql-log-error"; log.innerText = "⚠ Open lovable.dev on your project and wait for sync."; }
return;
}
var teamLicenseKey = resolveTeamLicenseKey(licenseKey);
if (!teamLicenseKey) {
if (log) {
log.className = "ql-log-error";
log.innerText = "⚠ Activate your license in the side panel first.";
}
return;
}
// Images uploaded to Powerkits storage — attach public links to prompt.
// We only attach the public links to the user prompt.
const uploadedImages = qlAttachedFiles.filter(function (f) {
return f.public_url && !f.uploading && !f.uploadFailed;
});
const hasImage = uploadedImages.length > 0;
// Compose final message: prompt + URL(s) das imagens
var finalMensagem = mensagem;
if (hasImage) {
var linkLines = uploadedImages.map(function (f) { return f.public_url; }).join('\n');
var sep = uploadedImages.length > 1 ? 'Analyze the files at these links:\n' : 'Analyze the file at this link: ';
finalMensagem = mensagem + '\n\n' + sep + linkLines;
}
try {
if (hasImage) {
if (log) { log.className = "ql-log-info"; log.innerText = "📎 Attaching image link..."; }
} else {
if (log) { log.className = "ql-log-info"; log.innerText = "⏳ Sending prompt..."; }
}
btn.classList.add("ql-sending");
btn.disabled = true;
await deliverPromptToLovable(finalMensagem);
if (log) {
if (hasImage) {
log.className = "ql-log-success";
log.innerText = "✓ Prompt sent! Valid image 😁";
} else {
log.className = "ql-log-success";
log.innerText = "✓ Prompt sent!";
}
}
try { if (typeof QLSounds !== "undefined") QLSounds.promptSent(); } catch (e) { }
// Save to chat history
addToChatHistory(mensagem, 'ok');
var msgEl = document.getElementById("ql-msg");
if (msgEl) msgEl.value = "";
qlAttachedFiles.forEach(f => { if (f.previewUrl) URL.revokeObjectURL(f.previewUrl); });
qlAttachedFiles = [];
renderAttachPreview();
} catch (err) {
if (log) { log.className = "ql-log-error"; log.innerText = "✗ " + formatApiError(err.message || err); }
addToChatHistory(mensagem, 'error');
} finally {
btn.classList.remove("ql-sending");
btn.disabled = false;
}
});
}
// Store references to avoid stacking listeners
let _dragCleanup = null;
let _resizeCleanup = null;
function setupDrag() {
if (_dragCleanup) { _dragCleanup(); _dragCleanup = null; }
const box = document.getElementById("ql-floating");
const header = document.getElementById("ql-header");
if (!box || !header) return;
let dragging = false, startX = 0, startY = 0, startLeft = 0, startTop = 0;
function onPointerDown(e) {
if (e.target.closest(".ql-minimize-btn") || e.target.closest(".ql-icon-btn") || e.target.closest("button")) return;
if (e.pointerType === "mouse" && e.button !== 0) return;
e.preventDefault();
const rect = box.getBoundingClientRect();
startX = e.clientX; startY = e.clientY;
startLeft = rect.left; startTop = rect.top;
dragging = true;
try { header.setPointerCapture(e.pointerId); } catch (ex) { }
document.addEventListener("pointermove", onPointerMove);
document.addEventListener("pointerup", onPointerUp);
document.body.style.userSelect = "none";
}
function onPointerMove(e) {
if (!dragging) return;
let newLeft = startLeft + (e.clientX - startX);
let newTop = startTop + (e.clientY - startY);
newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - box.offsetWidth));
newTop = Math.max(0, Math.min(newTop, window.innerHeight - box.offsetHeight));
box.style.left = newLeft + "px";
box.style.top = newTop + "px";
}
function onPointerUp(e) {
if (!dragging) return;
dragging = false;
try { header.releasePointerCapture(e.pointerId); } catch (ex) { }
document.removeEventListener("pointermove", onPointerMove);
document.removeEventListener("pointerup", onPointerUp);
document.body.style.userSelect = "";
}
header.addEventListener("pointerdown", onPointerDown, { passive: false });
_dragCleanup = function () {
header.removeEventListener("pointerdown", onPointerDown);
document.removeEventListener("pointermove", onPointerMove);
document.removeEventListener("pointerup", onPointerUp);
};
}
function setupResize() {
if (_resizeCleanup) { _resizeCleanup(); _resizeCleanup = null; }
const box = document.getElementById("ql-floating");
const handle = document.getElementById("ql-resize-handle");
if (!box || !handle) return;
let resizing = false, startY = 0, startH = 0;
function onDown(e) {
e.preventDefault();
e.stopPropagation();
resizing = true;
startY = e.clientY;
startH = box.offsetHeight;
try { handle.setPointerCapture(e.pointerId); } catch (ex) { }
document.addEventListener("pointermove", onMove);
document.addEventListener("pointerup", onUp);
document.body.style.userSelect = "none";
}
function onMove(e) {
if (!resizing) return;
let newH = startH + (e.clientY - startY);
newH = Math.max(200, Math.min(newH, window.innerHeight * 0.8));
box.style.height = newH + "px";
}
function onUp(e) {
if (!resizing) return;
resizing = false;
qlHeight = box.offsetHeight;
chrome.storage.local.set({ ql_height: qlHeight });
try { handle.releasePointerCapture(e.pointerId); } catch (ex) { }
document.removeEventListener("pointermove", onMove);
document.removeEventListener("pointerup", onUp);
document.body.style.userSelect = "";
}
handle.addEventListener("pointerdown", onDown, { passive: false });
_resizeCleanup = function () {
handle.removeEventListener("pointerdown", onDown);
document.removeEventListener("pointermove", onMove);
document.removeEventListener("pointerup", onUp);
};
}
// ===== CLIPBOARD PASTE (Ctrl+V) for ANY Files =====
function setupClipboardPaste() {
var textarea = document.getElementById('ql-msg');
if (!textarea) return;
// --- Drag and Drop ---
var dropZone = document.getElementById('ql-floating') || textarea;
var dragOverlay = null;
function showDragOverlay() {
if (dragOverlay) return;
dragOverlay = document.createElement('div');
dragOverlay.className = 'ql-drag-overlay';
dragOverlay.innerHTML = '📂 Drop files here
';
var parent = document.getElementById('ql-floating');
if (parent) parent.appendChild(dragOverlay);
}
function hideDragOverlay() {
if (dragOverlay) { dragOverlay.remove(); dragOverlay = null; }
}
dropZone.addEventListener('dragover', function (e) { e.preventDefault(); e.stopPropagation(); showDragOverlay(); });
dropZone.addEventListener('dragleave', function (e) { e.preventDefault(); e.stopPropagation(); if (!dropZone.contains(e.relatedTarget)) hideDragOverlay(); });
dropZone.addEventListener('drop', async function (e) {
e.preventDefault(); e.stopPropagation(); hideDragOverlay();
var files = Array.from(e.dataTransfer.files || []);
if (!files.length) return;
await handleFilesAttach(files);
});
// --- Paste (images + non-image files) ---
textarea.addEventListener('paste', async function (e) {
var items = e.clipboardData && e.clipboardData.items;
if (!items) return;
var filesToAttach = [];
for (var i = 0; i < items.length; i++) {
var item = items[i];
if (item.kind === 'file') {
e.preventDefault();
var file = item.getAsFile();
if (file) filesToAttach.push(file);
}
}
if (filesToAttach.length > 0) await handleFilesAttach(filesToAttach);
});
}
async function handleFilesAttach(files) {
if (qlAttachedFiles.length >= MAX_FILES) {
showCustomAlert('Limit', 'Maximum ' + MAX_FILES + ' files.');
return;
}
var sd = await new Promise(function (r) { chrome.storage.local.get(['lovable_token'], r); });
var token = sd.lovable_token || '';
if (!token) { showCustomAlert('Error', 'Token not captured.'); return; }
if (token.indexOf('Bearer ') === 0) token = token.slice(7);
for (var fi = 0; fi < files.length; fi++) {
var file = files[fi];
if (qlAttachedFiles.length >= MAX_FILES) break;
if (file.size > MAX_FILE_SIZE) { showCustomAlert('File Too Large', file.name + ' exceeds 20MB.'); continue; }
var processedFile = file;
var previewUrl = null;
if (isImageType(file.type)) {
var compressed = await compressImage(file);
processedFile = compressed.file;
previewUrl = compressed.previewUrl;
}
var idx = qlAttachedFiles.length;
qlAttachedFiles.push({
file_id: null,
file_name: file.name || ('file_' + Date.now()),
previewUrl: previewUrl,
file_type: processedFile.type,
sizeLabel: formatFileSize(processedFile.size),
uploading: true,
rawFile: processedFile
});
renderAttachPreview();
try {
var res = await uploadFileDirect(processedFile, token);
qlAttachedFiles[idx].file_id = res.file_id;
qlAttachedFiles[idx].uploading = false;
renderAttachPreview();
} catch (err) {
qlAttachedFiles[idx].uploading = false;
qlAttachedFiles[idx].file_id = 'local_direct_' + crypto.randomUUID();
qlAttachedFiles[idx].uploadFailed = true;
renderAttachPreview();
}
}
}
// ===== DOWNLOAD ALL PROJECT FILES (Popup) =====
var CURRENT_EXT_VERSION_POPUP = typeof extensionVersionShort === "function" ? extensionVersionShort() : (typeof EXTENSION_VERSION !== "undefined" ? EXTENSION_VERSION : "0.0.0");
function setupDownloadProject() {
var btn = document.getElementById('ql-download-project');
if (!btn) return;
btn.addEventListener('click', async function () {
var statusEl = document.getElementById('ql-download-status');
btn.disabled = true;
btn.textContent = 'Preparing...';
if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'ql-log-info'; statusEl.textContent = 'Checking token and project...'; }
try {
// ---- Feature flag gate ----
try {
var flagUrl = API_BASE + "/rest/v1/feature_flags?select=enabled&flag_key=eq.download_files";
var flagRows = await bgFetch(flagUrl, { method: "GET", headers: { apikey: API_KEY } });
if (flagRows && flagRows.length > 0 && flagRows[0].enabled === false) {
throw new Error('Error using the extension resources.');
}
} catch (flagErr) {
if (flagErr && flagErr.message === 'Error using the extension resources.') throw flagErr;
}
var sd = await new Promise(function (r) { chrome.storage.local.get(['lovable_token', 'lovable_projectId'], r); });
var authToken = sd.lovable_token || '';
var storedProjectId = sd.lovable_projectId || '';
if (authToken.indexOf('Bearer ') === 0) authToken = authToken.slice(7);
var projectId = storedProjectId;
if (!projectId) throw new Error('Open a Lovable project page first.');
if (!authToken) {
var cookieResponse = await new Promise(function (resolve) {
safeSendMessage({ action: "readCookies" }, function (resp) { resolve(resp); });
});
if (cookieResponse && cookieResponse.success && cookieResponse.tokens && cookieResponse.tokens.length > 0) {
authToken = cookieResponse.tokens[0].token;
}
}
if (!authToken) throw new Error('Token not found. Open a Lovable project and wait for sync.');
btn.textContent = 'Downloading...';
if (statusEl) statusEl.textContent = 'Downloading project files...';
var dlResponse = await new Promise(function (resolve) {
safeSendMessage({ action: "downloadProject", projectId: projectId, token: authToken }, function (resp) { resolve(resp); });
});
if (!dlResponse || !dlResponse.success) throw new Error(dlResponse && dlResponse.error ? dlResponse.error : 'Download failed');
var files = dlResponse.files;
if (!files || files.length === 0) throw new Error('No files found in the project.');
if (statusEl) statusEl.textContent = 'Creating ZIP with ' + files.length + ' files...';
btn.textContent = 'Packaging...';
if (typeof JSZip === 'undefined') throw new Error('JSZip not loaded. Use the Side Panel.');
var zip = new JSZip();
var imageExts = ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.webp', '.bmp', '.tiff'];
var addedFiles = 0;
for (var fi = 0; fi < files.length; fi++) {
var f = files[fi];
if (!f.name || f.sizeExceeded) continue;
if (f.contents && f.binary) { zip.file(f.name, f.contents, { base64: true, binary: true }); addedFiles++; }
else if (!f.contents && imageExts.some(function (ext) { return f.name.toLowerCase().endsWith(ext); })) {
try {
var imgResp = await fetch('https://api.lovable.dev/projects/' + projectId + '/files/raw?path=' + encodeURIComponent(f.name), { method: 'GET', headers: { 'Authorization': 'Bearer ' + authToken }, credentials: 'omit', mode: 'cors' });
if (imgResp.ok) { zip.file(f.name, await imgResp.arrayBuffer(), { binary: true }); addedFiles++; }
else if (f.contents) { zip.file(f.name, f.contents); addedFiles++; }
} catch (imgErr) { if (f.contents) { zip.file(f.name, f.contents); addedFiles++; } }
} else if (f.contents) { zip.file(f.name, f.contents); addedFiles++; }
}
var zipBlob = await zip.generateAsync({ type: 'blob', compression: 'DEFLATE', compressionOptions: { level: 9 } });
var a = document.createElement('a');
a.href = URL.createObjectURL(zipBlob);
a.download = 'lovable-' + projectId.substring(0, 8) + '-' + new Date().toISOString().split('T')[0] + '.zip';
document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(a.href);
if (statusEl) { statusEl.className = 'ql-log-success'; statusEl.textContent = addedFiles + ' files downloaded!'; }
btn.textContent = 'Download Complete!';
setTimeout(function () { btn.textContent = 'Download All Files'; btn.disabled = false; if (statusEl) statusEl.style.display = 'none'; }, 4000);
} catch (err) {
if (statusEl) { statusEl.className = 'ql-log-error'; statusEl.textContent = (err.message || err); statusEl.style.display = 'block'; }
btn.textContent = 'Failed';
setTimeout(function () { btn.textContent = 'Download All Files'; btn.disabled = false; }, 3000);
}
});
}
// ===== UPDATE CHECK (Popup) =====
async function checkForUpdatePopup() {
try {
var data = await bgFetch(VERSIONS_URL_POPUP, { method: "GET", headers: { apikey: API_KEY } });
if (!data || !data.length) return;
var latest = data[0];
if (latest.version !== CURRENT_EXT_VERSION_POPUP && latest.is_alert_active) {
var banner = document.getElementById('ql-update-banner');
if (banner) {
var dlUrl = latest.file_path ? API_BASE + "/storage/v1/object/public/extension-releases/" + latest.file_path : null;
banner.innerHTML = qlTemplateUpdateBanner(latest.version, latest.changelog || '', dlUrl);
banner.style.display = 'block';
}
}
} catch (e) { }
}
// ===== RESELLER ROLE CHECK (Popup) =====
async function checkResellerRolePopup() {
try {
var storageData = await new Promise(function (r) { chrome.storage.local.get(["ql_license_key"], r); });
if (!storageData.ql_license_key) return;
var licData = await bgFetch(LICENSES_URL + "&license_key=eq." + encodeURIComponent(storageData.ql_license_key) + "&limit=1", { method: "GET", headers: { apikey: API_KEY } });
if (!licData || !licData.length || !licData[0].user_id) return;
var userId = licData[0].user_id;
var roleData = await bgFetch(USER_ROLES_URL_POPUP + "&user_id=eq." + userId, { method: "GET", headers: { apikey: API_KEY } });
if (roleData && Array.isArray(roleData) && roleData.some(function (r) { return r.role === 'reseller' || r.role === 'admin'; })) {
var btn = document.getElementById('ql-reseller-btn');
if (btn) btn.style.display = 'block';
}
} catch (e) { }
}
// ===== NATIVE CHAT MODE =====
let qlNativeChatActive = false;
let qlNativeChatCleanup = null;
function activateNativeChat() {
qlNativeChatActive = true;
chrome.storage.local.set({ ql_native_chat: true });
// Hide the extension
const floatingBox = document.getElementById("ql-floating");
if (floatingBox) {
floatingBox.style.transition = "opacity 0.3s ease, transform 0.3s ease";
floatingBox.style.opacity = "0";
floatingBox.style.transform = "scale(0.95) translateX(20px)";
setTimeout(() => { floatingBox.style.display = "none"; }, 350);
}
injectNativeChatOverlay();
}
function deactivateNativeChat() {
qlNativeChatActive = false;
chrome.storage.local.set({ ql_native_chat: false });
// Clean up injected elements
if (qlNativeChatCleanup) { qlNativeChatCleanup(); qlNativeChatCleanup = null; }
const badge = document.getElementById("ql-native-badge");
if (badge) badge.remove();
const returnBtn = document.getElementById("ql-native-return-btn");
if (returnBtn) returnBtn.remove();
// Restore send button
const chatForm = document.querySelector("form#chat-input");
const sendBtn = document.getElementById("chatinput-send-message-button") || (chatForm && (chatForm.querySelector('button[type="submit"]') || chatForm.querySelector('button[aria-label*="send" i]') || chatForm.querySelector('button:last-of-type')));
if (sendBtn) {
sendBtn.classList.remove("ql-native-send-active");
sendBtn.style.animation = "";
}
// Show the extension again
const floatingBox = document.getElementById("ql-floating");
if (floatingBox) {
floatingBox.style.display = "";
floatingBox.style.opacity = "0";
floatingBox.style.transform = "scale(0.95)";
requestAnimationFrame(() => {
floatingBox.style.transition = "opacity 0.4s ease, transform 0.4s ease";
floatingBox.style.opacity = "1";
floatingBox.style.transform = "scale(1) translateX(0)";
});
} else {
// Rebuild if removed
_buildFloatingUI();
}
}
function injectNativeChatOverlay() {
// Wait for chat form to exist
const chatForm = document.querySelector("form#chat-input");
if (!chatForm) {
setTimeout(injectNativeChatOverlay, 500);
return;
}
// Add QL badge on top-right of chat form
if (!document.getElementById("ql-native-badge")) {
const existingPos = getComputedStyle(chatForm).position;
if (existingPos === "static") chatForm.style.position = "relative";
const badge = document.createElement("div");
badge.id = "ql-native-badge";
badge.className = "ql-native-badge";
badge.innerHTML = "⚡ Saqlain\'s Tech World ";
chatForm.appendChild(badge);
}
// Add return button below chat form
if (!document.getElementById("ql-native-return-btn")) {
const returnBtn = document.createElement("button");
returnBtn.id = "ql-native-return-btn";
returnBtn.className = "ql-native-return-btn";
returnBtn.innerHTML = "← Return to Extension";
returnBtn.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
deactivateNativeChat();
});
chatForm.parentElement.insertBefore(returnBtn, chatForm.nextSibling);
}
// Style the send button with blink animation
const sendBtn = document.getElementById("chatinput-send-message-button") || (chatForm && (chatForm.querySelector('button[type="submit"]') || chatForm.querySelector('button[aria-label*="send" i]') || chatForm.querySelector('button:last-of-type')));
if (sendBtn) {
sendBtn.classList.add("ql-native-send-active");
}
// History tracking only — Lovable native send proceeds (pageHook applies credit bypass).
function interceptSend() {
if (!qlNativeChatActive) return;
const editor = chatForm.querySelector('[contenteditable="true"]');
const text = editor ? (editor.innerText || editor.textContent || "").trim() : "";
if (text) addToChatHistory(text, "ok");
}
function interceptSubmit() {
if (!qlNativeChatActive) return;
const editor = chatForm.querySelector('[contenteditable="true"]');
const text = editor ? (editor.innerText || editor.textContent || "").trim() : "";
if (text) addToChatHistory(text, "ok");
}
function interceptKeydown(e) {
if (!qlNativeChatActive) return;
if (e.key === "Enter" && !e.shiftKey) {
const editor = chatForm.querySelector('[contenteditable="true"]');
const text = editor ? (editor.innerText || editor.textContent || "").trim() : "";
if (text) addToChatHistory(text, "ok");
}
}
if (sendBtn) sendBtn.addEventListener("click", interceptSend, true);
chatForm.addEventListener("submit", interceptSubmit, true);
chatForm.addEventListener("keydown", interceptKeydown, true);
qlNativeChatCleanup = function () {
if (sendBtn) sendBtn.removeEventListener("click", interceptSend, true);
chatForm.removeEventListener("submit", interceptSubmit, true);
chatForm.removeEventListener("keydown", interceptKeydown, true);
};
}
async function sendViaNativeChat(text) {
if (text) addToChatHistory(text, "ok");
}
function showNativeSendingOverlay(show) {
const id = "ql-native-sending-overlay";
const existing = document.getElementById(id);
if (!show) { if (existing) existing.remove(); return; }
if (existing) return;
const el = document.createElement("div");
el.id = id;
el.className = "ql-native-sending-overlay";
el.innerHTML = '
Sending prompt...';
document.body.appendChild(el);
}
function showNativeChatToast(msg, type) {
const existing = document.getElementById("ql-native-toast");
if (existing) existing.remove();
const toast = document.createElement("div");
toast.id = "ql-native-toast";
toast.className = "ql-native-toast ql-native-toast-" + type;
toast.textContent = qlUserText(msg);
document.body.appendChild(toast);
requestAnimationFrame(() => toast.classList.add("ql-native-toast-visible"));
setTimeout(() => {
toast.classList.remove("ql-native-toast-visible");
setTimeout(() => toast.remove(), 300);
}, 3000);
}
function setupNativeChatButton() {
const btn = document.getElementById("ql-native-chat-btn");
if (!btn) return;
btn.addEventListener("click", () => {
activateNativeChat();
});
}
chrome.runtime.onMessage.addListener(function (msg, _sender, sendResponse) {
if (msg && msg.action === "setCreditBypass") {
setPkCreditBypass(!!msg.active);
sendResponse({ ok: true });
return false;
}
if (msg && msg.action === "qlActivateBypass") {
setPkCreditBypass(true);
sendResponse({ ok: true });
return false;
}
if (msg && msg.action === "qlDeactivateBypass") {
setPkCreditBypass(false);
sendResponse({ ok: true });
return false;
}
if (msg && msg.action === "syncCreditBypass") {
syncPkCreditBypassFromStorage();
sendResponse({ ok: true });
return false;
}
if (msg && msg.action === "setShieldActive") {
qlShieldActive = !!msg.active;
if (qlShieldActive) injectShieldOverlay();
else removeShieldOverlay();
sendResponse({ ok: true });
return false;
}
if (msg && msg.action === "setNativeChatActive") {
if (msg.active) activateNativeChat();
else deactivateNativeChat();
sendResponse({ ok: true });
return false;
}
if (msg && msg.action === "qlActivateNativeChat") {
activateNativeChat();
sendResponse({ ok: true });
return false;
}
if (msg && msg.action === "qlDeactivateNativeChat") {
deactivateNativeChat();
sendResponse({ ok: true });
return false;
}
if (msg && msg.action === "qlQuickProjectInit") {
quickProjectInit()
.then(function () { sendResponse({ ok: true }); })
.catch(function (err) { sendResponse({ ok: false, error: err.message }); });
return true;
}
if (msg && msg.action === "getNativeChatCapture") {
sendResponse({ body: getNativeChatCaptureBody() });
return false;
}
if (msg && msg.action === "requestTokenRefresh") {
try { window.postMessage({ type: "lovableRequestToken" }, "*"); } catch (e) { }
setTimeout(function () {
try { window.postMessage({ type: "lovableRequestToken" }, "*"); } catch (e2) { }
}, 120);
sendResponse({ ok: true });
return false;
}
if (msg && msg.action === "getSessionHeaders") {
buildSessionHeaders(msg.projectId || "").then(function (headers) {
sendResponse({ headers: headers });
});
return true;
}
if (msg && msg.action === "getLovableSession") {
captureLovableSessionFromPage().then(function (session) {
sendResponse(session);
});
return true;
}
if (msg && msg.action === "resolveLovableAuth") {
captureLovableSessionFromPage().then(function (session) {
if (session.ok) {
sendResponse({
token: session.token,
cookieToken: session.token,
projectId: session.projectId
});
return;
}
sendResponse({
token: "",
cookieToken: "",
projectId: projectIdFromPage() || ""
});
});
return true;
}
});
// Check if native chat was active on page load
chrome.storage.local.get(["ql_native_chat"], (res) => {
if (res.ql_native_chat === true) {
qlNativeChatActive = true;
setTimeout(() => {
const floatingBox = document.getElementById("ql-floating");
if (floatingBox) floatingBox.style.display = "none";
injectNativeChatOverlay();
}, 500);
}
});
window.addEventListener("message", (event) => {
if (!event.data || event.source !== window) return;
if (event.data.type === "lovableBrowserSession" && event.data.browserSessionId) {
safeSendMessage({
action: "lovableSync",
browserSessionId: event.data.browserSessionId
});
return;
}
if (event.data.type !== "lovableTokenFound") return;
const updates = {};
if (event.data.token && typeof event.data.token === "string") {
updates.lovable_token = event.data.token.replace(/^Bearer\s+/i, "").trim();
}
if (event.data.projectId && typeof event.data.projectId === "string") {
updates.lovable_projectId = event.data.projectId;
}
if (!Object.keys(updates).length) return;
safeSendMessage({ action: "lovableSync", token: updates.lovable_token, projectId: updates.lovable_projectId });
chrome.storage.local.set(updates, () => {
updateSyncStatus();
});
});
if (location.hostname && location.hostname.indexOf("lovable.dev") !== -1) {
syncPkCreditBypassFromStorage();
}
(function initLovableAuthSync() {
if (!location.hostname || location.hostname.indexOf("lovable.dev") === -1) return;
function run() {
safeSendMessage({
action: "syncLovableAuth",
tabUrl: location.href,
projectId: projectIdFromPage()
});
try { window.postMessage({ type: "lovableRequestToken" }, "*"); } catch (e) { }
}
run();
setInterval(run, 8000);
document.addEventListener("visibilitychange", function () {
if (!document.hidden) run();
});
})();
function setupCreateProject() {
var btn = document.getElementById('ql-quick-init') || document.getElementById('ql-create-project');
if (!btn) return;
btn.addEventListener('click', async function () {
var statusEl = document.getElementById('ql-download-status');
var originalLabel = btn.textContent;
btn.disabled = true;
btn.textContent = 'Creating project...';
if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'ql-log-info'; statusEl.textContent = 'Typing placeholder and clicking Build...'; }
try {
await quickProjectInit();
if (statusEl) { statusEl.className = 'ql-log-success'; statusEl.textContent = '✅ Empty project created! Send your real prompt from the extension.'; }
btn.textContent = '✅ Done!';
setTimeout(function () {
btn.disabled = false;
btn.textContent = originalLabel;
if (statusEl) statusEl.style.display = 'none';
}, 5000);
} catch (err) {
console.error('[CreateProject]', err);
if (statusEl) { statusEl.className = 'ql-log-error'; statusEl.textContent = '❌ ' + (err.message || 'Error'); }
btn.disabled = false;
btn.textContent = originalLabel;
}
});
}
async function quickProjectInit() {
if (window.location.pathname.match(/\/projects\/[a-f0-9-]{36}/i)) {
throw new Error('Use this button on the Lovable home screen without a project open.');
}
const chatForm = document.querySelector('form#chat-input');
if (!chatForm) throw new Error('Chat form not found. Make sure you are on the Lovable home screen.');
const editor = chatForm.querySelector('[contenteditable="true"]');
if (!editor) throw new Error('Text field not found.');
const buildBtn = document.getElementById('chatinput-send-message-button') || chatForm.querySelector('button[type="submit"]') || chatForm.querySelector('button[aria-label*="send" i]') || chatForm.querySelector('button:last-of-type');
if (!buildBtn) throw new Error('Build button not found.');
editor.focus();
document.execCommand('selectAll', false, null);
document.execCommand('insertText', false, '.');
await new Promise(r => setTimeout(r, 300));
if (buildBtn.disabled) buildBtn.removeAttribute('disabled');
buildBtn.click();
const stopped = await new Promise(function (resolve) {
const TIMEOUT = 25000;
const start = Date.now();
const interval = setInterval(function () {
if (Date.now() - start > TIMEOUT) {
clearInterval(interval);
resolve(false);
return;
}
const stopBtn = document.querySelector('button[aria-label="Stop generating"]');
if (stopBtn && !stopBtn.disabled) {
clearInterval(interval);
stopBtn.click();
resolve(true);
}
}, 200);
});
if (!stopped) {
throw new Error('Timeout waiting for Stop. Check whether a project was created in your list.');
}
}
let lastObservedText = "";
let isObservingOutput = false;
function startOutputObservation() {
if (isObservingOutput) return;
isObservingOutput = true;
console.log("[Love Able AI] Starting output observation...");
let targetElement = null;
let attempts = 0;
const findAndObserve = () => {
const proseElements = document.querySelectorAll(".prose, [class*='prose'], .markdown-body");
if (proseElements.length > 0) {
targetElement = proseElements[proseElements.length - 1];
lastObservedText = targetElement.textContent || "";
// Notify background that generation started
safeSendMessage({
action: "lovableGenerationStarted",
initialText: lastObservedText
});
const observer = new MutationObserver(() => {
const currentText = targetElement.textContent || "";
if (currentText !== lastObservedText) {
lastObservedText = currentText;
safeSendMessage({
action: "lovableGenerationUpdated",
text: currentText
});
}
// Stop condition: Stop button is gone
const stopBtn = document.querySelector('button[aria-label="Stop generating"]');
if (!stopBtn) {
observer.disconnect();
isObservingOutput = false;
safeSendMessage({
action: "lovableGenerationFinished",
finalText: currentText
});
}
});
observer.observe(targetElement, { childList: true, subtree: true, characterData: true });
return true;
}
return false;
};
// Try to find the element immediately
if (!findAndObserve()) {
const findInterval = setInterval(() => {
attempts++;
if (findAndObserve() || attempts > 20) {
clearInterval(findInterval);
if (attempts > 20) {
isObservingOutput = false;
console.warn("[Love Able AI] Could not find assistant message element to observe.");
}
}
}, 200);
}
}
(function initOutputWatcher() {
if (!location.hostname || location.hostname.indexOf("lovable.dev") === -1) return;
setInterval(() => {
const stopBtn = document.querySelector('button[aria-label="Stop generating"]');
if (stopBtn && !isObservingOutput) {
startOutputObservation();
}
}, 500);
})();
window.addEventListener("message", (event) => {
if (!event.data || event.source !== window) return;
if (event.data.type === "lovableWsMessageReceived" && event.data.rawData) {
console.log("[Love Able AI WebSocket Message Received]:", event.data.rawData);
}
if (event.data.type === "lovableWsMessageSent" && event.data.rawData) {
console.log("[Love Able AI WebSocket Message Sent]:", event.data.rawData);
}
});