Auto-save NotebookLM chat content, triggers save on mouse movement (at most once every 10 seconds)
As of
// ==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');
})();