Scrapbox上からChatGPTに質問するUserScript_非Stream版_20230330
これはStream非設定時のUserScript
https://gyazo.com/6eb3018a269e21bceb1b41eabee0ffab
code:script.js
// カーソル位置に文章を挿入するUserScript
import { insertText } from "/api/code/gosyujin/scrapbox-insert-text/script.js"
// TampberMonkeyの関数呼び出し
function askConversations() {
return window.ask_chatgpt(
CONVERSATIONS,
MAX_TOKEN,
TEMPERATURE,
MODEL,
{role:'system', content:SYSTEM_PROMPT});
}
// システムの性格、なんか上手く効かない
const SYSTEM_PROMPT = `
You are helpful code-completioner.
`;
// 次回会話時に思い出してもらう用の定型文
const MATOMETE_PROMPT = `
Briefly summarize the conversation history so far. You can ignore the source code. Please Japanese.
1. first conversation summary
2. second conversation summary
3.
`;
const MAX_TOKEN = 1000;
const TEMPERATURE = 0.5;
const MODEL = 'gpt-3.5-turbo-0301';
//const MODEL = 'gpt-4'; //const MODEL = 'gpt-4-0314'; //const MODEL = 'gpt-4-32k'; //const MODEL = 'gpt-4-32k-0314';
// 約4000に到達する前
const TOTAL_TOKENS_AROUND_LIMIT = 2000;
// 会話履歴
const CONVERSATIONS = [];
// ページが切り替わったときに会話履歴CONVERSATIONSをクリアする
const clearConversations = () => {
CONVERSATIONS.length = 0;
};
scrapbox.on(page:changed, clearConversations);
scrapbox.on(layout:changed, clearConversations);
function getPrettyJson(json) {
return JSON.stringify(json, null, 2);
}
function getContent(str) {
const json = JSON.parse(str);
const icon = 'gosyujin_ChatGPT.icon';
let isCodeBegin = false;
const role = json.choices0.message.role; const content = json.choices0.message.content; console.log(getPrettyJson(json));
CONVERSATIONS.push({role:role, content:content});
const formattedContent = content.split('\n').map(m => {
// コードブロックらしきもの(`)がきたら、Scrapboxのコードブロック記法に置き換える
if (m.includes('`')) {
isCodeBegin = !isCodeBegin;
if (isCodeBegin) {
// コードブロック宣言の後ろに言語らしきものがあればそれをファイル名に使う、なければ_
const lang = m.split('`')1; if (lang === '') return code:_;
return code:${lang};
} else {
return ;
}
}
if (isCodeBegin) {
// コード中はインデントを一つ下げる
return ${m};
} else {
// コードブロック以外でかぎかっこが出てきたらバッククォートでくくる
// 配列はAI側でバッククォートで囲んで返却されたりされなかったり?
// わからないが、そのままreplaceすると「」になっちゃうので「」は無理やり「`」に戻している
return ${m.replace(/\[.*?\]/g, (m) => \${m}\`).replace(/\\`/g, (m) => '')}`;
}
}).filter(f => f !== ' '); // 空行扱いの行をfilterで除く
if (json.choices0.finish_reason === 'length') { formattedContent.push(' (1度に回答できる文字数を超えました。続きを促してください...)');
}
console.log(total_tokens: ${json.usage.total_tokens});
if (json.usage.total_tokens > TOTAL_TOKENS_AROUND_LIMIT) {
formattedContent.push(' (会話履歴の容量限界が近づいてきたので記憶を整理します...)');
// MATOMETE依頼、会話履歴クリア、MATOMETA要約push
CONVERSATIONS.push({role:'user', content:MATOMETE_PROMPT});
askConversations()
.then(p => {
clearConversations();
const json = JSON.parse(p.response);
const role = json.choices0.message.role; const content = json.choices0.message.content; CONVERSATIONS.push({role:role, content:content});
})
.catch(e => {
return \n\ncode:json\n catch error:\n${getPrettyJson(e)}\n\n;
})
.finally(f => {
console.log('記憶整理');
});
}
// 単行の場合はiconに文章を続ける、複数行の場合は一度改行を入れる
if (formattedContent.length === 1) {
return ${icon}:${formattedContent.join('\n')};
} else {
return ${icon}:\n${formattedContent.join('\n')};
}
}
scrapbox.PageMenu.addMenu({
title: 'ChatGPTに尋ねる',
onClick: () => {
if (!window.ask_chatgpt) {
console.error('TampermonkeyにAskChatGPTがインストールされていません');
return;
}
if (document.getElementById('text-input').value === '') {
console.warn('範囲が選択されていません')
return;
}
// 選択範囲の文字を取得
const select = document.getElementById('text-input').value;
// 選択した部分はカーソル位置にそのまま戻す
insertText({ text: ${select} });
// 応答が返ってくるまでカーソルを太くして点滅させる。終わったら戻すので事前のstyleを保持しておく
const cursor = document.getElementsByClassName('cursor')0; const top = cursor.style.top;
const left = cursor.style.left;
const height = cursor.style.height;
const width = cursor.style.width;
const defaultCssStyle = top: ${top};left: ${left};height: ${height};
cursor.style = ${defaultCssStyle};width: 12px!important;animation: blink 1s infinite;;
if (select === 'wq') {
CONVERSATIONS.push({role:'user', content:MATOMETE_PROMPT});
} else {
CONVERSATIONS.push({role:'user', content:select});
}
askConversations()
.then(p => {
cursor.style = ${defaultCssStyle};width: ${width};;
if (p.status === 200) {
insertText({ text: \n\n${getContent(p.response)}\n\n });
} else {
insertText({ text: \n\ncode:json\n status error ${p.status}:\n ${getPrettyJson(p.response)}\n\n });
}
})
.catch(e => {
cursor.style = ${defaultCssStyle};width: ${width};;
insertText({ text: \n\ncode:json\n catch error:\n${getPrettyJson(e)}\n\n });
})
.finally(f => {
console.log(${getPrettyJson(CONVERSATIONS)});
});
}
});
内容
選択した文字をOpenAI APIに送り、回答を文中に貼り付ける
ポップアップメニューじゃなくてページメニューに追加した方がよさそう
タイトルでも実行したい=タイトル行は選択してもポップアップでない
回答が返ってくるまで時間がかかるので、その間にカーソルを動かすと動かした先にペーストされてしまう
とりあえずカーソルを太くして点滅させるようにした
最初はstyle.cursor = 'progress'でマウスカーソルをぐるぐるさせるようにしていたが、マウスカーソルはテキストエリアの外しか効かないみたいだったので諦めた(テキストエリアにいるとカーソルが文字選択: Iみたいな感じになるので効かない?)
コードブロックの書式がバッククォート3つで囲むとcode:で違うので、判別できるようにしたい
無理やりやってみた
[]を検知したらバッククォートで囲みたい
無理やりやってみた
かっこが出てくる文脈は以下の2パターンが考えられる
1. [送信]ボタンを押しますのような装飾で使用されている場合は消したい
2. const array = [1, 2, 3];のように配列として使っている場合は消えると困る
さらにScrapboxではコードブロックの外でかっこで囲まれた文字はリンク扱いになるので、バッククォートで囲んで見た目に問題ないようにしたい