NotebookLM shows all citations

Automatically appends source infomation in NotebookLM

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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);
})();