ChatGPTのメッセージに日時(タイムスタンプ)を表示する方法3選

Blog-Thumbnail_ChatGPT ChatGPT

はじめに

こんにちは、KUDs です。

今回は、ChatGPT でタイムスタンプを記録する方法をご紹介します。

そもそも、ChatGPT ではやり取りをした日時が表示されません。

そのため、過去の会話を振り返った時に、「この会話はいつのやり取りだっけ…」というのがわかりません。

本記事では過去の会話の日時を表示する方法として、以下の3パターンを紹介します。

  1. ChatGPT をカスタマイズする (システムプロンプト)
  2. ChatGPT Timestamp Injector (Tampermonkey スクリプト)
  3. Superpower ChatGPT (Chrome 拡張機能)

「ChatGPT をカスタマイズする」は設定が非常に簡単です。
が、弱点がいくつかあります

おすすめは ChatGPT Timestamp Injector を導入する方です。
「ChatGPT をカスタマイズする」の弱点を解消するように私が作成したコードですので是非見てみて下さい。

「タイムスタンプだけじゃなくて他にも機能を色々付けたい」という方は Superpower ChatGPT もおすすめです。

以下では、それぞれの使用イメージや導入手順を紹介していきます。

方法1: 「ChatGPT をカスタマイズする」

「ChatGPT をカスタマイズする」機能で、毎回日時を教えてもらう方法です。
ChatGPT 内で設定でき、簡単です。
が、私としてはこっちの使用はお勧めしません。

以下にて解説します。

使用イメージ

まず、使用イメージは以下のような感じです。

上記画像では、会話の最後にタイムスタンプが追加されてますが、実際の時刻とのずれがひどいですね。

メリット・デメリット

  • メリット
    • ChatGPT 上で簡単に設定できる
    • アプリ版でも使用できる
  • デメリット
    • 時刻ずれがひどい(上述の画像参照)
    • 過去のメッセージには依然としてタイムスタンプは付かない(新規チャットからタイムスタンプ記録開始)
    • 表記揺れの可能性あり

過去のやりとりの記録が見たいのに、過去のメッセージには反映されないのが最大の弱点です。

設定手順

まず、ChatGPT を開き、画面左のメニューから「ChatGPT をカスタマイズする」を選択

次に、「ChatGPT にどのような特徴を求めていますか?」にてタイムスタンプの記録を依頼します。

なお、上記画像のようにタイムスタンプの形式を指定することも可能です。
(YYYY-MM-DD hh:mm:ss Timezone 等)

最後に設定を保存すれば、それ以降の新しいチャットの会話でタイムスタンプが記録されるようになります。

方法2: ChatGPT Timestamp Injector

私が作成したスクリプトです。

過去の会話のタイムスタンプを取得するためだけのスクリプトで、その他の余分な機能は付いていません。

単に過去の会話のタイムスタンプが見たいという方にはお勧めします。

使用イメージ

会話の最後にタイムスタンプが追加されており、時刻ずれがありません

メリット・デメリット

メリデメとしましては、

  • メリット
    • 時刻が正確
    • 過去のメッセージにもタイムスタンプが記録される
    • 表記揺れしない
  • デメリット
    • ブラウザ版でのみ使用可能 (アプリ版不可)

設定手順

設定手順としましては、以下のサイトからスクリプトをインストールしてブラウザをリロードするだけです。

ChatGPT Timestamp Injector
Displays the timestamp at the end of each ChatGPT message.

※このスクリプトをインストールするには、Tampermonkey 等の拡張機能のインストールが必要です。
まだインストールしていない方は、ブラウザの拡張機能からインストールしてください。

仕組みについて

処理の概要につきましてはざっくりと以下の通りです。

  1. ページ読込
  2. アクセストークン取得 /api/auth/session
  3. 会話IDをURLから抽出
  4. GET /backend-api/conversation/{id}
  5. 各message.create_time をMap化
  6. DOM上の[data-message-id] を走査
  7. 末尾に要素を挿入

処理の詳細について

詳細につきましては、スクリプトを貼っておきますのでご参照ください。
※最新版は Greasemonkey の方をご確認ください

// ==UserScript==
// @name         ChatGPT Timestamp Injector
// @namespace    https://kuds.win/
// @version      1.0
// @description  Displays the timestamp at the end of each ChatGPT message.
// @author       KUDs
// @match        https://chatgpt.com/*
// @icon         https://chatgpt.com/favicon.ico
// @grant        none
// @run-at       document-idle
// @license      MIT
// @downloadURL https://update.greasyfork.org/scripts/543305/ChatGPT%20Timestamp%20Injector.user.js
// @updateURL https://update.greasyfork.org/scripts/543305/ChatGPT%20Timestamp%20Injector.meta.js
// ==/UserScript==

