사용자:기나ㅏㄴ/WikiChatbot.js
< 사용자:기나ㅏㄴ
참고: 설정을 저장한 후에 바뀐 점을 확인하기 위해서는 브라우저의 캐시를 새로 고쳐야 합니다. 구글 크롬, 파이어폭스, 마이크로소프트 엣지, 사파리: ⇧ Shift 키를 누른 채 "새로 고침" 버튼을 클릭하십시오. 더 자세한 정보를 보려면 위키백과:캐시 무시하기 항목을 참고하십시오.
(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('안녕하세요! 저는 위키 챗봇이에요. 무엇을 도와드릴까요? <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": `당신의 이름은 위키백과 사용자들의 도우미인 '위키 챗봇'입니다. 사용자는 당신은 위키백과 문서 "${getTitle()}"에 대해 도와줘야합니다. 사용자는 작업할 텍스트를 선택할 것입니다.`},
{"role":"user","content": `저는 위키백과 문서를 검토하고 개선하는 데 도움이 필요합니다. 전후 맥락을 알 수 있도록 해당 문서의 리드 섹션에서 발췌한 내용을 알려드리겠습니다.
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';
}
}
})();