👀 Admin view — Viewing as
← Return to Admin
`;
}
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 => `${escapeHtml(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
Prepare the assistant profile, starter knowledge, and FAQs from your business details.
When a visitor conversation ends, the assistant sends a notification to these WhatsApp groups.
Groups are synced from your Sessions → Discover Groups.
Test Notification
📨 Send Test
⚙️ Notification Settings
Enable conversation summary notifications
When enabled, the assistant sends a notification to your configured groups after each conversation ends.
Notification Content
📝 Summary only — AI-generated brief overview
📜 Full transcript — every message in the conversation
📋 Both — summary followed by full transcript
Summary uses AI to condense the conversation. Full transcript sends every message verbatim.
Send Using Session (optional)
— Auto-select any available session —
Save Settings
Test & Diagnostics
🔍 Diagnose
⚡ Simulate Timeout
▶ Run Check Now
Diagnose — checks your config. Simulate — forces the latest conversation to expire and sends to WhatsApp (test end-to-end). Run Check Now — processes idle prompts & timeouts immediately (replaces cron for manual testing).
🤖 Digital Assistant Settings
Connect external sites and APIs. The assistant syncs their content automatically and uses it to answer visitor questions.
FAQ Entries
Keyword Triggers
Question Answer Keywords Hits Active Loading…
Keyword Match Type Response Hits Active Loading…
Select a contact to view the conversation
Send ▶
`;
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 = '— Select session above to load groups — ';
return;
}
sel.innerHTML = 'Loading groups… ';
const r = await apiGET(`groups.php?action=saved&session_id=${sessId}`);
const groups = r.data.groups || [];
if (!groups.length) {
sel.innerHTML = 'No groups saved — Discover Groups first ';
return;
}
// Preserve currently saved group JID if present
const curJid = sel.dataset.savedJid || '';
sel.innerHTML = '— Select group — ' +
groups.map(g => `${escapeHtml(g.name||g.jid)} `).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 = '— Auto-select any available session — ' +
sessions.map(s => `${escapeHtml(s.session_name)}${s.phone_number?' ('+s.phone_number+')':''} `).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 ? `Contact Last Message Updated ${rows.map(c=>`${escapeHtml(c.contact_name||c.contact_jid||'Contact')} ${escapeHtml(c.last_message||'')} ${c.last_msg_at?new Date(c.last_msg_at).toLocaleString():''} `).join('')}
` : 'No conversations yet.
';
}
function openOnboarding() {
openModal('Digital Assistant Onboarding', `
Services / Products
Extra Notes
Reset existing knowledge and FAQs before applying
Generate & Apply Setup
`);
}
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 = `
Name Type Status Last Sync Chunks Actions
${sources.map(s => `
${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}
🔌 Test
↻ Sync
${s.status==='paused'?'▶ Resume':'⏸ Pause'}
✏ Edit
✕
`).join('')}
`;
}
function editSource(id) { addSource(Object.assign({}, _srcStore[id] || {}, {id})); }
function addSource(existing) {
const s = existing || {};
openModal((s.id ? '✏️ Edit' : '+ Add') + ' Data Source', `
Source Name
Source Type
Generic REST API
WordPress Site (with Traffi.Click plugin)
Sync Interval
${[[0.5,'30 minutes'],[1,'1 hour'],[2,'2 hours'],[4,'4 hours'],[6,'6 hours'],[12,'12 hours'],[24,'Daily'],[48,'Every 2 days'],[168,'Weekly']].map(([h,label]) => `${label} `).join('')}
Live Query Config (optional — enables follow-ups, image & detail requests)
🏠 Real Estate
🛒 E-commerce
🏥 Healthcare
✕ Clear
Defines item_singular, endpoints, field aliases, and url_pattern for live follow-up queries. See presets for the exact shape.
${s.id?'Save Changes':'Add & Sync Now'}
Cancel
`);
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 = `Title Type Content Preview Actions ${
items.map(k=>`${escapeHtml(k.title||'—')} ${escapeHtml(k.type||k.content_type||'text')} ${escapeHtml((k.content||k.source_url||'').substring(0,100))}... Edit Delete `).join('')
}
`;
}
async function addKB(item=null) {
const isEdit = !!item;
openModal(isEdit ? 'Edit Knowledge' : 'Add Knowledge', `
Website URL
Content / Answer
${isEdit?'Save Changes':'Save 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 = `Title Content Preview Actions ${
items.map(k=>`${k.title||'—'} ${(k.content||'').substring(0,80)}… Delete `).join('')
}
`;
}
async function addKB() {
openModal('Add Knowledge Base Document', `
Title
Content
Save 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'}
Edit
Del
`).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'}
Del
`).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||'—')}
Edit Quota
${demoOn?'Demo Off':'Demo On'}
${suspended?'Activate':'Suspend'}
`;
}).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'}
Edit
Del
`).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 => `${escapeHtml(o.label)} `).join('') || 'No models ';
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 += ``;
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 += `
${day}${count>0?`${count} `:''}
`;
}
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 = `
❓ Add FAQ
✕
Question
Answer
Keywords (comma-separated triggers)
Cancel
Save
🎯 Add Keyword Trigger
✕
Response
Cancel
Save
🤖 AI Model
✕
Model ID
Cancel
Save