User:DodoMan/chatbot.js

From Test Wiki
Jump to navigation Jump to search

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
(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 backgroundColorBot = '#dfd';
	const backgroundColorError = '#faa';
	const messages = getInitialMessages();

	// declare for later references
	const bodyContent = document.getElementById('bodyContent');
	let controlContainer;
	let reRotateControl;
	let chatContainer;
	let chatLog;
	let chatSend;
	let displayWarningMessage = false;
	
	// 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();
		logBotMessage('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 WikiChatbot, an 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('WikiChatbotActivated') === '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.padding = '10px';
		controlContainer.style.borderRadius = '10px';
		controlContainer.style.whiteSpace = 'nowrap';
		controlContainer.style.alignItems = 'center';
		controlContainer.style.zIndex = '999';
		controlContainer.style.resize = 'vertical';
		controlContainer.style.maxHeight = '80%';
		controlContainer.style.transform = 'rotateZ(180deg)';
		
		reRotateControl = document.createElement('div');
		controlContainer.appendChild(reRotateControl);
		reRotateControl.style.width = '100%';
		reRotateControl.style.height = '100%';
		reRotateControl.style.overflowY = 'auto';
		reRotateControl.style.transform = 'rotateZ(180deg)';
		reRotateControl.style.display = 'flex';
		reRotateControl.style.flexDirection = 'column';
		
		addButtons();
		
		let currentHeight = controlContainer.clientHeight;
		if(currentHeight > 400){
			controlContainer.style.height = currentHeight + 'px';
		}
		
		function addButtons(){
			addControlButton('Copyedit', 'Copyedit the selected text.', getQueryFunction(charLimit * 0.5, function(){
				return `Copyedit the selected text:

Selected text: """${getSelectedText()}"""`;
			}));

			addControlButton('Simplify', 'Simplify the selected text.', getQueryFunction(charLimit * 0.5, function(){
				return `Simplify the selected text:

Selected text: """${getSelectedText()}"""`;
			}));

			addControlButton('Reformulate', 'Reformulate the selected text.', getQueryFunction(charLimit * 0.5, function(){
				return `Reformulate the selected text: 

Selected 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 selected text: 

Selected 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 selected text: 

Selected text: """${getSelectedText()}"""`;
			}));

			addControlButton('Check spelling/grammar', 'Assess the spelling and grammar of the selected text.', getQueryFunction(charLimit * 0.5, function(){
				return `Does the selected text have problems with spelling or grammar?

Selected text: """${getSelectedText()}"""`;
			}));
			/*
			addControlButton('Is it true?', 'Assess whether the selected text is factually correct.', getQueryFunction(charLimit * 0.5, function(){
				displayWarningMessage = true;
				return `Is the selected text factually correct or does it contain false claims?

Selected text: """${getSelectedText()}"""`;
			}));

			addControlButton('Is it biased?', 'Assess whether the selected text is biased.', getQueryFunction(charLimit * 0.5, function(){
				displayWarningMessage = true;
				return `Does the selected text present a neutral point of view without editorial bias? 

Selected text: """${getSelectedText()}"""`;
			}));

			addControlButton('Is this source reliable?', 'Select one or several sources in the reference section to assess their reliability.', 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 selected text to me:

Selected text: """${getSelectedText()}"""`;
			}));

			addControlButton('Provide example', 'Provide an example to illustrate the main point of the selected text.', getQueryFunction(charLimit * 0.5, function(){
				return `Provide an example to illustrate the main point of the selected text:

Selected text: """${getSelectedText()}"""`;
			}));

			addControlButton('Suggest expansion', 'Suggest ideas how the selected text could be expanded.', getQueryFunction(charLimit * 0.5, function(){
				displayWarningMessage = true;
				return `Suggest ideas how the selected text could be expanded: 
				
Selected 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 selected text: 
				
Selected 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 selected text should have a wikilink to another Wikipedia article? 
				
Selected text: """${getSelectedText()}"""`;
			}));

			addControlButton('Suggest DYK questions', 'Suggest questions for the "Did you know" section on the Wikipedia main page based on the selected text.', getQueryFunction(charLimit * 0.5, function(){
				return `The project "Wikipedia:Did you know" presents question on specific articles on the main page. They all have the form "Did you know that...". Based on the selected text, suggest questions: 
				
Selected text: """${getSelectedText()}"""`;
			}));

			addControlButton('Ask quiz question', 'Asks the reader a quiz question about the selected text.', getQueryFunction(charLimit * 0.5, function(){
				return `Ask me a quiz question about the selected text: 
				
Selected text: """${getSelectedText()}"""`;
			}));

			addControlLine();

			addControlButton('Write new article outline', 'Writes a general outline of the topic of this article. Ignores the content of the article and the selected text.', async function(){ // jshint ignore:line
				let userMessageText = `Write a detailed outline for a 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('WikiChatbotAPIKey');
				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('WikiChatbotAPIKey', input);
				}
			}); 
		}
		
		function addControlButton(heading, tooltip, clickFunction){
			let button = document.createElement('button');
			reRotateControl.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');
			reRotateControl.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('WikiChatbotActivated') === '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/command 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();
		
		let approximateRemainingTokens = tokenLimit - Math.floor(getMessagesLength(messages) / 3.5) - 50;
		if(approximateRemainingTokens < 200){
			approximateRemainingTokens = 200;
		}
		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('WikiChatbotAPIKey'),
		};
		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);
					let logText = message.content;
					if(displayWarningMessage){
						displayWarningMessage = false;
						logText = "(Please consult reliable sources to verify the following information)\n" +  logText;
					}
					
					logBotMessage(logText);
				});
			}
			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 no API key was entered or that the entered API key is incorrect.'));
				}
				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 = reRotateControl.getElementsByTagName('button');
		for(let controlButton of controlButtons){
			controlButton.disabled = true;
		}
	}
	
	function enableButtons(){
		chatSend.disabled = false;
		let controlButtons = reRotateControl.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 logBotMessage(text){
		logMessage("Bot: " + text, backgroundColorBot, '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 WikiChatbot', 'portletlinkActivateId');
		portletlinkActivate.onclick = function(e) {
			e.preventDefault();
			activate();
		};
		
		// portlet link to deactivate
		const portletlinkDeactivate = mw.util.addPortletLink('p-tb', '#', 'Deactivate WikiChatbot', 'portletlinkDeactivateId');
		portletlinkDeactivate.onclick = function(e) {
			e.preventDefault();
			deactivate();
		};
		
		if(localStorage.getItem('WikiChatbotActivated') === null){
			localStorage.setItem('WikiChatbotActivated', 'false');
		}
		
		if(localStorage.getItem('WikiChatbotActivated') === 'true'){
			activate();
		}
		
		else{
			deactivate();
		}

		function activate(){
			localStorage.setItem('WikiChatbotActivated', 'true');
			mw.util.hidePortlet('portletlinkActivateId');
			mw.util.showPortlet('portletlinkDeactivateId');
			controlContainer.style.display = '';
			chatContainer.style.display = '';
			
		}

		function deactivate(){
			localStorage.setItem('WikiChatbotActivated', 'false');
			mw.util.hidePortlet('portletlinkDeactivateId');
			mw.util.showPortlet('portletlinkActivateId');
			controlContainer.style.display = 'none';
			chatContainer.style.display = 'none';
		}
	}
})();