Skip to content

Instantly share code, notes, and snippets.

@lukestanley
Created October 18, 2023 12:04
Show Gist options
  • Save lukestanley/937ffd4de748cdc00a5dd701bd6fbdfb to your computer and use it in GitHub Desktop.
Save lukestanley/937ffd4de748cdc00a5dd701bd6fbdfb to your computer and use it in GitHub Desktop.
Browser user script using OpenAI GPT-3.5-turbo on text boxes to suggest British English spelling, grammar text improvments
// ==UserScript==
// @name Fix spelling with GPT
// @namespace https://gist.github.com/lukestanley/937ffd4de748cdc00a5dd701bd6fbdfb
// @version 1.0
// @description User script for checking spelling and grammar with OpenAI API
// @match http://*/*
// @match https://*/*
// @run-at document-end
// @grant GM_xmlhttpRequest
// ==/UserScript==
(function() {
'use strict';
const apiKey = "sk-INSERT_KEY_HERE";
const apiUrl = "https://api.openai.com/v1/chat/completions";
let focusedElement = null;
let lastFocusedElement = null;
let timer = null;
document.addEventListener('focus', function(event) {
if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {
focusedElement = event.target;
lastFocusedElement = event.target;
}
}, true);
document.addEventListener('blur', function() {
focusedElement = null;
}, true);
function throttle(func, delay) {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(func, delay);
}
async function checkText() {
if (focusedElement && focusedElement.type !== 'password') {
const text = focusedElement.value;
const words = text.split(' ');
if (text && text.length >= 3 && words.length <= 100 && text.length <= 300) {
throttle(() => checkSpellingAndGrammar(text), 1000);
}
}
}
function parseResponse(data, inputText) {
console.dir('Response parser', data);
const suggestions = [];
if (data.choices) {
for (const choice of data.choices) {
if (choice.message.function_call) {
const arg = JSON.parse(choice.message.function_call.arguments);
if (arg.suggestions) {
for (const suggestion of arg.suggestions) {
const correctedText = suggestion.text;
const explanation = suggestion.explanation;
if (correctedText !== inputText && !suggestions.some(item => item.text === correctedText)) {
suggestions.push({ text: correctedText, explanation });
}
}
}
}
}
}
return suggestions;
}
function clearSuggestions(suggestionsContainer) {
suggestionsContainer.innerHTML = '';
document.body.removeChild(suggestionsContainer);
}
async function checkSpellingAndGrammar(inputText) {
console.log('Spell corrector found text to check / fix:' + inputText);
const requestBody = {
model: "gpt-3.5-turbo",
temperature: 0.5,
n: 2,
messages: [
{
role: "system",
content: "You output improved text using British English! Do not use words with the letter 'z'!"
},
{
role: "user",
content: "1. We need highly clear text. The text should be simple, plain English. Critically consider ways the text could be more clear by reviewing it. 2. Propose multiple suggestions. Input text: `" + inputText + "`\nFor the review, let's think step by step to provide good suggestions."
}
],
functions: [
{
name: "text_eval",
parameters: {
"type": "object",
"properties": {
"review": {
"type": "string",
"description": "A step-by-step review of the original text, with detailed reasoning so that we understand it well."
},
"suggestions": {
"type": "array",
"description": "Suggest improvments using British English!",
"items": {
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "A suggested improvement for the original text."
},
"isBritish": {
"type": "boolean",
"description": "If the suggestion is US English but not British, this should be false. We only want UK spellings!"
},
"explanation": {
"type": "string",
"description": "Why this could be a good improvement upon the original text. TO BE SHOWN TO THE END-USER!"
}
},
"required": [
"text",
"explanation"
]
},
"minItems": 0,
"maxItems":4
}
},
"required": ["review", "suggestions"]
}
}
],
function_call: { name: "text_eval" }
};
GM_xmlhttpRequest({
method: "POST",
url: apiUrl,
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${apiKey}`
},
data: JSON.stringify(requestBody),
onload: function(response) {
if (response.status >= 200 && response.status < 400) {
try {
const responseData = JSON.parse(response.responseText);
const correctedTexts = parseResponse(responseData, inputText);
if (correctedTexts.length < 1) {
console.log('Spell checker has no suggestions.');
return
}
if (lastFocusedElement && lastFocusedElement.getBoundingClientRect()) {
console.log('Spell checker still has DOM element found');
} else {
console.log('Spell checker exiting because no focusedElement, no DOM element found.');
return;
};
const suggestionsContainer = document.createElement('ul');
suggestionsContainer.style.position = 'absolute';
suggestionsContainer.style.zIndex = '1000';
suggestionsContainer.style.left = `${focusedElement.getBoundingClientRect().left}px`;
suggestionsContainer.style.top = `${focusedElement.getBoundingClientRect().bottom}px`;
suggestionsContainer.style.background = '#fff';
suggestionsContainer.style.border = '1px solid #ccc';
suggestionsContainer.style.borderRadius = '4px';
suggestionsContainer.style.color = 'black';
suggestionsContainer.innerHTML = correctedTexts.map((textObj, index) => {
return `<li data-index="${index}" style="cursor:pointer">
"${textObj.text}"
<small style="font-size:0.5em;">
${textObj.explanation}
</small>
</li>`;
}).join('');
const listOfCorrections = correctedTexts.map(item => `${item.text} (Explanation: ${item.explanation})`).join(', ');
console.log('Spell checker suggestions and explanations: ' + listOfCorrections);
document.body.appendChild(suggestionsContainer);
lastFocusedElement.addEventListener('input', function() {
if (!lastFocusedElement.value.includes(inputText)) {
clearSuggestions(suggestionsContainer);
lastFocusedElement.removeEventListener('input', this);
}
});
suggestionsContainer.addEventListener('click', function(event) {
const selectedIndex = event.target.getAttribute('data-index');
const selectedCorrection = correctedTexts[selectedIndex];
console.log('to replace `' + inputText + '` the user selected: ' + selectedCorrection.text);
if (focusedElement) {
focusedElement.value = selectedCorrection.text;
} else if (lastFocusedElement && lastFocusedElement.value && lastFocusedElement.value === inputText) {
lastFocusedElement.value = selectedCorrection.text;
}
console.log('Explanation for the correction: ' + selectedCorrection.explanation);
// Clear the suggestions after selection
clearSuggestions(suggestionsContainer);
});
} catch (error) {
console.error('Spell corrector error:', error);
}
} else {
console.error(`Spell corrector HTTP error! Status: ${response.status}`);
}
},
onerror: function(error) {
console.error('Spell corrector request error:', error);
}
});
}
document.addEventListener('input', checkText);
console.log('waiting to fix spelling etc');
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment