// ==UserScript==
// @name Better NotebookLM
// @namespace http://tampermonkey.net/
// @version 0.5
// @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}%`;
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);
})();