// ============================================================ // PhishGuard AI - popup.js // Popup logic: displays verdict, feedback buttons, retraining // status, and session stats. // ============================================================ (function() { "use strict"; // ── DOM Elements ────────────────────────────────────────────── const $id = id => document.getElementById(id); const loadingState = $id("loadingState"); const resultPanel = $id("resultPanel"); const feedbackSection = $id("feedbackSection"); const retrainSection = $id("retrainSection"); const statsRow = $id("statsRow"); const blockedOverlay = $id("blockedOverlay"); const offlineBanner = $id("offlineBanner"); let currentResult = null; let currentUrlHash = null; let feedbackGiven = false; let feedbackTimeout = null; let countdownInterval = null; // ── Init ────────────────────────────────────────────────────── async function init() { // Check if this is a blocked page redirect const params = new URLSearchParams(window.location.search); if (params.get("blocked") === "1") { showBlockedPage(params); return; } // Get active tab const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); if (!tab?.url || !tab.url.startsWith("http")) { showResult({ status: "safe", tier: 0, method: "internal", confidence: 0, url: tab?.url || "N/A" }); return; } $id("currentUrl").textContent = tab.url; // Try per-tab cache first (instant) chrome.runtime.sendMessage( { type: "get_tab_result", tabId: tab.id }, response => { if (response?.result) { showResult(response.result); } else { // Fallback to chrome.storage chrome.storage.local.get("lastResult", data => { if (data.lastResult && data.lastResult.url === tab.url) { showResult(data.lastResult); } else { loadingState.style.display = "flex"; } }); } } ); // Load status loadStatus(); } // ── Show Result ─────────────────────────────────────────────── function showResult(result) { currentResult = result; loadingState.style.display = "none"; resultPanel.style.display = "block"; feedbackSection.style.display = "block"; retrainSection.style.display = "block"; statsRow.style.display = "flex"; const isBlocked = result.status === "blocked" || result.is_phishing; const isWarn = !isBlocked && (result.confidence || 0) >= 0.4; const confidence = Math.round((result.confidence || 0) * 100); const tier = result.tier || 0; // Score ring const ring = $id("scoreRing"); const circumference = 2 * Math.PI * 34; // r=34 const offset = circumference - (confidence / 100) * circumference; ring.style.strokeDasharray = circumference; setTimeout(() => { ring.style.strokeDashoffset = offset; ring.style.stroke = isBlocked ? "var(--danger)" : isWarn ? "var(--warning)" : "var(--safe)"; }, 100); $id("scorePct").textContent = confidence + "%"; $id("scorePct").className = `score-pct ${isBlocked ? "status-danger" : isWarn ? "status-warn" : "status-safe"}`; $id("scoreSub").textContent = isBlocked ? "THREAT" : "RISK"; // Shield $id("shieldIcon").textContent = isBlocked ? "🚨" : isWarn ? "⚠️" : "✅"; // Verdict text $id("verdictLabel").textContent = isBlocked ? "PHISHING DETECTED" : isWarn ? "SUSPICIOUS" : "SAFE"; $id("verdictLabel").className = `verdict-label ${isBlocked ? "status-danger" : isWarn ? "status-warn" : "status-safe"}`; const methodNames = { "whitelist": "Whitelist (Tier 1)", "heuristic": "Heuristic Engine (Tier 2)", "heuristic-fallback": "Heuristic Fallback", "bert_gnn_ensemble": "BERT + GNN Ensemble (Tier 3)", "full_ensemble_bert_gnn_cnn": "Full ML Ensemble (Tier 4)", "ensemble_with_visual": "Ensemble + Visual (Tier 4)", "user-override": "User Override", }; const methodText = methodNames[result.method] || result.method || "Unknown"; $id("verdictDetail").textContent = `${methodText} · Confidence: ${confidence}%`; // Tier dots and scores updateTierRow(1, tier >= 1 ? "checked" : "pending", tier === 1 ? "SAFE ✓" : "Miss →"); updateTierRow(2, tier >= 2 ? (isBlocked && tier === 2 ? "blocked" : "checked") : "pending", result.heuristic_score != null ? `${result.heuristic_score}/100` : "—"); updateTierRow(3, tier >= 3 ? (isBlocked && tier === 3 ? "blocked" : "checked") : "pending", result.details?.tier3_score != null ? (result.details.tier3_score * 100).toFixed(0) + "%" : "—"); updateTierRow(4, tier >= 4 ? (isBlocked && tier === 4 ? "blocked" : "checked") : "pending", result.details?.tier4_score != null ? (result.details.tier4_score * 100).toFixed(0) + "%" : "—"); // Tier 2 details — show triggered signals if (result.signals && result.signals.length > 0) { const badges = result.signals.map(s => `${s}`).join(" "); $id("t2Detail").innerHTML = `Signals triggered:
${badges}`; } // Compute URL hash for feedback computeUrlHash(result.url); // Check if we already gave feedback checkExistingFeedback(); } function updateTierRow(tier, status, scoreText) { const dot = $id(`t${tier}Dot`); const score = $id(`t${tier}Score`); const colors = { checked: "var(--safe)", blocked: "var(--danger)", pending: "var(--text-muted)", }; dot.style.background = colors[status] || colors.pending; score.textContent = scoreText; } // ── Blocked Page ────────────────────────────────────────────── function showBlockedPage(params) { loadingState.style.display = "none"; blockedOverlay.classList.add("show"); const url = decodeURIComponent(params.get("url") || ""); const score = params.get("score") || "0"; const method = decodeURIComponent(params.get("method") || ""); $id("blockedUrl").textContent = url; $id("blockedMethod").textContent = `Detection: ${method} · Risk: ${score}%`; $id("currentUrl").textContent = url; currentResult = { url, status: "blocked", confidence: parseInt(score) / 100, method }; computeUrlHash(url); // Show feedback for blocked pages too feedbackSection.style.display = "block"; retrainSection.style.display = "block"; statsRow.style.display = "flex"; loadStatus(); $id("proceedBtn").onclick = () => { chrome.runtime.sendMessage({ type: "whitelist_url", url }, () => { chrome.tabs.update({ url }); }); }; } // ── Feedback ────────────────────────────────────────────────── async function computeUrlHash(url) { if (!url) return; const encoded = new TextEncoder().encode(url); const hash = await crypto.subtle.digest("SHA-256", encoded); currentUrlHash = Array.from(new Uint8Array(hash)) .map(b => b.toString(16).padStart(2, "0")).join(""); } function submitFeedback(feedback) { if (feedbackGiven || !currentUrlHash) return; feedbackGiven = true; chrome.runtime.sendMessage({ type: "submit_feedback", url_hash: currentUrlHash, feedback: feedback, }, response => { if (response?.success) { // Highlight selected, dim other const correctBtn = $id("btnCorrect"); const wrongBtn = $id("btnWrong"); if (feedback === "correct") { correctBtn.classList.add("selected"); wrongBtn.classList.add("dimmed"); } else { wrongBtn.classList.add("selected"); correctBtn.classList.add("dimmed"); } $id("thankYou").classList.add("show"); // Allow changing within 5 minutes feedbackTimeout = setTimeout(() => { feedbackGiven = false; correctBtn.classList.remove("selected", "dimmed"); wrongBtn.classList.remove("selected", "dimmed"); }, 5 * 60 * 1000); } }); } function checkExistingFeedback() { // Check if feedback was already given for this URL chrome.storage.local.get("phishguard_feedback_queue", data => { const queue = data.phishguard_feedback_queue || []; const record = queue.find(r => r.url_hash === currentUrlHash); if (record?.user_feedback) { feedbackGiven = true; const correctBtn = $id("btnCorrect"); const wrongBtn = $id("btnWrong"); if (record.user_feedback === "correct") { correctBtn.classList.add("selected"); wrongBtn.classList.add("dimmed"); } else { wrongBtn.classList.add("selected"); correctBtn.classList.add("dimmed"); } } }); } // ── Retraining Status ───────────────────────────────────────── function loadStatus() { chrome.runtime.sendMessage({ type: "get_status" }, status => { if (!status) return; // Stats row $id("statScanned").textContent = `📊 ${status.scan_count} scanned`; $id("statFeedback").textContent = `💬 ${status.labeled_count || 0} labeled`; $id("statVersion").textContent = `🏷️ v${status.model_version}`; $id("versionBadge").textContent = `v${status.model_version || "3.0"}`; // Retrain progress const urlsRemaining = status.next_retrain_urls_remaining || 50; const progress = Math.round(((50 - urlsRemaining) / 50) * 100); $id("retrainProgressBar").style.width = `${progress}%`; // Retrain status text const timeMs = status.next_retrain_time_remaining_ms || 0; const hours = Math.floor(timeMs / 3600000); const mins = Math.floor((timeMs % 3600000) / 60000); const labeledNeeded = status.min_labeled_needed || 0; if (labeledNeeded > 0) { $id("retrainStatus").textContent = `Need ${labeledNeeded} more feedback to retrain`; } else { $id("retrainStatus").textContent = `Next retrain: ${urlsRemaining} URLs or ${hours}h ${mins}m`; } // Last retrain info if (status.last_retrain_ts) { const ago = timeSince(new Date(status.last_retrain_ts)); $id("retrainLast").textContent = `Last retrain: ${ago} ago`; } // Start countdown startCountdown(timeMs); }); } function startCountdown(initialMs) { if (countdownInterval) clearInterval(countdownInterval); let remaining = initialMs; countdownInterval = setInterval(() => { remaining -= 1000; if (remaining <= 0) { clearInterval(countdownInterval); $id("retrainStatus").textContent = "Retrain pending..."; return; } const h = Math.floor(remaining / 3600000); const m = Math.floor((remaining % 3600000) / 60000); const s = Math.floor((remaining % 60000) / 1000); // Only update the time portion if it's the time display const el = $id("retrainStatus"); if (el.textContent.includes("URLs or")) { const parts = el.textContent.split(" or "); el.textContent = `${parts[0]} or ${h}h ${m}m ${s}s`; } }, 1000); } function timeSince(date) { const seconds = Math.floor((Date.now() - date.getTime()) / 1000); if (seconds < 60) return `${seconds}s`; if (seconds < 3600) return `${Math.floor(seconds / 60)}m`; if (seconds < 86400) return `${Math.floor(seconds / 3600)}h`; return `${Math.floor(seconds / 86400)}d`; } // ── Tier Row Toggle ─────────────────────────────────────────── window.toggleTier = function(header) { const row = header.parentElement; row.classList.toggle("open"); }; // ── Event Listeners ─────────────────────────────────────────── $id("btnCorrect").addEventListener("click", () => submitFeedback("correct")); $id("btnWrong").addEventListener("click", () => submitFeedback("incorrect")); // ── Start ───────────────────────────────────────────────────── init(); })();