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に聞く",
});
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(
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 dialog = useRef(null);
useEffect(() => {
generatePrompt(type).then((res) => {
setText(res);
});
useLayoutEffect(() => {
if (open) {
dialog.current.showModal();
} else {
dialog.current.close();
}
useLayoutEffect(() => {
if (atosimatsu) {
atosimatsufn();
}
return h(
"dialog",
{ ref: dialog },
h(
"button",
{
type: "button",
onClick: () => {
setOpen(false);
setAtosimatsu(true);
},
},
"閉じる",
),
text == ""
? [
h("h2", null, "リンク先のページ内容を取得しています..."),
h("style", null, `
width: 24px;
height: 24px;
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, "主なLLMリスト"),
h(
"ul",
null,
h(
"li",
null,
"(1メッセージごとに20000文字)",
),
h(
"li",
null,
"(1メッセージごとに10000文字)",
),
),
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ページ目をコピー",
),
);
}