Zenn
Closed3

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

ie4ie4
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&&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;myLog("✅ 応答完了:チャイム再生 🎵"," (応答完了まで "+elapsed+"秒)",6);playChime();break}myLog("⏳ 応答中... ",elapsed+"秒");await sleep(1000)}}if(prevArticleCount!=currentCount){prevArticleCount=currentCount}}}startAutoResponseWatcher()})();

「ChatGTPにちょっと重めの依頼をした時に、回答完了までその様子を見守っているのも効率が悪いので、回答が完了したら、通知されるようにしたい」という思いで作ったブックマークレット。

DOMの構造が変わったら動かなくなるかもだし、ちょっと挙動が怪しいかもですが、概ね意図した通りに動いてる。

ie4ie4

内容は以下

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