NotebookLM Auto Save

Auto-save NotebookLM chat content, triggers save on mouse movement (at most once every 10 seconds)

2025/11/09のページです。最新版はこちら

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         NotebookLM Auto Save
// @namespace    http://tampermonkey.net/
// @version      1.0.1
// @description  Auto-save NotebookLM chat content, triggers save on mouse movement (at most once every 10 seconds)
// @author       You
// @match        https://notebooklm.google.com/notebook/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=google.com
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-end
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // Generate time key
    const timeKey = new Date().toLocaleDateString() + '_' + new Date().toLocaleTimeString();
    // Get current URL as key
    const currentUrl = window.location.href;

    // Throttle function: execute at most once every 10 seconds
    let lastSaveTime = 0;
    const SAVE_INTERVAL = 10000; // 10 seconds
    
    // Flag to indicate if event listener has been added
    let listenerAdded = false;

    // Save function
    function saveChatContent() {
        const currentTime = Date.now();
        
        // Check if within throttle interval
        if (currentTime - lastSaveTime < SAVE_INTERVAL) {
            console.log('Save operation throttled, skipping this save');
            return;
        }

        // Get chat container element
        const chatContainer = document.querySelector('div.chat-thread-container');
        
        if (!chatContainer) {
            console.log('chat-thread-container element not found');
            return;
        }

        
        // Get HTML content inside the container
        const htmlContent = chatContainer.innerHTML;
        
        // Save data
        try {
            // Read previously saved data
            let savedData = GM_getValue(currentUrl, null); // currentUrl undefined will not throw error
            
            // If there is previously saved data, parse it as an object; otherwise create a new object
            let dataObject = {};
            if (savedData) {
                try {
                    // If saved data is a string, try to parse it as an object
                    if (typeof savedData === 'string') {
                        dataObject = JSON.parse(savedData);
                    } else {
                        // If it's already an object, use it directly
                        dataObject = savedData;
                    }
                } catch (e) {
                    // If parsing fails, create a new object
                    console.log('Failed to parse old data, creating new object');
                    dataObject = {};
                }
            }
            
            // Add new time key and HTML content
            dataObject[timeKey] = htmlContent;
            
            // Save object
            GM_setValue(currentUrl, dataObject);
            lastSaveTime = currentTime;
            console.log('Chat content saved:', currentUrl);
            console.log('Time key:', timeKey);
            console.log('Saved data object:', dataObject);
        } catch (error) {
            console.error('Save failed:', error);
        }
    }

    // Mouse move event handler (with throttling)
    let mouseMoveTimer = null;
    function handleMouseMove() {
        console.log('handleMouseMove')
        // Clear previous timer
        if (mouseMoveTimer) {
            clearTimeout(mouseMoveTimer);
        }
        
        // Set new timer, delay 100ms before executing save (debounce)
        mouseMoveTimer = setTimeout(() => {
            saveChatContent();
        }, 100);
    }

    // Add mouse move listener (only add once)
    function addMouseMoveListener() {
        if (!listenerAdded) {
            document.addEventListener('mousemove', handleMouseMove, { passive: true });
            listenerAdded = true;
            console.log('Mouse move listener added');

            // Select div.chat-panel-content, insert a div at the beginning with id chat-thread-container-history, containing previously saved data.
            const chatPanelContent = document.querySelector('div.chat-panel-content');
            const chatThreadContainerHistory = document.createElement('div');
            chatThreadContainerHistory.id = 'chat-thread-container-history';
            chatThreadContainerHistory.style.backgroundColor = 'aliceblue';
            chatPanelContent.insertBefore(chatThreadContainerHistory, chatPanelContent.firstChild);
            chatThreadContainerHistory.innerHTML = '';
            
            const savedData = GM_getValue(currentUrl, null);
            console.log('savedData:', savedData);
            
            if (savedData) {
                // If saved data is a string, try to parse it as an object
                let dataObject = savedData;
                if (typeof savedData === 'string') {
                    try {
                        dataObject = JSON.parse(savedData);
                    } catch (e) {
                        console.error('Failed to parse saved data:', e);
                        return;
                    }
                }
                
                // Iterate through each time key in the object
                for (const timeKey in dataObject) {
                    if (dataObject.hasOwnProperty(timeKey)) {
                        chatThreadContainerHistory.innerHTML += dataObject[timeKey];
                        console.log('Inserted time key:', timeKey);
                    }
                }
            } else {
                console.log('No saved data found');
            }
        }
    }

    // Wait for page load to complete before adding mouse move listener
    function init() {
        // Check if element exists
        const chatContainer = document.querySelector('div.chat-thread-container');
        
        if (chatContainer) {
            // Element exists, add mouse move listener (listen to entire document)
            addMouseMoveListener();
            
            // Save immediately on page load
            saveChatContent();
        } else {
            // Element doesn't exist, use MutationObserver to wait for element to appear
            const observer = new MutationObserver((mutations, obs) => {
                const container = document.querySelector('div.chat-thread-container');
                if (container) {
                    addMouseMoveListener();
                    
                    // Save immediately on page load
                    saveChatContent();
                    
                    // Stop observing
                    obs.disconnect();
                }
            });
            
            // Start observing
            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
            
            console.log('Waiting for chat-thread-container element to appear...');
        }
    }

    // Initialize after page load completes
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        // If DOM is already loaded, initialize directly
        init();
    }
    console.log('Script loaded');

})();