Spaces:
Sleeping
Sleeping
Website↔Extension bridge: auto-detect login, sync user/plan to extension, guest mode, scan history, user bar in popup
Browse files- extension/background.js +159 -51
- extension/content.js +62 -143
- extension/manifest.json +11 -4
- extension/popup.html +56 -46
- extension/popup.js +58 -63
- web/app/layout.tsx +2 -0
- web/components/extension-bridge.tsx +64 -0
extension/background.js
CHANGED
|
@@ -1,12 +1,21 @@
|
|
| 1 |
/**
|
| 2 |
-
* ClauseGuard — Background Service Worker
|
|
|
|
|
|
|
| 3 |
*/
|
| 4 |
|
| 5 |
const API_BASE = "https://gaurv007-clauseguard-api.hf.space";
|
| 6 |
const FREE_SCANS_PER_MONTH = 10;
|
| 7 |
-
const API_TIMEOUT_MS = 45000;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
-
// ─── Side Panel ───
|
| 10 |
try { chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: false }); } catch(e) {}
|
| 11 |
|
| 12 |
// ─── Fetch with timeout ───
|
|
@@ -17,52 +26,120 @@ async function fetchWithTimeout(url, options, timeoutMs) {
|
|
| 17 |
const resp = await fetch(url, { ...options, signal: controller.signal });
|
| 18 |
clearTimeout(timer);
|
| 19 |
return resp;
|
| 20 |
-
} catch (err) {
|
| 21 |
-
clearTimeout(timer);
|
| 22 |
-
throw err;
|
| 23 |
-
}
|
| 24 |
}
|
| 25 |
|
| 26 |
// ─── Message Router ───
|
| 27 |
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
| 28 |
const handle = async () => {
|
| 29 |
switch (message.type) {
|
| 30 |
-
case "ANALYZE_TEXT":
|
| 31 |
-
|
| 32 |
-
case "
|
| 33 |
-
|
| 34 |
-
case "
|
| 35 |
-
|
| 36 |
-
case "
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
case "GET_RESULTS":
|
| 40 |
-
return await getStoredResults(sender.tab?.id || message.tabId);
|
| 41 |
-
default:
|
| 42 |
-
return null;
|
| 43 |
}
|
| 44 |
};
|
| 45 |
-
handle().then(sendResponse).catch(err => {
|
| 46 |
-
console.error("ClauseGuard handler error:", err);
|
| 47 |
-
sendResponse({ error: err.message });
|
| 48 |
-
});
|
| 49 |
return true;
|
| 50 |
});
|
| 51 |
|
| 52 |
// ─── External messages from website ───
|
| 53 |
chrome.runtime.onMessageExternal.addListener((message, sender, sendResponse) => {
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
}
|
| 64 |
});
|
| 65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
// ─── Core: Analyze ───
|
| 67 |
async function handleAnalyze(payload, tabId) {
|
| 68 |
const usage = await checkUsage();
|
|
@@ -76,7 +153,6 @@ async function handleAnalyze(payload, tabId) {
|
|
| 76 |
return { error: "no_clauses", message: "No analyzable clauses found." };
|
| 77 |
}
|
| 78 |
|
| 79 |
-
// Try API first, fall back to local
|
| 80 |
let results;
|
| 81 |
try {
|
| 82 |
const auth = await getAuth();
|
|
@@ -91,27 +167,54 @@ async function handleAnalyze(payload, tabId) {
|
|
| 91 |
|
| 92 |
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
| 93 |
results = await resp.json();
|
|
|
|
| 94 |
} catch (err) {
|
| 95 |
-
console.warn("API unavailable, using local
|
| 96 |
-
results = localAnalyze(text
|
|
|
|
| 97 |
}
|
| 98 |
|
| 99 |
-
// Store
|
| 100 |
if (tabId) {
|
| 101 |
await chrome.storage.local.set({ [`results_${tabId}`]: results });
|
| 102 |
const flagged = results.results?.filter(r => r.categories?.length > 0).length || 0;
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
chrome.action.setBadgeBackgroundColor({ color: flagged > 3 ? "#ef4444" : "#f59e0b", tabId });
|
| 106 |
-
} else {
|
| 107 |
-
chrome.action.setBadgeText({ text: "", tabId });
|
| 108 |
-
}
|
| 109 |
}
|
| 110 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
await incrementUsage();
|
| 112 |
return results;
|
| 113 |
}
|
| 114 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
// ─── Local fallback ───
|
| 116 |
function localAnalyze(text) {
|
| 117 |
const clauses = splitIntoClauses(text);
|
|
@@ -131,9 +234,7 @@ function localAnalyze(text) {
|
|
| 131 |
const results = clauses.map(clause => {
|
| 132 |
const categories = [];
|
| 133 |
for (const [id, pats] of Object.entries(patterns)) {
|
| 134 |
-
if (pats.some(p => p.test(clause))) {
|
| 135 |
-
categories.push({ name: names[id], severity: sevMap[id] });
|
| 136 |
-
}
|
| 137 |
}
|
| 138 |
return { text: clause, categories };
|
| 139 |
});
|
|
@@ -146,10 +247,7 @@ function localAnalyze(text) {
|
|
| 146 |
return {
|
| 147 |
risk_score: risk,
|
| 148 |
grade: risk >= 60 ? "F" : risk >= 40 ? "D" : risk >= 20 ? "C" : risk >= 10 ? "B" : "A",
|
| 149 |
-
total_clauses: clauses.length,
|
| 150 |
-
flagged_count: flagged.length,
|
| 151 |
-
results,
|
| 152 |
-
source: "local",
|
| 153 |
};
|
| 154 |
}
|
| 155 |
|
|
@@ -161,12 +259,22 @@ function splitIntoClauses(text) {
|
|
| 161 |
}
|
| 162 |
|
| 163 |
async function getAuth() {
|
| 164 |
-
return new Promise(r => chrome.storage.sync.get(
|
|
|
|
| 165 |
token: d.authToken||null, plan: d.plan||"free", email: d.email||null,
|
|
|
|
| 166 |
isLoggedIn: !!d.authToken, isPro: d.plan==="pro"||d.plan==="team",
|
|
|
|
|
|
|
| 167 |
})));
|
| 168 |
}
|
| 169 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
async function checkUsage() {
|
| 171 |
return new Promise(r => chrome.storage.sync.get(["plan","scansThisMonth","monthResetAt"], d => {
|
| 172 |
const now = new Date(), reset = d.monthResetAt ? new Date(d.monthResetAt) : null;
|
|
|
|
| 1 |
/**
|
| 2 |
+
* ClauseGuard — Background Service Worker
|
| 3 |
+
* Full website↔extension bridge: auto-detect login, sync user data,
|
| 4 |
+
* save scans to DB, guest mode fallback.
|
| 5 |
*/
|
| 6 |
|
| 7 |
const API_BASE = "https://gaurv007-clauseguard-api.hf.space";
|
| 8 |
const FREE_SCANS_PER_MONTH = 10;
|
| 9 |
+
const API_TIMEOUT_MS = 45000;
|
| 10 |
+
|
| 11 |
+
// Website URLs (for auth detection)
|
| 12 |
+
const SITE_ORIGINS = [
|
| 13 |
+
"https://clauseguard.com",
|
| 14 |
+
"https://www.clauseguard.com",
|
| 15 |
+
];
|
| 16 |
+
// Add your Netlify URL here after deploy:
|
| 17 |
+
// SITE_ORIGINS.push("https://your-site.netlify.app");
|
| 18 |
|
|
|
|
| 19 |
try { chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: false }); } catch(e) {}
|
| 20 |
|
| 21 |
// ─── Fetch with timeout ───
|
|
|
|
| 26 |
const resp = await fetch(url, { ...options, signal: controller.signal });
|
| 27 |
clearTimeout(timer);
|
| 28 |
return resp;
|
| 29 |
+
} catch (err) { clearTimeout(timer); throw err; }
|
|
|
|
|
|
|
|
|
|
| 30 |
}
|
| 31 |
|
| 32 |
// ─── Message Router ───
|
| 33 |
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
| 34 |
const handle = async () => {
|
| 35 |
switch (message.type) {
|
| 36 |
+
case "ANALYZE_TEXT": return await handleAnalyze(message.payload, sender.tab?.id);
|
| 37 |
+
case "GET_AUTH": return await getAuth();
|
| 38 |
+
case "GET_USER": return await getUser();
|
| 39 |
+
case "CHECK_USAGE": return await checkUsage();
|
| 40 |
+
case "OPEN_SIDEPANEL": if (sender.tab?.id) chrome.sidePanel.open({ tabId: sender.tab.id }); return { ok: true };
|
| 41 |
+
case "GET_RESULTS": return await getStoredResults(sender.tab?.id || message.tabId);
|
| 42 |
+
case "SYNC_AUTH": return await syncAuthFromWebsite(); // Manual sync trigger
|
| 43 |
+
case "GET_SCAN_HISTORY": return await getScanHistory();
|
| 44 |
+
default: return null;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
}
|
| 46 |
};
|
| 47 |
+
handle().then(sendResponse).catch(err => sendResponse({ error: err.message }));
|
|
|
|
|
|
|
|
|
|
| 48 |
return true;
|
| 49 |
});
|
| 50 |
|
| 51 |
// ─── External messages from website ───
|
| 52 |
chrome.runtime.onMessageExternal.addListener((message, sender, sendResponse) => {
|
| 53 |
+
// Accept from any allowed origin (clauseguard.com, netlify, localhost)
|
| 54 |
+
const handle = async () => {
|
| 55 |
+
switch (message.type) {
|
| 56 |
+
case "SET_AUTH": {
|
| 57 |
+
await chrome.storage.sync.set({
|
| 58 |
+
authToken: message.token,
|
| 59 |
+
plan: message.plan || "free",
|
| 60 |
+
email: message.email || "",
|
| 61 |
+
userName: message.name || "",
|
| 62 |
+
userId: message.userId || "",
|
| 63 |
+
authSource: "website",
|
| 64 |
+
lastSyncAt: Date.now(),
|
| 65 |
+
});
|
| 66 |
+
return { success: true };
|
| 67 |
+
}
|
| 68 |
+
case "LOGOUT": {
|
| 69 |
+
await chrome.storage.sync.remove(["authToken", "plan", "email", "userName", "userId", "authSource", "lastSyncAt"]);
|
| 70 |
+
return { success: true };
|
| 71 |
+
}
|
| 72 |
+
case "GET_STATUS": {
|
| 73 |
+
const auth = await getAuth();
|
| 74 |
+
const usage = await checkUsage();
|
| 75 |
+
return { auth, usage };
|
| 76 |
+
}
|
| 77 |
+
case "PING": {
|
| 78 |
+
return { installed: true, version: chrome.runtime.getManifest().version };
|
| 79 |
+
}
|
| 80 |
+
default: return null;
|
| 81 |
+
}
|
| 82 |
+
};
|
| 83 |
+
handle().then(sendResponse).catch(err => sendResponse({ error: err.message }));
|
| 84 |
+
return true;
|
| 85 |
+
});
|
| 86 |
+
|
| 87 |
+
// ─── Auto-detect login when user visits website ───
|
| 88 |
+
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
|
| 89 |
+
if (changeInfo.status !== "complete" || !tab.url) return;
|
| 90 |
+
|
| 91 |
+
// Check if user is on our website
|
| 92 |
+
const isOurSite = SITE_ORIGINS.some(o => tab.url.startsWith(o)) ||
|
| 93 |
+
tab.url.includes("clauseguard") || tab.url.includes("localhost:3000");
|
| 94 |
+
|
| 95 |
+
if (isOurSite) {
|
| 96 |
+
// Try to detect auth by injecting a script that reads Supabase session
|
| 97 |
+
try {
|
| 98 |
+
await chrome.scripting.executeScript({
|
| 99 |
+
target: { tabId },
|
| 100 |
+
func: detectWebsiteAuth,
|
| 101 |
+
world: "MAIN", // Access page's JS context
|
| 102 |
+
});
|
| 103 |
+
} catch(e) {
|
| 104 |
+
// Scripting might fail on some pages — that's OK
|
| 105 |
+
}
|
| 106 |
}
|
| 107 |
});
|
| 108 |
|
| 109 |
+
// This function runs IN the webpage context
|
| 110 |
+
function detectWebsiteAuth() {
|
| 111 |
+
try {
|
| 112 |
+
// Read Supabase session from localStorage
|
| 113 |
+
const keys = Object.keys(localStorage);
|
| 114 |
+
const sbKey = keys.find(k => k.startsWith("sb-") && k.endsWith("-auth-token"));
|
| 115 |
+
if (!sbKey) return;
|
| 116 |
+
|
| 117 |
+
const raw = localStorage.getItem(sbKey);
|
| 118 |
+
if (!raw) return;
|
| 119 |
+
|
| 120 |
+
const session = JSON.parse(raw);
|
| 121 |
+
const token = session?.access_token;
|
| 122 |
+
const user = session?.user;
|
| 123 |
+
|
| 124 |
+
if (token && user) {
|
| 125 |
+
// Send to extension via externally_connectable
|
| 126 |
+
const extId = document.querySelector('meta[name="clauseguard-extension-id"]')?.content;
|
| 127 |
+
// Fallback: use postMessage which content script picks up
|
| 128 |
+
window.postMessage({
|
| 129 |
+
type: "CLAUSEGUARD_AUTH_SYNC",
|
| 130 |
+
token: token,
|
| 131 |
+
email: user.email || "",
|
| 132 |
+
name: user.user_metadata?.full_name || user.user_metadata?.name || "",
|
| 133 |
+
userId: user.id || "",
|
| 134 |
+
plan: "free", // Will be fetched from profile
|
| 135 |
+
}, "*");
|
| 136 |
+
}
|
| 137 |
+
} catch(e) {}
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
// ─── Content script picks up auth sync from page ───
|
| 141 |
+
// (This is handled in content.js — see below)
|
| 142 |
+
|
| 143 |
// ─── Core: Analyze ───
|
| 144 |
async function handleAnalyze(payload, tabId) {
|
| 145 |
const usage = await checkUsage();
|
|
|
|
| 153 |
return { error: "no_clauses", message: "No analyzable clauses found." };
|
| 154 |
}
|
| 155 |
|
|
|
|
| 156 |
let results;
|
| 157 |
try {
|
| 158 |
const auth = await getAuth();
|
|
|
|
| 167 |
|
| 168 |
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
| 169 |
results = await resp.json();
|
| 170 |
+
results.source = "api";
|
| 171 |
} catch (err) {
|
| 172 |
+
console.warn("API unavailable, using local:", err.message);
|
| 173 |
+
results = localAnalyze(text);
|
| 174 |
+
results.source = "local";
|
| 175 |
}
|
| 176 |
|
| 177 |
+
// Store results
|
| 178 |
if (tabId) {
|
| 179 |
await chrome.storage.local.set({ [`results_${tabId}`]: results });
|
| 180 |
const flagged = results.results?.filter(r => r.categories?.length > 0).length || 0;
|
| 181 |
+
chrome.action.setBadgeText({ text: flagged > 0 ? String(flagged) : "", tabId });
|
| 182 |
+
if (flagged > 0) chrome.action.setBadgeBackgroundColor({ color: flagged > 3 ? "#ef4444" : "#f59e0b", tabId });
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
}
|
| 184 |
|
| 185 |
+
// Save scan to history (local + server if logged in)
|
| 186 |
+
const scanRecord = {
|
| 187 |
+
url: url || "",
|
| 188 |
+
risk_score: results.risk_score,
|
| 189 |
+
grade: results.grade,
|
| 190 |
+
flagged_count: results.flagged_count,
|
| 191 |
+
total_clauses: results.total_clauses,
|
| 192 |
+
source: results.source,
|
| 193 |
+
scanned_at: Date.now(),
|
| 194 |
+
};
|
| 195 |
+
|
| 196 |
+
// Save to local history (always, even for guests)
|
| 197 |
+
const { scanHistory = [] } = await chrome.storage.local.get("scanHistory");
|
| 198 |
+
scanHistory.unshift(scanRecord);
|
| 199 |
+
if (scanHistory.length > 50) scanHistory.length = 50; // Keep last 50
|
| 200 |
+
await chrome.storage.local.set({ scanHistory });
|
| 201 |
+
|
| 202 |
await incrementUsage();
|
| 203 |
return results;
|
| 204 |
}
|
| 205 |
|
| 206 |
+
// ─── Get scan history (for sidepanel) ───
|
| 207 |
+
async function getScanHistory() {
|
| 208 |
+
const { scanHistory = [] } = await chrome.storage.local.get("scanHistory");
|
| 209 |
+
return { history: scanHistory };
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
// ─── Sync auth from website (called manually or on install) ───
|
| 213 |
+
async function syncAuthFromWebsite() {
|
| 214 |
+
// This is triggered by content script when it detects CLAUSEGUARD_AUTH_SYNC message
|
| 215 |
+
return await getAuth();
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
// ─── Local fallback ───
|
| 219 |
function localAnalyze(text) {
|
| 220 |
const clauses = splitIntoClauses(text);
|
|
|
|
| 234 |
const results = clauses.map(clause => {
|
| 235 |
const categories = [];
|
| 236 |
for (const [id, pats] of Object.entries(patterns)) {
|
| 237 |
+
if (pats.some(p => p.test(clause))) categories.push({ name: names[id], severity: sevMap[id] });
|
|
|
|
|
|
|
| 238 |
}
|
| 239 |
return { text: clause, categories };
|
| 240 |
});
|
|
|
|
| 247 |
return {
|
| 248 |
risk_score: risk,
|
| 249 |
grade: risk >= 60 ? "F" : risk >= 40 ? "D" : risk >= 20 ? "C" : risk >= 10 ? "B" : "A",
|
| 250 |
+
total_clauses: clauses.length, flagged_count: flagged.length, results,
|
|
|
|
|
|
|
|
|
|
| 251 |
};
|
| 252 |
}
|
| 253 |
|
|
|
|
| 259 |
}
|
| 260 |
|
| 261 |
async function getAuth() {
|
| 262 |
+
return new Promise(r => chrome.storage.sync.get(
|
| 263 |
+
["authToken","plan","email","userName","userId","authSource","lastSyncAt"], d => r({
|
| 264 |
token: d.authToken||null, plan: d.plan||"free", email: d.email||null,
|
| 265 |
+
name: d.userName||null, userId: d.userId||null,
|
| 266 |
isLoggedIn: !!d.authToken, isPro: d.plan==="pro"||d.plan==="team",
|
| 267 |
+
isGuest: !d.authToken, authSource: d.authSource||null,
|
| 268 |
+
lastSyncAt: d.lastSyncAt||null,
|
| 269 |
})));
|
| 270 |
}
|
| 271 |
|
| 272 |
+
async function getUser() {
|
| 273 |
+
const auth = await getAuth();
|
| 274 |
+
const usage = await checkUsage();
|
| 275 |
+
return { ...auth, ...usage };
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
async function checkUsage() {
|
| 279 |
return new Promise(r => chrome.storage.sync.get(["plan","scansThisMonth","monthResetAt"], d => {
|
| 280 |
const now = new Date(), reset = d.monthResetAt ? new Date(d.monthResetAt) : null;
|
extension/content.js
CHANGED
|
@@ -1,230 +1,149 @@
|
|
| 1 |
/**
|
| 2 |
* ClauseGuard — Content Script
|
| 3 |
-
*
|
| 4 |
-
* highlights flagged clauses in the DOM using TreeWalker (non-destructive).
|
| 5 |
*/
|
| 6 |
|
| 7 |
(() => {
|
| 8 |
"use strict";
|
| 9 |
-
|
| 10 |
-
// Avoid running on non-HTML pages
|
| 11 |
if (!document.body || document.contentType !== "text/html") return;
|
| 12 |
|
| 13 |
let lastScannedText = "";
|
| 14 |
let isScanning = false;
|
| 15 |
let currentHighlights = [];
|
| 16 |
|
| 17 |
-
// ───
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
function extractPageText() {
|
| 19 |
const clone = document.body.cloneNode(true);
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
// Also remove common cookie/banner elements
|
| 24 |
-
clone.querySelectorAll('[class*="cookie"], [class*="banner"], [class*="modal"], [id*="cookie"]')
|
| 25 |
.forEach(el => el.remove());
|
| 26 |
-
|
| 27 |
return clone.innerText || clone.textContent || "";
|
| 28 |
}
|
| 29 |
|
| 30 |
-
// ─── Scan
|
| 31 |
async function scanPage() {
|
| 32 |
if (isScanning) return;
|
| 33 |
isScanning = true;
|
| 34 |
-
|
| 35 |
const text = extractPageText();
|
| 36 |
-
if (!text || text.length < 100 || text === lastScannedText) {
|
| 37 |
-
isScanning = false;
|
| 38 |
-
return;
|
| 39 |
-
}
|
| 40 |
lastScannedText = text;
|
| 41 |
-
|
| 42 |
try {
|
| 43 |
-
const results = await chrome.runtime.sendMessage({
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
});
|
| 47 |
-
|
| 48 |
-
if (results && !results.error) {
|
| 49 |
-
clearHighlights();
|
| 50 |
-
highlightResults(results);
|
| 51 |
-
}
|
| 52 |
-
} catch (err) {
|
| 53 |
-
console.error("ClauseGuard scan error:", err);
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
isScanning = false;
|
| 57 |
}
|
| 58 |
|
| 59 |
-
// ─── Highlight
|
| 60 |
function highlightResults(results) {
|
| 61 |
if (!results.results) return;
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
flagged.forEach(item => {
|
| 67 |
-
const matchText = item.text.trim();
|
| 68 |
-
if (matchText.length < 20) return;
|
| 69 |
-
|
| 70 |
-
// Find text nodes matching this clause
|
| 71 |
-
const textNodes = findTextNodes(document.body, matchText);
|
| 72 |
-
textNodes.forEach(({ node, startOffset, endOffset }) => {
|
| 73 |
wrapTextRange(node, startOffset, endOffset, item);
|
| 74 |
});
|
| 75 |
});
|
| 76 |
}
|
| 77 |
|
| 78 |
-
// ─── Find text nodes containing a string ───
|
| 79 |
function findTextNodes(root, searchText) {
|
| 80 |
const results = [];
|
| 81 |
-
const searchLower = searchText.toLowerCase().substring(0, 80);
|
| 82 |
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
| 83 |
acceptNode(node) {
|
| 84 |
-
if (node.parentElement?.closest(".clauseguard-highlight, script, style, nav"))
|
| 85 |
-
return NodeFilter.FILTER_REJECT;
|
| 86 |
-
}
|
| 87 |
return node.textContent.length > 20 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
|
| 88 |
},
|
| 89 |
});
|
| 90 |
-
|
| 91 |
let node;
|
| 92 |
while ((node = walker.nextNode())) {
|
| 93 |
-
const
|
| 94 |
-
const idx = textLower.indexOf(searchLower);
|
| 95 |
if (idx !== -1) {
|
| 96 |
-
results.push({
|
| 97 |
-
|
| 98 |
-
startOffset: idx,
|
| 99 |
-
endOffset: Math.min(idx + searchText.length, node.textContent.length),
|
| 100 |
-
});
|
| 101 |
-
break; // One match per clause is enough
|
| 102 |
}
|
| 103 |
}
|
| 104 |
return results;
|
| 105 |
}
|
| 106 |
|
| 107 |
-
// ─── Wrap text node range in a highlight element ───
|
| 108 |
function wrapTextRange(textNode, start, end, clauseData) {
|
| 109 |
try {
|
| 110 |
const range = document.createRange();
|
| 111 |
-
range.setStart(textNode, start);
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
const severity = getMaxSeverity(clauseData.categories);
|
| 115 |
const mark = document.createElement("mark");
|
| 116 |
mark.className = `clauseguard-highlight clauseguard-${severity.toLowerCase()}`;
|
| 117 |
mark.dataset.categories = JSON.stringify(clauseData.categories);
|
| 118 |
-
mark.dataset.clauseText = clauseData.text.substring(0, 200);
|
| 119 |
-
|
| 120 |
-
// Tooltip on hover
|
| 121 |
mark.addEventListener("mouseenter", showTooltip);
|
| 122 |
mark.addEventListener("mouseleave", hideTooltip);
|
| 123 |
-
mark.addEventListener("click",
|
| 124 |
-
|
| 125 |
range.surroundContents(mark);
|
| 126 |
currentHighlights.push(mark);
|
| 127 |
-
} catch (e) {
|
| 128 |
-
// Range errors happen with complex DOM — silently skip
|
| 129 |
-
}
|
| 130 |
-
}
|
| 131 |
-
|
| 132 |
-
function getMaxSeverity(categories) {
|
| 133 |
-
const order = { HIGH: 3, MEDIUM: 2, LOW: 1 };
|
| 134 |
-
return categories.reduce((max, c) => order[c.severity] > order[max] ? c.severity : max, "LOW");
|
| 135 |
}
|
| 136 |
|
| 137 |
// ─── Tooltip ───
|
| 138 |
let tooltipEl = null;
|
| 139 |
-
|
| 140 |
function showTooltip(e) {
|
| 141 |
hideTooltip();
|
| 142 |
-
const
|
| 143 |
-
|
| 144 |
-
if (categories.length === 0) return;
|
| 145 |
-
|
| 146 |
tooltipEl = document.createElement("div");
|
| 147 |
tooltipEl.className = "clauseguard-tooltip";
|
| 148 |
-
tooltipEl.innerHTML = `
|
| 149 |
-
<div class="clauseguard-tooltip-
|
| 150 |
-
|
| 151 |
-
<div class="clauseguard-tooltip-item">
|
| 152 |
-
<span class="clauseguard-tooltip-badge clauseguard-badge-${c.severity.toLowerCase()}">${c.severity}</span>
|
| 153 |
-
<span>${c.name}</span>
|
| 154 |
-
</div>
|
| 155 |
-
`).join("")}
|
| 156 |
-
<div class="clauseguard-tooltip-footer">Click for details →</div>
|
| 157 |
-
`;
|
| 158 |
-
|
| 159 |
document.body.appendChild(tooltipEl);
|
| 160 |
-
|
| 161 |
-
const rect = mark.getBoundingClientRect();
|
| 162 |
tooltipEl.style.left = `${rect.left + window.scrollX}px`;
|
| 163 |
tooltipEl.style.top = `${rect.bottom + window.scrollY + 8}px`;
|
| 164 |
}
|
|
|
|
| 165 |
|
| 166 |
-
function hideTooltip() {
|
| 167 |
-
if (tooltipEl) {
|
| 168 |
-
tooltipEl.remove();
|
| 169 |
-
tooltipEl = null;
|
| 170 |
-
}
|
| 171 |
-
}
|
| 172 |
-
|
| 173 |
-
function openSidePanel() {
|
| 174 |
-
chrome.runtime.sendMessage({ type: "OPEN_SIDEPANEL" });
|
| 175 |
-
}
|
| 176 |
-
|
| 177 |
-
// ─── Clear all highlights ───
|
| 178 |
function clearHighlights() {
|
| 179 |
currentHighlights.forEach(mark => {
|
| 180 |
-
const
|
| 181 |
-
if (
|
| 182 |
-
while (mark.firstChild) parent.insertBefore(mark.firstChild, mark);
|
| 183 |
-
parent.removeChild(mark);
|
| 184 |
-
}
|
| 185 |
});
|
| 186 |
currentHighlights = [];
|
| 187 |
}
|
| 188 |
|
| 189 |
-
// ─── Auto-scan
|
| 190 |
-
// Debounced to handle SPAs
|
| 191 |
let scanTimeout = null;
|
| 192 |
-
function debouncedScan() {
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
}
|
| 196 |
-
|
| 197 |
-
// Scan on initial load
|
| 198 |
-
if (document.readyState === "complete") {
|
| 199 |
-
debouncedScan();
|
| 200 |
-
} else {
|
| 201 |
-
window.addEventListener("load", debouncedScan);
|
| 202 |
-
}
|
| 203 |
|
| 204 |
-
// Re-scan on SPA navigation (MutationObserver)
|
| 205 |
const observer = new MutationObserver(mutations => {
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
);
|
| 209 |
-
if (hasSignificantChange) debouncedScan();
|
| 210 |
});
|
| 211 |
-
|
| 212 |
observer.observe(document.body, { childList: true, subtree: true });
|
| 213 |
|
| 214 |
-
// ───
|
| 215 |
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
| 216 |
-
if (msg.type === "TRIGGER_SCAN") {
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
return true;
|
| 220 |
-
}
|
| 221 |
-
if (msg.type === "CLEAR_HIGHLIGHTS") {
|
| 222 |
-
clearHighlights();
|
| 223 |
-
lastScannedText = "";
|
| 224 |
-
sendResponse({ done: true });
|
| 225 |
-
}
|
| 226 |
-
if (msg.type === "GET_PAGE_TEXT") {
|
| 227 |
-
sendResponse({ text: extractPageText(), url: window.location.href });
|
| 228 |
-
}
|
| 229 |
});
|
| 230 |
})();
|
|
|
|
| 1 |
/**
|
| 2 |
* ClauseGuard — Content Script
|
| 3 |
+
* Page scanning + highlighting + auth bridge (listens for website auth sync).
|
|
|
|
| 4 |
*/
|
| 5 |
|
| 6 |
(() => {
|
| 7 |
"use strict";
|
|
|
|
|
|
|
| 8 |
if (!document.body || document.contentType !== "text/html") return;
|
| 9 |
|
| 10 |
let lastScannedText = "";
|
| 11 |
let isScanning = false;
|
| 12 |
let currentHighlights = [];
|
| 13 |
|
| 14 |
+
// ─── Auth Bridge: Listen for auth sync from our website ───
|
| 15 |
+
window.addEventListener("message", (event) => {
|
| 16 |
+
if (event.data?.type === "CLAUSEGUARD_AUTH_SYNC") {
|
| 17 |
+
chrome.runtime.sendMessage({
|
| 18 |
+
type: "ANALYZE_TEXT", // dummy — just to keep SW alive
|
| 19 |
+
}).catch(() => {});
|
| 20 |
+
|
| 21 |
+
// Store auth in extension
|
| 22 |
+
chrome.storage.sync.set({
|
| 23 |
+
authToken: event.data.token || "",
|
| 24 |
+
email: event.data.email || "",
|
| 25 |
+
userName: event.data.name || "",
|
| 26 |
+
userId: event.data.userId || "",
|
| 27 |
+
plan: event.data.plan || "free",
|
| 28 |
+
authSource: "website",
|
| 29 |
+
lastSyncAt: Date.now(),
|
| 30 |
+
});
|
| 31 |
+
}
|
| 32 |
+
});
|
| 33 |
+
|
| 34 |
+
// ─── Extract page text ───
|
| 35 |
function extractPageText() {
|
| 36 |
const clone = document.body.cloneNode(true);
|
| 37 |
+
["script","style","nav","footer","header","aside","noscript","iframe","svg"]
|
| 38 |
+
.forEach(tag => clone.querySelectorAll(tag).forEach(el => el.remove()));
|
| 39 |
+
clone.querySelectorAll('[class*="cookie"],[class*="banner"],[class*="modal"],[id*="cookie"]')
|
|
|
|
|
|
|
| 40 |
.forEach(el => el.remove());
|
|
|
|
| 41 |
return clone.innerText || clone.textContent || "";
|
| 42 |
}
|
| 43 |
|
| 44 |
+
// ─── Scan ───
|
| 45 |
async function scanPage() {
|
| 46 |
if (isScanning) return;
|
| 47 |
isScanning = true;
|
|
|
|
| 48 |
const text = extractPageText();
|
| 49 |
+
if (!text || text.length < 100 || text === lastScannedText) { isScanning = false; return; }
|
|
|
|
|
|
|
|
|
|
| 50 |
lastScannedText = text;
|
|
|
|
| 51 |
try {
|
| 52 |
+
const results = await chrome.runtime.sendMessage({ type: "ANALYZE_TEXT", payload: { text, url: window.location.href } });
|
| 53 |
+
if (results && !results.error) { clearHighlights(); highlightResults(results); }
|
| 54 |
+
} catch (err) { console.error("ClauseGuard:", err); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
isScanning = false;
|
| 56 |
}
|
| 57 |
|
| 58 |
+
// ─── Highlight ───
|
| 59 |
function highlightResults(results) {
|
| 60 |
if (!results.results) return;
|
| 61 |
+
results.results.filter(r => r.categories?.length > 0).forEach(item => {
|
| 62 |
+
if (item.text.trim().length < 20) return;
|
| 63 |
+
findTextNodes(document.body, item.text).forEach(({ node, startOffset, endOffset }) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
wrapTextRange(node, startOffset, endOffset, item);
|
| 65 |
});
|
| 66 |
});
|
| 67 |
}
|
| 68 |
|
|
|
|
| 69 |
function findTextNodes(root, searchText) {
|
| 70 |
const results = [];
|
| 71 |
+
const searchLower = searchText.toLowerCase().substring(0, 80);
|
| 72 |
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
| 73 |
acceptNode(node) {
|
| 74 |
+
if (node.parentElement?.closest(".clauseguard-highlight, script, style, nav")) return NodeFilter.FILTER_REJECT;
|
|
|
|
|
|
|
| 75 |
return node.textContent.length > 20 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
|
| 76 |
},
|
| 77 |
});
|
|
|
|
| 78 |
let node;
|
| 79 |
while ((node = walker.nextNode())) {
|
| 80 |
+
const idx = node.textContent.toLowerCase().indexOf(searchLower);
|
|
|
|
| 81 |
if (idx !== -1) {
|
| 82 |
+
results.push({ node, startOffset: idx, endOffset: Math.min(idx + searchText.length, node.textContent.length) });
|
| 83 |
+
break;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
}
|
| 85 |
}
|
| 86 |
return results;
|
| 87 |
}
|
| 88 |
|
|
|
|
| 89 |
function wrapTextRange(textNode, start, end, clauseData) {
|
| 90 |
try {
|
| 91 |
const range = document.createRange();
|
| 92 |
+
range.setStart(textNode, start); range.setEnd(textNode, end);
|
| 93 |
+
const severity = clauseData.categories.reduce((m, c) => ({ HIGH:3,MEDIUM:2,LOW:1 }[c.severity] > ({ HIGH:3,MEDIUM:2,LOW:1 }[m]) ? c.severity : m), "LOW");
|
|
|
|
|
|
|
| 94 |
const mark = document.createElement("mark");
|
| 95 |
mark.className = `clauseguard-highlight clauseguard-${severity.toLowerCase()}`;
|
| 96 |
mark.dataset.categories = JSON.stringify(clauseData.categories);
|
|
|
|
|
|
|
|
|
|
| 97 |
mark.addEventListener("mouseenter", showTooltip);
|
| 98 |
mark.addEventListener("mouseleave", hideTooltip);
|
| 99 |
+
mark.addEventListener("click", () => chrome.runtime.sendMessage({ type: "OPEN_SIDEPANEL" }));
|
|
|
|
| 100 |
range.surroundContents(mark);
|
| 101 |
currentHighlights.push(mark);
|
| 102 |
+
} catch (e) {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
}
|
| 104 |
|
| 105 |
// ─── Tooltip ───
|
| 106 |
let tooltipEl = null;
|
|
|
|
| 107 |
function showTooltip(e) {
|
| 108 |
hideTooltip();
|
| 109 |
+
const cats = JSON.parse(e.currentTarget.dataset.categories || "[]");
|
| 110 |
+
if (!cats.length) return;
|
|
|
|
|
|
|
| 111 |
tooltipEl = document.createElement("div");
|
| 112 |
tooltipEl.className = "clauseguard-tooltip";
|
| 113 |
+
tooltipEl.innerHTML = `<div class="clauseguard-tooltip-header">ClauseGuard</div>` +
|
| 114 |
+
cats.map(c => `<div class="clauseguard-tooltip-item"><span class="clauseguard-tooltip-badge clauseguard-badge-${c.severity.toLowerCase()}">${c.severity}</span><span>${c.name}</span></div>`).join("") +
|
| 115 |
+
`<div class="clauseguard-tooltip-footer">Click for details</div>`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
document.body.appendChild(tooltipEl);
|
| 117 |
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
|
|
| 118 |
tooltipEl.style.left = `${rect.left + window.scrollX}px`;
|
| 119 |
tooltipEl.style.top = `${rect.bottom + window.scrollY + 8}px`;
|
| 120 |
}
|
| 121 |
+
function hideTooltip() { if (tooltipEl) { tooltipEl.remove(); tooltipEl = null; } }
|
| 122 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
function clearHighlights() {
|
| 124 |
currentHighlights.forEach(mark => {
|
| 125 |
+
const p = mark.parentNode;
|
| 126 |
+
if (p) { while (mark.firstChild) p.insertBefore(mark.firstChild, mark); p.removeChild(mark); }
|
|
|
|
|
|
|
|
|
|
| 127 |
});
|
| 128 |
currentHighlights = [];
|
| 129 |
}
|
| 130 |
|
| 131 |
+
// ─── Auto-scan ───
|
|
|
|
| 132 |
let scanTimeout = null;
|
| 133 |
+
function debouncedScan() { clearTimeout(scanTimeout); scanTimeout = setTimeout(scanPage, 1500); }
|
| 134 |
+
if (document.readyState === "complete") debouncedScan();
|
| 135 |
+
else window.addEventListener("load", debouncedScan);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
|
|
|
|
| 137 |
const observer = new MutationObserver(mutations => {
|
| 138 |
+
if (mutations.some(m => m.addedNodes.length > 0 && [...m.addedNodes].some(n => n.nodeType === 1 && n.textContent?.length > 100)))
|
| 139 |
+
debouncedScan();
|
|
|
|
|
|
|
| 140 |
});
|
|
|
|
| 141 |
observer.observe(document.body, { childList: true, subtree: true });
|
| 142 |
|
| 143 |
+
// ─── Messages from popup/sidepanel ───
|
| 144 |
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
| 145 |
+
if (msg.type === "TRIGGER_SCAN") { lastScannedText = ""; scanPage().then(() => sendResponse({ done: true })); return true; }
|
| 146 |
+
if (msg.type === "CLEAR_HIGHLIGHTS") { clearHighlights(); lastScannedText = ""; sendResponse({ done: true }); }
|
| 147 |
+
if (msg.type === "GET_PAGE_TEXT") { sendResponse({ text: extractPageText(), url: window.location.href }); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
});
|
| 149 |
})();
|
extension/manifest.json
CHANGED
|
@@ -1,16 +1,19 @@
|
|
| 1 |
{
|
| 2 |
"manifest_version": 3,
|
| 3 |
"name": "ClauseGuard — AI Fine Print Scanner",
|
| 4 |
-
"version": "1.0.
|
| 5 |
"description": "Highlights unfair clauses in Terms of Service, contracts, and lease agreements.",
|
| 6 |
"permissions": [
|
| 7 |
"activeTab",
|
| 8 |
"storage",
|
| 9 |
"sidePanel",
|
| 10 |
-
"scripting"
|
|
|
|
| 11 |
],
|
| 12 |
"host_permissions": [
|
| 13 |
-
"https://gaurv007-clauseguard-api.hf.space/*"
|
|
|
|
|
|
|
| 14 |
],
|
| 15 |
"background": {
|
| 16 |
"service_worker": "background.js"
|
|
@@ -36,7 +39,11 @@
|
|
| 36 |
}
|
| 37 |
],
|
| 38 |
"externally_connectable": {
|
| 39 |
-
"matches": [
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
},
|
| 41 |
"icons": {
|
| 42 |
"16": "icons/icon16.png",
|
|
|
|
| 1 |
{
|
| 2 |
"manifest_version": 3,
|
| 3 |
"name": "ClauseGuard — AI Fine Print Scanner",
|
| 4 |
+
"version": "1.0.1",
|
| 5 |
"description": "Highlights unfair clauses in Terms of Service, contracts, and lease agreements.",
|
| 6 |
"permissions": [
|
| 7 |
"activeTab",
|
| 8 |
"storage",
|
| 9 |
"sidePanel",
|
| 10 |
+
"scripting",
|
| 11 |
+
"cookies"
|
| 12 |
],
|
| 13 |
"host_permissions": [
|
| 14 |
+
"https://gaurv007-clauseguard-api.hf.space/*",
|
| 15 |
+
"https://*.clauseguard.com/*",
|
| 16 |
+
"https://*.netlify.app/*"
|
| 17 |
],
|
| 18 |
"background": {
|
| 19 |
"service_worker": "background.js"
|
|
|
|
| 39 |
}
|
| 40 |
],
|
| 41 |
"externally_connectable": {
|
| 42 |
+
"matches": [
|
| 43 |
+
"https://*.clauseguard.com/*",
|
| 44 |
+
"https://*.netlify.app/*",
|
| 45 |
+
"http://localhost:3000/*"
|
| 46 |
+
]
|
| 47 |
},
|
| 48 |
"icons": {
|
| 49 |
"16": "icons/icon16.png",
|
extension/popup.html
CHANGED
|
@@ -8,9 +8,24 @@
|
|
| 8 |
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 9 |
body { width: 340px; font-family: system-ui, -apple-system, sans-serif; background: #fff; color: #18181b; font-size: 13px; }
|
| 10 |
|
| 11 |
-
.header { padding:
|
| 12 |
-
.header svg { width: 18px; height: 18px;
|
| 13 |
-
.header h1 { font-size: 15px; font-weight: 600; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
.score-card { padding: 16px; }
|
| 16 |
.score-row { display: flex; align-items: baseline; justify-content: space-between; }
|
|
@@ -23,26 +38,27 @@
|
|
| 23 |
|
| 24 |
.bar-wrap { margin-top: 10px; height: 4px; background: #f4f4f5; border-radius: 99px; overflow: hidden; }
|
| 25 |
.bar-fill { height: 100%; border-radius: 99px; transition: width 0.6s ease; }
|
| 26 |
-
.bar-red { background: #ef4444; }
|
| 27 |
-
|
| 28 |
-
.
|
| 29 |
|
| 30 |
.counts { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 6px; padding: 0 16px 12px; }
|
| 31 |
.count-box { text-align: center; padding: 8px 4px; border-radius: 8px; border: 1px solid #f4f4f5; }
|
| 32 |
.count-num { font-size: 18px; font-weight: 600; }
|
| 33 |
.count-label { font-size: 10px; margin-top: 2px; display: flex; align-items: center; justify-content: center; gap: 4px; }
|
| 34 |
.dot { width: 6px; height: 6px; border-radius: 50%; }
|
| 35 |
-
.dot-red { background: #ef4444; }
|
| 36 |
-
.dot-amber { background: #f59e0b; }
|
| 37 |
-
.dot-blue { background: #3b82f6; }
|
| 38 |
|
| 39 |
.actions { padding: 0 16px 12px; display: flex; flex-direction: column; gap: 6px; }
|
| 40 |
.btn { display: flex; align-items: center; justify-content: center; gap: 6px; padding: 10px; border: none; border-radius: 8px; font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.15s; }
|
| 41 |
.btn svg { width: 15px; height: 15px; }
|
| 42 |
-
.btn-primary { background: #18181b; color: #fff; }
|
| 43 |
-
.btn-
|
| 44 |
-
|
| 45 |
-
.
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
.usage { padding: 10px 16px; border-top: 1px solid #f4f4f5; display: flex; align-items: center; justify-content: space-between; }
|
| 48 |
.usage-text { font-size: 11px; color: #a1a1aa; }
|
|
@@ -51,19 +67,10 @@
|
|
| 51 |
|
| 52 |
.empty { padding: 40px 16px; text-align: center; }
|
| 53 |
.empty svg { width: 32px; height: 32px; color: #d4d4d8; margin: 0 auto 8px; }
|
| 54 |
-
.empty p { color: #a1a1aa;
|
| 55 |
|
| 56 |
-
.
|
| 57 |
-
.
|
| 58 |
-
.limit-banner .limit-title { font-weight: 600; margin-bottom: 4px; display: flex; align-items: center; gap: 6px; }
|
| 59 |
-
.limit-banner .limit-title svg { width: 14px; height: 14px; }
|
| 60 |
-
.btn-upgrade { background: #18181b; color: #fff; margin-top: 8px; display: flex; align-items: center; justify-content: center; gap: 6px; padding: 8px; border: none; border-radius: 6px; font-size: 12px; font-weight: 500; cursor: pointer; width: 100%; }
|
| 61 |
-
.btn-upgrade:hover { background: #27272a; }
|
| 62 |
-
.btn-upgrade svg { width: 13px; height: 13px; }
|
| 63 |
-
|
| 64 |
-
.footer { padding: 8px 16px; border-top: 1px solid #f4f4f5; display: flex; justify-content: space-between; align-items: center; }
|
| 65 |
-
.footer a { color: #a1a1aa; text-decoration: none; font-size: 11px; }
|
| 66 |
-
.footer a:hover { color: #52525b; }
|
| 67 |
</style>
|
| 68 |
</head>
|
| 69 |
<body>
|
|
@@ -72,19 +79,23 @@
|
|
| 72 |
<h1>ClauseGuard</h1>
|
| 73 |
</div>
|
| 74 |
|
| 75 |
-
<!--
|
| 76 |
-
<div id="
|
| 77 |
-
<div class="
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
</div>
|
| 89 |
|
| 90 |
<div id="results-view" style="display:none;">
|
|
@@ -94,6 +105,7 @@
|
|
| 94 |
<span class="grade" id="grade-badge"></span>
|
| 95 |
</div>
|
| 96 |
<div class="bar-wrap"><div class="bar-fill" id="bar-fill" style="width:0%"></div></div>
|
|
|
|
| 97 |
</div>
|
| 98 |
<div class="counts">
|
| 99 |
<div class="count-box"><div class="count-num" id="c-high">0</div><div class="count-label"><span class="dot dot-red"></span>High</div></div>
|
|
@@ -103,35 +115,33 @@
|
|
| 103 |
<div class="actions">
|
| 104 |
<button class="btn btn-primary" id="btn-details">
|
| 105 |
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
|
| 106 |
-
View
|
| 107 |
</button>
|
| 108 |
<button class="btn btn-secondary" id="btn-rescan">
|
| 109 |
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>
|
| 110 |
-
Re-scan
|
| 111 |
</button>
|
| 112 |
</div>
|
| 113 |
</div>
|
| 114 |
|
| 115 |
-
<div id="empty-view">
|
| 116 |
<div class="empty">
|
| 117 |
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><path d="M7 12h10"/></svg>
|
| 118 |
<p>No scan results yet.</p>
|
| 119 |
</div>
|
| 120 |
<div class="actions">
|
| 121 |
-
<button class="btn btn-primary" id="btn-scan">
|
| 122 |
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><path d="M7 12h10"/></svg>
|
| 123 |
-
Scan this page
|
| 124 |
-
</button>
|
| 125 |
</div>
|
| 126 |
</div>
|
| 127 |
|
| 128 |
<div class="usage">
|
| 129 |
-
<span class="usage-text" id="usage-text">
|
| 130 |
<div class="usage-bar"><div class="usage-fill" id="usage-fill" style="width:0%"></div></div>
|
| 131 |
</div>
|
| 132 |
|
| 133 |
<div class="footer">
|
| 134 |
<a href="https://clauseguard.com" target="_blank">clauseguard.com</a>
|
|
|
|
| 135 |
<a href="https://clauseguard.com/#pricing" target="_blank">Upgrade</a>
|
| 136 |
</div>
|
| 137 |
|
|
|
|
| 8 |
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 9 |
body { width: 340px; font-family: system-ui, -apple-system, sans-serif; background: #fff; color: #18181b; font-size: 13px; }
|
| 10 |
|
| 11 |
+
.header { padding: 14px 16px; border-bottom: 1px solid #f4f4f5; display: flex; align-items: center; gap: 8px; }
|
| 12 |
+
.header svg { width: 18px; height: 18px; }
|
| 13 |
+
.header h1 { font-size: 15px; font-weight: 600; flex: 1; }
|
| 14 |
+
|
| 15 |
+
/* User bar */
|
| 16 |
+
.user-bar { padding: 10px 16px; border-bottom: 1px solid #f4f4f5; display: flex; align-items: center; gap: 8px; }
|
| 17 |
+
.user-avatar { width: 28px; height: 28px; border-radius: 50%; background: #f4f4f5; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 600; color: #71717a; }
|
| 18 |
+
.user-info { flex: 1; min-width: 0; }
|
| 19 |
+
.user-info p { font-size: 12px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
| 20 |
+
.plan-badge { font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 4px; }
|
| 21 |
+
.plan-free { background: #f4f4f5; color: #71717a; }
|
| 22 |
+
.plan-pro { background: #eff6ff; color: #1d4ed8; }
|
| 23 |
+
.plan-team { background: #f5f3ff; color: #7c3aed; }
|
| 24 |
+
|
| 25 |
+
.guest-bar { padding: 10px 16px; border-bottom: 1px solid #f4f4f5; display: flex; align-items: center; justify-content: space-between; }
|
| 26 |
+
.guest-bar p { font-size: 12px; color: #a1a1aa; }
|
| 27 |
+
.btn-link { background: none; border: none; color: #18181b; font-size: 12px; font-weight: 500; cursor: pointer; text-decoration: underline; }
|
| 28 |
+
.btn-link:hover { color: #3f3f46; }
|
| 29 |
|
| 30 |
.score-card { padding: 16px; }
|
| 31 |
.score-row { display: flex; align-items: baseline; justify-content: space-between; }
|
|
|
|
| 38 |
|
| 39 |
.bar-wrap { margin-top: 10px; height: 4px; background: #f4f4f5; border-radius: 99px; overflow: hidden; }
|
| 40 |
.bar-fill { height: 100%; border-radius: 99px; transition: width 0.6s ease; }
|
| 41 |
+
.bar-red { background: #ef4444; } .bar-amber { background: #f59e0b; } .bar-green { background: #22c55e; }
|
| 42 |
+
|
| 43 |
+
.scan-meta { margin-top: 8px; font-size: 11px; color: #a1a1aa; }
|
| 44 |
|
| 45 |
.counts { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 6px; padding: 0 16px 12px; }
|
| 46 |
.count-box { text-align: center; padding: 8px 4px; border-radius: 8px; border: 1px solid #f4f4f5; }
|
| 47 |
.count-num { font-size: 18px; font-weight: 600; }
|
| 48 |
.count-label { font-size: 10px; margin-top: 2px; display: flex; align-items: center; justify-content: center; gap: 4px; }
|
| 49 |
.dot { width: 6px; height: 6px; border-radius: 50%; }
|
| 50 |
+
.dot-red { background: #ef4444; } .dot-amber { background: #f59e0b; } .dot-blue { background: #3b82f6; }
|
|
|
|
|
|
|
| 51 |
|
| 52 |
.actions { padding: 0 16px 12px; display: flex; flex-direction: column; gap: 6px; }
|
| 53 |
.btn { display: flex; align-items: center; justify-content: center; gap: 6px; padding: 10px; border: none; border-radius: 8px; font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.15s; }
|
| 54 |
.btn svg { width: 15px; height: 15px; }
|
| 55 |
+
.btn-primary { background: #18181b; color: #fff; } .btn-primary:hover { background: #27272a; }
|
| 56 |
+
.btn-secondary { background: #f4f4f5; color: #3f3f46; } .btn-secondary:hover { background: #e4e4e7; }
|
| 57 |
+
|
| 58 |
+
.limit-banner { margin: 0 16px 12px; padding: 12px; border-radius: 8px; background: #fef2f2; border: 1px solid #fecaca; display: none; }
|
| 59 |
+
.limit-banner p { font-size: 12px; color: #991b1b; line-height: 1.5; }
|
| 60 |
+
.limit-title { font-weight: 600; margin-bottom: 4px; }
|
| 61 |
+
.btn-upgrade { background: #18181b; color: #fff; margin-top: 8px; display: flex; align-items: center; justify-content: center; gap: 6px; padding: 8px; border: none; border-radius: 6px; font-size: 12px; font-weight: 500; cursor: pointer; width: 100%; text-decoration: none; }
|
| 62 |
|
| 63 |
.usage { padding: 10px 16px; border-top: 1px solid #f4f4f5; display: flex; align-items: center; justify-content: space-between; }
|
| 64 |
.usage-text { font-size: 11px; color: #a1a1aa; }
|
|
|
|
| 67 |
|
| 68 |
.empty { padding: 40px 16px; text-align: center; }
|
| 69 |
.empty svg { width: 32px; height: 32px; color: #d4d4d8; margin: 0 auto 8px; }
|
| 70 |
+
.empty p { color: #a1a1aa; }
|
| 71 |
|
| 72 |
+
.footer { padding: 8px 16px; border-top: 1px solid #f4f4f5; display: flex; justify-content: space-between; }
|
| 73 |
+
.footer a { color: #a1a1aa; text-decoration: none; font-size: 11px; } .footer a:hover { color: #52525b; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
</style>
|
| 75 |
</head>
|
| 76 |
<body>
|
|
|
|
| 79 |
<h1>ClauseGuard</h1>
|
| 80 |
</div>
|
| 81 |
|
| 82 |
+
<!-- Logged in user -->
|
| 83 |
+
<div id="user-bar" class="user-bar" style="display:none;">
|
| 84 |
+
<div class="user-avatar" id="user-avatar">?</div>
|
| 85 |
+
<div class="user-info"><p id="user-email">Loading...</p></div>
|
| 86 |
+
<span class="plan-badge plan-free" id="user-plan">FREE</span>
|
| 87 |
+
</div>
|
| 88 |
+
|
| 89 |
+
<!-- Guest -->
|
| 90 |
+
<div id="guest-bar" class="guest-bar" style="display:none;">
|
| 91 |
+
<p>Guest mode</p>
|
| 92 |
+
<button class="btn-link" id="btn-login">Sign in for more</button>
|
| 93 |
+
</div>
|
| 94 |
+
|
| 95 |
+
<div id="limit-banner" class="limit-banner">
|
| 96 |
+
<p class="limit-title">Free limit reached</p>
|
| 97 |
+
<p>Upgrade for unlimited scans.</p>
|
| 98 |
+
<a href="https://clauseguard.com/#pricing" target="_blank" class="btn-upgrade">View plans</a>
|
| 99 |
</div>
|
| 100 |
|
| 101 |
<div id="results-view" style="display:none;">
|
|
|
|
| 105 |
<span class="grade" id="grade-badge"></span>
|
| 106 |
</div>
|
| 107 |
<div class="bar-wrap"><div class="bar-fill" id="bar-fill" style="width:0%"></div></div>
|
| 108 |
+
<div class="scan-meta">Engine: <span id="scan-source"></span></div>
|
| 109 |
</div>
|
| 110 |
<div class="counts">
|
| 111 |
<div class="count-box"><div class="count-num" id="c-high">0</div><div class="count-label"><span class="dot dot-red"></span>High</div></div>
|
|
|
|
| 115 |
<div class="actions">
|
| 116 |
<button class="btn btn-primary" id="btn-details">
|
| 117 |
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
|
| 118 |
+
View details
|
| 119 |
</button>
|
| 120 |
<button class="btn btn-secondary" id="btn-rescan">
|
| 121 |
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>
|
| 122 |
+
Re-scan
|
| 123 |
</button>
|
| 124 |
</div>
|
| 125 |
</div>
|
| 126 |
|
| 127 |
+
<div id="empty-view" style="display:none;">
|
| 128 |
<div class="empty">
|
| 129 |
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><path d="M7 12h10"/></svg>
|
| 130 |
<p>No scan results yet.</p>
|
| 131 |
</div>
|
| 132 |
<div class="actions">
|
| 133 |
+
<button class="btn btn-primary" id="btn-scan">Scan this page</button>
|
|
|
|
|
|
|
|
|
|
| 134 |
</div>
|
| 135 |
</div>
|
| 136 |
|
| 137 |
<div class="usage">
|
| 138 |
+
<span class="usage-text" id="usage-text">0/10 scans</span>
|
| 139 |
<div class="usage-bar"><div class="usage-fill" id="usage-fill" style="width:0%"></div></div>
|
| 140 |
</div>
|
| 141 |
|
| 142 |
<div class="footer">
|
| 143 |
<a href="https://clauseguard.com" target="_blank">clauseguard.com</a>
|
| 144 |
+
<a href="https://clauseguard.com/dashboard-pages/dashboard" target="_blank">Dashboard</a>
|
| 145 |
<a href="https://clauseguard.com/#pricing" target="_blank">Upgrade</a>
|
| 146 |
</div>
|
| 147 |
|
extension/popup.js
CHANGED
|
@@ -1,26 +1,44 @@
|
|
| 1 |
/**
|
| 2 |
* ClauseGuard — Popup Script
|
|
|
|
| 3 |
*/
|
| 4 |
|
| 5 |
document.addEventListener("DOMContentLoaded", async () => {
|
| 6 |
const resultsView = document.getElementById("results-view");
|
| 7 |
const emptyView = document.getElementById("empty-view");
|
| 8 |
const limitBanner = document.getElementById("limit-banner");
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
| 11 |
|
| 12 |
let tab;
|
| 13 |
try { const [t] = await chrome.tabs.query({ active: true, currentWindow: true }); tab = t; } catch { return; }
|
| 14 |
if (!tab?.id) return;
|
| 15 |
|
| 16 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
let usage = null;
|
| 18 |
try { usage = await chrome.runtime.sendMessage({ type: "CHECK_USAGE" }); } catch {}
|
| 19 |
updateUsage(usage);
|
| 20 |
|
| 21 |
-
if (usage && !usage.allowed && limitBanner)
|
| 22 |
-
limitBanner.style.display = "block";
|
| 23 |
-
}
|
| 24 |
|
| 25 |
// Load results
|
| 26 |
let results = null;
|
|
@@ -29,66 +47,48 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
| 29 |
if (results && results.risk_score !== undefined) {
|
| 30 |
showResults(results);
|
| 31 |
} else {
|
| 32 |
-
emptyView.style.display = "block";
|
| 33 |
-
resultsView.style.display = "none";
|
| 34 |
}
|
| 35 |
|
| 36 |
// Scan button
|
| 37 |
const btnScan = document.getElementById("btn-scan");
|
| 38 |
if (btnScan) {
|
| 39 |
btnScan.addEventListener("click", async () => {
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
if (limitBanner) limitBanner.style.display = "block";
|
| 43 |
-
return;
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
-
btnScan.textContent = "Scanning...";
|
| 47 |
-
btnScan.disabled = true;
|
| 48 |
-
|
| 49 |
try {
|
| 50 |
await chrome.tabs.sendMessage(tab.id, { type: "TRIGGER_SCAN" });
|
| 51 |
setTimeout(async () => {
|
| 52 |
try {
|
| 53 |
const r = await chrome.runtime.sendMessage({ type: "GET_RESULTS", tabId: tab.id });
|
| 54 |
-
if (r
|
| 55 |
-
} catch {}
|
| 56 |
-
|
| 57 |
-
// Refresh usage after scan
|
| 58 |
-
try {
|
| 59 |
-
usage = await chrome.runtime.sendMessage({ type: "CHECK_USAGE" });
|
| 60 |
-
updateUsage(usage);
|
| 61 |
-
if (usage && !usage.allowed && limitBanner) limitBanner.style.display = "block";
|
| 62 |
} catch {}
|
| 63 |
-
|
| 64 |
-
btnScan.textContent = "Scan this page";
|
| 65 |
-
btnScan.disabled = false;
|
| 66 |
}, 3000);
|
| 67 |
-
} catch {
|
| 68 |
-
btnScan.textContent = "Error — refresh page";
|
| 69 |
-
btnScan.disabled = false;
|
| 70 |
-
}
|
| 71 |
});
|
| 72 |
}
|
| 73 |
|
| 74 |
// Re-scan
|
| 75 |
const btnRescan = document.getElementById("btn-rescan");
|
| 76 |
-
if (btnRescan) {
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
window.close();
|
| 81 |
-
});
|
| 82 |
-
}
|
| 83 |
|
| 84 |
// Details
|
| 85 |
const btnDetails = document.getElementById("btn-details");
|
| 86 |
-
if (btnDetails) {
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
| 92 |
});
|
| 93 |
|
| 94 |
function showResults(results) {
|
|
@@ -97,31 +97,28 @@ function showResults(results) {
|
|
| 97 |
if (rv) rv.style.display = "block";
|
| 98 |
if (ev) ev.style.display = "none";
|
| 99 |
|
| 100 |
-
const
|
| 101 |
-
if (
|
| 102 |
|
| 103 |
const grade = results.grade || "C";
|
| 104 |
-
const badge =
|
| 105 |
-
if (badge) {
|
| 106 |
-
badge.className = "grade grade-" + grade.toLowerCase();
|
| 107 |
-
badge.textContent = "Grade " + grade;
|
| 108 |
-
}
|
| 109 |
|
| 110 |
-
const bar =
|
| 111 |
if (bar) {
|
| 112 |
bar.style.width = results.risk_score + "%";
|
| 113 |
bar.className = "bar-fill " + (results.risk_score >= 60 ? "bar-red" : results.risk_score >= 30 ? "bar-amber" : "bar-green");
|
| 114 |
}
|
| 115 |
|
| 116 |
const counts = { HIGH: 0, MEDIUM: 0, LOW: 0 };
|
| 117 |
-
(results.results || []).forEach(r => (r.categories || []).forEach(c => {
|
| 118 |
-
if (counts[c.severity] !== undefined) counts[c.severity]++;
|
| 119 |
-
}));
|
| 120 |
-
|
| 121 |
-
const el = (id) => document.getElementById(id);
|
| 122 |
if (el("c-high")) el("c-high").textContent = counts.HIGH;
|
| 123 |
if (el("c-med")) el("c-med").textContent = counts.MEDIUM;
|
| 124 |
if (el("c-low")) el("c-low").textContent = counts.LOW;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
}
|
| 126 |
|
| 127 |
function updateUsage(usage) {
|
|
@@ -129,16 +126,14 @@ function updateUsage(usage) {
|
|
| 129 |
const text = document.getElementById("usage-text");
|
| 130 |
const fill = document.getElementById("usage-fill");
|
| 131 |
if (!text || !fill) return;
|
| 132 |
-
|
| 133 |
if (usage.plan === "free") {
|
| 134 |
-
text.textContent =
|
| 135 |
const pct = Math.min(100, (usage.used / usage.limit) * 100);
|
| 136 |
fill.style.width = pct + "%";
|
| 137 |
if (pct >= 100) fill.style.background = "#ef4444";
|
| 138 |
else if (pct >= 70) fill.style.background = "#f59e0b";
|
| 139 |
} else {
|
| 140 |
-
text.textContent =
|
| 141 |
-
fill.style.width = "100%";
|
| 142 |
-
fill.style.background = "#22c55e";
|
| 143 |
}
|
| 144 |
}
|
|
|
|
| 1 |
/**
|
| 2 |
* ClauseGuard — Popup Script
|
| 3 |
+
* Shows user status (logged in / guest), scan results, usage.
|
| 4 |
*/
|
| 5 |
|
| 6 |
document.addEventListener("DOMContentLoaded", async () => {
|
| 7 |
const resultsView = document.getElementById("results-view");
|
| 8 |
const emptyView = document.getElementById("empty-view");
|
| 9 |
const limitBanner = document.getElementById("limit-banner");
|
| 10 |
+
const userBar = document.getElementById("user-bar");
|
| 11 |
+
const userEmail = document.getElementById("user-email");
|
| 12 |
+
const userPlan = document.getElementById("user-plan");
|
| 13 |
+
const guestBar = document.getElementById("guest-bar");
|
| 14 |
|
| 15 |
let tab;
|
| 16 |
try { const [t] = await chrome.tabs.query({ active: true, currentWindow: true }); tab = t; } catch { return; }
|
| 17 |
if (!tab?.id) return;
|
| 18 |
|
| 19 |
+
// Load user info
|
| 20 |
+
let auth = null;
|
| 21 |
+
try { auth = await chrome.runtime.sendMessage({ type: "GET_USER" }); } catch {}
|
| 22 |
+
|
| 23 |
+
if (auth?.isLoggedIn) {
|
| 24 |
+
if (userBar) userBar.style.display = "flex";
|
| 25 |
+
if (guestBar) guestBar.style.display = "none";
|
| 26 |
+
if (userEmail) userEmail.textContent = auth.email || "User";
|
| 27 |
+
if (userPlan) {
|
| 28 |
+
userPlan.textContent = auth.plan?.toUpperCase() || "FREE";
|
| 29 |
+
userPlan.className = "plan-badge plan-" + (auth.plan || "free");
|
| 30 |
+
}
|
| 31 |
+
} else {
|
| 32 |
+
if (userBar) userBar.style.display = "none";
|
| 33 |
+
if (guestBar) guestBar.style.display = "flex";
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// Check usage
|
| 37 |
let usage = null;
|
| 38 |
try { usage = await chrome.runtime.sendMessage({ type: "CHECK_USAGE" }); } catch {}
|
| 39 |
updateUsage(usage);
|
| 40 |
|
| 41 |
+
if (usage && !usage.allowed && limitBanner) limitBanner.style.display = "block";
|
|
|
|
|
|
|
| 42 |
|
| 43 |
// Load results
|
| 44 |
let results = null;
|
|
|
|
| 47 |
if (results && results.risk_score !== undefined) {
|
| 48 |
showResults(results);
|
| 49 |
} else {
|
| 50 |
+
if (emptyView) emptyView.style.display = "block";
|
| 51 |
+
if (resultsView) resultsView.style.display = "none";
|
| 52 |
}
|
| 53 |
|
| 54 |
// Scan button
|
| 55 |
const btnScan = document.getElementById("btn-scan");
|
| 56 |
if (btnScan) {
|
| 57 |
btnScan.addEventListener("click", async () => {
|
| 58 |
+
if (usage && !usage.allowed) { if (limitBanner) limitBanner.style.display = "block"; return; }
|
| 59 |
+
btnScan.textContent = "Scanning..."; btnScan.disabled = true;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
try {
|
| 61 |
await chrome.tabs.sendMessage(tab.id, { type: "TRIGGER_SCAN" });
|
| 62 |
setTimeout(async () => {
|
| 63 |
try {
|
| 64 |
const r = await chrome.runtime.sendMessage({ type: "GET_RESULTS", tabId: tab.id });
|
| 65 |
+
if (r?.risk_score !== undefined) showResults(r);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
} catch {}
|
| 67 |
+
try { usage = await chrome.runtime.sendMessage({ type: "CHECK_USAGE" }); updateUsage(usage); } catch {}
|
| 68 |
+
btnScan.textContent = "Scan this page"; btnScan.disabled = false;
|
|
|
|
| 69 |
}, 3000);
|
| 70 |
+
} catch { btnScan.textContent = "Error — refresh page"; btnScan.disabled = false; }
|
|
|
|
|
|
|
|
|
|
| 71 |
});
|
| 72 |
}
|
| 73 |
|
| 74 |
// Re-scan
|
| 75 |
const btnRescan = document.getElementById("btn-rescan");
|
| 76 |
+
if (btnRescan) btnRescan.addEventListener("click", async () => {
|
| 77 |
+
if (usage && !usage.allowed) { if (limitBanner) limitBanner.style.display = "block"; return; }
|
| 78 |
+
try { await chrome.tabs.sendMessage(tab.id, { type: "TRIGGER_SCAN" }); } catch {} window.close();
|
| 79 |
+
});
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
// Details
|
| 82 |
const btnDetails = document.getElementById("btn-details");
|
| 83 |
+
if (btnDetails) btnDetails.addEventListener("click", () => {
|
| 84 |
+
try { chrome.sidePanel.open({ tabId: tab.id }); } catch {} window.close();
|
| 85 |
+
});
|
| 86 |
+
|
| 87 |
+
// Login button
|
| 88 |
+
const btnLogin = document.getElementById("btn-login");
|
| 89 |
+
if (btnLogin) btnLogin.addEventListener("click", () => {
|
| 90 |
+
chrome.tabs.create({ url: "https://clauseguard.com/auth/login" }); // Update with your actual URL
|
| 91 |
+
});
|
| 92 |
});
|
| 93 |
|
| 94 |
function showResults(results) {
|
|
|
|
| 97 |
if (rv) rv.style.display = "block";
|
| 98 |
if (ev) ev.style.display = "none";
|
| 99 |
|
| 100 |
+
const el = (id) => document.getElementById(id);
|
| 101 |
+
if (el("risk-score")) el("risk-score").textContent = results.risk_score;
|
| 102 |
|
| 103 |
const grade = results.grade || "C";
|
| 104 |
+
const badge = el("grade-badge");
|
| 105 |
+
if (badge) { badge.className = "grade grade-" + grade.toLowerCase(); badge.textContent = "Grade " + grade; }
|
|
|
|
|
|
|
|
|
|
| 106 |
|
| 107 |
+
const bar = el("bar-fill");
|
| 108 |
if (bar) {
|
| 109 |
bar.style.width = results.risk_score + "%";
|
| 110 |
bar.className = "bar-fill " + (results.risk_score >= 60 ? "bar-red" : results.risk_score >= 30 ? "bar-amber" : "bar-green");
|
| 111 |
}
|
| 112 |
|
| 113 |
const counts = { HIGH: 0, MEDIUM: 0, LOW: 0 };
|
| 114 |
+
(results.results || []).forEach(r => (r.categories || []).forEach(c => { if (counts[c.severity] !== undefined) counts[c.severity]++; }));
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
if (el("c-high")) el("c-high").textContent = counts.HIGH;
|
| 116 |
if (el("c-med")) el("c-med").textContent = counts.MEDIUM;
|
| 117 |
if (el("c-low")) el("c-low").textContent = counts.LOW;
|
| 118 |
+
|
| 119 |
+
// Show source indicator
|
| 120 |
+
const src = el("scan-source");
|
| 121 |
+
if (src) src.textContent = results.source === "api" ? "Legal-BERT" : results.source === "local" ? "Local" : "";
|
| 122 |
}
|
| 123 |
|
| 124 |
function updateUsage(usage) {
|
|
|
|
| 126 |
const text = document.getElementById("usage-text");
|
| 127 |
const fill = document.getElementById("usage-fill");
|
| 128 |
if (!text || !fill) return;
|
|
|
|
| 129 |
if (usage.plan === "free") {
|
| 130 |
+
text.textContent = usage.used + "/" + usage.limit + " scans";
|
| 131 |
const pct = Math.min(100, (usage.used / usage.limit) * 100);
|
| 132 |
fill.style.width = pct + "%";
|
| 133 |
if (pct >= 100) fill.style.background = "#ef4444";
|
| 134 |
else if (pct >= 70) fill.style.background = "#f59e0b";
|
| 135 |
} else {
|
| 136 |
+
text.textContent = "Unlimited";
|
| 137 |
+
fill.style.width = "100%"; fill.style.background = "#22c55e";
|
|
|
|
| 138 |
}
|
| 139 |
}
|
web/app/layout.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import type { Metadata } from "next";
|
| 2 |
import { Nav } from "@/components/nav";
|
|
|
|
| 3 |
import "./globals.css";
|
| 4 |
|
| 5 |
export const metadata: Metadata = {
|
|
@@ -21,6 +22,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|
| 21 |
<body className="antialiased text-zinc-900 bg-white">
|
| 22 |
<Nav />
|
| 23 |
{children}
|
|
|
|
| 24 |
</body>
|
| 25 |
</html>
|
| 26 |
);
|
|
|
|
| 1 |
import type { Metadata } from "next";
|
| 2 |
import { Nav } from "@/components/nav";
|
| 3 |
+
import { ExtensionBridge } from "@/components/extension-bridge";
|
| 4 |
import "./globals.css";
|
| 5 |
|
| 6 |
export const metadata: Metadata = {
|
|
|
|
| 22 |
<body className="antialiased text-zinc-900 bg-white">
|
| 23 |
<Nav />
|
| 24 |
{children}
|
| 25 |
+
<ExtensionBridge />
|
| 26 |
</body>
|
| 27 |
</html>
|
| 28 |
);
|
web/components/extension-bridge.tsx
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { useEffect, useState } from "react";
|
| 4 |
+
import { createClient } from "@/lib/supabase/client";
|
| 5 |
+
|
| 6 |
+
/**
|
| 7 |
+
* ExtensionBridge — Add to root layout.
|
| 8 |
+
* Syncs auth state to ClauseGuard Chrome extension.
|
| 9 |
+
* Also detects if extension is installed.
|
| 10 |
+
*/
|
| 11 |
+
export function ExtensionBridge() {
|
| 12 |
+
const [extensionInstalled, setExtensionInstalled] = useState(false);
|
| 13 |
+
const supabase = createClient();
|
| 14 |
+
|
| 15 |
+
useEffect(() => {
|
| 16 |
+
// Sync auth to extension whenever auth state changes
|
| 17 |
+
const { data: { subscription } } = supabase.auth.onAuthStateChange(async (event, session) => {
|
| 18 |
+
if (event === "SIGNED_IN" && session) {
|
| 19 |
+
// Get user profile for plan info
|
| 20 |
+
const { data: profile } = await supabase
|
| 21 |
+
.from("profiles")
|
| 22 |
+
.select("plan, full_name")
|
| 23 |
+
.eq("id", session.user.id)
|
| 24 |
+
.single();
|
| 25 |
+
|
| 26 |
+
// Method 1: postMessage (content script picks it up)
|
| 27 |
+
window.postMessage({
|
| 28 |
+
type: "CLAUSEGUARD_AUTH_SYNC",
|
| 29 |
+
token: session.access_token,
|
| 30 |
+
email: session.user.email || "",
|
| 31 |
+
name: profile?.full_name || session.user.user_metadata?.full_name || "",
|
| 32 |
+
userId: session.user.id,
|
| 33 |
+
plan: profile?.plan || "free",
|
| 34 |
+
}, "*");
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
if (event === "SIGNED_OUT") {
|
| 38 |
+
window.postMessage({ type: "CLAUSEGUARD_AUTH_SYNC", token: "", email: "", name: "", userId: "", plan: "free" }, "*");
|
| 39 |
+
}
|
| 40 |
+
});
|
| 41 |
+
|
| 42 |
+
// Also sync on initial load if already logged in
|
| 43 |
+
supabase.auth.getUser().then(async ({ data: { user } }) => {
|
| 44 |
+
if (user) {
|
| 45 |
+
const { data: session } = await supabase.auth.getSession();
|
| 46 |
+
const { data: profile } = await supabase.from("profiles").select("plan, full_name").eq("id", user.id).single();
|
| 47 |
+
|
| 48 |
+
window.postMessage({
|
| 49 |
+
type: "CLAUSEGUARD_AUTH_SYNC",
|
| 50 |
+
token: session.session?.access_token || "",
|
| 51 |
+
email: user.email || "",
|
| 52 |
+
name: profile?.full_name || "",
|
| 53 |
+
userId: user.id,
|
| 54 |
+
plan: profile?.plan || "free",
|
| 55 |
+
}, "*");
|
| 56 |
+
}
|
| 57 |
+
});
|
| 58 |
+
|
| 59 |
+
return () => subscription.unsubscribe();
|
| 60 |
+
}, []);
|
| 61 |
+
|
| 62 |
+
// This component renders nothing — it's purely for side effects
|
| 63 |
+
return null;
|
| 64 |
+
}
|