Better NotebookLM

Enhanced NotebookLM: Auto-collapse sidebar, resizable Studio panel, UI improvements

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 or Violentmonkey 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         Better NotebookLM
// @namespace    http://tampermonkey.net/
// @version      0.6
// @description  Enhanced NotebookLM: Auto-collapse sidebar, resizable Studio panel, UI improvements
// @author       djshigel
// @license      MIT
// @match        https://notebooklm.google.com/*
// @match        https://notebooklm.google/*
// @icon         https://notebooklm.google.com/favicon.ico
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    const CONFIG = {
        defaultStudioWidth: 30, // Default Studio panel width (%)
        expandStudioWidth: 53, // Width to expand Studio panel when min-inline-size is detected (%)
        minPanelWidth: 20, // Minimum panel width (%)
        maxPanelWidth: 80, // Maximum panel width (%)
        dragHandleWidth: 8, // Drag handle width (px)
        rightPanelOffset: 88, // Fixed offset to prevent right panel overflow (px)
    };

    // ========================
    // Auto-collapse sidebar
    // ========================
    
    function collapseSidebar() {
        const panel = document.querySelector('section.source-panel:not(.panel-collapsed)');
        
        if (panel) {
            const toggleButton = panel.querySelector('button.toggle-source-panel-button');
            
            if (toggleButton) {
                toggleButton.click();
                console.log('Better NotebookLM: Sidebar collapsed');
                return true;
            }
        }
        
        const collapsedPanel = document.querySelector('section.source-panel.panel-collapsed');
        if (collapsedPanel) {
            return true;
        }
        
        return false;
    }

    // ========================
    // Studio panel resizer
    // ========================
    
    let dragHandlers = {
        mousedown: null,
        mousemove: null,
        mouseup: null
    };
    
    let resizerInitialized = false;
    
    function destroyStudioResizer() {
        const studioPanel = document.querySelector('section.studio-panel, [class*="studio"], [class*="right-panel"]');
        const gutter = studioPanel?.previousElementSibling;
        const chatPanel = gutter?.previousElementSibling;
        
        if (gutter && gutter.dataset.resizerInitialized) {
            // Remove event listeners
            if (dragHandlers.mousedown) {
                gutter.removeEventListener('mousedown', dragHandlers.mousedown);
            }
            if (dragHandlers.mousemove) {
                document.removeEventListener('mousemove', dragHandlers.mousemove);
            }
            if (dragHandlers.mouseup) {
                document.removeEventListener('mouseup', dragHandlers.mouseup);
            }
            
            // Reset gutter styles
            gutter.style.cursor = '';
            gutter.style.backgroundColor = '';
            gutter.style.width = '';
            delete gutter.dataset.resizerInitialized;
            
            // Remove ALL custom styles from panels
            if (studioPanel) {
                studioPanel.removeAttribute('style');
            }
            if (chatPanel) {
                chatPanel.removeAttribute('style');
            }
            
            // Reset initialization flag
            resizerInitialized = false;
            
            console.log('Better NotebookLM: Studio resizer destroyed');
        }
    }
    
    function observeStudioCollapse() {
        let lastCollapseState = null;
        
        const observer = new MutationObserver(() => {
            const studioPanel = document.querySelector('section.studio-panel, [class*="studio"], [class*="right-panel"]');
            if (!studioPanel) return;
            
            // Check for min-inline-size and handle appropriately
            if (studioPanel.style.minInlineSize) {
                const minInlineValue = studioPanel.style.minInlineSize;
                
                // Always remove min-inline-size
                studioPanel.style.minInlineSize = '';
                
                // Only expand for specific large values (like 37.5vw that NotebookLM uses)
                // This indicates actual content like video or report is being shown
                if ((minInlineValue.includes('vw') && parseFloat(minInlineValue) >= 30) ||
                    (minInlineValue.includes('%') && parseFloat(minInlineValue) >= 40)) {
                    
                    const chatPanel = document.querySelector('section.chat-panel');
                    if (!studioPanel.classList.contains('panel-collapsed') && chatPanel) {
                        // Add transition for smooth animation
                        studioPanel.style.transition = 'all 0.3s ease';
                        chatPanel.style.transition = 'all 0.3s ease';
                        
                        // Apply expanded width
                        studioPanel.style.flex = '';
                        studioPanel.style.maxWidth = `calc(${CONFIG.expandStudioWidth}% - ${CONFIG.rightPanelOffset}px)`;
                        studioPanel.style.width = `calc(${CONFIG.expandStudioWidth}% - ${CONFIG.rightPanelOffset}px)`;
                        chatPanel.style.flex = `0 0 ${100 - CONFIG.expandStudioWidth}%`;
                        chatPanel.style.maxWidth = `${100 - CONFIG.expandStudioWidth}%`;
                        
                        // Save the expanded width
                        localStorage.setItem('betterNotebookLM_studioWidth', CONFIG.expandStudioWidth.toString());
                        
                        // Remove transition after animation
                        setTimeout(() => {
                            studioPanel.style.transition = '';
                            chatPanel.style.transition = '';
                        }, 300);
                        
                        console.log(`Better NotebookLM: Detected ${minInlineValue} - expanded Studio to ${CONFIG.expandStudioWidth}%`);
                    }
                } else {
                    console.log(`Better NotebookLM: Removed min-inline-size (${minInlineValue})`);
                }
            }
            
            const isCollapsed = studioPanel.classList.contains('panel-collapsed');
            
            // Only act if state actually changed
            if (lastCollapseState !== isCollapsed) {
                lastCollapseState = isCollapsed;
                
                if (isCollapsed) {
                    // Destroy resizer when collapsed
                    destroyStudioResizer();
                    
                    // Remove flex and max-width from chat panel when Studio is collapsed
                    const chatPanel = document.querySelector('section.chat-panel');
                    if (chatPanel) {
                        chatPanel.style.flex = '';
                        chatPanel.style.maxWidth = '';
                        console.log('Better NotebookLM: Removed chat panel custom styles on Studio collapse');
                    }
                    
                    console.log('Better NotebookLM: Studio collapsed - resizer destroyed, width forced to 56px');
                } else {
                    // Re-initialize resizer when expanded
                    setTimeout(() => {
                        resizerInitialized = false; // Reset flag before re-initializing
                        resizerInitialized = initStudioResizer();
                        
                        // Restore saved size if available
                        const savedWidth = localStorage.getItem('betterNotebookLM_studioWidth');
                        if (savedWidth && resizerInitialized) {
                            const gutter = studioPanel.previousElementSibling;
                            const chatPanel = gutter?.previousElementSibling;
                            
                            if (chatPanel && studioPanel) {
                                const width = parseFloat(savedWidth);
                                studioPanel.style.flex = '';
                                studioPanel.style.maxWidth = `calc(${width}% - ${CONFIG.rightPanelOffset}px)`;
                                studioPanel.style.width = `calc(${width}% - ${CONFIG.rightPanelOffset}px)`;
                                chatPanel.style.flex = `0 0 ${100 - width}%`;
                                chatPanel.style.maxWidth = `${100 - width}%`;
                                console.log('Better NotebookLM: Studio expanded - resizer and size restored');
                            }
                        } else {
                            console.log('Better NotebookLM: Studio expanded - resizer restored');
                        }
                    }, 100);
                }
            }
        });
        
        // Observe the entire document for class changes
        observer.observe(document.body, {
            attributes: true,
            attributeFilter: ['class', 'style'],
            subtree: true
        });
        
        return observer;
    }
    
    function initStudioResizer() {
        // Find the gutter between chat and Studio panels
        const studioPanel = document.querySelector('section.studio-panel, [class*="studio"], [class*="right-panel"]');
        if (!studioPanel) return false;
        
        // Don't initialize if Studio is collapsed
        if (studioPanel.classList.contains('panel-collapsed')) {
            console.log('Better NotebookLM: Studio is collapsed, skipping resizer init');
            return false;
        }
        
        // Find the gutter that's immediately before the Studio panel
        const gutter = studioPanel.previousElementSibling;
        if (!gutter || !gutter.classList.contains('panel-gutter')) return false;
        
        // Skip if already initialized
        if (gutter.dataset.resizerInitialized) return true;
        
        // Apply resize handle styles
        gutter.style.cursor = 'col-resize';
        gutter.style.position = 'relative';
        gutter.style.userSelect = 'none';
        gutter.style.width = CONFIG.dragHandleWidth + 'px';
        gutter.style.backgroundColor = 'transparent';
        gutter.style.transition = 'background-color 0.2s';
        
        // Visual feedback on hover
        gutter.addEventListener('mouseenter', () => {
            gutter.style.backgroundColor = 'rgba(66, 133, 244, 0.2)';
        });
        
        gutter.addEventListener('mouseleave', () => {
            if (!gutter.dataset.dragging) {
                gutter.style.backgroundColor = 'transparent';
            }
        });
        
        // Drag handling
        let isDragging = false;
        let startX = 0;
        let startRightWidth = 0;
        let chatPanel = null;
        let rightPanel = studioPanel;
        let container = null;
        
        const handleMouseDown = (e) => {
            isDragging = true;
            startX = e.clientX;
            gutter.dataset.dragging = 'true';
            gutter.style.backgroundColor = 'rgba(66, 133, 244, 0.4)';
            
            // Get adjacent panels
            chatPanel = gutter.previousElementSibling;
            container = gutter.parentElement;
            
            if (rightPanel && container) {
                const containerWidth = container.offsetWidth;
                startRightWidth = (rightPanel.offsetWidth / containerWidth) * 100;
            }
            
            // Prevent cursor and text selection during drag
            document.body.style.cursor = 'col-resize';
            document.body.style.userSelect = 'none';
            
            // Add overlay to prevent iframe interference
            const overlay = document.createElement('div');
            overlay.id = 'resize-overlay';
            overlay.style.cssText = `
                position: fixed;
                top: 0;
                left: 0;
                right: 0;
                bottom: 0;
                z-index: 9999;
                cursor: col-resize;
            `;
            document.body.appendChild(overlay);
            
            e.preventDefault();
        };
        
        const handleMouseMove = (e) => {
            if (!isDragging || !chatPanel || !rightPanel || !container) return;
            
            const containerWidth = container.offsetWidth;
            const deltaX = e.clientX - startX;
            const deltaPercent = (deltaX / containerWidth) * 100;
            let newRightWidth = startRightWidth - deltaPercent;
            
            // Limit width
            newRightWidth = Math.max(CONFIG.minPanelWidth, Math.min(CONFIG.maxPanelWidth, newRightWidth));
            const newLeftWidth = 100 - newRightWidth;
            
            // Update panel sizes
            chatPanel.style.flex = '';
            chatPanel.style.maxWidth = `${newLeftWidth}%`;
            rightPanel.style.flex = '';
            rightPanel.style.maxWidth = `calc(${newRightWidth}% - ${CONFIG.rightPanelOffset}px)`;
            rightPanel.style.width = `calc(${newRightWidth}% - ${CONFIG.rightPanelOffset}px)`;
            
            // Save to localStorage
            localStorage.setItem('betterNotebookLM_studioWidth', newRightWidth.toString());
            
            e.preventDefault();
        };
        
        const handleMouseUp = () => {
            if (!isDragging) return;
            
            isDragging = false;
            delete gutter.dataset.dragging;
            gutter.style.backgroundColor = 'transparent';
            
            // Restore cursor and text selection
            document.body.style.cursor = '';
            document.body.style.userSelect = '';
            
            // Remove overlay
            const overlay = document.getElementById('resize-overlay');
            if (overlay) overlay.remove();
        };
        
        // Store handlers globally for cleanup
        dragHandlers.mousedown = handleMouseDown;
        dragHandlers.mousemove = handleMouseMove;
        dragHandlers.mouseup = handleMouseUp;
        
        gutter.addEventListener('mousedown', handleMouseDown);
        document.addEventListener('mousemove', handleMouseMove);
        document.addEventListener('mouseup', handleMouseUp);
        
        // Mark as initialized
        gutter.dataset.resizerInitialized = 'true';
        
        // Apply saved width
        const savedWidth = localStorage.getItem('betterNotebookLM_studioWidth');
        if (savedWidth && chatPanel && rightPanel) {
            const width = parseFloat(savedWidth);
            rightPanel.style.flex = `0 0 ${width}%`;
            rightPanel.style.maxWidth = `calc(${width}% - ${CONFIG.rightPanelOffset}px)`;
            rightPanel.style.width = `calc(${width}% - ${CONFIG.rightPanelOffset}px)`;
            chatPanel.style.flex = `0 0 ${100 - width}%`;
            chatPanel.style.maxWidth = `${100 - width}%`;
        }
        
        console.log('Better NotebookLM: Studio resizer initialized');
        return true;
    }

    // ========================
    // Main processing
    // ========================
    
    let sidebarProcessed = false;
    let retryCount = 0;
    const maxRetries = 20;
    
    function processElements() {
        // Process sidebar
        if (!sidebarProcessed) {
            sidebarProcessed = collapseSidebar();
        }
        
        // Initialize Studio resizer
        if (!resizerInitialized) {
            const studioPanel = document.querySelector('section.studio-panel, [class*="studio"], [class*="right-panel"]');
            
            // If Studio exists and is collapsed, mark as complete but don't initialize
            if (studioPanel && studioPanel.classList.contains('panel-collapsed')) {
                resizerInitialized = true;
                console.log('Better NotebookLM: Studio is collapsed, skipping resizer');
            } else {
                resizerInitialized = initStudioResizer();
            }
        }
        
        // Check completion
        if ((sidebarProcessed && resizerInitialized) || retryCount >= maxRetries) {
            if (retryCount >= maxRetries) {
                console.log('Better NotebookLM: Maximum retries reached');
            } else {
                console.log('Better NotebookLM: All features enabled');
            }
            return true;
        }
        
        return false;
    }
    
    // Periodic element check
    const checkInterval = setInterval(() => {
        retryCount++;
        
        if (processElements()) {
            clearInterval(checkInterval);
            // Start observing Studio collapse state after initialization
            observeStudioCollapse();
        }
    }, 500);
    
    // Handle page navigation
    let lastUrl = location.href;
    const urlObserver = new MutationObserver(() => {
        const currentUrl = location.href;
        if (currentUrl !== lastUrl) {
            lastUrl = currentUrl;
            console.log('Better NotebookLM: Page changed, reinitializing...');
            
            // Reset flags
            sidebarProcessed = false;
            resizerInitialized = false;
            retryCount = 0;
            
            setTimeout(() => {
                const retryInterval = setInterval(() => {
                    retryCount++;
                    
                    if (!sidebarProcessed) {
                        sidebarProcessed = collapseSidebar();
                    }
                    
                    if (!resizerInitialized) {
                        resizerInitialized = initStudioResizer();
                    }
                    
                    if ((sidebarProcessed && resizerInitialized) || retryCount >= 10) {
                        clearInterval(retryInterval);
                        observeStudioCollapse();
                    }
                }, 500);
            }, 1000);
        }
    });
    
    // Monitor URL changes
    urlObserver.observe(document.body, {
        childList: true,
        subtree: true
    });
    
    // ========================
    // Style injection
    // ========================
    
    const style = document.createElement('style');
    style.textContent = `
        /* Force logo margins */
        labs-tailwind-logo img {
            margin-left: 5px !important;
            margin-right: 5px !important;
        }
        
        /* Force Source panel expanded width */
        section.source-panel:not(.panel-collapsed) {
            width: 395px !important;
            min-width: 395px !important;
            max-width: 395px !important;
            flex: 0 0 395px !important;
        }
        
        /* Panel gutter hover effect */
        .panel-gutter[data-resizer-initialized="true"] {
            position: relative;
            z-index: 100;
        }
        
        .panel-gutter[data-resizer-initialized="true"]::before {
            content: '';
            position: absolute;
            top: 0;
            bottom: 0;
            left: 50%;
            transform: translateX(-50%);
            width: 3px;
            background: rgba(66, 133, 244, 0.5);
            opacity: 0;
            transition: opacity 0.2s;
        }
        
        .panel-gutter[data-resizer-initialized="true"]:hover::before {
            opacity: 1;
        }
        
        /* Disable animations during drag to prevent jitter */
        .panel-gutter[data-dragging="true"] ~ *,
        .panel-gutter[data-dragging="true"] ~ * * {
            transition: none !important;
        }
        
        /* Source panel collapsed state - hide scrollbar until hover */
        .source-panel.panel-collapsed .source-panel-content {
            overflow: hidden !important;
        }
        
        .source-panel.panel-collapsed .source-panel-content:hover {
            overflow-y: auto !important;
            overflow-x: hidden !important;
        }
        
        /* Force Studio panel width when collapsed */
        section.studio-panel.panel-collapsed,
        section[class*="studio"].panel-collapsed,
        section[class*="right-panel"].panel-collapsed {
            width: 56px !important;
            min-width: 56px !important;
            max-width: 56px !important;
            flex: 0 0 56px !important;
        }
        
        /* Remove min-inline-size from Studio panel */
        section.studio-panel,
        section[class*="studio"],
        section[class*="right-panel"] {
            min-inline-size: unset !important;
        }
        
        /* Chat panel should use available space when Studio is collapsed */
        section.chat-panel:has(~ .panel-collapsed) {
            flex: 1 1 auto !important;
            max-width: none !important;
        }
        
        /* Sticky Studio panel buttons to prevent overflow */
        .studio-panel button.toggle-studio-panel-button,
        .studio-panel button[aria-label*="Studio"],
        .studio-panel button[aria-label*="メモ"],
        .studio-panel button[aria-label*="note"] {
            position: sticky;
            right: 0;
            z-index: 10;
        }
        
        /* Studio panel header sticky positioning */
        .studio-panel .panel-header,
        .studio-panel .studio-header {
            position: sticky;
            top: 0;
            z-index: 10;
            background: inherit;
        }
    `;
    document.head.appendChild(style);
    
    console.log('Better NotebookLM: Initialization complete');
    
    // Additional monitoring for min-inline-size changes as a backup
    const styleObserver = new MutationObserver(() => {
        const studioPanel = document.querySelector('section.studio-panel, [class*="studio"], [class*="right-panel"]');
        if (studioPanel && studioPanel.style.minInlineSize) {
            const minInlineValue = studioPanel.style.minInlineSize;
            
            // Always remove min-inline-size
            studioPanel.style.minInlineSize = '';
            
            // Only expand for specific large values
            if ((minInlineValue.includes('vw') && parseFloat(minInlineValue) >= 30) ||
                (minInlineValue.includes('%') && parseFloat(minInlineValue) >= 40)) {
                
                const chatPanel = document.querySelector('section.chat-panel');
                if (!studioPanel.classList.contains('panel-collapsed') && chatPanel) {
                    // Add transition for smooth animation
                    studioPanel.style.transition = 'all 0.3s ease';
                    chatPanel.style.transition = 'all 0.3s ease';
                    
                    // Apply expanded width
                    studioPanel.style.flex = `0 0 ${CONFIG.expandStudioWidth}%`;
                    studioPanel.style.maxWidth = `calc(${CONFIG.expandStudioWidth}% - ${CONFIG.rightPanelOffset}px)`;
                    studioPanel.style.width = `calc(${CONFIG.expandStudioWidth}% - ${CONFIG.rightPanelOffset}px)`;
                    chatPanel.style.flex = `0 0 ${100 - CONFIG.expandStudioWidth}% + ${CONFIG.rightPanelOffset}px`;
                    chatPanel.style.maxWidth = `${100 - CONFIG.expandStudioWidth}%`;
                    
                    // Save the expanded width
                    localStorage.setItem('betterNotebookLM_studioWidth', CONFIG.expandStudioWidth.toString());
                    
                    // Remove transition after animation
                    setTimeout(() => {
                        studioPanel.style.transition = '';
                        chatPanel.style.transition = '';
                    }, 300);
                    
                    console.log(`Better NotebookLM (backup): Detected ${minInlineValue} - expanded to ${CONFIG.expandStudioWidth}%`);
                }
            }
        }
    });
    
    // Start monitoring after initial setup
    setTimeout(() => {
        const studioPanel = document.querySelector('section.studio-panel, [class*="studio"], [class*="right-panel"]');
        if (studioPanel) {
            styleObserver.observe(studioPanel, {
                attributes: true,
                attributeFilter: ['style']
            });
        }
    }, 2000);
    
})();