1/** 2 * Planr AI Chat - HubSpot Lead Capture (Fixed with duplicate prevention) 3 * 4 * Call window.planrCaptureToHubSpot(conversationArray, userEmail) when ready 5 */ 6 7(function() { 8 'use strict'; 9 10 // Track captured emails to prevent duplicates in this session 11 const capturedEmails = new Set(); 12 const captureAttempts = new Map(); // Track attempts per email 13 14 /** 15 * Main function to capture lead to HubSpot 16 * 17 * @param {Array} conversation - Array of {role: 'user'|'assistant', content: 'text'} messages 18 * @param {string} email - User's email address 19 * @param {Object} additionalData - Optional: {name, company, role, portfolioCompanies, contentRequested} 20 */ 21 window.planrCaptureToHubSpot = function(conversation, email, additionalData = {}) { 22 // Check if HubSpot is enabled 23 if (!window.planrChatConfig || !window.planrChatConfig.hubspotEnabled) { 24 console.log('HubSpot integration not enabled'); 25 return Promise.resolve({success: false, message: 'HubSpot not enabled'}); 26 } 27 28 if (!email || !conversation || conversation.length === 0) { 29 console.error('Email and conversation are required'); 30 return Promise.reject('Email and conversation required'); 31 } 32 33 // Normalize email for comparison 34 const normalizedEmail = email.toLowerCase().trim(); 35 36 // CRITICAL: Prevent duplicate captures 37 if (capturedEmails.has(normalizedEmail)) { 38 console.log('â ï¸ Email already captured in this session:', normalizedEmail); 39 return Promise.resolve({success: true, duplicate: true, message: 'Already captured'}); 40 } 41 42 // Track capture attempts (rate limiting) 43 const attempts = captureAttempts.get(normalizedEmail) || 0; 44 if (attempts >= 3) { 45 console.error('â Too many capture attempts for:', normalizedEmail); 46 return Promise.reject('Too many attempts'); 47 } 48 captureAttempts.set(normalizedEmail, attempts + 1); 49 50 // Mark as captured IMMEDIATELY to prevent race conditions 51 capturedEmails.add(normalizedEmail); 52 53 // Format conversation for HubSpot 54 const formattedConversation = conversation.map((msg, index) => { 55 // Determine speaker with clear labels 56 let speaker; 57 if (msg.role === 'user') { 58 speaker = 'You'; 59 } else { 60 speaker = 'ð¤ PLANR AI'; 61 } 62 63 const time = new Date().toLocaleTimeString('en-US', { 64 hour: '2-digit', 65 minute: '2-digit', 66 hour12: false 67 }); 68 69 // Clean content - remove escape characters and extra whitespace 70 const cleanContent = msg.content 71 .replace(/\\'/g, "'") // Fix escaped quotes 72 .replace(/\\"/g, '"') 73 .replace(/\s+/g, ' ') // Normalize whitespace 74 .trim(); 75 76 return `${speaker}: ${cleanContent}`; 77 }).join('\n\n---\n\n'); 78 79 // Parse conversation for additional context 80 const parsedData = parseConversationForContext(conversation); 81 82 // Debug logging 83 console.log('ð Parsed data from conversation:', parsedData); 84 85 // Merge parsed data with provided data 86 const finalData = { 87 email: email, 88 name: additionalData.name || parsedData.name || '', 89 company: additionalData.company || parsedData.company || '', 90 role: additionalData.role || parsedData.role || '', 91 portfolio_companies: additionalData.portfolioCompanies || parsedData.portfolioCompanies || '', 92 content_requested: additionalData.contentRequested || parsedData.contentRequested || '', 93 conversation: formattedConversation, 94 lead_source: 'AI Chatbot' 95 }; 96 97 console.log('ð¤ Final data being sent to HubSpot:', finalData); 98 99 // Prepare AJAX data 100 const ajaxData = new URLSearchParams({ 101 action: 'planr_capture_lead', 102 nonce: window.planrChatConfig.nonce, 103 ...finalData 104 }); 105 106 console.log('ð¤ Sending to HubSpot:', normalizedEmail); 107 108 // Send to HubSpot 109 return fetch(window.planrChatConfig.ajaxUrl, { 110 method: 'POST', 111 headers: { 112 'Content-Type': 'application/x-www-form-urlencoded', 113 }, 114 body: ajaxData 115 }) 116 .then(response => response.json()) 117 .then(result => { 118 if (result.success) { 119 console.log('â Lead captured to HubSpot:', normalizedEmail); 120 return {success: true, contactId: result.data.contact_id}; 121 } else { 122 console.error('â Failed to capture lead:', result.data.message); 123 if (result.data.details) { 124 console.error('Details:', result.data.details); 125 } 126 if (result.data.debug) { 127 console.error('Full error data:', result.data); 128 } 129 // Remove from captured set if it failed 130 capturedEmails.delete(normalizedEmail); 131 return {success: false, message: result.data.message, details: result.data.details}; 132 } 133 }) 134 .catch(error => { 135 console.error('â Error capturing lead:', error); 136 // Remove from captured set if it failed 137 capturedEmails.delete(normalizedEmail); 138 return {success: false, error: error.toString()}; 139 }); 140 }; 141 142 /** 143 * Parse conversation for additional context 144 */ 145 function parseConversationForContext(conversation) { 146 const data = { 147 name: null, 148 company: null, 149 role: null, 150 portfolioCompanies: null, 151 contentRequested: null 152 }; 153 154 // ONLY look at user messages for name extraction 155 const userMessages = conversation.filter(m => m.role === 'user'); 156 157 // Extract name - check EACH user message individually 158 const namePatterns = [ 159 /(?:my name is|i'm|i am|this is)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)/i, 160 /^([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)$/ // Single capitalized word(s) as entire message 161 ]; 162 163 for (const message of userMessages) { 164 const messageText = message.content.trim(); 165 166 for (const pattern of namePatterns) { 167 const match = messageText.match(pattern); 168 if (match && !data.name) { 169 const extractedName = match[1].trim(); 170 // Don't extract if it looks like an email 171 if (!extractedName.includes('@') && !extractedName.includes('.com')) { 172 data.name = extractedName; 173 break; 174 } 175 } 176 } 177 178 if (data.name) break; // Stop once we found a name 179 } 180 181 // For company, role, etc., we can look at all text 182 const allText = conversation.map(m => m.content).join(' '); 183 184 // Extract company name 185 const companyPatterns = [ 186 /(?:from|at|with|work for|work at)\s+([A-Z][A-Za-z0-9\s&]+?)(?:\s+and|\s+in|\.|,|$)/, 187 /([A-Z][A-Za-z0-9\s&]+?)\s+(?:portfolio|firm|fund|partners|capital|ventures)/i 188 ]; 189 190 for (const pattern of companyPatterns) { 191 const match = allText.match(pattern); 192 if (match && !data.company) { 193 const company = match[1].trim(); 194 // Filter out common false positives 195 if (!['Partner', 'Managing Director', 'VP', 'CEO', 'CFO', 'COO'].includes(company)) { 196 data.company = company; 197 break; 198 } 199 } 200 } 201 202 // Extract role mentions 203 const rolePatterns = [ 204 /\b(partner|managing director|principal|analyst|associate|VP|director|head of|CFO|CEO|COO|operating partner)\b/i 205 ]; 206 207 rolePatterns.forEach(pattern => { 208 const match = allText.match(pattern); 209 if (match && !data.role) { 210 data.role = match[1]; 211 } 212 }); 213 214 // Extract portfolio company count 215 const portfolioMatch = allText.match(/\b(\d+)\s+(?:portfolio\s+)?compan(?:y|ies)
215/i); 216 if (portfolioMatch) { 217 data.portfolioCompanies = portfolioMatch[1]; 218 } 219 220 // Extract content requests 221 const contentKeywords = { 222 'video': 'Product Walkthrough Videos', 223 'walkthrough': 'Product Walkthrough Videos', 224 'case study': 'Case Studies', 225 'case studies': 'Case Studies', 226 'collateral': 'Additional Collateral', 227 'one-pager': 'Additional Collateral', 228 'comparison': 'Comparison Guides' 229 }; 230 231 Object.keys(contentKeywords).forEach(keyword => { 232 if (allText.toLowerCase().includes(keyword) && !data.contentRequested) { 233 data.contentRequested = contentKeywords[keyword]; 234 } 235 }); 236 237 return data; 238 } 239 240 /** 241 * Helper: Extract email from text 242 */ 243 window.planrExtractEmail = function(text) { 244 const emailRegex = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/; 245 const match = text.match(emailRegex); 246 return match ? match[0] : null; 247 }; 248 249 /** 250 * Helper: Check if email is personal (gmail, yahoo, etc.) 251 */ 252 window.planrIsPersonalEmail = function(email) { 253 const personalDomains = [ 254 'gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com', 255 'icloud.com', 'aol.com', 'protonmail.com', 'mail.com', 256 'zoho.com', 'yandex.com', 'gmx.com', 'live.com', 'msn.com', 257 'me.com', 'mac.com', 'inbox.com', 'hey.com' 258 ]; 259 260 if (!email || typeof email !== 'string') { 261 return true; // Treat invalid emails as personal 262 } 263 264 const domain = email.split('@')[1]?.toLowerCase(); 265 return personalDomains.includes(domain); 266 }; 267 268 /** 269 * Auto-capture: Watch for email in messages 270 * This monitors the chat and auto-captures when email is detected 271 * WITH PROPER DEBOUNCING AND DUPLICATE PREVENTION 272 */ 273 function setupAutoCapture() { 274 let autoCaptureFired = false; // Only fire auto-capture once per session 275 276 // Look for the chat messages container 277 const checkForMessages = setInterval(function() { 278 const messagesContainer = document.querySelector('.planr-chat-messages'); 279 280 if (messagesContainer) { 281 clearInterval(checkForMessages); 282 283 // Debounce function to prevent rapid-fire captures 284 let captureTimeout = null; 285 286 // Use MutationObserver to watch for new messages 287 const observer = new MutationObserver(function(mutations) { 288 // Only process if we haven't auto-captured yet 289 if (autoCaptureFired) { 290 return; 291 } 292 293 // Clear any pending capture 294 if (captureTimeout) { 295 clearTimeout(captureTimeout); 296 } 297 298 // Debounce: Wait 3 seconds after last mutation before checking 299 captureTimeout = setTimeout(function() { 300 if (autoCaptureFired) { 301 return; // Double-check 302 } 303 304 // Get all text from all messages 305 const allMessages = Array.from( 306 document.querySelectorAll('.planr-message') 307 ); 308 309 let foundEmail = null; 310 311 // Search for email in any message 312 for (const msg of allMessages) { 313 // CRITICAL FIX: Only get content from .planr-message-content div 314 const contentDiv = msg.querySelector('.planr-message-content'); 315 const content = contentDiv ? contentDiv.textContent : ''; 316 const email = window.planrExtractEmail(content); 317 318 if (email) { 319 foundEmail = email; 320 break; 321 } 322 } 323 324 if (foundEmail) { 325 console.log('ð§ Email detected (auto-capture):', foundEmail); 326 autoCaptureFired = true; // Mark as fired IMMEDIATELY 327 328 // Disconnect observer to prevent further triggers 329 observer.disconnect(); 330 331 // Build conversation array 332 const conversation = allMessages.map(function(msg) { 333 // Check if message is from user or bot 334 // Messages have class 'planr-message user' or 'planr-message assistant' 335 const isUser = msg.classList.contains('user'); 336 const isAssistant = msg.classList.contains('assistant'); 337 338 // CRITICAL FIX: Only get content from .planr-message-content div 339 // Do NOT use msg.textContent as it includes the avatar "You" text 340 const contentDiv = msg.querySelector('.planr-message-content'); 341 const content = contentDiv ? contentDiv.textContent.trim() : ''; 342 343 // Determine role based on class 344 let role = 'assistant'; // default to assistant 345 if (isUser) { 346 role = 'user'; 347 } else if (isAssistant) { 348 role = 'assistant'; 349 } 350 351 return { 352 role: role, 353 content: content 354 }; 355 }); 356 357 if (conversation.length > 0) { 358 window.planrCaptureToHubSpot(conversation, foundEmail) 359 .then(result => { 360 if (result.success) { 361 console.log('â Auto-captured to HubSpot successfully'); 362 } 363 }); 364 } 365 } 366 }, 3000); // Wait 3 seconds after last mutation 367 }); 368 369 // Start observing 370 observer.observe(messagesContainer, { 371 childList: true, 372 subtree: true 373 }); 374 375 console.log('â HubSpot auto-capture initialized (with duplicate prevention)'); 376 } 377 }, 500); 378 379 // Stop checking after 10 seconds 380 setTimeout(function() { 381 clearInterval(checkForMessages); 382 }, 10000); 383 } 384 385 // Initialize auto-capture when DOM is ready 386 if (document.readyState === 'loading') { 387 document.addEventListener('DOMContentLoaded', setupAutoCapture); 388 } else { 389 setupAutoCapture(); 390 } 391 392 console.log('Planr HubSpot Lead Capture loaded'); 393 console.log('Manual capture: window.planrCaptureToHubSpot(conversation, email)'); 394 console.log('Auto-capture: Monitoring for emails in chat...'); 395})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.