@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
JavaScript
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();
;