Welcome back

Sign in to your account

or
Continue with Google
Don't have an account? Sign up
Dashboard
Loading...
`; } function getBotProviderModels() { try { return JSON.parse(document.getElementById('bot-provider-models-json')?.textContent || '[]'); } catch(e) { return []; } } function syncBotModelDropdown() { const providers = getBotProviderModels(); const providerId = document.getElementById('bot-provider')?.value || ''; const modelEl = document.getElementById('bot-model'); if (!modelEl) return; const provider = providers.find(p => p.id === providerId) || providers[0] || {}; const models = Array.isArray(provider.models) && provider.models.length ? provider.models : [provider.default || '']; const current = modelEl.value || modelEl.dataset.selected || provider.default || models[0] || ''; const selected = models.includes(current) ? current : (provider.default || models[0] || ''); modelEl.innerHTML = models.map(m => ``).join(''); modelEl.dataset.selected = selected; const display = document.getElementById('gbs-model'); if (display) display.value = selected || provider.label || 'Platform Default'; } async function renderChatbot(el) { if (!CURRENT_SUB?.module_chatbot) { el.innerHTML = inactiveModule('Digital Assistant','chatbot'); return; } const [r, sessR] = await Promise.all([ apiGET('chatbot.php?action=config'), apiGET('sessions.php?action=list'), ]); const cfg = r.data.config || {}; const providers = r.data.available_providers || []; const canChoose = r.data.can_choose_provider; const connectedSessions = (sessR.data.sessions || []).filter(s => ['active','connected','open'].includes(s.status)); el.innerHTML = `
Dashboard Knowledge Base Data Sources FAQ & Triggers Conversations Widget Notifications Settings
0
Messages Today
0
Total Messages
0
Conversations
0
Knowledge Items
Quick Setup

Prepare the assistant profile, starter knowledge, and FAQs from your business details.

Loading...

`; await loadDADashboard(); } async function saveChatbotCfg() { await saveGBSettings(); } // ── Chatbot notification settings ─────────────────────────── function onNotifyToggle() { const enabled = document.getElementById('bot-notify-enabled')?.checked; const fields = document.getElementById('bot-notify-fields'); if (fields) fields.style.display = enabled ? '' : 'none'; } async function loadNotifyGroups() { const sessId = document.getElementById('bot-notify-sess')?.value; const sel = document.getElementById('bot-notify-group'); if (!sel) return; // Keep current selection if no session chosen if (!sessId) { const cur = sel.querySelector('option[selected]'); if (!cur) sel.innerHTML = ''; return; } sel.innerHTML = ''; const r = await apiGET(`groups.php?action=saved&session_id=${sessId}`); const groups = r.data.groups || []; if (!groups.length) { sel.innerHTML = ''; return; } // Preserve currently saved group JID if present const curJid = sel.dataset.savedJid || ''; sel.innerHTML = '' + groups.map(g => ``).join(''); } async function loadChatbotNotifications() { loadDestinations('chatbot'); const [cfgR, sessR] = await Promise.all([ apiGET('chatbot.php?action=config'), apiGET('sessions.php?action=list'), ]); const c = cfgR.data?.config || {}; const sessions = (sessR.data.sessions || []).filter(s => ['active','connected','open'].includes(s.status)); // Populate enable toggle const ne = document.getElementById('bot-notify-enabled'); if (ne) { ne.checked = !!Number(c.notify_group_enabled || 0); onNotifyToggle(); } // Populate fields const nt = document.getElementById('cn-type'); const to = document.getElementById('cn-timeout'); const ci = document.getElementById('cn-idle'); if (nt) nt.value = c.notify_type || 'summary'; if (to) to.value = c.conversation_timeout_min || 30; if (ci) ci.value = c.idle_prompt_min || 20; // Populate session selector const ns = document.getElementById('bot-notify-sess'); if (ns) { ns.innerHTML = '' + sessions.map(s => ``).join(''); } } async function saveChatbotNotifySettings() { const enabled = document.getElementById('bot-notify-enabled')?.checked ? 1 : 0; const type = document.getElementById('cn-type')?.value || 'summary'; const timeout = parseInt(document.getElementById('cn-timeout')?.value || '30'); const idle = parseInt(document.getElementById('cn-idle')?.value || '20'); const sessId = document.getElementById('bot-notify-sess')?.value || ''; const msgEl = document.getElementById('cn-msg'); const r = await apiPOST('chatbot.php?action=save', { notify_group_enabled: enabled, notify_type: type, notify_session_id: sessId ? parseInt(sessId) : null, conversation_timeout_min: isNaN(timeout) ? 30 : Math.max(1, timeout), idle_prompt_min: isNaN(idle) ? 20 : Math.max(1, idle), }); if (r.ok) showPageMsg('Notification settings saved.', 'success'); else { if (msgEl) msgEl.innerHTML = `
${r.data?.error||'Save failed.'}
`; } } async function saveNotifyConfig() { const enabled = document.getElementById('bot-notify-enabled')?.checked ? 1 : 0; const sessId = document.getElementById('bot-notify-sess')?.value || ''; const groupSel = document.getElementById('bot-notify-group'); const groupJid = groupSel?.value || ''; const groupName= groupSel?.options[groupSel?.selectedIndex]?.text || ''; const timeout = parseInt(document.getElementById('bot-notify-timeout')?.value || '30'); if (enabled && !groupJid) { showPageMsg('Please select a notification group.', 'error'); return; } const r = await apiPOST('chatbot.php?action=save', { notify_group_enabled: enabled, notify_session_id: sessId ? parseInt(sessId) : null, notify_group_jid: groupJid, notify_group_name: groupName === '— Select group —' ? '' : groupName, conversation_timeout_min: isNaN(timeout) ? 30 : Math.max(5, timeout), }); if (r.ok) showPageMsg('Notification settings saved.', 'success'); else showPageMsg(r.data?.error || 'Save failed.', 'error'); } async function diagnoseSummary() { const r = await apiGET('chatbot.php?action=diagnose_summary'); const d = r.data || {}; let html = '
'; if (d.issues && d.issues.length) { html += d.issues.map(i => `
❌ ${escapeHtml(i)}
`).join(''); } else { html += '
✅ Configuration looks good!
'; } html += '
'; html += `
Notifications: ${d.config?.notify_group_enabled ? '✅ Enabled' : '❌ Disabled'}
Notify type: ${d.config?.notify_type || '—'}
Idle prompt after: ${d.config?.idle_prompt_min ?? '—'} min  |  Timeout after: ${d.config?.conversation_timeout_min ?? '—'} min
`; html += `
Destinations (${d.destinations?.length || 0} total, ${d.active_destinations || 0} active):
`; if (d.destinations?.length) { d.destinations.forEach(dest => { const active = ['active','connected','open'].includes(dest.session_status); html += `
${active ? '✅' : '❌'} ${escapeHtml(dest.group_name || dest.group_jid)} (${escapeHtml(dest.session_name)}, ${dest.session_status})
`; }); } else { html += 'None configured'; } html += '
'; html += `
Recent conversations: ${d.recent_conversations?.length || 0}  |  Total messages: ${d.total_messages || 0}`; if (d.recent_conversations?.length) { html += '
'; d.recent_conversations.forEach(c => { const sent = c.summary_sent_at ? '✅ sent' : '⏳ pending'; html += `
${escapeHtml(c.contact_name||c.contact_jid)} — last msg: ${c.last_msg_at||'—'} — summary: ${sent}
`; }); html += '
'; } html += '
'; openModal('🔍 Summary Diagnostic', html); } async function simulateTimeout() { if (!confirm('This will force the most recent conversation to look "timed out" and attempt to send a WhatsApp summary now. Continue?')) return; showPageMsg('Simulating...', 'success'); const r = await apiPOST('chatbot.php?action=simulate_timeout', {}); const d = r.data || {}; if (d.ok) { openModal('⚡ Simulation Result', `
✅ ${escapeHtml(d.message)}
Conversation: ${escapeHtml(d.contact_name || d.contact_jid)}
`); } else { openModal('⚡ Simulation Result', `
❌ ${escapeHtml(d.message || d.error || 'Simulation failed.')}

