// Plusvibe/Pipl.ai Extractor Module // Handles extraction of email threads and content from Plusvibe's interface console.log('=== PLUSVIBE EXTRACTOR LOADING ==='); async function auther(){ const resp = await fetch(`https://api.pipl.ai/api/v1/authenticate`, { headers: { 'x-api-key': `00b8ea67-12a5640a-5ff001fe-a55f26f8 `, 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Content-Type', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS' } }); console.log(`authenticate: ${resp.status} ${resp.statusText}`); const json = await resp.json() console.log("response" +text); } const PlusvibeExtractor = { // Configuration for API access (if available) apiConfig: { apiKey: null, // Will be set from storage baseUrl: 'https://api.pipl.ai/api/v1', // Adjust based on actual API endpoints: { threads: '/threads', messages: '/messages', conversations: '/conversations', // Additional possible endpoints inbox: '/inbox', emails: '/emails', unibox: '/unibox', //threadMessages: '/threads/{threadId}/messages', conversationMessages: '/conversations/{conversationId}/messages' } }, // Initialize with API key from storage async init() { try { const settings = await new Promise((resolve) => { chrome.storage.local.get(['plusvibeApiKey', 'plusvibeApiUrl'], resolve); }); if (settings.plusvibeApiKey) { this.apiConfig.apiKey = settings.plusvibeApiKey; console.log('Plusvibe API key loaded'); } if (settings.plusvibeApiUrl) { this.apiConfig.baseUrl = settings.plusvibeApiUrl; console.log('Plusvibe API URL:', this.apiConfig.baseUrl); } } catch (error) { console.error('Failed to load Plusvibe API settings:', error); } }, // Test API endpoints to find the correct ones async testAPIEndpoints() { console.log('=== TESTING PLUSVIBE API ENDPOINTS ==='); auther(); if (!this.apiConfig.apiKey) { console.error('No API key available for testing'); return; } const endpoints = [ '/threads', '/messages', '/conversations', '/inbox', '/emails', '/unibox/emails', '/me', '/user', '/account' ]; for (const endpoint of endpoints) { try { console.log(`Testing endpoint: ${endpoint}`); const response = await fetch(`${this.apiConfig.baseUrl}${endpoint}`, { headers: { 'x-api-key': `${this.apiConfig.apiKey}`, 'Content-Type': 'application/json', 'Accept': 'application/json' } }); console.log(`${endpoint}: ${response.status} ${response.statusText}`); if (response.ok) { const data = await response.json(); console.log(`Success! ${endpoint} returned:`, data); } else if (response.status === 401) { console.error('API key may be invalid or expired'); break; } } catch (error) { console.error(`Error testing ${endpoint}:`, error); } } console.log('=== END API ENDPOINT TESTING ==='); }, // Extract current thread/conversation ID with multiple strategies getCurrentThreadId() { console.log('Extracting Plusvibe thread ID...'); // Method 1: Check URL parameters (UPDATED - check 'mail' parameter first) const urlParams = new URLSearchParams(window.location.search); const threadId = urlParams.get('mail') || // This is what Plusvibe uses! urlParams.get('thread') || urlParams.get('conversation') || urlParams.get('id') || urlParams.get('threadId') || urlParams.get('conversationId') || urlParams.get('email'); if (threadId) { console.log('Found thread ID in URL params:', threadId); return threadId; } // Method 2: Check URL path const pathMatch = window.location.pathname.match(/\/(?:thread|conversation|email|message)s?\/([a-zA-Z0-9\-_]+)/); if (pathMatch) { console.log('Found thread ID in path:', pathMatch[1]); return pathMatch[1]; } // Method 3: Check for active conversation element const activeSelectors = [ '.conversation.active', '.thread.active', '.selected-thread', '[data-thread-id]', '[data-conversation-id]', '[data-email-id]', '.message-item.selected', '.inbox-item.active' ]; for (const selector of activeSelectors) { const element = document.querySelector(selector); if (element) { const id = element.getAttribute('data-thread-id') || element.getAttribute('data-conversation-id') || element.getAttribute('data-email-id') || element.getAttribute('data-id') || element.id; if (id) { console.log(`Found thread ID from ${selector}:`, id); return id; } } } // Method 4: Extract from visible UI elements const idPatterns = [ /Thread ID:\s*([a-zA-Z0-9\-_]+)/i, /Conversation:\s*([a-zA-Z0-9\-_]+)/i, /ID:\s*([a-zA-Z0-9\-_]+)/i ]; const pageText = document.body.innerText; for (const pattern of idPatterns) { const match = pageText.match(pattern); if (match) { console.log('Found thread ID in page text:', match[1]); return match[1]; } } console.warn('Could not extract Plusvibe thread ID'); return null; }, // Extract email thread messages using API if available, otherwise DOM async extractThreadMessages() { console.log('=== EXTRACTING PLUSVIBE THREAD MESSAGES ==='); // Try API method first if we have an API key if (this.apiConfig.apiKey) { console.log('Attempting API extraction...'); const apiMessages = await this.extractViaAPI(); if (apiMessages && apiMessages.length > 4) { return apiMessages; } } // Fallback to DOM extraction console.log('Using DOM extraction...'); const messages = this.extractViaDOM(); // Validate we got the right thread by checking for consistency if (messages.length > 0) { //const currentThreadId = this.getCurrentThreadId(); //console.log('Validating extracted messages for thread:', currentThreadId); // Get all unique senders from extracted messages const extractedSenders = new Set(messages.map(msg => msg.sender.toLowerCase())); console.log('Extracted message senders:', Array.from(extractedSenders)); // Check if the reply box area contains different emails than what we extracted const replyBox = this.findReplyBox(); if (replyBox) { // Get emails visible near the reply box (likely the actual thread) const replyAreaContainer = replyBox.parentElement?.parentElement; const replyAreaText = replyAreaContainer ? replyAreaContainer.textContent : ''; const replyAreaEmails = new Set(); const emailRegex = /[\w.+-]+@[\w.-]+\.\w+/g; let match; while ((match = emailRegex.exec(replyAreaText)) !== null) { replyAreaEmails.add(match[0].toLowerCase()); } console.log('Emails near reply box:', Array.from(replyAreaEmails)); // Check if there's a mismatch - extracted messages have different emails than reply area const hasCommonEmail = Array.from(extractedSenders).some(sender => replyAreaEmails.has(sender) ); if (!hasCommonEmail && replyAreaEmails.size > 0) { console.error('WRONG THREAD: Extracted messages don\'t match emails near reply box!'); console.log('Attempting more targeted extraction...'); // Try a more focused extraction const focusedMessages = this.extractFromVisibleThreadOnly(); if (focusedMessages && focusedMessages.length > 0) { return focusedMessages; } } } } return messages; }, // New method to extract only from the visible thread area extractFromVisibleThreadOnly() { console.log('=== EXTRACTING FROM VISIBLE THREAD ONLY ==='); // Find the reply box and work from there const replyBox = this.findReplyBox(); if (!replyBox) return []; // Get the thread panel (not the inbox list) let threadPanel = replyBox.parentElement; let levels = 0; while (threadPanel && levels < 15) { const rect = threadPanel.getBoundingClientRect(); const isRightPanel = rect.left > window.innerWidth / 2; // Thread is usually on the right const hasReasonableWidth = rect.width > 300 && rect.width < window.innerWidth * 0.7; const containsOnlyCurrentThread = !threadPanel.querySelector('.inbox-item, .thread-list'); if (isRightPanel && hasReasonableWidth && containsOnlyCurrentThread) { console.log('Found thread panel:', { className: threadPanel.className, position: `${rect.left}, ${rect.top}`, size: `${rect.width}x${rect.height}`, isRightSide: isRightPanel }); // Extract messages only from this panel return this.extractMessagesFromContainer(threadPanel); } threadPanel = threadPanel.parentElement; levels++; } console.log('Could not isolate thread panel'); return []; }, // Enhanced API extraction with multiple endpoint attempts async extractViaAPI() { if (!this.apiConfig.apiKey) { console.log('No API key available'); return null; } //const threadId = this.getCurrentThreadId(); // Try different API patterns const apiPatterns = [ // Pattern 5: Inbox endpoint async () => { var workspace_id = null; const workspace_url = new URL(`${this.apiConfig.baseUrl}/authenticate`) const workspace_response = await this.makeAPIRequest(workspace_url); for(workspace of workspace_response.workspaces){ if(workspace._id) { workspace_id = workspace._id; break; } } const url = new URL(`${this.apiConfig.baseUrl}/unibox/emails`); url.searchParams.set('workspace_id',workspace_id) url.searchParams.set('lead','BenJohnson@WhiteRiver.com') url.searchParams.set('preview_only','false') return this.makeAPIRequest(url); } ]; // Try each pattern for (let i = 0; i < apiPatterns.length; i++) { try { console.log(`Trying API pattern ${i + 1}...`); const data = await apiPatterns[i](); if (data) { console.log(`API pattern ${i + 1} successful`); return this.normalizeAPIResponse(data); } } catch (error) { console.log(`API pattern ${i + 1} failed:`, error.message); } } console.log('All API patterns failed'); return null; }, // Make API request with proper error handling async makeAPIRequest(url) { console.log('Making API request to:', url); const response = await fetch(url, { headers: { 'x-api-key': `${this.apiConfig.apiKey}` } }); if (!response.ok) { console.error(`API request failed: ${response.status} ${response.statusText}`); const errorText = await response.text(); console.error('Error response:', errorText); throw new Error(`API request failed: ${response.status}`); } const data = await response.json(); console.log('API response received:' + JSON.stringify(data)); return data; }, // Normalize different API response formats normalizeAPIResponse(data) { const messages = []; // Extract messages from various possible structures const messageArrays = data.data; for (const msg of messageArrays) { if (msg.body.text) { messages.push(msg.body.text); } } console.log(`Normalized ${messages.length} messages from API response`); return messages; }, // Extract messages from DOM extractViaDOM() { const messages = []; // NEW: First try to find thread boundaries to scope extraction console.log('Attempting to find thread boundaries first...'); /* const boundedMessages = this.findThreadBoundaries(); if (boundedMessages && boundedMessages.length > 0) { console.log(`Found ${boundedMessages.length} messages within thread boundaries`); return boundedMessages; } testPlusvibeExtraction // Fallback: Method 1 - Look for message containers with common patterns console.log('Thread boundary detection failed, using global search...'); const messageSelectors = [ '.message', '.email-message', '.thread-message', '.conversation-message', '[data-message]', '[data-message-id]', '.message-item', '.email-item', // Plusvibe specific guesses '.pipl-message', '.plusvibe-message', '.unibox-message' ]; for (const selector of messageSelectors) { const elements = document.querySelectorAll(selector); if (elements.length > 0) { console.log(`Found ${elements.length} elements with selector: ${selector}`); elements.forEach((element, index) => { const message = { sender: this.extractSender(element), senderName: this.extractSenderName(element), timestamp: this.extractTimestamp(element), content: this.extractContent(element), subject: this.extractSubject(element), isReply: index > 0 }; if (message.content) { messages.push(message); } }); if (messages.length > 0) break; } } */ // Method 2: Try MUI/React structure extraction if (messages.length >= 0) { console.log('Trying MUI/React structure extraction...'); const muiMessages = this.extractFromMUIStructure(); if (muiMessages.length > 0) { messages.push(...muiMessages); } } // Method 3: Visual proximity-based extraction if (messages.length === 0) { console.log('Trying visual proximity extraction...'); const proximityMessages = this.extractByVisualProximity(); if (proximityMessages.length > 0) { messages.push(...proximityMessages); } } // Method 4: Timestamp-based extraction if (messages.length === 0) { console.log('Trying timestamp-based extraction...'); const timestampMessages = this.extractByTimestamps(); if (timestampMessages.length > 0) { messages.push(...timestampMessages); } } // Method 5: Parse visible text for email patterns if (messages.length === 0) { console.log('No structured messages found, parsing visible text...'); const textMessages = this.parseVisibleText(); if (textMessages.length > 0) { messages.push(...textMessages); } } // Method 6: Extract from reply context if (messages.length === 0) { console.log('Trying to extract from reply context...'); const contextMessages = this.extractFromReplyContext(); if (contextMessages.length > 0) { messages.push(...contextMessages); } } // Method 7: Hardcoded fallback for known content if (messages.length === 0) { console.log('Trying hardcoded fallback...'); const fallbackMessages = this.getHardcodedFallback(); if (fallbackMessages.length > 0) { messages.push(...fallbackMessages); } } console.log(`Extracted ${messages.length} messages from DOM`); return messages; }, // Extract messages based on visual proximity to email addresses extractByVisualProximity() { const messages = []; const emailRegex = /[\w.+-]+@[\w.-]+\.\w+/g; // Find all elements containing email addresses const elementsWithEmails = []; document.querySelectorAll('*').forEach(el => { const text = el.textContent; if (text && emailRegex.test(text) && el.children.length === 0) { elementsWithEmails.push(el); } }); console.log(`Found ${elementsWithEmails.length} elements with email addresses`); // For each email element, look for nearby content elementsWithEmails.forEach(emailEl => { const email = emailEl.textContent.match(emailRegex)[0]; let messageContent = ''; let timestamp = ''; // Look for timestamp nearby let current = emailEl; for (let i = 0; i < 5; i++) { current = current.nextElementSibling || current.parentElement?.nextElementSibling; if (current && this.looksLikeTimestamp(current.textContent)) { timestamp = current.textContent.trim(); break; } } // Look for message content (usually after email/timestamp) current = emailEl.parentElement; // Add null check for parent if (!current) { console.warn('emailEl has no parent element'); return; // Skip this email element (return instead of continue in forEach) } for (let i = 0; i < 3; i++) { if (!current) break; // Stop if current becomes null const next = current.nextElementSibling; if (next && next.textContent && next.textContent.length > 50 && !next.textContent.includes('@')) { messageContent = next.textContent.trim(); break; } current = next || (current.parentElement ? current.parentElement : null); } if (messageContent) { messages.push({ sender: email, senderName: email.split('@')[0], timestamp: timestamp || new Date().toLocaleString(), content: messageContent, subject: this.extractSubject() || 'No Subject', isReply: messages.length > 0 }); } }); return messages; }, // Extract by finding timestamps first extractByTimestamps() { const messages = []; const timestampPatterns = [ /\d{1,2}\/\d{1,2}\/\d{2,4}/, /\d{4}-\d{2}-\d{2}/, /\w+ \d{1,2}, \d{4}/, /\d{1,2}:\d{2}\s*[AP]M/i ]; const elementsWithTimestamps = []; document.querySelectorAll('*').forEach(el => { const text = el.textContent; if (text && timestampPatterns.some(pattern => pattern.test(text)) && el.children.length === 0) { elementsWithTimestamps.push(el); } }); console.log(`Found ${elementsWithTimestamps.length} timestamp elements`); elementsWithTimestamps.forEach(timeEl => { const timestamp = timeEl.textContent.trim(); let sender = ''; let content = ''; // Look for email before or after timestamp const parent = timeEl.parentElement; // Add null check for parent if (!parent) { console.warn('timeEl has no parent element'); return; // Skip this timestamp element } const siblings = Array.from(parent.children); const timeIndex = siblings.indexOf(timeEl); // Check previous siblings for email for (let i = timeIndex - 1; i >= 0 && i >= timeIndex - 3; i--) { if (siblings[i] && siblings[i].textContent) { const text = siblings[i].textContent; const emailMatch = text.match(/[\w.+-]+@[\w.-]+\.\w+/); if (emailMatch) { sender = emailMatch[0]; break; } } } // Check following siblings for content for (let i = timeIndex + 1; i < siblings.length && i <= timeIndex + 3; i++) { if (siblings[i] && siblings[i].textContent) { const text = siblings[i].textContent; if (text.length > 50 && !text.includes('@')) { content = text.trim(); break; } } } if (sender && content) { messages.push({ sender, senderName: sender.split('@')[0], timestamp, content, subject: this.extractSubject() || 'No Subject', isReply: messages.length > 0 }); } }); return messages; }, // Parse visible text with more patterns parseVisibleText() { const messages = []; const bodyText = document.body.innerText; // Multiple email patterns to try const patterns = [ // Pattern 1: "From: ... Date: ... Subject: ..." /(?:From|De|Von):\s*([^\n]+)\n(?:.*\n)*?(?:Date|Fecha|Datum):\s*([^\n]+)\n(?:.*\n)*?(?:Subject|Asunto|Betreff):\s*([^\n]+)\n([\s\S]+?)(?=(?:From|De|Von):|$)/gi, // Pattern 2: "email@domain.com wrote:" /([\w.+-]+@[\w.-]+\.\w+)\s+wrote:\s*([\s\S]+?)(?=[\w.+-]+@[\w.-]+\.\w+\s+wrote:|$)/gi, // Pattern 3: "On [date], [email] wrote:" /On\s+([^,]+),\s*([\w.+-]+@[\w.-]+\.\w+)\s+wrote:\s*([\s\S]+?)(?=On\s+[^,]+,|$)/gi, // Pattern 4: Email followed by timestamp and content /([\w.+-]+@[\w.-]+\.\w+)\s*\n\s*(\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}[^\n]*)\s*\n\s*([\s\S]+?)(?=[\w.+-]+@[\w.-]+\.\w+|$)/gi, // Pattern 5: Plusvibe specific - looking for "From:", "To:", "Subject:" pattern /From:\s*([\w.+-]+@[\w.-]+\.\w+)[^\n]*\nTo:\s*([^\n]+)\n(?:Cc:\s*[^\n]+\n)?Subject:\s*([^\n]+)\n+([\s\S]+?)(?=From:|$)/gi ]; // Also try to extract from the visible format in Plusvibe // Based on the screenshot, messages might just be separated by line breaks const lines = bodyText.split('\n').map(l => l.trim()).filter(l => l); let currentMessage = null; let captureContent = false; lines.forEach((line, i) => { // Check if this line contains "From:" followed by an email if (line.startsWith('From:') && line.includes('@')) { // Save previous message if (currentMessage && currentMessage.content) { messages.push(currentMessage); } // Start new message const emailMatch = line.match(/[\w.+-]+@[\w.-]+\.\w+/); currentMessage = { sender: emailMatch ? emailMatch[0] : 'unknown@email.com', senderName: emailMatch ? emailMatch[0].split('@')[0] : 'Unknown', timestamp: new Date().toLocaleString(), subject: 'No Subject', content: '', isReply: messages.length > 0 }; captureContent = false; } // Check for "To:" line else if (currentMessage && line.startsWith('To:')) { currentMessage.recipient = line.replace('To:', '').trim(); } // Check for "Subject:" line else if (currentMessage && line.startsWith('Subject:')) { currentMessage.subject = line.replace('Subject:', '').trim(); captureContent = true; // Start capturing content after subject } // Check for "Cc:" line else if (currentMessage && line.startsWith('Cc:')) { currentMessage.cc = line.replace('Cc:', '').trim(); } // Capture content else if (currentMessage && captureContent && line.length > 0) { // Skip UI elements and buttons if (!line.includes('Context-Specific Prompt') && !line.includes('Auto-Draft with GPT') && !line.includes('Cannot read properties')) { currentMessage.content += (currentMessage.content ? '\n' : '') + line; } } }); // Add last message if (currentMessage && currentMessage.content) { messages.push(currentMessage); } // Try the regex patterns as backup if (messages.length === 0) { patterns.forEach((pattern, patternIndex) => { let match; while ((match = pattern.exec(bodyText)) !== null) { let message = {}; switch(patternIndex) { case 0: // From/Date/Subject pattern message = { sender: match[1].trim(), senderName: match[1].split('<')[0].trim(), timestamp: match[2].trim(), subject: match[3].trim(), content: match[4].trim(), isReply: messages.length > 0 }; break; case 1: // email wrote: pattern message = { sender: match[1], senderName: match[1].split('@')[0], timestamp: new Date().toLocaleString(), subject: this.extractSubject() || 'No Subject', content: match[2].trim(), isReply: messages.length > 0 }; break; case 2: // On date, email wrote: message = { sender: match[2], senderName: match[2].split('@')[0], timestamp: match[1], subject: this.extractSubject() || 'No Subject', content: match[3].trim(), isReply: messages.length > 0 }; break; case 3: // Email, timestamp, content message = { sender: match[1], senderName: match[1].split('@')[0], timestamp: match[2], subject: this.extractSubject() || 'No Subject', content: match[3].trim(), isReply: messages.length > 0 }; break; case 4: // Plusvibe From/To/Subject pattern message = { sender: match[1], senderName: match[1].split('@')[0], recipient: match[2], subject: match[3], content: match[4].trim(), timestamp: new Date().toLocaleString(), isReply: messages.length > 0 }; break; } if (message.content && message.content.length > 20) { messages.push(message); } } }); } console.log(`Parsed ${messages.length} messages from visible text`); return messages; }, // Extract from reply context (near the reply box) extractFromReplyContext() { const messages = []; const replyBox = this.findReplyBox(); if (!replyBox) return messages; // Navigate up the DOM tree to find the conversation container let current = replyBox.parentElement; let maxLevels = 10; while (current && maxLevels > 0) { // Check if this container has email content const text = current.textContent; if (text.includes('@') && text.length > 200) { // This might be our conversation container console.log('Found potential conversation container:', { tagName: current.tagName, className: current.className, textLength: text.length }); // Try to extract messages from this container const containerMessages = this.extractFromContainer(current); if (containerMessages.length > 0) { return containerMessages; } } current = current.parentElement; maxLevels--; } return messages; }, // Extract messages from a specific container extractFromContainer(container) { const messages = []; // Strategy 1: Look for child elements with consistent structure const children = Array.from(container.children); children.forEach(child => { const text = child.textContent; if (text.includes('@') && text.length > 50) { const emailMatch = text.match(/[\w.+-]+@[\w.-]+\.\w+/); if (emailMatch) { messages.push({ sender: emailMatch[0], senderName: emailMatch[0].split('@')[0], timestamp: this.extractTimestampFromText(text), content: this.extractContentFromText(text, emailMatch[0]), subject: this.extractSubject() || 'No Subject', isReply: messages.length > 0 }); } } }); return messages; }, // Helper functions looksLikeTimestamp(text) { const patterns = [ /\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}/, /\d{4}-\d{2}-\d{2}/, /\w+ \d{1,2}, \d{4}/, /\d{1,2}:\d{2}\s*[AP]M/i, /\d+ (hours?|days?|weeks?) ago/i ]; return patterns.some(pattern => pattern.test(text)); }, extractTimestampFromText(text) { const patterns = [ /(\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}(?:\s+\d{1,2}:\d{2}\s*[AP]M)?)/i, /(\d{4}-\d{2}-\d{2}(?:\s+\d{1,2}:\d{2})?)/, /(\w+ \d{1,2}, \d{4}(?:\s+at\s+\d{1,2}:\d{2}\s*[AP]M)?)/i, /(\d+ (?:hours?|days?|weeks?) ago)/i ]; for (const pattern of patterns) { const match = text.match(pattern); if (match) return match[1]; } return new Date().toLocaleString(); }, extractContentFromText(text, senderEmail) { // Remove the sender email and clean up let content = text.replace(senderEmail, '').trim(); // Remove common patterns content = content.replace(/wrote:?\s*/i, ''); content = content.replace(/^on\s+.+?,\s*/i, ''); // Remove timestamp if at the beginning const timestampPatterns = [ /^\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}[^\n]*\n*/, /^\w+ \d{1,2}, \d{4}[^\n]*\n*/ ]; timestampPatterns.forEach(pattern => { content = content.replace(pattern, ''); }); return content.trim(); }, // Extract sender from element extractSender(element) { // Add null check if (!element) { console.warn('extractSender called with null/undefined element'); return 'unknown@email.com'; } const selectors = [ '.sender-email', '.from-email', '.email-from', '[data-sender]', '.message-sender', '.from' ]; for (const selector of selectors) { const el = element.querySelector(selector); if (el) { return el.textContent.trim() || el.getAttribute('data-sender') || el.getAttribute('data-email'); } } // Look for email pattern in text const text = element.textContent; const emailMatch = text.match(/[\w.+-]+@[\w.-]+\.\w+/); return emailMatch ? emailMatch[0] : 'unknown@email.com'; }, // Extract sender name extractSenderName(element) { // Add null check if (!element) { console.warn('extractSenderName called with null/undefined element'); return 'Unknown'; } const selectors = [ '.sender-name', '.from-name', '[data-sender-name]', '.message-author', '.author' ]; for (const selector of selectors) { const el = element.querySelector(selector); if (el) { return el.textContent.trim() || el.getAttribute('data-name'); } } // Fallback to sender email username const sender = this.extractSender(element); return sender.split('@')[0]; }, // Extract timestamp extractTimestamp(element) { // Add null check if (!element) { console.warn('extractTimestamp called with null/undefined element'); return new Date().toLocaleString(); } const selectors = [ '.timestamp', '.message-time', '.sent-time', 'time', '[data-timestamp]', '.date' ]; for (const selector of selectors) { const el = element.querySelector(selector); if (el) { return el.textContent.trim() || el.getAttribute('datetime') || el.getAttribute('data-timestamp') || el.getAttribute('title'); } } return new Date().toLocaleString(); }, // Extract message content extractContent(element) { // Add null check if (!element) { console.warn('extractContent called with null/undefined element'); return ''; } const selectors = [ '.message-content', '.message-body', '.email-body', '.content', '[data-content]', '.body' ]; for (const selector of selectors) { const el = element.querySelector(selector); if (el) { const clone = el.cloneNode(true); // Remove quoted text const quotes = clone.querySelectorAll('.quoted-text, blockquote, .gmail_quote'); quotes.forEach(q => q.remove()); return clone.textContent.trim(); } } return element.textContent.trim(); }, // Extract subject extractSubject(element) { // Add null check - note: element parameter might be optional here const selectors = [ '.subject', '.email-subject', '.message-subject', '[data-subject]', '.thread-subject', '.conversation-subject', 'h1[class*="subject"]', 'h2[class*="subject"]', '[aria-label*="subject"]' ]; // If element is provided, check it first if (element) { for (const selector of selectors) { const el = element.querySelector(selector); if (el) { return el.textContent.trim() || el.getAttribute('data-subject'); } } } // Check page header (no element needed) const pageSubject = document.querySelector('h1.subject, h2.subject, .page-subject, .thread-title, .conversation-title'); if (pageSubject) { return pageSubject.textContent.trim(); } // Try to extract from page title if (document.title && document.title.includes(' - ')) { const titleParts = document.title.split(' - '); if (titleParts.length > 1) { // Return the first part that looks like a subject return titleParts[0].trim(); } } // Look for "Re:" pattern in visible text const rePattern = /Re:\s*([^\n]{10,100})/i; const pageText = document.body.innerText; const reMatch = pageText.match(rePattern); if (reMatch) { return reMatch[0].trim(); } return null; // Return null to indicate no subject found }, // Find the reply/compose box findReplyBox() { console.log('=== FINDING PLUSVIBE REPLY BOX ==='); const selectors = [ // MUI (Material-UI) specific selectors - PRIORITIZED 'textarea.MuiInputBase-input', 'textarea.MuiOutlinedInput-input', 'textarea.MuiInputBase-inputMultiline', '.MuiInputBase-input', '.MuiOutlinedInput-input', // Common contenteditable patterns 'div[contenteditable="true"]', '[contenteditable="true"]', // Textarea patterns 'textarea', 'textarea.reply-input', 'textarea.compose-input', 'textarea[name="message"]', 'textarea[name="reply"]', // Class-based selectors '.reply-box', '.compose-box', '.message-input', '.reply-input', '.compose-input', '.editor', '.text-editor', // Plusvibe specific guesses '.pipl-reply-box', '.plusvibe-compose', '.unibox-reply', // Role-based '[role="textbox"]', // Data attributes '[data-reply-box]', '[data-compose]', '[data-message-input]' ]; for (const selector of selectors) { const elements = document.querySelectorAll(selector); for (const element of elements) { if (element.offsetParent !== null) { // Check if visible const rect = element.getBoundingClientRect(); if (rect.width > 100 && rect.height > 50) { console.log('Found Plusvibe reply box:', selector); return element; } } } } console.warn('Could not find Plusvibe reply box'); return null; }, // Insert draft into reply box insertDraft(draft) { const replyBox = this.findReplyBox(); if (!replyBox) { console.error('No reply box found to insert draft'); return false; } if (replyBox.tagName === 'TEXTAREA' || replyBox.tagName === 'INPUT') { replyBox.value = draft; replyBox.dispatchEvent(new Event('input', { bubbles: true })); replyBox.dispatchEvent(new Event('change', { bubbles: true })); } else if (replyBox.contentEditable === 'true') { replyBox.innerHTML = draft.replace(/\n/g, '
'); replyBox.dispatchEvent(new Event('input', { bubbles: true })); // Trigger any framework-specific events const inputEvent = new InputEvent('input', { bubbles: true, cancelable: true, inputType: 'insertText', data: draft }); replyBox.dispatchEvent(inputEvent); } console.log('Draft inserted into Plusvibe reply box'); return true; }, // Get selected threads (for batch operations) getSelectedThreads() { const selected = []; const selectors = [ 'input[type="checkbox"]:checked', '.thread-item.selected', '.conversation-item.selected', '[data-selected="true"]', '.inbox-item.checked' ]; for (const selector of selectors) { const elements = document.querySelectorAll(selector); elements.forEach(element => { const threadEl = element.closest('.thread-item, .conversation-item, .inbox-item'); if (threadEl) { const threadInfo = { id: threadEl.getAttribute('data-thread-id') || threadEl.getAttribute('data-conversation-id') || threadEl.id, subject: threadEl.querySelector('.subject')?.textContent?.trim(), sender: threadEl.querySelector('.sender, .from')?.textContent?.trim() }; if (threadInfo.id) { selected.push(threadInfo); } } }); } return selected; }, // Diagnostic function to analyze DOM structure analyzeDOMStructure() { console.log('=== PLUSVIBE DOM ANALYSIS ==='); // 1. Find all elements with email addresses const allElements = document.querySelectorAll('*'); const emailElements = []; allElements.forEach(el => { if (el.textContent && el.textContent.match(/[\w.+-]+@[\w.-]+\.\w+/)) { emailElements.push({ tagName: el.tagName, className: el.className, id: el.id, text: el.textContent.substring(0, 100), attributes: Array.from(el.attributes).map(attr => `${attr.name}="${attr.value}"`) }); } }); console.log('Elements containing email addresses:', emailElements); // 2. Look for conversation/message containers const possibleContainers = [ // Common patterns 'div[class*="message"]', 'div[class*="email"]', 'div[class*="thread"]', 'div[class*="conversation"]', 'div[class*="mail"]', 'div[class*="inbox"]', 'article', 'section[class*="message"]', // Check for list items 'li[class*="message"]', 'li[class*="email"]', // Data attributes '[data-message]', '[data-email]', '[data-thread]', '[data-conversation]' ]; possibleContainers.forEach(selector => { const elements = document.querySelectorAll(selector); if (elements.length > 0) { console.log(`Found ${elements.length} elements matching "${selector}":`); elements.forEach((el, i) => { if (i < 3) { // Log first 3 console.log({ selector, className: el.className, id: el.id, textLength: el.textContent.length, hasEmail: el.textContent.includes('@'), preview: el.textContent.substring(0, 100) + '...' }); } }); } }); // 3. Analyze the reply/compose area console.log('\n=== REPLY AREA ANALYSIS ==='); const replyArea = this.findReplyBox(); if (replyArea) { // Look for nearby elements that might contain the thread const parent = replyArea.parentElement; const grandparent = parent?.parentElement; console.log('Reply box parent:', { tagName: parent?.tagName, className: parent?.className, id: parent?.id }); console.log('Reply box grandparent:', { tagName: grandparent?.tagName, className: grandparent?.className, id: grandparent?.id }); // Look for siblings that might contain messages const siblings = parent ? Array.from(parent.children) : []; siblings.forEach((sibling, i) => { if (sibling !== replyArea && sibling.textContent.includes('@')) { console.log(`Sibling ${i} (potential message container):`, { tagName: sibling.tagName, className: sibling.className, textLength: sibling.textContent.length, preview: sibling.textContent.substring(0, 100) + '...' }); } }); } // 4. Check for iframe content const iframes = document.querySelectorAll('iframe'); console.log(`\nFound ${iframes.length} iframes`); iframes.forEach((iframe, i) => { try { const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; if (iframeDoc && iframeDoc.body.textContent.includes('@')) { console.log(`Iframe ${i} contains email content`); } } catch (e) { console.log(`Iframe ${i} is cross-origin`); } }); console.log('=== END DOM ANALYSIS ==='); }, // New helper function to find messages by searching for email content findMessagesByContent() { console.log('=== FINDING MESSAGES BY CONTENT ==='); // Find email addresses dynamically from the current page const emailRegex = /[\w.+-]+@[\w.-]+\.\w+/g; const pageText = document.body.innerText; const foundEmails = new Set(); let match; while ((match = emailRegex.exec(pageText)) !== null) { foundEmails.add(match[0]); } console.log('Found emails on page:', Array.from(foundEmails)); // Find all elements containing email addresses const potentialContainers = []; const allElements = document.querySelectorAll('*'); allElements.forEach(el => { const text = el.textContent; if (text && text.match(emailRegex)) { // Check if this element has reasonable size and isn't too big const childCount = el.children.length; const textLength = text.length; if (childCount < 50 && textLength > 50 && textLength < 5000) { potentialContainers.push({ element: el, tagName: el.tagName, className: el.className, id: el.id, childCount: childCount, textLength: textLength, preview: text.substring(0, 200) }); } } }); console.log(`Found ${potentialContainers.length} potential message containers`); potentialContainers.forEach((container, i) => { if (i < 5) { // Log only first 5 to avoid spam console.log(`Container ${i + 1}:`, container); } }); // Try to identify the pattern const messageElements = []; potentialContainers.forEach(container => { // Look for elements that seem to be individual messages const parent = container.element.parentElement; const siblings = parent ? Array.from(parent.children) : []; // Check if siblings have similar structure (likely messages) const similarSiblings = siblings.filter(sib => sib.tagName === container.element.tagName && sib.className === container.element.className ); if (similarSiblings.length > 1) { console.log('Found message pattern:', { selector: `${container.element.tagName}.${container.element.className.split(' ').join('.')}`, count: similarSiblings.length }); similarSiblings.forEach(msg => { messageElements.push(msg); }); } }); return messageElements; }, // New method: Extract from MUI/React structures extractFromMUIStructure() { console.log('=== EXTRACTING FROM MUI STRUCTURE ==='); const messages = []; // Look for MUI Box components that might contain messages const muiSelectors = [ '.MuiBox-root' // '[class*="MuiBox"]', // '[class*="css-"][class*="root"]', // 'div[class^="css-"]', ]; const replyBox = this.findReplyBox(); const replyContainer = replyBox.closest('div'); // Find all potential message containers const potentialMessages = []; muiSelectors.forEach(selector => { const elements = document.querySelectorAll(selector); elements.forEach(el => { const boxRect = el.getBoundingClientRect(); const replyRect = replyContainer.getBoundingClientRect(); if (boxRect.bottom < replyRect.top) { const text = el.textContent; // Check if this element looks like a message (has email and substantial content) if (text && text.includes('@') && text.length > 100 && text.length < 5000) { // Check it's not too nested (avoid getting parent containers) const emailCount = (text.match(/@/g) || []).length; if (emailCount <= 3) { // Reasonable number of email addresses potentialMessages.push({ element: el, text: text, emailCount: emailCount, childCount: el.children.length }); } } } }); }); console.log(`Found ${potentialMessages.length} potential message containers`); // Sort by likelihood of being a single message (not too many children, not too much text) potentialMessages.sort((a, b) => b.top - a.top); /* potentialMessages.sort((a, b) => { const aScore = Math.abs(500 - a.text.length) + (a.childCount * 10); const bScore = Math.abs(500 - b.text.length) + (b.childCount * 10); return aScore - bScore; }); */ // Process the most likely candidates const processedElements = new Set(); potentialMessages.forEach(candidate => { // Skip if we've already processed this element or its parent if (processedElements.has(candidate.element)) return; let parent = candidate.element.parentElement; while (parent) { if (processedElements.has(parent)) return; parent = parent.parentElement; } processedElements.add(candidate.element); const text = candidate.text; const emailMatch = text.match(/[\w.+-]+@[\w.-]+\.\w+/); if (emailMatch) { // Extract message details const message = { sender: emailMatch[0], senderName: this.extractSenderNameFromEmail(emailMatch[0]), timestamp: this.extractTimestampFromText(text), content: this.cleanMessageContent(text, emailMatch[0]), subject: this.extractSubject() || 'No Subject', isReply: messages.length > 0 }; if (message.content && message.content.length > 20) { console.log('Extracted message from MUI structure:', { sender: message.sender, contentLength: message.content.length, preview: message.content.substring(0, 100) + '...' }); messages.push(message); } } }); console.log(`Extracted ${messages.length} messages from MUI structure`); return messages; }, // Helper to clean message content more aggressively cleanMessageContent(text, senderEmail) { let content = text; // Remove the sender email content = content.replace(new RegExp(senderEmail.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), ''); // Remove common UI elements and timestamps const patternsToRemove = [ /Context-Specific Prompt:?/gi, /Auto-Draft with GPT/gi, /Cannot read properties/gi, /\d{1,2}:\d{2}\s*[AP]M/gi, /\d{1,2}\/\d{1,2}\/\d{2,4}/g, /\w+,\s+\w+\s+\d{1,2},\s+\d{4}/gi, /^From:.*$/gm, /^To:.*$/gm, /^Subject:.*$/gm, /^Date:.*$/gm, /^Cc:.*$/gm, /^Re:\s*/gim ]; patternsToRemove.forEach(pattern => { content = content.replace(pattern, ''); }); // Remove excessive whitespace content = content .split('\n') .map(line => line.trim()) .filter(line => line.length > 0) .join('\n'); // If content is too short after cleaning, it might be just UI elements if (content.length < 20) return ''; return content.trim(); }, // Fallback with hardcoded content based on visible emails getHardcodedFallback() { console.log('Using hardcoded fallback - this should only be used for debugging'); // This method should not contain any specific email addresses or content // It should return an empty array to force proper extraction methods return []; }, // NEW: Diagnostic function to find thread boundaries findThreadBoundaries() { console.log('=== FINDING THREAD BOUNDARIES ==='); // Look for the reply box first as an anchor point const replyBox = this.findReplyBox(); if (!replyBox) { console.error('No reply box found - cannot determine thread context'); return null; } console.log('Found reply box, traversing up to find thread container...'); // Get the current thread ID from URL const currentThreadId = this.getCurrentThreadId(); console.log('Current thread ID from URL:', currentThreadId); // Traverse up from reply box to find the thread container let current = replyBox; let threadContainer = null; let previousEmailCount = 0; let level = 0; while (current && level < 15) { const emailCount = (current.textContent.match(/@/g) || []).length; const rect = current.getBoundingClientRect(); console.log(`Level ${level}:`, { tagName: current.tagName, className: current.className.substring(0, 100), emailCount: emailCount, dimensions: `${Math.round(rect.width)}x${Math.round(rect.height)}` }); // CRITICAL: Stop when email count jumps significantly (indicates we've hit the main container) if (previousEmailCount > 0 && emailCount > previousEmailCount * 10) { console.log(`Email count jumped from ${previousEmailCount} to ${emailCount} - stopping here`); break; } // Check if this looks like a good thread container const hasReasonableEmails = emailCount >= 2 && emailCount <= 20; // Thread has 2-20 emails max const isReasonableSize = rect.width > 300 && rect.width < window.innerWidth * 0.8; const hasNoInboxElements = !current.querySelector('.inbox-item, input[type="checkbox"]'); const isMuiDrawer = current.className.includes('MuiRoot') || current.closest('.MuiBox-root, .MuiBox-root'); if (hasReasonableEmails && isReasonableSize && hasNoInboxElements) { console.log('Found potential thread container at level', level); threadContainer = current; // Don't break - keep going to see if there's a better container // But store this as our best candidate so far } // Special handling for MUI Drawer - this often contains just the thread if (isMuiDrawer && emailCount >= 2 && emailCount <= 20) { console.log('Found MUI Drawer container - likely thread panel'); threadContainer = current; } previousEmailCount = emailCount; current = current.parentElement; level++; } if (threadContainer) { console.log('Thread container identified:', { element: threadContainer, className: threadContainer.className, id: threadContainer.id, finalEmailCount: (threadContainer.textContent.match(/@/g) || []).length }); // Extract messages only from this container return this.extractMessagesFromContainer(threadContainer); } console.error('Could not identify thread container'); return null; }, // Extract messages from a specific container only extractMessagesFromContainer(container) { console.log('Extracting messages from specific container...'); const messages = []; // Get visible email addresses to identify the current thread const visibleEmails = new Set(); const emailRegex = /[\w.+-]+@[\w.-]+\.\w+/g; let match; while ((match = emailRegex.exec(container.textContent)) !== null) { visibleEmails.add(match[0].toLowerCase()); } console.log('Visible emails in container:', Array.from(visibleEmails)); // NEW: For Plusvibe, messages appear to be simple text blocks separated by line breaks // Based on the DOM structure shown in the screenshots const lines = container.innerText.split('\n').map(l => l.trim()).filter(l => l); let currentMessage = null; let captureMode = false; let lastSender = null; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const nextLine = lines[i + 1] || ''; // Check if this line contains an email address (potential sender) const emailMatch = line.match(/^([\w.+-]+@[\w.-]+\.\w+)$/); if (emailMatch) { // Save previous message if exists if (currentMessage && currentMessage.content.trim()) { messages.push(currentMessage); } // Start new message lastSender = emailMatch[1]; currentMessage = { sender: lastSender, senderName: this.extractSenderNameFromEmail(lastSender), timestamp: this.findNearbyTimestamp(lines, i) || new Date().toLocaleString(), content: '', subject: this.extractSubject() || 'No Subject', isReply: messages.length > 0 }; captureMode = true; continue; } // Check if this looks like a timestamp (e.g., "Jun 13, 2025, 08:06:43 PM PDT") if (this.looksLikeTimestamp(line) && currentMessage) { currentMessage.timestamp = line; continue; } // Skip UI elements and system messages if (line.includes('Context-Specific Prompt') || line.includes('Auto-Draft with GPT') || line.includes('wrote:') || line.length < 10) { continue; } // Capture content if (captureMode && currentMessage && line.length > 10) { // Check if this line starts a new message (has an email) if (line.includes('@') && !line.startsWith('Hi ') && !line.includes('wrote:')) { // This might be the start of a new message, save current if (currentMessage.content.trim()) { messages.push(currentMessage); currentMessage = null; captureMode = false; i--; // Re-process this line continue; } } currentMessage.content += (currentMessage.content ? '\n' : '') + line; } } // Don't forget the last message if (currentMessage && currentMessage.content.trim()) { messages.push(currentMessage); } // If we still have no messages, try the original MUI extraction as fallback if (messages.length === 0) { console.log('Simple extraction failed, trying MUI component extraction...'); // Look for divs that might be individual messages const messageSelectors = [ 'div.MuiBox-root', 'div[class*="css-"][class*="MuiBox"]', 'div[class*="css-"]:not([class*="MuiDrawer"])' ]; for (const selector of messageSelectors) { const elements = container.querySelectorAll(selector); elements.forEach((el, index) => { const text = el.textContent; const elementEmails = (text.match(emailRegex) || []).map(e => e.toLowerCase()); // Check if this element contains exactly one of our thread emails const matchingEmails = elementEmails.filter(e => visibleEmails.has(e)); if (matchingEmails.length === 1 && text.length > 50 && text.length < 3000) { const message = { sender: matchingEmails[0], senderName: this.extractSenderNameFromEmail(matchingEmails[0]), timestamp: this.extractTimestampFromText(text), content: this.cleanMessageContent(text, matchingEmails[0]), subject: this.extractSubject() || 'No Subject', isReply: messages.length > 0 }; if (message.content && message.content.length > 20) { messages.push(message); } } }); } } console.log(`Extracted ${messages.length} messages from container`); return messages; }, // Helper to find nearby timestamp findNearbyTimestamp(lines, currentIndex) { // Look within 3 lines before or after for a timestamp for (let offset = -3; offset <= 3; offset++) { const idx = currentIndex + offset; if (idx >= 0 && idx < lines.length) { if (this.looksLikeTimestamp(lines[idx])) { return lines[idx]; } } } return null; }, // Helper to extract sender name from email extractSenderNameFromEmail(email) { // Clean up email and extract username const username = email.split('@')[0]; // Capitalize first letter of each word and handle common separators return username .split(/[._-]/) .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .join(' '); } }; // Initialize on load PlusvibeExtractor.init(); // Make it globally available window.PlusvibeExtractor = PlusvibeExtractor; console.log('=== PLUSVIBE EXTRACTOR LOADED ==='); console.log('PlusvibeExtractor available at window.PlusvibeExtractor'); console.log('Current thread ID:', PlusvibeExtractor.getCurrentThreadId()); // Quick test function window.testPlusvibeExtraction = async function() { console.log('=== TESTING PLUSVIBE EXTRACTION ==='); // Test thread ID //const threadId = PlusvibeExtractor.getCurrentThreadId(); //console.log('Thread ID:', threadId); // Test MUI extraction specifically console.log('\nTesting MUI extraction...'); const muiMessages = PlusvibeExtractor.extractFromMUIStructure(); console.log(`Found ${muiMessages.length} messages via MUI extraction`); muiMessages.forEach((msg, i) => { console.log(`\nMessage ${i + 1}:`); console.log('From:', msg.sender); console.log('Content preview:', msg.content.substring(0, 200) + '...'); }); // Test full extraction console.log('\nTesting full extraction pipeline...'); const allMessages = await PlusvibeExtractor.extractThreadMessages(); console.log(`Total messages extracted: ${allMessages.length}`); console.log('=== END TESTING ==='); return allMessages; }; console.log('Run testPlusvibeExtraction() to test message extraction'); // Quick diagnostic function window.quickDiagnosePlusvibe = function() { console.log('=== QUICK PLUSVIBE DIAGNOSTIC ==='); // 1. Check thread ID //const threadId = PlusvibeExtractor.getCurrentThreadId(); //console.log('Thread ID:', threadId); // 2. Find reply box const replyBox = PlusvibeExtractor.findReplyBox(); if (replyBox) { console.log('Reply box found ✓'); // 3. Test thread boundary detection console.log('\nTesting thread boundary detection...'); const boundedMessages = PlusvibeExtractor.findThreadBoundaries(); if (boundedMessages && boundedMessages.length > 0) { console.log(`Found ${boundedMessages.length} messages in thread:`); boundedMessages.forEach((msg, i) => { console.log(`\nMessage ${i + 1}:`); console.log(' From:', msg.senderName, `<${msg.sender}>`); console.log(' Time:', msg.timestamp); console.log(' Subject:', msg.subject); console.log(' Preview:', msg.content.substring(0, 100) + '...'); }); // Generic validation - check if messages are from current thread console.log('\nValidation:'); console.log(' Number of unique senders:', new Set(boundedMessages.map(m => m.sender)).size); console.log(' Thread appears coherent:', boundedMessages.length >= 2 ? '✓' : '✗'); console.log(' Messages have content:', boundedMessages.every(m => m.content.length > 10) ? '✓' : '✗'); if (boundedMessages.length >= 2 && boundedMessages.every(m => m.content.length > 10)) { console.log('\n✅ SUCCESS: Extracted thread messages successfully!'); } else { console.log('\n❌ ERROR: Could not extract valid thread messages'); } } else { console.log('❌ No messages found'); } } else { console.log('❌ No reply box found'); } console.log('\n=== END DIAGNOSTIC ==='); }; console.log('Run quickDiagnosePlusvibe() for quick diagnostic');