Automatically appends source infomation in NotebookLM
// ==UserScript==
// @name NotebookLM shows all citations
// @namespace http://tampermonkey.net/
// @version 1.8
// @description Automatically appends source infomation in NotebookLM
// @author Bui Quoc Dung
// @match https://notebooklm.google.com/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
const delay = ms => new Promise(res => setTimeout(res, ms));
const SUPPORTED_EXTS = ['.pdf', '.txt', '.md', '.markdown', '.mp3', '.docx'];
const isSupportedFile = text => SUPPORTED_EXTS.some(ext => text.trim().toLowerCase().endsWith(ext));
const extractFileName = text => text.trim().replace(/\.(pdf|txt|md|markdown|mp3|docx)$/i, '');
function getFileFromButton(btn) {
const span = btn.querySelector('span[aria-label]');
if (!span) return null;
const label = span.getAttribute('aria-label') || '';
const match = label.match(/(\d+: )?(.+\.(pdf|txt|md|markdown|mp3|docx))/i);
if (match && isSupportedFile(match[2])) return extractFileName(match[2]);
return null;
}
async function processCitationButton(button) {
if (button.dataset.processed === "true" || button.dataset.processing === "true") return;
const text = button.textContent.trim();
if (text === '> <') {
button.dataset.processed = "true";
return;
}
button.dataset.processing = "true";
if (text === '...') {
button.click();
await delay(1000);
const parent = button.closest('span')?.parentElement;
const found = new Set();
if (parent) {
const newBtns = Array.from(parent.querySelectorAll('button.citation-marker'))
.filter(b => b !== button && !['...', '> <'].includes(b.textContent.trim()));
for (const b of newBtns) {
const name = getFileFromButton(b);
if (name) found.add(name);
}
}
if (found.size > 0) {
const textNode = document.createElement('span');
textNode.textContent = ` [${Array.from(found).join('][')}]`;
textNode.style.cssText = "font-style:italic;margin-left:2px;font-size:0.9em;color:#666;";
button.parentNode.replaceChild(textNode, button);
}
button.dataset.processed = "true";
button.dataset.processing = "false";
return;
}
const fileName = getFileFromButton(button);
if (fileName) {
const textNode = document.createElement('span');
textNode.textContent = ` [${fileName}]`;
textNode.style.cssText = "font-style:italic;margin-left:2px;font-size:0.9em;color:#666;";
button.parentNode.replaceChild(textNode, button);
button.dataset.processed = "true";
}
button.dataset.processing = "false";
}
async function processAllCitations() {
const buttons = Array.from(document.querySelectorAll('button.citation-marker'))
.filter(btn => !btn.dataset.processed && !btn.dataset.processing);
if (buttons.length === 0) return;
for (const btn of buttons) {
await processCitationButton(btn);
await delay(300);
}
setTimeout(processAllCitations, 1500);
}
function collectAllCitations() {
const spans = document.querySelectorAll('span');
const citations = new Set();
spans.forEach(span => {
const match = span.textContent.match(/\[([^\]]+)\]/g);
if (match) {
match.forEach(m => {
const name = m.replace(/[\[\]]/g, '').trim();
if (name) citations.add(name);
});
}
});
return Array.from(citations).sort().join('\n');
}
function addCopyButtons() {
const containers = document.querySelectorAll('.mat-mdc-card-content.message-content.to-user-message-inner-content');
const baseButtonStyle = `
padding:4px 10px;
font-size:0.85em;
border:none;
border-radius:6px;
background:#f4f4f4;
cursor:pointer;
align-self:flex-end;
transition:background 0.2s, opacity 0.2s;
`;
containers.forEach(container => {
if (container.dataset.copyAdded === "true") return;
const wrap = document.createElement('div');
wrap.style.cssText = "display:flex;gap:6px;margin-top:10px;";
const btnCopy = document.createElement('button');
btnCopy.textContent = 'Copy';
btnCopy.style.cssText = baseButtonStyle;
btnCopy.addEventListener('click', async () => {
try {
const text = container.innerText.trim();
await navigator.clipboard.writeText(text);
btnCopy.textContent = 'Copying';
setTimeout(() => btnCopy.textContent = 'Copy', 1200);
} catch (err) {
console.error('Copy failed:', err);
btnCopy.textContent = 'Error';
setTimeout(() => btnCopy.textContent = 'Copy', 1200);
}
});
const btnBib = document.createElement('button');
btnBib.textContent = 'Bibliography';
btnBib.style.cssText = baseButtonStyle;
btnBib.addEventListener('click', async () => {
const bibText = collectAllCitations();
if (!bibText.trim()) {
btnBib.textContent = 'No Citations';
setTimeout(() => btnBib.textContent = 'Bibliography', 1500);
return;
}
try {
await navigator.clipboard.writeText(bibText);
btnBib.textContent = 'Copying';
setTimeout(() => btnBib.textContent = 'Bibliography', 1500);
} catch (err) {
console.error('Bibliography copy failed:', err);
btnBib.textContent = 'Error';
setTimeout(() => btnBib.textContent = 'Bibliography', 1500);
}
});
wrap.appendChild(btnCopy);
wrap.appendChild(btnBib);
container.appendChild(wrap);
container.dataset.copyAdded = "true";
});
}
const observer = new MutationObserver(() => {
setTimeout(processAllCitations, 800);
setTimeout(addCopyButtons, 1000);
});
observer.observe(document.body, { childList: true, subtree: true });
setTimeout(() => {
processAllCitations();
addCopyButtons();
}, 3000);
})();