Run Diagnose to see what needs to be fixed.

`); } } async function runChatCron() { showPageMsg('Running check...', 'success'); const r = await apiPOST('chatbot.php?action=run_chat_cron', {}); const d = r.data || {}; if (r.ok) showPageMsg(d.message || 'Done.', 'success'); else showPageMsg(d.error || 'Failed.', 'error'); } async function sendDATestNotify() { const msg = document.getElementById('da-test-msg')?.value.trim() || ''; const resultEl = document.getElementById('da-test-result'); if (resultEl) { resultEl.style.color = 'var(--muted)'; resultEl.textContent = 'Sending…'; } const r = await apiPOST('chatbot.php?action=test_notify', { message: msg }); const d = r.data || {}; if (resultEl) { resultEl.style.color = d.ok ? 'var(--primary)' : 'var(--danger)'; resultEl.textContent = d.ok ? `✅ ${d.message}` : `❌ ${d.error || 'Failed'}`; } if (d.ok) showToast(d.message || 'Test notification sent!', 'success'); } async function loadDADashboard() { const r = await apiGET('chatbot.php?action=dashboard'); const s = r.data.stats || r.data || {}; const set = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val ?? 0; }; set('da-msg-today', s.messages_today || 0); set('da-msg-total', s.messages_total || s.messages_used || 0); set('da-convs', s.conversations || 0); set('da-kb-count', s.knowledge_items || s.knowledge_count || 0); const conv = await apiGET('chatbot.php?action=conversations&limit=5'); const rows = conv.data.conversations || []; const host = document.getElementById('da-dash-recent'); if (!host) return; host.innerHTML = rows.length ? `
${rows.map(c=>``).join('')}
ContactLast MessageUpdated
${escapeHtml(c.contact_name||c.contact_jid||'Contact')}${escapeHtml(c.last_message||'')}${c.last_msg_at?new Date(c.last_msg_at).toLocaleString():''}
` : '

No conversations yet.

'; } function openOnboarding() { openModal('Digital Assistant Onboarding', `
`); } async function runOnboarding() { const body = { company_name: document.getElementById('ob-company').value.trim(), business_type: document.getElementById('ob-biz').value.trim(), services: document.getElementById('ob-services').value.trim(), audience: document.getElementById('ob-audience').value.trim(), website: document.getElementById('ob-website').value.trim(), contact_phone: document.getElementById('ob-phone').value.trim(), contact_email: document.getElementById('ob-email').value.trim(), ai_mode: document.getElementById('ob-mode').value, tone: document.getElementById('ob-tone').value.trim(), about: document.getElementById('ob-about').value.trim(), reset_kb: document.getElementById('ob-reset').checked ? 1 : 0 }; const msg = document.getElementById('ob-msg'); if (!body.company_name || !body.business_type || !body.services) { msg.innerHTML = '

Company name, business type, and services are required.

'; return; } const r = await apiPOST('chatbot.php?action=run_onboarding', body); if (r.ok) { closeModal(); showPageMsg('Onboarding completed. Your Digital Assistant is ready to test.','success'); await renderChatbot(document.getElementById('page-content')); } else msg.innerHTML = `

${r.data.error||'Onboarding failed'}

`; } // ── Chatbot KB ───────────────────────────────────────────── // ── Data Sources ──────────────────────────────────────────── const _srcStore = {}; async function loadSources() { const el = document.getElementById('sources-list'); if (!el) return; const r = await apiGET('chatbot.php?action=sources_list'); const sources = r.data.sources || []; sources.forEach(s => { _srcStore[s.id] = s; }); if (!sources.length) { el.innerHTML = `

No data sources yet. Add your first one to keep the assistant up to date automatically.

