askllm
code:script.js
import {Component,Fragment,cloneElement,createContext,createElement,createRef,h,hydrate,isValidElement,options,render,toChildArray} from "npm:preact";
import {useCallback,useContext,useDebugValue,useEffect,useErrorBoundary,useId,useImperativeHandle,useLayoutEffect,useMemo,useReducer,useRef,useState} from "npm:preact/hooks";
scrapbox.PageMenu.addMenu({
title: "LLMに聞く",
image: "https://scrapbox.io/api/pages/villagepump/AI/icon",
});
scrapbox.PageMenu("LLMに聞く").addItem({
title: "要約",
onClick: () => ask("summarize"),
});
scrapbox.PageMenu("LLMに聞く").addItem({
title: "分割支援",
onClick: () => ask("split"),
});
scrapbox.PageMenu("LLMに聞く").addItem({
title: "会話のネタ",
onClick: () => ask("chat"),
});
scrapbox.PageMenu("LLMに聞く").addItem({
title: "カスタムプロンプト",
onClick: () => ask("custom"),
});
function bytesToBase64(text) {
const bytes = new TextEncoder().encode(text);
const binString = Array.from(bytes, (byte) =>
String.fromCodePoint(byte),
).join("");
return btoa(binString);
}
function getPage(pageName) {
return fetch(
"https://scrapbox.io/api/pages/" +
scrapbox.Project.name +
"/" +
encodeURIComponent(pageName) +
"/text",
).then((pageRes) => {
if (pageRes.ok) {
return pageRes.text();
} else {
return "";
}
});
}
function generatePrompt(type) {
const pageText = scrapbox.Page.lines.reduce((a, b) => a + b.text + "\n", "");
let searchText = "";
return Promise.all(scrapbox.Page.metadata.links.map((e) => getPage(e))).then(
(pages) => {
const pagesSorted = pages.toSorted((a, b) => {
a.length - b.length;
});
for (const element of pagesSorted) {
const tempText = "\n" + element.replace(/^\n*|\n*$/g, "") + "\n";
if (
(searchText.length + tempText.length < 40000) &
(element.replace(/^\n*|\n*$/g, "") != "")
) {
searchText = tempText+searchText;
}
}
searchText = "リンク先ページの内容になります。"+searchText;
searchText +=
"リンク先ページの内容はここまでになります。ここから本文です";
if (type === "summarize") {
searchText +=
scrapbox.Page.title +
"とそのリンク先を読んで800文字程度の要約を出力してください";
} else if (type === "split") {
searchText +=
scrapbox.Page.title +
"とそのリンク先を読んで次の内容を出力してください:\n\
分割ポイント:ページの内容を話題ごとに分割するとしたら、対象部分を引用してどの部分を分割すべきか示す。\n\
分割しないポイント:ページの内容を分割するべきでないとしたら、なぜ分割するべきでないか\n\
評価:「分割ポイント」と「分割しないポイント」を踏まえ、折衷案を示す\n\
文字数:" +
pageText.length;
} else if (type === "custom") {
const outputPrompt = window.prompt("出力してほしい内容");
if (outputPrompt === null) {
return;
}
searchText +=
scrapbox.Page.title +
"とそのリンク先を読んで" +
outputPrompt +
"を出力してください";
} else if (type === "chat") {
searchText +=
scrapbox.Page.title + "とそのリンク先を読んで会話を開始してください";
}
searchText +=
"\n\
出力は**日本語**で全体をコードブロックで囲まずに行ってください。\n\
コードブロックはScrapbox記法で書かれています。\n\
Scrapbox記法の簡単な説明:\n\
- 正規表現で言う^[\\t  ]+はインデントとみなされます。\n\
- 当てはまるもの1文字につき1インデントレベルです。\n\
- [と]で囲まれたテキストは他のページへのリンクになっています。\n\
- [と.icon]で囲まれたテキストはカスタム絵文字です。\n`" +
pageText.replace(/^\n*|\n*$/g, "") +
"\n`\n";
return searchText;
},
);
}
function ask(type) {
const dialogContainer = document.body.appendChild(
document.createElement("div"),
);
dialogContainer.setAttribute("data-userscript-name", "ask-to-llm");
function deletedialog() {
render(null, dialogContainer);
dialogContainer.remove();
}
render(h(LlmView, { type, atosimatsufn: deletedialog }), dialogContainer);
}
function LlmView({ type, atosimatsufn }) {
const open, setOpen = useState(true);
const text, setText = useState("");
const atosimatsu, setAtosimatsu = useState(false);
const dialog = useRef(null);
useEffect(() => {
generatePrompt(type).then((res) => {
setText(res);
});
}, type);
useLayoutEffect(() => {
if (open) {
dialog.current.showModal();
} else {
dialog.current.close();
}
}, open);
useLayoutEffect(() => {
if (atosimatsu) {
atosimatsufn();
}
}, atosimatsu);
return h(
"dialog",
{ ref: dialog },
h(
"button",
{
type: "button",
onClick: () => {
setOpen(false);
setAtosimatsu(true);
},
},
"閉じる",
),
text == ""
? [
h("h2", null, "リンク先のページ内容を取得しています..."),
h("style", null, `
#userscript-spinner {
width: 24px;
height: 24px;
border: 2px solid #444;
border-bottom-color: transparent;
border-radius: 50%;
display: inline-block;
box-sizing: border-box;
animation: userscript-rotation .5s linear infinite;
}
@keyframes userscript-rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
`),
h("span", {id:"userscript-spinner"}, ""),
h(
"pre",
null,
scrapbox.Page.metadata.links.length + "件のリンク先\n",
scrapbox.Page.metadata.links.join("\n"),
),
]
: h(LlmResultView, { text }),
);
}
function LlmResultView({ text: searchText }) {
return h(
Fragment,
null,
h("h2", null, "手順"),
h(
"ol",
null,
h("li", null, "テキストボックスを全選択してコピー"),
h("li", null, "LLMを開いてペースト、送信"),
),
h("h2", null, "主なAIチャットサイト"),
h(
"ul",
null,
h(
"li",
null,
h("a", { href: "https://chat.openai.com/" ,target:"_blank"}, "ChatGPT"),
"(1メッセージごとに20000文字)",
),
h(
"li",
null,
h("a", { href: "https://copilot.microsoft.com/" ,target:"_blank"}, "Microsoft Copilot"),
"(1メッセージごとに10000文字)",
),
h("li", null, h("a", { href: "https://claude.ai/",target:"_blank" }, "Anthropic Claude"),"(使ったことないのでわからない)"),
),
h("h2", null, "本文(40000文字 非分割版)"),
h(
"button",
{
onClick: () => {
navigator.clipboard.writeText(searchText);
},
},
"コピー",
),
h("h2", null, "本文(20000文字 非分割版)"),
h(
"button",
{
onClick: () => {
navigator.clipboard.writeText(searchText.slice(-20001, -1));
},
},
"コピー",
),
h("h2", null, "本文(20000文字*2 分割版)"),
h(
"button",
{
onClick: () => {
navigator.clipboard.writeText(
"このテキストはは2ページある内、1ページ目です**「それでは2ページ目を送信してください」と言ってください**\n----以下コンテンツ----\n" +
searchText.slice(0, 20000),
);
},
},
"1ページ目をコピー",
),
h(
"button",
{
onClick: () => {
navigator.clipboard.writeText(
"このテキストはは2ページある内、2ページ目です。1ページ目とつなげて解釈してください。\n----以下コンテンツ----\n" +
searchText.slice(20000, 40000),
);
},
},
"2ページ目をコピー",
),
h("h2", null, "本文(10000文字*2 分割版)"),
h(
"button",
{
onClick: () => {
navigator.clipboard.writeText(
"このテキストはは2ページある内、1ページ目です**「それでは2ページ目を送信してください」と言ってください**\n----以下コンテンツ----\n" +
searchText.slice(-20001, -10001),
);
},
},
"1ページ目をコピー",
),
h(
"button",
{
onClick: () => {
navigator.clipboard.writeText(
"このテキストはは2ページある内、2ページ目です。1ページ目とつなげて解釈してください。\n----以下コンテンツ----\n" +
searchText.slice(-10001, -1),
);
},
},
"2ページ目をコピー",
),
h("h2", null, "本文(10000文字*4 分割版)"),
h(
"button",
{
onClick: () => {
navigator.clipboard.writeText(
"このテキストはは4ページある内、1ページ目です**「それでは2ページ目を送信してください」と言ってください**\n----以下コンテンツ----\n" +
searchText.slice(0, 10000),
);
},
},
"1ページ目をコピー",
),
h(
"button",
{
onClick: () => {
navigator.clipboard.writeText(
"このテキストはは4ページある内、2ページ目です**「それでは3ページ目を送信してください」と言ってください**\n----以下コンテンツ----\n" +
searchText.slice(10000, 20000),
);
},
},
"2ページ目をコピー",
),
h(
"button",
{
onClick: () => {
navigator.clipboard.writeText(
"このテキストはは4ページある内、3ページ目です**「それでは4ページ目を送信してください」と言ってください**\n----以下コンテンツ----\n" +
searchText.slice(20000, 30000),
);
},
},
"3ページ目をコピー",
),
h(
"button",
{
onClick: () => {
navigator.clipboard.writeText(
"このテキストはは4ページある内、4ページ目です。1ページ目・2ページ目・3ページ目とつなげて解釈してください。\n----以下コンテンツ----\n" +
searchText.slice(30000, 40000),
);
},
},
"4ページ目をコピー",
),
);
}