├── README.md ├── background.js ├── content.js ├── hello.html ├── llm.js ├── manifest.json ├── sidepanel.html ├── sidepanel.js ├── summarize.png ├── summarize_all.png └── summarize_hn.png /README.md: -------------------------------------------------------------------------------- 1 | # FastDigest 2 | 3 | ## Description 4 | 5 | FastDigest is a Chrome extension designed to summarize Hacker News comments using OpenAI's GPT-4 model. The extension provides a "Summarize" button for individual comments and a "Summarize All" button to summarize all comments on a page. The summaries are generated in real-time and displayed in the Chrome side panel. 6 | 7 | ## Features 8 | 9 | - Adds a "Summarize" button to each comment on Hacker News. 10 | ![summarize button](summarize.png) 11 | - Adds a "Summarize All" button to summarize all comments on a page. 12 | ![summarize all button](summarize_all.png) 13 | - Supports both OpenAI and Ollama. 14 | - Displays summaries in the Chrome side panel. 15 | 16 | ## Installation 17 | 18 | 1. **Clone the repository**: 19 | 20 | ```sh 21 | git clone https://github.com/yourusername/fastdigest.git 22 | cd fastdigest 23 | ``` 24 | 25 | 2. **Configure LLM Provider** 26 | 27 | - Go to background.js. At the top, you will see a variable to set your OpenAI API Key. 28 | - FastDigest also supports Ollama(llama3.1) for using local LLMs. To use Ollama, you will need to do the following: 29 | - shut down Ollama if it is currently running 30 | - run `launchctl setenv OLLAMA_ORIGINS "*"` 31 | - restart Ollama 32 | 33 | 3. **Load the extension in Chrome**: 34 | - Open Chrome and navigate to chrome://extensions/. 35 | - Enable "Developer mode" by toggling the switch in the top right corner. 36 | - Click on "Load unpacked" and select the directory where you cloned the repository. 37 | 38 | ## Usage 39 | 40 | Visit any Hacker News item page (e.g., https://news.ycombinator.com/item?id=123456). 41 | The extension will add "Summarize" and "Summarize All" buttons to the comments. 42 | Click the "Summarize" button: 43 | 44 | Click the "Summarize" button next to a comment to generate a summary for that comment. 45 | Click the "Summarize All" button: 46 | 47 | Click the "Summarize All" button to generate summaries for all comments on the page. 48 | 49 | ## Contributing 50 | 51 | Contributions are welcome! Please open an issue or submit a pull request for any improvements or bug fixes. 52 | 53 | ## License 54 | 55 | This project is licensed under the MIT License. See the LICENSE file for details. 56 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | import { OpenAIProvider } from "./llm.js"; 2 | 3 | const OPENAI_API_KEY = "YOUR_OPEN_AI_API_KEY"; 4 | // Configure the LLMProvider of your choice, see llm.js for more details 5 | // OpenAI: const llmProvider = new OpenAIProvider(OPENAI_API_KEY); 6 | // Ollama: const llmProvider = new OllamaProvider(); 7 | const llmProvider = new OpenAIProvider(OPENAI_API_KEY); 8 | const ALLOWED_ORIGIN = "https://news.ycombinator.com"; 9 | 10 | var isSummarizing = false; 11 | var sidePanelReady = false; 12 | 13 | // Allows users to open the side panel by clicking on the action toolbar icon 14 | chrome.sidePanel 15 | .setPanelBehavior({ openPanelOnActionClick: true }) 16 | .catch((error) => console.error(error)); 17 | 18 | chrome.tabs.onUpdated.addListener(async (tabId, info, tab) => { 19 | if (!tab.url) return; 20 | const url = new URL(tab.url); 21 | 22 | // Enables the side panel on google.com 23 | if (url.origin === ALLOWED_ORIGIN) { 24 | await chrome.sidePanel.setOptions({ 25 | tabId, 26 | path: "sidepanel.html", 27 | enabled: true, 28 | }); 29 | } else { 30 | // Disables the side panel on all other sites 31 | await chrome.sidePanel.setOptions({ 32 | tabId, 33 | enabled: false, 34 | }); 35 | } 36 | }); 37 | 38 | // background.js 39 | chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => { 40 | if (message.action === "summarize_comments") { 41 | if (isSummarizing) { 42 | chrome.tabs.sendMessage(sender.tab.id, { 43 | action: "alert", 44 | message: "Summarization in progress, you can summarize after", 45 | }); 46 | return; 47 | } 48 | isSummarizing = true; 49 | await chrome.sidePanel.open({ tabId: sender.tab.id }); 50 | if (sidePanelReady) { 51 | await chrome.runtime.sendMessage({ action: "start_summary" }); 52 | } 53 | await streamSummarizedComments(message.commentsChunks, sender.tab.id); 54 | } else if (message.action === "sidepanel_ready") { 55 | sidePanelReady = true; 56 | if (isSummarizing) { 57 | await chrome.runtime.sendMessage({ action: "start_summary" }); 58 | } 59 | } else if (message.action === "sidepanel_closed") { 60 | sidePanelReady = false; 61 | } 62 | }); 63 | 64 | async function fetchFinalSummary(combinedSummaries, tabId) { 65 | const finalPrompt = `Summarize the following summaries, be concise and focus on the recurring thoughts and overall sentiment. Have a section with the key points made in the discussions. Don't include a title.: ${combinedSummaries}`; 66 | 67 | // Use streaming summarization for the final summary 68 | return await llmProvider.summarizeStream(finalPrompt, (chunkText) => { 69 | chrome.runtime.sendMessage({ 70 | action: "stream_chunk", 71 | chunk: chunkText, 72 | tabId, 73 | }); 74 | }); 75 | } 76 | 77 | async function summarizeAndStreamChunk(chunk, tabId) { 78 | const prompt = `Summarize the following Hacker News comments focusing on the key points and recurring thoughts: ${JSON.stringify( 79 | chunk 80 | )}`; 81 | 82 | // Use streaming summarization via provider 83 | return await llmProvider.summarizeStream(prompt, (chunkText) => { 84 | chrome.runtime.sendMessage({ 85 | action: "stream_chunk", 86 | chunk: chunkText, 87 | tabId, 88 | }); 89 | }); 90 | } 91 | 92 | async function streamSummarizedComments(commentsChunks, tabId) { 93 | if (commentsChunks.length === 1) { 94 | const summary = await summarizeAndStreamChunk(commentsChunks[0], tabId); 95 | isSummarizing = false; 96 | chrome.runtime.sendMessage({ 97 | action: "stream_complete", 98 | summary, 99 | tabId, 100 | }); 101 | return; 102 | } 103 | try { 104 | // Create an array of promises to call the LLM provider for each chunk 105 | const summaries = await Promise.all( 106 | commentsChunks.map(async (chunk) => { 107 | const prompt = `Summarize the following Hacker News comments focusing on the key points and recurring thoughts: ${JSON.stringify( 108 | chunk 109 | )}`; 110 | 111 | // Use provider summarization for each chunk 112 | return await llmProvider.summarize(prompt); 113 | }) 114 | ); 115 | 116 | isSummarizing = false; 117 | 118 | // Combine the summaries for the final summarization 119 | const combinedSummaries = summaries.join("\n"); 120 | const finalSummary = await fetchFinalSummary(combinedSummaries, tabId); 121 | 122 | chrome.runtime.sendMessage({ 123 | action: "stream_complete", 124 | summary: finalSummary, 125 | tabId, 126 | }); 127 | } catch (error) { 128 | console.error("Error in streamSummarizedComments:", error.message); 129 | chrome.runtime.sendMessage({ 130 | action: "stream_error", 131 | error: error.message, 132 | tabId, 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /content.js: -------------------------------------------------------------------------------- 1 | // Function to add the summarize button 2 | function addSummarizeButtons() { 3 | // get the subline from hacker news submission 4 | const sublines = document.querySelectorAll(".subline"); 5 | // add a summarize all link to the subline 6 | sublines.forEach((subline) => { 7 | if (!subline.querySelector(".summarize-all-btn")) { 8 | const summarizeAllLink = document.createElement("a"); 9 | summarizeAllLink.classList.add("summarize-all-btn"); 10 | summarizeAllLink.href = "#"; 11 | summarizeAllLink.innerText = "summarize all"; 12 | // add a text node with a pipe character 13 | const pipe = document.createTextNode(" | "); 14 | subline.appendChild(pipe); 15 | subline.appendChild(summarizeAllLink); 16 | 17 | // Add click event listener to the summarize all button 18 | summarizeAllLink.addEventListener("click", async (event) => { 19 | event.preventDefault(); 20 | await summarizeAllComments(); 21 | }); 22 | } 23 | }); 24 | 25 | // get all hacker news comment elements 26 | const comments = document.querySelectorAll(".comtr"); 27 | comments.forEach((comment) => { 28 | // get hacker news comment header 29 | const header = comment.querySelector(".comhead"); 30 | if (!header.querySelector(".summarize-btn")) { 31 | // get the navs element with class navs 32 | const navs = header.querySelector(".navs"); 33 | 34 | if (!navs) { 35 | return; 36 | } 37 | 38 | // create a link for summarizing and add it to the existing links 39 | const summarizeLink = document.createElement("a"); 40 | summarizeLink.classList.add("summarize-btn"); 41 | summarizeLink.href = "#"; 42 | summarizeLink.innerText = "summarize"; 43 | navs.appendChild(summarizeLink); 44 | 45 | // Add click event listener to the summarize button 46 | summarizeLink.addEventListener("click", async (event) => { 47 | event.preventDefault(); 48 | await summarizeComment(comment); 49 | }); 50 | } 51 | }); 52 | } 53 | 54 | async function summarizeComment(commentElement) { 55 | let nextElement = commentElement.nextElementSibling; 56 | const currentIndentLevel = parseInt( 57 | commentElement.querySelector(".ind").getAttribute("indent"), 58 | 10 59 | ); 60 | 61 | const commentsArray = []; 62 | let currentChunk = []; 63 | let currentChunkLength = 0; 64 | const maxChunkSize = 50000; 65 | 66 | // Extract the original parent comment 67 | const parentCommentTextElement = commentElement.querySelector(".commtext"); 68 | const parentCommentText = parentCommentTextElement 69 | ? parentCommentTextElement.innerText 70 | : ""; 71 | const parentAuthorElement = commentElement.querySelector(".hnuser"); 72 | const parentAuthor = parentAuthorElement 73 | ? parentAuthorElement.innerText 74 | : "Unknown"; 75 | const parentId = commentElement.id; 76 | 77 | const parentComment = { 78 | id: parentId, 79 | author: parentAuthor, 80 | text: parentCommentText, 81 | parent: null, // Parent has no parent 82 | }; 83 | 84 | // Function to add a comment to the current chunk or create a new one if necessary 85 | function addCommentToChunk(comment) { 86 | const commentLength = comment.text.length; 87 | 88 | // Check if adding the current comment would exceed the chunk size limit 89 | if (currentChunkLength + commentLength > maxChunkSize) { 90 | // Push the current chunk (with the parent comment) to commentsArray 91 | commentsArray.push([parentComment, ...currentChunk]); 92 | // Start a new chunk 93 | currentChunk = []; 94 | currentChunkLength = 0; 95 | } 96 | 97 | // Add the comment to the current chunk 98 | currentChunk.push(comment); 99 | currentChunkLength += commentLength; 100 | } 101 | 102 | // Iterate over the sibling comments until the indent level changes or comments run out 103 | while (nextElement) { 104 | const commentTextElement = nextElement.querySelector(".commtext"); 105 | const commentText = commentTextElement ? commentTextElement.innerText : ""; 106 | 107 | const authorElement = nextElement.querySelector(".hnuser"); 108 | const author = authorElement ? authorElement.innerText : "Unknown"; 109 | 110 | const commentId = nextElement.id; 111 | 112 | // Check for the parent (reply) by looking for 'par' class 113 | const parentElement = nextElement.querySelector(".par a"); 114 | const parentId = parentElement 115 | ? parentElement.getAttribute("href").replace("#", "") 116 | : null; 117 | 118 | if (commentText) { 119 | const comment = { 120 | id: commentId, 121 | author: author, 122 | text: commentText, 123 | parent: parentId, // The parent comment ID, or null if it's a top-level comment 124 | }; 125 | 126 | // Add the comment to the chunk 127 | addCommentToChunk(comment); 128 | } 129 | 130 | // Check the indent level of the next comment 131 | const indentElement = nextElement.querySelector(".ind"); 132 | if (indentElement) { 133 | const nextIndentLevel = parseInt( 134 | indentElement.getAttribute("indent"), 135 | 10 136 | ); 137 | if (nextIndentLevel <= currentIndentLevel) { 138 | break; 139 | } 140 | } 141 | 142 | nextElement = nextElement.nextElementSibling; 143 | } 144 | 145 | // If there are any remaining comments in the current chunk, add them to the array 146 | if (currentChunk.length > 0) { 147 | commentsArray.push([parentComment, ...currentChunk]); 148 | } 149 | 150 | // Send the array of chunks to the background script in one message 151 | try { 152 | await chrome.runtime.sendMessage({ 153 | action: "summarize_comments", 154 | commentsChunks: commentsArray, 155 | }); 156 | } catch (error) { 157 | console.error("Error sending comments for summarization:", error); 158 | } 159 | } 160 | 161 | // Function to summarize all comments (stub for now) 162 | async function summarizeAllComments() { 163 | const topLevelComments = document.querySelectorAll(".comtr"); 164 | 165 | const commentsArray = []; 166 | let currentChunk = []; 167 | let currentChunkLength = 0; 168 | const maxChunkSize = 50000; 169 | 170 | // Function to add a comment to the current chunk or create a new one if necessary 171 | function addCommentToChunk(comment) { 172 | const commentLength = comment.text.length; 173 | 174 | // Check if adding the current comment would exceed the chunk size limit 175 | if (currentChunkLength + commentLength > maxChunkSize) { 176 | // Push the current chunk to commentsArray 177 | commentsArray.push([...currentChunk]); 178 | // Start a new chunk 179 | currentChunk = []; 180 | currentChunkLength = 0; 181 | } 182 | 183 | // Add the comment to the current chunk 184 | currentChunk.push(comment); 185 | currentChunkLength += commentLength; 186 | } 187 | 188 | // Process each top-level comment and its replies 189 | topLevelComments.forEach((commentElement) => { 190 | const indentLevel = parseInt( 191 | commentElement.querySelector(".ind").getAttribute("indent"), 192 | 10 193 | ); 194 | 195 | // Skip comments that aren't top-level (indent level 0) 196 | if (indentLevel !== 0) return; 197 | 198 | let nextElement = commentElement.nextElementSibling; 199 | const currentIndentLevel = parseInt( 200 | commentElement.querySelector(".ind").getAttribute("indent"), 201 | 10 202 | ); 203 | 204 | const parentCommentTextElement = commentElement.querySelector(".commtext"); 205 | const parentCommentText = parentCommentTextElement 206 | ? parentCommentTextElement.innerText 207 | : ""; 208 | const parentAuthorElement = commentElement.querySelector(".hnuser"); 209 | const parentAuthor = parentAuthorElement 210 | ? parentAuthorElement.innerText 211 | : "Unknown"; 212 | const parentId = commentElement.id; 213 | 214 | const parentComment = { 215 | id: parentId, 216 | author: parentAuthor, 217 | text: parentCommentText, 218 | parent: null, // Parent has no parent 219 | }; 220 | 221 | addCommentToChunk(parentComment); 222 | 223 | // Process replies (immediate children, indent level 1) 224 | while (nextElement) { 225 | const commentTextElement = nextElement.querySelector(".commtext"); 226 | const commentText = commentTextElement 227 | ? commentTextElement.innerText 228 | : ""; 229 | 230 | const authorElement = nextElement.querySelector(".hnuser"); 231 | const author = authorElement ? authorElement.innerText : "Unknown"; 232 | 233 | const commentId = nextElement.id; 234 | 235 | const indentElement = nextElement.querySelector(".ind"); 236 | if (indentElement) { 237 | const nextIndentLevel = parseInt( 238 | indentElement.getAttribute("indent"), 239 | 10 240 | ); 241 | if (nextIndentLevel <= currentIndentLevel) { 242 | break; // Stop if the next comment is not a direct reply 243 | } else if (nextIndentLevel === currentIndentLevel + 1) { 244 | // Process immediate replies (indent level 1) 245 | const replyComment = { 246 | id: commentId, 247 | author: author, 248 | text: commentText, 249 | parent: parentId, 250 | }; 251 | addCommentToChunk(replyComment); 252 | } 253 | } 254 | 255 | nextElement = nextElement.nextElementSibling; 256 | } 257 | }); 258 | 259 | // If there are any remaining comments in the current chunk, add them to the array 260 | if (currentChunk.length > 0) { 261 | commentsArray.push([...currentChunk]); 262 | } 263 | 264 | // Send the array of chunks to the background script 265 | try { 266 | await chrome.runtime.sendMessage({ 267 | action: "summarize_comments", 268 | commentsChunks: commentsArray, 269 | }); 270 | } catch (error) { 271 | console.error("Error sending comments for summarization:", error); 272 | } 273 | } 274 | 275 | // Function to send the request to OpenAI 276 | async function sendSummarizationRequest(commentsText, accumulatedSummary) { 277 | return new Promise((resolve, reject) => { 278 | chrome.runtime.sendMessage( 279 | { 280 | action: "summarize_comments", 281 | commentsText, 282 | accumulatedSummary, 283 | }, 284 | (response) => { 285 | if (response && response.summary) { 286 | resolve(response.summary); // Return the new summary 287 | } else { 288 | reject("Summarization failed"); 289 | } 290 | } 291 | ); 292 | }); 293 | } 294 | 295 | // Run the function when the page loads 296 | window.addEventListener("load", () => { 297 | addSummarizeButtons(); 298 | 299 | // listen for alert messages from the background script 300 | chrome.runtime.onMessage.addListener((message) => { 301 | if (message.action === "alert") { 302 | alert(message.message); 303 | } 304 | }); 305 | }); 306 | -------------------------------------------------------------------------------- /hello.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Hello Extensions

