NotebookLM Auto Save

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

Fra og med 09.11.2025. Se den nyeste version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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');

})();