Supported source types:
Generic REST API — any site with a REST API (property sites, custom CMS, etc.)
WordPress — WordPress sites with the Traffi.Click plugin installed
`; return; } el.innerHTML = `
${sources.map(s => ``).join('')}
NameTypeStatusLast SyncChunksActions
${escapeHtml(s.name)}${s.config?'⚡ Live':''}
${escapeHtml(s.api_url||'')}
${s.source_type==='wordpress'?'WordPress':'REST API'} ${s.status}${s.error_count>0?`
${s.error_count} error(s)`:''}
${s.last_synced_at?s.last_synced_at.replace('T',' ').substring(0,16):'Never'} ${s.chunk_count||0}
`; } function editSource(id) { addSource(Object.assign({}, _srcStore[id] || {}, {id})); } function addSource(existing) { const s = existing || {}; openModal((s.id ? '✏️ Edit' : '+ Add') + ' Data Source', `
The root URL of the site's REST API
${['properties','blog','agents','stats'].map(ep => { const eps = s.sync_endpoints || ['properties','blog']; const checked = Array.isArray(eps) ? eps.includes(ep) : JSON.parse(eps||'[]').includes(ep); return ``; }).join('')}
Select what data to pull from the API
Defines item_singular, endpoints, field aliases, and url_pattern for live follow-up queries. See presets for the exact shape.
`); sourceTypeChanged(); } const _srcConfigPresets = { realestate: JSON.stringify({ item_singular: "property", item_plural: "properties", endpoints: { search: "?action=properties.search", get: "?action=properties.get" }, params: { search_query: "location", id_param: "id" }, fields: { id: ["id"], title: ["title","name"], url: ["url","permalink"], price: ["price_formatted","price"], image_main: ["image","image_url","thumb_url","photo","cover_image"], images_array: ["images"], description: ["description"] }, url_pattern: "/property\\.php\\?id=(\\d+)" }, null, 2), ecommerce: JSON.stringify({ item_singular: "product", item_plural: "products", endpoints: { search: "/wp-json/wc/v3/products", get: "/wp-json/wc/v3/products/" }, params: { search_query: "search", id_param: "id" }, fields: { id: ["id"], title: ["name","title"], url: ["permalink"], price: ["price_html","price"], image_main: ["images.0.src","image_url"], images_array: ["images"], description: ["short_description","description"] }, url_pattern: "/product/([a-z0-9-]+)" }, null, 2), healthcare: JSON.stringify({ item_singular: "doctor", item_plural: "doctors", endpoints: { search: "?action=doctors.search", get: "?action=doctors.get" }, params: { search_query: "specialty", id_param: "id" }, fields: { id: ["id"], title: ["name","display_name"], url: ["profile_url","url"], price: ["consultation_fee"], image_main: ["avatar","photo"], description: ["bio","description"] }, url_pattern: "/doctor/(\\d+)" }, null, 2), clear: '' }; function setSrcConfigPreset(key) { const el = document.getElementById('src-config'); if (el) el.value = _srcConfigPresets[key] || ''; } function sourceTypeChanged() { const t = document.getElementById('src-type')?.value; const api = document.getElementById('src-api-fields'); const wp = document.getElementById('src-wp-fields'); const cfg = document.getElementById('src-config-wrap'); if (api) api.style.display = t === 'api' ? '' : 'none'; if (wp) wp.style.display = t === 'wordpress' ? '' : 'none'; if (cfg) cfg.style.display = t === 'api' ? '' : 'none'; } async function doSaveSource(id) { const msgEl = document.getElementById('src-msg'); const type = document.getElementById('src-type').value; const name = document.getElementById('src-name').value.trim(); const interval = parseInt(document.getElementById('src-interval').value); const eps = Array.from(document.querySelectorAll('.src-ep:checked')).map(el => el.value); let url = '', key = ''; if (type === 'wordpress') { url = (document.getElementById('src-wp-url')?.value||'').trim(); key = (document.getElementById('src-wp-token')?.value||'').trim(); } else { url = (document.getElementById('src-url')?.value||'').trim(); key = (document.getElementById('src-key')?.value||'').trim(); } if (!name) { msgEl.innerHTML = '
Name is required.
'; return; } if (!url) { msgEl.innerHTML = '
URL is required.
'; return; } if (!id && !key) { msgEl.innerHTML = '
API Key / Token is required.
'; return; } const configVal = (document.getElementById('src-config')?.value || '').trim(); const payload = { name, source_type: type, api_url: url, sync_endpoints: eps, sync_interval_hours: interval, config: configVal }; if (key) payload.api_key = key; if (id) payload.id = id; msgEl.innerHTML = '
Saving…
'; const r = await apiPOST(`chatbot.php?action=${id?'update_source':'add_source'}`, payload); if (r.ok) { closeModal(); loadSources(); showPageMsg(id ? 'Source updated.' : 'Source added — initial sync started!', 'success'); } else { msgEl.innerHTML = `
${r.data.error||'Failed to save source.'}
`; } } async function testSourceApi(id, name) { showPageMsg(`Testing API connection for "${name}"…`, 'success'); const r = await apiGET(`chatbot.php?action=test_source&id=${id}`); const status = r.data?.status || '?'; const ok = r.data?.ok; const resp = r.data?.response; const items = Array.isArray(resp?.data) ? resp.data.length : (Array.isArray(resp) ? resp.length : null); const curl = r.data?.curl_error; const url = r.data?.test_url || ''; let msg = ok ? `✅ HTTP ${status} — ${items !== null ? items + ' items returned' : 'connected'}. Ready to sync.` : `❌ HTTP ${status}${curl ? ' ('+curl+')' : ''}. ${resp?.error || resp?.message || 'Check URL and API key.'}`; openModal(`🔌 API Test — ${name}`, `
URL tested:
${escapeHtml(url)}
${escapeHtml(msg)}
Raw response preview:
${escapeHtml(JSON.stringify(resp, null, 2) || r.data?.raw_preview || '(empty)')}
`); } async function syncSourceNow(id, name) { showPageMsg(`Syncing "${name}"…`, 'success'); const r = await apiPOST('chatbot.php?action=sync_source', { id }); if (r.ok) { loadSources(); const chunks = r.data.chunks || 0; const warn = r.data.error || ''; if (chunks > 0) { showPageMsg(`"${name}" synced — ${chunks} chunks saved.${warn ? ' ⚠ '+warn : ''}`, warn ? 'info' : 'success'); } else { showPageMsg(`"${name}": ${warn || 'Sync returned 0 chunks. Check API URL and key.'}`, 'error'); } } else { showPageMsg(r.data.error || 'Sync failed.', 'error'); } } async function toggleSourcePause(id, currentStatus) { const r = await apiPOST('chatbot.php?action=toggle_source', { id }); if (r.ok) loadSources(); else alert(r.data.error || 'Failed'); } async function deleteSource(id, name) { if (!confirm(`Delete source "${name}"? This will also remove all its synced chunks.`)) return; const r = await apiPOST('chatbot.php?action=delete_source', { id }); if (r.ok) { loadSources(); showPageMsg('Source deleted.', 'success'); } else alert(r.data.error || 'Delete failed'); } // ── KB ─────────────────────────────────────────────────────── async function loadKB() { const el = document.getElementById('kb-list'); if (!el) return; const r = await apiGET('chatbot.php?action=kb_list'); const items = r.data.items || []; if (!items.length) { el.innerHTML = '

No knowledge yet. Add your website, business profile, documents, or manual Q&A.

'; return; } el.innerHTML = `
${ items.map(k=>``).join('') }
TitleTypeContent PreviewActions
${escapeHtml(k.title||'—')}${escapeHtml(k.type||k.content_type||'text')}${escapeHtml((k.content||k.source_url||'').substring(0,100))}...
`; } async function addKB(item=null) { const isEdit = !!item; openModal(isEdit ? 'Edit Knowledge' : 'Add Knowledge', `
`); toggleKBFields(); } function toggleKBFields() { const type = document.getElementById('kb-type')?.value || 'text'; const urlRow = document.getElementById('kb-url-row'); const contentRow = document.getElementById('kb-content-row'); if (urlRow) urlRow.style.display = type === 'website' ? '' : 'none'; if (contentRow) contentRow.style.display = type === 'website' ? 'none' : ''; } async function editKB(id) { const r = await apiGET('chatbot.php?action=kb_list'); const item = (r.data.items || []).find(x => Number(x.id) === Number(id)); if (!item) return alert('Knowledge item not found'); addKB(item); } async function submitKB() { const id = parseInt(document.getElementById('kb-id')?.value || '0', 10); const type = document.getElementById('kb-type')?.value || 'text'; const title = document.getElementById('kb-title')?.value.trim(); const content = document.getElementById('kb-content')?.value.trim(); const source_url = document.getElementById('kb-url')?.value.trim(); const msgEl = document.getElementById('kb-msg'); if (!title) { if(msgEl) msgEl.innerHTML='

Title is required.

'; return; } if (type === 'website' && !source_url) { if(msgEl) msgEl.innerHTML='

Website URL is required.

'; return; } if (type !== 'website' && !content) { if(msgEl) msgEl.innerHTML='

Content is required.

'; return; } const r = await apiPOST(`chatbot.php?action=${id?'update_knowledge':'add_knowledge'}`, { id, type, title, content, source_url }); if (r.ok) { closeModal(); loadKB(); showPageMsg('Knowledge saved!','success'); } else if(msgEl) msgEl.innerHTML=`

