diff --git a/popup/popup.js b/popup/popup.js index d30332e..ee35592 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -13,6 +13,8 @@ let isChatLoading = false; let contentWasTruncated = false; // Track if content was truncated during extraction /** TEMP: remove after testing — "readability" | "legacy" | "unknown" */ let contentExtractionSource = "unknown"; +/** When set, page text must not be sent to the model; user sees Reddit-specific UI instead. */ +let extractionUnsupportedReason = null; // "old-reddit" | "non-discussion" // Streaming state let streamingChatContent = ""; @@ -479,6 +481,20 @@ document.addEventListener("DOMContentLoaded", async () => { // If we have cached content for this tab, restore it if (cachedContent && cachedContent.url === currentTabUrl) { + if (cachedContent.unsupportedReason) { + extractionUnsupportedReason = cachedContent.unsupportedReason; + currentPageContent = ""; + contentWasTruncated = false; + contentExtractionSource = + cachedContent.extractionSource || "reddit-unsupported"; + isExtracting = false; + summarizeBtn.disabled = false; + setSummarizeLabel("Quick Summary"); + showRedditUnsupportedError(extractionUnsupportedReason); + return; + } + + extractionUnsupportedReason = null; currentPageContent = cachedContent.content; contentWasTruncated = cachedContent.wasTruncated || false; contentExtractionSource = cachedContent.extractionSource || "unknown"; @@ -532,6 +548,12 @@ document.addEventListener("DOMContentLoaded", async () => { // No cache, extract fresh content await extractPageContent(); isExtracting = false; + + if (extractionUnsupportedReason) { + showRedditUnsupportedError(extractionUnsupportedReason); + return; + } + summarizeBtn.disabled = false; setSummarizeLabel("Quick Summary"); @@ -562,6 +584,7 @@ function resetUI() { chatHistory = []; generatedSuggestions = []; contentWasTruncated = false; + extractionUnsupportedReason = null; resultContainer.innerHTML = ""; resultContainer.classList.add("hidden"); initialState.classList.remove("hidden"); @@ -582,6 +605,10 @@ summarizeBtn.addEventListener("click", async () => { showToast("Still loading page content…"); return; } + if (extractionUnsupportedReason) { + showToast(redditUnsupportedShortLine(extractionUnsupportedReason)); + return; + } if (!currentPageContent) { showToast("Could not extract page content. Try refreshing."); return; @@ -608,6 +635,7 @@ async function extractPageContent() { currentPageContent = ""; contentWasTruncated = false; contentExtractionSource = "unknown"; + extractionUnsupportedReason = null; return; } @@ -616,12 +644,33 @@ async function extractPageContent() { tabId: currentTabId, }); - if (response && response.success && response.content) { - currentPageContent = response.content; - contentWasTruncated = response.wasTruncated || false; + if (response && response.success) { contentExtractionSource = response.extractionSource || "unknown"; - // Cache the content with truncation info - if (currentTabId) { + contentWasTruncated = response.wasTruncated || false; + + if ( + response.extractionSource === "reddit-unsupported" && + response.unsupportedReason + ) { + currentPageContent = ""; + extractionUnsupportedReason = response.unsupportedReason; + if (currentTabId) { + await chrome.storage.session.set({ + [CONTENT_CACHE_PREFIX + currentTabId]: { + content: "", + wasTruncated: false, + url: currentTabUrl, + extractionSource: contentExtractionSource, + unsupportedReason: extractionUnsupportedReason, + }, + }); + } + return; + } + + extractionUnsupportedReason = null; + currentPageContent = response.content || ""; + if (currentTabId && currentPageContent) { await chrome.storage.session.set({ [CONTENT_CACHE_PREFIX + currentTabId]: { content: currentPageContent, @@ -631,12 +680,15 @@ async function extractPageContent() { }, }); } + } else { + extractionUnsupportedReason = null; } } catch (error) { console.error("Error extracting content:", error); currentPageContent = ""; contentWasTruncated = false; contentExtractionSource = "unknown"; + extractionUnsupportedReason = null; } } @@ -738,6 +790,10 @@ function showSummary(content) { showToast("Still loading page content…"); return; } + if (extractionUnsupportedReason) { + showToast(redditUnsupportedShortLine(extractionUnsupportedReason)); + return; + } if (!currentPageContent) { showToast("Could not extract page content. Try refreshing."); return; @@ -1014,6 +1070,11 @@ chatInput.addEventListener("keypress", (e) => { }); async function generateQuickSummary() { + if (extractionUnsupportedReason) { + showRedditUnsupportedError(extractionUnsupportedReason); + return; + } + bumpSuggestionsTask(); setLoading(true); currentSummaryMode = "quick"; @@ -1640,3 +1701,29 @@ function showUnsupportedExtractionError(title, message) { `; } + +const REDDIT_UNSUPPORTED_COPY = { + "old-reddit": { + title: "Old Reddit isn’t supported for thread extraction", + message: + "Open the same post on www.reddit.com (new Reddit), then open Summarizer again.", + }, + "non-discussion": { + title: "This Reddit page can’t be summarized with focused extraction yet", + message: + "Only new-Reddit discussion threads are supported (URLs containing /comments/). Feeds, profiles, and other Reddit pages aren’t covered yet.", + }, +}; + +function redditUnsupportedShortLine(reason) { + if (reason === "old-reddit") { + return "Open this thread on www.reddit.com to summarize it."; + } + return "Open a post’s comment thread on new Reddit to summarize it."; +} + +function showRedditUnsupportedError(reason) { + const copy = + REDDIT_UNSUPPORTED_COPY[reason] || REDDIT_UNSUPPORTED_COPY["non-discussion"]; + showUnsupportedExtractionError(copy.title, copy.message); +} diff --git a/scripts/background.js b/scripts/background.js index 624d6a3..1549caa 100644 --- a/scripts/background.js +++ b/scripts/background.js @@ -303,20 +303,22 @@ async function extractPageContentForTab(tabId) { } // Readability must load before content.js (content.js is not bundled with it). + // reddit-new.js registers window.__webaiTryRedditNew for new-Reddit thread extraction. await chrome.scripting.executeScript({ target: { tabId }, - files: ["scripts/Readability.js", "scripts/content.js"], + files: ["scripts/Readability.js", "scripts/reddit-new.js", "scripts/content.js"], }); const response = await chrome.tabs.sendMessage(tabId, { action: "extract" }); - if (!response || !response.content) { + if (!response) { return { content: "", wasTruncated: false }; } return { - content: response.content, + content: response.content ?? "", wasTruncated: Boolean(response.wasTruncated), extractionSource: response.extractionSource ?? "unknown", + unsupportedReason: response.unsupportedReason ?? null, }; } diff --git a/scripts/content.js b/scripts/content.js index 05757d2..b4c2f50 100644 --- a/scripts/content.js +++ b/scripts/content.js @@ -522,7 +522,30 @@ ); } + function tryRedditShredditExtract() { + if (typeof window.__webaiTryRedditNew !== "function") { + return null; + } + try { + return window.__webaiTryRedditNew(MAX_LENGTH); + } catch (err) { + console.error("[Summarizer] Reddit extract error:", err); + return null; + } + } + function extractContent() { + const redditResult = tryRedditShredditExtract(); + if (redditResult) { + logExtractionDebug(redditResult.extractionSource, redditResult.text); + return { + text: redditResult.text, + wasTruncated: Boolean(redditResult.wasTruncated), + extractionSource: redditResult.extractionSource, + unsupportedReason: redditResult.unsupportedReason, + }; + } + try { const readability = extractWithReadability(); const rbLen = readability.text.trim().length; @@ -628,6 +651,7 @@ content: result.text, wasTruncated: result.wasTruncated, extractionSource: result.extractionSource, + unsupportedReason: result.unsupportedReason, }); } return true; diff --git a/scripts/reddit-new.js b/scripts/reddit-new.js new file mode 100644 index 0000000..cbb96a9 --- /dev/null +++ b/scripts/reddit-new.js @@ -0,0 +1,195 @@ +// New Reddit (shreddit) discussion-thread extraction for content scripts. +// Loaded before scripts/content.js. Exposes window.__webaiTryRedditNew(maxLength). + +(function () { + "use strict"; + + /** @type {Set} */ + const NEW_REDDIT_HOSTS = new Set([ + "www.reddit.com", + "reddit.com", + "new.reddit.com", + "m.reddit.com", + "np.reddit.com", + ]); + + const DISCUSSION_PATH_SUB = /^\/r\/[^/]+\/comments\/[^/]+/; + const DISCUSSION_PATH_SHORT = /^\/comments\/[^/]+/; + + const MAX_COMMENTS = 90; + + function isNewRedditHost(host) { + return NEW_REDDIT_HOSTS.has(String(host || "").toLowerCase()); + } + + function isDiscussionPath(pathname) { + const p = pathname || ""; + return DISCUSSION_PATH_SUB.test(p) || DISCUSSION_PATH_SHORT.test(p); + } + + function cleanText(s) { + if (!s || typeof s !== "string") return ""; + return s.replace(/\s+/g, " ").trim(); + } + + function shouldSkipCommentBody(text) { + if (!text || text.length < 2) return true; + if (/^(\d+\s*)?(more replies|more reply)\.?$/i.test(text)) return true; + return false; + } + + /** + * @param {number} maxLength + * @returns {{ text: string, wasTruncated: boolean, extractionSource: string, unsupportedReason?: string } | null} + */ + function tryRedditNew(maxLength) { + let host = ""; + try { + host = String(window.location.hostname || "").toLowerCase(); + } catch (e) { + return null; + } + + if (!host || !host.includes("reddit.com")) { + return null; + } + + if (host === "old.reddit.com") { + return { + text: "", + wasTruncated: false, + extractionSource: "reddit-unsupported", + unsupportedReason: "old-reddit", + }; + } + + if (!isNewRedditHost(host)) { + return null; + } + + let path = ""; + try { + path = window.location.pathname || ""; + } catch (e2) { + return null; + } + + if (!isDiscussionPath(path)) { + return { + text: "", + wasTruncated: false, + extractionSource: "reddit-unsupported", + unsupportedReason: "non-discussion", + }; + } + + const post = document.querySelector( + 'shreddit-post[view-context="CommentsPage"]', + ); + if (!post) { + return null; + } + + const postId = post.getAttribute("id") || ""; + const title = + cleanText(post.getAttribute("post-title") || "") || + cleanText( + post.querySelector('h1[slot="title"]')?.textContent || "", + ); + const sub = cleanText(post.getAttribute("subreddit-prefixed-name") || ""); + const author = cleanText(post.getAttribute("author") || ""); + const score = cleanText(post.getAttribute("score") || ""); + const commentCount = cleanText(post.getAttribute("comment-count") || ""); + + let bodyText = ""; + if (postId) { + const bodyEl = document.getElementById(`${postId}-post-rtjson-content`); + bodyText = cleanText(bodyEl?.textContent || ""); + } + if (!bodyText) { + const fallback = post.querySelector("shreddit-post-text-body [id$='-post-rtjson-content']"); + bodyText = cleanText(fallback?.textContent || ""); + } + + const tree = document.querySelector("shreddit-comment-tree#comment-tree"); + const commentEls = tree + ? Array.from(tree.querySelectorAll("shreddit-comment")) + : []; + + let out = ""; + let wasTruncated = false; + + function add(s) { + if (wasTruncated) return false; + const chunk = s == null ? "" : String(s); + if (out.length + chunk.length <= maxLength) { + out += chunk; + return true; + } + wasTruncated = true; + const room = maxLength - out.length; + if (room > 20) { + out += chunk.slice(0, room).trimEnd() + "…"; + } + return false; + } + + const header = + "Reddit thread (new Reddit)\n" + + (sub ? `Subreddit: ${sub}\n` : "") + + (title ? `Title: ${title}\n` : "") + + (author ? `Author: u/${author}\n` : "") + + (score ? `Post score: ${score}\n` : "") + + (commentCount ? `Comment count (thread): ${commentCount}\n` : "") + + `URL: ${window.location.href}\n`; + + add(header); + add("\n--- Post ---\n"); + + if (bodyText) { + add(bodyText); + } else { + add("(No post body text — link/media post or content not loaded.)"); + } + + add("\n\n--- Comments (in page order) ---\n"); + + let n = 0; + for (let i = 0; i < commentEls.length && n < MAX_COMMENTS; i++) { + const el = commentEls[i]; + const depth = parseInt(el.getAttribute("depth") || "0", 10) || 0; + const cAuthor = cleanText(el.getAttribute("author") || ""); + const cScore = cleanText(el.getAttribute("score") || ""); + + const bodyEl = + el.querySelector("[id$='-comment-rtjson-content']") || + el.querySelector('[slot="comment"]'); + let cText = cleanText(bodyEl?.textContent || ""); + if (shouldSkipCommentBody(cText)) continue; + + n++; + const indent = " ".repeat(Math.min(depth, 8)); + const block = + `\n[${n}] u/${cAuthor || "?"} · score ${cScore || "?"} · depth ${depth}\n` + + indent + + cText.replace(/\n/g, "\n" + indent) + + "\n"; + + if (!add(block)) { + break; + } + } + + if (commentEls.length === 0) { + add("(No comments found in the DOM — they may still be loading.)"); + } + + return { + text: out.trim(), + wasTruncated, + extractionSource: "reddit-thread", + }; + } + + window.__webaiTryRedditNew = tryRedditNew; +})();