Scrapbox上からChatGPTに質問するUserScript
https://gyazo.com/0378e411ce593b421343dcfc5934f1b1
なにがやりたいのか
1. 「Scrapboxの編集画面」からChatGPTへ質問し、回答をもらいたい
「ChatGPT画面でやりとりをして結果をScrapboxに貼る」はやりたいことと違う
2. 回答は概ねMarkdown的な文章で返ってくるためそれをScrapbox記法に置き換えたい
コードブロック
バッククォート3つで受け取るのでScrapboxのコードブロック記法に無理やり変換したい フロー
code:mmd
sequenceDiagram
autonumber
box Browser
participant UserScript(Scrapbox)
participant AskChatGPT(TamperMonkey)
end
UserScript(Scrapbox) ->> AskChatGPT(TamperMonkey): 選択した文字列(質問)などを送る
AskChatGPT(TamperMonkey) ->> OpenAI ChatGPT API: request
OpenAI ChatGPT API -->> AskChatGPT(TamperMonkey): response
AskChatGPT(TamperMonkey) -->> UserScript(Scrapbox): 回答を受け取る
UserScript(Scrapbox) ->> UserScript(Scrapbox): ページに回答を書き込み
リンク
code:script.js
// カーソル位置に文章を挿入するUserScript
import { insertText } from "/api/code/gosyujin/scrapbox-insert-text/script.js"
// Chat APIの呼び出し
function askConversations() {
return window.ask_chatgpt(
CONVERSATIONS, MAX_TOKEN, TEMPERATURE,
MODEL, INIT_PROMPT, IS_STREAM);
}
// DALL-E APIの呼び出し
function generateImage(select) {
return window.ask_dalle(select);
}
// システムの性格、なんか上手く効かない
const SYSTEM_PROMPT = ;
const INIT_PROMPT = {role:'system', content:SYSTEM_PROMPT};
// 次回会話時に思い出してもらう用の定型文
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 IS_STREAM = false;
const MAX_TOKEN = 4000;
const TEMPERATURE = 0.5;
const TOTAL_TOKENS_AROUND_LIMIT = 120000;
// 会話履歴
const CONVERSATIONS = [];
let TOKEN_COUNT = 0;
// ページが切り替わったときに会話履歴とトークンをクリアする
const clearConversations = () => {
CONVERSATIONS.length = 0;
TOKEN_COUNT = 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で除く
console.warn(finish_reason:${json.choices[0].finish_reason});
if (json.choices0.finish_reason === 'length') { formattedContent.push(' (1度に回答できる文字数を超えました。続きを促してください...)');
}
console.warn(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('記憶整理');
});
}
return ${icon}:\n${formattedContent.join('\n')};
}
function isReadyTamperMonkey() {
if (window.ask_chatgpt) return true;
console.error('TampermonkeyにAskChatGPTがインストールされていません。');
return false;
}
function isSelectTextInput() {
if (document.getElementById('text-input').value !== '') return true;
console.error('範囲が選択されていません。');
return false;
}
function isExecProject() {
if (whitelist.findIndex(f => f === scrapbox.Project.name) >= 0) return true;
console.error('実行できないプロジェクトです。');
return false;
}
// Scrapboxのカーソルのcss設定
let top; let left; let height; let width;
const blinkWidth = 15px!important;animation: blink 1s infinite;
function saveCursorStyle(cursor) {
top = cursor.style.top; left = cursor.style.left;
height = cursor.style.height; width = cursor.style.width;
}
function getDefaultCursorStyle() {
// widthは別に定義するのでデフォルトスタイルには入れない
return top: ${top};left: ${left};height: ${height};
}
async function execGPT() {
if (!isReadyTamperMonkey()) return;
if (!isSelectTextInput()) return;
if (!isExecProject()) return;
// 選択文字
let select = document.getElementById('text-input').value;
// 選択文字はカーソル位置に一旦戻す
insertText({ text: ${select} });
// 命令を追加してみる
select = ${select}。回答に単語強調の装飾は使わないで;
// 応答が返ってくるまでカーソルを太くして点滅させる。終わったら戻すので事前のstyleを保持しておく
const cursor = document.getElementsByClassName('cursor')0; saveCursorStyle(cursor);
cursor.style = ${getDefaultCursorStyle()};width: ${blinkWidth};
CONVERSATIONS.push({role:'user', content:select});
askConversations()
.then(p => {
if (p.status === 200) {
insertText({ text: \n\n${getContent(p.response)}\n\n\n });
return;
}
insertText(
{ text: \n\ncode:json\n ${p.status}:\n ${getPrettyJson(p.response)}\n\n\n });
})
.catch(e => {
insertText(
{ text: \n\ncode:json\n catch:\n${getPrettyJson(e)}\n\n\n });
})
.finally(f => {
cursor.style = ${getDefaultCursorStyle()};width: ${width};;
console.log(${getPrettyJson(CONVERSATIONS)});
});
}
async function execDALLE() {
if (!isReadyTamperMonkey()) return;
if (!isSelectTextInput()) return;
if (!isExecProject()) return;
// 選択文字
const select = document.getElementById('text-input').value;
// 選択文字はカーソル位置に一旦戻す
insertText({ text: ${select} });
// 応答が返ってくるまでカーソルを太くして点滅させる。終わったら戻すので事前のstyleを保持しておく
const cursor = document.getElementsByClassName('cursor')0; saveCursorStyle(cursor);
cursor.style = ${getDefaultCursorStyle()};width: ${blinkWidth};
const img = await generateImage(select);
console.log(img.response);
const json = JSON.parse(img.response);
if (json.error) {
insertText(
{ text: \n${json['error']['code']}: ${json['error']['message']}\n });
} else {
insertText(
{ text: \n[${json['data'][0]['url']}]\n${json['data'][0]['revised_prompt']}\n });
}
cursor.style = ${getDefaultCursorStyle()};width: ${width};;
}
scrapbox.PageMenu.addMenu({
title: 'ChatGPTに尋ねる',
onClick: async () => execGPT()
});
scrapbox.PageMenu.addMenu({
title: '画像生成',
onClick: async () => execDALLE()
});
scrapbox.PopupMenu.addButton({
onClick: async (text) => execGPT()
});
scrapbox.PopupMenu.addButton({
onClick: async (text) => execDALLE()
});
関連
TOKENはちゃんと計算しよう
出力中にカーソルをいじると出力もズレる
参考
role: systemの使い方がいまいちわからない
会話のやり取りに付加しても守ってくれるときと守ってくれないときがある
更新履歴
v1.0(2023/03/15)
会話の履歴を付加して送信するようにした(2023/03/16)
Streamで受け取るようにした(2023/04/10)
回答が返って来なくなって、いろいろ試行錯誤していたらModelを最新版にしたら返ってくるようになった(2023/11/30)
gpt-3.5-turbo-0301 Legacy
Snapshot of gpt-3.5-turbo from March 1st 2023. Will be deprecated on June 13th 2024.
会話に対して会話ができなくなっている(まだ調べてない)
なんかStreamじゃなくていい気もしてきた…
code:js
index.js:2 TypeError: Cannot read properties of undefined (reading 'error')
at processChunk (script.js:150:18)
at getContentStream (script.js:167:13)
at async execGPT (script.js:236:3)
chunkの要素が0で返ってくる可能性があるみたいなのでなおした(2023/12/15)
Streamやめた(2024/04/11)