Closed3
ChatGPTの回答完了をチャイムで知らせるブックマークレット

javascript:(function(){const originalTitle=document.title;function getByXPath(xpath){const result=document.evaluate(xpath,document,null,XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null);const nodes=[];for(let i=0;i<result.snapshotLength;i++){nodes.push(result.snapshotItem(i))}return nodes}function sleep(ms){return new Promise(resolve=>setTimeout(resolve,ms))}function myLog(msg,elapsedText="",fadeDuration=0){console.log(msg);const targets=document.querySelectorAll('[data-testid="model-switcher-dropdown-button"]');targets.forEach(target=>{let logContainer=target.nextElementSibling;if(!logContainer||!logContainer.classList.contains("log-container")){logContainer=document.createElement("div");logContainer.className="log-container";logContainer.style.display="inline-block";logContainer.style.marginLeft="10px";target.insertAdjacentElement("afterend",logContainer)}logContainer.innerHTML="";const logLine=document.createElement("div");logLine.textContent=msg+elapsedText;logLine.style.opacity="1";logLine.style.transition="opacity "+fadeDuration+"s ease";logContainer.appendChild(logLine);logLine.offsetWidth;if(fadeDuration>0){setTimeout(()=>{logLine.style.opacity="0";if(elapsedText!=""){console.log(elapsedText)}setTimeout(()=>{logContainer.innerHTML=""},fadeDuration*1000)},2000)}})}function playChime(){const ctx=new AudioContext();const now=ctx.currentTime;const notes=[{freq:523.25,time:0},{freq:659.25,time:0.5},{freq:783.99,time:1.0},{freq:1046.50,time:1.6}];const delay=ctx.createDelay();delay.delayTime.value=0.2;const feedback=ctx.createGain();feedback.gain.value=0.08;const filter=ctx.createBiquadFilter();filter.type="lowpass";filter.frequency.value=1500;delay.connect(feedback);feedback.connect(filter);filter.connect(delay);notes.forEach(({freq,time})=>{const osc=ctx.createOscillator();const gain=ctx.createGain();osc.type="sine";osc.frequency.setValueAtTime(freq,now+time);gain.gain.setValueAtTime(0,now+time);gain.gain.linearRampToValueAtTime(0.5,now+time+0.1);gain.gain.linearRampToValueAtTime(0.2,now+time+0.4);gain.gain.linearRampToValueAtTime(0,now+time+1.2);osc.connect(gain).connect(ctx.destination);gain.connect(delay);osc.start(now+time);osc.stop(now+time+2)});delay.connect(ctx.destination)}async function startAutoResponseWatcher(){myLog("🚀 自動応答監視を開始しました","",3);let prevArticleCount=getByXPath('//*[@id="main"]/div[1]/div/div[2]/div/div/div[2]/article').length;while(true){await sleep(1000);const articles=getByXPath('//*[@id="main"]/div[1]/div/div[2]/div/div/div[2]/article');const currentCount=articles.length;if(prevArticleCount>0&¤tCount>=prevArticleCount+2){myLog("🆕 投稿検知:記事数 "+prevArticleCount+" → "+currentCount+"(応答監視へ)");const article=articles[articles.length-1];const responseStart=Date.now();while(true){document.title="⏳ "+originalTitle;const elapsed=Math.round((Date.now()-responseStart)/1000);if(article.querySelector('button[data-testid="copy-turn-action-button"]')!==null){document.title=originalTitle;myLog("✅ 応答完了:チャイム再生 🎵"," (応答完了まで "+elapsed+"秒)",6);playChime();break}myLog("⏳ 応答中... ",elapsed+"秒");await sleep(1000)}}if(prevArticleCount!=currentCount){prevArticleCount=currentCount}}}startAutoResponseWatcher()})();
「ChatGTPにちょっと重めの依頼をした時に、回答完了までその様子を見守っているのも効率が悪いので、回答が完了したら、通知されるようにしたい」という思いで作ったブックマークレット。
DOMの構造が変わったら動かなくなるかもだし、ちょっと挙動が怪しいかもですが、概ね意図した通りに動いてる。

