Summarizes YouTube comments using AI to provide quick insights into viewer opinions and discussions
Size
11.5 KB
Version
1.1.3
Created
Nov 27, 2025
Updated
19 days ago
1// ==UserScript==
2// @name YouTube Comment Summarizer
3// @description Summarizes YouTube comments using AI to provide quick insights into viewer opinions and discussions
4// @version 1.1.3
5// @match https://*.youtube.com/*
6// @icon https://www.youtube.com/s/desktop/e5522eef/img/logos/favicon_32x32.png
7// ==/UserScript==
8(function() {
9 'use strict';
10
11 let isProcessing = false;
12
13 function debounce(func, wait) {
14 let timeout;
15 return function(...args) {
16 clearTimeout(timeout);
17 timeout = setTimeout(() => func.apply(this, args), wait);
18 };
19 }
20
21 function createSummaryButton() {
22 const commentsHeader = document.querySelector('ytd-comments-header-renderer[class*="style-scope"] h2#count') ||
23 document.querySelector('#comments #header h2');
24
25 if (!commentsHeader) {
26 return null;
27 }
28
29 if (document.querySelector('#comment-summary-btn')) {
30 return;
31 }
32
33 const button = document.createElement('button');
34 button.id = 'comment-summary-btn';
35 button.textContent = '🤖 Summarize Comments';
36 button.style.backgroundColor = '#1976d2';
37 button.style.color = 'white';
38 button.style.border = 'none';
39 button.style.padding = '8px 16px';
40 button.style.borderRadius = '20px';
41 button.style.cursor = 'pointer';
42 button.style.fontSize = '14px';
43 button.style.marginLeft = '16px';
44
45 button.addEventListener('click', handleSummaryClick);
46 commentsHeader.appendChild(button);
47 }
48
49 async function handleSummaryClick() {
50 if (isProcessing) return;
51
52 isProcessing = true;
53 const button = document.querySelector('#comment-summary-btn');
54 button.textContent = 'Processing...';
55 button.disabled = true;
56
57 const videoId = extractVideoId();
58 if (!videoId) {
59 alert('Unable to identify video');
60 button.textContent = '🤖 Summarize Comments';
61 button.disabled = false;
62 isProcessing = false;
63 return;
64 }
65
66 const cachedSummary = await loadCachedSummary(videoId);
67 if (cachedSummary) {
68 displaySummary(cachedSummary);
69 button.textContent = '🤖 Summarize Comments';
70 button.disabled = false;
71 isProcessing = false;
72 return;
73 }
74
75 const comments = await collectVisibleComments();
76 if (comments.length === 0) {
77 alert('No comments found to summarize');
78 button.textContent = '🤖 Summarize Comments';
79 button.disabled = false;
80 isProcessing = false;
81 return;
82 }
83
84 try {
85 const summary = await generateSummary(comments);
86 await saveSummaryToCache(videoId, summary);
87 displaySummary(summary);
88 } catch (error) {
89 console.error('Summary generation failed:', error);
90 alert('Failed to generate summary. Please try again.');
91 }
92
93 isProcessing = false;
94 button.textContent = '🤖 Summarize Comments';
95 button.disabled = false;
96 }
97
98 function extractVideoId() {
99 const urlParams = new URLSearchParams(window.location.search);
100 return urlParams.get('v') || null;
101 }
102
103 async function loadCachedSummary(videoId) {
104 try {
105 const cachedData = await GM.getValue('summary_' + videoId, null);
106 if (cachedData) {
107 return JSON.parse(cachedData);
108 }
109 return null;
110 } catch (error) {
111 console.error('Error loading cached summary:', error);
112 return null;
113 }
114 }
115
116 async function saveSummaryToCache(videoId, summary) {
117 try {
118 await GM.setValue('summary_' + videoId, JSON.stringify(summary));
119 } catch (error) {
120 console.error('Failed to cache summary:', error);
121 }
122 }
123
124 async function collectVisibleComments() {
125 const commentElements = document.querySelectorAll('yt-attributed-string#content-text[class*="style-scope"] span[role="text"]');
126 const comments = [];
127
128 for (const element of commentElements) {
129 const text = element.textContent.trim();
130 if (text.length > 10) {
131 comments.push(text);
132 }
133 }
134
135 return comments.slice(0, 50);
136 }
137
138 async function generateSummary(comments) {
139 const prompt = 'Analyze and summarize these YouTube comments. Provide key themes, sentiment analysis, and main discussion points:\n\n' + comments.join('\n---\n');
140
141 const summary = await RM.aiCall(prompt, {
142 type: 'json_schema',
143 json_schema: {
144 name: 'comment_summary',
145 schema: {
146 type: 'object',
147 properties: {
148 overallSentiment: {
149 type: 'string',
150 enum: ['positive', 'negative', 'neutral', 'mixed']
151 },
152 keyThemes: {
153 type: 'array',
154 items: { type: 'string' }
155 },
156 mainPoints: {
157 type: 'array',
158 items: { type: 'string' }
159 },
160 controversialTopics: {
161 type: 'array',
162 items: { type: 'string' }
163 },
164 viewerOpinions: {
165 type: 'array',
166 items: { type: 'string' }
167 },
168 commentCount: {
169 type: 'number'
170 }
171 },
172 required: ['overallSentiment', 'keyThemes', 'mainPoints', 'commentCount']
173 }
174 }
175 });
176
177 return summary;
178 }
179
180 function displaySummary(summary) {
181 removePreviousSummary();
182
183 const summaryContainer = document.createElement('div');
184 summaryContainer.id = 'youtube-comment-summary';
185 summaryContainer.style.backgroundColor = '#f8f9fa';
186 summaryContainer.style.border = '1px solid #e0e0e0';
187 summaryContainer.style.borderRadius = '8px';
188 summaryContainer.style.padding = '16px';
189 summaryContainer.style.margin = '16px 0';
190 summaryContainer.style.fontFamily = 'Roboto, Arial, sans-serif';
191
192 const title = document.createElement('h3');
193 title.textContent = 'Comment Summary';
194 title.style.margin = '0 0 12px 0';
195 title.style.fontSize = '16px';
196 title.style.fontWeight = 'bold';
197 title.style.color = '#333';
198
199 const sentimentSection = createSummarySection('Overall Sentiment', summary.overallSentiment, getSentimentColor(summary.overallSentiment));
200 const themesSection = createSummarySection('Key Themes', summary.keyThemes.join(', '), '#1976d2');
201
202 const mainPointsList = summary.mainPoints.map(point => `<li>${point}</li>`).join('');
203 const pointsSection = createSummarySection('Main Discussion Points', `<ul>${mainPointsList}</ul>`, '#4caf50');
204
205 summaryContainer.appendChild(title);
206 summaryContainer.appendChild(sentimentSection);
207 summaryContainer.appendChild(themesSection);
208 summaryContainer.appendChild(pointsSection);
209
210 if (summary.controversialTopics && summary.controversialTopics.length > 0) {
211 const controversySection = createSummarySection('Controversial Topics', summary.controversialTopics, '#ff5722');
212 summaryContainer.appendChild(controversySection);
213 }
214
215 const header = document.querySelector('#comments #header');
216 if (header) {
217 header.insertAdjacentElement('afterend', summaryContainer);
218 }
219 }
220
221 function createSummarySection(title, content, color) {
222 const section = document.createElement('div');
223
224 const sectionTitle = document.createElement('div');
225 sectionTitle.textContent = title;
226 sectionTitle.style.fontWeight = 'bold';
227 sectionTitle.style.color = color;
228 sectionTitle.style.marginBottom = '8px';
229 sectionTitle.style.fontSize = '14px';
230
231 const sectionContent = document.createElement('div');
232 if (Array.isArray(content)) {
233 const ul = document.createElement('ul');
234 content.forEach(item => {
235 const li = document.createElement('li');
236 li.textContent = item;
237 ul.appendChild(li);
238 });
239 sectionContent.appendChild(ul);
240 } else {
241 sectionContent.textContent = content;
242 }
243
244 sectionContent.style.fontSize = '13px';
245 sectionContent.style.color = '#666';
246 sectionContent.style.lineHeight = '1.4';
247
248 section.appendChild(sectionTitle);
249 section.appendChild(sectionContent);
250
251 return section;
252 }
253
254 function getSentimentColor(sentiment) {
255 switch (sentiment) {
256 case 'positive':
257 return '#4caf50';
258 case 'negative':
259 return '#f44336';
260 case 'mixed':
261 return '#ff9800';
262 default:
263 return '#757575';
264 }
265 }
266
267 function removePreviousSummary() {
268 const existingSummary = document.querySelector('#youtube-comment-summary');
269 if (existingSummary) {
270 existingSummary.remove();
271 }
272 }
273
274 function waitForComments() {
275 let attempts = 0;
276 const maxAttempts = 20;
277
278 function checkForComments() {
279 const commentsSection = document.querySelector('#comments');
280 if (commentsSection) {
281 createSummaryButton();
282 return;
283 }
284
285 attempts++;
286 if (attempts < maxAttempts) {
287 setTimeout(() => checkForComments(), 5000);
288 }
289 }
290
291 checkForComments();
292 }
293
294 function handlePageChange() {
295 if (window.location.href.includes('/watch?v=') && document.querySelector('#comments')) {
296 createSummaryButton();
297 } else {
298 console.log('Not on video page or comments not loaded yet');
299 }
300 }
301
302 const debouncedHandlePageChange = debounce(handlePageChange, 1000);
303
304 function init() {
305 // Check if window.location.href.includes('/watch?v=') to ensure we're on a video page, if not return early
306 if (!window.location.href.includes('/watch?v=')) {
307 return;
308 }
309
310 // Call waitForComments() to initialize the summary button when comments are loaded
311 waitForComments();
312
313 // Add window.addEventListener('popstate', debouncedHandlePageChange) to reinitialize when user navigates using browser back/forward with debounce
314 window.addEventListener('popstate', debouncedHandlePageChange);
315
316 // Create MutationObserver with debounced callback: new MutationObserver(debounce(() => { if (window.location.href.includes('/watch?v=')) handlePageChange(); }, 500)).observe(document.body, { childList: true, subtree: true }) with 500ms debounce
317 new MutationObserver(debounce(() => {
318 if (window.location.href.includes('/watch?v=')) {
319 handlePageChange();
320 }
321 }, 500)).observe(document.body, { childList: true, subtree: true });
322 }
323
324 if (document.readyState === 'loading') {
325 document.addEventListener('DOMContentLoaded', init);
326 } else {
327 init();
328 }
329})();