gpt_chrome_autodrafter_v3/plusvibe_extractor.js
2025-07-06 13:18:20 -07:00

1850 lines
62 KiB
JavaScript

// 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, '<br>');
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');