@roomi-fields/notebooklm-mcp 1.5.7 → 1.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -22
- package/README.md +219 -219
- package/deployment/INDEX.md +0 -0
- package/deployment/PACKAGE-FILES.txt +180 -180
- package/deployment/QUICK-START.md +0 -0
- package/deployment/docs/01-INSTALL.md +21 -1
- package/deployment/docs/02-CONFIGURATION.md +0 -0
- package/deployment/docs/03-API.md +16 -10
- package/deployment/docs/04-N8N-INTEGRATION.md +0 -0
- package/deployment/docs/05-TROUBLESHOOTING.md +19 -1
- package/deployment/docs/06-NOTEBOOK-LIBRARY.md +9 -9
- package/deployment/docs/07-AUTO-DISCOVERY.md +6 -15
- package/deployment/docs/08-DOCKER.md +0 -0
- package/deployment/docs/08-WSL-USAGE.md +7 -7
- package/deployment/docs/09-MULTI-INTERFACE.md +6 -6
- package/deployment/docs/10-CONTENT-MANAGEMENT.md +0 -0
- package/deployment/docs/11-MULTI-ACCOUNT.md +1 -1
- package/deployment/docs/README.md +0 -0
- package/deployment/scripts/README.md +0 -0
- package/deployment/scripts/install.ps1 +114 -114
- package/deployment/scripts/setup-auth.ps1 +217 -217
- package/deployment/scripts/start-server.ps1 +72 -72
- package/deployment/scripts/stop-server.ps1 +51 -51
- package/deployment/scripts/test-api.ps1 +651 -651
- package/deployment/scripts/test-auth.ps1 +0 -0
- package/deployment/scripts/test-auto-discovery.ps1 +295 -295
- package/deployment/scripts/test-cors.ps1 +398 -398
- package/deployment/scripts/test-errors.ps1 +581 -581
- package/deployment/scripts/test-server.ps1 +140 -140
- package/deployment/scripts/test-sessions.ps1 +426 -426
- package/deployment/scripts/test-validation.ps1 +299 -299
- package/dist/accounts/account-manager.d.ts +0 -0
- package/dist/accounts/account-manager.d.ts.map +0 -0
- package/dist/accounts/account-manager.js +0 -0
- package/dist/accounts/account-manager.js.map +0 -0
- package/dist/accounts/auto-login-manager.d.ts +0 -0
- package/dist/accounts/auto-login-manager.d.ts.map +0 -0
- package/dist/accounts/auto-login-manager.js +0 -0
- package/dist/accounts/auto-login-manager.js.map +0 -0
- package/dist/accounts/crypto.d.ts +0 -0
- package/dist/accounts/crypto.d.ts.map +0 -0
- package/dist/accounts/crypto.js +0 -0
- package/dist/accounts/crypto.js.map +0 -0
- package/dist/accounts/index.d.ts +0 -0
- package/dist/accounts/index.d.ts.map +0 -0
- package/dist/accounts/index.js +0 -0
- package/dist/accounts/index.js.map +0 -0
- package/dist/accounts/types.d.ts +0 -0
- package/dist/accounts/types.d.ts.map +0 -0
- package/dist/accounts/types.js +0 -0
- package/dist/accounts/types.js.map +0 -0
- package/dist/auth/auth-manager.d.ts +0 -0
- package/dist/auth/auth-manager.d.ts.map +0 -0
- package/dist/auth/auth-manager.js +0 -0
- package/dist/auth/auth-manager.js.map +0 -0
- package/dist/auto-discovery/auto-discovery.d.ts +0 -0
- package/dist/auto-discovery/auto-discovery.d.ts.map +0 -0
- package/dist/auto-discovery/auto-discovery.js +0 -0
- package/dist/auto-discovery/auto-discovery.js.map +0 -0
- package/dist/cli/accounts.d.ts +0 -0
- package/dist/cli/accounts.d.ts.map +0 -0
- package/dist/cli/accounts.js +0 -0
- package/dist/cli/accounts.js.map +0 -0
- package/dist/cli/de-auth.d.ts +0 -0
- package/dist/cli/de-auth.d.ts.map +0 -0
- package/dist/cli/de-auth.js +0 -0
- package/dist/cli/de-auth.js.map +0 -0
- package/dist/cli/help.d.ts +0 -0
- package/dist/cli/help.d.ts.map +0 -0
- package/dist/cli/help.js +5 -0
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/setup-auth.d.ts +0 -0
- package/dist/cli/setup-auth.d.ts.map +0 -0
- package/dist/cli/setup-auth.js +0 -0
- package/dist/cli/setup-auth.js.map +0 -0
- package/dist/config.d.ts +0 -0
- package/dist/config.d.ts.map +0 -0
- package/dist/config.js +0 -0
- package/dist/config.js.map +0 -0
- package/dist/content/content-generator.d.ts +0 -0
- package/dist/content/content-generator.d.ts.map +0 -0
- package/dist/content/content-generator.js +0 -0
- package/dist/content/content-generator.js.map +0 -0
- package/dist/content/content-manager.d.ts +8 -0
- package/dist/content/content-manager.d.ts.map +1 -1
- package/dist/content/content-manager.js +411 -67
- package/dist/content/content-manager.js.map +1 -1
- package/dist/content/content-templates.d.ts +0 -0
- package/dist/content/content-templates.d.ts.map +0 -0
- package/dist/content/content-templates.js +0 -0
- package/dist/content/content-templates.js.map +0 -0
- package/dist/content/index.d.ts +0 -0
- package/dist/content/index.d.ts.map +0 -0
- package/dist/content/index.js +0 -0
- package/dist/content/index.js.map +0 -0
- package/dist/content/types.d.ts +0 -0
- package/dist/content/types.d.ts.map +0 -0
- package/dist/content/types.js +0 -0
- package/dist/content/types.js.map +0 -0
- package/dist/errors.d.ts +0 -0
- package/dist/errors.d.ts.map +0 -0
- package/dist/errors.js +0 -0
- package/dist/errors.js.map +0 -0
- package/dist/http-wrapper.d.ts +0 -0
- package/dist/http-wrapper.d.ts.map +0 -0
- package/dist/http-wrapper.js +0 -0
- package/dist/http-wrapper.js.map +0 -0
- package/dist/i18n/en.json +0 -0
- package/dist/i18n/fr.json +0 -0
- package/dist/i18n/index.d.ts +0 -0
- package/dist/i18n/index.d.ts.map +0 -0
- package/dist/i18n/index.js +0 -0
- package/dist/i18n/index.js.map +0 -0
- package/dist/index.d.ts +0 -0
- package/dist/index.d.ts.map +0 -0
- package/dist/index.js +0 -0
- package/dist/index.js.map +0 -0
- package/dist/library/notebook-library.d.ts +0 -0
- package/dist/library/notebook-library.d.ts.map +0 -0
- package/dist/library/notebook-library.js +0 -0
- package/dist/library/notebook-library.js.map +0 -0
- package/dist/library/types.d.ts +0 -0
- package/dist/library/types.d.ts.map +0 -0
- package/dist/library/types.js +0 -0
- package/dist/library/types.js.map +0 -0
- package/dist/session/browser-session.d.ts +12 -0
- package/dist/session/browser-session.d.ts.map +1 -1
- package/dist/session/browser-session.js +156 -32
- package/dist/session/browser-session.js.map +1 -1
- package/dist/session/session-manager.d.ts +0 -0
- package/dist/session/session-manager.d.ts.map +0 -0
- package/dist/session/session-manager.js +0 -0
- package/dist/session/session-manager.js.map +0 -0
- package/dist/session/shared-context-manager.d.ts +0 -0
- package/dist/session/shared-context-manager.d.ts.map +0 -0
- package/dist/session/shared-context-manager.js +0 -0
- package/dist/session/shared-context-manager.js.map +0 -0
- package/dist/startup/startup-manager.d.ts +0 -0
- package/dist/startup/startup-manager.d.ts.map +0 -0
- package/dist/startup/startup-manager.js +0 -0
- package/dist/startup/startup-manager.js.map +0 -0
- package/dist/stdio-http-proxy.d.ts +0 -0
- package/dist/stdio-http-proxy.d.ts.map +0 -0
- package/dist/stdio-http-proxy.js +0 -0
- package/dist/stdio-http-proxy.js.map +0 -0
- package/dist/tools/index.d.ts +0 -0
- package/dist/tools/index.d.ts.map +0 -0
- package/dist/tools/index.js +0 -0
- package/dist/tools/index.js.map +0 -0
- package/dist/types.d.ts +0 -0
- package/dist/types.d.ts.map +0 -0
- package/dist/types.js +0 -0
- package/dist/types.js.map +0 -0
- package/dist/utils/citation-extractor.d.ts +0 -0
- package/dist/utils/citation-extractor.d.ts.map +0 -0
- package/dist/utils/citation-extractor.js +67 -67
- package/dist/utils/citation-extractor.js.map +0 -0
- package/dist/utils/cleanup-manager.d.ts +0 -0
- package/dist/utils/cleanup-manager.d.ts.map +0 -0
- package/dist/utils/cleanup-manager.js +0 -0
- package/dist/utils/cleanup-manager.js.map +0 -0
- package/dist/utils/logger.d.ts +0 -0
- package/dist/utils/logger.d.ts.map +0 -0
- package/dist/utils/logger.js +0 -0
- package/dist/utils/logger.js.map +0 -0
- package/dist/utils/page-utils.d.ts +5 -0
- package/dist/utils/page-utils.d.ts.map +1 -1
- package/dist/utils/page-utils.js +73 -15
- package/dist/utils/page-utils.js.map +1 -1
- package/dist/utils/stealth-utils.d.ts +0 -0
- package/dist/utils/stealth-utils.d.ts.map +0 -0
- package/dist/utils/stealth-utils.js +0 -0
- package/dist/utils/stealth-utils.js.map +0 -0
- package/docs/ADDING_A_LANGUAGE.md +0 -0
- package/docs/ARCHITECTURE_MIGRATION_STUDY.md +0 -0
- package/docs/CHROME_PROFILE_LIMITATION.md +0 -0
- package/docs/MULTI_ACCOUNT_SYSTEM.md +0 -0
- package/package.json +5 -3
- package/scripts/archive/add-and-activate-notebook.ps1 +4 -4
- package/scripts/archive/add-new-notebook.ps1 +4 -4
- package/scripts/archive/add-rom1pey.ps1 +2 -2
- package/scripts/archive/add-rpmonster.ps1 +2 -2
- package/scripts/archive/add-source-debug.ps1 +1 -1
- package/scripts/archive/add-source-e2e.ps1 +1 -1
- package/scripts/archive/add-source-visible.ps1 +1 -1
- package/scripts/archive/add-test-notebook.ps1 +1 -1
- package/scripts/archive/add-test-source.ps1 +1 -1
- package/scripts/archive/capture-screen.ps1 +1 -1
- package/scripts/archive/change-language.mjs +4 -3
- package/scripts/archive/change-language.ts +5 -3
- package/scripts/archive/check-account.ps1 +0 -0
- package/scripts/archive/check-notebook-2.ps1 +0 -0
- package/scripts/archive/check-test-notebook.ps1 +0 -0
- package/scripts/archive/create-notebook-auto.ps1 +0 -0
- package/scripts/archive/create-notebook.ps1 +2 -2
- package/scripts/archive/create-rom1pey-notebook.ps1 +2 -2
- package/scripts/archive/create-rom1pey.ps1 +2 -2
- package/scripts/archive/create-test-notebook-fresh.ps1 +0 -0
- package/scripts/archive/create-test-notebook.ps1 +0 -0
- package/scripts/archive/debug-add-source-auto.ps1 +0 -0
- package/scripts/archive/debug-add-source.ps1 +0 -0
- package/scripts/archive/debug-add-text-source.ps1 +4 -4
- package/scripts/archive/debug-home.ps1 +0 -0
- package/scripts/archive/debug-selectors.ps1 +1 -1
- package/scripts/archive/debug-sources-panel.ps1 +0 -0
- package/scripts/archive/debug-ui.ps1 +0 -0
- package/scripts/archive/discover-home.ps1 +2 -2
- package/scripts/archive/kill-automation-chrome.ps1 +0 -0
- package/scripts/archive/list-my-notebooks.ps1 +0 -0
- package/scripts/archive/navigate-home-visible.ps1 +1 -1
- package/scripts/archive/navigate-home.ps1 +1 -1
- package/scripts/archive/run-e2e-english.ps1 +3 -3
- package/scripts/archive/run-e2e-rom1pey-v2.ps1 +4 -4
- package/scripts/archive/run-e2e-rom1pey.ps1 +4 -4
- package/scripts/archive/setup-english-test.ps1 +6 -6
- package/scripts/archive/setup-test-notebook.ps1 +1 -1
- package/scripts/archive/simple-add-source.ps1 +1 -1
- package/scripts/archive/t10.ps1 +1 -1
- package/scripts/archive/t20.ps1 +1 -1
- package/scripts/archive/t30.ps1 +1 -1
- package/scripts/archive/t31.ps1 +1 -1
- package/scripts/archive/t32.ps1 +1 -1
- package/scripts/archive/t39.ps1 +0 -0
- package/scripts/archive/t40.ps1 +0 -0
- package/scripts/archive/t53.ps1 +1 -1
- package/scripts/archive/t54.ps1 +0 -0
- package/scripts/archive/t55.ps1 +0 -0
- package/scripts/archive/t9.ps1 +0 -0
- package/scripts/archive/test-access.ps1 +1 -1
- package/scripts/archive/test-add-delete-source.ps1 +4 -4
- package/scripts/archive/test-add-source-visible.ps1 +1 -1
- package/scripts/archive/test-add-source.ps1 +1 -1
- package/scripts/archive/test-add-text-debug.ps1 +2 -2
- package/scripts/archive/test-add-text-source.ps1 +0 -0
- package/scripts/archive/test-add-url-source.ps1 +0 -0
- package/scripts/archive/test-ask-ascii.ps1 +0 -0
- package/scripts/archive/test-ask-cnv.ps1 +0 -0
- package/scripts/archive/test-ask-headed.ps1 +1 -1
- package/scripts/archive/test-ask-ifs.ps1 +0 -0
- package/scripts/archive/test-ask-now.ps1 +0 -0
- package/scripts/archive/test-ask-real.ps1 +0 -0
- package/scripts/archive/test-ask-visible.ps1 +0 -0
- package/scripts/archive/test-create-notebook.ps1 +0 -0
- package/scripts/archive/test-create-then-add.ps1 +0 -0
- package/scripts/archive/test-delete-source.ps1 +4 -4
- package/scripts/archive/test-e2e-notebook.ps1 +2 -2
- package/scripts/archive/test-english-notebook.ps1 +4 -4
- package/scripts/archive/test-english.ps1 +1 -1
- package/scripts/archive/test-full-custom-instructions.ps1 +1 -1
- package/scripts/archive/test-full-infographic.ps1 +1 -1
- package/scripts/archive/test-full-language.ps1 +1 -1
- package/scripts/archive/test-full-presentation.ps1 +1 -1
- package/scripts/archive/test-full-report.ps1 +1 -1
- package/scripts/archive/test-full-source-selection.ps1 +1 -1
- package/scripts/archive/test-full-video-brief.ps1 +1 -1
- package/scripts/archive/test-full-video-explainer.ps1 +1 -1
- package/scripts/archive/test-full-video-styles.ps1 +1 -1
- package/scripts/archive/test-generate-report.ps1 +0 -0
- package/scripts/archive/test-generate-study-guide.ps1 +0 -0
- package/scripts/archive/test-headed-ask.ps1 +1 -1
- package/scripts/archive/test-headed-now.ps1 +2 -2
- package/scripts/archive/test-headed.ps1 +2 -2
- package/scripts/archive/test-hello.ps1 +1 -1
- package/scripts/archive/test-i18n-studio.ps1 +0 -0
- package/scripts/archive/test-i18n.ps1 +0 -0
- package/scripts/archive/test-manual-headed.ps1 +1 -1
- package/scripts/archive/test-mathieu-quota.ps1 +1 -1
- package/scripts/archive/test-notebook-1.ps1 +0 -0
- package/scripts/archive/test-notebook-2-sources.ps1 +0 -0
- package/scripts/archive/test-notebook1.ps1 +0 -0
- package/scripts/archive/test-personal-notebook.ps1 +1 -1
- package/scripts/archive/test-rate-limit.ps1 +1 -1
- package/scripts/archive/test-real-ask.ps1 +1 -1
- package/scripts/archive/test-real-ask2.ps1 +1 -1
- package/scripts/archive/test-rom1pey.ps1 +1 -1
- package/scripts/archive/test-rotation-complete.ps1 +1 -1
- package/scripts/archive/test-rotation.ps1 +2 -2
- package/scripts/archive/test-show-browser.ps1 +1 -1
- package/scripts/archive/test-update-notebook.ps1 +1 -1
- package/scripts/archive/verify-language-slow.ps1 +1 -1
- package/scripts/archive/verify-language.ps1 +1 -1
- package/scripts/check-server.ps1 +0 -0
- package/scripts/docker-entrypoint.sh +25 -25
- package/scripts/doctor.mjs +257 -0
- package/scripts/mcp-proxy-hidden.ps1 +0 -0
- package/scripts/mcp-wsl-helper.sh +146 -146
- package/scripts/start-server-hidden.vbs +13 -10
- package/scripts/start-server.ps1 +1 -1
- package/scripts/start-vnc.sh +0 -0
- package/scripts/stop-server.bat +0 -0
- package/scripts/stop-server.ps1 +0 -0
- package/scripts/switch-account-language.sh +87 -128
- package/scripts/test-account.ps1 +0 -0
- package/docs/archive/auto-discovery-complet.md +0 -906
- package/scripts/add-totp.ts +0 -110
|
@@ -50,22 +50,33 @@ export class ContentManager {
|
|
|
50
50
|
const expectedNotebookUuid = initialUrl.match(/notebook\/([a-f0-9-]+)/)?.[1];
|
|
51
51
|
log.info(` 🎯 Target notebook UUID: ${expectedNotebookUuid || 'NOT FOUND'}`);
|
|
52
52
|
try {
|
|
53
|
+
const existingSourceNames = await this.getAllSourceLabels();
|
|
53
54
|
// Click "Add source" button
|
|
54
55
|
await this.clickAddSource();
|
|
56
|
+
// DEBUG: Screenshot after clicking add source to see what UI appeared
|
|
57
|
+
try {
|
|
58
|
+
await this.page.screenshot({
|
|
59
|
+
path: path.join(CONFIG.dataDir, 'debug-after-add-click.png'),
|
|
60
|
+
});
|
|
61
|
+
log.info(` 📸 Debug screenshot saved: debug-after-add-click.png`);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
/* ignore */
|
|
65
|
+
}
|
|
55
66
|
// Wait for upload dialog
|
|
56
67
|
await this.waitForUploadDialog();
|
|
57
68
|
// Select upload type and upload (pass expectedNotebookUuid for redirect detection)
|
|
58
69
|
switch (input.type) {
|
|
59
70
|
case 'file':
|
|
60
|
-
return await this.uploadFile(input, expectedNotebookUuid);
|
|
71
|
+
return await this.uploadFile(input, expectedNotebookUuid, existingSourceNames);
|
|
61
72
|
case 'url':
|
|
62
|
-
return await this.uploadUrl(input, expectedNotebookUuid);
|
|
73
|
+
return await this.uploadUrl(input, expectedNotebookUuid, existingSourceNames);
|
|
63
74
|
case 'text':
|
|
64
|
-
return await this.uploadText(input, expectedNotebookUuid);
|
|
75
|
+
return await this.uploadText(input, expectedNotebookUuid, existingSourceNames);
|
|
65
76
|
case 'google_drive':
|
|
66
|
-
return await this.uploadGoogleDrive(input, expectedNotebookUuid);
|
|
77
|
+
return await this.uploadGoogleDrive(input, expectedNotebookUuid, existingSourceNames);
|
|
67
78
|
case 'youtube':
|
|
68
|
-
return await this.uploadYouTube(input, expectedNotebookUuid);
|
|
79
|
+
return await this.uploadYouTube(input, expectedNotebookUuid, existingSourceNames);
|
|
69
80
|
default:
|
|
70
81
|
return { success: false, error: `Unsupported source type: ${input.type}` };
|
|
71
82
|
}
|
|
@@ -272,11 +283,11 @@ export class ContentManager {
|
|
|
272
283
|
/**
|
|
273
284
|
* Upload a local file
|
|
274
285
|
*/
|
|
275
|
-
async uploadFile(input, expectedNotebookUuid) {
|
|
286
|
+
async uploadFile(input, expectedNotebookUuid, previousSourceNames = []) {
|
|
276
287
|
if (!input.filePath) {
|
|
277
288
|
return { success: false, error: 'File path is required' };
|
|
278
289
|
}
|
|
279
|
-
// Path traversal protection: resolve
|
|
290
|
+
// Path traversal protection: resolve first, then validate startsWith(allowedDir)
|
|
280
291
|
const resolvedPath = path.resolve(input.filePath);
|
|
281
292
|
const allowedDir = path.resolve(CONFIG.dataDir);
|
|
282
293
|
// Allow files from dataDir or current working directory
|
|
@@ -328,7 +339,7 @@ export class ContentManager {
|
|
|
328
339
|
// Click upload/confirm button
|
|
329
340
|
await this.clickUploadButton();
|
|
330
341
|
// Wait for processing
|
|
331
|
-
const result = await this.waitForSourceProcessing(input.title || path.basename(input.filePath), undefined, expectedNotebookUuid);
|
|
342
|
+
const result = await this.waitForSourceProcessing(input.title || path.basename(input.filePath), undefined, expectedNotebookUuid, previousSourceNames);
|
|
332
343
|
return result;
|
|
333
344
|
}
|
|
334
345
|
catch (error) {
|
|
@@ -339,20 +350,31 @@ export class ContentManager {
|
|
|
339
350
|
/**
|
|
340
351
|
* Upload from URL
|
|
341
352
|
*/
|
|
342
|
-
async uploadUrl(input, expectedNotebookUuid) {
|
|
353
|
+
async uploadUrl(input, expectedNotebookUuid, previousSourceNames = []) {
|
|
343
354
|
if (!input.url) {
|
|
344
355
|
return { success: false, error: 'URL is required' };
|
|
345
356
|
}
|
|
346
357
|
log.info(` 🌐 Adding URL: ${input.url}`);
|
|
347
358
|
try {
|
|
348
359
|
// Click on URL/Website option (bilingual selectors)
|
|
360
|
+
// NotebookLM UI may show source types as buttons, spans, divs, or list items
|
|
349
361
|
const urlTypeSelectors = [
|
|
362
|
+
// Button-based (original dialog UI)
|
|
350
363
|
...i18nSelectors('button:has-text("{text}")', 'sourceTypes', 'website'),
|
|
351
364
|
...i18nSelectors('button:has-text("{text}")', 'sourceTypes', 'link'),
|
|
352
365
|
...i18nSelectors('button:has-text("{text}")', 'sourceTypes', 'url'),
|
|
366
|
+
// New UI (2025+): source types may be spans, divs, or clickable list items
|
|
367
|
+
...i18nSelectors('span:has-text("{text}")', 'sourceTypes', 'website'),
|
|
368
|
+
...i18nSelectors('div:has-text("{text}")', 'sourceTypes', 'website'),
|
|
369
|
+
...i18nSelectors('[role="menuitem"]:has-text("{text}")', 'sourceTypes', 'website'),
|
|
370
|
+
...i18nSelectors('[role="option"]:has-text("{text}")', 'sourceTypes', 'website'),
|
|
371
|
+
...i18nSelectors('span:has-text("{text}")', 'sourceTypes', 'link'),
|
|
372
|
+
...i18nSelectors('[role="menuitem"]:has-text("{text}")', 'sourceTypes', 'link'),
|
|
373
|
+
// Aria-label patterns
|
|
353
374
|
'[data-type="url"]',
|
|
354
|
-
'[aria-label*="website"]',
|
|
375
|
+
'[aria-label*="website" i]',
|
|
355
376
|
'[aria-label*="URL"]',
|
|
377
|
+
'[aria-label*="link" i]:not([aria-label*="unlink"])',
|
|
356
378
|
];
|
|
357
379
|
log.info(` 🔍 Looking for URL option...`);
|
|
358
380
|
let foundUrlOption = false;
|
|
@@ -394,6 +416,7 @@ export class ContentManager {
|
|
|
394
416
|
// Wait for input to appear after clicking option
|
|
395
417
|
await randomDelay(500, 1000);
|
|
396
418
|
// Find URL input (can be input OR textarea) - bilingual selectors
|
|
419
|
+
// IMPORTANT: Exclude the "Search the web" search bar which is always visible
|
|
397
420
|
log.info(` 🔍 Looking for URL input...`);
|
|
398
421
|
const urlInputSelectors = [
|
|
399
422
|
// i18n placeholder selectors
|
|
@@ -412,7 +435,7 @@ export class ContentManager {
|
|
|
412
435
|
'textarea[placeholder*="http"]',
|
|
413
436
|
'input[name="url"]',
|
|
414
437
|
'input[type="url"]',
|
|
415
|
-
//
|
|
438
|
+
// Dialog-based selectors (old UI)
|
|
416
439
|
'[role="dialog"] input[type="text"]',
|
|
417
440
|
'[role="dialog"] input:not([type="hidden"])',
|
|
418
441
|
'[role="dialog"] textarea',
|
|
@@ -420,6 +443,14 @@ export class ContentManager {
|
|
|
420
443
|
'.mat-dialog-content textarea',
|
|
421
444
|
'.mdc-dialog__content input',
|
|
422
445
|
'.mdc-dialog__content textarea',
|
|
446
|
+
// New UI (2025+): overlay/panel/popover selectors (not [role="dialog"])
|
|
447
|
+
'[role="menu"] input[type="text"]',
|
|
448
|
+
'[role="menu"] textarea',
|
|
449
|
+
'.cdk-overlay-pane input[type="text"]',
|
|
450
|
+
'.cdk-overlay-pane textarea',
|
|
451
|
+
'.cdk-overlay-pane input:not([type="hidden"])',
|
|
452
|
+
'.mat-menu-panel input',
|
|
453
|
+
'.mat-menu-panel textarea',
|
|
423
454
|
];
|
|
424
455
|
let urlInput = null;
|
|
425
456
|
for (const selector of urlInputSelectors) {
|
|
@@ -435,31 +466,71 @@ export class ContentManager {
|
|
|
435
466
|
continue;
|
|
436
467
|
}
|
|
437
468
|
}
|
|
438
|
-
// Fallback: find any visible input or textarea in
|
|
469
|
+
// Fallback: find any visible input or textarea in dialog or overlay
|
|
439
470
|
if (!urlInput) {
|
|
440
|
-
log.info(` 🔍 Trying fallback: any visible input/textarea in dialog...`);
|
|
471
|
+
log.info(` 🔍 Trying fallback: any visible input/textarea in dialog or overlay...`);
|
|
472
|
+
// Search in multiple container types (dialog, overlay, menu, panel)
|
|
473
|
+
const containerSelectors = [
|
|
474
|
+
'[role="dialog"]',
|
|
475
|
+
'.cdk-overlay-pane',
|
|
476
|
+
'[role="menu"]',
|
|
477
|
+
'.mat-menu-panel',
|
|
478
|
+
];
|
|
441
479
|
try {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
for (const input of allInputs) {
|
|
445
|
-
if (await input.isVisible()) {
|
|
446
|
-
urlInput = input;
|
|
447
|
-
const placeholder = await input.getAttribute('placeholder');
|
|
448
|
-
log.info(` ✅ Found input via fallback: placeholder="${placeholder}"`);
|
|
480
|
+
for (const container of containerSelectors) {
|
|
481
|
+
if (urlInput)
|
|
449
482
|
break;
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
urlInput = textarea;
|
|
458
|
-
const placeholder = await textarea.getAttribute('placeholder');
|
|
459
|
-
log.info(` ✅ Found textarea via fallback: placeholder="${placeholder}"`);
|
|
483
|
+
// Try inputs first
|
|
484
|
+
const allInputs = await this.page.locator(`${container} input`).all();
|
|
485
|
+
for (const input of allInputs) {
|
|
486
|
+
if (await input.isVisible()) {
|
|
487
|
+
urlInput = input;
|
|
488
|
+
const placeholder = await input.getAttribute('placeholder');
|
|
489
|
+
log.info(` ✅ Found input via fallback (${container}): placeholder="${placeholder}"`);
|
|
460
490
|
break;
|
|
461
491
|
}
|
|
462
492
|
}
|
|
493
|
+
// Try textareas if no input found
|
|
494
|
+
if (!urlInput) {
|
|
495
|
+
const allTextareas = await this.page.locator(`${container} textarea`).all();
|
|
496
|
+
for (const textarea of allTextareas) {
|
|
497
|
+
if (await textarea.isVisible()) {
|
|
498
|
+
urlInput = textarea;
|
|
499
|
+
const placeholder = await textarea.getAttribute('placeholder');
|
|
500
|
+
log.info(` ✅ Found textarea via fallback (${container}): placeholder="${placeholder}"`);
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
/* ignore */
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
// Last resort: find ANY visible input/textarea on page, excluding the search bar
|
|
512
|
+
if (!urlInput) {
|
|
513
|
+
log.info(` 🔍 Last resort: scanning ALL visible inputs (excluding search bar)...`);
|
|
514
|
+
try {
|
|
515
|
+
const allPageInputs = await this.page.locator('input, textarea').all();
|
|
516
|
+
for (const input of allPageInputs) {
|
|
517
|
+
if (!(await input.isVisible()))
|
|
518
|
+
continue;
|
|
519
|
+
const placeholder = (await input.getAttribute('placeholder')) || '';
|
|
520
|
+
// Skip the "Search the web" search bar and other non-URL inputs
|
|
521
|
+
if (placeholder.toLowerCase().includes('search'))
|
|
522
|
+
continue;
|
|
523
|
+
if (placeholder.toLowerCase().includes('ask'))
|
|
524
|
+
continue;
|
|
525
|
+
if (placeholder.toLowerCase().includes('chat'))
|
|
526
|
+
continue;
|
|
527
|
+
// Prefer inputs that look URL-related
|
|
528
|
+
const type = (await input.getAttribute('type')) || '';
|
|
529
|
+
if (type === 'search')
|
|
530
|
+
continue;
|
|
531
|
+
urlInput = input;
|
|
532
|
+
log.info(` ✅ Found input via last resort: placeholder="${placeholder}", type="${type}"`);
|
|
533
|
+
break;
|
|
463
534
|
}
|
|
464
535
|
}
|
|
465
536
|
catch {
|
|
@@ -468,12 +539,10 @@ export class ContentManager {
|
|
|
468
539
|
}
|
|
469
540
|
// Debug: list all inputs/textareas if still not found
|
|
470
541
|
if (!urlInput) {
|
|
471
|
-
log.warning(` ⚠️ URL input not found, listing
|
|
542
|
+
log.warning(` ⚠️ URL input not found, listing ALL page inputs for debug...`);
|
|
472
543
|
try {
|
|
473
|
-
const inputs = await this.page
|
|
474
|
-
|
|
475
|
-
.all();
|
|
476
|
-
for (let i = 0; i < inputs.length; i++) {
|
|
544
|
+
const inputs = await this.page.locator('input, textarea').all();
|
|
545
|
+
for (let i = 0; i < Math.min(inputs.length, 20); i++) {
|
|
477
546
|
const el = inputs[i];
|
|
478
547
|
const tag = await el.evaluate((e) => e.tagName?.toLowerCase() || 'unknown');
|
|
479
548
|
const type = await el.getAttribute('type');
|
|
@@ -483,7 +552,7 @@ export class ContentManager {
|
|
|
483
552
|
}
|
|
484
553
|
}
|
|
485
554
|
catch (e) {
|
|
486
|
-
log.warning(` ⚠️ Could not list
|
|
555
|
+
log.warning(` ⚠️ Could not list page elements: ${e}`);
|
|
487
556
|
}
|
|
488
557
|
throw new Error('URL input not found');
|
|
489
558
|
}
|
|
@@ -494,7 +563,7 @@ export class ContentManager {
|
|
|
494
563
|
log.info(` 🔍 Looking for upload button...`);
|
|
495
564
|
await this.clickUploadButton();
|
|
496
565
|
// Wait for processing
|
|
497
|
-
const result = await this.waitForSourceProcessing(input.title || input.url, undefined, expectedNotebookUuid);
|
|
566
|
+
const result = await this.waitForSourceProcessing(input.title || input.url, undefined, expectedNotebookUuid, previousSourceNames);
|
|
498
567
|
return result;
|
|
499
568
|
}
|
|
500
569
|
catch (error) {
|
|
@@ -505,7 +574,7 @@ export class ContentManager {
|
|
|
505
574
|
/**
|
|
506
575
|
* Upload text content
|
|
507
576
|
*/
|
|
508
|
-
async uploadText(input, expectedNotebookUuid) {
|
|
577
|
+
async uploadText(input, expectedNotebookUuid, previousSourceNames = []) {
|
|
509
578
|
if (!input.text) {
|
|
510
579
|
return { success: false, error: 'Text content is required' };
|
|
511
580
|
}
|
|
@@ -608,8 +677,9 @@ export class ContentManager {
|
|
|
608
677
|
}
|
|
609
678
|
throw new Error('Text input not found in dialog');
|
|
610
679
|
}
|
|
611
|
-
|
|
612
|
-
|
|
680
|
+
let textToInsert = input.text;
|
|
681
|
+
await textInput.fill(textToInsert);
|
|
682
|
+
log.info(` ✅ Text entered (${textToInsert.length} chars)`);
|
|
613
683
|
// Set title if provided
|
|
614
684
|
log.info(` 🔍 Looking for title input...`);
|
|
615
685
|
if (input.title) {
|
|
@@ -638,6 +708,9 @@ export class ContentManager {
|
|
|
638
708
|
}
|
|
639
709
|
if (!titleSet) {
|
|
640
710
|
log.warning(` ⚠️ Title input NOT found - source will have default name`);
|
|
711
|
+
textToInsert = `${input.title}\n\n${input.text}`;
|
|
712
|
+
await textInput.fill(textToInsert);
|
|
713
|
+
log.info(` ✅ Fallback title injected into pasted text: ${input.title}`);
|
|
641
714
|
// Debug: list all inputs in dialog
|
|
642
715
|
try {
|
|
643
716
|
const allInputs = await this.page.locator('[role="dialog"] input').all();
|
|
@@ -683,7 +756,7 @@ export class ContentManager {
|
|
|
683
756
|
log.warning(` ⚠️ Could not check button state: ${e}`);
|
|
684
757
|
}
|
|
685
758
|
// Get first few words of text for later verification (NotebookLM uses text content as title)
|
|
686
|
-
const textPreview =
|
|
759
|
+
const textPreview = textToInsert.slice(0, 30).trim();
|
|
687
760
|
log.info(` 📝 Text preview for verification: "${textPreview}..."`);
|
|
688
761
|
// Click add button
|
|
689
762
|
log.info(` 🔍 Looking for upload button...`);
|
|
@@ -691,7 +764,7 @@ export class ContentManager {
|
|
|
691
764
|
// Wait for processing - NotebookLM names pasted text sources "Texte collé" in French or "Pasted text"
|
|
692
765
|
// We'll look for either the expected name or "Texte collé"
|
|
693
766
|
// Pass initialUuid to detect notebook redirection
|
|
694
|
-
const result = await this.waitForSourceProcessing(input.title || 'Texte collé', textPreview, expectedNotebookUuid);
|
|
767
|
+
const result = await this.waitForSourceProcessing(input.title || 'Texte collé', textPreview, expectedNotebookUuid, previousSourceNames);
|
|
695
768
|
return result;
|
|
696
769
|
}
|
|
697
770
|
catch (error) {
|
|
@@ -702,18 +775,18 @@ export class ContentManager {
|
|
|
702
775
|
/**
|
|
703
776
|
* Upload from Google Drive
|
|
704
777
|
*/
|
|
705
|
-
async uploadGoogleDrive(input, expectedNotebookUuid) {
|
|
778
|
+
async uploadGoogleDrive(input, expectedNotebookUuid, previousSourceNames = []) {
|
|
706
779
|
if (!input.url) {
|
|
707
780
|
return { success: false, error: 'Google Drive URL is required' };
|
|
708
781
|
}
|
|
709
782
|
log.info(` 📂 Adding Google Drive source: ${input.url}`);
|
|
710
783
|
// Similar to URL upload but with Google Drive specific handling
|
|
711
|
-
return await this.uploadUrl({ ...input, type: 'url' }, expectedNotebookUuid);
|
|
784
|
+
return await this.uploadUrl({ ...input, type: 'url' }, expectedNotebookUuid, previousSourceNames);
|
|
712
785
|
}
|
|
713
786
|
/**
|
|
714
787
|
* Upload YouTube video
|
|
715
788
|
*/
|
|
716
|
-
async uploadYouTube(input, expectedNotebookUuid) {
|
|
789
|
+
async uploadYouTube(input, expectedNotebookUuid, previousSourceNames = []) {
|
|
717
790
|
if (!input.url) {
|
|
718
791
|
return { success: false, error: 'YouTube URL is required' };
|
|
719
792
|
}
|
|
@@ -816,7 +889,7 @@ export class ContentManager {
|
|
|
816
889
|
log.info(` ✅ YouTube URL entered`);
|
|
817
890
|
await randomDelay(500, 1000);
|
|
818
891
|
await this.clickUploadButton();
|
|
819
|
-
const result = await this.waitForSourceProcessing(input.title || 'YouTube video', undefined, expectedNotebookUuid);
|
|
892
|
+
const result = await this.waitForSourceProcessing(input.title || 'YouTube video', undefined, expectedNotebookUuid, previousSourceNames);
|
|
820
893
|
return result;
|
|
821
894
|
}
|
|
822
895
|
catch (error) {
|
|
@@ -861,13 +934,19 @@ export class ContentManager {
|
|
|
861
934
|
continue;
|
|
862
935
|
}
|
|
863
936
|
}
|
|
864
|
-
// Debug: list all buttons in dialog
|
|
865
|
-
log.warning(` ⚠️ No upload button found, listing
|
|
937
|
+
// Debug: list all buttons in dialog or overlay
|
|
938
|
+
log.warning(` ⚠️ No upload button found, listing buttons...`);
|
|
866
939
|
try {
|
|
867
|
-
const
|
|
868
|
-
for (
|
|
869
|
-
const
|
|
870
|
-
|
|
940
|
+
const containers = ['[role="dialog"]', '.cdk-overlay-pane', '[role="menu"]'];
|
|
941
|
+
for (const container of containers) {
|
|
942
|
+
const containerButtons = await this.page.locator(`${container} button`).all();
|
|
943
|
+
if (containerButtons.length > 0) {
|
|
944
|
+
log.info(` 🔍 Buttons in ${container}:`);
|
|
945
|
+
for (let i = 0; i < Math.min(containerButtons.length, 5); i++) {
|
|
946
|
+
const text = await containerButtons[i].textContent();
|
|
947
|
+
log.info(` 🔍 Button[${i}]: "${text?.trim()}"`);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
871
950
|
}
|
|
872
951
|
}
|
|
873
952
|
catch {
|
|
@@ -883,27 +962,50 @@ export class ContentManager {
|
|
|
883
962
|
* @param _textPreview Optional first words of text (for text sources - NotebookLM may use this as name)
|
|
884
963
|
* @param expectedNotebookUuid Optional UUID of the notebook we expect to be on (to detect redirects)
|
|
885
964
|
*/
|
|
886
|
-
async waitForSourceProcessing(sourceName, _textPreview, expectedNotebookUuid) {
|
|
965
|
+
async waitForSourceProcessing(sourceName, _textPreview, expectedNotebookUuid, previousSourceNames = []) {
|
|
887
966
|
log.info(` ⏳ Waiting for source processing: ${sourceName}`);
|
|
888
967
|
const timeout = 90000; // 1.5 minutes (sources can take time)
|
|
889
968
|
const startTime = Date.now();
|
|
969
|
+
// COUNT-BASED DETECTION (primary method for 2025 UI):
|
|
970
|
+
// Capture source count NOW — dialog is still open, source not yet added to DOM.
|
|
971
|
+
// Later, if count increases, we know a source was successfully added.
|
|
972
|
+
let initialSourceCount = -1;
|
|
973
|
+
let initialSourceLabels = [];
|
|
974
|
+
try {
|
|
975
|
+
initialSourceCount = await this.page.locator('.single-source-container').count();
|
|
976
|
+
log.info(` 📊 Source count before processing: ${initialSourceCount}`);
|
|
977
|
+
initialSourceLabels = await this.getAllSourceLabels();
|
|
978
|
+
}
|
|
979
|
+
catch {
|
|
980
|
+
/* ignore */
|
|
981
|
+
}
|
|
890
982
|
// First, wait a bit for the dialog to close (indicates upload started)
|
|
891
983
|
await randomDelay(2000, 3000);
|
|
892
984
|
while (Date.now() - startTime < timeout) {
|
|
893
985
|
// Check for errors in the dialog or page
|
|
986
|
+
// NOTE: Avoid overly broad selectors like [class*="error"] which match
|
|
987
|
+
// random page elements and produce garbage error messages
|
|
894
988
|
const errorSelectors = [
|
|
895
989
|
'.error-message',
|
|
896
990
|
'[role="alert"]:has-text("error")',
|
|
897
991
|
'[role="alert"]:has-text("Error")',
|
|
898
992
|
'.mdc-snackbar--error',
|
|
899
|
-
'
|
|
993
|
+
'.mdc-snackbar--open:has-text("error")',
|
|
994
|
+
'[role="dialog"] [class*="error"]',
|
|
995
|
+
'.cdk-overlay-pane [class*="error"]',
|
|
900
996
|
];
|
|
901
997
|
for (const errorSelector of errorSelectors) {
|
|
902
998
|
try {
|
|
903
999
|
const errorEl = this.page.locator(errorSelector).first();
|
|
904
1000
|
if (await errorEl.isVisible({ timeout: 500 })) {
|
|
905
|
-
const errorText = await errorEl.textContent();
|
|
906
|
-
|
|
1001
|
+
const errorText = (await errorEl.textContent())?.trim();
|
|
1002
|
+
// Skip garbage text that contains icon names (more_vert, etc.)
|
|
1003
|
+
if (errorText &&
|
|
1004
|
+
errorText.length < 200 &&
|
|
1005
|
+
!errorText.includes('more_vert') &&
|
|
1006
|
+
!errorText.includes('more_horiz')) {
|
|
1007
|
+
return { success: false, error: errorText || 'Upload failed', status: 'failed' };
|
|
1008
|
+
}
|
|
907
1009
|
}
|
|
908
1010
|
}
|
|
909
1011
|
catch {
|
|
@@ -928,6 +1030,65 @@ export class ContentManager {
|
|
|
928
1030
|
// If dialog closed, check if source appears in the sources list
|
|
929
1031
|
if (!dialogVisible) {
|
|
930
1032
|
log.info(` ℹ️ Dialog closed, checking for source in list...`);
|
|
1033
|
+
// PRIMARY: Count-based detection (most reliable for 2025 UI)
|
|
1034
|
+
// NotebookLM shows page titles (not URLs) in the source list, so name-based
|
|
1035
|
+
// detection fails for URL sources. Count-based detection works regardless.
|
|
1036
|
+
try {
|
|
1037
|
+
const currentCount = await this.page.locator('.single-source-container').count();
|
|
1038
|
+
log.info(` 📊 Source count: ${initialSourceCount} → ${currentCount}`);
|
|
1039
|
+
if (initialSourceCount >= 0 && currentCount > initialSourceCount) {
|
|
1040
|
+
let detectedSourceName;
|
|
1041
|
+
let detectedSourceId;
|
|
1042
|
+
try {
|
|
1043
|
+
const currentLabels = await this.getAllSourceLabels();
|
|
1044
|
+
detectedSourceName =
|
|
1045
|
+
this.findAddedSourceName(initialSourceLabels, currentLabels) ||
|
|
1046
|
+
this.findAddedSourceName(previousSourceNames, currentLabels);
|
|
1047
|
+
if (detectedSourceName) {
|
|
1048
|
+
let detectedIndex = -1;
|
|
1049
|
+
for (let i = currentLabels.length - 1; i >= 0; i--) {
|
|
1050
|
+
if (this.normalizeSourceName(currentLabels[i]) ===
|
|
1051
|
+
this.normalizeSourceName(detectedSourceName)) {
|
|
1052
|
+
detectedIndex = i;
|
|
1053
|
+
break;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
if (detectedIndex >= 0) {
|
|
1057
|
+
detectedSourceId = `source-${detectedIndex}`;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
catch {
|
|
1062
|
+
// Continue below and fall back if needed
|
|
1063
|
+
}
|
|
1064
|
+
if (!detectedSourceName) {
|
|
1065
|
+
log.info(' ℹ️ Source count increased, but a stable source name is not available yet');
|
|
1066
|
+
}
|
|
1067
|
+
else {
|
|
1068
|
+
log.info(` ℹ️ Detected new source: ${detectedSourceName}`);
|
|
1069
|
+
log.success(` ✅ Source added! Count increased from ${initialSourceCount} to ${currentCount}`);
|
|
1070
|
+
return {
|
|
1071
|
+
success: true,
|
|
1072
|
+
sourceId: detectedSourceId,
|
|
1073
|
+
sourceName: detectedSourceName,
|
|
1074
|
+
status: 'ready',
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
catch {
|
|
1080
|
+
/* ignore */
|
|
1081
|
+
}
|
|
1082
|
+
try {
|
|
1083
|
+
const currentNames = await this.getVisibleSourceNames();
|
|
1084
|
+
if (currentNames.some((name) => this.normalizeSourceName(name) === this.normalizeSourceName(sourceName))) {
|
|
1085
|
+
log.success(` ✅ Source added and matched requested source name: ${sourceName}`);
|
|
1086
|
+
return { success: true, sourceName, status: 'ready' };
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
catch {
|
|
1090
|
+
/* ignore */
|
|
1091
|
+
}
|
|
931
1092
|
// CRITICAL: Verify we're still on the correct notebook after dialog closes
|
|
932
1093
|
// NotebookLM sometimes redirects to a NEW notebook when adding text sources!
|
|
933
1094
|
const currentUrl = this.page.url();
|
|
@@ -1790,12 +1951,181 @@ export class ContentManager {
|
|
|
1790
1951
|
hasAudioOverview,
|
|
1791
1952
|
};
|
|
1792
1953
|
}
|
|
1954
|
+
normalizeSourceName(name) {
|
|
1955
|
+
return name.replace(/\s+/g, ' ').trim().toLowerCase();
|
|
1956
|
+
}
|
|
1957
|
+
extractSourceName(rawText) {
|
|
1958
|
+
const lines = rawText
|
|
1959
|
+
.replace(/\r/g, '')
|
|
1960
|
+
.split('\n')
|
|
1961
|
+
.map((line) => line.replace(/\s+/g, ' ').trim())
|
|
1962
|
+
.filter(Boolean);
|
|
1963
|
+
const ignoredExact = new Set([
|
|
1964
|
+
'sources',
|
|
1965
|
+
'chat',
|
|
1966
|
+
'studio',
|
|
1967
|
+
'add sources',
|
|
1968
|
+
'add source',
|
|
1969
|
+
'select all sources',
|
|
1970
|
+
'web',
|
|
1971
|
+
'fast research',
|
|
1972
|
+
'description',
|
|
1973
|
+
]);
|
|
1974
|
+
for (const line of lines) {
|
|
1975
|
+
const lower = line.toLowerCase();
|
|
1976
|
+
if (ignoredExact.has(lower))
|
|
1977
|
+
continue;
|
|
1978
|
+
if (lower.includes('search the web for new sources'))
|
|
1979
|
+
continue;
|
|
1980
|
+
if (/^(check|drive_pdf|markdown|description|more_vert|more_horiz)$/i.test(line))
|
|
1981
|
+
continue;
|
|
1982
|
+
return line;
|
|
1983
|
+
}
|
|
1984
|
+
return null;
|
|
1985
|
+
}
|
|
1986
|
+
async getVisibleSourceRows(dedupe = true) {
|
|
1987
|
+
await this.ensureSourcesPanel();
|
|
1988
|
+
const rowReadySelectors = [
|
|
1989
|
+
'.single-source-container',
|
|
1990
|
+
'mat-checkbox.select-checkbox',
|
|
1991
|
+
'.source-stretched-button',
|
|
1992
|
+
];
|
|
1993
|
+
for (const selector of rowReadySelectors) {
|
|
1994
|
+
try {
|
|
1995
|
+
await this.page.waitForSelector(selector, { state: 'attached', timeout: 2000 });
|
|
1996
|
+
break;
|
|
1997
|
+
}
|
|
1998
|
+
catch {
|
|
1999
|
+
continue;
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
await randomDelay(1200, 1800);
|
|
2003
|
+
const rows = [];
|
|
2004
|
+
const seen = new Set();
|
|
2005
|
+
const selectors = ['.single-source-container', 'mat-checkbox', '[class*="source-item"]'];
|
|
2006
|
+
for (const selector of selectors) {
|
|
2007
|
+
try {
|
|
2008
|
+
const elements = await this.page.$$(selector);
|
|
2009
|
+
if (elements.length === 0)
|
|
2010
|
+
continue;
|
|
2011
|
+
for (const element of elements) {
|
|
2012
|
+
try {
|
|
2013
|
+
const buttonLabel = (await element
|
|
2014
|
+
.$eval('.source-stretched-button', (node) => node.getAttribute('aria-label') || '')
|
|
2015
|
+
.catch(() => '')) || '';
|
|
2016
|
+
const checkboxLabel = (await element
|
|
2017
|
+
.$eval('input[type="checkbox"]', (node) => node.getAttribute('aria-label') || '')
|
|
2018
|
+
.catch(() => '')) || '';
|
|
2019
|
+
const text = (await element.textContent()) || '';
|
|
2020
|
+
const name = this.extractSourceName(buttonLabel) ||
|
|
2021
|
+
this.extractSourceName(checkboxLabel) ||
|
|
2022
|
+
this.extractSourceName(text);
|
|
2023
|
+
if (!name)
|
|
2024
|
+
continue;
|
|
2025
|
+
const normalized = this.normalizeSourceName(name);
|
|
2026
|
+
if (dedupe && seen.has(normalized))
|
|
2027
|
+
continue;
|
|
2028
|
+
seen.add(normalized);
|
|
2029
|
+
rows.push({ name, element });
|
|
2030
|
+
}
|
|
2031
|
+
catch {
|
|
2032
|
+
continue;
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
if (rows.length > 0) {
|
|
2036
|
+
break;
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
catch {
|
|
2040
|
+
continue;
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
if (rows.length === 0) {
|
|
2044
|
+
log.warning(' ⚠️ No visible source rows parsed from current NotebookLM UI');
|
|
2045
|
+
}
|
|
2046
|
+
return rows;
|
|
2047
|
+
}
|
|
2048
|
+
async getVisibleSourceNames() {
|
|
2049
|
+
const rows = await this.getVisibleSourceRows();
|
|
2050
|
+
return rows.map((row) => row.name);
|
|
2051
|
+
}
|
|
2052
|
+
async getAllSourceLabels() {
|
|
2053
|
+
const rows = await this.getVisibleSourceRows(false);
|
|
2054
|
+
return rows.map((row) => row.name);
|
|
2055
|
+
}
|
|
2056
|
+
findAddedSourceName(previousLabels, currentLabels) {
|
|
2057
|
+
const counts = new Map();
|
|
2058
|
+
for (const label of previousLabels) {
|
|
2059
|
+
const normalized = this.normalizeSourceName(label);
|
|
2060
|
+
counts.set(normalized, (counts.get(normalized) || 0) + 1);
|
|
2061
|
+
}
|
|
2062
|
+
for (const label of currentLabels) {
|
|
2063
|
+
const normalized = this.normalizeSourceName(label);
|
|
2064
|
+
const count = counts.get(normalized) || 0;
|
|
2065
|
+
if (count === 0) {
|
|
2066
|
+
return label;
|
|
2067
|
+
}
|
|
2068
|
+
counts.set(normalized, count - 1);
|
|
2069
|
+
}
|
|
2070
|
+
return undefined;
|
|
2071
|
+
}
|
|
2072
|
+
async findVisibleSourceRow(sourceId, sourceName) {
|
|
2073
|
+
const rows = await this.getVisibleSourceRows(false);
|
|
2074
|
+
if (rows.length === 0) {
|
|
2075
|
+
return null;
|
|
2076
|
+
}
|
|
2077
|
+
const normalizedTargetName = sourceName ? this.normalizeSourceName(sourceName) : null;
|
|
2078
|
+
for (const [index, row] of rows.entries()) {
|
|
2079
|
+
if (sourceId && sourceId === `source-${index}`) {
|
|
2080
|
+
return row;
|
|
2081
|
+
}
|
|
2082
|
+
if (!normalizedTargetName) {
|
|
2083
|
+
continue;
|
|
2084
|
+
}
|
|
2085
|
+
const normalizedRowName = this.normalizeSourceName(row.name);
|
|
2086
|
+
if (normalizedRowName === normalizedTargetName ||
|
|
2087
|
+
normalizedRowName.includes(normalizedTargetName) ||
|
|
2088
|
+
normalizedTargetName.includes(normalizedRowName)) {
|
|
2089
|
+
return row;
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
return null;
|
|
2093
|
+
}
|
|
2094
|
+
async dismissBlockingOverlays() {
|
|
2095
|
+
const backdrop = this.page
|
|
2096
|
+
.locator('.cdk-overlay-backdrop.cdk-overlay-backdrop-showing')
|
|
2097
|
+
.first();
|
|
2098
|
+
try {
|
|
2099
|
+
if (await backdrop.isVisible({ timeout: 300 })) {
|
|
2100
|
+
log.info(' ℹ️ Dismissing blocking overlay backdrop');
|
|
2101
|
+
await this.page.keyboard.press('Escape').catch(() => { });
|
|
2102
|
+
await randomDelay(200, 400);
|
|
2103
|
+
if (await backdrop.isVisible({ timeout: 300 }).catch(() => false)) {
|
|
2104
|
+
await backdrop.click({ force: true }).catch(() => { });
|
|
2105
|
+
await randomDelay(200, 400);
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
catch {
|
|
2110
|
+
// Ignore missing/backdrop timing issues
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
1793
2113
|
/**
|
|
1794
2114
|
* List all sources in the notebook
|
|
1795
2115
|
*/
|
|
1796
2116
|
async listSources() {
|
|
1797
2117
|
const sources = [];
|
|
1798
2118
|
try {
|
|
2119
|
+
const rows = await this.getVisibleSourceRows(false);
|
|
2120
|
+
if (rows.length > 0) {
|
|
2121
|
+
log.info(` 📚 Parsed ${rows.length} visible source rows`);
|
|
2122
|
+
return rows.map((row, index) => ({
|
|
2123
|
+
id: `source-${index}`,
|
|
2124
|
+
name: row.name,
|
|
2125
|
+
type: 'document',
|
|
2126
|
+
status: 'ready',
|
|
2127
|
+
}));
|
|
2128
|
+
}
|
|
1799
2129
|
// First ensure Sources panel is active
|
|
1800
2130
|
await this.ensureSourcesPanel();
|
|
1801
2131
|
await randomDelay(500, 800);
|
|
@@ -1919,8 +2249,10 @@ export class ContentManager {
|
|
|
1919
2249
|
// First, ensure we're on the Sources panel
|
|
1920
2250
|
await this.ensureSourcesPanel();
|
|
1921
2251
|
await randomDelay(500, 800);
|
|
2252
|
+
await this.dismissBlockingOverlays();
|
|
2253
|
+
const matchedRow = await this.findVisibleSourceRow(sourceId, sourceName);
|
|
1922
2254
|
// Find the source element
|
|
1923
|
-
const sourceElement = await this.findSourceElement(sourceId, sourceName);
|
|
2255
|
+
const sourceElement = matchedRow?.element ?? (await this.findSourceElement(sourceId, sourceName));
|
|
1924
2256
|
if (!sourceElement) {
|
|
1925
2257
|
return {
|
|
1926
2258
|
success: false,
|
|
@@ -1928,7 +2260,7 @@ export class ContentManager {
|
|
|
1928
2260
|
};
|
|
1929
2261
|
}
|
|
1930
2262
|
// Get the source name for logging before deletion
|
|
1931
|
-
let deletedSourceName = sourceName;
|
|
2263
|
+
let deletedSourceName = sourceName || matchedRow?.name;
|
|
1932
2264
|
if (!deletedSourceName) {
|
|
1933
2265
|
try {
|
|
1934
2266
|
deletedSourceName = await sourceElement.$eval('.source-name, .title, [class*="name"], [class*="title"]', (e) => e.textContent?.trim() || 'Unknown');
|
|
@@ -1937,15 +2269,12 @@ export class ContentManager {
|
|
|
1937
2269
|
deletedSourceName = sourceId || 'Unknown';
|
|
1938
2270
|
}
|
|
1939
2271
|
}
|
|
1940
|
-
// Click on the source to select it
|
|
1941
|
-
await sourceElement.click();
|
|
1942
|
-
await randomDelay(300, 500);
|
|
1943
2272
|
// Open the source menu (3-dot menu or right-click)
|
|
1944
2273
|
const menuOpened = await this.openSourceMenu(sourceElement);
|
|
1945
2274
|
if (!menuOpened) {
|
|
1946
2275
|
// Try right-click as fallback
|
|
1947
2276
|
log.info(` 🔍 Trying right-click on source...`);
|
|
1948
|
-
await sourceElement.click({ button: 'right' });
|
|
2277
|
+
await sourceElement.click({ button: 'right', force: true });
|
|
1949
2278
|
await randomDelay(300, 500);
|
|
1950
2279
|
}
|
|
1951
2280
|
// Click delete option
|
|
@@ -1961,7 +2290,9 @@ export class ContentManager {
|
|
|
1961
2290
|
// Wait for source to be removed
|
|
1962
2291
|
await randomDelay(1000, 2000);
|
|
1963
2292
|
// Verify deletion by checking if source is still present
|
|
1964
|
-
const stillExists =
|
|
2293
|
+
const stillExists = deletedSourceName
|
|
2294
|
+
? await this.findSourceElement(undefined, deletedSourceName)
|
|
2295
|
+
: await this.findSourceElement(sourceId, sourceName);
|
|
1965
2296
|
if (stillExists) {
|
|
1966
2297
|
return {
|
|
1967
2298
|
success: false,
|
|
@@ -1988,6 +2319,11 @@ export class ContentManager {
|
|
|
1988
2319
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1989
2320
|
) {
|
|
1990
2321
|
log.info(` 🔍 Finding source: id="${sourceId}", name="${sourceName}"`);
|
|
2322
|
+
const visibleRow = await this.findVisibleSourceRow(sourceId, sourceName);
|
|
2323
|
+
if (visibleRow) {
|
|
2324
|
+
log.info(` ✅ Found source via visible source rows: "${visibleRow.name}"`);
|
|
2325
|
+
return visibleRow.element;
|
|
2326
|
+
}
|
|
1991
2327
|
// METHOD 1: Direct text search (most reliable for NotebookLM)
|
|
1992
2328
|
if (sourceName) {
|
|
1993
2329
|
const directSelectors = [
|
|
@@ -2080,6 +2416,7 @@ export class ContentManager {
|
|
|
2080
2416
|
async openSourceMenu(
|
|
2081
2417
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2082
2418
|
sourceElement) {
|
|
2419
|
+
await this.dismissBlockingOverlays();
|
|
2083
2420
|
const menuButtonSelectors = [
|
|
2084
2421
|
// Material Design 3-dot menu button
|
|
2085
2422
|
'button:has(mat-icon:has-text("more_vert"))',
|
|
@@ -2105,7 +2442,7 @@ export class ContentManager {
|
|
|
2105
2442
|
const isVisible = await menuBtn.isVisible();
|
|
2106
2443
|
if (isVisible) {
|
|
2107
2444
|
log.info(` ✅ Found menu button: ${selector}`);
|
|
2108
|
-
await menuBtn.click();
|
|
2445
|
+
await menuBtn.click({ force: true });
|
|
2109
2446
|
await randomDelay(300, 500);
|
|
2110
2447
|
return true;
|
|
2111
2448
|
}
|
|
@@ -2117,7 +2454,13 @@ export class ContentManager {
|
|
|
2117
2454
|
}
|
|
2118
2455
|
// Hover over the source to reveal hidden menu button
|
|
2119
2456
|
log.info(` 🔍 Hovering to reveal menu button...`);
|
|
2120
|
-
|
|
2457
|
+
try {
|
|
2458
|
+
await sourceElement.hover();
|
|
2459
|
+
}
|
|
2460
|
+
catch {
|
|
2461
|
+
log.warning(' ⚠️ Could not hover source row to reveal menu button');
|
|
2462
|
+
return false;
|
|
2463
|
+
}
|
|
2121
2464
|
await randomDelay(500, 800);
|
|
2122
2465
|
// Try again after hover
|
|
2123
2466
|
for (const selector of menuButtonSelectors) {
|
|
@@ -2127,7 +2470,7 @@ export class ContentManager {
|
|
|
2127
2470
|
const isVisible = await menuBtn.isVisible();
|
|
2128
2471
|
if (isVisible) {
|
|
2129
2472
|
log.info(` ✅ Found menu button after hover: ${selector}`);
|
|
2130
|
-
await menuBtn.click();
|
|
2473
|
+
await menuBtn.click({ force: true });
|
|
2131
2474
|
await randomDelay(300, 500);
|
|
2132
2475
|
return true;
|
|
2133
2476
|
}
|
|
@@ -2892,7 +3235,8 @@ export class ContentManager {
|
|
|
2892
3235
|
const btn = this.page.locator(selector).first();
|
|
2893
3236
|
if (await btn.isVisible({ timeout: 1000 })) {
|
|
2894
3237
|
log.info(` ✅ Found Add note button: ${selector}`);
|
|
2895
|
-
await
|
|
3238
|
+
await btn.scrollIntoViewIfNeeded();
|
|
3239
|
+
await btn.click({ force: true, timeout: 5000 });
|
|
2896
3240
|
addButtonFound = true;
|
|
2897
3241
|
await randomDelay(500, 1000);
|
|
2898
3242
|
break;
|