const DEFAULT_ENDPOINT = "http://127.0.0.1:8765/screenshot"; const HISTORY_KEY = "capture_history_v1"; const HISTORY_LIMIT = 25; const EXTRA_INSTRUCTIONS_KEY = "extra_instructions_v1"; function $(id) { return document.getElementById(id); } function setStatus(msg, kind) { const el = $("status"); el.textContent = msg; el.className = kind || ""; } async function storageGet(key) { return new Promise((resolve) => { chrome.storage.local.get([key], (res) => resolve(res[key])); }); } async function storageSet(obj) { return new Promise((resolve) => { chrome.storage.local.set(obj, () => resolve()); }); } function clampString(s, maxLen) { const t = String(s || ""); if (t.length <= maxLen) return t; return t.slice(0, Math.max(0, maxLen - 1)) + "…"; } function renderResult(entryOrResp) { const resp = entryOrResp && entryOrResp.png_path ? entryOrResp : entryOrResp && entryOrResp.resp ? entryOrResp.resp : entryOrResp; const meta = entryOrResp && entryOrResp.title ? entryOrResp : null; const lines = []; lines.push("Saved:"); lines.push(` PNG: ${resp.png_path || "(unknown)"}`); lines.push(` META: ${resp.meta_path || "(unknown)"}`); if (resp.content_path) lines.push(` CONTENT: ${resp.content_path}`); if (meta && meta.url) lines.push(` URL: ${meta.url}`); if (meta && meta.extra_instructions) lines.push(` EXTRA: ${clampString(meta.extra_instructions, 220)}`); if (resp.ai_result) { if (resp.ai_result.ok && resp.ai_result.ai && Array.isArray(resp.ai_result.ai.posts)) { lines.push(""); { const ms = resp.ai_result.took_ms || "?"; const model = resp.ai_result.model ? ` model=${resp.ai_result.model}` : ""; const rid = resp.ai_result.response_id ? ` id=${resp.ai_result.response_id}` : ""; const status = resp.ai_result.status && resp.ai_result.status !== "completed" ? ` status=${resp.ai_result.status}` : ""; const inc = resp.ai_result.incomplete_reason ? ` incomplete=${resp.ai_result.incomplete_reason}` : ""; lines.push(`AI (${ms}ms${model}${rid}${status}${inc}):`); } for (const p of resp.ai_result.ai.posts) { const idx = typeof p.index === "number" ? p.index : "?"; const postText = (p.post_text || "").replace(/\s+/g, " ").trim(); // Back-compat with older saved results. const hasNew = "improved_short" in p || "improved_medium" in p || "critical_short" in p || "critical_medium" in p || "suggested_short" in p || "suggested_medium" in p; const improvedShort = (p.improved_short || "").replace(/\s+/g, " ").trim(); const improvedMedium = (p.improved_medium || "").replace(/\s+/g, " ").trim(); const criticalShort = (p.critical_short || "").replace(/\s+/g, " ").trim(); const criticalMedium = (p.critical_medium || "").replace(/\s+/g, " ").trim(); const suggestedShort = (p.suggested_short || "").replace(/\s+/g, " ").trim(); const suggestedMedium = (p.suggested_medium || "").replace(/\s+/g, " ").trim(); const shortLegacy = (p.short_response || "").replace(/\s+/g, " ").trim(); const mediumLegacy = (p.medium_response || "").replace(/\s+/g, " ").trim(); lines.push(""); lines.push(`#${idx} Post: ${clampString(postText, 180)}`); if (hasNew) { lines.push(` improved (short): ${improvedShort}`); lines.push(` improved (medium): ${improvedMedium}`); lines.push(` Critical (short): ${criticalShort}`); lines.push(` Critical (medium): ${criticalMedium}`); lines.push(` Suggested (short): ${suggestedShort}`); lines.push(` Suggested (medium): ${suggestedMedium}`); } else { lines.push(` Short: ${shortLegacy}`); lines.push(` Medium: ${mediumLegacy}`); } } if (resp.ai_result.ai_path) lines.push(`\nAI file: ${resp.ai_result.ai_path}`); if (resp.ai_result.usage && typeof resp.ai_result.usage === "object") { const u = resp.ai_result.usage; const ins = typeof u.input_tokens === "number" ? u.input_tokens : null; const outs = typeof u.output_tokens === "number" ? u.output_tokens : null; const tots = typeof u.total_tokens === "number" ? u.total_tokens : null; const parts = []; if (ins != null) parts.push(`in=${ins}`); if (outs != null) parts.push(`out=${outs}`); if (tots != null) parts.push(`total=${tots}`); if (parts.length) lines.push(`AI usage: ${parts.join(" ")}`); } } else { lines.push(""); const ms = resp.ai_result.took_ms || "?"; const model = resp.ai_result.model ? ` model=${resp.ai_result.model}` : ""; const rid = resp.ai_result.response_id ? ` id=${resp.ai_result.response_id}` : ""; const status = resp.ai_result.status ? ` status=${resp.ai_result.status}` : ""; const inc = resp.ai_result.incomplete_reason ? ` incomplete=${resp.ai_result.incomplete_reason}` : ""; const err = resp.ai_result.error || (resp.ai_result.ai && resp.ai_result.ai.error) || "unknown"; lines.push(`AI error (${ms}ms${model}${rid}${status}${inc}): ${err}`); if (resp.ai_result.detail) lines.push(`Detail: ${clampString(resp.ai_result.detail, 600)}`); if (resp.ai_result.raw_preview) lines.push(`Raw: ${clampString(resp.ai_result.raw_preview, 600)}`); if (resp.ai_result.usage && typeof resp.ai_result.usage === "object") { const u = resp.ai_result.usage; const ins = typeof u.input_tokens === "number" ? u.input_tokens : null; const outs = typeof u.output_tokens === "number" ? u.output_tokens : null; const tots = typeof u.total_tokens === "number" ? u.total_tokens : null; const parts = []; if (ins != null) parts.push(`in=${ins}`); if (outs != null) parts.push(`out=${outs}`); if (tots != null) parts.push(`total=${tots}`); if (parts.length) lines.push(`AI usage: ${parts.join(" ")}`); } if (resp.ai_result.ai_path) lines.push(`AI file: ${resp.ai_result.ai_path}`); } } if (resp.ran) { lines.push("Ran:"); if (resp.ran.error) { lines.push(` error: ${resp.ran.error}`); } else { lines.push(` exit: ${resp.ran.exit_code}`); if (resp.ran.stdout) lines.push(` stdout: ${resp.ran.stdout.trim()}`); if (resp.ran.stderr) lines.push(` stderr: ${resp.ran.stderr.trim()}`); } } return lines.join("\n"); } async function loadHistory() { const h = (await storageGet(HISTORY_KEY)) || []; return Array.isArray(h) ? h : []; } async function saveHistory(history) { await storageSet({ [HISTORY_KEY]: history }); } function historyLabel(item) { const when = item.saved_at ? item.saved_at.replace("T", " ").replace("Z", "Z") : ""; const t = item.title ? clampString(item.title, 36) : ""; const u = item.url ? clampString(item.url, 46) : ""; const base = t || u || item.id || ""; return when ? `${when} | ${base}` : base; } async function refreshHistoryUI() { const sel = $("history"); const history = await loadHistory(); sel.textContent = ""; if (!history.length) { const opt = document.createElement("option"); opt.value = ""; opt.textContent = "(empty)"; sel.appendChild(opt); sel.disabled = true; return; } sel.disabled = false; for (const item of history) { const opt = document.createElement("option"); opt.value = item.id || item.png_path || item.meta_path || ""; opt.textContent = historyLabel(item); sel.appendChild(opt); } } async function getActiveTab() { const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); return tabs[0] || null; } async function captureVisibleTab() { // Defaults to current window when windowId is null. return await chrome.tabs.captureVisibleTab(null, { format: "png" }); } async function extractPageContent(tabId) { const [{ result }] = await chrome.scripting.executeScript({ target: { tabId }, func: () => { const SKIP_TAGS = new Set(["script", "style", "noscript", "template", "head", "meta", "link", "svg", "canvas"]); const MAX_DEPTH = 14; const MAX_NODES = 1800; const MAX_TEXT_NODE_LEN = 500; let nodeCount = 0; let truncated = false; function cleanText(s) { return String(s || "") .replace(/\s+/g, " ") .trim(); } function rectIntersectsViewport(r) { const vw = window.innerWidth || document.documentElement.clientWidth || 0; const vh = window.innerHeight || document.documentElement.clientHeight || 0; return r.bottom > 0 && r.right > 0 && r.top < vh && r.left < vw; } function isVisibleElement(el) { try { const cs = window.getComputedStyle(el); if (!cs) return false; if (cs.display === "none" || cs.visibility === "hidden") return false; const op = Number(cs.opacity || "1"); if (!Number.isNaN(op) && op <= 0.02) return false; const r = el.getBoundingClientRect(); if ((r.width || 0) < 1 || (r.height || 0) < 1) return false; if (!rectIntersectsViewport(r)) return false; return true; } catch { return false; } } function hasMeaningfulAttrs(el) { const role = (el.getAttribute("role") || "").trim(); if (role) return true; const aria = (el.getAttribute("aria-label") || "").trim(); if (aria) return true; const dt = (el.getAttribute("data-testid") || "").trim(); if (dt) return true; const tag = el.tagName.toLowerCase(); if (tag === "a" && (el.getAttribute("href") || el.href)) return true; if (tag === "button" || tag === "input" || tag === "textarea" || tag === "select") return true; return false; } function directVisibleText(el) { // Only include immediate text nodes and form values. This avoids duplicating text up the tree. const parts = []; for (const n of Array.from(el.childNodes || [])) { if (n.nodeType === Node.TEXT_NODE) { const t = cleanText(n.nodeValue || ""); if (t) parts.push(t.length > MAX_TEXT_NODE_LEN ? t.slice(0, MAX_TEXT_NODE_LEN - 1) + "…" : t); } } const tag = el.tagName.toLowerCase(); if (tag === "input") { const type = (el.getAttribute("type") || "text").toLowerCase(); if (!["hidden", "submit", "button"].includes(type)) { const v = cleanText(el.value || el.getAttribute("value") || el.getAttribute("placeholder") || ""); if (v) parts.push(v); } } else if (tag === "textarea") { const v = cleanText(el.value || el.getAttribute("placeholder") || ""); if (v) parts.push(v); } return cleanText(parts.join(" ")); } function simplifyTag(tag) { // Keep some semantics; treat most wrapper tags as "div". if (["main", "article", "section", "ul", "ol", "li", "p"].includes(tag)) return tag; if (tag.match(/^h[1-6]$/)) return tag; if (["a", "button", "label"].includes(tag)) return tag; if (["header", "footer", "nav", "aside"].includes(tag)) return tag; return "div"; } function shouldSkip(el, tag) { if (SKIP_TAGS.has(tag)) return true; // Skip common overlay noise. const id = (el.id || "").toLowerCase(); if (id.includes("cookie") || id.includes("consent")) return true; return false; } function collapseWrappers(node) { // Collapse div/span wrappers: div > div > div ... with a single child and no text/attrs. // We only collapse when the wrapper has exactly one child. while ( node && node.tag === "div" && !node.text && !node.attrs && Array.isArray(node.children) && node.children.length === 1 && node.children[0] && node.children[0].tag ) { node = node.children[0]; } return node; } function maybeHoistInlineText(node) { // Common pattern: Text or if (!node || node.text) return node; if (!["a", "button", "label", "p", "li"].includes(node.tag)) return node; if (!Array.isArray(node.children) || node.children.length !== 1) return node; const ch = node.children[0]; if (ch && ch.text && (!ch.children || ch.children.length === 0) && (!ch.attrs || Object.keys(ch.attrs).length === 0)) { node.text = ch.text; delete node.children; } return node; } function build(el, depth) { if (truncated) return null; if (!el || depth > MAX_DEPTH) return null; if (!(el instanceof Element)) return null; const rawTag = el.tagName.toLowerCase(); if (shouldSkip(el, rawTag)) return null; if (!isVisibleElement(el)) return null; nodeCount += 1; if (nodeCount > MAX_NODES) { truncated = true; return null; } const tag = simplifyTag(rawTag); const text = directVisibleText(el); const children = []; for (const ch of Array.from(el.children || [])) { const n = build(ch, depth + 1); if (n) children.push(n); if (truncated) break; } if (!text && children.length === 0) return null; const node = { tag }; if (text) node.text = text; // Only keep attrs when they help identify purpose/action. if (hasMeaningfulAttrs(el)) { const attrs = {}; const role = (el.getAttribute("role") || "").trim(); const aria = (el.getAttribute("aria-label") || "").trim(); const dt = (el.getAttribute("data-testid") || "").trim(); if (role) attrs.role = role; if (aria) attrs.aria_label = aria; if (dt) attrs.data_testid = dt; if (rawTag === "a") { const href = (el.href || el.getAttribute("href") || "").trim(); if (href) attrs.href = href; } if (Object.keys(attrs).length) node.attrs = attrs; } if (children.length) node.children = children; return maybeHoistInlineText(collapseWrappers(node)); } const extractedAt = new Date().toISOString(); const url = String(location.href || ""); const title = String(document.title || ""); const root = document.body ? build(document.body, 0) : null; return { extracted_at: extractedAt, url, title, hostname: location.hostname, visible_tree: root, truncated, stats: { nodes: nodeCount }, }; }, }); return result; } async function postScreenshot(endpoint, payload) { const r = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); const text = await r.text(); let data = null; try { data = JSON.parse(text); } catch { // ignore } if (!r.ok) { throw new Error(`HTTP ${r.status}: ${text}`); } return data; } async function ping(endpoint) { const base = endpoint.replace(/\/screenshot\s*$/, ""); const r = await fetch(`${base}/health`, { method: "GET" }); if (!r.ok) return `HTTP ${r.status}`; const j = await r.json(); return j && j.ok ? "ok" : "unexpected_response"; } async function main() { const endpointEl = $("endpoint"); const extraEl = $("extra_instructions"); const captureBtn = $("capture"); const pingBtn = $("ping"); const historySel = $("history"); const showHistoryBtn = $("show_history"); const clearHistoryBtn = $("clear_history"); endpointEl.value = (await storageGet("endpoint")) || DEFAULT_ENDPOINT; extraEl.value = (await storageGet(EXTRA_INSTRUCTIONS_KEY)) || ""; await refreshHistoryUI(); endpointEl.addEventListener("change", async () => { await storageSet({ endpoint: endpointEl.value.trim() }); }); extraEl.addEventListener("change", async () => { await storageSet({ [EXTRA_INSTRUCTIONS_KEY]: extraEl.value }); }); pingBtn.addEventListener("click", async () => { const endpoint = endpointEl.value.trim() || DEFAULT_ENDPOINT; setStatus("Pinging...", ""); const msg = await ping(endpoint); setStatus(`Ping result: ${msg}`, msg === "ok" ? "ok" : "err"); }); showHistoryBtn.addEventListener("click", async () => { const history = await loadHistory(); if (!history.length) { setStatus("History is empty.", "err"); return; } const id = historySel.value; const item = history.find((x) => (x.id || x.png_path || x.meta_path) === id) || history[0]; if (!item) { setStatus("No history item selected.", "err"); return; } setStatus(renderResult(item), "ok"); }); clearHistoryBtn.addEventListener("click", async () => { await saveHistory([]); await refreshHistoryUI(); setStatus("History cleared.", "ok"); }); captureBtn.addEventListener("click", async () => { const endpoint = endpointEl.value.trim() || DEFAULT_ENDPOINT; const extraInstructions = (extraEl.value || "").trim(); captureBtn.disabled = true; setStatus("Extracting page content...", ""); try { const tab = await getActiveTab(); if (!tab) throw new Error("No active tab found"); let content = null; try { content = await extractPageContent(tab.id); } catch (e) { // Some URLs (chrome://*, Web Store, PDF viewer) cannot be scripted. content = { error: String(e && e.message ? e.message : e) }; } setStatus("Capturing visible tab...", ""); const dataUrl = await captureVisibleTab(); setStatus("Uploading to local server...", ""); const resp = await postScreenshot(endpoint, { data_url: dataUrl, title: tab.title || "", url: tab.url || "", ts: new Date().toISOString(), content, extra_instructions: extraInstructions, }); // Persist for later viewing in the popup. const history = await loadHistory(); const entry = { id: resp.png_path || resp.meta_path || String(Date.now()), saved_at: new Date().toISOString(), title: tab.title || "", url: tab.url || "", extra_instructions: extraInstructions, resp: { png_path: resp.png_path || "", meta_path: resp.meta_path || "", content_path: resp.content_path || "", ai_result: resp.ai_result || null, }, }; const deduped = [entry, ...history.filter((x) => (x.id || x.png_path || x.meta_path) !== entry.id)].slice(0, HISTORY_LIMIT); await saveHistory(deduped); await refreshHistoryUI(); setStatus(renderResult(entry), "ok"); } catch (e) { setStatus(String(e && e.message ? e.message : e), "err"); } finally { captureBtn.disabled = false; } }); } main();