A
AMIT BHAVIK Enterprise Systems
AI Engine: GPT-4o-mini 🔒 Secured

Command Center

AI Engine: Pluggable (Gemini / OpenAI / Claude / Custom)

Automated High-Performance Lead Engineering

Real-time Intelligent Lead Orchestration

Data Vault: Multi-Industry Repository

0 Files Stored
Total Leads
0
From imported files
High Intent Leads
0
Score ≥ 75%
Leads Mapped
0
Saved & processed
Pending Review
0
Analyzed but not saved
Intent Score Distribution
High (≥75)
Medium (50-74)
Low (<50)
Lead Status Funnel
Unprocessed
Analyzed
Mapped
Source File Distribution
Time Activity (Last 7 Days)
Avg Intent Score
0
Overall quality metric
% High Intent
0%
Qualified leads
Conversion Rate
0%
Mapped / Analyzed
Pending Ratio
0%
Review needed

No industry files in vault. Import a file to begin registration.

01. Ingested Stream

Name Title Company LinkedIn
Import file to begin

02. Verified Leads

Name Company Score Action
Execute mapping to verify

Automation Architecture

From Form Fill to CRM in Seconds

01

Capture

Real-time webhook ingestion from forms, LinkedIn ads, or direct stream imports.

02

Enrich

Automatic waterfall enrichment across LinkedIn, Apollo, and Clearbit for full firmographics.

03

Score

AI-driven qualification based on Amit Bhavik's proprietary ICP/GTM alignment logic.

04

Route

Instant lead assignment to CRM (Hubspot/Salesforce) or Slack alerts for Tier-1 accounts.

05

Outreach

AI-synthesized personalized messaging deployed via Smartlead or Instantly.