4 | 5 | -------------------------------------------------------------------------------- /llm.js: -------------------------------------------------------------------------------- 1 | // LLMProvider.js 2 | class LLMProvider { 3 | async summarize(prompt, stream = false) { 4 | throw new Error("summarize method must be implemented."); 5 | } 6 | 7 | async summarizeStream(prompt, onChunk) { 8 | throw new Error("summarizeStream method must be implemented."); 9 | } 10 | } 11 | 12 | // OpenAIProvider.js 13 | export class OpenAIProvider extends LLMProvider { 14 | constructor(apiKey) { 15 | super(); 16 | this.apiKey = apiKey; 17 | this.apiUrl = "https://api.openai.com/v1/chat/completions"; 18 | this.model = "gpt-4o"; 19 | } 20 | 21 | async summarize(prompt) { 22 | const response = await fetch(this.apiUrl, { 23 | method: "POST", 24 | headers: { 25 | Authorization: `Bearer ${this.apiKey}`, 26 | "Content-Type": "application/json", 27 | }, 28 | body: JSON.stringify({ 29 | model: this.model, 30 | messages: [ 31 | { role: "system", content: "You are a helpful assistant." }, 32 | { role: "user", content: prompt }, 33 | ], 34 | stream: false, 35 | }), 36 | }); 37 | 38 | const data = await response.json(); 39 | return data.choices[0].message.content; 40 | } 41 | 42 | async summarizeStream(prompt, onChunk) { 43 | const response = await fetch(this.apiUrl, { 44 | method: "POST", 45 | headers: { 46 | Authorization: `Bearer ${this.apiKey}`, 47 | "Content-Type": "application/json", 48 | }, 49 | body: JSON.stringify({ 50 | model: this.model, 51 | messages: [ 52 | { role: "system", content: "You are a helpful assistant." }, 53 | { role: "user", content: prompt }, 54 | ], 55 | stream: true, 56 | }), 57 | }); 58 | 59 | const reader = response.body.getReader(); 60 | const decoder = new TextDecoder(); 61 | let text = ""; 62 | 63 | while (true) { 64 | const { done, value } = await reader.read(); 65 | if (done) break; 66 | 67 | const chunk = decoder.decode(value, { stream: true }); 68 | const lines = chunk.split("\n").filter((line) => line.trim() !== ""); 69 | for (const line of lines) { 70 | if (!line.startsWith("data: ")) continue; 71 | const jsonData = line.replace(/^data: /, ""); 72 | if (jsonData && jsonData !== "[DONE]") { 73 | const parsed = JSON.parse(jsonData); 74 | const content = parsed.choices[0]?.delta?.content || ""; 75 | text += content; 76 | onChunk(content); 77 | } 78 | } 79 | } 80 | 81 | return text; 82 | } 83 | } 84 | 85 | // OllamaProvider.js 86 | export class OllamaProvider extends LLMProvider { 87 | constructor() { 88 | super(); 89 | this.apiUrl = "http://localhost:11434/api/generate"; 90 | this.model = "llama3.1"; 91 | } 92 | 93 | async summarize(prompt) { 94 | const response = await fetch(this.apiUrl, { 95 | method: "POST", 96 | headers: { "Content-Type": "application/json" }, 97 | body: JSON.stringify({ 98 | model: this.model, 99 | prompt: prompt, 100 | stream: false, 101 | }), 102 | }); 103 | 104 | const data = await response.json(); 105 | return data.response; 106 | } 107 | 108 | async summarizeStream(prompt, onChunk) { 109 | const response = await fetch(this.apiUrl, { 110 | method: "POST", 111 | headers: { "Content-Type": "application/json" }, 112 | body: JSON.stringify({ 113 | model: this.model, 114 | prompt: prompt, 115 | stream: true, 116 | }), 117 | }); 118 | 119 | const reader = response.body.getReader(); 120 | const decoder = new TextDecoder(); 121 | let text = ""; 122 | 123 | while (true) { 124 | const { done, value } = await reader.read(); 125 | if (done) break; 126 | 127 | const chunk = decoder.decode(value, { stream: true }); 128 | const lines = chunk.split("\n").filter((line) => line.trim() !== ""); 129 | for (const line of lines) { 130 | const jsonData = JSON.parse(line); 131 | const content = jsonData.response; 132 | if (content) { 133 | text += content; 134 | onChunk(content); 135 | } 136 | } 137 | } 138 | 139 | return text; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "FastDigest", 4 | "description": "Summarizes the content of the Hacker News comments", 5 | "version": "1.0", 6 | "action": { 7 | "default_icon": "summarize_hn.png", 8 | "default_title": "Click to open panel" 9 | }, 10 | "permissions": [ 11 | "activeTab","tabs","sidePanel" 12 | ], 13 | "background": { 14 | "service_worker": "background.js", 15 | "type": "module" 16 | }, 17 | "host_permissions": ["http://*/*", "https://*/*", "*://*/*"], 18 | "content_scripts": [ 19 | { 20 | "matches": ["https://news.ycombinator.com/item*"], 21 | "js": ["content.js"], 22 | "run_at": "document_idle" 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /sidepanel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Summary Panel 7 | 51 | 52 | 53 |
54 |

