NotebookLMのボタンを押す作業の自動化
※このChrome拡張を使用した際の不利益については、自己責任でお願いします。
※2026/6/14に動いてることを確認しましたが、今後うごかなくなる可能性はあります。
GoogleのAIノートブックツール「NotebookLM」の画面上で、資料(ソース)の切り替えや各種生成ボタン(音声解説、クイズ、レポートなど)のクリックをワンクリックで自動実行してくれるGoogle Chromeの拡張機能(プラグイン)です。
プログラミングの知識がない初心者の方でも理解できるように、「この拡張機能が何をするものか(仕様)」と「実際に自分のパソコンに導入して使う方法(使い方)」を分かりやすく解説します。
🛠️ この拡張機能の仕様(何ができるの?)
NotebookLMで複数の資料から「音声」や「クイズ」を一つずつ作っていくのは、クリック数が多くて面倒な作業です。この拡張機能は、その作業を楽にするための「専用のリモコン画面(フローティングパネル)」を表示します。
主要な機能
自動でNotebookLMの画面を認識
NotebookLMのページ(notebooklm.google.com)を開いているときだけ動作します。
ボタンの自動クリック機能
画面上にある以下のボタンを自動で探して、順番にクリックしてくれます。
音声解説 / 動画解説 / スライド / マインドマップ / レポート / フラッシュカード / クイズ / インフォグラフィック / DataTable
ソース(資料)の自動切り替え
読み込んでいる資料が複数ある場合、指定した1つの資料だけにチェックを入れ、他のチェックを外すという「切り替え作業」を自動で行います。
ポップアップへの自動対応
「動画解説」や「レポート」を押した際に出現する「生成」などの確認ボタンも、自動で追いかけてクリックします。
ログの保存(CSV出力)
どの資料に対して、何のボタンを押したかの記録(ログ)が画面に表示され、最後にCSVファイルとしてダウンロードできます。
💻 使い方(初心者向け導入ステップ)
この拡張機能はまだChromeストアに公開されていない「自作の拡張機能」の状態です。以下の手順であなたのパソコンのChromeに読み込ませることができます。
ステップ 1:パソコンにフォルダとファイルを作る
デスクトップなど、どこでも良いのでパソコンに新しいフォルダを作ります。名前は NotebookLM-Automator など何でも構いません。
そのフォルダの中に、以下の3つのテキストファイルを作って、提示されたコードをそのまま貼り付けて保存してください。
manifest.json (設定ファイル)
background.js (裏側で動くシステム)
panel.js (リモコン画面を作るシステム)
※提示されたコードの3つ目は code:panel.json と書かれていますが、中身はJavaScriptなので、ファイルの拡張子は必ず .js(panel.js)にして保存してください。
⚠️ 注意点:ファイル名や拡張子(.json や .js)が間違っていると動きません。メモ帳などで保存する際は、ファイルの種類を「すべてのファイル」にして保存してください。
ステップ 2:Chromeに拡張機能を読み込ませる
Google Chromeを開き、アドレスバーに chrome://extensions/ と入力してEnterキーを押します(拡張機能の管理画面が開きます)。
画面右上にある「デベロッパー モード」のスイッチをオン(ON)にします。
画面左上に表示される「展開されていない拡張機能を読み込む」というボタンをクリックします。
ステップ1で作ったフォルダを選択して読み込ませます。
画面に「NotebookLM Studio Automator」というカードが表示されれば成功です!
ステップ 3:実際に使ってみる
NotebookLM( https://notebooklm.google.com/* )にアクセスし、作業したいノートブックを開きます。
Chromeの画面右上にある「パズルピースのアイコン(拡張機能)」をクリックし、今回入れた 「NotebookLM Studio Automator」 のアイコンをクリックします。
画面右上に 「🤖 Studio Automator v6」 という黒いリモコン画面(パネル)が出現します。
※このパネルは、上のヘッダー部分をドラッグすることで画面内の好きな場所に動かせます。
💡 操作の手順
① ソースを選択(1つ) の欄に、現在読み込まれている資料の一覧が自動で表示されます。作業したい資料を1つクリックして選択します(青く囲まれます)。
※うまく表示されない場合は「↺ 再取得」ボタンを押してください。
② 実行するボタン の欄で、自動でクリックしてほしい機能(音声解説やクイズなど)をON(青色)にします。不要なものはクリックしてOFF(灰色)にできます。
準備ができたら、青い 「▶ 実行する」 ボタンを押します。
拡張機能が自動で「資料のチェックを切り替え」→「選んだボタンを順番にクリック」してくれます。進行状況は一番下の「ログ」にリアルタイムで表示されます。
1つの資料の処理が終わったら、また次の資料を一覧から選んで「実行する」を繰り返します。
使わなくなったら、パネルの右上にある「✕」ボタンを押せば閉じることができます。再度アイコンを押せばいつでも表示されます。
code:background.js
// 拡張アイコンをクリックしたらフローティングパネルをページに注入
chrome.action.onClicked.addListener(async (tab) => {
if (!tab.url?.includes('notebooklm.google.com')) return;
await chrome.scripting.executeScript({
target: { tabId: tab.id },
files: 'panel.js'
});
});
code:manifest.json
{
"manifest_version": 3,
"name": "NotebookLM Studio Automator",
"version": "6.0",
"description": "NotebookLMにフローティングパネルを表示して操作します。",
"permissions": [
"activeTab",
"scripting",
"storage"
],
"host_permissions": [
"https://notebooklm.google.com/*"
],
"action": {
"default_popup": ""
},
"background": {
"service_worker": "background.js"
}
}
code:panel.json
// ============================================================
// panel.js — NotebookLM ページに直接注入するフローティングUI
// アイコンクリックのたびに呼ばれる。既存パネルがあればトグル。
// ============================================================
(function() {
'use strict';
const PANEL_ID = 'nlm-auto-panel-v6';
// 既存パネルがあればトグル(表示/非表示)
const existing = document.getElementById(PANEL_ID);
if (existing) {
existing.style.display = existing.style.display === 'none' ? 'flex' : 'none';
return;
}
// ============================================================
// ボタン定義
// ============================================================
const BUTTON_DEFS = [
{ key:'audio', label:'音声解説', hasPopup:false, texts:'音声解説','Audio Overview','音声' },
{ key:'video', label:'動画解説', hasPopup:'video', texts:'動画解説','Video Overview','動画' },
{ key:'slide', label:'スライド', hasPopup:false, texts:'スライド資料','Slides','スライド' },
{ key:'mindmap', label:'マインドマップ', hasPopup:false, texts:'マインドマップ','Mind Map' },
{ key:'report', label:'レポート', hasPopup:'report', texts:'レポート','Report' },
{ key:'flash', label:'フラッシュカード', hasPopup:false, texts:'フラッシュカード','Flashcards' },
{ key:'quiz', label:'クイズ', hasPopup:false, texts:'クイズ','Quiz' },
{ key:'infograph', label:'インフォグラフィック', hasPopup:false, texts:'インフォグラフィック','Infographic' },
{ key:'datatable', label:'DataTable', hasPopup:false, texts:'DataTable','Data Table','データテーブル' },
];
// ============================================================
// 状態
// ============================================================
let selectedIdx = -1;
let selectedName = '';
let running = false;
let allOn = true;
let doneSources = new Set();
let logHistory = [];
let enabledKeys = new Set(BUTTON_DEFS.map(b => b.key));
const sleep = ms => new Promise(r => setTimeout(r, ms));
// ============================================================
// パネルHTML生成 & DOM挿入
// ============================================================
const panel = document.createElement('div');
panel.id = PANEL_ID;
panel.innerHTML = `
<div id="nlm-drag-handle" style="
padding:8px 10px 6px;
background:#0f172a;
border-radius:8px 8px 0 0;
cursor:grab;
display:flex;
justify-content:space-between;
align-items:center;
user-select:none;
border-bottom:1px solid #1e293b;
">
<span style="font-size:12px;font-weight:700;color:#fff">🤖 Studio Automator v6</span>
<span id="nlm-close-btn" style="color:#6b7280;cursor:pointer;font-size:16px;line-height:1;padding:0 2px">✕</span>
</div>
<div style="padding:10px;overflow-y:auto;flex:1">
<!-- ソース一覧 -->
<div style="margin-bottom:8px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:5px">
<span style="font-size:9px;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.8px">① ソースを選択(1つ)</span>
<button id="nlm-refresh" style="font-size:10px;color:#3b82f6;background:none;border:none;cursor:pointer">↺ 再取得</button>
</div>
<div id="nlm-source-list" style="max-height:140px;overflow-y:auto;background:#0f172a;border-radius:6px;padding:4px"></div>
</div>
<!-- ボタントグル -->
<div style="margin-bottom:8px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:5px">
<span style="font-size:9px;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.8px">② 実行するボタン</span>
<button id="nlm-all-toggle" style="font-size:10px;color:#3b82f6;background:none;border:none;cursor:pointer">全ON/OFF</button>
</div>
<div id="nlm-btn-grid" style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:4px"></div>
</div>
<!-- 実行 -->
<button id="nlm-start" style="
width:100%;padding:8px;border:none;border-radius:6px;
background:linear-gradient(135deg,#2563eb,#3b82f6);
color:#fff;font-size:12px;font-weight:700;cursor:pointer;
margin-bottom:8px;opacity:0.4;pointer-events:none;
">▶ 実行する</button>
<!-- ログ -->
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
<span style="font-size:9px;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.8px">ログ</span>
<div>
<button id="nlm-csv" style="font-size:10px;color:#4b5563;background:none;border:none;cursor:pointer;margin-right:6px">📥 CSV</button>
<button id="nlm-clear" style="font-size:10px;color:#4b5563;background:none;border:none;cursor:pointer">クリア</button>
</div>
</div>
<div id="nlm-log" style="
background:#0d111a;border:1px solid #1e293b;border-radius:6px;
height:100px;overflow-y:auto;padding:6px;
font-size:10px;font-family:monospace;line-height:1.6;
"></div>
</div>
`;
Object.assign(panel.style, {
position: 'fixed',
top: '60px',
right: '20px',
width: '300px',
maxHeight: '85vh',
background: '#111827',
border: '1px solid #1e293b',
borderRadius: '8px',
boxShadow: '0 8px 32px rgba(0,0,0,.6)',
zIndex: '2147483647',
display: 'flex',
flexDirection: 'column',
fontFamily: "'Segoe UI',system-ui,sans-serif",
color: '#e0e0e0',
fontSize: '12px',
});
document.body.appendChild(panel);
// ============================================================
// ドラッグ移動(ページ内なので自由に動かせる)
// ============================================================
const handle = document.getElementById('nlm-drag-handle');
let isDragging = false, dragStartX, dragStartY, panelStartX, panelStartY;
handle.addEventListener('mousedown', e => {
isDragging = true;
dragStartX = e.clientX;
dragStartY = e.clientY;
const rect = panel.getBoundingClientRect();
panelStartX = rect.left;
panelStartY = rect.top;
handle.style.cursor = 'grabbing';
e.preventDefault();
});
document.addEventListener('mousemove', e => {
if (!isDragging) return;
const dx = e.clientX - dragStartX;
const dy = e.clientY - dragStartY;
panel.style.left = (panelStartX + dx) + 'px';
panel.style.top = (panelStartY + dy) + 'px';
panel.style.right = 'auto';
});
document.addEventListener('mouseup', () => {
isDragging = false;
handle.style.cursor = 'grab';
});
// 閉じるボタン
document.getElementById('nlm-close-btn').addEventListener('click', () => {
panel.style.display = 'none';
});
// ============================================================
// ログ
// ============================================================
function log(msg, color = '#4b5563') {
const box = document.getElementById('nlm-log');
const el = document.createElement('div');
el.style.color = color;
el.style.marginBottom = '2px';
const t = new Date().toLocaleTimeString('ja-JP', { hour12: false });
el.textContent = ${t} ${msg};
box.appendChild(el);
box.scrollTop = box.scrollHeight;
logHistory.push({ time: t, source: selectedName, message: msg });
}
// ============================================================
// ボタングリッド
// ============================================================
function renderBtnGrid(statuses = {}) {
const grid = document.getElementById('nlm-btn-grid');
grid.innerHTML = BUTTON_DEFS.map(b => {
const on = enabledKeys.has(b.key);
const st = statusesb.key || '';
const dotColor = st === 'done' ? '#22c55e' : st === 'skip' ? '#f59e0b' : on ? '#3b82f6' : '#374151';
const bg = on ? '#1e3a5f' : '#0f172a';
const border = st === 'done' ? '#22c55e' : st === 'skip' ? '#f59e0b' : on ? '#1d4ed8' : '#1e293b';
const txtColor = st === 'done' ? '#86efac' : st === 'skip' ? '#fbbf24' : on ? '#93c5fd' : '#6b7280';
const pp = b.hasPopup ? '💬' : '';
return `<div data-key="${b.key}" style="
display:flex;align-items:center;gap:3px;
padding:3px 5px;border-radius:4px;cursor:pointer;
font-size:10px;color:${txtColor};
border:1.5px solid ${border};background:${bg};
white-space:nowrap;overflow:hidden;
${st==='active'?'animation:nlm-pulse .9s infinite':''}
"><span style="
width:6px;height:6px;border-radius:50%;
background:${dotColor};flex-shrink:0;display:inline-block;
"></span>${b.label}${pp}</div>`;
}).join('');
grid.querySelectorAll('data-key').forEach(el => {
el.addEventListener('click', () => {
if (running) return;
const k = el.dataset.key;
enabledKeys.has(k) ? enabledKeys.delete(k) : enabledKeys.add(k);
renderBtnGrid();
});
});
}
function setChip(key, status) {
renderBtnGrid(Object.fromEntries(BUTTON_DEFS.map(b => b.key, b.key === key ? status : '')));
}
renderBtnGrid();
// ============================================================
// ソース一覧
// ============================================================
function getSourceCheckboxes() {
return Array.from(document.querySelectorAll(
'inputtype="checkbox", input.mdc-checkbox__native-control'
)).filter(cb => {
const id = cb.id || '';
const label = cb.getAttribute('aria-label') || '';
return (
id !== 'mat-mdc-checkbox-0-input' &&
!label.includes('すべて') &&
!label.includes('Select') &&
!id.includes('pdf-note')
);
});
}
function getSourceName(cb, index) {
// ソースリストのアイテムを直接取得する — チェックボックスの親ではなく
// ページ上の「sources-list」内の全アイテムと index で対応付ける
const sourceListEl = document.querySelector(
'sources-list, class*="source-list", class*="sources-list"'
);
if (sourceListEl) {
// mat-list-item を全取得 (最初の1個はヘッダー行の場合あり)
const items = Array.from(sourceListEl.querySelectorAll('mat-list-item, role="listitem"'));
// チェックボックスを持つアイテムだけに絞る
const sourceItems = items.filter(item => item.querySelector('inputtype="checkbox"'));
const item = sourceItemsindex;
if (item) {
// テキストノードを直接持つ span を探す(Material icon テキストを除く)
const spans = Array.from(item.querySelectorAll('span'));
for (const sp of spans) {
// 直接のテキストノードのみ
const directText = Array.from(sp.childNodes)
.filter(n => n.nodeType === Node.TEXT_NODE)
.map(n => n.textContent.trim())
.join('').trim();
if (directText.length > 2 && !/^a-z_\s+$/.test(directText)) {
return directText.slice(0, 60);
}
}
// フォールバック: title 属性
const titleEl = item.querySelector('title');
if (titleEl) return titleEl.getAttribute('title').slice(0, 60);
}
}
// 最終フォールバック: チェックボックス自体の aria-label
const aria = cb.getAttribute('aria-label') || cb.getAttribute('aria-labelledby') || '';
if (aria.length > 2) return aria.slice(0, 60);
return ソース #${index + 1};
}
function fetchSources() {
const listEl = document.getElementById('nlm-source-list');
listEl.innerHTML = '<div style="color:#4b5563;padding:4px;font-size:10px">読み込み中...</div>';
const checks = getSourceCheckboxes();
if (checks.length === 0) {
listEl.innerHTML = '<div style="color:#f87171;padding:4px;font-size:10px">ソースが見つかりません</div>';
return;
}
const sources = checks.map((cb, i) => ({ index: i, text: getSourceName(cb, i), checked: cb.checked }));
// ツールチップ要素(1つ作って使い回す)
let nlmTip = document.getElementById('nlm-tooltip');
if (!nlmTip) {
nlmTip = document.createElement('div');
nlmTip.id = 'nlm-tooltip';
Object.assign(nlmTip.style, {
position: 'fixed', zIndex: '2147483647',
background: '#0f172a', color: '#e0e0e0',
border: '1px solid #3b82f6', borderRadius: '5px',
padding: '5px 8px', fontSize: '11px',
maxWidth: '320px', wordBreak: 'break-all',
pointerEvents: 'none', display: 'none',
boxShadow: '0 4px 12px rgba(0,0,0,.5)',
lineHeight: '1.5',
});
document.body.appendChild(nlmTip);
}
listEl.innerHTML = sources.map(s => {
const done = doneSources.has(s.text);
const bg = done ? 'opacity:.4;pointer-events:none' : '';
// 末尾が見えるよう direction:rtl で末尾から表示
return `<div data-i="${s.index}" data-n="${s.text.replace(/"/g,'&quot;')}" style="
display:flex;align-items:center;gap:6px;
padding:4px 6px;border-radius:4px;cursor:pointer;
font-size:10px;color:#9ca3af;
border:1.5px solid transparent;
${bg}
">
<span style="font-size:9px;color:#374151;min-width:16px;text-align:right;flex-shrink:0">${s.index+1}</span>
<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;direction:rtl;text-align:left;unicode-bidi:plaintext">${s.text}</span>
${done ? '<span style="color:#4ade80;font-size:9px;flex-shrink:0">✓</span>' : ''}
</div>`;
}).join('');
listEl.querySelectorAll('data-i').forEach(item => {
const fullName = item.dataset.n;
item.addEventListener('mouseenter', e => {
if (!item.style.opacity) item.style.background = '#1e293b';
// ツールチップ表示
nlmTip.textContent = fullName;
nlmTip.style.display = 'block';
// パネルの左側に表示(パネルが右側にある想定)
const r = item.getBoundingClientRect();
const tipLeft = r.left - 10 - 320;
nlmTip.style.left = Math.max(4, tipLeft) + 'px';
nlmTip.style.top = Math.max(4, r.top) + 'px';
});
item.addEventListener('mousemove', e => {
const r = item.getBoundingClientRect();
nlmTip.style.top = Math.max(4, r.top) + 'px';
});
item.addEventListener('mouseleave', () => {
item.style.background = '';
nlmTip.style.display = 'none';
});
item.addEventListener('click', () => {
if (running) return;
listEl.querySelectorAll('data-i').forEach(x => {
x.style.background = '';
x.style.border = '1.5px solid transparent';
x.style.color = '#9ca3af';
});
item.style.background = '#1e3a5f';
item.style.border = '1.5px solid #3b82f6';
item.style.color = '#fff';
selectedIdx = parseInt(item.dataset.i);
selectedName = item.dataset.n;
const btn = document.getElementById('nlm-start');
btn.style.opacity = '1';
btn.style.pointerEvents = 'auto';
});
});
log(ソース ${sources.length} 件取得, '#4ade80');
}
// ============================================================
// ソース選択(ブックマークレットと同じ個別ON/OFF)
// ============================================================
async function selectOnlySource(sourceIndex) {
const checks = getSourceCheckboxes();
if (!checkssourceIndex) return false;
log(ソースを切り替え中...);
for (let i = 0; i < checks.length; i++) {
const cb = checksi;
if (i === sourceIndex) { if (!cb.checked) { cb.click(); await sleep(200); } }
else { if (cb.checked) { cb.click(); await sleep(150); } }
}
await sleep(1500);
log(✓ ソース選択完了, '#4ade80');
return true;
}
// ============================================================
// Studio ボタンクリック
// ============================================================
function findStudioButton(texts) {
const panel = document.querySelector('section.studio-panel');
const root = panel || document.body;
// 方法A: span.default-container テキスト照合
for (const dc of root.querySelectorAll('span.default-container')) {
const t = (dc.textContent || '').trim().replace(/\s+/g, '');
if (texts.some(txt => t.includes(txt.replace(/\s+/g, '')))) {
const target = dc.closest('div.mat-mdc-tooltip-trigger') || dc.closest('button') || dc.parentElement;
if (target && !target.disabled) return target;
}
}
// 方法B: aria-label
for (const el of root.querySelectorAll('aria-label')) {
const a = (el.getAttribute('aria-label') || '').trim();
if (texts.some(t => a.includes(t)) && !el.disabled) return el;
}
// 方法C: button テキスト
for (const el of root.querySelectorAll('button')) {
const t = (el.textContent || '').trim().replace(/\s+/g, '');
if (texts.some(txt => t.includes(txt.replace(/\s+/g, ''))) && !el.disabled) return el;
}
return null;
}
// ============================================================
// 確認ポップアップ処理
// ============================================================
async function handlePopup(type) {
// ポップアップが表示されるまで少し待つ
const deadline = Date.now() + 5000;
if (type === 'report') {
// DOM確認済み: button.primary-action-buttonaria-label="概要説明資料"
const FORMAT = '概要説明資料';
let card = null;
while (Date.now() < deadline) {
// 方法A: 確認済みのクラス + aria-label で一発特定
card = document.querySelector('button.primary-action-buttonaria-label="' + FORMAT + '"');
// 方法B: primary-action-button の中でテキスト一致
if (!card) {
card = Array.from(document.querySelectorAll('button.primary-action-button')).find(b => {
const aria = (b.getAttribute('aria-label') || '').trim();
const text = (b.textContent || '').trim();
return aria === FORMAT || text.startsWith(FORMAT);
});
}
// 方法C: aria-label だけで探す(クラスが変わった場合の保険)
if (!card) {
card = document.querySelector('aria-label="' + FORMAT + '"');
}
if (card && !card.disabled) break;
card = null;
await sleep(300);
}
if (card) {
card.click();
log(' → 「' + FORMAT + '」をクリック(生成開始)', '#4ade80');
} else {
log(' → 「' + FORMAT + '」ボタンが見つかりません', '#fbbf24');
}
return;
}
if (type === 'video') {
// 動画: 「生成」ボタンをクリック
while (Date.now() < deadline) {
const all = Array.from(document.querySelectorAll('button, role="button"'));
const btn = all.find(b => {
const t = (b.textContent || '').trim();
return (t === '生成' || t === 'Generate') && !b.disabled && !b.hasAttribute('disabled');
});
if (btn) {
btn.click();
log( → 「生成」ボタンをクリック, '#4ade80');
return;
}
await sleep(300);
}
log( → 「生成」ボタンが見つかりません, '#fbbf24');
}
}
// ============================================================
// メインシーケンス
// ============================================================
async function runSequence() {
running = true;
document.getElementById('nlm-start').style.opacity = '0.4';
document.getElementById('nlm-start').style.pointerEvents = 'none';
log(━━ 開始: ${selectedName} ━━, '#374151');
const ok = await selectOnlySource(selectedIdx);
if (!ok) { log('ソース選択失敗', '#f87171'); finishSequence(); return; }
const targets = BUTTON_DEFS.filter(b => enabledKeys.has(b.key));
for (const btn of targets) {
setChip(btn.key, 'active');
log(探索: 「${btn.label}」...);
let el = null;
const deadline = Date.now() + 8000;
while (Date.now() < deadline) {
el = findStudioButton(btn.texts);
if (el) break;
await sleep(400);
}
if (!el) {
log(– 見つからず: 「${btn.label}」, '#fbbf24');
setChip(btn.key, 'skip');
continue;
}
el.click();
log(✓ クリック: 「${btn.label}」, '#4ade80');
if (btn.hasPopup) {
await sleep(2500); // ポップアップ表示を待つ
await handlePopup(btn.hasPopup); // 'video' or 'report'
await sleep(500); // クリック後の安定待ち
}
setChip(btn.key, 'done');
await sleep(3000);
}
doneSources.add(selectedName);
log(━━ 完了。次のソースを選んでください ━━, '#374151');
finishSequence();
}
function finishSequence() {
running = false;
selectedIdx = -1; selectedName = '';
renderBtnGrid();
fetchSources();
}
// ============================================================
// イベント
// ============================================================
document.getElementById('nlm-refresh').addEventListener('click', fetchSources);
document.getElementById('nlm-all-toggle').addEventListener('click', () => {
allOn = !allOn;
enabledKeys = allOn ? new Set(BUTTON_DEFS.map(b=>b.key)) : new Set();
renderBtnGrid();
});
document.getElementById('nlm-start').addEventListener('click', () => {
if (running || selectedIdx < 0) return;
runSequence();
});
document.getElementById('nlm-csv').addEventListener('click', () => {
if (!logHistory.length) return;
const rows = logHistory.map(r =>
"${r.time}","${r.source.replace(/"/g,'""')}","${r.message.replace(/"/g,'""')}"
).join('\n');
const a = Object.assign(document.createElement('a'), {
href: URL.createObjectURL(new Blob('\uFEFF'+'Time,Source,Message\n'+rows, {type:'text/csv;charset=utf-8;'})),
download: nlm_log_${new Date().toISOString().slice(0,10)}.csv,
});
a.click();
});
document.getElementById('nlm-clear').addEventListener('click', () => {
document.getElementById('nlm-log').innerHTML = '';
logHistory = [];
renderBtnGrid();
});
// 起動時にソース取得
fetchSources();
})();
#Chrome拡張