${r.data.error||'Failed'}

`; } /* const el = document.getElementById('kb-list'); if (!el) return; const r = await apiGET('chatbot.php?action=kb_list'); const items = r.data.items || []; if (!items.length) { el.innerHTML = '

No documents yet. Add one to give the Digital Assistant background knowledge.

'; return; } el.innerHTML = `
${ items.map(k=>``).join('') }
TitleContent PreviewActions
${k.title||'—'}${(k.content||'').substring(0,80)}…
`; } async function addKB() { openModal('Add Knowledge Base Document', `
`); } async function submitKB() { const title = document.getElementById('kb-title')?.value.trim(); const content = document.getElementById('kb-content')?.value.trim(); const msgEl = document.getElementById('kb-msg'); if (!content) { if(msgEl) msgEl.innerHTML='

Content is required.

'; return; } const r = await apiPOST('chatbot.php?action=kb_add', { title, content }); if (r.ok) { closeModal(); loadKB(); showPageMsg('✓ Document saved!','success'); } else if(msgEl) msgEl.innerHTML=`

${r.data.error||'Failed'}

`; } async function deleteKB(id) { */ async function deleteKB(id) { if (!confirm('Delete this knowledge base entry?')) return; const r = await apiPOST('chatbot.php?action=kb_delete', { id }); if (r.ok) { loadKB(); showPageMsg('Entry deleted.','success'); } else alert(r.data.error||'Delete failed'); } // ── FAQ & Triggers (merged tab) ────────────────────────────── function ftTab(which, el) { document.getElementById('ft-faq').style.display = which === 'faq' ? '' : 'none'; document.getElementById('ft-triggers').style.display = which === 'triggers' ? '' : 'none'; if (el) { const bar = el.closest('.tabs') || el.parentElement; bar.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); el.classList.add('active'); } } async function loadFT() { const [fr, tr] = await Promise.all([ apiGET('chatbot.php?action=faq_list'), apiGET('chatbot.php?action=triggers') ]); const faqs = fr.data?.items || fr.data?.faqs || []; const triggers = tr.data?.items || tr.data?.triggers || []; const faqTb = document.getElementById('faq-tb'); const trigTb = document.getElementById('trig-tb'); if (faqTb) faqTb.innerHTML = faqs.map(f => ` ${escapeHtml(f.question||f.trigger||'—')} ${escapeHtml(f.answer||f.response||'')} ${escapeHtml(f.keywords||'')} ${f.hit_count||0} ${f.is_active||1 ? 'Yes' : 'No'} `).join('') || 'No FAQ entries'; if (trigTb) trigTb.innerHTML = triggers.map(t => ` ${escapeHtml(t.keyword||'')} ${escapeHtml(t.match_type||'contains')} ${escapeHtml(t.response||'')} ${t.hit_count||0} ${t.is_active||1 ? 'Yes' : 'No'} `).join('') || 'No triggers'; } function openFAQAdd() { document.getElementById('faq-id').value = '0'; document.getElementById('faq-q').value = ''; document.getElementById('faq-a').value = ''; document.getElementById('faq-kw').value = ''; document.getElementById('faq-modal-title').textContent = '❓ Add FAQ'; document.getElementById('faq-modal-alert').innerHTML = ''; document.getElementById('m-faq').style.display = 'flex'; } async function editFAQEntry(id) { const r = await apiGET('chatbot.php?action=faq_list'); const f = (r.data?.items||r.data?.faqs||[]).find(x => parseInt(x.id) === parseInt(id)); if (!f) { showPageMsg('FAQ entry not found','error'); return; } document.getElementById('faq-id').value = String(f.id); document.getElementById('faq-q').value = f.question||f.trigger||''; document.getElementById('faq-a').value = f.answer||f.response||''; document.getElementById('faq-kw').value = f.keywords||''; document.getElementById('faq-modal-title').textContent = '❓ Edit FAQ'; document.getElementById('faq-modal-alert').innerHTML = ''; document.getElementById('m-faq').style.display = 'flex'; } async function saveFAQEntry() { const btn = document.getElementById('faq-save-btn'); if (btn) { btn.disabled = true; btn.textContent = 'Saving…'; } const r = await apiPOST('chatbot.php?action=faq_add', { id: parseInt(document.getElementById('faq-id').value) || 0, question: document.getElementById('faq-q').value.trim(), answer: document.getElementById('faq-a').value.trim(), keywords: document.getElementById('faq-kw').value.trim() }); if (r.data?.error||!r.ok) { document.getElementById('faq-modal-alert').innerHTML = `
${r.data?.error||'Failed'}
`; if (btn) { btn.disabled = false; btn.textContent = 'Save'; } return; } document.getElementById('m-faq').style.display = 'none'; loadFT(); if (btn) { btn.disabled = false; btn.textContent = 'Save'; } } async function deleteFAQEntry(id) { if (!confirm('Delete this FAQ?')) return; await apiPOST('chatbot.php?action=faq_delete', { id }); loadFT(); } function openTriggerAdd() { document.getElementById('trig-id').value = '0'; document.getElementById('trig-kw').value = ''; document.getElementById('trig-mt').value = 'contains'; document.getElementById('trig-resp').value = ''; document.getElementById('trig-modal-alert').innerHTML = ''; document.getElementById('m-trigger').style.display = 'flex'; } async function saveTriggerEntry() { const btn = document.getElementById('trig-save-btn'); if (btn) { btn.disabled = true; btn.textContent = 'Saving…'; } const r = await apiPOST('chatbot.php?action=save_trigger', { id: parseInt(document.getElementById('trig-id').value) || 0, keyword: document.getElementById('trig-kw').value.trim(), match_type: document.getElementById('trig-mt').value, response: document.getElementById('trig-resp').value.trim() }); if (r.data?.error||!r.ok) { document.getElementById('trig-modal-alert').innerHTML = `
${r.data?.error||'Failed'}
`; if (btn) { btn.disabled = false; btn.textContent = 'Save'; } return; } document.getElementById('m-trigger').style.display = 'none'; loadFT(); if (btn) { btn.disabled = false; btn.textContent = 'Save'; } } async function deleteTriggerEntry(id) { if (!confirm('Delete this trigger?')) return; await apiPOST('chatbot.php?action=delete_trigger', { id }); loadFT(); } // ── Widget: Send to Developer ────────────────────────────── async function sendDev() { const uuid = document.getElementById('dev-session')?.value; const to = document.getElementById('dev-to')?.value.trim(); const msg = document.getElementById('dev-msg')?.value.trim(); const alertEl = document.getElementById('dev-alert'); const showA = (m, t) => { if(alertEl) alertEl.innerHTML = `
${m}
`; }; if (!uuid) return showA('No session selected. Add/connect a session first.', 'error'); if (!to || !msg) return showA('Recipient and message are required.', 'error'); const btn = document.querySelector('#tab-widget button[onclick="sendDev()"]'); if (btn) { btn.disabled = true; btn.textContent = 'Sending…'; } const r = await apiPOST('sessions.php?action=send&uuid=' + uuid, { to, message: msg }); if (btn) { btn.disabled = false; btn.textContent = 'Send via WhatsApp'; } showA(r.data?.error || '✅ Sent successfully', r.data?.error ? 'error' : 'success'); } // ── Settings: GBSettings (tab-cfg helpers) ───────────────── async function loadGBSettings() { const r = await apiGET('chatbot.php?action=config'); const c = r.data?.config || r.data?.client || {}; const setV = (id, v) => { const el = document.getElementById(id); if(el) el.value = v||''; }; setV('bot-name', c.bot_name); setV('bot-greeting', c.greeting_message); setV('gbs-prompt', c.system_prompt); setV('gbs-mode', c.ai_mode||'support'); setV('gbs-industry', c.assistant_mode||'general'); setV('gbs-model', c.ai_model||c.plan_tier||'Platform Default'); setV('bot-fallback', c.fallback_message); const bg = document.getElementById('bot-groups'); if(bg) bg.checked = !!Number(c.respond_in_groups||0); if (c.notify_session_id && c.notify_group_enabled) loadNotifyGroups(); const bl = document.getElementById('bot-lang'); if(bl) bl.value = c.language||'en'; const pp = document.getElementById('bot-provider'); if(pp && c.ai_provider) pp.value = c.ai_provider; syncBotModelDropdown(); const pm = document.getElementById('bot-model'); if(pm && c.ai_model && [...pm.options].some(o => o.value === c.ai_model)) pm.value = c.ai_model; } async function saveGBSettings() { const data = { bot_name: document.getElementById('bot-name')?.value, greeting_message:document.getElementById('bot-greeting')?.value, system_prompt: document.getElementById('gbs-prompt')?.value, ai_mode: document.getElementById('gbs-mode')?.value, assistant_mode: document.getElementById('gbs-industry')?.value, fallback_message:document.getElementById('bot-fallback')?.value, language: document.getElementById('bot-lang')?.value, respond_in_groups: document.getElementById('bot-groups')?.checked ? 1 : 0 }; const pp = document.getElementById('bot-provider'); const pm = document.getElementById('bot-model'); if (pp) { data.ai_provider = pp.value; data.ai_model = pm?.value || ''; } const r = await apiPOST('chatbot.php?action=save', data); const alertEl = document.getElementById('gbs-alert'); if (alertEl) alertEl.innerHTML = `
${r.ok ? '✅ Assistant settings saved' : (r.data?.error||'Failed')}
`; } // ── Admin: DA Clients ────────────────────────────────────── async function loadGBAdmin() { const r = await apiGET('chatbot.php?action=admin_clients'); const tb = document.getElementById('gba-tb'); if (!tb) return; tb.innerHTML = (r.data?.clients||[]).map(c => { const lim = c.message_limit||200; const demoOn = Number(c.quota_bypass||0) === 1; const label = c.user_name||c.name||c.user_email||c.email||('User #'+c.user_id); const suspended = c.status === 'suspended'; return ` ${escapeHtml(label)} ${escapeHtml(c.plan_tier||'—')} ${c.messages_used||0}/${lim}${demoOn?` Demo`:''} ${c.status||'—'} ${escapeHtml(c.session_name||'—')} `; }).join('') || 'No Digital Assistant clients'; } async function gbToggle(id, st) { await apiPOST('chatbot.php?action=admin_toggle_client', { client_id: id, status: st }); loadGBAdmin(); } async function gbToggleDemo(id, enabled) { const r = await apiPOST('chatbot.php?action=admin_toggle_quota_bypass', { client_id: id, enabled }); if (r.data?.error) { alert(r.data.error); return; } loadGBAdmin(); } async function gbEditQuota(id, current) { const v = prompt('Set per-client AI message quota:', String(current||200)); if (v === null) return; const limit = parseInt(v, 10); if (!Number.isFinite(limit) || limit < 1) { alert('Enter a valid number greater than 0'); return; } const r = await apiPOST('chatbot.php?action=admin_set_client_quota', { client_id: id, limit }); if (r.data?.error) { alert(r.data.error); return; } loadGBAdmin(); } // ── Admin: AI Config ─────────────────────────────────────── async function loadGBAIConfig() { const [sr, mr] = await Promise.all([ apiGET('admin.php?action=settings'), apiGET('chatbot.php?action=admin_models') ]); const s = sr.data?.settings || {}; const setV = (id, v) => { const el = document.getElementById(id); if(el) el.value = v||''; }; setV('gbac-openai', s.gorroboy_openai_api_key||s.openai_api_key||''); setV('gbac-deepseek', s.gorroboy_deepseek_api_key||''); setV('gbac-gemini', s.gorroboy_gemini_api_key||''); setV('gbac-coreprompt', s.gorroboy_core_system_prompt||''); setV('gbac-sales-threshold', parseInt(s.gorroboy_sales_handoff_threshold||72,10)||72); setV('gbac-sales-prompt', s.gorroboy_sales_closer_prompt||''); setV('gbac-sales-cta', s.gorroboy_sales_close_cta||''); const se = document.getElementById('gbac-sales-enabled'); if(se) se.checked = String(s.gorroboy_sales_closer_enabled||'0')==='1'; const models = mr.data?.models || []; _fillModelRouting(models, s); const tb = document.getElementById('gbac-models-tb'); if (tb) tb.innerHTML = models.map(m => ` ${escapeHtml(m.name)} ${escapeHtml(m.provider)} ${escapeHtml(m.model_id)} $${parseFloat(m.cost_per_1k||m.cost_per_1k_prompt||0).toFixed(4)} ${m.is_active?'Yes':'No'} `).join('') || 'No models configured'; } function _fillModelRouting(models, s) { const opts = models.filter(m => String(m.is_active) === '1' || m.is_active === 1) .map(m => ({ id: String(m.model_id||''), label: `${m.name} (${m.provider})` })) .filter(m => m.id); const all = [...opts]; [s.gorroboy_starter_model, s.gorroboy_growth_model, s.gorroboy_pro_model, s.gorroboy_business_model, s.gorroboy_fallback_model] .map(v => String(v||'').trim()).filter(Boolean).forEach(id => { if (!all.find(x => x.id === id)) all.push({ id, label: `${id} (saved)` }); }); const pick = (elId, val) => { const el = document.getElementById(elId); if (!el) return; el.innerHTML = all.map(o => ``).join('') || ''; if (val) el.value = val; }; pick('gbac-tier-starter', s.gorroboy_starter_model||'gpt-4o-mini'); pick('gbac-tier-growth', s.gorroboy_growth_model||'gpt-4o-mini'); pick('gbac-tier-pro', s.gorroboy_pro_model||'gpt-4.1'); pick('gbac-tier-business', s.gorroboy_business_model||'gpt-4.1'); pick('gbac-tier-fallback', s.gorroboy_fallback_model||'gpt-4o-mini'); } async function saveGBAIConfig() { const threshold = Math.min(95, Math.max(30, parseInt(document.getElementById('gbac-sales-threshold')?.value||72,10)||72)); const r = await apiPOST('admin.php?action=save_settings', { gorroboy_openai_api_key: document.getElementById('gbac-openai')?.value, gorroboy_deepseek_api_key: document.getElementById('gbac-deepseek')?.value, gorroboy_gemini_api_key: document.getElementById('gbac-gemini')?.value, gorroboy_core_system_prompt: document.getElementById('gbac-coreprompt')?.value, gorroboy_sales_closer_enabled: document.getElementById('gbac-sales-enabled')?.checked ? '1' : '0', gorroboy_sales_handoff_threshold: String(threshold), gorroboy_sales_closer_prompt: document.getElementById('gbac-sales-prompt')?.value, gorroboy_sales_close_cta: document.getElementById('gbac-sales-cta')?.value, gorroboy_starter_model: document.getElementById('gbac-tier-starter')?.value, gorroboy_growth_model: document.getElementById('gbac-tier-growth')?.value, gorroboy_pro_model: document.getElementById('gbac-tier-pro')?.value, gorroboy_business_model: document.getElementById('gbac-tier-business')?.value, gorroboy_fallback_model: document.getElementById('gbac-tier-fallback')?.value, }); const alertEl = document.getElementById('gbac-alert'); if (alertEl) alertEl.innerHTML = `
${r.ok ? '✅ Assistant config saved' : (r.data?.error||'Failed')}
`; if (r.ok) loadGBAIConfig(); } function openAIModelAdd() { document.getElementById('aim-id').value = '0'; document.getElementById('aim-name').value = ''; document.getElementById('aim-provider').value = 'openai'; document.getElementById('aim-model').value = ''; document.getElementById('aim-cost').value = '0'; document.getElementById('aim-maxtok').value = '4096'; document.getElementById('aim-modal-alert').innerHTML = ''; document.getElementById('m-aimodel').style.display = 'flex'; } function editAIModel(id, name, provider, mid, cost, max) { document.getElementById('aim-id').value = id; document.getElementById('aim-name').value = name; document.getElementById('aim-provider').value = (provider||'openai').toLowerCase(); document.getElementById('aim-model').value = mid; document.getElementById('aim-cost').value = cost||0; document.getElementById('aim-maxtok').value = max||4096; document.getElementById('aim-modal-alert').innerHTML = ''; document.getElementById('m-aimodel').style.display = 'flex'; } function quickGemini() { document.getElementById('aim-id').value = '0'; document.getElementById('aim-name').value = 'Gemini 1.5 Flash'; document.getElementById('aim-provider').value = 'gemini'; document.getElementById('aim-model').value = 'gemini-1.5-flash'; document.getElementById('aim-cost').value = '0.0003'; document.getElementById('aim-maxtok').value = '8192'; document.getElementById('aim-modal-alert').innerHTML = ''; document.getElementById('m-aimodel').style.display = 'flex'; } async function saveAIModel() { const btn = document.getElementById('aim-save-btn'); if (btn) { btn.disabled = true; btn.textContent = 'Saving…'; } const r = await apiPOST('chatbot.php?action=admin_save_model', { id: parseInt(document.getElementById('aim-id').value)||0, name: document.getElementById('aim-name').value, provider: document.getElementById('aim-provider').value, model_id: document.getElementById('aim-model').value, cost_per_1k: parseFloat(document.getElementById('aim-cost').value)||0, max_tokens: parseInt(document.getElementById('aim-maxtok').value)||4096 }); if (btn) { btn.disabled = false; btn.textContent = 'Save'; } if (r.data?.error||!r.ok) { document.getElementById('aim-modal-alert').innerHTML = `
${r.data?.error||'Failed'}
`; return; } document.getElementById('m-aimodel').style.display = 'none'; loadGBAIConfig(); } async function delAIModel(id) { if (!confirm('Delete this model?')) return; await apiPOST('chatbot.php?action=admin_save_model', { id, is_active: 0 }); loadGBAIConfig(); } // ══════════════════════════════════════════════════════ // CONVERSATIONS — full wapi-style implementation // ══════════════════════════════════════════════════════ let _convDate = ''; // active date filter let _convContact = ''; // active contact JID let _convAiEnabled = true; // AI toggle state for active contact let _convCalYear = new Date().getFullYear(); let _convCalMonth = new Date().getMonth(); // 0-based let _convDates = []; // [{chat_date, count, message_total}] async function loadConversations() { _convContact = ''; document.getElementById('conv-thread').innerHTML = '
Select a contact to view the conversation
'; const sendBar = document.getElementById('conv-send-bar'); if (sendBar) { const inp = sendBar.querySelector('textarea'); if(inp){ inp.disabled=true; inp.placeholder='Select a conversation to reply…';} const btn=sendBar.querySelector('button'); if(btn) btn.disabled=true; } document.getElementById('conv-mode-wrap').style.display = 'none'; document.getElementById('conv-export-wrap').style.display = 'flex'; document.getElementById('conv-name').textContent = 'Conversations'; document.getElementById('conv-phone-label').textContent = ''; // Sync layout height on first open setTimeout(syncConvLayout, 50); await _refreshContactList(); } let _allContacts = []; function filterConvContacts(q) { const listEl = document.getElementById('conv-list'); if (!listEl || !_allContacts.length) return; const filtered = q ? _allContacts.filter(c => { const name = (c.sender_name || c.contact_name || c.sender_phone || '').toLowerCase(); const last = (c.last_message || '').toLowerCase(); return name.includes(q.toLowerCase()) || last.includes(q.toLowerCase()); }) : _allContacts; _renderContactList(listEl, filtered); } async function _refreshContactList() { const listEl = document.getElementById('conv-list'); const statsEl = document.getElementById('conv-stats'); listEl.innerHTML = '
Loading…
'; let url = 'chatbot.php?action=conversations'; if (_convDate) url += '&date=' + _convDate; const r = await apiGET(url); const contacts = r.data.contacts || r.data.conversations || []; _allContacts = contacts; _convDates = r.data.dates || []; _renderCalendar(); if (!contacts.length) { listEl.innerHTML = '
No conversations' + (_convDate ? ' on this date.' : ' yet.') + '
'; statsEl.textContent = ''; return; } statsEl.textContent = contacts.length + ' contact' + (contacts.length !== 1 ? 's' : '') + (_convDate ? ' · ' + _convDate : ''); _renderContactList(listEl, contacts); } function _renderContactList(listEl, contacts) { const stageColor = s => s === 'hot' ? '#ef4444' : s === 'warm' ? '#f59e0b' : '#6b7280'; const stageIcon = s => s === 'hot' ? '🔥' : s === 'warm' ? '🌡' : '❄️'; if (!contacts.length) { listEl.innerHTML = '
No matches.
'; return; } listEl.innerHTML = contacts.map(c => { const name = c.sender_name || c.contact_name || c.sender_phone || 'Unknown'; const last = (c.last_message || '').substring(0, 55); const time = c.last_message_at ? new Date(c.last_message_at).toLocaleString([], {month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}) : ''; const stage = c.lead_stage || 'cold'; const active = c.sender_phone === _convContact ? 'background:var(--bg)' : ''; return `
${escapeHtml(name)} ${stageIcon(stage)}
${escapeHtml(last)}
${c.message_count||0} msgs${time}
`; }).join(''); } async function openConvContact(encContact) { _convContact = decodeURIComponent(encContact); const threadEl = document.getElementById('conv-thread'); const sendBar = document.getElementById('conv-send-bar'); const modeWrap = document.getElementById('conv-mode-wrap'); const nameEl = document.getElementById('conv-name'); const phoneEl = document.getElementById('conv-phone-label'); threadEl.innerHTML = '
Loading…
'; // Load thread let url = 'chatbot.php?action=conversations&contact=' + encodeURIComponent(_convContact); if (_convDate) url += '&date=' + _convDate; const r = await apiGET(url); const thread = r.data.thread || r.data.messages || []; // Get name from contact list const listEl = document.getElementById('conv-list'); const activeRow = listEl.querySelector('[onclick*="' + encContact + '"]'); const nameText = activeRow ? activeRow.querySelector('span')?.textContent : _convContact; nameEl.textContent = nameText || _convContact; phoneEl.textContent = _convContact; // Render thread _renderThread(thread); // Mobile: show right panel if (window.innerWidth <= 768) { const left = document.getElementById('conv-left'); const back = document.getElementById('conv-back'); if (left) left.style.display = 'none'; document.getElementById('conv-right').style.display = 'flex'; if (back) back.style.display = 'block'; } // Show send bar — always visible, enable/disable based on replyable const replyable = r.data.is_replyable ?? (r.data.has_connected_session && _convContact.indexOf('web_') === -1); sendBar.style.display = 'flex'; const inp = sendBar.querySelector('textarea'); const btn = document.getElementById('conv-send-btn'); if (inp) { inp.disabled = !replyable; inp.placeholder = replyable ? 'Type a message… (Shift+Enter for new line)' : 'No WhatsApp session connected — cannot reply.'; } if (btn) btn.disabled = !replyable; // Show AI/Human toggle modeWrap.style.display = 'flex'; document.getElementById('conv-export-wrap').style.display = 'flex'; // Load override state const ov = await apiGET('chatbot.php?action=get_override&contact=' + encodeURIComponent(_convContact)); _convAiEnabled = ov.data.ai_enabled !== false; _updateModeButtons(); // Highlight selected in list listEl.querySelectorAll('div[onclick]').forEach(d => d.style.background = ''); if (activeRow) activeRow.style.background = 'var(--bg)'; } function _renderThread(thread) { const el = document.getElementById('conv-thread'); if (!thread.length) { el.innerHTML = '
No messages yet.
'; return; } el.innerHTML = thread.map(m => { const isIn = m.direction === 'inbound'; const text = isIn ? (m.message_text || m.message || '') : (m.response_text || m.message || ''); const src = m.response_source || ''; const srcLabel = src && !isIn ? `${src}` : ''; return `
${escapeHtml(text)}${srcLabel}
${m.created_at?new Date(m.created_at).toLocaleString([], {month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}):''}
`; }).join(''); el.scrollTop = el.scrollHeight; } async function convSend() { const input = document.getElementById('conv-msg-input'); const msg = input?.value.trim(); if (!msg || !_convContact) return; input.value = ''; input.style.height = ''; await apiPOST('chatbot.php?action=send_message', { contact: _convContact, message: msg }); // Append locally for instant feedback const el = document.getElementById('conv-thread'); el.innerHTML += `
${escapeHtml(msg)}
`; el.scrollTop = el.scrollHeight; } async function setConvOverride(aiOn) { _convAiEnabled = aiOn; _updateModeButtons(); await apiPOST('chatbot.php?action=set_override', { contact: _convContact, ai_enabled: aiOn ? 1 : 0 }); } function _updateModeButtons() { const btnAI = document.getElementById('conv-btn-ai'); const btnHuman = document.getElementById('conv-btn-human'); if (!btnAI) return; btnAI.style.background = _convAiEnabled ? 'var(--primary)' : 'transparent'; btnAI.style.color = _convAiEnabled ? '#fff' : 'var(--text)'; btnHuman.style.background = !_convAiEnabled ? 'var(--primary)' : 'transparent'; btnHuman.style.color = !_convAiEnabled ? '#fff' : 'var(--text)'; } function exportLeads(type) { const base = window.location.origin + APP_BASE; window.open(base + '/api/chatbot.php?action=export_data&type=' + type + (_convDate ? '&date=' + _convDate : ''), '_blank'); } // ── Calendar ───────────────────────────────────────────────── function setConvDate(d) { _convDate = d; _renderCalendar(); _refreshContactList(); } function convCalPrev() { _convCalMonth--; if (_convCalMonth < 0) { _convCalMonth = 11; _convCalYear--; } _renderCalendar(); } function convCalNext() { _convCalMonth++; if (_convCalMonth > 11) { _convCalMonth = 0; _convCalYear++; } _renderCalendar(); } function _convFmtYmd(d) { return d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0'); } function _renderCalendar() { const el = document.getElementById('conv-calendar'); const lbl = document.getElementById('conv-cal-label'); if (!el) return; const months = ['January','February','March','April','May','June','July','August','September','October','November','December']; if (lbl) lbl.textContent = months[_convCalMonth] + ' ' + _convCalYear; const activityMap = {}; (_convDates || []).forEach(d => { activityMap[d.chat_date] = parseInt(d.count)||0; }); const firstDay = new Date(_convCalYear, _convCalMonth, 1).getDay(); const daysInMonth = new Date(_convCalYear, _convCalMonth + 1, 0).getDate(); const today = _convFmtYmd(new Date()); let html = '
'; ['S','M','T','W','T','F','S'].forEach(d => { html += `
${d}
`; }); html += '
'; for (let i = 0; i < firstDay; i++) html += '
'; for (let day = 1; day <= daysInMonth; day++) { const ymd = _convCalYear + '-' + String(_convCalMonth+1).padStart(2,'0') + '-' + String(day).padStart(2,'0'); const count = activityMap[ymd] || 0; const active = _convDate === ymd; const isToday = today === ymd; const ttl = count > 0 ? `${count} conversation${count===1?'':'s'}` : 'No chats'; html += ``; } html += '
'; el.innerHTML = html; const lbl2 = document.getElementById('conv-date-label'); if (lbl2) lbl2.textContent = _convDate || 'All dates'; } function syncConvLayout() { const wrap = document.getElementById('conv-wrap'); const tab = document.getElementById('tab-chat'); if (!wrap || !tab) return; const topbar = document.querySelector('.app-topbar'); const topH = topbar ? topbar.getBoundingClientRect().height : 60; const tabTop = tab.getBoundingClientRect().top; const h = Math.max(420, Math.floor(window.innerHeight - tabTop - 16)); wrap.style.height = h + 'px'; } function convBack() { const right = document.getElementById('conv-right'); const left = document.getElementById('conv-left'); const back = document.getElementById('conv-back'); if (right) right.style.display = 'none'; if (left) left.style.display = 'flex'; if (back) back.style.display = 'none'; _convContact = ''; } // Window resize — re-sync conversation height window.addEventListener('resize', () => { if (document.getElementById('tab-chat')?.style.display !== 'none') syncConvLayout(); }); // ── Webform page ─────────────────────────────────────────── async function loadWidgetSettings() { const r = await apiGET('chatbot.php?action=client_config'); const cfg = r.data.client || r.data.config || {}; const title = document.getElementById('wg-title'); if (!title) return; title.value = cfg.widget_title || 'Chat with us'; document.getElementById('wg-color').value = cfg.widget_color || '#25D366'; document.getElementById('wg-pos').value = cfg.widget_position || 'bottom-right'; document.getElementById('wg-enabled').checked = !!Number(cfg.widget_enabled || 0); if (cfg.widget_icon) selectWidgetIcon(cfg.widget_icon); const token = cfg.widget_token || r.data.widget_token || ''; const base = window.location.origin + APP_BASE; const widgetVersion = '20260712-ui4'; const embedCode = `