(function() {
    'use strict';

    /** フォーマット済み日時文字列を取得する関数 */
    function formatTimestamp(date) {
        // 各数値を2桁ゼロ埋め(年以外)
        const pad = (n) => n.toString().padStart(2, '0');
        const YYYY = date.getFullYear();
        const M = date.getMonth() + 1;
        const D = date.getDate();
        const HH = pad(date.getHours());
        const mm = pad(date.getMinutes());
        const ss = pad(date.getSeconds());
        return `${YYYY}/${M}/${D} ${HH}:${mm}:${ss}`;
    }

    /** チャットIDをURLから取得する関数 */
    function getCurrentChatId() {
        // パス形式: /c/<chat-id> または /g/<team-id>/c/<chat-id> (また、/share/<share-id>は今回は除外)
        const match = location.pathname.match(/^\/(?:c|g\/[^\/]+\/c)\/([^\/]+)/);
        return match ? match[1] : null;
    }

    let accessToken = null;
    /** アクセストークンを取得(既に取得済みなら再利用) */
    async function getAccessToken() {
        if (accessToken) return accessToken;
        try {
            const res = await fetch('/api/auth/session');
            if (!res.ok) throw new Error(res.statusText);
            const data = await res.json();
            accessToken = data.accessToken;
            return accessToken;
        } catch (err) {
            console.error('Failed to get access token:', err);
            return null;
        }
    }

    /** 会話データを取得する関数 */
    async function fetchConversationData(chatId) {
        const token = await getAccessToken();
        if (!token) return null;
        try {
            const res = await fetch(`/backend-api/conversation/${chatId}`, {
                headers: {
                    'Authorization': `Bearer ${token}`,
                    'X-Authorization': `Bearer ${token}`
                }
            });
            if (!res.ok) throw new Error(res.statusText);
            return await res.json();
        } catch (err) {
            console.error('Failed to fetch conversation data:', err);
            return null;
        }
    }

    /** メッセージ要素にタイムスタンプを付与する関数 */
    function appendTimestampElement(messageElem, timestamp) {
        // **重複防止**:すでに挿入済みならスキップ
        if (messageElem.querySelector('time.chatgpt-timestamp')) return;

        // 時刻をフォーマットし<time>要素を生成
        const date = new Date(timestamp * 1000);
        const timeText = formatTimestamp(date);
        const timeEl = document.createElement('time');
        timeEl.className = 'chatgpt-timestamp w-full';
        timeEl.dateTime = date.toISOString();
        timeEl.title = date.toLocaleString();
        timeEl.textContent = timeText;

        // 役割に応じて左右寄せを切り替え
        const role = messageElem.getAttribute('data-message-author-role');
        Object.assign(timeEl.style, {
            fontStyle: 'italic',
            opacity: '0.6',
            fontSize: '0.875rem',
            display: 'block',
            textAlign: role === 'user' ? 'right' : 'left'
        });
        messageElem.appendChild(timeEl);
    }

    // 挿入済みメッセージIDのセット(重複防止)
    const processedIds = new Set();

    // MutationObserverの設定(新しいメッセージの追加を監視)
    const mainElem = document.querySelector('main');
    let suppressObserver = false;
    let suppressedQueue = [];
    const observer = new MutationObserver((mutations) => {
        for (const mut of mutations) {
            for (const node of mut.addedNodes) {
                if (!(node instanceof HTMLElement)) continue;
                // 追加されたノード(またはその子孫)にメッセージ要素があるか確認
                let messageElements = [];
                if (node.hasAttribute && node.hasAttribute('data-message-id')) {
                    messageElements.push(node);
                } else {
                    messageElements = Array.from(node.querySelectorAll?.('[data-message-id]') || []);
                }
                for (const msgElem of messageElements) {
                    const msgId = msgElem.getAttribute('data-message-id');
                    if (!msgId || processedIds.has(msgId)) continue;
                    if (suppressObserver) {
                        // 一時抑制中はキューに溜めるだけ
                        suppressedQueue.push(msgId);
                    } else {
                        // 新規メッセージに対しタイムスタンプを取得・表示
                        updateMessageTimestamp(msgElem, msgId);
                    }
                }
            }
        }
    });
    if (mainElem) {
        observer.observe(mainElem, { childList: true, subtree: true });
    }

    /** 特定のメッセージIDに対してタイムスタンプを取得し挿入する関数 */
    async function updateMessageTimestamp(messageElem, messageId, retryCount = 0) {
        const chatId = getCurrentChatId();
        if (!chatId) return;
        const convo = await fetchConversationData(chatId);
        if (!convo || !convo.mapping) return;
        const node = convo.mapping[messageId];
        if (!node || !node.message || node.message.create_time === undefined) {
            // 該当メッセージが会話データに無い(※生成中の可能性)場合、一定回数リトライ
            if (retryCount < 10) {
                setTimeout(() => updateMessageTimestamp(messageElem, messageId, retryCount + 1), 1000);
            } else {
                console.warn(`Timestamp not found for message ${messageId}`);
            }
        } else {
            // タイムスタンプ挿入
            appendTimestampElement(messageElem, node.message.create_time);
            processedIds.add(messageId);
        }
    }

    /** 現在の会話内の全メッセージにタイムスタンプを付与 */
    async function processAllMessagesInConversation() {
        const chatId = getCurrentChatId();
        if (!chatId) return;
        const convo = await fetchConversationData(chatId);
        if (!convo) return;
        // 会話データから順序通りメッセージのリストを取得
        const mapping = convo.mapping || {};
        const startNodeId = convo.current_node || Object.values(mapping).find(n => !n.children || n.children.length === 0)?.id;
        if (!startNodeId) {
            console.warn('No valid start node for conversation');
            return;
        }
        // 末尾(最新)から親をたどって最初のメッセージまでリスト化
        const nodes = [];
        let nodeId = startNodeId;
        while (nodeId) {
            const node = mapping[nodeId];
            if (!node) break;
            // ルートに到達
            if (node.parent === undefined) break;
            // systemや非表示のメタメッセージをスキップ
            const msg = node.message;
            if (msg && msg.author.role !== 'system' &&
                msg.content?.content_type !== 'model_editable_context' &&
                msg.content?.content_type !== 'user_editable_context') {
                nodes.unshift(node);
            }
            nodeId = node.parent;
        }
        // DOM上の各メッセージ要素に対応するタイムスタンプを付与
        for (const node of nodes) {
            const msg = node.message;
            if (!msg || msg.id === undefined || msg.create_time === undefined) continue;
            const msgId = msg.id;
            const elem = document.querySelector(`[data-message-id="${msgId}"]`);
            if (elem && !processedIds.has(msgId)) {
                appendTimestampElement(elem, msg.create_time);
                processedIds.add(msgId);
            }
        }
    }

    // チャットの切り替えや初期読み込みを監視し、適宜タイムスタンプを挿入
    let currentChatId = null;
    setInterval(() => {
        const chatId = getCurrentChatId();
        if (chatId && chatId !== currentChatId) {
            // チャットが新規に開かれた/切り替わった
            currentChatId = chatId;
            processedIds.clear();
            suppressObserver = true;
            suppressedQueue = [];
            // 少し待ってから過去メッセージにまとめてタイムスタンプを付与
            setTimeout(async () => {
                await processAllMessagesInConversation();
                // 抑制中に溜まった新規メッセージも処理する
                for (const newId of suppressedQueue) {
                    const elem = document.querySelector(`[data-message-id="${newId}"]`);
                    if (elem && !processedIds.has(newId)) {
                        updateMessageTimestamp(elem, newId);
                    }
                }
                suppressedQueue = [];
                suppressObserver = false;
            }, 500);
        }
    }, 1000);
})();

