// content.js // Color scheme from newfrontierfunding.com const COLORS = { green: '#1a4d2e', gold: '#e6b800', white: '#ffffff', }; // Enhanced logging function function logWithStyle(message, type = 'info') { const styles = { info: 'color: #1a4d2e; font-weight: bold;', success: 'color: #1a4d2e; font-weight: bold; background: #e6ffe6; padding: 2px 5px; border-radius: 3px;', error: 'color: #cc0000; font-weight: bold; background: #ffe6e6; padding: 2px 5px; border-radius: 3px;', warning: 'color: #e6b800; font-weight: bold; background: #fff9e6; padding: 2px 5px; border-radius: 3px;' }; console.log(`%c${message}`, styles[type]); } // Initialize with enhanced logging logWithStyle('=== GPT Auto-Draft Extension Initializing ===', 'info'); logWithStyle('Content script loaded successfully', 'success'); // Load enhanced thread extractor const script = document.createElement('script'); script.src = chrome.runtime.getURL('enhanced_thread_extractor.js'); script.onload = function() { logWithStyle('Enhanced thread extractor loaded successfully', 'success'); logWithStyle('Thread monitoring started - 7+ extraction methods active', 'info'); }; script.onerror = function() { logWithStyle('Failed to load enhanced thread extractor!', 'error'); }; (document.head || document.documentElement).appendChild(script); // Utility: Wait for an element to appear function waitForElement(selector, timeout = 10000) { console.log('Waiting for element:', selector); return new Promise((resolve, reject) => { const interval = 100; let elapsed = 0; const check = () => { const el = document.querySelector(selector); if (el) { console.log('Element found:', selector); return resolve(el); } elapsed += interval; if (elapsed >= timeout) { console.warn('Element not found after timeout:', selector); return reject('Element not found: ' + selector); } setTimeout(check, interval); }; check(); }); } // Utility: Get settings from storage function getSettings() { console.log('Getting settings from storage'); return new Promise((resolve) => { chrome.storage.local.get([ 'autoDraft', 'openAIApiKey', 'customPrompt' ], (settings) => { console.log('Settings loaded:', { autoDraftEnabled: !!settings.autoDraft, apiKeyExists: !!settings.openAIApiKey, customPromptExists: !!settings.customPrompt }); resolve(settings); }); }); } // Detect Gmail reply box function findReplyBox() { // Try the standard Gmail reply/compose box let replyBox = document.querySelector('div[aria-label="Message Body"]'); if (replyBox) { console.log('Reply box found with aria-label="Message Body"'); return replyBox; } // Fallback: look for contenteditable divs that are visible and editable const candidates = Array.from(document.querySelectorAll('div[contenteditable="true"]')); for (const el of candidates) { // Exclude hidden or offscreen elements const style = window.getComputedStyle(el); if (style.display !== 'none' && style.visibility !== 'hidden' && el.offsetParent !== null) { // Optionally, check for a minimum size to avoid toolbars, etc. if (el.offsetHeight > 30 && el.offsetWidth > 100) { console.log('Reply box found with contenteditable fallback'); return el; } } } // Fallback: look for Gmail's .editable class (older or alternate layouts) replyBox = document.querySelector('div.editable'); if (replyBox) { console.log('Reply box found with .editable class'); return replyBox; } // If not found, log a warning console.warn('Reply box not found! Selectors tried: [aria-label="Message Body"], [contenteditable="true"], .editable'); return null; } // Patch: Only look for reply box if not on inbox page function safeFindReplyBox() { if (isInboxPage()) return null; return findReplyBox(); } // Request draft from background let draftCooldownUntil = 0; // timestamp in ms function requestDraft(threadMessages, contextPrompt = null) { console.log('=== DRAFT REQUEST PAYLOAD ==='); console.log('Requesting draft from background script'); console.log('Thread messages type:', typeof threadMessages); // Detailed logging of thread messages if (typeof threadMessages === 'string') { console.log('Thread message length:', threadMessages.length); console.log('Thread message preview:', threadMessages.substring(0, 100) + '...'); console.log('Full thread message string:', threadMessages); } else if (Array.isArray(threadMessages)) { console.log('Thread contains', threadMessages.length, 'messages'); console.log('Thread messages structure:'); threadMessages.forEach((msg, index) => { console.log(`\n--- Message ${index + 1} ---`); console.log('From:', msg.from || msg.sender); console.log('To:', msg.to); console.log('Subject:', msg.subject); console.log('Date:', msg.date); console.log('Content length:', msg.content ? msg.content.length : 0); console.log('Content preview:', msg.content ? msg.content.substring(0, 200) + '...' : 'No content'); if (msg.content && msg.content.length < 500) { console.log('Full content:', msg.content); } }); } console.log('\nContext prompt:', contextPrompt); // Log the exact payload being sent const payload = { action: 'generateDraft', threadMessages, contextPrompt }; console.log('\nFull payload being sent to background:'); console.log(JSON.stringify(payload, null, 2)); console.log('=== END DRAFT REQUEST PAYLOAD ===\n'); return new Promise((resolve, reject) => { chrome.runtime.sendMessage(payload, (response) => { console.log('=== DRAFT RESPONSE ==='); if (response && response.draft) { console.log('Draft received from background, length:', response.draft.length); console.log('Draft preview:', response.draft.substring(0, 200) + '...'); console.log('Full draft:', response.draft); resolve(response.draft); } else if (response && response.error && response.error.includes('429')) { console.error('Draft request failed with rate limit error:', response.error); reject({ code: 429, message: response.error }); } else { console.error('Draft request failed:', response.error || 'No response'); reject(response && response.error ? response.error : 'No draft'); } console.log('=== END DRAFT RESPONSE ===\n'); }); }); } // Parse MIME email function parseMimeEmail(mimeContent) { console.log('Sending MIME content to background for parsing'); console.log('MIME content length:', mimeContent.length); console.log('MIME content preview:', mimeContent.substring(0, 100) + '...'); return new Promise((resolve, reject) => { chrome.runtime.sendMessage({ action: 'parseMimeEmail', mimeContent }, (response) => { if (response && response.success) { console.log('MIME email parsed successfully'); console.log('Parsed email data:', { subject: response.emailData.subject, sender: response.emailData.sender, recipient: response.emailData.recipient, contentLength: response.emailData.content.length }); resolve(response.emailData); } else { console.error('MIME parsing failed:', response && response.error ? response.error : 'Unknown error'); reject(response && response.error ? response.error : 'Failed to parse email'); } }); }); } // Track UI visibility state let isUIVisible = false; let uiContainer = null; let currentThreadMessages = ''; let observer = null; // Add observer reference // Function to update thread messages function updateThreadMessages() { console.log('Updating thread messages'); const messageElements = document.querySelectorAll('div[role="listitem"]'); console.log('Found', messageElements.length, 'message elements'); // Get thread ID from URL or data attribute const threadId = (() => { const urlMatch = window.location.pathname.match(/\/d\/[a-zA-Z0-9]+/); if (urlMatch) return urlMatch[0].split('/')[2]; const threadElement = document.querySelector('[data-thread-id]'); return threadElement ? threadElement.getAttribute('data-thread-id') : null; })(); const messages = Array.from(messageElements).map((item, index) => { const senderSpan = item.querySelector('span[email]'); const sender = senderSpan ? senderSpan.getAttribute('email') || 'Unknown' : 'Unknown'; const senderName = senderSpan ? senderSpan.textContent || 'Unknown' : 'Unknown'; const timestamp = item.querySelector('span[title]')?.getAttribute('title') || ''; const messageId = item.getAttribute('data-message-id') || null; console.log(`Processing message ${index} from`, senderName || sender); const content = (() => { let bodyDiv = item.querySelector('div[dir="ltr"], div[dir="auto"]'); if (!bodyDiv) bodyDiv = item.querySelector('.a3s'); // fallback if (!bodyDiv) { if (item.querySelector('.ajR')) { console.warn(`Message ${index} appears collapsed (trimmed). Expand to view full content`); return '[Message collapsed. Please expand to view full content.]'; } console.warn(`Message ${index}: No message body found`); return ''; } const clone = bodyDiv.cloneNode(true); clone.querySelectorAll('.gmail_quote, blockquote, .gmail_signature').forEach(q => q.remove()); const content = clone.innerText.trim(); console.log(`Message ${index} content length:`, content.length); return content; })(); return { sender, senderName, timestamp, content, messageId, isReply: index > 0, // Mark if this is a reply in the thread threadId // Include thread ID in each message }; }); currentThreadMessages = messages; console.log('Thread messages updated, total messages:', messages.length); return messages; } // Function to check if content is a MIME email function isMimeEmail(content) { const isMime = typeof content === 'string' && content.trim().startsWith('MIME-Version:') && content.includes('Content-Type:'); console.log('Checking if content is MIME email:', isMime); return isMime; } // Function to handle MIME email content async function handleMimeEmail(content) { console.log('Handling MIME email content'); console.log('Content length:', content.length); try { const emailData = await parseMimeEmail(content); console.log('Parsed MIME email:', emailData); return emailData; } catch (error) { console.error('Failed to parse MIME email:', error); return null; } } // Function to toggle UI visibility function toggleUIVisibility() { console.log('Toggling UI visibility, current state:', isUIVisible); // If UI is not visible or container is missing, show it if (!isUIVisible || !uiContainer) { console.log('Showing UI'); const replyBox = safeFindReplyBox(); if (replyBox) { updateThreadMessages(); injectUI(replyBox, '', currentThreadMessages); uiContainer = document.querySelector('.gpt-autodraft-ui'); isUIVisible = true; console.log('UI injected and visible'); // Start observer if it doesn't exist if (!observer) { console.log('Starting DOM observer'); observer = new MutationObserver(async () => { const replyBox = safeFindReplyBox(); if (replyBox) { updateThreadMessages(); if (uiContainer) { const draftBtn = uiContainer.querySelector('button'); if (draftBtn) { draftBtn.onclick = async () => { // Cooldown check if (Date.now() < draftCooldownUntil) { const errorMsg = uiContainer ? uiContainer.querySelector('div[style*="color: red"]') : null; if (errorMsg) { errorMsg.textContent = `Please wait ${(Math.ceil((draftCooldownUntil - Date.now()) / 1000))} seconds before trying again.`; errorMsg.style.display = 'block'; } console.log('Draft button clicked during cooldown period'); return; } console.log('Draft button clicked'); draftBtn.disabled = true; const errorMsg = uiContainer ? uiContainer.querySelector('div[style*="color: red"]') : null; if (errorMsg) errorMsg.style.display = 'none'; try { const promptInput = document.querySelector('.gpt-autodraft-ui textarea'); const contextPrompt = promptInput ? promptInput.value.trim() : null; // Use robust Gmail API-based one-off autodraft await oneOffAutoDraftForCurrentThread(contextPrompt); // Optionally, show a success message or update UI } catch (e) { console.error('Drafting failed:', e); const errorMsg = uiContainer ? uiContainer.querySelector('div[style*="color: red"]') : null; if (errorMsg) { if (e && e.code === 429) { errorMsg.textContent = 'Too many requests. Please wait 60 seconds.'; errorMsg.style.display = 'block'; draftCooldownUntil = Date.now() + 60000; draftBtn.disabled = true; console.log('Rate limit hit, setting cooldown for 60 seconds'); setTimeout(() => { draftBtn.disabled = false; if (errorMsg) errorMsg.style.display = 'none'; console.log('Cooldown period ended, re-enabling draft button'); }, 60000); } else { errorMsg.textContent = 'Drafting failed. Retrying...'; errorMsg.style.display = 'block'; console.log('Drafting failed, retrying in 2 seconds'); setTimeout(() => draftBtn.onclick(), 2000); } } } finally { if (Date.now() >= draftCooldownUntil) { draftBtn.disabled = false; console.log('Draft button re-enabled'); } } }; } } } }); observer.observe(document.body, { childList: true, subtree: true }); } } else { console.log('No reply box found, cannot show UI'); } } else { // Hide the UI console.log('Hiding UI'); isUIVisible = false; if (observer) { observer.disconnect(); observer = null; console.log('DOM observer disconnected'); } if (uiContainer) { uiContainer.remove(); uiContainer = null; console.log('UI container removed'); } } } // Insert Auto-Draft button and feedback UI function injectUI(replyBox, draftText, threadMessages) { console.log('Injecting UI into reply box'); // Prevent duplicate UI if (replyBox.parentElement.querySelector('.gpt-autodraft-ui')) { console.log('UI already exists, not injecting duplicate'); return; } // Container const container = document.createElement('div'); container.className = 'gpt-autodraft-ui'; container.style.background = COLORS.white; container.style.border = `2px solid ${COLORS.green}`; container.style.padding = '8px'; container.style.margin = '8px 0'; container.style.borderRadius = '8px'; container.style.position = 'fixed'; container.style.bottom = '24px'; container.style.right = '24px'; container.style.zIndex = '99999'; container.style.boxShadow = '0 2px 12px rgba(0,0,0,0.15)'; // Custom prompt input const promptLabel = document.createElement('label'); promptLabel.textContent = 'Context-Specific Prompt:'; promptLabel.style.color = COLORS.green; promptLabel.style.display = 'block'; promptLabel.style.marginBottom = '4px'; const promptInput = document.createElement('textarea'); promptInput.style.width = '100%'; promptInput.style.minHeight = '24px'; promptInput.style.height = 'auto'; promptInput.style.maxHeight = '240px'; promptInput.style.overflowY = 'auto'; promptInput.style.marginBottom = '8px'; promptInput.style.border = `1px solid ${COLORS.gold}`; promptInput.style.borderRadius = '4px'; promptInput.style.padding = '4px'; promptInput.style.paddingRight = '0px'; promptInput.style.resize = 'vertical'; // Auto-expand textarea promptInput.addEventListener('input', function() { this.style.height = 'auto'; this.style.height = (this.scrollHeight) + 'px'; }); // Auto-Draft button const draftBtn = document.createElement('button'); draftBtn.textContent = 'Auto-Draft with GPT'; draftBtn.style.background = COLORS.green; draftBtn.style.color = COLORS.white; draftBtn.style.border = 'none'; draftBtn.style.padding = '6px 12px'; draftBtn.style.borderRadius = '4px'; draftBtn.style.cursor = 'pointer'; draftBtn.style.width = '100%'; draftBtn.style.marginBottom = '8px'; // Add margin to separate from other elements // Error message const errorMsg = document.createElement('div'); errorMsg.style.color = 'red'; errorMsg.style.marginTop = '4px'; errorMsg.style.display = 'none'; errorMsg.style.marginRight = '40px'; // Add elements (no MIME box) container.appendChild(promptLabel); container.appendChild(promptInput); container.appendChild(draftBtn); container.appendChild(errorMsg); // Move toggleBtn creation and styling just after container creation for better stacking const toggleBtn = document.createElement('button'); toggleBtn.textContent = '–'; toggleBtn.title = 'Minimize'; toggleBtn.style.position = 'absolute'; toggleBtn.style.top = '8px'; toggleBtn.style.right = '8px'; toggleBtn.style.background = COLORS.gold; toggleBtn.style.color = COLORS.green; toggleBtn.style.border = 'none'; toggleBtn.style.borderRadius = '50%'; toggleBtn.style.width = '28px'; toggleBtn.style.height = '28px'; toggleBtn.style.cursor = 'pointer'; toggleBtn.style.fontWeight = 'bold'; toggleBtn.style.fontSize = '18px'; toggleBtn.style.zIndex = '100000'; // Ensure above other elements container.appendChild(toggleBtn); let minimized = false; function setPromptInputPadding(isMinimized) { if (isMinimized) { promptInput.style.paddingRight = ''; promptInput.style.width = '0'; } else { promptInput.style.paddingRight = '0px'; promptInput.style.width = '100%'; } } setPromptInputPadding(false); toggleBtn.onclick = () => { minimized = !minimized; if (minimized) { promptLabel.style.display = 'none'; promptInput.style.display = 'none'; errorMsg.style.display = 'none'; container.style.height = '44px'; container.style.width = '340px'; container.style.minWidth = '340px'; container.style.padding = '0 12px 0 16px'; container.style.display = 'flex'; container.style.alignItems = 'center'; container.style.justifyContent = 'flex-end'; toggleBtn.textContent = '+'; toggleBtn.title = 'Maximize'; toggleBtn.style.position = 'static'; toggleBtn.style.marginLeft = 'auto'; toggleBtn.style.marginRight = '0'; toggleBtn.style.top = ''; toggleBtn.style.right = ''; toggleBtn.style.display = 'block'; toggleBtn.style.background = COLORS.gold; toggleBtn.style.zIndex = '100000'; setPromptInputPadding(true); } else { promptLabel.style.display = 'block'; promptInput.style.display = 'block'; container.style.width = ''; container.style.minWidth = ''; container.style.height = ''; container.style.padding = '8px'; container.style.display = ''; container.style.alignItems = ''; container.style.justifyContent = ''; toggleBtn.textContent = '–'; toggleBtn.title = 'Minimize'; toggleBtn.style.position = 'absolute'; toggleBtn.style.top = '8px'; toggleBtn.style.right = '8px'; toggleBtn.style.margin = ''; toggleBtn.style.display = 'block'; toggleBtn.style.background = COLORS.gold; toggleBtn.style.zIndex = '100000'; setPromptInputPadding(false); } }; // Draft button logic (remove MIME logic) draftBtn.onclick = async () => { console.log('Draft button clicked'); // Cooldown check if (Date.now() < draftCooldownUntil) { errorMsg.textContent = `Please wait ${(Math.ceil((draftCooldownUntil - Date.now()) / 1000))} seconds before trying again.`; errorMsg.style.display = 'block'; console.log('Draft button clicked during cooldown period'); return; } draftBtn.disabled = true; errorMsg.style.display = 'none'; try { // Only use threadMessages and context prompt const threadContent = threadMessages; const contextPrompt = promptInput.value.trim(); const draftPrompt = threadContent ? contextPrompt : 'Write a professional email draft based on the following context: ' + contextPrompt; const draft = await requestDraft(threadContent || '', draftPrompt); if (threadContent) { const recipientName = extractRecipientName(); const formattedDraft = formatEmailReplySmart(draft, recipientName); insertDraftWithRetry(formattedDraft); } else { insertDraftWithRetry(draft); } } catch (e) { console.error('Drafting failed:', e); if (e && e.code === 429) { errorMsg.textContent = 'Too many requests. Please wait 60 seconds.'; errorMsg.style.display = 'block'; draftCooldownUntil = Date.now() + 60000; draftBtn.disabled = true; setTimeout(() => { draftBtn.disabled = false; errorMsg.style.display = 'none'; }, 60000); } else { errorMsg.textContent = 'Drafting failed. Retrying...'; errorMsg.style.display = 'block'; setTimeout(() => draftBtn.onclick(), 2000); } } finally { if (Date.now() >= draftCooldownUntil) { draftBtn.disabled = false; } } }; document.body.appendChild(container); } // Detect if we're on the main inbox page (any user index) function isInboxPage() { // Only match the main inbox, not thread view // Accepts: #inbox, #inbox/, #inbox?... Rejects: #inbox/ or #inbox/