UNPKG

@konvo-ai/sdk-web

Version:

KonvoAI Conversational Ad SDK for Web Applications - Intelligent contextual advertising with AI-powered decision engine

263 lines (262 loc) 10.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.KonvoAIAds = void 0; class KonvoAICore { constructor() { this.config = null; this.initialized = false; this.autoMode = false; this.chatObserver = null; } init(config) { if (!config.apiKey) { throw new Error('KonvoAI: apiKey is required'); } this.config = { apiKey: config.apiKey, baseUrl: config.baseUrl || 'https://api.konvo-ai.com', autoIntegrate: config.autoIntegrate !== false, // Default to true chatSelectors: config.chatSelectors || [ '[data-testid="chat-messages"]', // Common chat containers '.chat-messages', '.messages-container', '[class*="messages"]', '[class*="chat"]' ] }; this.initialized = true; if (this.config.autoIntegrate) { this.startAutoIntegration(); } } startAutoIntegration() { this.autoMode = true; // Wait for DOM to be ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => this.observeChatContainers()); } else { this.observeChatContainers(); } } observeChatContainers() { // Try to find chat containers const findChatContainer = () => { for (const selector of this.config.chatSelectors) { const container = document.querySelector(selector); if (container) return container; } return null; }; let chatContainer = findChatContainer(); if (chatContainer) { this.setupChatIntegration(chatContainer); } // Set up observer for dynamically added chat containers this.chatObserver = new MutationObserver((mutations) => { if (!chatContainer) { chatContainer = findChatContainer(); if (chatContainer) { this.setupChatIntegration(chatContainer); } } }); this.chatObserver.observe(document.body, { childList: true, subtree: true }); } setupChatIntegration(chatContainer) { console.log('🚀 KonvoAI: Auto-integration activated'); // Observe new messages const messageObserver = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { const element = node; this.checkForAdPlacement(element, chatContainer); } }); }); }); messageObserver.observe(chatContainer, { childList: true, subtree: true }); } async checkForAdPlacement(newElement, chatContainer) { // Simple heuristic: if this looks like a user message, potentially show an ad const textContent = newElement.textContent?.trim() || ''; // Skip very short messages or system messages if (textContent.length < 10 || this.isSystemMessage(newElement)) { return; } // Extract user context automatically const userContext = this.extractUserContext(); try { const response = await this.decide({ user: { anonId: this.generateAnonId(), country: userContext.country, language: userContext.language }, chat: { lastUserMsg: textContent }, surface: 'inline-chip' }); if (response.fill && response.render) { this.injectAdElement(response, chatContainer); } } catch (error) { console.error('KonvoAI: Auto-integration ad request failed:', error); } } isSystemMessage(element) { const classNames = element.className.toLowerCase(); const systemIndicators = ['system', 'bot', 'assistant', 'ai', 'automated']; return systemIndicators.some(indicator => classNames.includes(indicator)); } extractUserContext() { // Auto-detect user context from browser const language = navigator.language.split('-')[0]; const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; // Simple country detection from timezone (basic approach) const country = timezone.split('/')[1]?.toLowerCase().substring(0, 2) || 'us'; return { country, language }; } generateAnonId() { // Check for existing anonymous ID in localStorage let anonId = localStorage.getItem('konvo-anon-id'); if (!anonId) { anonId = 'anon_' + Math.random().toString(36).substr(2, 9) + Date.now().toString(36); localStorage.setItem('konvo-anon-id', anonId); } return anonId; } injectAdElement(response, chatContainer) { const adElement = document.createElement('div'); adElement.className = 'konvo-ai-auto-ad'; adElement.style.cssText = ` margin: 8px 0; padding: 12px; background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); border: 1px solid #dee2e6; border-radius: 8px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; position: relative; max-width: 300px; `; adElement.innerHTML = ` <div style="display: flex; align-items: center; gap: 12px;"> ${response.render.imageUrl ? `<img src="${response.render.imageUrl}" style="width: 40px; height: 40px; border-radius: 4px; object-fit: cover;" />` : ''} <div style="flex: 1;"> <h4 style="margin: 0 0 4px 0; font-size: 14px; font-weight: 600; color: #333;">${response.render.title}</h4> <p style="margin: 0 0 8px 0; font-size: 13px; color: #666; line-height: 1.4;">${response.render.desc}</p> <button onclick="this.nextSibling.click()" style="padding: 6px 12px; background: #0066cc; color: white; border: none; border-radius: 4px; font-size: 13px; cursor: pointer;"> ${response.render.cta.label} </button> <button style="display: none;"></button> </div> <span style="position: absolute; top: 4px; right: 4px; font-size: 10px; color: #999; background: rgba(255,255,255,0.9); padding: 2px 4px; border-radius: 2px;">Ad</span> </div> `; // Add click handler const button = adElement.querySelector('button:last-child'); button.onclick = () => response.render.cta.handler(); // Insert ad after the last message chatContainer.appendChild(adElement); // Track impression this.handleImpression(response); } ensureInitialized() { if (!this.initialized || !this.config) { throw new Error('KonvoAI: SDK not initialized. Call init() first.'); } } async decide(input) { this.ensureInitialized(); const scrubbed = { ...input, chat: { ...input.chat, lastUserMsg: this.scrub(input.chat.lastUserMsg) } }; try { const response = await fetch(`${this.config.baseUrl}/v1/decide`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.config.apiKey}` }, body: JSON.stringify(scrubbed) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); if (data.fill && data.render) { const originalCta = data.render.cta; data.render.cta = { label: originalCta.label, handler: () => this.handleClick(data.decisionId, originalCta.url) }; } return data; } catch (error) { console.error('KonvoAI: Failed to decide ad', error); return { decisionId: '', fill: false }; } } async handleImpression(response) { if (!response.decisionId || !response.fill) return; this.ensureInitialized(); const token = this.createToken(response.decisionId); const url = `${this.config.baseUrl}/t/i?d=${encodeURIComponent(token)}`; try { await fetch(url, { method: 'GET', mode: 'no-cors' }); } catch (error) { console.error('KonvoAI: Failed to track impression', error); } } async handleClick(decisionId, targetUrl) { this.ensureInitialized(); const token = this.createToken(decisionId); const clickUrl = `${this.config.baseUrl}/t/c?d=${encodeURIComponent(token)}`; try { await fetch(clickUrl, { method: 'GET', mode: 'no-cors' }); } catch (error) { console.error('KonvoAI: Failed to track click', error); } if (targetUrl) { window.open(targetUrl, '_blank'); } } scrub(text) { return text .replace(/\b[A-Z][a-z]+ [A-Z][a-z]+\b/g, '[NAME]') .replace(/\b\d{3}-\d{3}-\d{4}\b/g, '[PHONE]') .replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, '[EMAIL]') .replace(/\b\d{3}-\d{2}-\d{4}\b/g, '[SSN]') .replace(/\b(?:\d{4}[-\s]?){3}\d{4}\b/g, '[CARD]') .replace(/\b\d{5}(?:-\d{4})?\b/g, '[ZIP]'); } createToken(decisionId) { const token = { decisionId, timestamp: Date.now() }; return btoa(JSON.stringify(token)); } } exports.KonvoAIAds = new KonvoAICore();