NotebookLM Auto Save

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

Versión del día 10/11/2025. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         NotebookLM Auto Save
// @namespace    http://tampermonkey.net/
// @version      1.0.2
// @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
// @grant        GM_listValues
// @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;
    
    // Flag to indicate if download button has been added
    let downloadButtonAdded = 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');
            }
        }
    }

    // Download all saved data
    function downloadAllData() {
        try {
            // Get all keys
            const keys = GM_listValues();
            const allData = {};
            
            // Get value for each key
            keys.forEach(key => {
                const value = GM_getValue(key);
                allData[key] = value;
            });
            
            // Convert data to JSON string (formatted with 2-space indent)
            const jsonData = JSON.stringify(allData, null, 2);
            
            // Create Blob object
            const blob = new Blob([jsonData], {
                type: 'application/json;charset=utf-8'
            });
            
            // Create download link
            const link = document.createElement('a');
            link.href = URL.createObjectURL(blob);
            
            // Generate filename (include current time)
            const now = new Date();
            const year = now.getFullYear();
            const month = String(now.getMonth() + 1).padStart(2, '0');
            const day = String(now.getDate()).padStart(2, '0');
            const hour = String(now.getHours()).padStart(2, '0');
            const minute = String(now.getMinutes()).padStart(2, '0');
            const second = String(now.getSeconds()).padStart(2, '0');
            const currentDate = `${year}${month}${day}_${hour}${minute}${second}`;
            link.download = `notebooklm_saved_data_${currentDate}.json`;
            
            // Trigger download
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
            
            // Release URL object
            URL.revokeObjectURL(link.href);
            
            console.log('Data download completed, file:', link.download);
            console.log('Downloaded', keys.length, 'items');
        } catch (error) {
            console.error('Error during download:', error);
            alert('Download failed: ' + error.message);
        }
    }
    
    // Create download button
    function createDownloadButton() {
        if (downloadButtonAdded) {
            return;
        }
        
        // Create button element
        const downloadBtn = document.createElement('button');
        downloadBtn.textContent = 'Download';
        downloadBtn.id = 'notebooklm-download-btn';
        
        // Set button styles
        downloadBtn.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            z-index: 10000;
            padding: 10px 20px;
            background-color: #4285f4;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            font-weight: 500;
            box-shadow: 0 2px 4px rgba(0,0,0,0.2);
            transition: background-color 0.3s;
        `;
        
        // Mouse hover effect
        downloadBtn.addEventListener('mouseenter', () => {
            downloadBtn.style.backgroundColor = '#357ae8';
        });
        downloadBtn.addEventListener('mouseleave', () => {
            downloadBtn.style.backgroundColor = '#4285f4';
        });
        
        // Click event
        downloadBtn.addEventListener('click', () => {
            downloadAllData();
        });
        
        // Add button to page
        document.body.appendChild(downloadBtn);
        downloadButtonAdded = true;
        console.log('Download button added');
    }

    // 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', () => {
            // Create download button immediately (does not depend on chat container)
            createDownloadButton();
            init();
        });
    } else {
        // If DOM is already loaded, initialize directly
        // Create download button immediately (does not depend on chat container)
        createDownloadButton();
        init();
    }
    console.log('Script loaded');

})();