notebooklm-mcp-ultimate 2.3.0 → 2.3.2
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/dist/api/batch-execute-client.d.ts +9 -0
- package/dist/api/batch-execute-client.d.ts.map +1 -1
- package/dist/api/batch-execute-client.js +36 -0
- package/dist/api/batch-execute-client.js.map +1 -1
- package/dist/auth/auth-manager.d.ts +14 -0
- package/dist/auth/auth-manager.d.ts.map +1 -1
- package/dist/auth/auth-manager.js +55 -1
- package/dist/auth/auth-manager.js.map +1 -1
- package/dist/auth/cookie-store.d.ts +2 -1
- package/dist/auth/cookie-store.d.ts.map +1 -1
- package/dist/auth/cookie-store.js +16 -1
- package/dist/auth/cookie-store.js.map +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/operations/notebook-crud-operations.d.ts +5 -9
- package/dist/operations/notebook-crud-operations.d.ts.map +1 -1
- package/dist/operations/notebook-crud-operations.js +273 -70
- package/dist/operations/notebook-crud-operations.js.map +1 -1
- package/dist/session/browser-session.d.ts +70 -0
- package/dist/session/browser-session.d.ts.map +1 -1
- package/dist/session/browser-session.js +840 -5
- package/dist/session/browser-session.js.map +1 -1
- package/dist/session/shared-context-manager.d.ts.map +1 -1
- package/dist/session/shared-context-manager.js +1 -0
- package/dist/session/shared-context-manager.js.map +1 -1
- package/dist/tools/handlers/notebook-crud-handlers.d.ts +10 -0
- package/dist/tools/handlers/notebook-crud-handlers.d.ts.map +1 -1
- package/dist/tools/handlers/notebook-crud-handlers.js +37 -1
- package/dist/tools/handlers/notebook-crud-handlers.js.map +1 -1
- package/dist/tools/handlers/research-handlers.d.ts +7 -1
- package/dist/tools/handlers/research-handlers.d.ts.map +1 -1
- package/dist/tools/handlers/research-handlers.js +34 -1
- package/dist/tools/handlers/research-handlers.js.map +1 -1
- package/dist/tools/handlers/source-handlers.d.ts +96 -9
- package/dist/tools/handlers/source-handlers.d.ts.map +1 -1
- package/dist/tools/handlers/source-handlers.js +214 -78
- package/dist/tools/handlers/source-handlers.js.map +1 -1
- package/dist/tools/handlers.d.ts +105 -8
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +3 -3
- package/dist/tools/handlers.js.map +1 -1
- package/dist/utils/page-utils.d.ts +12 -0
- package/dist/utils/page-utils.d.ts.map +1 -1
- package/dist/utils/page-utils.js +47 -0
- package/dist/utils/page-utils.js.map +1 -1
- package/package.json +7 -3
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* Based on the Python implementation from browser_session.py
|
|
14
14
|
*/
|
|
15
15
|
import { humanType, randomDelay } from "../utils/stealth-utils.js";
|
|
16
|
-
import { waitForLatestAnswer, snapshotAllResponses, } from "../utils/page-utils.js";
|
|
16
|
+
import { waitForLatestAnswer, snapshotAllResponses, extractCSRFFromPage, } from "../utils/page-utils.js";
|
|
17
|
+
import { csrfManager } from "../api/csrf-manager.js";
|
|
17
18
|
import { CONFIG } from "../config.js";
|
|
18
19
|
import { log } from "../utils/logger.js";
|
|
19
20
|
import { RateLimitError } from "../errors.js";
|
|
@@ -67,13 +68,16 @@ export class BrowserSession {
|
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
70
|
log.success(` ✅ Created new page`);
|
|
71
|
+
// Set iPad Mini viewport for NotebookLM responsive UI
|
|
72
|
+
await this.page.setViewportSize({ width: 768, height: 1024 });
|
|
70
73
|
// Navigate to notebook
|
|
71
74
|
log.info(` 🌐 Navigating to: ${this.notebookUrl}`);
|
|
72
75
|
await this.page.goto(this.notebookUrl, {
|
|
73
76
|
waitUntil: "domcontentloaded",
|
|
74
77
|
timeout: CONFIG.browserTimeout,
|
|
75
78
|
});
|
|
76
|
-
// Wait for
|
|
79
|
+
// Wait for Angular to finish rendering (use load instead of networkidle to avoid timeout)
|
|
80
|
+
await this.page.waitForLoadState('load').catch(() => { });
|
|
77
81
|
await randomDelay(2000, 3000);
|
|
78
82
|
// Check if we need to login
|
|
79
83
|
const isAuthenticated = await this.authManager.validateCookiesExpiry(this.context);
|
|
@@ -106,6 +110,17 @@ export class BrowserSession {
|
|
|
106
110
|
// Wait for NotebookLM interface to load
|
|
107
111
|
log.info(` ⏳ Waiting for NotebookLM interface...`);
|
|
108
112
|
await this.waitForNotebookLMReady();
|
|
113
|
+
// Extract CSRF token from browser and set on API client
|
|
114
|
+
// This ensures API calls use the same session as the browser
|
|
115
|
+
if (this.page) {
|
|
116
|
+
const csrfToken = await extractCSRFFromPage(this.page);
|
|
117
|
+
if (csrfToken) {
|
|
118
|
+
csrfManager.setCachedToken(csrfToken);
|
|
119
|
+
// Also save CSRF to disk for cold-start API usage
|
|
120
|
+
await this.authManager.saveCSRFToken(csrfToken);
|
|
121
|
+
log.success(` 🔑 CSRF token synced to API client`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
109
124
|
this.initialized = true;
|
|
110
125
|
this.updateActivity();
|
|
111
126
|
log.success(`✅ Session ${this.sessionId} initialized successfully`);
|
|
@@ -137,7 +152,7 @@ export class BrowserSession {
|
|
|
137
152
|
// PRIMARY: Exact Python selector - textarea.query-box-input
|
|
138
153
|
log.info(" ⏳ Waiting for chat input (textarea.query-box-input)...");
|
|
139
154
|
await this.page.waitForSelector("textarea.query-box-input", {
|
|
140
|
-
timeout:
|
|
155
|
+
timeout: 15000, // Extended for headless tests
|
|
141
156
|
state: "visible", // ONLY check visibility (NO disabled check!)
|
|
142
157
|
});
|
|
143
158
|
log.success(" ✅ Chat input ready!");
|
|
@@ -145,10 +160,12 @@ export class BrowserSession {
|
|
|
145
160
|
catch {
|
|
146
161
|
// Fix 5: Try multiple locale-agnostic fallback selectors instead of German-only
|
|
147
162
|
const fallbackSelectors = [
|
|
163
|
+
'textarea[aria-label="Query box"]', // Exact aria-label (verified)
|
|
164
|
+
'textarea[placeholder="Start typing..."]', // Exact placeholder (verified)
|
|
165
|
+
'textarea[placeholder*="Start typing"]', // Partial match
|
|
166
|
+
'textarea.mat-mdc-autocomplete-trigger', // Material autocomplete
|
|
148
167
|
'textarea[aria-label*="query"]', // English partial
|
|
149
168
|
'textarea[aria-label*="question"]', // English
|
|
150
|
-
'textarea[aria-label*="Ask"]', // English
|
|
151
|
-
'textarea[aria-label="Feld für Anfragen"]', // German (original)
|
|
152
169
|
'textarea[role="textbox"]', // Generic fallback
|
|
153
170
|
];
|
|
154
171
|
let found = false;
|
|
@@ -599,6 +616,824 @@ export class BrowserSession {
|
|
|
599
616
|
throw error;
|
|
600
617
|
}
|
|
601
618
|
}
|
|
619
|
+
/**
|
|
620
|
+
* Add a text source to the notebook via browser UI
|
|
621
|
+
*
|
|
622
|
+
* @param title - Title for the source
|
|
623
|
+
* @param content - Text content to add
|
|
624
|
+
* @param sendProgress - Optional progress callback
|
|
625
|
+
* @returns true if source was added successfully
|
|
626
|
+
*/
|
|
627
|
+
async addTextSourceViaUI(title, content, sendProgress) {
|
|
628
|
+
if (!this.page || !this.initialized) {
|
|
629
|
+
throw new Error("Session not initialized");
|
|
630
|
+
}
|
|
631
|
+
log.info(`📝 [${this.sessionId}] Adding text source via UI: "${title}"`);
|
|
632
|
+
try {
|
|
633
|
+
const page = this.page;
|
|
634
|
+
await sendProgress?.("Opening source panel...", 1, 6);
|
|
635
|
+
// Dismiss any overlay/modal that might block clicks (e.g., new notebook onboarding)
|
|
636
|
+
const overlay = page.locator('.cdk-overlay-backdrop-showing');
|
|
637
|
+
if (await overlay.isVisible({ timeout: 1000 }).catch(() => false)) {
|
|
638
|
+
log.info(" 🔄 Dismissing overlay modal...");
|
|
639
|
+
await page.keyboard.press('Escape');
|
|
640
|
+
await randomDelay(500, 800);
|
|
641
|
+
}
|
|
642
|
+
// Click on Sources tab first (iPad Mini viewport uses role="tab")
|
|
643
|
+
const sourcesTab = page.locator('[role="tab"]:has-text("Sources")').first();
|
|
644
|
+
if (await sourcesTab.isVisible({ timeout: 3000 })) {
|
|
645
|
+
await sourcesTab.click();
|
|
646
|
+
await randomDelay(500, 800);
|
|
647
|
+
}
|
|
648
|
+
// Click the "Add sources" button (found via debug: button:has-text("Add sources"))
|
|
649
|
+
const addSourceSelectors = [
|
|
650
|
+
'button:has-text("Add sources")', // Primary - found via debug
|
|
651
|
+
'button[aria-label*="Add source"]',
|
|
652
|
+
'button[aria-label*="add source"]',
|
|
653
|
+
'[data-tooltip*="Add source"]',
|
|
654
|
+
'button.add-source-button',
|
|
655
|
+
];
|
|
656
|
+
let addButtonClicked = false;
|
|
657
|
+
for (const sel of addSourceSelectors) {
|
|
658
|
+
try {
|
|
659
|
+
const btn = page.locator(sel).first();
|
|
660
|
+
if (await btn.isVisible({ timeout: 2000 })) {
|
|
661
|
+
await btn.click();
|
|
662
|
+
addButtonClicked = true;
|
|
663
|
+
log.info(` ✅ Clicked add source button: ${sel}`);
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
catch {
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (!addButtonClicked) {
|
|
672
|
+
// Try keyboard shortcut or alternative approach
|
|
673
|
+
log.warning(" ⚠️ Could not find add source button, trying alternative...");
|
|
674
|
+
// Some UIs use a menu - try clicking on Sources header
|
|
675
|
+
const sourcesHeader = page.locator('text="Sources"').first();
|
|
676
|
+
if (await sourcesHeader.isVisible({ timeout: 2000 })) {
|
|
677
|
+
await sourcesHeader.click();
|
|
678
|
+
await randomDelay(500, 1000);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
await randomDelay(1000, 1500);
|
|
682
|
+
await sendProgress?.("Selecting text source option...", 2, 5);
|
|
683
|
+
// Select "Copied text" or "Paste text" option
|
|
684
|
+
const textOptionSelectors = [
|
|
685
|
+
'button:has-text("Copied text")', // Primary option
|
|
686
|
+
'button:has-text("Paste text")', // Alternative
|
|
687
|
+
'[role="menuitem"]:has-text("Copied text")',
|
|
688
|
+
'[role="menuitem"]:has-text("Paste text")',
|
|
689
|
+
'button:has-text("Text")', // Generic fallback
|
|
690
|
+
];
|
|
691
|
+
let textOptionClicked = false;
|
|
692
|
+
for (const sel of textOptionSelectors) {
|
|
693
|
+
try {
|
|
694
|
+
const opt = page.locator(sel).first();
|
|
695
|
+
if (await opt.isVisible({ timeout: 2000 })) {
|
|
696
|
+
await opt.click();
|
|
697
|
+
textOptionClicked = true;
|
|
698
|
+
log.info(` ✅ Selected text source option: ${sel}`);
|
|
699
|
+
break;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
catch {
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
if (!textOptionClicked) {
|
|
707
|
+
throw new Error("Could not find text source option in menu");
|
|
708
|
+
}
|
|
709
|
+
await randomDelay(1000, 1500);
|
|
710
|
+
await sendProgress?.("Filling in source details...", 3, 5);
|
|
711
|
+
// Fill in the title field
|
|
712
|
+
const titleInputSelectors = [
|
|
713
|
+
'input[placeholder*="title"]',
|
|
714
|
+
'input[placeholder*="Title"]',
|
|
715
|
+
'input[aria-label*="title"]',
|
|
716
|
+
'input[aria-label*="Title"]',
|
|
717
|
+
'input[placeholder*="name"]',
|
|
718
|
+
// German
|
|
719
|
+
'input[placeholder*="Titel"]',
|
|
720
|
+
// Generic - first input in dialog
|
|
721
|
+
'[role="dialog"] input:first-of-type',
|
|
722
|
+
];
|
|
723
|
+
for (const sel of titleInputSelectors) {
|
|
724
|
+
try {
|
|
725
|
+
const input = page.locator(sel).first();
|
|
726
|
+
if (await input.isVisible({ timeout: 2000 })) {
|
|
727
|
+
await input.fill(title);
|
|
728
|
+
log.info(` ✅ Filled title: ${sel}`);
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
catch {
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
// Fill in the content field (textarea)
|
|
737
|
+
const contentSelectors = [
|
|
738
|
+
'textarea[placeholder*="Paste"]',
|
|
739
|
+
'textarea[placeholder*="paste"]',
|
|
740
|
+
'textarea[placeholder*="content"]',
|
|
741
|
+
'textarea[aria-label*="content"]',
|
|
742
|
+
'textarea[aria-label*="text"]',
|
|
743
|
+
// German
|
|
744
|
+
'textarea[placeholder*="Einfügen"]',
|
|
745
|
+
// Generic - textarea in dialog
|
|
746
|
+
'[role="dialog"] textarea',
|
|
747
|
+
];
|
|
748
|
+
for (const sel of contentSelectors) {
|
|
749
|
+
try {
|
|
750
|
+
const textarea = page.locator(sel).first();
|
|
751
|
+
if (await textarea.isVisible({ timeout: 2000 })) {
|
|
752
|
+
await textarea.fill(content);
|
|
753
|
+
log.info(` ✅ Filled content: ${sel}`);
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
catch {
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
await randomDelay(500, 1000);
|
|
762
|
+
await sendProgress?.("Submitting source...", 4, 5);
|
|
763
|
+
// Click submit/insert button
|
|
764
|
+
const submitSelectors = [
|
|
765
|
+
'button:has-text("Insert")',
|
|
766
|
+
'button:has-text("Add")',
|
|
767
|
+
'button:has-text("Save")',
|
|
768
|
+
'button:has-text("Submit")',
|
|
769
|
+
// German
|
|
770
|
+
'button:has-text("Einfügen")',
|
|
771
|
+
'button:has-text("Hinzufügen")',
|
|
772
|
+
// Generic primary button in dialog
|
|
773
|
+
'[role="dialog"] button[type="submit"]',
|
|
774
|
+
'[role="dialog"] button.primary',
|
|
775
|
+
];
|
|
776
|
+
let submitted = false;
|
|
777
|
+
for (const sel of submitSelectors) {
|
|
778
|
+
try {
|
|
779
|
+
const btn = page.locator(sel).first();
|
|
780
|
+
if (await btn.isVisible({ timeout: 2000 })) {
|
|
781
|
+
await btn.click();
|
|
782
|
+
submitted = true;
|
|
783
|
+
log.info(` ✅ Clicked submit: ${sel}`);
|
|
784
|
+
break;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
catch {
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
if (!submitted) {
|
|
792
|
+
throw new Error("Could not find submit button");
|
|
793
|
+
}
|
|
794
|
+
// Wait for source to be processed
|
|
795
|
+
await randomDelay(2000, 3000);
|
|
796
|
+
await sendProgress?.("Source added, waiting for processing...", 5, 5);
|
|
797
|
+
this.updateActivity();
|
|
798
|
+
log.success(`✅ [${this.sessionId}] Text source added via UI: "${title}"`);
|
|
799
|
+
return true;
|
|
800
|
+
}
|
|
801
|
+
catch (error) {
|
|
802
|
+
log.error(`❌ [${this.sessionId}] Failed to add text source via UI: ${error}`);
|
|
803
|
+
throw error;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Add a file source to the notebook via browser UI
|
|
808
|
+
*
|
|
809
|
+
* @param filePath - Local file path to upload
|
|
810
|
+
* @param sendProgress - Optional progress callback
|
|
811
|
+
* @returns true if source was added successfully
|
|
812
|
+
*/
|
|
813
|
+
async addFileSourceViaUI(filePath, sendProgress) {
|
|
814
|
+
if (!this.page || !this.initialized) {
|
|
815
|
+
throw new Error("Session not initialized");
|
|
816
|
+
}
|
|
817
|
+
log.info(`📁 [${this.sessionId}] Adding file source via UI: "${filePath}"`);
|
|
818
|
+
try {
|
|
819
|
+
const page = this.page;
|
|
820
|
+
await sendProgress?.("Opening source panel...", 1, 6);
|
|
821
|
+
// Dismiss any overlay/modal that might block clicks (e.g., new notebook onboarding)
|
|
822
|
+
const overlay = page.locator('.cdk-overlay-backdrop-showing');
|
|
823
|
+
if (await overlay.isVisible({ timeout: 1000 }).catch(() => false)) {
|
|
824
|
+
log.info(" 🔄 Dismissing overlay modal...");
|
|
825
|
+
await page.keyboard.press('Escape');
|
|
826
|
+
await randomDelay(500, 800);
|
|
827
|
+
}
|
|
828
|
+
// Click on Sources tab first (iPad Mini viewport uses role="tab")
|
|
829
|
+
const sourcesTab = page.locator('[role="tab"]:has-text("Sources")').first();
|
|
830
|
+
if (await sourcesTab.isVisible({ timeout: 3000 })) {
|
|
831
|
+
await sourcesTab.click();
|
|
832
|
+
await randomDelay(500, 800);
|
|
833
|
+
}
|
|
834
|
+
// Click add source button
|
|
835
|
+
const addBtn = page.locator('button:has-text("Add sources"), button[aria-label*="Add source"]').first();
|
|
836
|
+
await addBtn.click({ timeout: 5000 });
|
|
837
|
+
await randomDelay(1000, 1500);
|
|
838
|
+
await sendProgress?.("Selecting file upload option...", 2, 6);
|
|
839
|
+
// Select file upload option
|
|
840
|
+
const fileOptionSelectors = [
|
|
841
|
+
'button:has-text("Upload")',
|
|
842
|
+
'button:has-text("File")',
|
|
843
|
+
'[role="menuitem"]:has-text("Upload")',
|
|
844
|
+
'[role="menuitem"]:has-text("File")',
|
|
845
|
+
];
|
|
846
|
+
let selected = false;
|
|
847
|
+
for (const sel of fileOptionSelectors) {
|
|
848
|
+
try {
|
|
849
|
+
const opt = page.locator(sel).first();
|
|
850
|
+
if (await opt.isVisible({ timeout: 2000 })) {
|
|
851
|
+
await opt.click();
|
|
852
|
+
log.info(` ✅ Selected file option: ${sel}`);
|
|
853
|
+
selected = true;
|
|
854
|
+
break;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
catch {
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
if (!selected) {
|
|
862
|
+
throw new Error("Could not find file upload option");
|
|
863
|
+
}
|
|
864
|
+
await randomDelay(1500, 2000);
|
|
865
|
+
await sendProgress?.("Selecting file...", 3, 4);
|
|
866
|
+
// Look for the file input or a button that triggers file chooser
|
|
867
|
+
// NotebookLM may show a drag-drop area with a "browse" link or hidden file input
|
|
868
|
+
const fileInputSelectors = [
|
|
869
|
+
'input[type="file"]',
|
|
870
|
+
'.cdk-overlay-pane input[type="file"]',
|
|
871
|
+
'[role="dialog"] input[type="file"]',
|
|
872
|
+
];
|
|
873
|
+
let fileInput = null;
|
|
874
|
+
for (const sel of fileInputSelectors) {
|
|
875
|
+
try {
|
|
876
|
+
const input = page.locator(sel).first();
|
|
877
|
+
// File inputs are often hidden, so just check if it exists
|
|
878
|
+
if (await input.count() > 0) {
|
|
879
|
+
fileInput = input;
|
|
880
|
+
log.info(` ✅ Found file input: ${sel}`);
|
|
881
|
+
break;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
catch {
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
if (fileInput) {
|
|
889
|
+
// Direct file input - use setInputFiles
|
|
890
|
+
await fileInput.setInputFiles(filePath);
|
|
891
|
+
log.info(` ✅ File set via input: ${filePath}`);
|
|
892
|
+
}
|
|
893
|
+
else {
|
|
894
|
+
// Try clicking a browse/choose button with file chooser
|
|
895
|
+
const browseSelectors = [
|
|
896
|
+
'button:has-text("browse")',
|
|
897
|
+
'button:has-text("Browse")',
|
|
898
|
+
'button:has-text("Choose")',
|
|
899
|
+
'a:has-text("browse")',
|
|
900
|
+
'.cdk-overlay-pane button',
|
|
901
|
+
];
|
|
902
|
+
let clicked = false;
|
|
903
|
+
for (const sel of browseSelectors) {
|
|
904
|
+
try {
|
|
905
|
+
const [fileChooser] = await Promise.all([
|
|
906
|
+
page.waitForEvent('filechooser', { timeout: 5000 }),
|
|
907
|
+
page.locator(sel).first().click({ timeout: 2000 })
|
|
908
|
+
]);
|
|
909
|
+
await fileChooser.setFiles(filePath);
|
|
910
|
+
log.info(` ✅ File selected via chooser: ${filePath}`);
|
|
911
|
+
clicked = true;
|
|
912
|
+
break;
|
|
913
|
+
}
|
|
914
|
+
catch {
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
if (!clicked) {
|
|
919
|
+
throw new Error("Could not find file input or browse button");
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
await sendProgress?.("Processing upload...", 4, 4);
|
|
923
|
+
await randomDelay(3000, 5000); // File upload takes time
|
|
924
|
+
this.updateActivity();
|
|
925
|
+
log.success(`✅ [${this.sessionId}] File source added via UI: "${filePath}"`);
|
|
926
|
+
return true;
|
|
927
|
+
}
|
|
928
|
+
catch (error) {
|
|
929
|
+
log.error(`❌ [${this.sessionId}] Failed to add file source via UI: ${error}`);
|
|
930
|
+
throw error;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Add a URL source to the notebook via browser UI
|
|
935
|
+
*
|
|
936
|
+
* @param url - URL to add as source
|
|
937
|
+
* @param sendProgress - Optional progress callback
|
|
938
|
+
* @returns true if source was added successfully
|
|
939
|
+
*/
|
|
940
|
+
async addURLSourceViaUI(url, sendProgress) {
|
|
941
|
+
if (!this.page || !this.initialized) {
|
|
942
|
+
throw new Error("Session not initialized");
|
|
943
|
+
}
|
|
944
|
+
log.info(`🔗 [${this.sessionId}] Adding URL source via UI: "${url}"`);
|
|
945
|
+
try {
|
|
946
|
+
const page = this.page;
|
|
947
|
+
await sendProgress?.("Opening source panel...", 1, 5);
|
|
948
|
+
// Dismiss any overlay/modal that might block clicks (e.g., new notebook onboarding)
|
|
949
|
+
const overlay = page.locator('.cdk-overlay-backdrop-showing');
|
|
950
|
+
if (await overlay.isVisible({ timeout: 1000 }).catch(() => false)) {
|
|
951
|
+
log.info(" 🔄 Dismissing overlay modal...");
|
|
952
|
+
await page.keyboard.press('Escape');
|
|
953
|
+
await randomDelay(500, 800);
|
|
954
|
+
}
|
|
955
|
+
// Click on Sources tab first (iPad Mini viewport uses role="tab")
|
|
956
|
+
const sourcesTab = page.locator('[role="tab"]:has-text("Sources")').first();
|
|
957
|
+
if (await sourcesTab.isVisible({ timeout: 3000 })) {
|
|
958
|
+
await sourcesTab.click();
|
|
959
|
+
await randomDelay(500, 800);
|
|
960
|
+
}
|
|
961
|
+
// Click add source button
|
|
962
|
+
const addBtn = page.locator('button:has-text("Add sources"), button[aria-label*="Add source"]').first();
|
|
963
|
+
await addBtn.click({ timeout: 5000 });
|
|
964
|
+
await randomDelay(1000, 1500);
|
|
965
|
+
await sendProgress?.("Selecting website option...", 2, 5);
|
|
966
|
+
// Select website/URL option
|
|
967
|
+
const urlOptionSelectors = [
|
|
968
|
+
'button:has-text("Websites")',
|
|
969
|
+
'button:has-text("Website")',
|
|
970
|
+
'[role="menuitem"]:has-text("Websites")',
|
|
971
|
+
'[role="menuitem"]:has-text("Website")',
|
|
972
|
+
];
|
|
973
|
+
for (const sel of urlOptionSelectors) {
|
|
974
|
+
try {
|
|
975
|
+
const opt = page.locator(sel).first();
|
|
976
|
+
if (await opt.isVisible({ timeout: 2000 })) {
|
|
977
|
+
await opt.click();
|
|
978
|
+
log.info(` ✅ Selected URL option: ${sel}`);
|
|
979
|
+
break;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
catch {
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
await randomDelay(1500, 2000);
|
|
987
|
+
await sendProgress?.("Entering URL...", 3, 4);
|
|
988
|
+
// Fill URL textarea - NotebookLM uses textarea with "Paste any links" placeholder
|
|
989
|
+
const urlTextareaSelectors = [
|
|
990
|
+
'textarea[placeholder*="Paste any links"]',
|
|
991
|
+
'textarea[placeholder*="Paste"]',
|
|
992
|
+
'.cdk-overlay-pane textarea',
|
|
993
|
+
'[role="dialog"] textarea',
|
|
994
|
+
];
|
|
995
|
+
let filled = false;
|
|
996
|
+
for (const sel of urlTextareaSelectors) {
|
|
997
|
+
try {
|
|
998
|
+
const textarea = page.locator(sel).first();
|
|
999
|
+
if (await textarea.isVisible({ timeout: 1500 })) {
|
|
1000
|
+
await textarea.fill(url);
|
|
1001
|
+
log.info(` ✅ Filled URL: ${sel}`);
|
|
1002
|
+
filled = true;
|
|
1003
|
+
break;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
catch {
|
|
1007
|
+
continue;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
if (!filled) {
|
|
1011
|
+
throw new Error("Could not find URL textarea");
|
|
1012
|
+
}
|
|
1013
|
+
await randomDelay(500, 1000);
|
|
1014
|
+
await sendProgress?.("Submitting...", 4, 4);
|
|
1015
|
+
// Submit - look in dialog context
|
|
1016
|
+
const submitSelectors = [
|
|
1017
|
+
'.cdk-overlay-pane button:has-text("Insert")',
|
|
1018
|
+
'[role="dialog"] button:has-text("Insert")',
|
|
1019
|
+
'button:has-text("Insert")',
|
|
1020
|
+
];
|
|
1021
|
+
for (const sel of submitSelectors) {
|
|
1022
|
+
try {
|
|
1023
|
+
const btn = page.locator(sel).first();
|
|
1024
|
+
if (await btn.isVisible({ timeout: 1500 })) {
|
|
1025
|
+
await btn.click();
|
|
1026
|
+
log.info(` ✅ Clicked submit: ${sel}`);
|
|
1027
|
+
break;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
catch {
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
// Wait for dialog to close
|
|
1035
|
+
await page.waitForSelector('textarea[placeholder*="Paste any links"]', {
|
|
1036
|
+
state: 'hidden',
|
|
1037
|
+
timeout: 30000
|
|
1038
|
+
}).catch(() => { });
|
|
1039
|
+
await randomDelay(2000, 3000);
|
|
1040
|
+
// Verify source was added by checking source count
|
|
1041
|
+
const sourceCount = await page.locator('.source-item, [data-source-id], mat-list-item').count();
|
|
1042
|
+
const sourceAdded = sourceCount > 0;
|
|
1043
|
+
this.updateActivity();
|
|
1044
|
+
if (sourceAdded) {
|
|
1045
|
+
log.success(`✅ [${this.sessionId}] URL source added via UI: "${url}" (${sourceCount} sources)`);
|
|
1046
|
+
return true;
|
|
1047
|
+
}
|
|
1048
|
+
else {
|
|
1049
|
+
log.warning(`⚠️ [${this.sessionId}] Source may not have been added: "${url}"`);
|
|
1050
|
+
return true; // Still return true as click succeeded, source might be processing
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
catch (error) {
|
|
1054
|
+
log.error(`❌ [${this.sessionId}] Failed to add URL source via UI: ${error}`);
|
|
1055
|
+
throw error;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Add a YouTube source to the notebook via browser UI
|
|
1060
|
+
*
|
|
1061
|
+
* @param youtubeUrl - YouTube video URL
|
|
1062
|
+
* @param sendProgress - Optional progress callback
|
|
1063
|
+
* @returns true if source was added successfully
|
|
1064
|
+
*/
|
|
1065
|
+
async addYouTubeSourceViaUI(youtubeUrl, sendProgress) {
|
|
1066
|
+
if (!this.page || !this.initialized) {
|
|
1067
|
+
throw new Error("Session not initialized");
|
|
1068
|
+
}
|
|
1069
|
+
log.info(`🎥 [${this.sessionId}] Adding YouTube source via UI: "${youtubeUrl}"`);
|
|
1070
|
+
try {
|
|
1071
|
+
const page = this.page;
|
|
1072
|
+
await sendProgress?.("Opening source panel...", 1, 6);
|
|
1073
|
+
// Dismiss any overlay/modal that might block clicks (e.g., new notebook onboarding)
|
|
1074
|
+
const overlay = page.locator('.cdk-overlay-backdrop-showing');
|
|
1075
|
+
if (await overlay.isVisible({ timeout: 1000 }).catch(() => false)) {
|
|
1076
|
+
log.info(" 🔄 Dismissing overlay modal...");
|
|
1077
|
+
await page.keyboard.press('Escape');
|
|
1078
|
+
await randomDelay(500, 800);
|
|
1079
|
+
}
|
|
1080
|
+
// Click on Sources tab first (iPad Mini viewport uses role="tab")
|
|
1081
|
+
const sourcesTab = page.locator('[role="tab"]:has-text("Sources")').first();
|
|
1082
|
+
if (await sourcesTab.isVisible({ timeout: 3000 })) {
|
|
1083
|
+
await sourcesTab.click();
|
|
1084
|
+
await randomDelay(500, 800);
|
|
1085
|
+
}
|
|
1086
|
+
// Click add source button
|
|
1087
|
+
const addBtn = page.locator('button:has-text("Add sources"), button[aria-label*="Add source"]').first();
|
|
1088
|
+
await addBtn.click({ timeout: 5000 });
|
|
1089
|
+
await randomDelay(1000, 1500);
|
|
1090
|
+
await sendProgress?.("Selecting Website/YouTube option...", 2, 6);
|
|
1091
|
+
// Select Website option - opens "Website and YouTube URLs" dialog
|
|
1092
|
+
const urlOptionSelectors = [
|
|
1093
|
+
'button:has-text("Website")',
|
|
1094
|
+
'[role="menuitem"]:has-text("Website")',
|
|
1095
|
+
'button:has-text("Link")',
|
|
1096
|
+
'button:has-text("URL")',
|
|
1097
|
+
'button:has-text("YouTube")',
|
|
1098
|
+
'[role="menuitem"]:has-text("YouTube")',
|
|
1099
|
+
];
|
|
1100
|
+
let selected = false;
|
|
1101
|
+
for (const sel of urlOptionSelectors) {
|
|
1102
|
+
try {
|
|
1103
|
+
const opt = page.locator(sel).first();
|
|
1104
|
+
if (await opt.isVisible({ timeout: 2000 })) {
|
|
1105
|
+
await opt.click();
|
|
1106
|
+
log.info(` ✅ Selected URL option: ${sel}`);
|
|
1107
|
+
selected = true;
|
|
1108
|
+
break;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
catch {
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
if (!selected) {
|
|
1116
|
+
throw new Error("Could not find Website/YouTube source option");
|
|
1117
|
+
}
|
|
1118
|
+
await randomDelay(2000, 2500); // Wait for dialog to fully render
|
|
1119
|
+
await sendProgress?.("Entering YouTube URL...", 3, 4);
|
|
1120
|
+
// Fill URL textarea - NotebookLM uses textarea with "Paste any links" placeholder
|
|
1121
|
+
const urlTextareaSelectors = [
|
|
1122
|
+
'textarea[placeholder*="Paste any links"]',
|
|
1123
|
+
'textarea[placeholder*="Paste"]',
|
|
1124
|
+
'textarea[placeholder*="paste"]',
|
|
1125
|
+
'textarea[placeholder*="link"]',
|
|
1126
|
+
'textarea[placeholder*="URL"]',
|
|
1127
|
+
'.cdk-overlay-pane textarea',
|
|
1128
|
+
'[role="dialog"] textarea',
|
|
1129
|
+
];
|
|
1130
|
+
let filled = false;
|
|
1131
|
+
for (const sel of urlTextareaSelectors) {
|
|
1132
|
+
try {
|
|
1133
|
+
const textarea = page.locator(sel).first();
|
|
1134
|
+
if (await textarea.isVisible({ timeout: 1500 })) {
|
|
1135
|
+
await textarea.fill(youtubeUrl);
|
|
1136
|
+
log.info(` ✅ Filled YouTube URL: ${sel}`);
|
|
1137
|
+
filled = true;
|
|
1138
|
+
break;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
catch {
|
|
1142
|
+
continue;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
if (!filled) {
|
|
1146
|
+
throw new Error("Could not find URL textarea");
|
|
1147
|
+
}
|
|
1148
|
+
await randomDelay(500, 1000);
|
|
1149
|
+
await sendProgress?.("Submitting...", 4, 4);
|
|
1150
|
+
// Submit - look in dialog context first
|
|
1151
|
+
const submitSelectors = [
|
|
1152
|
+
'.cdk-overlay-pane button:has-text("Insert")',
|
|
1153
|
+
'.cdk-overlay-pane button:has-text("Add")',
|
|
1154
|
+
'[role="dialog"] button:has-text("Insert")',
|
|
1155
|
+
'[role="dialog"] button:has-text("Add")',
|
|
1156
|
+
'button:has-text("Insert")',
|
|
1157
|
+
'button:has-text("Add")',
|
|
1158
|
+
'button[type="submit"]',
|
|
1159
|
+
];
|
|
1160
|
+
let submitted = false;
|
|
1161
|
+
for (const sel of submitSelectors) {
|
|
1162
|
+
try {
|
|
1163
|
+
const btn = page.locator(sel).first();
|
|
1164
|
+
if (await btn.isVisible({ timeout: 1500 })) {
|
|
1165
|
+
await btn.click();
|
|
1166
|
+
log.info(` ✅ Clicked submit: ${sel}`);
|
|
1167
|
+
submitted = true;
|
|
1168
|
+
break;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
catch {
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
if (!submitted) {
|
|
1176
|
+
log.warning(' ⚠️ Submit button not found, dialog may have auto-submitted');
|
|
1177
|
+
}
|
|
1178
|
+
await randomDelay(3000, 4000); // YouTube processing takes longer
|
|
1179
|
+
this.updateActivity();
|
|
1180
|
+
log.success(`✅ [${this.sessionId}] YouTube source added via UI: "${youtubeUrl}"`);
|
|
1181
|
+
return true;
|
|
1182
|
+
}
|
|
1183
|
+
catch (error) {
|
|
1184
|
+
log.error(`❌ [${this.sessionId}] Failed to add YouTube source via UI: ${error}`);
|
|
1185
|
+
throw error;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
/**
|
|
1189
|
+
* Search the web for new sources and add them to the notebook
|
|
1190
|
+
*
|
|
1191
|
+
* @param query - Search query
|
|
1192
|
+
* @param sendProgress - Optional progress callback
|
|
1193
|
+
* @returns true if search was submitted successfully
|
|
1194
|
+
*/
|
|
1195
|
+
async searchWebForSourcesViaUI(query, sendProgress) {
|
|
1196
|
+
if (!this.page || !this.initialized) {
|
|
1197
|
+
throw new Error("Session not initialized");
|
|
1198
|
+
}
|
|
1199
|
+
log.info(`🔍 [${this.sessionId}] Searching web for sources: "${query}"`);
|
|
1200
|
+
try {
|
|
1201
|
+
const page = this.page;
|
|
1202
|
+
await sendProgress?.("Opening search panel...", 1, 3);
|
|
1203
|
+
// Find the search input in the sources panel
|
|
1204
|
+
const searchInputSelectors = [
|
|
1205
|
+
'input[placeholder*="Search the web"]',
|
|
1206
|
+
'input[placeholder*="Search"]',
|
|
1207
|
+
'input[aria-label*="Search"]',
|
|
1208
|
+
'.sources-panel input[type="text"]',
|
|
1209
|
+
];
|
|
1210
|
+
let searchInput = null;
|
|
1211
|
+
for (const sel of searchInputSelectors) {
|
|
1212
|
+
try {
|
|
1213
|
+
const input = page.locator(sel).first();
|
|
1214
|
+
if (await input.isVisible({ timeout: 2000 })) {
|
|
1215
|
+
searchInput = input;
|
|
1216
|
+
log.info(` ✅ Found search input: ${sel}`);
|
|
1217
|
+
break;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
catch {
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
if (!searchInput) {
|
|
1225
|
+
throw new Error("Could not find web search input");
|
|
1226
|
+
}
|
|
1227
|
+
await sendProgress?.("Entering search query...", 2, 3);
|
|
1228
|
+
// Fill the search query
|
|
1229
|
+
await searchInput.fill(query);
|
|
1230
|
+
await randomDelay(500, 1000);
|
|
1231
|
+
await sendProgress?.("Submitting search...", 3, 3);
|
|
1232
|
+
// Click the submit button (arrow icon)
|
|
1233
|
+
const submitSelectors = [
|
|
1234
|
+
'button[aria-label*="Search"]',
|
|
1235
|
+
'button[aria-label*="Submit"]',
|
|
1236
|
+
'button:has(mat-icon:has-text("arrow_forward"))',
|
|
1237
|
+
'button:has(span:has-text("arrow_forward"))',
|
|
1238
|
+
'.sources-panel button[type="submit"]',
|
|
1239
|
+
];
|
|
1240
|
+
let submitted = false;
|
|
1241
|
+
for (const sel of submitSelectors) {
|
|
1242
|
+
try {
|
|
1243
|
+
const btn = page.locator(sel).first();
|
|
1244
|
+
if (await btn.isVisible({ timeout: 1500 })) {
|
|
1245
|
+
await btn.click();
|
|
1246
|
+
log.info(` ✅ Clicked search submit: ${sel}`);
|
|
1247
|
+
submitted = true;
|
|
1248
|
+
break;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
catch {
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
if (!submitted) {
|
|
1256
|
+
// Try pressing Enter as fallback
|
|
1257
|
+
await searchInput.press('Enter');
|
|
1258
|
+
log.info(` ✅ Pressed Enter to submit search`);
|
|
1259
|
+
}
|
|
1260
|
+
await randomDelay(3000, 5000); // Wait for search results
|
|
1261
|
+
this.updateActivity();
|
|
1262
|
+
log.success(`✅ [${this.sessionId}] Web search submitted: "${query}"`);
|
|
1263
|
+
return true;
|
|
1264
|
+
}
|
|
1265
|
+
catch (error) {
|
|
1266
|
+
log.error(`❌ [${this.sessionId}] Failed to search web for sources: ${error}`);
|
|
1267
|
+
throw error;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* List sources in the notebook via browser UI
|
|
1272
|
+
*
|
|
1273
|
+
* @returns Array of source objects with title and id
|
|
1274
|
+
*/
|
|
1275
|
+
async listSourcesViaUI() {
|
|
1276
|
+
if (!this.page || !this.initialized) {
|
|
1277
|
+
throw new Error("Session not initialized");
|
|
1278
|
+
}
|
|
1279
|
+
log.info(`📚 [${this.sessionId}] Listing sources via UI...`);
|
|
1280
|
+
try {
|
|
1281
|
+
const page = this.page;
|
|
1282
|
+
// Click on Sources tab first (iPad Mini viewport uses role="tab")
|
|
1283
|
+
const sourcesTab = page.locator('[role="tab"]:has-text("Sources")').first();
|
|
1284
|
+
if (await sourcesTab.isVisible({ timeout: 5000 })) {
|
|
1285
|
+
await sourcesTab.click();
|
|
1286
|
+
await randomDelay(1000, 1500);
|
|
1287
|
+
}
|
|
1288
|
+
// Use JS evaluation to get source titles (most reliable method)
|
|
1289
|
+
// Sources have button.source-stretched-button with aria-label containing the title
|
|
1290
|
+
const sourceData = await page.evaluate(`
|
|
1291
|
+
Array.from(document.querySelectorAll('button[aria-label]'))
|
|
1292
|
+
.filter(b => b.classList.contains('source-stretched-button'))
|
|
1293
|
+
.map(b => ({
|
|
1294
|
+
title: b.getAttribute('aria-label') || '',
|
|
1295
|
+
id: b.closest('[draggable]')?.querySelector('[id*="source"]')?.id || undefined
|
|
1296
|
+
}))
|
|
1297
|
+
.filter(s => s.title.length > 0)
|
|
1298
|
+
`);
|
|
1299
|
+
log.success(`✅ [${this.sessionId}] Found ${sourceData.length} sources via UI`);
|
|
1300
|
+
this.updateActivity();
|
|
1301
|
+
return sourceData;
|
|
1302
|
+
}
|
|
1303
|
+
catch (error) {
|
|
1304
|
+
log.error(`❌ [${this.sessionId}] Failed to list sources via UI: ${error}`);
|
|
1305
|
+
return [];
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Delete a source by title via browser UI
|
|
1310
|
+
*
|
|
1311
|
+
* @param sourceTitle - The title/name of the source to delete
|
|
1312
|
+
* @param sendProgress - Optional progress callback
|
|
1313
|
+
* @returns true if source was deleted successfully
|
|
1314
|
+
*/
|
|
1315
|
+
async deleteSourceViaUI(sourceTitle, sendProgress) {
|
|
1316
|
+
if (!this.page || !this.initialized) {
|
|
1317
|
+
throw new Error("Session not initialized");
|
|
1318
|
+
}
|
|
1319
|
+
log.info(`🗑️ [${this.sessionId}] Deleting source via UI: ${sourceTitle}`);
|
|
1320
|
+
try {
|
|
1321
|
+
const page = this.page;
|
|
1322
|
+
// Click on Sources tab first
|
|
1323
|
+
const sourcesTab = page.locator('[role="tab"]:has-text("Sources")').first();
|
|
1324
|
+
if (await sourcesTab.isVisible({ timeout: 5000 })) {
|
|
1325
|
+
await sourcesTab.click();
|
|
1326
|
+
await randomDelay(1000, 1500);
|
|
1327
|
+
}
|
|
1328
|
+
// Find the source button by aria-label (title)
|
|
1329
|
+
const sourceButton = page.locator(`button.source-stretched-button[aria-label="${sourceTitle}"]`).first();
|
|
1330
|
+
if (!await sourceButton.isVisible({ timeout: 5000 })) {
|
|
1331
|
+
log.error(`❌ [${this.sessionId}] Source not found: ${sourceTitle}`);
|
|
1332
|
+
return false;
|
|
1333
|
+
}
|
|
1334
|
+
// Find the more button (⋮) in the same source container
|
|
1335
|
+
// IDs are like: source-item-more-button-{uuid}
|
|
1336
|
+
const sourceContainer = sourceButton.locator('xpath=ancestor::*[@draggable="true"]').first();
|
|
1337
|
+
const moreButton = sourceContainer.locator('[id^="source-item-more-button"], button[aria-label*="More"], button[aria-label*="more"], [aria-haspopup="menu"]').first();
|
|
1338
|
+
if (await moreButton.isVisible({ timeout: 3000 })) {
|
|
1339
|
+
await moreButton.click();
|
|
1340
|
+
log.info(` ✅ Clicked more button for: ${sourceTitle}`);
|
|
1341
|
+
await randomDelay(500, 800);
|
|
1342
|
+
}
|
|
1343
|
+
else {
|
|
1344
|
+
// Fallback: hover to reveal more button
|
|
1345
|
+
await sourceButton.hover();
|
|
1346
|
+
await randomDelay(500, 800);
|
|
1347
|
+
const hoverMoreBtn = sourceContainer.locator('[id^="source-item-more-button"], button[aria-label*="More"]').first();
|
|
1348
|
+
if (await hoverMoreBtn.isVisible({ timeout: 2000 })) {
|
|
1349
|
+
await hoverMoreBtn.click();
|
|
1350
|
+
await randomDelay(500, 800);
|
|
1351
|
+
}
|
|
1352
|
+
else {
|
|
1353
|
+
log.error(`❌ [${this.sessionId}] More button not found`);
|
|
1354
|
+
return false;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
// Click "Delete" from the menu
|
|
1358
|
+
const deleteMenuItem = page.locator('[role="menuitem"]:has-text("Delete"), button:has-text("Delete")').first();
|
|
1359
|
+
if (await deleteMenuItem.isVisible({ timeout: 3000 })) {
|
|
1360
|
+
await deleteMenuItem.click();
|
|
1361
|
+
log.info(` ✅ Clicked Delete menu item`);
|
|
1362
|
+
await randomDelay(500, 800);
|
|
1363
|
+
// Confirm deletion if dialog appears
|
|
1364
|
+
const confirmBtn = page.locator('button:has-text("Delete"):not([role="menuitem"])').first();
|
|
1365
|
+
if (await confirmBtn.isVisible({ timeout: 3000 })) {
|
|
1366
|
+
await confirmBtn.click();
|
|
1367
|
+
log.info(` ✅ Confirmed deletion`);
|
|
1368
|
+
await randomDelay(1000, 1500);
|
|
1369
|
+
}
|
|
1370
|
+
log.success(`✅ [${this.sessionId}] Source deleted: ${sourceTitle}`);
|
|
1371
|
+
if (sendProgress)
|
|
1372
|
+
await sendProgress(`✅ Source deleted: ${sourceTitle}`);
|
|
1373
|
+
this.updateActivity();
|
|
1374
|
+
return true;
|
|
1375
|
+
}
|
|
1376
|
+
log.error(`❌ [${this.sessionId}] Delete menu item not found`);
|
|
1377
|
+
return false;
|
|
1378
|
+
}
|
|
1379
|
+
catch (error) {
|
|
1380
|
+
log.error(`❌ [${this.sessionId}] Failed to delete source via UI: ${error}`);
|
|
1381
|
+
return false;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
/**
|
|
1385
|
+
* Get source details by clicking on it via browser UI
|
|
1386
|
+
*
|
|
1387
|
+
* @param sourceTitle - The title/name of the source
|
|
1388
|
+
* @returns Source details object or null if not found
|
|
1389
|
+
*/
|
|
1390
|
+
async getSourceDetailsViaUI(sourceTitle) {
|
|
1391
|
+
if (!this.page || !this.initialized) {
|
|
1392
|
+
throw new Error("Session not initialized");
|
|
1393
|
+
}
|
|
1394
|
+
log.info(`📄 [${this.sessionId}] Getting source details via UI: ${sourceTitle}`);
|
|
1395
|
+
try {
|
|
1396
|
+
const page = this.page;
|
|
1397
|
+
// Click on Sources tab first
|
|
1398
|
+
const sourcesTab = page.locator('[role="tab"]:has-text("Sources")').first();
|
|
1399
|
+
if (await sourcesTab.isVisible({ timeout: 5000 })) {
|
|
1400
|
+
await sourcesTab.click();
|
|
1401
|
+
await randomDelay(1000, 1500);
|
|
1402
|
+
}
|
|
1403
|
+
// Find and click the source to open details panel
|
|
1404
|
+
const sourceButton = page.locator(`button.source-stretched-button[aria-label="${sourceTitle}"]`).first();
|
|
1405
|
+
if (!await sourceButton.isVisible({ timeout: 5000 })) {
|
|
1406
|
+
log.error(`❌ [${this.sessionId}] Source not found: ${sourceTitle}`);
|
|
1407
|
+
return null;
|
|
1408
|
+
}
|
|
1409
|
+
await sourceButton.click();
|
|
1410
|
+
await randomDelay(1500, 2000);
|
|
1411
|
+
// Scrape details from the source panel
|
|
1412
|
+
const details = await page.evaluate(`
|
|
1413
|
+
(() => {
|
|
1414
|
+
// Look for source detail panel elements
|
|
1415
|
+
const titleEl = document.querySelector('[class*="source-title"], [class*="sourceTitle"], h2, h3');
|
|
1416
|
+
const typeEl = document.querySelector('[class*="source-type"], [class*="sourceType"]');
|
|
1417
|
+
const dateEl = document.querySelector('[class*="date"], time');
|
|
1418
|
+
const summaryEl = document.querySelector('[class*="summary"], [class*="description"], p');
|
|
1419
|
+
|
|
1420
|
+
return {
|
|
1421
|
+
title: titleEl?.textContent?.trim() || '${sourceTitle}',
|
|
1422
|
+
type: typeEl?.textContent?.trim() || undefined,
|
|
1423
|
+
addedDate: dateEl?.textContent?.trim() || undefined,
|
|
1424
|
+
summary: summaryEl?.textContent?.trim() || undefined
|
|
1425
|
+
};
|
|
1426
|
+
})()
|
|
1427
|
+
`);
|
|
1428
|
+
log.success(`✅ [${this.sessionId}] Got source details: ${sourceTitle}`);
|
|
1429
|
+
this.updateActivity();
|
|
1430
|
+
return details;
|
|
1431
|
+
}
|
|
1432
|
+
catch (error) {
|
|
1433
|
+
log.error(`❌ [${this.sessionId}] Failed to get source details via UI: ${error}`);
|
|
1434
|
+
return null;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
602
1437
|
/**
|
|
603
1438
|
* Close the session
|
|
604
1439
|
*/
|