Summarized comments will appear here.

55 |
56 |
57 |
58 |

Summarizing, please wait...

59 |
60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /sidepanel.js: -------------------------------------------------------------------------------- 1 | // sidepanel.js 2 | const summaryDiv = document.getElementById("summary"); 3 | const spinner = document.getElementById("spinner"); 4 | const loadingText = document.getElementById("loading-text"); 5 | let summaryContent = ""; 6 | 7 | // Function to show the loading spinner and text 8 | function showLoading() { 9 | spinner.style.display = "block"; 10 | loadingText.style.display = "block"; 11 | } 12 | 13 | // Function to hide the loading spinner and text 14 | function hideLoading() { 15 | spinner.style.display = "none"; 16 | loadingText.style.display = "none"; 17 | } 18 | 19 | // Function to format text 20 | function formatText(text) { 21 | const formattedText = text 22 | .replace(/\n/g, "
") // Convert newlines to
23 | .replace(/(?:\r\n|\r|\n){2,}/g, "

") // Convert multiple newlines to paragraphs 24 | .replace(/\s+/g, " ") // Ensure there are no extra spaces between words 25 | .trim(); // Remove leading and trailing whitespace 26 | return `

${formattedText}

`; 27 | } 28 | 29 | // Add message listener 30 | chrome.runtime.onMessage.addListener((message) => { 31 | console.log("Received message:", message); 32 | 33 | if (message.action === "stream_chunk") { 34 | // Show loading when the first chunk is received 35 | showLoading(); 36 | 37 | summaryContent += message.chunk; 38 | summaryDiv.innerHTML = formatText(summaryContent); 39 | } else if (message.action === "stream_complete") { 40 | console.log("Stream complete, final summary:", message.summary); 41 | summaryContent = message.summary; 42 | summaryDiv.innerHTML = `

Summary Complete: ${formatText( 43 | summaryContent 44 | )}

`; 45 | hideLoading(); // Hide loading once streaming is complete 46 | } else if (message.action === "stream_error") { 47 | summaryDiv.innerHTML = 48 | "

Error summarizing comments.

"; 49 | hideLoading(); // Hide loading if an error occurs 50 | } else if (message.action === "start_summary") { 51 | console.log("received start summary"); 52 | summaryContent = ""; // Reset the summary content 53 | summaryDiv.innerHTML = ""; // Clear the summary div 54 | showLoading(); 55 | } 56 | }); 57 | 58 | // Ensure the listener is set up before sending messages from the background 59 | console.log("Side panel script loaded and listener set up."); 60 | 61 | chrome.runtime.sendMessage({ action: "sidepanel_ready" }); 62 | -------------------------------------------------------------------------------- /summarize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/built-by-as/FastDigest/af03e66d6ccf39dfc73b44cc31a3eef24f036f5d/summarize.png -------------------------------------------------------------------------------- /summarize_all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/built-by-as/FastDigest/af03e66d6ccf39dfc73b44cc31a3eef24f036f5d/summarize_all.png -------------------------------------------------------------------------------- /summarize_hn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/built-by-as/FastDigest/af03e66d6ccf39dfc73b44cc31a3eef24f036f5d/summarize_hn.png --------------------------------------------------------------------------------