/* ========================= SUPABASE CONFIG ========================= */ const SUPABASE_URL = "https://tgqxsmbnpnswxkqvyulz.supabase.co"; const SUPABASE_ANON_KEY = "sb_publishable_mNBzwoMYp5nF1F1KzWY_tA_BTgAU35C"; const supabaseClient = supabase.createClient( SUPABASE_URL, SUPABASE_ANON_KEY ); /* ========================= GLOBAL STATE ========================= */ const state = { allLeads: [], fileRegistry: [], currentSourceFile: null, activeLead: null, savedLeads: [] }; /* ========================= PERSISTENCE LAYER Keys: ab_fileRegistry | ab_savedLeads | ab_currentSourceFile ========================= */ const LS = { REGISTRY: 'ab_fileRegistry', SAVED: 'ab_savedLeads', CURRENT_FILE: 'ab_currentSourceFile' }; function saveStateToStorage() { try { localStorage.setItem(LS.REGISTRY, JSON.stringify(state.fileRegistry)); localStorage.setItem(LS.SAVED, JSON.stringify(state.savedLeads)); localStorage.setItem(LS.CURRENT_FILE, state.currentSourceFile || ''); } catch (e) { console.warn('localStorage write failed:', e); } } function loadStateFromStorage() { try { const reg = localStorage.getItem(LS.REGISTRY); const saved = localStorage.getItem(LS.SAVED); const cur = localStorage.getItem(LS.CURRENT_FILE); if (reg) state.fileRegistry = JSON.parse(reg); if (saved) state.savedLeads = JSON.parse(saved); if (cur) state.currentSourceFile = cur || null; } catch (e) { console.warn('localStorage read failed:', e); } } /* ========================= initApp — called on DOMContentLoaded ========================= */ async function initApp() { // 1. Hydrate state from localStorage loadStateFromStorage(); // 2. If a file was previously selected, re-load its leads into allLeads if (state.currentSourceFile) { const entry = state.fileRegistry.find(f => f.name === state.currentSourceFile); if (entry) { state.allLeads = [...entry.data]; } } // 3. Re-render all views renderVault(); renderP1(); renderSaved(); updateDashboardCards(); // 4. If cloud mode was on, pull latest from Supabase if (document.getElementById('modeCloud')?.checked) { await loadSavedFromDB(); } // 5. Restore Execute Mapping table if leads exist if (state.allLeads.length > 0) { document.getElementById('analyzeAllBtn')?.classList.remove('hidden'); } } document.addEventListener('DOMContentLoaded', initApp); /* ========================= AUTH — Sign Out ========================= */ async function handleSignOut() { await supabaseClient.auth.signOut(); window.location.replace('/login.html'); } /* ========================= CHART INSTANCES (Global) ========================= */ let intentScoreChart = null; let statusFunnelChart = null; let sourceFileChart = null; let timeActivityChart = null; /* ========================= VIEW SWITCH ========================= */ function switchMainView(view) { document.getElementById('view-live').classList.toggle('hidden', view !== 'live'); document.getElementById('view-saved').classList.toggle('hidden', view !== 'saved'); document.getElementById('tab-live').classList.toggle('active', view === 'live'); document.getElementById('tab-saved').classList.toggle('active', view === 'saved'); if (view === 'saved') { if (document.getElementById('modeCloud').checked) { loadSavedFromDB(); } else { renderSaved(); } } } /* ========================= HELPERS ========================= */ const findKey = (obj, patterns) => { const key = Object.keys(obj).find(k => patterns.some(p => k.toLowerCase().trim() === p.toLowerCase()) ); if (key) return obj[key]; const partial = Object.keys(obj).find(k => patterns.some(p => k.toLowerCase().includes(p.toLowerCase())) ); return partial ? obj[partial] : null; }; const getScoreClass = (score) => { if (score >= 75) return 'score-high'; if (score >= 50) return 'score-med'; return 'score-low'; }; /* ========================= DASHBOARD CARDS + FILTER LOGIC ========================= */ function updateDashboardCards() { applyCardFilter('intent'); applyCardFilter('mapped'); applyCardFilter('pending'); document.getElementById('card-total-leads').innerText = state.allLeads.length; updateInfographics(); } function applyCardFilter(card) { if (card === 'intent') { const filter = document.getElementById('intent-filter')?.value || 'high'; const labelEl = document.getElementById('card-intent-label'); const subEl = document.getElementById('card-intent-sub'); const valEl = document.getElementById('card-high-intent'); const labels = { high: ['High Intent Leads','Score ≥ 75%'], medium: ['Medium Intent Leads','Score 50–74%'], low: ['Low Intent Leads','Score < 50%'] }; const [label, sub] = labels[filter] || labels.high; const count = filter === 'high' ? state.allLeads.filter(l => l.score >= 75).length : filter === 'medium' ? state.allLeads.filter(l => l.score >= 50 && l.score < 75).length : state.allLeads.filter(l => l.score < 50).length; if (labelEl) labelEl.innerText = label; if (subEl) subEl.innerText = sub; if (valEl) valEl.innerText = count; // ✅ FIX: Re-render Verified Leads table filtered by selected intent tier // Only re-render if Execute Mapping has been run (body-p2 has real rows) const p2Body = document.getElementById('body-p2'); if (p2Body && state.allLeads.length) { const filtered = filter === 'high' ? state.allLeads.filter(l => l.score >= 75) : filter === 'medium' ? state.allLeads.filter(l => l.score >= 50 && l.score < 75) : state.allLeads.filter(l => l.score < 50); if (!filtered.length) { p2Body.innerHTML = `No ${label.toLowerCase()} in current file`; return; } p2Body.innerHTML = filtered.map(l => ` ${l.name} ${l.company} ${l.score}% ${l.gtmAnalysis ? '✓ Analyzed' : ''} `).join(''); } } if (card === 'mapped') { const filter = document.getElementById('mapped-filter')?.value || 'all'; const labelEl = document.getElementById('card-mapped-label'); const subEl = document.getElementById('card-mapped-sub'); const valEl = document.getElementById('card-leads-mapped'); const filtered = filter === 'high' ? state.savedLeads.filter(l => l.score >= 75) : filter === 'medium' ? state.savedLeads.filter(l => l.score >= 50 && l.score < 75) : filter === 'low' ? state.savedLeads.filter(l => l.score < 50) : state.savedLeads; const subLabels = { all: 'Saved & processed', high: 'High intent saved', medium: 'Medium intent saved', low: 'Low intent saved' }; if (labelEl) labelEl.innerText = filter === 'all' ? 'Leads Mapped' : (subLabels[filter] || 'Leads Mapped'); if (subEl) subEl.innerText = subLabels[filter] || 'Saved & processed'; if (valEl) valEl.innerText = filtered.length; } if (card === 'pending') { const filter = document.getElementById('pending-filter')?.value || 'analyzed'; const labelEl = document.getElementById('card-pending-label'); const subEl = document.getElementById('card-pending-sub'); const valEl = document.getElementById('card-pending-review'); const count = filter === 'analyzed' ? state.allLeads.filter(l => l.gtmAnalysis && l.status !== 'Mapped').length : filter === 'unprocessed' ? state.allLeads.filter(l => l.status === 'Unprocessed').length : state.allLeads.filter(l => l.status !== 'Mapped').length; const labels = { analyzed: ['Pending Review','Analyzed but not saved'], unprocessed: ['Unprocessed Leads','Not yet analyzed'], all: ['All Pending','Total unfinished leads'] }; const [label, sub] = labels[filter] || labels.analyzed; if (labelEl) labelEl.innerText = label; if (subEl) subEl.innerText = sub; if (valEl) valEl.innerText = count; } } /* ========================= INFGRAPHICS UPDATE ========================= */ function updateInfographics() { updateIntentScoreChart(); updateStatusFunnelChart(); updateSourceFileChart(); updateTimeActivityChart(); updateStrategicHeatPanel(); } function updateIntentScoreChart() { const ctx = document.getElementById('intentScoreChart'); if (!ctx) return; const high = state.allLeads.filter(l => l.score >= 75).length; const medium = state.allLeads.filter(l => l.score >= 50 && l.score < 75).length; const low = state.allLeads.filter(l => l.score < 50).length; // FIX 7: Never pass all-zero data to doughnut — Chart.js renders nothing const total = high + medium + low; const safeData = total > 0 ? [high, medium, low] : [1, 1, 1]; const safeColors = total > 0 ? ['#22c55e', '#f59e0b', '#ef4444'] : ['rgba(34,197,94,0.15)', 'rgba(245,158,11,0.15)', 'rgba(239,68,68,0.15)']; const data = { labels: ['High Intent', 'Medium', 'Low'], datasets: [{ data: safeData, backgroundColor: safeColors, borderWidth: 0, hoverOffset: 4 }] }; const config = { type: 'doughnut', data: data, options: { responsive: true, maintainAspectRatio: false, cutout: '70%', plugins: { legend: { display: false }, tooltip: { backgroundColor: 'rgba(11, 15, 26, 0.9)', titleColor: '#a855f7', bodyColor: '#e5e7eb', bodyFont: { size: 11, weight: 'bold' }, padding: 12, cornerRadius: 8, displayColors: true, callbacks: { label: function(context) { const total = context.dataset.data.reduce((a, b) => a + b, 0); const percentage = total > 0 ? ((context.raw / total) * 100).toFixed(1) : 0; return `${context.label}: ${context.raw} (${percentage}%)`; } } } } } }; if (intentScoreChart) intentScoreChart.destroy(); intentScoreChart = new Chart(ctx, config); } function updateStatusFunnelChart() { const ctx = document.getElementById('statusFunnelChart'); if (!ctx) return; const unprocessed = state.allLeads.filter(l => l.status === 'Unprocessed').length; const analyzed = state.allLeads.filter(l => l.gtmAnalysis && l.status !== 'Mapped').length; const mapped = state.savedLeads.length; const data = { labels: ['Unprocessed', 'Analyzed', 'Mapped'], datasets: [{ data: [unprocessed, analyzed, mapped], backgroundColor: ['rgba(99,102,241,0.8)', 'rgba(168,85,247,0.8)', 'rgba(16,185,129,0.8)'], borderColor: ['rgba(99,102,241,1)', 'rgba(168,85,247,1)', 'rgba(16,185,129,1)'], borderWidth: 2, borderRadius: 8, borderSkipped: false }] }; const config = { type: 'bar', data: data, options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { backgroundColor: 'rgba(11,15,26,0.9)', titleColor: '#a855f7', bodyColor: '#e5e7eb', bodyFont: { size: 11, weight: 'bold' }, padding: 12, cornerRadius: 8 } }, scales: { x: { beginAtZero: true, grid: { color: 'rgba(255,255,255,0.05)', drawBorder: false }, ticks: { color: '#64748b', font: { size: 9, weight: '600' } } }, y: { grid: { display: false }, ticks: { color: '#e5e7eb', font: { size: 10, weight: '700' } } } } } }; if (statusFunnelChart) statusFunnelChart.destroy(); statusFunnelChart = new Chart(ctx, config); } function updateSourceFileChart() { const ctx = document.getElementById('sourceFileChart'); if (!ctx) return; const sourceMap = {}; state.allLeads.forEach(lead => { const source = lead.sourceFile || 'Unknown'; sourceMap[source] = (sourceMap[source] || 0) + 1; }); const labels = Object.keys(sourceMap); const data = Object.values(sourceMap); const config = { type: 'bar', data: { labels: labels, datasets: [{ label: 'Leads', data: data, backgroundColor: 'rgba(168,85,247,0.8)', borderColor: 'rgba(168,85,247,1)', borderWidth: 2, borderRadius: 8, borderSkipped: false }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { backgroundColor: 'rgba(11,15,26,0.9)', titleColor: '#a855f7', bodyColor: '#e5e7eb', bodyFont: { size: 11, weight: 'bold' }, padding: 12, cornerRadius: 8 } }, scales: { x: { grid: { display: false }, ticks: { color: '#64748b', font: { size: 9, weight: '600' }, maxRotation: 45, minRotation: 45 } }, y: { beginAtZero: true, grid: { color: 'rgba(255,255,255,0.05)', drawBorder: false }, ticks: { color: '#e5e7eb', font: { size: 10, weight: '700' } } } } } }; if (sourceFileChart) sourceFileChart.destroy(); sourceFileChart = new Chart(ctx, config); } function updateTimeActivityChart() { const ctx = document.getElementById('timeActivityChart'); if (!ctx) return; const dayMap = {}; const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; state.savedLeads.forEach(lead => { const date = new Date(lead.updated_at || lead.created_at); const dayName = days[date.getDay()]; dayMap[dayName] = (dayMap[dayName] || 0) + 1; }); const data = days.map(day => dayMap[day] || 0); const config = { type: 'line', data: { labels: days, datasets: [{ label: 'Leads Processed', data: data, borderColor: '#a855f7', backgroundColor: 'rgba(168,85,247,0.1)', borderWidth: 3, pointBackgroundColor: '#a855f7', pointBorderColor: '#fff', pointBorderWidth: 2, pointRadius: 6, pointHoverRadius: 8, fill: true, tension: 0.4 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { backgroundColor: 'rgba(11,15,26,0.9)', titleColor: '#a855f7', bodyColor: '#e5e7eb', bodyFont: { size: 11, weight: 'bold' }, padding: 12, cornerRadius: 8 } }, scales: { x: { grid: { display: false }, ticks: { color: '#e5e7eb', font: { size: 10, weight: '700' } } }, y: { beginAtZero: true, grid: { color: 'rgba(255,255,255,0.05)', drawBorder: false }, ticks: { color: '#64748b', font: { size: 9, weight: '600' }, stepSize: 1 } } } } }; if (timeActivityChart) timeActivityChart.destroy(); timeActivityChart = new Chart(ctx, config); } function updateStrategicHeatPanel() { const totalLeads = state.allLeads.length; const highIntentLeads = state.allLeads.filter(l => l.score >= 75).length; const mappedLeads = state.savedLeads.length; const analyzedLeads = state.allLeads.filter(l => l.gtmAnalysis && l.status !== 'Mapped').length; const avgScore = totalLeads > 0 ? Math.round(state.allLeads.reduce((sum, l) => sum + l.score, 0) / totalLeads) : 0; const highIntentPercent = totalLeads > 0 ? ((highIntentLeads / totalLeads) * 100).toFixed(1) : 0; const conversionRate = analyzedLeads > 0 ? ((mappedLeads / analyzedLeads) * 100).toFixed(1) : 0; const pendingRatio = totalLeads > 0 ? ((analyzedLeads / totalLeads) * 100).toFixed(1) : 0; document.getElementById('heat-avg-score').innerText = avgScore; document.getElementById('heat-high-intent').innerText = highIntentPercent + '%'; document.getElementById('heat-conversion').innerText = conversionRate + '%'; document.getElementById('heat-pending').innerText = pendingRatio + '%'; } /* ========================= FILE IMPORT ========================= */ document.getElementById('leadInbound').addEventListener('change', (e) => { const file = e.target.files[0]; if (!file) return; // FIX 5: Duplicate file detection before vault registration const isDuplicate = state.fileRegistry.some(f => f.name === file.name); if (isDuplicate) { document.getElementById('warningModal').classList.remove('hidden'); e.target.value = ''; // Reset so same file can be re-attempted after rename return; } const isCSV = file.name.toLowerCase().endsWith('.csv'); const reader = new FileReader(); reader.onload = (evt) => { try { let data; if (isCSV) { const wb = XLSX.read(evt.target.result, { type: 'string' }); data = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]); } else { const dataArr = new Uint8Array(evt.target.result); const wb = XLSX.read(dataArr, { type: 'array' }); data = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]); } if (!data || data.length === 0) { alert('File appears to be empty or could not be parsed.'); return; } const processed = data.map(l => ({ id: crypto.randomUUID(), sourceFile: file.name, name: findKey(l, ['name','full name','contact name']) || 'Unknown', title: findKey(l, ['title','role','position']) || 'Professional', company: findKey(l, ['company','organization']) || 'Enterprise', linkedin: findKey(l, ['linkedin','url']) || 'NA', score: Math.floor(Math.random() * 80) + 20, status: 'Unprocessed', gtmAnalysis: null, source: file.name, tags: ['Imported'], updated_at: new Date().toISOString() })); // Check for cross-vault duplicates before finalizing const hasDupes = checkImportDuplicates(processed, file.name); if (hasDupes) { // Store pending data — user will decide via modal _pendingImportData = { fileName: file.name, processed }; } else { finalizeImport({ fileName: file.name, processed }); } } catch (err) { console.error('File parse error:', err); alert('Failed to parse file: ' + err.message); } }; e.target.value = ''; // Reset so same file can be re-uploaded if (isCSV) { reader.readAsText(file); } else { reader.readAsArrayBuffer(file); } }); /* ========================= VAULT ========================= */ function renderVault() { const grid = document.getElementById('file-vault-grid'); document.getElementById('vault-count').innerText = `${state.fileRegistry.length} Files Stored`; if (!state.fileRegistry.length) { grid.innerHTML = `
No files in vault
`; return; } grid.innerHTML = state.fileRegistry.map((f, i) => `

${f.name}

${f.data.length} leads · Click to load

${state.currentSourceFile === f.name ? '

● Active

' : ''}
`).join(''); } function selectVaultFile(index) { const selectedFile = state.fileRegistry[index]; if (!selectedFile) return; state.currentSourceFile = selectedFile.name; state.allLeads = [...selectedFile.data]; // FIX 3: Persist the selected file so it reloads on refresh saveStateToStorage(); renderP1(); resetVerified(); renderVault(); updateDashboardCards(); document.getElementById('analyzeAllBtn')?.classList.remove('hidden'); } function resetVerified() { document.getElementById('body-p2').innerHTML = `Execute mapping to verify`; } /* ========================= RENDER TABLES ========================= */ function renderP1() { const body = document.getElementById('body-p1'); if (!state.allLeads.length) { body.innerHTML = `Import file to begin`; return; } body.innerHTML = state.allLeads.map(l => ` ${l.name} ${l.title} ${l.company} ${l.linkedin} `).join(''); } /* ========================= EXECUTE MAPPING ========================= */ document.getElementById('saveMappedBtn').onclick = () => { if (!state.allLeads.length) return; // Execute Mapping: pure display render — no AI, just show leads scored and ready // Respects the current intent filter if set const filter = document.getElementById('intent-filter')?.value || 'all'; const leadsToShow = (filter !== 'all' && filter !== 'high') ? (filter === 'medium' ? state.allLeads.filter(l => l.score >= 50 && l.score < 75) : state.allLeads.filter(l => l.score < 50)) : state.allLeads; // default: show all document.getElementById('body-p2').innerHTML = leadsToShow.map(l => ` ${l.name} ${l.company} ${l.score}% ${l.gtmAnalysis ? '✓ AI' : ''} `).join(''); document.getElementById('analyzeAllBtn')?.classList.remove('hidden'); }; /* ========================= AI ENGINE — Pluggable BYK Router Supports: OpenAI | Gemini | Claude (proxy) | Custom ========================= */ // Legacy stubs — safe no-ops, kept to prevent any reference errors function saveOpenAIKey() {} function getOpenAIKey() { return ''; } /* ========================= AI ENGINE — Secure Server-Side Proxy All calls go to /api/analyze (Cloudflare Pages Function). The OpenAI API key lives ONLY in Cloudflare environment variables. It is never sent to the browser. Never in source code. Never in localStorage. ========================= */ async function getAIIntel(lead, customPrompt = null) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 35000); try { const res = await fetch('/api/analyze', { method: 'POST', signal: controller.signal, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ lead, customPrompt: customPrompt || null }), }); if (!res.ok) { const errBody = await res.json().catch(() => ({})); throw new Error(errBody.error || `Server error ${res.status}`); } return await res.json(); } catch (err) { if (err.name === 'AbortError') throw new Error('Request timed out (35s). Please try again.'); throw err; } finally { clearTimeout(timeout); } } // These stubs exist only to avoid runtime errors if any old reference survived function saveOpenAIKey() {} function getOpenAIKey() { return ''; } /* ========================= ANALYZE FLOW ========================= */ async function openAnalyze(id) { const lead = state.allLeads.find(l => l.id === id); if (!lead) return; state.activeLead = lead; document.getElementById('leadModal').classList.remove('hidden'); document.getElementById('modalLeadName').innerText = lead.name; document.getElementById('modalLeadMeta').innerText = `${lead.title} @ ${lead.company}`; // Clear previous content & show loading immediately ['painArea','keyInsight','proposedSolution','outreachMsg'].forEach(id => { document.getElementById(id).innerText = ''; }); document.getElementById('customPrompt').value = ''; document.getElementById('modalLoading').classList.remove('hidden'); document.getElementById('modalContent').style.opacity = '0.3'; try { const data = await getAIIntel(lead); lead.gtmAnalysis = data; document.getElementById('painArea').innerText = data['Pain Area'] || 'N/A'; document.getElementById('keyInsight').innerText = data['Key Insight'] || 'N/A'; document.getElementById('proposedSolution').innerText = data['Proposed Solution'] || 'N/A'; document.getElementById('outreachMsg').innerText = data['Cold Outreach Message'] || 'N/A'; // Show only a clean user-editable hint — JSON instructions stay hidden in code document.getElementById('customPrompt').value = `Refine this analysis for ${lead.name} at ${lead.company}: (e.g. "Focus on SaaS pain points", "Rewrite outreach more casually", "Emphasize ROI angle")`; } catch (err) { document.getElementById('painArea').innerText = '❌ ' + err.message; document.getElementById('keyInsight').innerText = 'Check your API key and internet connection, then try again.'; console.error('openAnalyze error:', err); } finally { document.getElementById('modalLoading').classList.add('hidden'); document.getElementById('modalContent').style.opacity = '1'; } } /* ========================= RE-ANALYZE WITH CUSTOM PROMPT ========================= */ async function reAnalyzeWithCustomPrompt() { const userHint = document.getElementById('customPrompt').value.trim(); if (!userHint) { alert('Please enter a refinement instruction.'); return; } document.getElementById('modalLoading').classList.remove('hidden'); document.getElementById('modalContent').style.opacity = '0.4'; try { const lead = state.activeLead; // Always wrap user's free-text hint into a structured prompt with JSON enforcement const finalPrompt = `Analyze the following B2B lead and generate GTM intelligence. Name: ${lead.name} Title: ${lead.title} Company: ${lead.company} Specific instruction from user: ${userHint} Return ONLY a valid JSON object with exactly these keys: "Pain Area", "Key Insight", "Proposed Solution", "Cold Outreach Message". No markdown, no explanation. Just the raw JSON object.`; const data = await getAIIntel(lead, finalPrompt); state.activeLead.gtmAnalysis = data; document.getElementById('painArea').innerText = data['Pain Area'] || 'N/A'; document.getElementById('keyInsight').innerText = data['Key Insight'] || 'N/A'; document.getElementById('proposedSolution').innerText = data['Proposed Solution'] || 'N/A'; document.getElementById('outreachMsg').innerText = data['Cold Outreach Message'] || 'N/A'; } catch (error) { alert('Re-analysis failed: ' + error.message); } finally { document.getElementById('modalLoading').classList.add('hidden'); document.getElementById('modalContent').style.opacity = '1'; } } /* ========================= SAVE STRATEGY LOGIC ========================= */ document.getElementById('saveAnalysisBtn').onclick = async () => { if (!state.activeLead) return; const lead = state.activeLead; const saveLocal = document.getElementById('modeLocal').checked; const saveCloud = document.getElementById('modeCloud').checked; if (!saveLocal && !saveCloud) { alert("Select at least one save mode."); return; } try { lead.status = "Mapped"; lead.updated_at = new Date().toISOString(); if (saveLocal) { const existingIndex = state.savedLeads.findIndex(l => l.id === lead.id); if (existingIndex >= 0) { state.savedLeads[existingIndex] = { ...lead }; } else { state.savedLeads.push({ ...lead }); } // FIX 1+3: Persist immediately after local save saveStateToStorage(); } if (saveCloud) { // FIX 4: upsert with onConflict to prevent duplicates + proper error surface const { data: upsertData, error } = await supabaseClient .from('leads') .upsert({ id: lead.id, name: lead.name, title: lead.title, company: lead.company, linkedin: lead.linkedin, source_file: lead.source || lead.sourceFile, score: lead.score, status: lead.status, tags: lead.tags, updated_at: lead.updated_at, pain_area: lead.gtmAnalysis?.['Pain Area'] || null, key_insight: lead.gtmAnalysis?.['Key Insight'] || null, proposed_solution: lead.gtmAnalysis?.['Proposed Solution'] || null, outreach_message: lead.gtmAnalysis?.['Cold Outreach Message'] || null }, { onConflict: 'id' }); if (error) { console.error('Supabase upsert error:', error); alert('Database Error: ' + error.message + '\n\nLead saved locally as fallback.'); // FIX 4: Prevent silent failure — ensure local copy exists even if cloud fails if (!saveLocal) { const existingIndex = state.savedLeads.findIndex(l => l.id === lead.id); if (existingIndex >= 0) { state.savedLeads[existingIndex] = { ...lead }; } else { state.savedLeads.push({ ...lead }); } saveStateToStorage(); } // Don't return — still close modal and update UI } } renderSaved(); updateDashboardCards(); closeModal(); } catch (err) { alert("Unexpected error: " + err.message); } }; /* ========================= LOAD FROM SUPABASE ========================= */ async function loadSavedFromDB() { const { data, error } = await supabaseClient .from('leads') .select('*') .order('updated_at', { ascending: false }); if (!error && data) { state.savedLeads = data; renderSaved(); updateDashboardCards(); } } /* ========================= BULK SELECT HELPERS ========================= */ let _selectedLeadIds = new Set(); function toggleSelectAll(checked) { _selectedLeadIds = new Set(); document.querySelectorAll('.repo-row-check').forEach(cb => { cb.checked = checked; if (checked) _selectedLeadIds.add(cb.dataset.id); }); updateBulkBar(); } function toggleRowSelect(id, checked) { if (checked) _selectedLeadIds.add(id); else _selectedLeadIds.delete(id); const allChecks = document.querySelectorAll('.repo-row-check'); const selectAll = document.getElementById('select-all-check'); if (selectAll) selectAll.checked = allChecks.length > 0 && [...allChecks].every(c => c.checked); updateBulkBar(); } function updateBulkBar() { const bar = document.getElementById('bulk-action-bar'); const label = document.getElementById('bulk-count-label'); if (_selectedLeadIds.size > 0) { bar?.classList.remove('hidden'); if (label) label.innerText = `${_selectedLeadIds.size} lead${_selectedLeadIds.size > 1 ? 's' : ''} selected`; } else { bar?.classList.add('hidden'); } } function clearBulkSelection() { _selectedLeadIds = new Set(); document.querySelectorAll('.repo-row-check').forEach(cb => cb.checked = false); const selectAll = document.getElementById('select-all-check'); if (selectAll) selectAll.checked = false; updateBulkBar(); } function deleteSelected() { if (!_selectedLeadIds.size) return; if (!confirm(`Delete ${_selectedLeadIds.size} selected lead(s)? This cannot be undone.`)) return; state.savedLeads = state.savedLeads.filter(l => !_selectedLeadIds.has(l.id)); _selectedLeadIds = new Set(); saveStateToStorage(); renderSaved(); updateDashboardCards(); } function exportSelected() { if (!_selectedLeadIds.size) return; const selected = state.savedLeads.filter(l => _selectedLeadIds.has(l.id)); const ws = XLSX.utils.json_to_sheet(selected); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, 'Selected_Leads'); XLSX.writeFile(wb, 'Selected_Leads.xlsx'); } /* ========================= LEAD NOTES & RESCORE ========================= */ function setLeadNote(leadId, note) { const lead = state.savedLeads.find(l => l.id === leadId); if (!lead) return; lead.notes = note; saveStateToStorage(); } function setLeadScore(leadId, newScore) { const score = Math.max(0, Math.min(100, parseInt(newScore) || 0)); const lead = state.savedLeads.find(l => l.id === leadId); if (!lead) return; lead.score = score; lead.scoreOverride = true; saveStateToStorage(); // Update score display in main row const cell = document.getElementById('score-cell-' + leadId); if (cell) { cell.className = getScoreClass(score) + ' font-black'; cell.innerHTML = score + '%'; } } /* ========================= ACTIVITY LOG ========================= */ function logLeadActivity(leadId, action, label) { const lead = state.savedLeads.find(l => l.id === leadId); if (!lead) return; if (!lead.activityLog) lead.activityLog = []; lead.activityLog.unshift({ action, label, timestamp: new Date().toISOString() }); // Keep last 20 entries if (lead.activityLog.length > 20) lead.activityLog = lead.activityLog.slice(0, 20); saveStateToStorage(); // Re-render just the timeline div const tlEl = document.getElementById('timeline-' + leadId); if (tlEl) tlEl.innerHTML = buildTimelineHTML(lead.activityLog); } function buildTimelineHTML(log) { if (!log || !log.length) return 'No activity logged yet'; return log.map(entry => { const d = new Date(entry.timestamp); const timeStr = d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'}); return `
${entry.label} ${timeStr}
`; }).join(''); } /* ========================= RENDER SAVED TABLES ========================= */ async function renderSaved() { const body = document.getElementById('body-saved'); const searchTerm = (document.getElementById('repoSearch')?.value || '').toLowerCase().trim(); const leads = searchTerm ? state.savedLeads.filter(l => (l.name || '').toLowerCase().includes(searchTerm) || (l.company || '').toLowerCase().includes(searchTerm) || (l.source_file || l.sourceFile || l.source || '').toLowerCase().includes(searchTerm) || (l.status || '').toLowerCase().includes(searchTerm) || (Array.isArray(l.tags) ? l.tags.join(' ') : (l.tags || '')).toLowerCase().includes(searchTerm) ) : state.savedLeads; if (!leads.length) { body.innerHTML = `${searchTerm ? 'No results for "' + searchTerm + '"' : 'No saved leads'}`; clearBulkSelection(); return; } const ACTION_OPTIONS = [ { value: '', label: '— Select Action —' }, { value: 'email_sent', label: '📧 Email Sent' }, { value: 'linkedin_sent', label: '💼 LinkedIn Message Sent' }, { value: 'call_made', label: '📞 Call Made' }, { value: 'meeting_scheduled', label: '📅 Meeting Scheduled' }, { value: 'demo_given', label: '🖥️ Demo Given' }, { value: 'proposal_sent', label: '📄 Proposal Sent' }, { value: 'follow_up', label: '🔄 Follow-Up Sent' }, { value: 'no_response', label: '🔇 No Response' }, { value: 'not_interested', label: '❌ Not Interested' }, { value: 'closed_won', label: '✅ Closed Won' }, ]; const rows = leads.map(l => { const date = new Date(l.updated_at || l.created_at); const formatted = date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'}); const source = l.source_file || l.sourceFile || l.source || 'Unknown'; const scoreClass = getScoreClass(l.score); const actionVal = l.actionTaken || ''; const actionCls = actionVal ? 'action-select actioned' : 'action-select'; const gtm = l.gtmAnalysis || {}; const isChecked = _selectedLeadIds.has(l.id) ? 'checked' : ''; const actionOptionsHtml = ACTION_OPTIONS.map(o => `` ).join(''); const expandHtml = `
🔴 Pain Area
${gtm['Pain Area'] || 'Not analyzed yet'}
🟡 Key Insight
${gtm['Key Insight'] || 'Not analyzed yet'}
🟣 Proposed Solution
${gtm['Proposed Solution'] || 'Not analyzed yet'}
🔵 Cold Outreach Draft
${gtm['Cold Outreach Message'] || 'Not analyzed yet'}
📝 Notes
🎯 Override Score
/ 100
${l.scoreOverride ? '
✎ Manually set
' : ''}
⏱ Activity Timeline
${buildTimelineHTML(l.activityLog)}
`; const mainRow = ` ${l.name} ${l.company} ${source} ${l.score}%${l.scoreOverride ? '' : ''} ${l.status} ${formatted} ${expandHtml}`; return mainRow; }).join(''); body.innerHTML = rows; updateBulkBar(); } function setLeadAction(leadId, action) { const lead = state.savedLeads.find(l => l.id === leadId); if (!lead) return; lead.actionTaken = action; saveStateToStorage(); const sel = document.getElementById('action-' + leadId); if (sel) sel.className = action ? 'action-select actioned' : 'action-select'; // Log to activity timeline if (action) { const ACTION_LABELS = { email_sent: '📧 Email Sent', linkedin_sent: '💼 LinkedIn Message Sent', call_made: '📞 Call Made', meeting_scheduled: '📅 Meeting Scheduled', demo_given: '🖥️ Demo Given', proposal_sent: '📄 Proposal Sent', follow_up: '🔄 Follow-Up Sent', no_response: '🔇 No Response', not_interested: '❌ Not Interested', closed_won: '✅ Closed Won' }; logLeadActivity(leadId, action, ACTION_LABELS[action] || action); } } function toggleRepoExpand(leadId) { const expandRow = document.getElementById('expand-' + leadId); if (!expandRow) return; const isHidden = expandRow.classList.contains('hidden'); expandRow.classList.toggle('hidden', !isHidden); const btn = document.getElementById('btn-expand-' + leadId); if (btn) btn.innerText = isHidden ? '▲ Hide' : '▼ Intel'; } function exportRepository(type = "xlsx") { const leads = getExportLeads(); if (!leads.length) { alert("No data to export."); return; } const flat = leads.map(l => ({ Name: l.name, Title: l.title, Company: l.company, LinkedIn: l.linkedin, Source: l.source_file || l.sourceFile || l.source, Score: l.score, Status: l.status, 'Pain Area': l.gtmAnalysis?.['Pain Area'] || '', 'Key Insight': l.gtmAnalysis?.['Key Insight'] || '', 'Proposed Solution': l.gtmAnalysis?.['Proposed Solution'] || '', 'Cold Outreach': l.gtmAnalysis?.['Cold Outreach Message'] || '', 'Action Taken': l.actionTaken || '', Notes: l.notes || '', Updated: l.updated_at })); const ws = XLSX.utils.json_to_sheet(flat); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, "Strategic_Repository"); if (type === "csv") { XLSX.writeFile(wb, "Strategic_Repository.csv", { bookType: "csv" }); } else { XLSX.writeFile(wb, "Strategic_Repository.xlsx"); } } function getExportLeads() { const searchTerm = (document.getElementById('repoSearch')?.value || '').toLowerCase().trim(); return searchTerm ? state.savedLeads.filter(l => (l.name || '').toLowerCase().includes(searchTerm) || (l.company || '').toLowerCase().includes(searchTerm) || (l.source_file || l.sourceFile || l.source || '').toLowerCase().includes(searchTerm) ) : state.savedLeads; } function exportPDF() { const leads = getExportLeads(); const { jsPDF } = window.jspdf; const doc = new jsPDF(); let y = 14; doc.setFontSize(14); doc.setFont(undefined,'bold'); doc.text('Strategic Repository', 10, y); y += 10; doc.setFontSize(9); doc.setFont(undefined,'normal'); leads.forEach((l, i) => { if (y > 270) { doc.addPage(); y = 14; } doc.text(`${i+1}. ${l.name} | ${l.company} | ${l.score}% | ${l.status} | ${l.actionTaken || 'No action'}`, 10, y); y += 7; if (l.gtmAnalysis?.['Pain Area']) { const lines = doc.splitTextToSize(' Pain: ' + l.gtmAnalysis['Pain Area'], 185); doc.text(lines, 10, y); y += lines.length * 5 + 3; } }); doc.save("Strategic_Repository.pdf"); } function exportWord() { const leads = getExportLeads(); let content = "

Strategic Repository

"; leads.forEach(l => { content += `

${l.name} — ${l.company}

`; content += `

Score: ${l.score}% | Status: ${l.status} | Source: ${l.source_file || l.source || ''}

`; if (l.gtmAnalysis?.['Pain Area']) content += `

Pain Area: ${l.gtmAnalysis['Pain Area']}

`; if (l.gtmAnalysis?.['Key Insight']) content += `

Key Insight: ${l.gtmAnalysis['Key Insight']}

`; if (l.gtmAnalysis?.['Proposed Solution']) content += `

Solution: ${l.gtmAnalysis['Proposed Solution']}

`; if (l.notes) content += `

Notes: ${l.notes}

`; content += '
'; }); const blob = new Blob(['\ufeff', content], { type: 'application/msword' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = "Strategic_Repository.doc"; link.click(); } /* ========================= VAULT MERGE ========================= */ function openMergeModal() { if (state.fileRegistry.length < 2) { alert('You need at least 2 files in the vault to merge.'); return; } const opts = state.fileRegistry.map((f,i) => ``).join(''); document.getElementById('merge-file-a').innerHTML = opts; document.getElementById('merge-file-b').innerHTML = opts; document.getElementById('merge-file-b').selectedIndex = 1; document.getElementById('merge-name-input').value = 'Merged_' + new Date().toISOString().slice(0,10) + '.xlsx'; document.getElementById('merge-error').classList.add('hidden'); document.getElementById('mergeModal').classList.remove('hidden'); } function closeMergeModal() { document.getElementById('mergeModal').classList.add('hidden'); } function confirmMerge() { const idxA = parseInt(document.getElementById('merge-file-a').value); const idxB = parseInt(document.getElementById('merge-file-b').value); const name = document.getElementById('merge-name-input').value.trim(); const errEl = document.getElementById('merge-error'); if (idxA === idxB) { errEl.textContent = 'Please select two different files.'; errEl.classList.remove('hidden'); return; } if (!name) { errEl.textContent = 'Please enter a name for the merged file.'; errEl.classList.remove('hidden'); return; } if (state.fileRegistry.some(f => f.name === name)) { errEl.textContent = 'A file with that name already exists.'; errEl.classList.remove('hidden'); return; } const fileA = state.fileRegistry[idxA]; const fileB = state.fileRegistry[idxB]; const seen = new Set(); const merged = [...fileA.data, ...fileB.data] .map(l => ({ ...l, id: crypto.randomUUID(), sourceFile: name, source: name })) .filter(l => { const key = (l.name + '|' + l.company).toLowerCase(); if (seen.has(key)) return false; seen.add(key); return true; }); state.fileRegistry.push({ name, data: merged }); saveStateToStorage(); closeMergeModal(); renderVault(); updateDashboardCards(); alert(`✅ Merged "${fileA.name}" + "${fileB.name}" → "${name}"\n${merged.length} unique leads`); } /* ========================= DUPLICATE DETECTION ========================= */ let _pendingImportData = null; function checkImportDuplicates(processed, fileName) { const allExisting = state.fileRegistry.flatMap(f => f.data); const dupes = processed.filter(newLead => allExisting.some(ex => ex.name.toLowerCase() === newLead.name.toLowerCase() && ex.company.toLowerCase() === newLead.company.toLowerCase() ) ); if (!dupes.length) return false; const preview = dupes.slice(0,3).map(d => d.name + ' @ ' + d.company).join(', '); document.getElementById('dup-import-msg').innerText = `Found ${dupes.length} lead(s) in "${fileName}" that already exist in your vault (same Name + Company).\n\nExamples: ${preview}${dupes.length > 3 ? '...' : ''}\n\nYou can import anyway (duplicates will be added as separate entries) or cancel.`; document.getElementById('dupImportModal').classList.remove('hidden'); return true; } function cancelDupImport() { _pendingImportData = null; document.getElementById('dupImportModal').classList.add('hidden'); } function proceedDupImport() { if (!_pendingImportData) return; document.getElementById('dupImportModal').classList.add('hidden'); finalizeImport(_pendingImportData); _pendingImportData = null; } function finalizeImport({ fileName, processed }) { state.fileRegistry.push({ name: fileName, data: processed }); state.currentSourceFile = fileName; state.allLeads = [...processed]; saveStateToStorage(); renderVault(); renderP1(); updateDashboardCards(); document.getElementById('analyzeAllBtn')?.classList.remove('hidden'); } /* ========================= BULK ANALYZE ========================= */ /* ========================= BULK ANALYZE — Concurrent Batch Engine Strategy: Process BATCH_SIZE leads simultaneously using Promise.allSettled This is ~5x faster than sequential. For 150 leads @ batch=5: ~30 calls instead of 150 sequential round trips. For 10K leads, use batches of 10 with a small inter-batch delay to respect API rate limits. ========================= */ let bulkStopped = false; function stopBulkAnalyze() { bulkStopped = true; } async function analyzeAllLeads() { if (!state.allLeads.length) return; // Only analyze leads that haven't been analyzed yet const unanalyzed = state.allLeads.filter(l => !l.gtmAnalysis); if (!unanalyzed.length) { alert('All leads in this file have already been analyzed!'); return; } bulkStopped = false; const btn = document.getElementById('analyzeAllBtn'); const loader = document.getElementById('bulkLoader'); const statusText = document.getElementById('bulkStatusText'); const progressBar= document.getElementById('bulkProgressBar'); btn?.classList.add('hidden'); loader?.classList.remove('hidden'); const BATCH_SIZE = 5; // concurrent API calls per batch — safe for most providers const BATCH_DELAY_MS = 300; // small pause between batches to respect rate limits let completed = 0; const total = unanalyzed.length; // Process in batches of BATCH_SIZE for (let i = 0; i < unanalyzed.length; i += BATCH_SIZE) { if (bulkStopped) break; const batch = unanalyzed.slice(i, i + BATCH_SIZE); // Run all in this batch simultaneously const results = await Promise.allSettled( batch.map(lead => getAIIntel(lead).then(data => ({ lead, data }))) ); // Apply results and update rows in-place (no full re-render) results.forEach(result => { if (result.status === 'fulfilled') { const { lead, data } = result.value; lead.gtmAnalysis = data; lead.status = 'Analyzed'; // Update just this row's score cell to show ✓ AI badge const row = document.getElementById('p2-row-' + lead.id); if (row) { const scoreCell = row.querySelectorAll('td')[2]; if (scoreCell) { scoreCell.innerHTML = `${lead.score}% ✓ AI`; } } completed++; } else { console.warn('Batch item failed:', result.reason?.message); completed++; } }); // Update progress const pct = Math.round((completed / total) * 100); if (progressBar) progressBar.style.width = pct + '%'; if (statusText) statusText.innerText = bulkStopped ? `Stopped at ${completed} / ${total}` : `Analyzing ${completed} / ${total} leads...`; // Small inter-batch delay to avoid hammering the API if (!bulkStopped && i + BATCH_SIZE < unanalyzed.length) { await new Promise(r => setTimeout(r, BATCH_DELAY_MS)); } } loader?.classList.add('hidden'); btn?.classList.remove('hidden'); if (progressBar) progressBar.style.width = '0%'; const doneCount = state.allLeads.filter(l => l.gtmAnalysis).length; if (statusText) statusText.innerText = 'Synchronizing Neural Intel...'; updateDashboardCards(); if (bulkStopped) { alert(`Stopped. ${doneCount} of ${state.allLeads.length} leads analyzed so far.`); } } /* ========================= REPOSITORY DELETE ========================= */ function deleteRepoLead(leadId) { state.savedLeads = state.savedLeads.filter(l => l.id !== leadId); saveStateToStorage(); renderSaved(); updateDashboardCards(); } function clearAllRepository() { if (!state.savedLeads.length) { alert('Repository is already empty.'); return; } if (!confirm(`Delete all ${state.savedLeads.length} saved leads from the repository? This cannot be undone.`)) return; state.savedLeads = []; saveStateToStorage(); renderSaved(); updateDashboardCards(); } /* ========================= VAULT DELETE ========================= */ let _vaultDeleteIndex = null; function openVaultDeleteModal(index) { const file = state.fileRegistry[index]; if (!file) return; _vaultDeleteIndex = index; document.getElementById('vaultDeleteName').innerText = file.name; document.getElementById('vaultDeleteModal').classList.remove('hidden'); } function closeVaultDeleteModal() { document.getElementById('vaultDeleteModal').classList.add('hidden'); _vaultDeleteIndex = null; } function confirmVaultDelete() { if (_vaultDeleteIndex === null) return; const file = state.fileRegistry[_vaultDeleteIndex]; // Remove from registry state.fileRegistry.splice(_vaultDeleteIndex, 1); // If it was the active file, clear the command center if (state.currentSourceFile === file.name) { state.currentSourceFile = null; state.allLeads = []; document.getElementById('body-p1').innerHTML = `Import file to begin`; document.getElementById('body-p2').innerHTML = `Execute mapping to verify`; document.getElementById('analyzeAllBtn')?.classList.add('hidden'); } saveStateToStorage(); closeVaultDeleteModal(); renderVault(); updateDashboardCards(); } /* ========================= CLOSE MODAL ========================= */ function closeModal() { document.getElementById('leadModal').classList.add('hidden'); state.activeLead = null; }