Instantly summarize Wikipedia articles with AI-powered insights
Size
18.2 KB
Version
1.0.1
Created
Nov 6, 2025
Updated
about 1 month ago
1// ==UserScript==
2// @name Wikipedia AI Summarizer
3// @description Instantly summarize Wikipedia articles with AI-powered insights
4// @version 1.0.1
5// @match https://*.en.wikipedia.org/*
6// @icon https://en.wikipedia.org/static/favicon/wikipedia.ico
7// ==/UserScript==
8(function() {
9 'use strict';
10
11 console.log('Wikipedia AI Summarizer extension loaded');
12
13 // Debounce function to prevent excessive calls
14 function debounce(func, wait) {
15 let timeout;
16 return function executedFunction(...args) {
17 const later = () => {
18 clearTimeout(timeout);
19 func(...args);
20 };
21 clearTimeout(timeout);
22 timeout = setTimeout(later, wait);
23 };
24 }
25
26 // Function to extract article content
27 function getArticleContent() {
28 const contentDiv = document.querySelector('#mw-content-text .mw-parser-output');
29 if (!contentDiv) {
30 console.error('Could not find article content');
31 return null;
32 }
33
34 // Get all paragraphs, excluding references and other non-content sections
35 const paragraphs = Array.from(contentDiv.querySelectorAll('p'))
36 .filter(p => {
37 // Exclude paragraphs in reference sections, infoboxes, etc.
38 const parent = p.closest('.reflist, .navbox, .infobox, .metadata, .ambox');
39 return !parent && p.textContent.trim().length > 50;
40 })
41 .map(p => p.textContent.trim())
42 .slice(0, 15); // Take first 15 paragraphs to avoid token limits
43
44 const articleText = paragraphs.join('\n\n');
45 console.log('Extracted article content, length:', articleText.length);
46 return articleText;
47 }
48
49 // Function to get article title
50 function getArticleTitle() {
51 const titleElement = document.querySelector('h1#firstHeading .mw-page-title-main, h1.firstHeading');
52 return titleElement ? titleElement.textContent.trim() : 'this article';
53 }
54
55 // Function to show loading indicator
56 function showLoadingIndicator() {
57 const existingLoader = document.getElementById('wiki-ai-loader');
58 if (existingLoader) return;
59
60 const loader = document.createElement('div');
61 loader.id = 'wiki-ai-loader';
62 loader.innerHTML = `
63 <div style="display: flex; align-items: center; gap: 10px;">
64 <div class="spinner"></div>
65 <span>AI is analyzing the article...</span>
66 </div>
67 `;
68 document.body.appendChild(loader);
69 console.log('Loading indicator shown');
70 }
71
72 // Function to hide loading indicator
73 function hideLoadingIndicator() {
74 const loader = document.getElementById('wiki-ai-loader');
75 if (loader) {
76 loader.remove();
77 console.log('Loading indicator hidden');
78 }
79 }
80
81 // Function to display summary in a modal
82 function displaySummary(summaryData) {
83 console.log('Displaying summary:', summaryData);
84
85 // Remove existing modal if any
86 const existingModal = document.getElementById('wiki-ai-summary-modal');
87 if (existingModal) {
88 existingModal.remove();
89 }
90
91 const modal = document.createElement('div');
92 modal.id = 'wiki-ai-summary-modal';
93 modal.innerHTML = `
94 <div class="modal-content">
95 <div class="modal-header">
96 <h2>📚 AI Summary</h2>
97 <button class="close-btn" id="close-summary-modal">×</button>
98 </div>
99 <div class="modal-body">
100 <div class="summary-section">
101 <h3>🎯 Quick Summary</h3>
102 <p>${summaryData.summary}</p>
103 </div>
104
105 <div class="summary-section">
106 <h3>🔑 Key Points</h3>
107 <ul>
108 ${summaryData.keyPoints.map(point => `<li>${point}</li>`).join('')}
109 </ul>
110 </div>
111
112 <div class="summary-section">
113 <h3>💡 Main Takeaway</h3>
114 <p>${summaryData.mainTakeaway}</p>
115 </div>
116
117 ${summaryData.historicalContext ? `
118 <div class="summary-section">
119 <h3>📅 Historical Context</h3>
120 <p>${summaryData.historicalContext}</p>
121 </div>
122 ` : ''}
123 </div>
124 </div>
125 `;
126
127 document.body.appendChild(modal);
128
129 // Add event listeners
130 document.getElementById('close-summary-modal').addEventListener('click', () => {
131 modal.remove();
132 });
133
134 modal.addEventListener('click', (e) => {
135 if (e.target === modal) {
136 modal.remove();
137 }
138 });
139
140 // Close on Escape key
141 const escapeHandler = (e) => {
142 if (e.key === 'Escape') {
143 modal.remove();
144 document.removeEventListener('keydown', escapeHandler);
145 }
146 };
147 document.addEventListener('keydown', escapeHandler);
148 }
149
150 // Main function to generate AI summary
151 async function generateAISummary() {
152 console.log('Starting AI summary generation');
153
154 const articleTitle = getArticleTitle();
155 const articleContent = getArticleContent();
156
157 if (!articleContent) {
158 alert('Could not extract article content. Please make sure you are on a Wikipedia article page.');
159 return;
160 }
161
162 showLoadingIndicator();
163
164 try {
165 // Use RM.aiCall with structured output for better results
166 const summaryData = await RM.aiCall(
167 `You are a helpful AI assistant that creates concise, informative summaries of Wikipedia articles.
168
169Article Title: ${articleTitle}
170
171Article Content:
172${articleContent}
173
174Please analyze this Wikipedia article and provide a comprehensive summary.`,
175 {
176 type: "json_schema",
177 json_schema: {
178 name: "wikipedia_summary",
179 schema: {
180 type: "object",
181 properties: {
182 summary: {
183 type: "string",
184 description: "A concise 2-3 sentence summary of the entire article"
185 },
186 keyPoints: {
187 type: "array",
188 description: "3-5 key points or facts from the article",
189 items: { type: "string" },
190 minItems: 3,
191 maxItems: 5
192 },
193 mainTakeaway: {
194 type: "string",
195 description: "The single most important thing to understand from this article"
196 },
197 historicalContext: {
198 type: "string",
199 description: "Brief historical context if applicable, otherwise empty string"
200 }
201 },
202 required: ["summary", "keyPoints", "mainTakeaway"]
203 }
204 }
205 }
206 );
207
208 console.log('AI summary generated successfully:', summaryData);
209 hideLoadingIndicator();
210 displaySummary(summaryData);
211
212 // Cache the summary
213 const cacheKey = 'wiki_summary_' + window.location.pathname;
214 await GM.setValue(cacheKey, JSON.stringify({
215 data: summaryData,
216 timestamp: Date.now()
217 }));
218 console.log('Summary cached');
219
220 } catch (error) {
221 console.error('Error generating AI summary:', error);
222 hideLoadingIndicator();
223 alert('Failed to generate summary. Please try again. Error: ' + error.message);
224 }
225 }
226
227 // Function to check if we have a cached summary
228 async function getCachedSummary() {
229 const cacheKey = 'wiki_summary_' + window.location.pathname;
230 const cached = await GM.getValue(cacheKey);
231
232 if (cached) {
233 try {
234 const parsedCache = JSON.parse(cached);
235 // Cache expires after 24 hours
236 const cacheAge = Date.now() - parsedCache.timestamp;
237 if (cacheAge < 24 * 60 * 60 * 1000) {
238 console.log('Using cached summary');
239 return parsedCache.data;
240 }
241 } catch (e) {
242 console.error('Error parsing cached summary:', e);
243 }
244 }
245 return null;
246 }
247
248 // Function to create and add the summarize button
249 function createSummarizeButton() {
250 console.log('Creating summarize button');
251
252 // Check if button already exists
253 if (document.getElementById('wiki-ai-summarize-btn')) {
254 console.log('Button already exists');
255 return;
256 }
257
258 // Find the views menu where we'll add our button
259 const viewsMenu = document.querySelector('#p-views .vector-menu-content-list');
260
261 if (!viewsMenu) {
262 console.error('Could not find views menu');
263 return;
264 }
265
266 // Create the button as a list item matching Wikipedia's style
267 const buttonLi = document.createElement('li');
268 buttonLi.id = 'ca-ai-summarize';
269 buttonLi.className = 'vector-tab-noicon mw-list-item';
270
271 const buttonLink = document.createElement('a');
272 buttonLink.href = '#';
273 buttonLink.id = 'wiki-ai-summarize-btn';
274 buttonLink.innerHTML = '<span>✨ AI Summary</span>';
275 buttonLink.title = 'Generate AI-powered summary of this article';
276
277 buttonLink.addEventListener('click', async (e) => {
278 e.preventDefault();
279 console.log('Summarize button clicked');
280
281 // Check for cached summary first
282 const cachedSummary = await getCachedSummary();
283 if (cachedSummary) {
284 displaySummary(cachedSummary);
285 } else {
286 generateAISummary();
287 }
288 });
289
290 buttonLi.appendChild(buttonLink);
291 viewsMenu.appendChild(buttonLi);
292
293 console.log('Summarize button added successfully');
294 }
295
296 // Add CSS styles
297 function addStyles() {
298 const style = document.createElement('style');
299 style.textContent = `
300 /* Loading indicator styles */
301 #wiki-ai-loader {
302 position: fixed;
303 top: 20px;
304 right: 20px;
305 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
306 color: white;
307 padding: 16px 24px;
308 border-radius: 12px;
309 box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
310 z-index: 10000;
311 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
312 font-size: 14px;
313 font-weight: 500;
314 animation: slideIn 0.3s ease-out;
315 }
316
317 @keyframes slideIn {
318 from {
319 transform: translateX(400px);
320 opacity: 0;
321 }
322 to {
323 transform: translateX(0);
324 opacity: 1;
325 }
326 }
327
328 #wiki-ai-loader .spinner {
329 width: 20px;
330 height: 20px;
331 border: 3px solid rgba(255, 255, 255, 0.3);
332 border-top-color: white;
333 border-radius: 50%;
334 animation: spin 0.8s linear infinite;
335 }
336
337 @keyframes spin {
338 to { transform: rotate(360deg); }
339 }
340
341 /* Modal styles */
342 #wiki-ai-summary-modal {
343 position: fixed;
344 top: 0;
345 left: 0;
346 width: 100%;
347 height: 100%;
348 background: rgba(0, 0, 0, 0.7);
349 display: flex;
350 justify-content: center;
351 align-items: center;
352 z-index: 9999;
353 animation: fadeIn 0.3s ease-out;
354 padding: 20px;
355 box-sizing: border-box;
356 }
357
358 @keyframes fadeIn {
359 from { opacity: 0; }
360 to { opacity: 1; }
361 }
362
363 #wiki-ai-summary-modal .modal-content {
364 background: white;
365 border-radius: 16px;
366 max-width: 700px;
367 width: 100%;
368 max-height: 85vh;
369 overflow-y: auto;
370 box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
371 animation: slideUp 0.3s ease-out;
372 }
373
374 @keyframes slideUp {
375 from {
376 transform: translateY(50px);
377 opacity: 0;
378 }
379 to {
380 transform: translateY(0);
381 opacity: 1;
382 }
383 }
384
385 #wiki-ai-summary-modal .modal-header {
386 display: flex;
387 justify-content: space-between;
388 align-items: center;
389 padding: 24px 28px;
390 border-bottom: 2px solid #f0f0f0;
391 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
392 color: white;
393 border-radius: 16px 16px 0 0;
394 }
395
396 #wiki-ai-summary-modal .modal-header h2 {
397 margin: 0;
398 font-size: 24px;
399 font-weight: 700;
400 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
401 }
402
403 #wiki-ai-summary-modal .close-btn {
404 background: rgba(255, 255, 255, 0.2);
405 border: none;
406 color: white;
407 font-size: 32px;
408 cursor: pointer;
409 width: 40px;
410 height: 40px;
411 border-radius: 50%;
412 display: flex;
413 align-items: center;
414 justify-content: center;
415 transition: all 0.2s;
416 line-height: 1;
417 padding: 0;
418 }
419
420 #wiki-ai-summary-modal .close-btn:hover {
421 background: rgba(255, 255, 255, 0.3);
422 transform: rotate(90deg);
423 }
424
425 #wiki-ai-summary-modal .modal-body {
426 padding: 28px;
427 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
428 color: #333;
429 line-height: 1.7;
430 }
431
432 #wiki-ai-summary-modal .summary-section {
433 margin-bottom: 28px;
434 padding: 20px;
435 background: #f8f9fa;
436 border-radius: 12px;
437 border-left: 4px solid #667eea;
438 }
439
440 #wiki-ai-summary-modal .summary-section:last-child {
441 margin-bottom: 0;
442 }
443
444 #wiki-ai-summary-modal .summary-section h3 {
445 margin: 0 0 12px 0;
446 font-size: 18px;
447 font-weight: 600;
448 color: #667eea;
449 }
450
451 #wiki-ai-summary-modal .summary-section p {
452 margin: 0;
453 font-size: 15px;
454 color: #2c3e50;
455 }
456
457 #wiki-ai-summary-modal .summary-section ul {
458 margin: 0;
459 padding-left: 24px;
460 }
461
462 #wiki-ai-summary-modal .summary-section li {
463 margin-bottom: 10px;
464 font-size: 15px;
465 color: #2c3e50;
466 }
467
468 #wiki-ai-summary-modal .summary-section li:last-child {
469 margin-bottom: 0;
470 }
471
472 /* Button hover effect */
473 #wiki-ai-summarize-btn:hover {
474 background: rgba(102, 126, 234, 0.1);
475 }
476
477 /* Scrollbar styling for modal */
478 #wiki-ai-summary-modal .modal-content::-webkit-scrollbar {
479 width: 8px;
480 }
481
482 #wiki-ai-summary-modal .modal-content::-webkit-scrollbar-track {
483 background: #f1f1f1;
484 border-radius: 0 16px 16px 0;
485 }
486
487 #wiki-ai-summary-modal .modal-content::-webkit-scrollbar-thumb {
488 background: #667eea;
489 border-radius: 4px;
490 }
491
492 #wiki-ai-summary-modal .modal-content::-webkit-scrollbar-thumb:hover {
493 background: #764ba2;
494 }
495 `;
496 document.head.appendChild(style);
497 console.log('Styles added');
498 }
499
500 // Initialize the extension
501 function init() {
502 console.log('Initializing Wikipedia AI Summarizer');
503
504 // Check if we're on an article page (not main page, special pages, etc.)
505 const isArticlePage = document.querySelector('h1#firstHeading') &&
506 !window.location.pathname.includes('Special:') &&
507 window.location.pathname !== '/wiki/Main_Page';
508
509 if (!isArticlePage) {
510 console.log('Not on an article page, skipping initialization');
511 return;
512 }
513
514 addStyles();
515 createSummarizeButton();
516
517 console.log('Wikipedia AI Summarizer initialized successfully');
518 }
519
520 // Wait for the page to be fully loaded
521 if (document.readyState === 'loading') {
522 document.addEventListener('DOMContentLoaded', init);
523 } else {
524 init();
525 }
526
527 // Handle navigation changes (for single-page navigation)
528 const debouncedInit = debounce(init, 500);
529 const observer = new MutationObserver(debouncedInit);
530 observer.observe(document.body, { childList: true, subtree: true });
531
532})();