PageSourceSearch

https://planr.com/wp-content/plugins/planr-ai-chat-v4.4.1/assets/js/planr-hubspot-capture.js?ver=4.4.0

js planr.com collected 2026-10-08 06:23:33 UTC 16,583 bytes, 395 lines download raw bytes

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.