方法3: Superpower ChatGPT

こちらは Chrome 拡張機能で、多機能を求めている方には割とおすすめです。
私は時間フォーマットやその他機能の使用感があんまりしっくりこなかったので使ってないというだけです。

使用イメージ

本日なら “Today”、昨日なら “Yesterday”、それ以前なら “MM/DD/YYYY”(アメリカ表記) です。

メリット・デメリット

  • メリット
    • 多機能
    • ユーザ多数で安心
  • デメリット
    • 多機能すぎて複雑
    • 日時はアメリカ表記 (MM/DD/YYYY hh:mm AM/PM)
    • 設定やメニュー等が英語表記

設定手順

まず、以下の Chrome Web Store から Superpower ChatGPT をインストールします。

Superpower ChatGPT - Chrome ウェブストア
ChatGPT with Superpowers! Folders, Search, GPT Store, Image Gallery, Voice GPT, Export, Custom Prompts, Prompt Chains, H...

次に、ChatGPT に移動して⚡️マークにカーソルを合わせます。
すると、⚙️が表示されるため、こちらをクリックします。

Settings ウィンドウが開くため、Conversation タブから 「Show Message Timestamp」 を有効化します。

最後に画面をリフレッシュすると、メッセージにタイムスタンプが表示されるはずです。

さいごに

Superpower ChatGPT も強力だしすごくいいんですけどね、、
ここまでいらないんですよね、もっとシンプルでいい。

ということで、「ぱっと見世の中に良さげな奴がなさそうだったので自分で作ってみた」のシリーズでした。

なお、今回のスクリプトは ChatGPT Exporter を参考にさせてもらってます。
興味があれば是非見てみてください。

以上です。

コメント