本文へ移動
vast-cowのブログ
前のページへ戻る

ChatGPTの「新しいチャット」を新規タブで開けるTampermonkeyスクリプト

この記事を編集

ChatGPTを使っていて、地味に欲しかったのが 「新しいチャットを新規タブで開く」ボタン です。

通常の「新しいチャット」を押すと、現在のタブがそのまま新しいチャットに切り替わります。

しかし、

という使い方をしていると、専用ボタンが欲しくなります。

そこでTampermonkeyで、ChatGPTの「新しいチャット」ボタンの横に 「新規タブ」ボタンを自動追加 するUserScriptを作りました。

完成イメージ

ChatGPTの画面にある

「新しいチャット」

の近くに、

「新規タブ」

というボタンを追加します。

「新規タブ」をクリックすると、現在開いているチャットはそのまま残して、ChatGPTの新しいチャット画面を別タブで開けます。

複数のテーマについてChatGPTと並行して会話するときにかなり便利です。

Tampermonkeyスクリプト

以下をTampermonkeyに登録します。

// ==UserScript==
// @name         Add New Chat Link
// @namespace    tampermonkey
// @version      1.0
// @description  Duplicates the "New Chat" button and adds a link.
// @match        https://chatgpt.com/*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    const TARGET_TEXT = 'New Chat';
    const CLONED_TEXT = 'New Tab';
    const ADDED_LINK_SELECTOR = 'a[data-tm-new-chat-link]';

    /**
     * Searches for the "New Chat" span within the target root or its descendants.
     */
    function findTargetSpan(targetRoot) {
        // Check the root element itself
        if (
            targetRoot instanceof HTMLSpanElement &&
            targetRoot.innerText.trim() === TARGET_TEXT
        ) {
            return targetRoot;
        }

        // Check the descendants
        if (targetRoot.querySelectorAll) {
            for (const span of targetRoot.querySelectorAll('span')) {
                if (span.innerText.trim() === TARGET_TEXT) {
                    return span;
                }
            }
        }

        return null;
    }

    function initialize() {
        const root = document.querySelector('#root');

        if (!root) {
            console.warn('[Tampermonkey] #root not found');
            return;
        }

        const observerOptions = {
            childList: true,
            subtree: true
        };

        let observer;

        function process(targetRoot) {
            // Stop the observer while processing
            observer?.disconnect();

            try {
                const span = findTargetSpan(targetRoot);

                if (!span) {
                    return;
                }

                // Get the ancestor button
                const button = span.closest('button');

                if (!button) {
                    return;
                }

                // Get the button's parent
                const parent = button.parentElement;

                if (!parent) {
                    return;
                }

                // Do nothing if already added
                if (parent.querySelector(ADDED_LINK_SELECTOR)) {
                    return;
                }

                // Clone the button
                const buttonCloned = button.cloneNode(true);

                // Change "New Chat" to "New Window" in the cloned button
                const clonedSpan = findTargetSpan(buttonCloned);

                if (clonedSpan) {
                    clonedSpan.innerText = CLONED_TEXT;
                }

                buttonCloned.classList.replace("bg-primary-ghost-hover", "hover:bg-primary-ghost-hover");
                buttonCloned.classList.add("data-[state=open]:bg-primary-ghost-hover");
                buttonCloned.firstChild.classList.replace("text-emphasis", "text-default");

                // Create the <a> tag
                const anchor = document.createElement('a');

                anchor.href = '/';
                anchor.target = "_blank";
                anchor.dataset.tmNewChatLink = 'true';

                anchor.appendChild(buttonCloned);

                // Add to the parent of the original button
                parent.prepend(anchor);

                console.log('[Tampermonkey] link added:', anchor);
            } finally {
                // Resume observation after processing
                observer?.observe(root, observerOptions);
            }
        }

        observer = new MutationObserver((mutations) => {
            // Stop at the beginning of the callback
            observer.disconnect();

            try {
                for (const mutation of mutations) {
                    for (const node of mutation.addedNodes) {
                        if (!(node instanceof Element)) {
                            continue;
                        }

                        process(node);
                    }
                }
            } finally {
                observer.observe(root, observerOptions);
            }
        });

        // Initial execution
        process(root);

        // Observe child element additions under #root
        observer.observe(root, observerOptions);
    }

    if (document.readyState === 'complete') {
        initialize();
    } else {
        window.addEventListener('load', initialize, { once: true });
    }
})();

何をしているのか

仕組み自体はシンプルです。

ChatGPTの画面から「新しいチャット」という文字列を持つspanを探し、その親にあるbuttonを取得します。

const span = findTargetSpan(targetRoot);
const button = span.closest('button');

そして、そのボタンを丸ごと複製します。

