User:Phlsph7/SourceVerificationAIAssistant.js

Source: Wikipedia, the free encyclopedia.
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
(function(){
	// restrict script to mainspace, userspace, template, and draftspace
	const namespaceNumber = mw.config.get('wgNamespaceNumber');
	const allowedNamespaces = [0, 2, 10, 118];
	const scriptName = 'Source Verification AI Assistant';
	const apiKeyName = scriptName.split(' ').join('') + 'ApiKey';
	
	if (allowedNamespaces.indexOf(namespaceNumber) != -1) {
		$.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();
				openScript(getSelectedText());
			};
		});
	}
		
	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 openScript(selectedText){
		const tokenLimit = 16384;
		const tokenLimit4k = 4096;
		const charToTokenRatio = 3.5;
		const maxCharactersClaim = 500;
		const maxCharactersSource = Math.floor(getCharacterEstimate(tokenLimit) * 0.8);
		const temperature = 0.5;

		const content = document.getElementById('content');
		const contentContainer = content.parentElement;
		content.style.display = 'none';

		let scriptContainer = document.createElement('div');
		contentContainer.appendChild(scriptContainer);
		scriptContainer.outerHTML = `
		<div id="scriptContainer" style="display:flex; flex-direction: column; min-width: 800px">
			<style>
				textarea {
					resize: none;
					border-radius: 5px;
					padding: 5px;
				}
				button {
					margin: 5px;
				}
				span {
					margin-bottom: 10px;
				}
			</style>
			<h1 id="scriptHeading">${scriptName}</h1>
			<div  style="display:flex;">
				<div style="flex: 1; display:flex; flex-direction: column; margin: 5px;">
					<div style="display:flex; flex-direction: column;">
						<label for="taClaim">Claim:</label>
						<textarea id="taClaim" rows="4" placeholder="Enter the claim to be verified here. Short and straightforward claims work best."></textarea>
						<span id="claimCounter">max chars</span>
					</div>
					<div style="display:flex; flex-direction: column;">
						<label for="taSource">Reliable source:</label>
						<textarea id="taSource" rows="15" placeholder="Paste the text of the reliable source here. Try to only include the sections that are relevant."></textarea>
						<span id="sourceCounter">max chars</span>
					</div>
				</div>
				<div style="flex: 1; display:flex; flex-direction: column; margin: 5px;">
					<label for="taOutput">Suggestions:</label>
					<textarea id="taOutput" style="height: 100%;" placeholder="Sentences from the reliable source that are relevant to the claim will be displayed here. Do not blindly trust these suggestions and check for yourself that the sentences actually come from the reliable source and that they support the claim." disabled></textarea>
					<span style="visibility: hidden;">Placeholder</span>
				</div>
			</div>
			<div style="display:flex; flex-direction: column;">
				<div style="display:flex;">
					<button id="btCopy" title="Copies the prompt so it can be used with another AI model." style="flex: 1;">Copy prompt</button>
					<button id="btSuggest" title="Attempts to find sentences in the source that support the claim." style="flex: 1;">Suggest supporting sentences</button>
				</div>
				<div style="display:flex;">
					<button id="btRemove" title="Removes source text that goes beyond the character limit. Be careful since this may remove substantial parts of the source." style="flex: 1;">Remove excessive source text</button>
					<button id="btSetApiKey" title="Enter the OpenAI API key required for usage." style="flex: 1;">Set API key</button>
				</div>
				<div style="display:flex;">
					<button id="btClose" title="Closes the script and return to the previous Wikipedia article." style="flex: 1;">Close script</button>
				</div>
			</div>
		</div>
		`;
		
		const taClaim = document.getElementById('taClaim');
		taClaim.value = selectedText;
		taClaim.addEventListener('input', updateClaimLengthCounter);
		const taSource = document.getElementById('taSource');
		taSource.addEventListener('input', updateSourceLengthCounter);
		
		const btRemove = document.getElementById('btRemove');
		btRemove.addEventListener('click', removeExcessiveSourceText);
		const btSuggest = document.getElementById('btSuggest');
		btSuggest.addEventListener('click', getSuggestions);
		const btSetApiKey = document.getElementById('btSetApiKey');
		btSetApiKey.addEventListener('click', setApiKey);
		const btCopy = document.getElementById('btCopy');
		btCopy.addEventListener('click', copyPrompt);
		const btClose = document.getElementById('btClose');
		btClose.addEventListener('click', closeScript);
		
		updateClaimLengthCounter();
		updateSourceLengthCounter();
		
		document.getElementById('scriptHeading').scrollIntoView();
		if(taClaim.value.length < 1){
			taClaim.focus();
		}
		else{
			taSource.focus();
		}

		function updateClaimLengthCounter(){
			let span = document.getElementById('claimCounter');
			let currentCharacters = taClaim.value.length;
			let maxCharacters = maxCharactersClaim;
			updateLengthCounter(span, currentCharacters, maxCharacters);
		}

		function updateSourceLengthCounter(){
			let span = document.getElementById('sourceCounter');
			let currentCharacters = taSource.value.length;
			let maxCharacters = maxCharactersSource;
			updateLengthCounter(span, currentCharacters, maxCharacters);
		}

		function updateLengthCounter(span, currentCharacters, maxCharacters){
			span.innerText = `(${currentCharacters}/${maxCharacters} characters)`;
			if(currentCharacters > maxCharacters){
				span.style.color = '#f00';
			}
			else{
				span.style.color = '#ccc';
			}
		}

		function removeExcessiveSourceText(){
			taSource.value = taSource.value.substring(0, maxCharactersSource);
			updateSourceLengthCounter();
		}
		
		function setApiKey(){
			let currentAPIKey = localStorage.getItem(apiKeyName);
			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);
			if(input !== null){
				localStorage.setItem(apiKeyName, input);
			}
		}

		function copyPrompt(){
			let claimText = taClaim.value.trim();
			let sourceText = reformatText(taSource.value).trim();
			const promptText = getPromptText(claimText, sourceText);
			copyToClipboard(promptText);
			alert("The prompt was copied to the clipboard.");
			
			function copyToClipboard(text) {
				const textarea = document.createElement('textarea');
				textarea.value = text;
				document.body.appendChild(textarea);
				textarea.select();
				document.execCommand('copy');
				document.body.removeChild(textarea);
			}
		}

		function closeScript(){
			let scriptContainer = document.getElementById('scriptContainer');
			scriptContainer.parentElement.removeChild(scriptContainer);
			content.style.display = '';
		}
		
		function getSuggestions(){
			let claimText = taClaim.value.trim();
			let sourceText = reformatText(taSource.value).trim();
			let apiKey = localStorage.getItem(apiKeyName);
			if(claimText.length < 1){
				alert('Error: enter text in the field for the claim.');
			}
			else if(sourceText.length < 1){
				alert('Error: enter text in the field for the reliable source.');
			}
			else if(claimText.length > maxCharactersClaim){
				alert('Error: the text in the claim field is too long.');
			}
			else if(sourceText.length > maxCharactersSource){
				alert('Error: the text in the reliable source field is too long.');
			}
			else if(apiKey === null || apiKey.length < 1){
				alert('Error: use the button "Set API key" to enter a valid OpenAI API key.');
			}
			else{
				const promptText = getPromptText(claimText, sourceText);
				const promptTokens = getTokenEstimate(promptText);
				let model;
				let remainingTokenEstimate;
				if(promptTokens < Math.floor(tokenLimit4k * 0.8)){
					model = 'gpt-3.5-turbo';
					remainingTokenEstimate = tokenLimit4k - promptTokens - 50;
				}
				else{
					model = 'gpt-3.5-turbo-16k';
					remainingTokenEstimate = tokenLimit - promptTokens - 50;
				}
			
				getResponse(promptText, model, remainingTokenEstimate, apiKey, false);
			}
			
			function getResponse(promptText, model, remainingTokenEstimate, apiKey, isRetry){
				disableButtons();
				taOutput.value = '';
				taOutput.placeholder = '';
				
				console.log(`Getting response ... (Model: ${model}; PromptTokenEstimate: ${getTokenEstimate(promptText)}; RemainingTokenEstimate: ${remainingTokenEstimate})`);
				
				const url = "https://api.openai.com/v1/chat/completions";
				const body = JSON.stringify({
					"messages": [{"role":"user","content": promptText}],
					"model": model,
					"temperature": temperature,
					"max_tokens": remainingTokenEstimate,
				});
				const headers = {
					"content-type": "application/json",
					Authorization: "Bearer " + apiKey,
				};
				const init = {
					method: "POST",
					body: body,
					headers: headers
				};
				
				fetch(url, init).then(function(response){
					enableButtons();
					if(response.ok){
						response.json().then(function(json){
							const responseText = json.choices[0].message.content;
							const responseTextTokenEstimate = getTokenEstimate(responseText);
							console.log(`answer received (${responseTextTokenEstimate}/${remainingTokenEstimate} tokens) tokens`);
							const fixedResponseText = fixResponseText(responseText, claimText, sourceText);
							const warning = '(Please double-check the following information yourself and do not blindly trust it)\n';
							const outputMessage = warning + fixedResponseText;
							const taOutput = document.getElementById('taOutput');
							taOutput.value = outputMessage;
						});
					}
					else {
						let errorCode = response.status;
						if(errorCode == 400){
							
							// This error sometimes happens with foreign languages because they have more tokens per word. One retry with a reduced token number
							let adjustedRemainingTokens = Math.floor((remainingTokenEstimate - 50) / 2);
							if(!isRetry && adjustedRemainingTokens > 50){
								getResponse(promptText, model, adjustedRemainingTokens, apiKey, true);
							}
							else{
								alert(`Error: the error code is ${errorCode}. This indicates that the text of the reliable source was too long.`);
							}
						}
						else if(errorCode == 401){
							alert(`Error: the error code is ${errorCode}. This indicates that no API key was entered or that the entered API key is incorrect.`);
						}
						else if(errorCode == 429){
							alert(`Error: the error code is ${errorCode}. This indicates that you have sent requests too quickly or that you have reached your monthly limit.`);
						}
						else {
							alert(`Error: the error code is ${errorCode}. You can try to use google and search for "OpenAI api error ${errorCode}" to learn more about this error.`);
						}
						console.log(response);
					}
				});	
			}
		}
			
		function getPromptText(claimText, articleText){
			//let command = 'I have a claim and a reliable source. I want to find out whether the text in the reliable supports all parts of the claim. If it does then please cite all the sentences in the reliable that directly support the claim.';
			//let command = 'I have a claim and a reliable source. I want to find out whether the text in the reliable supports all parts of the claim. If it does then please cite all the sentences in the reliable that directly support the claim.';
			//let command = 'I have a claim and a reliable source. Cite the sentences in the reliable source that provide some support to the claim.';
			let command = 'I have a claim and a reliable source. Cite the sentences in the reliable source that support the claim.';
			let context = `\n\nClaim: """${claimText}"""\n\nReliable source: """${articleText}"""`;
			let promptText = command + context;
			
			return promptText;
		}
			
		function getTokenEstimate(text){
			return Math.floor(text.length / charToTokenRatio);
		}

		function getCharacterEstimate(tokenNumber){
			return Math.floor(tokenNumber * charToTokenRatio);
		}
		
		function disableButtons(){
			btRemove.disabled = true;
			btSuggest.disabled = true;
			btSetApiKey.disabled = true;
			btClose.disabled = true;
			btCopy.disabled = true;
		}
		
		function enableButtons(){
			btRemove.disabled = false;
			btSuggest.disabled = false;
			btSetApiKey.disabled = false;
			btClose.disabled = false;
			btCopy.disabled = false;
		}
		
		// remove arbitrary linebreaks for pdf text
		function reformatText(text){
			let paragraphEnd = "PARAGRAPH_END_PLACEHOLDER";
			let reformatedText = text.split('\r').join('')
				.split('\n\n').join(paragraphEnd)
				.split('\n').join(' ')
				.split('  ').join(' ')
				.split(paragraphEnd).join('\n\n');
			return reformatedText;
		}
		
		// fix cases where the response simply repeats the claimText
		function fixResponseText(responseText, claimText, sourceText){
			if(sourceText.includes(claimText)){
				return `The exact sentence "${claimText}" is found in the reliable source. Please ensure that this sentence is attributed to the author to avoid a copyright violation.`;
			}
			else if(responseText.includes(claimText)){
				const claimText1 = '"' + claimText + '"';
				const claimText2 = "'" + claimText + "'";
				const fixedResponseText = responseText.split(claimText1).join('')
					.split(claimText2).join('')
					.split(claimText).join('');
				console.log('responseText fixed');
				return fixedResponseText;
			}
			else{
				return responseText;
			}
		}
	}
})();