内容は以下
(function () {
// ページタイトルの元の値を保存
const originalTitle = document.title;
// XPath で要素を取得する関数
function getByXPath(xpath) {
const result = document.evaluate(
xpath,
document,
null,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
null
);
const nodes = [];
for (let i = 0; i < result.snapshotLength; i++) {
nodes.push(result.snapshotItem(i));
}
return nodes;
}
// 指定したミリ秒だけ待機する非同期関数
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// ログをコンソールとログ用コンテナに表示する関数
function myLog(msg, elapsedText = "", fadeDuration = 0) {
console.log(msg);
// data-testid="model-switcher-dropdown-button" を持つすべての要素を取得
const targets = document.querySelectorAll('[data-testid="model-switcher-dropdown-button"]');
targets.forEach((target) => {
let logContainer = target.nextElementSibling;
// すでにログ用コンテナが存在するか、クラス名で判定
if (!logContainer || !logContainer.classList.contains("log-container")) {
logContainer = document.createElement("div");
logContainer.className = "log-container";
logContainer.style.display = "inline-block";
logContainer.style.marginLeft = "10px";
target.insertAdjacentElement("afterend", logContainer);
}
// 前回の内容をクリアして最新メッセージのみ表示する
logContainer.innerHTML = "";
const logLine = document.createElement("div");
logLine.textContent = msg + elapsedText;
logLine.style.opacity = "1";
logLine.style.transition = "opacity " + fadeDuration + "s ease";
logContainer.appendChild(logLine);
// リフローを強制して transition を有効化
logLine.offsetWidth;
if (fadeDuration > 0) {
setTimeout(() => {
logLine.style.opacity = "0";
if (elapsedText != '') {
console.log(elapsedText);
}
setTimeout(() => {
logContainer.innerHTML = "";
}, fadeDuration * 1000);
}, 2000);
}
});
}
// 学校チャイム風のチャイム音を再生する関数
function playChime() {
const ctx = new AudioContext();
const now = ctx.currentTime;
const notes = [
{ freq: 523.25, time: 0 }, // C5
{ freq: 659.25, time: 0.5 }, // E5
{ freq: 783.99, time: 1.0 }, // G5
{ freq: 1046.50, time: 1.6 } // C6
];
// ディレイ設定(少しの遅れを付加)
const delay = ctx.createDelay();
delay.delayTime.value = 0.2;
// 軽いエコーのためのフィードバック設定
const feedback = ctx.createGain();
feedback.gain.value = 0.08;
// ローファイ効果用フィルター設定
const filter = ctx.createBiquadFilter();
filter.type = "lowpass";
filter.frequency.value = 1500;
// エフェクトチェーンの接続
delay.connect(feedback);
feedback.connect(filter);
filter.connect(delay);
// 各ノートを生成し再生
notes.forEach(({ freq, time }) => {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = "sine";
osc.frequency.setValueAtTime(freq, now + time);
// 音量のフェードイン・アウト設定
gain.gain.setValueAtTime(0, now + time);
gain.gain.linearRampToValueAtTime(0.5, now + time + 0.1);
gain.gain.linearRampToValueAtTime(0.2, now + time + 0.4);
gain.gain.linearRampToValueAtTime(0, now + time + 1.2);
osc.connect(gain).connect(ctx.destination);
gain.connect(delay);
osc.start(now + time);
osc.stop(now + time + 2);
});
delay.connect(ctx.destination);
}
// 自動応答監視ループを開始する関数
async function startAutoResponseWatcher() {
myLog("🚀 自動応答監視を開始しました", "", 3);
// 初期の投稿記事数を XPath で取得
let prevArticleCount = getByXPath('//*[@id="main"]/div[1]/div/div[2]/div/div/div[2]/article').length;
// 無限ループで新規投稿の監視
while (true) {
await sleep(1000);
const articles = getByXPath('//*[@id="main"]/div[1]/div/div[2]/div/div/div[2]/article');
const currentCount = articles.length;
// 2件以上増加した場合に処理を開始
if (prevArticleCount > 0 && currentCount >= prevArticleCount + 2) {
myLog("🆕 投稿検知:記事数 " + prevArticleCount + " → " + currentCount + "(応答監視へ)");
const article = articles[articles.length - 1];
// 応答開始時刻を記録
const responseStart = Date.now();
// 応答完了を示すボタンが表示されるまで監視
while (true) {
// 応答中はタブタイトルに砂時計マークを表示
document.title = "⏳ " + originalTitle;
const elapsed = Math.round((Date.now() - responseStart) / 1000);
if (article.querySelector('button[data-testid="copy-turn-action-button"]') !== null) {
// 応答完了時はタイトルを元に戻し、応答完了までの経過時間を表示
document.title = originalTitle;
// 応答完了時はフェードアウト時間を3秒に変更
myLog("✅ 応答完了:チャイム再生 🎵", " (応答完了まで " + elapsed + "秒)", 6);
playChime();
break;
}
myLog("⏳ 応答中... ", elapsed + "秒");
await sleep(1000);
}
}
if (prevArticleCount != currentCount) {
prevArticleCount = currentCount;
}
}
}
// 自動応答監視を開始
startAutoResponseWatcher();
})();

- ブックマークレットの登録方法
- ブックマークバーで「ページを追加」
- 名前に「ChatGPTレス監視」、URLに上記ブックマークレットを入力
- 使い方
- チャット画面でブックマークレットを実行すると、「自動応答監視を開始しました」と出て、GPTの回答が完了するとチャイムが鳴るようになる(音量注意)
このスクラップは16日前にクローズされました
作成者以外のコメントは許可されていません