User:Phlsph7/WikiChatbot.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/WikiChatbot. |
(function(){
// define constants
const tokenLimit = 4096;
const temperature = 0.5;
const model = 'gpt-3.5-turbo';
const charLimit = tokenLimit * 5; // rough estimate
const articleContextLimit = charLimit * 0.1;
const historyLimit = charLimit * 0.2;
const selectionLimit = charLimit * 0.25;
const promptLimit = charLimit * 0.25;
const backgroundColor = '#def';
const backgroundColorUser = '#ddd';
const backgroundColorAI = '#dfd';
const backgroundColorError = '#faa';
const messages = getInitialMessages();
// declare for later references
const bodyContent = document.getElementById('bodyContent');
let controlContainer;
let chatContainer;
let chatLog;
let chatSend;
// restrict script to mainspace, userspace, wikipedia, help, and draftspace
const namespaceNumber = mw.config.get('wgNamespaceNumber');
const allowedNamespaces = [0, 2, 4, 12, 118];
if (allowedNamespaces.indexOf(namespaceNumber) != -1) {
createControlUI();
createChatUI();
logAIMessage('How can I assist you? <br>(Please scrutinize all my responses before making changes to the article. See <a href="https://en.wikipedia.org/wiki/Wikipedia:Large_language_models">WP:LLM</a> for more information.)');
// add a link to the toolbox
$.when(mw.loader.using('mediawiki.util'), $.ready).then(addPortletAndActivate);
}
function getInitialMessages(){
return [
{"role":"system", "content": `You are a Wikipedia AI assistant. You help users with the Wikipedia article "${getTitle()}". User can select the text they wish to work on.`},
{"role":"user","content": `I need help in reviewing and improving a Wikipedia article. So you know the context, I'll give an excerpt from the lead section of the article.
Context:"""${getArticleIntroduction()}"""
`},
{"role":"assistant","content": "Thank you, I will use this information as context. How can I help you?"}
];
}
function createControlUI(){
controlContainer = document.createElement('div');
if(localStorage.getItem('AIAssistantActivated') === 'true'){
controlContainer.style.display = 'flex';
}
else {
controlContainer.style.display = 'none';
}
bodyContent.appendChild(controlContainer);
controlContainer.style.position = 'fixed';
controlContainer.style.right = '10px';
controlContainer.style.bottom = '10px';
controlContainer.style.backgroundColor = backgroundColor;
controlContainer.style.overflowY = 'auto';
controlContainer.style.flexDirection = 'column';
controlContainer.style.padding = '10px';
controlContainer.style.borderRadius = '10px';
controlContainer.style.whiteSpace = 'nowrap';
controlContainer.style.alignItems = 'center';
controlContainer.style.zIndex = '999';
addButtons();
function addButtons(){
addControlButton('Copyedit', 'Copyedit the selected text', getQueryFunction(charLimit * 0.5, function(){
return `Copyedit the following text:
text: """${getSelectedText()}"""`;
}));
addControlButton('Reformulate', 'Reformulate the selected text', getQueryFunction(charLimit * 0.5, function(){
return `Reformulate the following text:
text: """${getSelectedText()}"""`;
}));
addControlButton('Simplify', 'Simplify the selected text', getQueryFunction(charLimit * 0.5, function(){
return `Simplify the following text:
text: """${getSelectedText()}"""`;
}));
addControlButton('Regular summary', 'Provide a regular summary of the selected text', getQueryFunction(charLimit * 0.5, function(){
return `Provide a summary to reduce the length of the following text:
text: """${getSelectedText()}"""`;
}));
addControlButton('Short summary', 'Provide a short summary of the selected text', getQueryFunction(charLimit * 0.5, function(){
return `Provide a very short summary to greatly reduce the length of the following text:
text: """${getSelectedText()}"""`;
}));
addControlButton('Check spelling/grammar', 'Assess the spelling and grammar of the selected text', getQueryFunction(charLimit * 0.5, function(){
return `Does the following text have problems with spelling or grammar?
text: """${getSelectedText()}"""`;
}));
addControlButton('Is it true?', 'Assess whether the selected text is factually correct', getQueryFunction(charLimit * 0.5, function(){
return `Is the following text factually correct or does it contain false claims?
text: """${getSelectedText()}"""`;
}));
addControlButton('Is it biased?', 'Assess whether the selected text is biased', getQueryFunction(charLimit * 0.5, function(){
return `Does the following text present a neutral point of view without editorial bias?
text: """${getSelectedText()}"""`;
}));
addControlButton('Is this source reliable?', 'Select one or several sources in the reference section to assess their reliablity', getQueryFunction(charLimit * 0.5, function(){
return `Wikipedia has strict guidelines on what sources are generally considered to be reliable. Please give a rough estimation: which of the following sources could be unreliable?
sources: """${getSelectedText()}"""`;
}));
addControlButton('Explain', 'Explain the selected text', getQueryFunction(charLimit * 0.5, function(){
return `Please explain the following text to me:
text: """${getSelectedText()}"""`;
}));
addControlButton('Suggest expansion', 'Suggest ideas how the selected text could be expanded', getQueryFunction(charLimit * 0.5, function(){
return `Suggest ideas how the following text could be expanded:
text: """${getSelectedText()}"""`;
}));
addControlButton('Suggest images', 'Suggest which images could be used to illustrate the selected text', getQueryFunction(charLimit * 0.5, function(){
return `Describe some images that could be used to illustrate the following text:
text: """${getSelectedText()}"""`;
}));
addControlButton('Suggest wikilinks', 'Suggest terms in the selected text that could get a wikilink to another article', getQueryFunction(charLimit * 0.5, function(){
return `Which terms in the following text should have a wikilink to another Wikipedia article?
text: """${getSelectedText()}"""`;
}));
addControlLine();
addControlButton('Write new article', 'Writes a new article on the topic of this article. Ignores the contents of the article and the selected text. Beware of hallucinations!', async function(){ // jshint ignore:line
let userMessageText = `Write a detailed Wikipedia article on the topic "${getTitle()}".`;
let customMessages = [{"role":"user","content":userMessageText}];
logUserMessage(userMessageText);
await getResponse(customMessages).then(function(){
setTimeout(function(){
if(customMessages.length > 1){
messages.push(customMessages[0]);
messages.push(customMessages[1]);
}
}, 100);
});
});
addControlLine();
addControlButton('Set API key', 'Enter the OpenAI API key required for usage', function(){
let currentAPIKey = localStorage.getItem('AIAssistantAPIKey');
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('AIAssistantAPIKey', input);
}
});
}
function addControlButton(heading, tooltip, clickFunction){
let button = document.createElement('button');
controlContainer.appendChild(button);
button.innerHTML = heading;
button.title = tooltip;
button.style.width = '100%';
button.style.marginTop = '5px';
button.style.marginBottom = '5px';
button.style.borderRadius = '5px';
button.style.border = '1px solid black';
button.style.textAlign = 'left';
button.onclick = clickFunction;
}
function addControlLine(){
const borderLine = document.createElement('div');
controlContainer.appendChild(borderLine);
borderLine.style.width = '100%';
borderLine.style.marginTop = '5px';
borderLine.style.marginBottom = '5px';
borderLine.style.borderBottom = '1px solid grey';
}
function getQueryFunction(selectedTextLimit, promptFunction){
return function(){
let selectedText = getSelectedText();
if(selectedText.length < 1){
logErrorMessage("No text was selected. Please use the mouse to select a text first.");
}
else if(selectedText.length > selectedTextLimit){
logErrorMessage(`The selected text was too long: ${selectedText.length} characters were selected but the limit is ${selectedTextLimit} characters.`);
}
else{
const promptText = promptFunction();
clearHistory(messages);
messages.push(createUserMessage(promptText));
logUserMessage(promptText);
getResponse(messages);
}
};
}
}
function createChatUI(){
chatContainer = document.createElement('div');
if(localStorage.getItem('AIAssistantActivated') === 'true'){
chatContainer.style.display = '';
}
else {
chatContainer.style.display = 'none';
}
bodyContent.appendChild(chatContainer);
chatContainer.style.position = 'fixed';
chatContainer.style.bottom = '10px';
chatContainer.style.left = '10px';
chatContainer.style.width = '50%';
chatContainer.style.height = '40%';
chatContainer.style.backgroundColor = backgroundColor;
chatContainer.style.resize = 'both';
chatContainer.style.overflow = 'auto';
chatContainer.style.transform = 'rotateX(180deg)';
chatContainer.style.padding = '5px';
chatContainer.style.borderRadius = '10px';
chatContainer.style.zIndex = '999';
const reRotateChat = document.createElement('div');
chatContainer.appendChild(reRotateChat);
reRotateChat.style.width = '100%';
reRotateChat.style.height = '100%';
reRotateChat.style.overflow = 'auto';
reRotateChat.style.transform = 'rotateX(180deg)';
reRotateChat.style.display = 'flex';
reRotateChat.style.flexDirection = 'column';
chatLog = document.createElement('div');
reRotateChat.appendChild(chatLog);
chatLog.style.width = '100%';
chatLog.style.overflow = 'auto';
chatLog.style.flex = 1;
chatLog.style.marginBottom = '5px';
const chatResponse = document.createElement('div');
reRotateChat.appendChild(chatResponse);
chatResponse.style.width = '100%';
chatResponse.style.height = '45px';
chatResponse.style.display = 'flex';
const chatTextarea = document.createElement('textarea');
chatResponse.appendChild(chatTextarea);
chatTextarea.style.flexGrow = '1';
chatTextarea.style.backgroundColor = backgroundColorUser;
chatTextarea.style.resize = 'none';
chatTextarea.style.marginRight = '10px';
chatTextarea.style.borderRadius = '5px';
chatTextarea.style.padding = '5px';
chatTextarea.placeholder = 'Enter your question/comand here...';
chatTextarea.title = 'If text was selected, you can refer to it as "the selected text" in your questions/commands';
chatTextarea.onkeydown = function(event){
if (event.key === 'Enter' && !event.shiftKey){
event.preventDefault();
chatSend.click();
}
};
// store selected text before focus is lost.
let storedSelection = '';
chatTextarea.onmousedown = function(){
storedSelection = getSelectedText();
console.log(storedSelection);
};
chatSend = document.createElement('button');
chatResponse.appendChild(chatSend);
chatSend.innerHTML = 'Send';
chatSend.style.height = '100%';
chatSend.style.borderRadius = '5px';
chatSend.style.border = '1px solid black';
chatSend.title = 'Send your command/question';
chatSend.onclick = function(){
let promptText = chatTextarea.value;
let promptLength = promptText.length;
let promptLimit = charLimit * 0.25;
let selectedText = storedSelection;
storedSelection = '';
let selectedLength = storedSelection.length;
let selectedLimit = charLimit * 0.25;
if(promptLength > promptLimit){
logErrorMessage(`The prompt text was too long: ${promptLength} characters were entered but the limit is ${promptLimit} characters.`);
}
else if(selectedLength > selectedLimit){
logErrorMessage(`The selected text was too long: ${selectedText.length} characters were selected but the limit is ${selectedTextLimit} characters.`);
}
else {
chatTextarea.value = '';
if(selectedText.length > 0){
promptText += '\n\n(The user selected the following text. Please consider it in your response if it is relevant.)\n\nSelected text:"""' + selectedText + '"""';
}
imposeHistoryLimit(messages);
messages.push(createUserMessage(promptText));
console.log(messages);
logUserMessage(promptText);
getResponse(messages);
}
};
}
async function getResponse(messages){ // jshint ignore:line
disableButtons();
const approximateRemainingTokens = tokenLimit - Math.floor(getMessagesLength(messages) / 3);
const url = "https://api.openai.com/v1/chat/completions";
const body = JSON.stringify({
"messages": messages,
"model": model,
"temperature": temperature,
"max_tokens": approximateRemainingTokens,
});
const headers = {
"content-type": "application/json",
Authorization: "Bearer " + localStorage.getItem('AIAssistantAPIKey'),
};
const init = {
method: "POST",
body: body,
headers: headers
};
console.log(messages);
await fetch(url, init).then(function(response){
enableButtons();
if(response.ok){
response.json().then(function(json){
const message = json.choices[0].message;
messages.push(message);
console.log(messages);
logAIMessage(message.content);
});
}
else {
if(response.status == 400){
logErrorMessage(composeErrorMessage(400, 'Selecting too much text or writing a very long request can cause this error.'));
}
else if(response.status == 401){
logErrorMessage(composeErrorMessage(401, 'This indicates that an incorrect API key was used.'));
}
else if(response.status == 429){
logErrorMessage(composeErrorMessage(429, 'This indicates that you have sent requests too quickly or that you have reached your monthly limit.'));
}
else {
logErrorMessage(response.status, `You can try to use google and search for "OpenAI api error ${response.status}" to learn more about this error.`);
}
}
});
function composeErrorMessage(errorCode, additionalMessage){
return `The error code is ${errorCode}. ${additionalMessage}`;
}
}
function disableButtons(){
chatSend.disabled = true;
let controlButtons = controlContainer.getElementsByTagName('button');
for(let controlButton of controlButtons){
controlButton.disabled = true;
}
}
function enableButtons(){
chatSend.disabled = false;
let controlButtons = controlContainer.getElementsByTagName('button');
for(let controlButton of controlButtons){
controlButton.disabled = false;
}
}
function getArticleIntroduction(){
let paragraphs = document.querySelectorAll('.mw-parser-output > p');
let innerText = '';
hideRefs();
for(let paragraph of paragraphs){
innerText += paragraph.innerText;
if(innerText.length > articleContextLimit){
break;
}
}
showRefs();
articleIntroduction = innerText.substring(0, articleContextLimit);
return articleIntroduction;
}
function getSelectedText(){
hideRefs();
let selectedText = window.getSelection().toString();
showRefs();
return selectedText;
}
function hideRefs(){
let refs = document.body.querySelectorAll('.reference, .Inline-Template');
for(let ref of refs){
ref.style.display = 'none';
}
}
function showRefs(){
let refs = document.body.querySelectorAll('.reference, .Inline-Template');
for(let ref of refs){
ref.style.display = '';
}
}
function createUserMessage(promptText){
return {"role":"user","content": promptText};
}
function imposeHistoryLimit(messages){
while(getMessagesLength(messages) > historyLimit){
if(messages.length <= 3){
break;
}
messages.splice(3, 1);
}
}
function clearHistory(messages){
while(messages.length > 3){
messages.pop();
}
}
function getMessagesLength(messages){
let totalLength = 0;
for(let message of messages){
totalLength += message.content.length;
}
return totalLength;
}
function logAIMessage(text){
logMessage("AI: " + text, backgroundColorAI, '0.1em', '1em');
}
function logUserMessage(text){
logMessage("User: " + text, backgroundColorUser, '1em', '0.1em');
}
function logErrorMessage(text){
logMessage("Error: " + text, backgroundColorError, '0.1em', '0.1em');
}
function logMessage(text, backgroundColor, marginLeft, marginRight){
let pre = document.createElement('pre');
pre.innerHTML = text;
pre.style.backgroundColor = backgroundColor;
pre.style.margin = '0.2em';
pre.style.padding = '0.2em';
pre.style.marginRight = marginRight;
pre.style.marginLeft = marginLeft;
pre.style.borderRadius = '5px';
pre.style.fontFamily = 'sans-serif';
chatLog.appendChild(pre);
pre.scrollIntoView();
}
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;
}
function addPortletAndActivate(){
// portlet link to activate
const portletlinkActivate = mw.util.addPortletLink('p-tb', '#', 'Activate AI Assistant', 'portletlinkActivateId');
portletlinkActivate.onclick = function(e) {
e.preventDefault();
activate();
};
// portlet link to deactivate
const portletlinkDeactivate = mw.util.addPortletLink('p-tb', '#', 'Deactivate AI Assistant', 'portletlinkDeactivateId');
portletlinkDeactivate.onclick = function(e) {
e.preventDefault();
deactivate();
};
if(localStorage.getItem('AIAssistantActivated') === null){
localStorage.setItem('AIAssistantActivated', 'false');
}
if(localStorage.getItem('AIAssistantActivated') === 'true'){
activate();
}
else{
deactivate();
}
function activate(){
localStorage.setItem('AIAssistantActivated', 'true');
mw.util.hidePortlet('portletlinkActivateId');
mw.util.showPortlet('portletlinkDeactivateId');
controlContainer.style.display = 'flex';
chatContainer.style.display = '';
}
function deactivate(){
localStorage.setItem('AIAssistantActivated', 'false');
mw.util.hidePortlet('portletlinkDeactivateId');
mw.util.showPortlet('portletlinkActivateId');
controlContainer.style.display = 'none';
chatContainer.style.display = 'none';
}
}
})();