사용자:Respice post te/test.js

위키백과, 우리 모두의 백과사전.

참고: 설정을 저장한 후에 바뀐 점을 확인하기 위해서는 브라우저의 캐시를 새로 고쳐야 합니다. 구글 크롬, 파이어폭스, 마이크로소프트 엣지, 사파리: ⇧ Shift 키를 누른 채 "새로 고침" 버튼을 클릭하십시오. 더 자세한 정보를 보려면 위키백과:캐시 무시하기 항목을 참고하십시오.

(function(){
	// define constants
	const tokenLimit = 2000;
	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('안녕하세요! 저는 위키 챗봇이에요. 무엇을 도와드릴까요? <br>(제 답변을 검토하신 후 문서를 편집해주세요. 저에 대해 더 알고 싶으시다면 <a href="https://ko.wikipedia.org/wiki/위키백과:Large_language_models">사용설명서</a>를 참고해주세요.)');
		
		// 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('교열', '교열하기', getQueryFunction(charLimit * 0.5, function(){
				return `다음 텍스트를 교열해주세요:

선택한 텍스트: """${getSelectedText()}"""`;
			}));

			addControlButton('단순화', '단순화하기', getQueryFunction(charLimit * 0.5, function(){
				return `다음 텍스트를 단순화해주세요:

선택한 텍스트: """${getSelectedText()}"""`;
			}));

			addControlButton('재구성', '재구성하기', getQueryFunction(charLimit * 0.5, function(){
				return `다음 텍스트를 재구성해주세요: 

선택한 텍스트: """${getSelectedText()}"""`;
			}));

			addControlButton('요약', '요약하기', getQueryFunction(charLimit * 0.5, function(){
				return `선택한 텍스트를 요약하여 길이를 줄여주세요.: 

선택한 텍스트: """${getSelectedText()}"""`;
			}));

			addControlButton('짧은 요약', '짧게 요약하기', getQueryFunction(charLimit * 0.5, function(){
				return `선택한 텍스트를 아주 짧게 요약하여 텍스트의 길이를 크게 줄여주세요.: 

선택한 텍스트: """${getSelectedText()}"""`;
			}));

			addControlButton('오탈자/문법', '오탈자와 문법 점검하기.', getQueryFunction(charLimit * 0.5, function(){
				return `선택한 텍스트에 오탈자나 문법 오류가 있나요?

선택한 텍스트: """${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('설명', '설명하기.', getQueryFunction(charLimit * 0.5, function(){
				return `선택한 텍스트에 대해 설명해줘:

선택한 텍스트: """${getSelectedText()}"""`;
			}));

			addControlButton('예시', '예시 제공하기.', getQueryFunction(charLimit * 0.5, function(){
				return `선택한 텍스트의 요점을 설명할 수 있는 예시를 알려주세요.:

선택한 텍스트: """${getSelectedText()}"""`;
			}));

			addControlButton('확장 제안', '확장 제안하기.', getQueryFunction(charLimit * 0.5, function(){
				displayWarningMessage = true;
				return `선택한 텍스트를 어떻게 확장할 수 있을지지 아이디어를 제안하세요.: 
				
선택한 텍스트: """${getSelectedText()}"""`;
			}));

			addControlButton('그림 제안', '그림 제안하기', getQueryFunction(charLimit * 0.5, function(){
				return `선택한 텍스트를 설명하는 데 사용할 수 있는 몇 가지 이미지를 설명해주세요.: 
				
선택한 텍스트: """${getSelectedText()}"""`;
			}));

			addControlButton('위키 링크 제안', '선택한 텍스트에서 다른 문서로의 위키 링크 제안하기.', getQueryFunction(charLimit * 0.5, function(){
				return `선택한 텍스트의 어떤 용어에 다른 위키백과 문서로 연결되는 위키링크가 있어야 하나요? 
				
선택한 텍스트: """${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: 
				
선택한 텍스트: """${getSelectedText()}"""`;
			}));
			*/
			addControlButton('퀴즈', '독자에게 선택한 텍스트에 대해 물어보기', getQueryFunction(charLimit * 0.5, function(){
				return `선택한 텍스트에 대한 퀴즈 질문하기: 
				
선택한 텍스트: """${getSelectedText()}"""`;
			}));

			addControlLine();

			addControlButton('새 문서 개요 작성', '이 문서의 주제에 대한 개요를 작성합니다. 문서의 콘텐츠와 선택한 텍스트를 무시합니다.', async function(){ // jshint ignore:line
				let userMessageText = `"${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('API key', 'OpenAI API key 입력하기', function(){
				let currentAPIKey = localStorage.getItem('WikiChatbotAPIKey');
				if(currentAPIKey === 'null' || currentAPIKey === null){
					currentAPIKey = '';
				}
				
				let input = prompt('OpenAI API key를 입력해주세요. "sk-..."로 시작합니다. 이것은 귀하의 기기에 로컬로 저장됩니다. 이 정보는 누구와도 공유되지 않으며 OpenAI에 대한 쿼리에만 사용됩니다. API 키를 삭제하려면 이 필드를 비워두고 [확인]을 누릅니다.', 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("선택한 텍스트가 없습니다. 먼저 마우스를 사용하여 텍스트를 드래그하여 선택해 주세요.");
				}
				else if(selectedText.length > selectedTextLimit){
					logErrorMessage(`선택된 텍스트가 너무 깁니다. ${selectedText.length} 문자가 선택되었지만, 텍스트 제한은 ${selectedTextLimit} 문자입니다.`);
				}
				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 = '질문이나 명령어를 입력하세요...';
		chatTextarea.title = '텍스트가 한번 선택된 경우에는 해당 텍스트를 계속 사용할 수 있습니다.';
		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 = '전송';
		chatSend.style.height = '100%';
		chatSend.style.borderRadius = '5px';
		chatSend.style.border = '1px solid black';
		chatSend.title = '보내기';

		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(`텍스트가 너무 깁니다. ${promptLength} 문자가 선택되었지만, 텍스트 제한은 ${promptLimit} 문자입니다.`);
			}
			else if(selectedLength > selectedLimit){
				logErrorMessage(`텍스트가 너무 깁니다. ${selectedText.length} 문자가 선택되었지만, 텍스트 제한은 ${selectedTextLimit} 문자입니다.`);
			}
			else {
				chatTextarea.value = '';
				if(selectedText.length > 0){
					promptText += '\n\n(사용자가 다음 텍스트를 선택했습니다. 관련성이 있는 경우 응답에 반영해 주세요.)\n\n선택한 텍스트트:"""' + 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 = "(다음 정보를 확인하려면 신뢰할 수 있는 출처를 참조하세요.)\n" +  logText;
					}
					
					logBotMessage(logText);
				});
			}
			else {
				if(response.status == 400){
					logErrorMessage(composeErrorMessage(400, '너무 많은 텍스트를 선택하거나 매우 긴 요청을 작성하면 이 오류가 발생할 수 있습니다.'));
				}
				else if(response.status == 401){
					logErrorMessage(composeErrorMessage(401, 'API key를 입력하지 않았거나 입력한 API key가 올바르지 않습니다.'));
				}
				else if(response.status == 429){
					logErrorMessage(composeErrorMessage(429, '요청을 너무 빨리 보냈거나 월별 한도에 도달했습니다.'));
				}
				else {
					logErrorMessage(response.status, `구글에 "OpenAI api error ${response.status}"을 검색하면 이 에러의 원인을 알 수 있습니다.`);
				}
			}
		});
		
		function composeErrorMessage(errorCode, additionalMessage){
			return `에러 코드는 ${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("위키 챗봇: " + text, backgroundColorBot, '0.1em', '1em');
	}

	function logUserMessage(text){
		logMessage("사용자: " + text, backgroundColorUser, '1em', '0.1em');
	}

	function logErrorMessage(text){
		logMessage("에러: " + 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', '#', '위키 챗봇 활성화하기', 'portletlinkActivateId');
		portletlinkActivate.onclick = function(e) {
			e.preventDefault();
			activate();
		};
		
		// portlet link to deactivate
		const portletlinkDeactivate = mw.util.addPortletLink('p-tb', '#', '위키 챗봇 비활성화하기', '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';
		}
	}
})();