const buttonCloned = button.cloneNode(true);

ゼロからCSSを書くのではなく、 ChatGPT自身のボタンを複製している のがポイントです。

これによって、ChatGPT側のUIと比較的馴染みやすい見た目になります。

複製したボタンについては、表示を

新しいチャット

から

新規タブ

へ変更します。

const clonedSpan = findTargetSpan(buttonCloned);

if (clonedSpan) {
    clonedSpan.innerText = CLONED_TEXT;
}

新しいタブでChatGPTを開く

複製したボタンはa要素の中に入れています。

const anchor = document.createElement('a');

anchor.href = '/';
anchor.target = '_blank';

ポイントは、

anchor.target = '_blank';

です。

これによってボタンを押したとき、現在のChatGPT画面を置き換えるのではなく、 新しいタブでChatGPTを開く ようになります。

現在のチャットを残したまま別の会話を開始できます。

MutationObserverを使っている理由

ChatGPTは画面遷移のたびにページ全体を読み込み直す一般的なWebサイトとは少し違い、JavaScriptによって画面の内容が動的に変更されます。

そのため、

window.addEventListener('load', ...)

だけを利用してボタンを追加すると、ChatGPT側でUIが再描画されたときに追加したボタンが消える可能性があります。

そこでMutationObserverを利用しています。

observer = new MutationObserver((mutations) => {
    // ...
});

そして、

observer.observe(root, {
    childList: true,
    subtree: true
});

として、#root以下に追加されるDOM要素を監視します。

ChatGPT側でUIが更新された場合にも「新しいチャット」を探して、必要なら「新規タブ」を追加し直す仕組みです。

ボタンが大量に増えないようにする

MutationObserverを使うと注意したいのが、自分自身がDOMを書き換えた結果まで監視対象になることです。

そのため、このスクリプトでは追加するリンクに

anchor.dataset.tmNewChatLink = 'true';

という目印を付けています。

HTML上では、

<a data-tm-new-chat-link="true">

のようになります。

そして、

if (parent.querySelector(ADDED_LINK_SELECTOR)) {
    return;
}

として、すでにボタンが存在している場合には追加しません。

さらにprocess()中は一時的にMutationObserverを停止し、処理が終わったら監視を再開しています。

observer?.disconnect();

try {
    // DOM操作
} finally {
    observer?.observe(root, observerOptions);
}

これによって、自分自身のDOM変更を拾って処理が繰り返されることを避けています。

インストール方法

Tampermonkeyを導入済みなら設定は簡単です。

  1. Tampermonkeyのダッシュボードを開く
  2. 「新規スクリプトを追加」を選択
  3. 最初から入っているコードを削除
  4. 上記のUserScriptを貼り付ける
  5. 保存する
  6. ChatGPTを再読み込みする

これでChatGPTの画面に「新規タブ」が追加されます。

こういう人には特におすすめ

ChatGPTを「1つのチャットですべて質問する」のではなく、 テーマごとにチャットを分けて使っている人 には特に便利です。

例えば、

タブ1:プログラムについて質問
タブ2:メール文章を作成
タブ3:技術情報を調査
タブ4:アイデア出し

といった使い方がしやすくなります。

元のチャットから離れずに次のチャットを作れるので、ChatGPTを大量に使うほど恩恵があります。

注意点

このUserScriptはChatGPTのDOM構造を利用しています。

特に、

#root

や、

span.closest('button')

といったChatGPT側のHTML構造を前提にしています。

そのため、ChatGPTのUIがアップデートされると動作しなくなる可能性があります。

また、「新しいチャット」という日本語表示を探しているため、ChatGPTを英語など別の言語設定で使用している場合は、そのままでは動作しません。

例えば英語UIに対応させる場合は、検索対象をNew chatにも対応させるなどの変更が必要です。

TampermonkeyのUserScriptはWebページ上でJavaScriptを実行する仕組みなので、内容を理解できるスクリプトだけを導入することも重要です。

まとめ

今回のUserScriptを入れると、ChatGPTの「新しいチャット」の近くに 「新規タブ」ボタン を追加できます。

やっていることは小さなUI変更ですが、

「今のチャットを残しておきたい → 新しいタブを開く → ChatGPTを開く」

という操作を、

「新規タブ」を1回クリック

に短縮できます。

複数のチャットを並行して使うことが多い人ほど便利になるTampermonkeyスクリプトです。

ChatGPTをブラウザで頻繁に使っているなら、試してみる価値があります。


この記事を編集
この記事を共有:

コメント


前の記事
はてなブログのテーマ「Smooth」をダークモード対応にするCSS
次の記事
LiteLLMでLLMの出力をキャッシュする方法