(function(){
const scriptName = 'SpellGrammarSuggestionsList';
let stopProcessing = false;
let totalProcessedArticles = 0;
const totalConcurrentlyProcessedArticles = 10;
const maxWikitextLength = 30000;
$.when(mw.loader.using('mediawiki.util'), $.ready).then(function(){
const portletLink = mw.util.addPortletLink('p-tb', '#', scriptName, scriptName + 'Id');
portletLink.onclick = function(e) {
e.preventDefault();
prepareUI();
defineButtonFunctions();
};
});
function prepareUI(){
$(function() {
$('<style>')
.prop('type', 'text/css')
.html(`
.btn-spellGrammarSuggestionsList {
background-color: #1e6fff;
color: #fff;
border: none;
padding: 10px 16px;
border-radius: 6px;
font: inherit;
cursor: pointer;
}
.btn-spellGrammarSuggestionsList:hover {
background-color: #1557d6;
}
`)
.appendTo('head');
});
$('#content').hide();
$("#content").parent().append(`<div id="scriptContainer" style="display:flex; flex-direction: column;">
<style>
textarea {
resize: none;
padding: 5px;
}
button {
margin: 5px;
}
</style>
<h1>SpellGrammarSuggestionsList</h1>
<div style="display:flex;">
<div style="flex: 1; display:flex; flex-direction: column; margin: 5px; height: 50vh; overflow-y: auto;">
<textarea id="article-list" style="height: 100%;" placeholder="Enter a list of articles or click on the button [Add 10 random articles]. Then click on the button [Start]. If this is your first use, click the button [Add/Remove API key] before.\n\nThe automatic suggestions can contain inaccuracies and require a thorough human review."></textarea>
</div>
<div style="flex: 2; display:flex; flex-direction: column; margin: 5px; height: 50vh; overflow-y: auto;">
<table class="wikitable" style="height: 100%; margin: 0px; width: 100%; border-collapse: collapse;">
<thead>
</thead>
<tbody id="results"></tbody>
</table>
</div>
</div>
<div style="display:flex;">
<button id="btStart" style="flex: 1;">Start</button>
<button id="btStop" disabled="" style="flex: 1;">Stop</button>
<button id="btAddRandom" style="flex: 1;">Add 10 random articles</button>
<button id="btAddCurrent" style="flex: 1;">Add current page</button>
<button id="btAPI" style="flex: 1;">Add/Remove API key</button>
<button id="btClose" style="flex: 1;">Close</button>
</div>
</div>`);
let currentAPIKey = localStorage.getItem('SpellGrammarSuggestionsListAPIKey');
if(currentAPIKey === 'null' || currentAPIKey === null || currentAPIKey === ''){
$('#btStart').prop('disabled', true);
}
}
function defineButtonFunctions(){
$('#btStart').click(async function(){
if($('#article-list').val().trim().length === 0) return;
stopProcessing = false;
$('#btStart').prop("disabled", true);
$('#btStop').prop("disabled", false);
let articleTitles = $('#article-list').val().trim()
.split('\r').join('')
.split('\n');
$("#article-list").val("");
// remove duplicates
articleTitles = [...new Set(articleTitles)];
// (empty and re-)populate table
$("#results").empty();
for(let i = 0; i < articleTitles.length; i++){
let articleTitle = articleTitles[i];
const resultRow =
`<tr>
<td><a href="https://en.wikipedia.org/wiki/${encodeURIComponent(articleTitle)}">${articleTitle}</a></td>
<td id="cell-${i}">-</td>
</tr>`;
$('#results').append(resultRow);
}
$('#btStop').html(`Stop (${totalProcessedArticles}/${articleTitles.length})`);
await processArticlesInBatches(articleTitles);
$('#btStop').trigger('click');
});
$('#btStop').click(function(){
stopProcessing = true;
totalProcessedArticles = 0;
$('#btStart').prop("disabled", false);
$('#btStop').prop("disabled", true);
$('#btStop').html(`Stop`);
});
$('#btAddRandom').click(async function(){
const url = `https://en.wikipedia.org/w/api.php?action=query&list=random&rnnamespace=0&rnlimit=10&format=json&origin=*`;
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP error ${res.status}`);
const data = await res.json();
const titles = data.query.random.map(item => item.title);
let newList = ($('#article-list').val() + '\n' + titles.join('\n')).trim();
$('#article-list').val(newList);
});
$('#btAddCurrent').click(async function(){
let pagetitle = mw.config.get('wgTitle');
let newList = ($('#article-list').val() + '\n' + pagetitle).trim();
$('#article-list').val(newList);
});
$('#btAPI').click(function(){
let currentAPIKey = localStorage.getItem('SpellGrammarSuggestionsListAPIKey');
if(currentAPIKey === 'null' || currentAPIKey === null){
currentAPIKey = '';
}
let input = prompt('Please enter your OpenAI API key. It starts with "sk-...". It will be saved locally on your device. It will not be shared with anyone and will only be used for your queries to OpenAI. To delete your API key, leave this field empty and press [OK].', currentAPIKey);
// check that the cancel-button was not pressed
if(input !== null){
localStorage.setItem('SpellGrammarSuggestionsListAPIKey', input);
$('#btStart').prop('disabled', false);
}
});
$('#btClose').click(function(){
$('#btStop').trigger('click');
$('#scriptContainer').remove();
$('#content').show(); // show original Wikipedia page again
});
}
async function processArticle(articleTitle, index, articleTitleLength){
if(!stopProcessing){
let articleInfo = await getArticleInfo(articleTitle);
articleInfo.index = index;
if(articleInfo.status === "ok"){
await addCorrection(articleInfo);
}
console.log(articleInfo);
addPreviewButton(articleInfo);
totalProcessedArticles++;
$('#btStop').html(`Stop (${totalProcessedArticles}/${articleTitleLength})`);
}
}
async function processArticlesInBatches(articleTitles) {
for (let i = 0; i < articleTitles.length; i += totalConcurrentlyProcessedArticles) {
const batch = articleTitles.slice(i, i + totalConcurrentlyProcessedArticles);
const promises = [];
for (let j = 0; j < batch.length; j++) {
promises.push(processArticle(batch[j], i+j, articleTitles.length));
}
await Promise.all(promises);
}
}
async function getArticleInfo(articleTitle){
const api = new mw.Api();
const data = await api.get({
action: 'query',
prop: 'revisions',
titles: articleTitle,
rvprop: 'timestamp|content',
rvslots: 'main',
formatversion: 2
});
if(data.query.pages[0].missing){
return{title: articleTitle, status: "missing"};
}
let editTime = data.query.pages[0].revisions[0].timestamp.replace(/[-:TZ]/g, '');
let wikitext = data.query.pages[0].revisions[0].slots.main.content;
let parts = splitWikitext(wikitext);
let articleInfo = {title: articleTitle, editTime: editTime, relevantWikitext: parts[0], remainingWikitext: parts[1]};
if(articleInfo.relevantWikitext.length > maxWikitextLength){
articleInfo.status = "too long";
}
else{
articleInfo.status = "ok";
}
return articleInfo;
function splitWikitext(wikitext){
const splitStrings = ["\n\n==See also==", "\n==See also==", "\n\n== See also ==", "\n== See also ==", "\n\n==References==", "\n==References==", "\n\n== References ==", "\n== References ==", "\n\n==Notes==", "\n==Notes==", "\n\n== Notes ==", "\n== Notes =="];
for(let splitString of splitStrings){
const idx = wikitext.indexOf(splitString);
if(idx !== -1){
return [wikitext.slice(0, idx), wikitext.slice(idx)];
}
}
return [wikitext, ""];
}
}
async function addCorrection(articleInfo){
const systemPrompt =
`- Task: You are given the wikitext of the Wikipedia article "${articleInfo.title}". Correct only obvious, uncontroversial spelling and grammar errors (e.g., typos, misspellings, subject-verb agreement, articles, basic punctuation, capitalization).
- Do not change: content, meaning, tone, style, structure, section headings, templates, links, references, categories, HTML or wikitext markup, lists, tables, or formatting, except when a minimal change is required to fix a spelling/grammar error.
- Do not switch between national varieties of English. Preserve the variety used in the input (e.g., American vs British spelling)
- Preserve proper nouns and titles as written unless they contain clear typos. Do not correct errors in quoted text.
- Do not change the style of quotation marks and apostrophes.
- Do not change typographic HTML entities like non-breaking spaces (" ") and dashes ("–" or "—").
- Do not change wikilinks (which use double brackets, as in [[human]]) or headings (which use double equals signs, as in ==Definition==).
- Preserve whitespace and line breaks. Do not add or remove spaces in wikitext lists (as in "*list item" or "# list item")
- If a sentence is ungrammatical and ambiguous, make the minimal fix that yields a grammatical reading without changing meaning; if unsure, leave it as is.
- Output: Return only the corrected wikitext with no commentary. If there are no mistakes, return the original wikitext without any changes. Start your response with "${articleInfo.relevantWikitext.slice(0, 30)}..."`;
const messages = [
{role: "system", content: systemPrompt},
{role: "user", content: articleInfo.relevantWikitext},
];
const url = "https://api.openai.com/v1/chat/completions";
const body = JSON.stringify({
"messages": messages,
"model": "gpt-5",
reasoning_effort: "minimal",
});
const headers = {
"content-type": "application/json",
Authorization: "Bearer " + localStorage.getItem('SpellGrammarSuggestionsListAPIKey'),
};
const init = {
method: "POST",
body: body,
headers: headers
};
const response = await fetch(url, init);
console.log(response);
if(response.ok){
const json = await response.json();
let correctedWikitext = json.choices[0].message.content;
articleInfo.correctedWikitext = correctedWikitext;
}
else{
articleInfo.status = "API error";
}
}
function addPreviewButton(articleInfo){
const cell = $(`#cell-${articleInfo.index}`);
if(articleInfo.status === "missing"){
cell.html('Article not found.');
}
else if(articleInfo.status === "too long"){
cell.html('The Wikitext of the article is too long for this script, which is only intended for short and medium-sized articles. You can use the script <a href="https://en.wikipedia.org/wiki/User:Phlsph7/SpellGrammarSuggestions">SpellGrammarSuggestions</a> for individual articles of any length.');
}
else if(articleInfo.status === 'API error'){
cell.html('There was an error when contacting the LLM. If you keep getting this error, please check that the API key you provided is functional.');
}
else{
const startTime = new Date().toISOString().replace(/[-:TZ.]/g, '').slice(0, 14);
const formHtml =
`<form action="https://en.wikipedia.org/w/index.php?title=${encodeURIComponent(articleInfo.title)}&action=submit" method="post" target="_blank">
<textarea name="wpTextbox1" style="display:none"></textarea>
<input type="hidden" name="wpSummary" value="spelling/grammar corrections using [[User:Phlsph7/SpellGrammarSuggestionsList]]">
<input type="hidden" name="wpStarttime" value="${startTime}">
<input type="hidden" name="wpEdittime" value="${articleInfo.editTime}">
<input type="hidden" name="wpDiff" value="Show changes">
<input type="hidden" name="wpUltimateParam" value="1">
<button type="submit" class="btn-spellGrammarSuggestionsList" onclick="this.style.background='#aaa'">Preview suggested changes</button>
</form>`;
cell.html(formHtml);
cell.children().first().children().first().val(articleInfo.correctedWikitext + articleInfo.remainingWikitext);
}
}
})();