User:Phlsph7/SpellGrammarSuggestions.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 user script seems to have a documentation page at User:Phlsph7/SpellGrammarSuggestions. |
// Uses the OpenAI API to provide suggestions on spelling and grammar. The suggestions are in wikitext using the template "text diff".
(function(){
const scriptName = 'SpellGrammarSuggestions';
$.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();
start();
};
});
const originalSentenceTag = "**Original sentence:**";
const correctionTag = "**Correction:**";
const explanationTag = "**Explanation:**";
const noSuggestionsMessage = "**No errors found.**";
let hasError = false;
let modalTextarea;
function start(){
modalTextarea = openModalWithTextarea();
}
// get AI assessments for each text segment, transform them to wikitext, and display the result in the log area
async function getAssessments(){
const textSegments = getTextSegments();
const assessments = [];
for(let i = 0; i < textSegments.length; i++){
let message = `Processing text segment ${i+1}/${textSegments.length} ...`;
logTextarea(message);
assessment = await getAssessment(textSegments[i]) + '\n';
if(hasError){
break;
}
assessments.push(assessment);
}
if(hasError){
clearTextarea();
logTextarea(`There was an error. The most likely sources of the error are:
* You entered a false OpenAI API key.
* Your OpenAI account ran out of credit.
You can ask at the script talk page if you are unable to resolve the error.`);
}
else{
let fullAssessment = assessments.join('\n').split('\r').join('');
fullAssessment = fullAssessment.split(noSuggestionsMessage).join('');
fullAssessment = fullReplace(fullAssessment, '\n\n\n', '\n\n');
fullAssessment = fullReplace(fullAssessment, '\n ', '\n');
while(fullAssessment[0] === '\n'){
fullAssessment = fullAssessment.substring(1);
}
fullAssessment = fullAssessment.trim();
let wikiText = '';
if(fullAssessment.length < 20){
wikiText = 'The script did not detect any spelling or grammar errors.';
}
else{
wikiText = fullAssessmentToWikitext(fullAssessment);
}
clearTextarea();
logTextarea(wikiText);
}
}
// get an individual assessment for a single text segment
async function getAssessment(textSegment){
const systemPrompt =
`Check the provided encyclopedic text for spelling and grammar errors, and suggest corrections.
- Review the text thoroughly to identify any spelling mistakes or grammatical issues.
- Offer clear corrections for each identified error.
- Maintain the original meaning and encyclopedic style of the text.
- Only report sentences that contain objective errors. Do not make subjective style suggestions.
- Do not make changes to the English variant (American English, British English)
- Provide a concise explanation for each correction about why it is necessary.
# Output Format
The output should be a list of identified errors and their corrections in the following format:
${originalSentenceTag} "[original sentence with error]"
${correctionTag} "[corrected sentence]"
${explanationTag} "[explanation of the correction]"
${originalSentenceTag} ...
Do not list sentences that contain no errors. If the whole text contains no errors, respond only with the following standard message:
${noSuggestionsMessage}`;
const messages = [
{role: "system", content: systemPrompt},
{role: "user", content: textSegment},
];
console.log(messages);
const url = "https://api.openai.com/v1/chat/completions";
const body = JSON.stringify({
"messages": messages,
"model": "gpt-4o",
"temperature": 0,
});
const headers = {
"content-type": "application/json",
Authorization: "Bearer " + localStorage.getItem('SpellGrammarSuggestionsAPIKey'),
};
const init = {
method: "POST",
body: body,
headers: headers
};
let assessment;
const response = await fetch(url, init);
console.log(response);
if(response.ok){
const json = await response.json();
assessment = json.choices[0].message.content;
}
else{
hasError = true;
assessment = 'error';
}
console.log(assessment);
return assessment;
}
// transform the html paragraphs containing the article text into text segments
function getTextSegments(){
const elementContainer = $('#mw-content-text').find('.mw-parser-output').eq(0)[0].cloneNode(true);
// remove references
const refs = elementContainer.querySelectorAll('.reference, .Inline-Template');
for(let ref of refs){
ref.outerHTML = '';
}
// remove annotation (of math elements)
const anElements = elementContainer.querySelectorAll('annotation');
for(let anElement of anElements){
anElement.outerHTML = '';
}
// get p-elements
const children = Array.from(elementContainer.children);
const elementParas = children.filter(function(item){
return item.tagName.toLowerCase() === 'p';
});
// transform p-elements to text
const textParas = [];
for(let elementPara of elementParas){
let tempText = elementPara.innerText;
// adjustments like removing newlines caused by mathematical formulas
tempText = tempText.split('\r').join('')
.split('\n').join(' ')
.split('\t').join(' ');
tempText = fullReplace(tempText, ' ', ' ');
tempText = tempText.trim();
// ignore very short paragraphs
const minTextParaLength = 50;
if(tempText.length >= minTextParaLength){
textParas.push(tempText);
}
}
// combine textParas to form longer textSegments
const maxTextSegmentLength = 5000;
const textSegments = textParas.splice(0, 1);
let curSegmentIndex = 0;
while(textParas.length > 0){
let curPara = textParas.splice(0, 1)[0];
if(textSegments[curSegmentIndex].length + curPara.length < 5000){
textSegments[curSegmentIndex] += '\n\n' + curPara;
}
else{
textSegments.push(curPara);
curSegmentIndex++;
}
}
return textSegments;
}
// transform the full assessment to wikitext
function fullAssessmentToWikitext(fullAssessment){
let wikitext = '';
let individualAssessments = fullAssessment.split(originalSentenceTag);
for(let individualAssessment of individualAssessments){
if(hasExpectedFormat(individualAssessment)){
let originalSentence = individualAssessment.split(correctionTag)[0].trim();
originalSentence = removeQuotationMarks(originalSentence);
let remainingAssesment = individualAssessment.split(correctionTag)[1];
let correctedSentence = remainingAssesment.split(explanationTag)[0].trim();
correctedSentence = removeQuotationMarks(correctedSentence);
let explanationSentence = remainingAssesment.split(explanationTag)[1].trim();
wikitext += `{{text diff|${originalSentence}|${correctedSentence}}}\n:::Explanation: ${explanationSentence}\n\n\n\n`;
}
else if (individualAssessment.trim() > 50){
individualAssessment = originalSentenceTag + ' ' + individualAssessment.trim();
individualAssessment = individualAssessment.split(':**').join('')
.split('**').join('*');
individualAssessment += '\n*:[This suggestion is not displayed using the template "text diff" because the AI response did not have the expected format.]\n\n\n\n';
}
}
return wikitext.trim();
// LLM output format may be inconsistent so it has to be checked
function hasExpectedFormat(individualAssessment){
let correctionTagCount = individualAssessment.split(correctionTag).length - 1;
let explanationTagCount = individualAssessment.split(correctionTag).length - 1;
return correctionTagCount === 1 && explanationTagCount === 1;
}
function removeQuotationMarks(text){
if (text.startsWith('"')) {
text = text.substring(1);
}
if (text.endsWith('"')) {
text = text.substring(0, text.length - 1);
}
return text;
}
}
// create the modal overlay to display controls and output
function openModalWithTextarea() {
const overlay = document.createElement('div');
overlay.style.position = 'fixed';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
overlay.style.display = 'flex';
overlay.style.justifyContent = 'center';
overlay.style.alignItems = 'center';
overlay.style.zIndex = '1000';
const modal = document.createElement('div');
modal.style.backgroundColor = 'white';
modal.style.padding = '15px';
modal.style.borderRadius = '5px';
modal.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.1)';
modal.style.width = '80%';
modal.style.height = '70%';
modal.style.display = 'flex';
modal.style.flexDirection = 'column';
overlay.appendChild(modal);
const title = document.createElement('div');
title.innerHTML = "SpellGrammarSuggestions";
title.style.marginBottom = '15px';
modal.appendChild(title);
const textarea = document.createElement('textarea');
textarea.style.width = '100%';
textarea.style.height = '80%';
textarea.style.resize = 'none';
textarea.style.marginBottom = '15px';
textarea.style.borderRadius = '5px';
textarea.readOnly = true;
modal.appendChild(textarea);
const buttonContainer = document.createElement('div');
buttonContainer.style.display = 'flex';
buttonContainer.style.flexDirection = 'row';
modal.appendChild(buttonContainer);
const startButton = addButton("Start", function(){
clearTextarea();
let currentAPIKey = localStorage.getItem('SpellGrammarSuggestionsAPIKey');
if(currentAPIKey === 'null' || currentAPIKey === null || currentAPIKey === ''){
clearTextarea();
logTextarea('No OpenAI API key detected. This script requires an OpenAI API key. Use the button below to add one.');
}
else{
getAssessments();
startButton.disabled = true;
}
});
addButton("Copy", function(){
modalTextarea.select();
document.execCommand("copy");
});
addButton("Add/Remove API key", function(){
let currentAPIKey = localStorage.getItem('SpellGrammarSuggestionsAPIKey');
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('SpellGrammarSuggestionsAPIKey', input);
}
startButton.disabled = false;
});
addButton("Close", function(){
document.body.removeChild(overlay);
});
document.body.appendChild(overlay);
return textarea;
function addButton(textContent, clickFunction){
const button = document.createElement('button');
button.textContent = textContent;
button.style.padding = '5px';
button.style.margin = '5px';
button.style.flex = '1';
button.addEventListener('click', clickFunction);
buttonContainer.appendChild(button);
return button;
}
}
function logTextarea(text){
modalTextarea.value = text + '\n' + modalTextarea.value;
}
function clearTextarea(){
modalTextarea.value = '';
}
// get the title of the page
function getTitle(){
let innerText = document.getElementById('firstHeading').innerText;
if(innerText.substring(0, 8) === 'Editing '){
innerText = innerText.substring(8);
}
if(innerText.substring(0, 6) === 'Draft:'){
innerText = innerText.substring(6);
}
if(innerText.includes('User:')){
let parts = innerText.split('/');
parts.shift();
innerText = parts.join('/');
}
return innerText;
}
// replace the old string with the new string until no instance of the old string remains
function fullReplace(string, oldSubstring, newSubstring){
let newString = string;
while(newString.includes(oldSubstring)){
newString = newString.split(oldSubstring).join(newSubstring);
}
return newString;
}
})();