User:Overandoutnerd/Scripts/articleSummary.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump.
This code will be executed when previewing this page.
This code will be executed when previewing this page.
This user script seems to have a documentation page at User:Overandoutnerd/Scripts/articleSummary.
// ==UserScript==
// @name Wikipedia AI Summary (Google Gemini)
// @namespace wiki-ai-summary-gemini
// @version 1.3.0
// @description Generate AI-based summaries of Wikipedia articles for private reading
// @match https://*.wikipedia.org/wiki/*
// @grant none
// ==/UserScript==
(() => {
"use strict";
/* ------------------------------------------------------------------
* CONFIG
* ------------------------------------------------------------------ */
const MODEL = "gemini-2.5-flash";
const STORAGE_KEY = "gemini_api_key";
const MAX_CHARS = 6000;
/* ------------------------------------------------------------------
* DARK MODE
* ------------------------------------------------------------------ */
function isDarkMode() {
if (
document.documentElement.classList.contains("skin-theme-clientpref-night") ||
document.body.classList.contains("skin-theme-clientpref-night")
) return true;
if (
(
document.documentElement.classList.contains("skin-theme-clientpref-os") ||
document.body.classList.contains("skin-theme-clientpref-os")
) &&
window.matchMedia("(prefers-color-scheme: dark)").matches
) return true;
return false;
}
/* ------------------------------------------------------------------
* API KEY STORAGE
* ------------------------------------------------------------------ */
function getApiKey() {
return localStorage.getItem(STORAGE_KEY) || "";
}
function saveApiKey(key) {
localStorage.setItem(STORAGE_KEY, key.trim());
}
function clearApiKey() {
localStorage.removeItem(STORAGE_KEY);
}
/* ------------------------------------------------------------------
* MARKDOWN → HTML (SAFE + BASIC)
* ------------------------------------------------------------------ */
function renderMarkdown(markdown) {
let html = markdown
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
html = html.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
html = html.replace(/^\s*[-*]\s+(.*)$/gm, "<li>$1</li>");
html = html.replace(/(<li>.*<\/li>)/gs, "<ul>$1</ul>");
html = html
.split(/\n{2,}/)
.map(block => {
if (block.startsWith("<ul>")) return block;
return `<p>${block.replace(/\n/g, "<br>")}</p>`;
})
.join("");
return html;
}
/* ------------------------------------------------------------------
* WIKIPEDIA HELPERS
* ------------------------------------------------------------------ */
function getPageTitle() {
return mw.config.get("wgPageName").replace(/_/g, " ");
}
async function fetchArticleText() {
const url = new URL("/w/api.php", location.origin);
url.search = new URLSearchParams({
action: "query",
prop: "extracts",
explaintext: "1",
redirects: "1",
titles: getPageTitle(),
format: "json"
});
const res = await fetch(url);
const json = await res.json();
return Object.values(json.query.pages)[0].extract || "";
}
function chunkText(text, size) {
const chunks = [];
for (let i = 0; i < text.length; i += size) {
chunks.push(text.slice(i, i + size));
}
return chunks;
}
/* ------------------------------------------------------------------
* GEMINI API
* ------------------------------------------------------------------ */
async function callGemini(prompt, apiKey) {
const endpoint =
`https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent?key=${apiKey}`;
const res = await fetch(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
contents: [{ parts: [{ text: prompt }] }]
})
});
if (!res.ok) {
throw new Error(`Gemini API error ${res.status}`);
}
const json = await res.json();
return json.candidates?.[0]?.content?.parts?.[0]?.text || "";
}
/* ------------------------------------------------------------------
* STYLES
* ------------------------------------------------------------------ */
function injectStyles() {
if (document.getElementById("ai-summary-styles")) return;
const dark = isDarkMode();
const style = document.createElement("style");
style.id = "ai-summary-styles";
style.textContent = `
.ai-summary-box {
margin: 18px 0;
padding: 16px;
border-radius: 14px;
background:
${dark
? "linear-gradient(#202124, #202124) padding-box"
: "linear-gradient(#f8f9fa, #f8f9fa) padding-box"},
linear-gradient(135deg, #7c4dff, #00bcd4) border-box;
border: 2px solid transparent;
box-shadow: ${dark
? "0 6px 18px rgba(0,0,0,.6)"
: "0 6px 18px rgba(0,0,0,.08)"};
color: ${dark ? "#e8eaed" : "#202124"};
}
.ai-summary-box *,
.ai-summary-box *::before,
.ai-summary-box *::after {
box-sizing: border-box;
}
.ai-summary-btn {
border: none;
border-radius: 999px;
padding: 8px 16px;
font-size: 13px;
font-weight: 600;
color: #fff;
background: linear-gradient(135deg, #7c4dff, #00bcd4);
cursor: pointer;
}
.ai-summary-output {
margin-top: 12px;
font-size: 14px;
line-height: 1.5;
}
.ai-summary-output p { margin: 6px 0; }
.ai-summary-output ul { margin: 10px 0; padding-left: 20px; }
.ai-summary-output li + li { margin-top: 5px; }
.ai-summary-loader {
margin-top: 12px;
display: none;
}
.ai-summary-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: linear-gradient(135deg, #7c4dff, #00bcd4);
animation: bounce 1.4s infinite ease-in-out both;
display: inline-block;
}
.ai-summary-dot:nth-child(1) { animation-delay: -0.32s; }
.ai-summary-dot:nth-child(2) { animation-delay: -0.16s; }
@keyframes bounce {
0%,80%,100% { transform: scale(0); opacity: .3; }
40% { transform: scale(1); opacity: 1; }
}
`;
document.head.appendChild(style);
}
/* ------------------------------------------------------------------
* UI
* ------------------------------------------------------------------ */
function createUi() {
injectStyles();
const hasKey = !!getApiKey();
const box = document.createElement("div");
box.className = "ai-summary-box";
box.innerHTML = `
<div style="display:flex;justify-content:space-between;align-items:center">
<strong>AI Summary</strong>
<button
id="edit-key-btn"
title="Edit API key"
aria-label="Edit API key"
style="
display:${hasKey ? "flex" : "none"};
align-items:center;
justify-content:center;
background:none;
border:none;
padding:6px;
cursor:pointer;
opacity:.7;
color:inherit;
"
>
<svg
viewBox="0 0 24 24"
width="16"
height="16"
aria-hidden="true"
focusable="false"
>
<path
d="M15.673,3.913C16.892,2.694 18.868,2.694 20.087,3.913C21.306,5.132 21.306,7.108 20.087,8.327L14.15,14.264C13.385,15.029 12.392,15.525 11.321,15.678L9.141,15.99C8.83,16.034 8.515,15.929 8.293,15.707C8.07,15.484 7.966,15.17 8.01,14.858L8.321,12.678C8.474,11.607 8.971,10.615 9.736,9.85L15.673,3.913ZM18.673,5.327C18.235,4.889 17.525,4.889 17.087,5.327L11.15,11.264C10.691,11.723 10.393,12.319 10.301,12.961L10.179,13.821L11.039,13.698C11.681,13.607 12.277,13.309 12.736,12.85L18.673,6.913C19.111,6.475 19.111,5.765 18.673,5.327ZM11,3.999C11,4.551 10.553,5 10.001,5C9.002,5.001 8.298,5.008 7.747,5.061C7.207,5.112 6.885,5.201 6.638,5.327C6.074,5.614 5.615,6.073 5.327,6.638C5.193,6.901 5.101,7.249 5.051,7.854C5.001,8.471 5,9.263 5,10.4V13.6C5,14.736 5.001,15.529 5.051,16.146C5.101,16.751 5.193,17.098 5.327,17.362C5.615,17.926 6.074,18.385 6.638,18.673C6.901,18.807 7.249,18.899 7.854,18.949C8.471,18.999 9.263,19 10.4,19H13.6C14.737,19 15.529,18.999 16.146,18.949C16.751,18.899 17.099,18.807 17.362,18.673C17.927,18.385 18.385,17.926 18.673,17.362C18.799,17.115 18.888,16.793 18.939,16.253C18.992,15.702 18.999,14.998 19,13.999C19,13.447 19.448,12.999 20.001,13C20.553,13 21,13.448 21,14.001C20.999,14.979 20.993,15.781 20.93,16.442C20.866,17.116 20.739,17.713 20.455,18.27C19.976,19.211 19.211,19.976 18.27,20.455C17.678,20.757 17.038,20.882 16.309,20.942C15.601,21 14.727,21 13.643,21H10.357C9.273,21 8.399,21 7.691,20.942C6.963,20.882 6.322,20.757 5.73,20.455C4.789,19.976 4.024,19.211 3.545,18.27C3.243,17.677 3.117,17.037 3.058,16.309C3,15.601 3,14.726 3,13.643V10.357C3,9.273 3,8.399 3.058,7.691C3.117,6.962 3.243,6.322 3.545,5.73C4.024,4.789 4.789,4.024 5.73,3.545C6.286,3.261 6.884,3.133 7.557,3.069C8.219,3.007 9.021,3.001 9.999,3C10.552,3 11,3.447 11,3.999Z"
fill="currentColor"
/>
</svg>
</button>
</div>
<p style="font-size:12px;opacity:.7">Generated with Google Gemini.</p>
<div id="key-section" style="display:${hasKey ? "none" : "block"}">
<input
id="api-key-input"
type="text"
placeholder="Paste your Gemini API key"
value="${hasKey ? getApiKey() : ""}"
spellcheck="false"
autocomplete="off"
style="
width:100%;
max-width: 100%;
padding:8px 10px;
border-radius:8px;
border:1px solid #ccc;
font-size:13px;
"
/>
<button id="save-key-btn" class="ai-summary-btn" style="margin-top:8px">
Save API Key
</button>
</div>
<button id="generate-btn" class="ai-summary-btn" style="margin-top:12px">
Generate AI Summary
</button>
<div id="loader" class="ai-summary-loader">
<span class="ai-summary-dot"></span>
<span class="ai-summary-dot"></span>
<span class="ai-summary-dot"></span>
</div>
<div id="output" class="ai-summary-output"></div>
`;
return box;
}
/* ------------------------------------------------------------------
* SUMMARY LOGIC
* ------------------------------------------------------------------ */
async function generateSummary() {
const output = document.getElementById("output");
const loader = document.getElementById("loader");
const btn = document.getElementById("generate-btn");
const apiKey = getApiKey();
if (!apiKey) {
output.textContent = "Add your Gemini API key first.";
return;
}
output.textContent = "";
loader.style.display = "block";
btn.disabled = true;
try {
const text = await fetchArticleText();
const chunks = chunkText(text, MAX_CHARS);
const partials = [];
for (const chunk of chunks) {
const prompt = `
Create bullet-point notes from the text below.
Rules:
- Maximum 4 bullet points
- One sentence per bullet
- Neutral, factual tone
- No repetition
TEXT:
${chunk}
`.trim();
partials.push(await callGemini(prompt, apiKey));
}
const finalPrompt = `
Create a concise Wikipedia-style summary.
Format:
- One short paragraph (2–3 sentences)
- 3–5 bullet points
Rules:
- Neutral
- No repetition
- No new info
- Max 120 words
CONTENT:
${partials.join("\n")}
`.trim();
const markdown = await callGemini(finalPrompt, apiKey);
output.innerHTML = renderMarkdown(markdown);
} catch (err) {
console.error(err);
output.textContent = "Failed to generate summary.";
} finally {
loader.style.display = "none";
btn.disabled = false;
}
}
/* ------------------------------------------------------------------
* INIT
* ------------------------------------------------------------------ */
function init() {
const heading = document.getElementById("firstHeading");
if (!heading) return;
const siteSub = document.getElementById("siteSub");
const box = createUi();
(siteSub || heading).parentNode.insertBefore(
box,
siteSub ? siteSub.nextSibling : heading.nextSibling
);
document.getElementById("generate-btn").onclick = generateSummary;
const saveBtn = document.getElementById("save-key-btn");
const editBtn = document.getElementById("edit-key-btn");
const keySection = document.getElementById("key-section");
const keyInput = document.getElementById("api-key-input");
if (saveBtn) {
saveBtn.onclick = () => {
if (!keyInput.value.trim()) return;
saveApiKey(keyInput.value);
keySection.style.display = "none";
editBtn.style.display = "block";
document.getElementById("generate-btn").disabled = false;
};
}
if (editBtn) {
editBtn.onclick = () => {
keyInput.value = getApiKey();
keySection.style.display = "block";
editBtn.style.display = "none";
document.getElementById("generate-btn").disabled = true;
};
}
}
init();
})();