document.addEventListener('DOMContentLoaded', () => { const floatBtn = document.getElementById('floating-ai-assistant-btn'); const sidebar = document.getElementById('inline-ai-assistant'); const closeBtn = document.getElementById('inline-ai-close-btn'); const clearBtn = document.getElementById('inline-ai-clear-btn'); const newChatBtn = document.getElementById('inline-ai-new-chat-btn'); const chatInput = document.getElementById('inline-ai-chat-input'); const chatSend = document.getElementById('inline-ai-chat-send'); const chatFeed = document.getElementById('inline-ai-chat-feed'); if (!floatBtn || !sidebar) return; let chatMessages = []; const welcomeMessage = { role: 'bot', content: 'Hello! I am your AI CRM Assistant. I can help you fully automate tasks, manage company records, schedule actions, and answer questions about CRM data.' }; // Load history from localStorage const loadLocalHistory = () => { try { const stored = localStorage.getItem('prospectiq-inline-chat-history'); if (stored) { chatMessages = JSON.parse(stored); } else { chatMessages = [welcomeMessage]; } } catch (e) { chatMessages = [welcomeMessage]; } renderChatFeed(); }; // Save history to localStorage const saveLocalHistory = () => { try { localStorage.setItem('prospectiq-inline-chat-history', JSON.stringify(chatMessages)); } catch (e) { console.error("Failed to save chat history", e); } }; // Render entire feed from memory const renderChatFeed = () => { if (!chatFeed) return; chatFeed.innerHTML = ''; chatMessages.forEach(msg => { const div = document.createElement('div'); div.className = `chat-message ${msg.role}`; if (msg.role === 'bot') { if (typeof marked !== 'undefined') { div.innerHTML = marked.parse(msg.content); } else { div.innerHTML = `

${escapeHtml(msg.content).replace(/\n/g, '
')}

`; } } else { div.innerHTML = `

${escapeHtml(msg.content)}

`; } chatFeed.appendChild(div); }); chatFeed.scrollTop = chatFeed.scrollHeight; }; const escapeHtml = (text) => { if (!text) return ""; return text.replace(/&/g, "&").replace(//g, ">"); }; // Toggle assistant sidebar const toggleAssistant = () => { const isOpen = sidebar.classList.toggle('open'); floatBtn.classList.toggle('active', isOpen); if (isOpen) { floatBtn.classList.add('d-none'); setTimeout(() => { chatInput.focus(); chatFeed.scrollTop = chatFeed.scrollHeight; }, 100); } else { floatBtn.classList.remove('d-none'); } // Trigger window resize so responsive layouts recalculate window.dispatchEvent(new Event('resize')); }; floatBtn.addEventListener('click', toggleAssistant); if (closeBtn) closeBtn.addEventListener('click', toggleAssistant); // Wire up Clear Chat Button if (clearBtn) { clearBtn.addEventListener('click', () => { if (confirm("Are you sure you want to clear your chat history?")) { chatMessages = [welcomeMessage]; saveLocalHistory(); renderChatFeed(); } }); } // Wire up New Chat Button if (newChatBtn) { newChatBtn.addEventListener('click', () => { chatMessages = [welcomeMessage]; saveLocalHistory(); renderChatFeed(); if (chatInput) { chatInput.value = ''; chatInput.style.height = 'auto'; chatInput.focus(); } }); } const appendMessageAndSave = (role, content) => { chatMessages.push({ role, content }); saveLocalHistory(); renderChatFeed(); }; const sendInlineMessage = async () => { const msg = chatInput.value.trim(); if (!msg) return; // Append user query and save appendMessageAndSave('user', msg); chatInput.value = ''; chatInput.style.height = '24px'; // Reset height of textarea chatInput.disabled = true; // Create thinking bubble with Stop button const loadingDiv = document.createElement('div'); loadingDiv.className = 'chat-message bot'; loadingDiv.id = 'inline-ai-thinking-bubble'; loadingDiv.innerHTML = `
AI is thinking...
`; chatFeed.appendChild(loadingDiv); chatFeed.scrollTop = chatFeed.scrollHeight; // Initialize abort controller window.currentInlineAssistantController = new AbortController(); // Wire up stop button const stopBtn = document.getElementById('inline-ai-stop-btn'); if (stopBtn) { stopBtn.addEventListener('click', () => { if (window.currentInlineAssistantController) { window.currentInlineAssistantController.abort(); } }); } try { // Get database context const dbContext = typeof mockCompanies !== 'undefined' ? JSON.stringify(mockCompanies.map(c => ({ id: c.id, name: c.name, legal_name: c.legal_name, website: c.website, phone: c.phone, nic_code: c.nic_code, industry: c.industry, stage: c.stage_name || c.stage, province: c.province, city: c.city, size: c.size_staff }))) : ""; const dbContactsContext = typeof mockContacts !== 'undefined' ? JSON.stringify(mockContacts.map(c => ({ id: c.id, first_name: c.first_name, last_name: c.last_name, email: c.email, phone: c.phone, company: c.company_name || c.company }))) : ""; const tasksContext = localStorage.getItem('prospectiq-tasks') || '[]'; const res = await fetch('/api/command', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: msg, history: chatMessages.slice(0, -1), context: `Full Database (Companies): ${dbContext}\nFull Database (Contacts): ${dbContactsContext}\nTasks list: ${tasksContext}`, client_time: new Date().toString() }), signal: window.currentInlineAssistantController.signal }); if (!res.ok) throw new Error("Failed to contact CRM Assistant"); const data = await res.json(); // Clear loading state loadingDiv.remove(); if (data.error) { appendMessageAndSave('bot', `Error: ${data.error}`); return; } // ── AUTOMATION HANDLERS ── let botReply = ""; if (data.action === 'tool_route') { if (window.submitCommandToolRoute) { await window.submitCommandToolRoute(data, msg); } botReply = `Opening tool route: **${data.tool}**.`; } else if (data.action === 'view_company_profile' || data.action === 'view_company') { const cid = parseInt(data.company_id); if (cid) { window.location.hash = `#/companies/${cid}`; if (window.handleNavigation) window.handleNavigation(); botReply = `Opening profile page for **${data.company_name || 'company'}**.`; } else { botReply = `Could not locate company ID for ${data.company_name || 'requested company'}.`; } } else if (data.action === 'filter_companies') { window.industryFilter = ''; window.provinceFilter = ''; window.stageFilter = ''; window.cityFilter = ''; const titleCase = (str) => str ? str.split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(' ') : ''; if (data.filters && data.filters.industry) window.industryFilter = titleCase(data.filters.industry); if (data.filters && data.filters.province) window.provinceFilter = titleCase(data.filters.province); if (data.filters && data.filters.stage) window.stageFilter = titleCase(data.filters.stage); if (data.filters && data.filters.city) window.cityFilter = titleCase(data.filters.city); if (data.view_type === 'board' || data.view_type === 'kanban') { window.activeCompanyLayout = 'board'; } else if (data.view_type === 'table') { window.activeCompanyLayout = 'table'; } window.location.hash = '#/companies'; if (window.handleNavigation) window.handleNavigation(); botReply = `Filtering companies. Filters applied: \`${JSON.stringify(data.filters)}\`.`; } else if (data.action === 'add_company') { const addRes = await fetch('/api/companies', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data.company_data) }); if (!addRes.ok) { const err = await addRes.json(); throw new Error(err.error || "Failed to add company"); } if (window.loadDataFromDb) await window.loadDataFromDb(); window.location.hash = '#/companies'; if (window.handleNavigation) window.handleNavigation(); botReply = `Added company record: **${data.company_data.name}**.`; } else if (data.action === 'edit_company') { const companiesList = typeof mockCompanies !== 'undefined' ? mockCompanies : []; const existing = companiesList.find(c => c.id === parseInt(data.company_id)) || {}; const mergedData = { ...existing, ...data.company_data }; const editRes = await fetch('/api/companies/' + data.company_id, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(mergedData) }); if (!editRes.ok) { const err = await editRes.json(); throw new Error(err.error || "Failed to update company"); } if (window.loadDataFromDb) await window.loadDataFromDb(); window.location.hash = '#/companies'; if (window.handleNavigation) window.handleNavigation(); botReply = `Updated company record: **${mergedData.name || data.company_id}**.`; } else if (data.action === 'delete_company') { const delRes = await fetch('/api/companies/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ids: [data.company_id] }) }); if (!delRes.ok) { const err = await delRes.json(); throw new Error(err.error || "Failed to delete company"); } if (window.loadDataFromDb) await window.loadDataFromDb(); window.location.hash = '#/companies'; if (window.handleNavigation) window.handleNavigation(); botReply = `Deleted company record **ID: ${data.company_id}**.`; } else if (data.action === 'filter_contacts') { window.contactsCompanyFilter = ''; window.location.hash = '#/contacts'; if (window.handleNavigation) window.handleNavigation(); botReply = `Displaying contacts list view.`; } else if (data.action === 'add_contact') { const addRes = await fetch('/api/contacts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data.contact_data) }); if (!addRes.ok) { const err = await addRes.json(); throw new Error(err.error || "Failed to add contact"); } if (window.loadDataFromDb) await window.loadDataFromDb(); window.location.hash = '#/contacts'; if (window.handleNavigation) window.handleNavigation(); botReply = `Added contact: **${data.contact_data.first_name} ${data.contact_data.last_name}**.`; } else if (data.action === 'edit_contact') { const contactsList = typeof mockContacts !== 'undefined' ? mockContacts : []; const existing = contactsList.find(c => c.id === parseInt(data.contact_id)) || {}; const mergedData = { ...existing, ...data.contact_data }; // Ensure company_id is present as it is a required field in the Flask endpoint if (!mergedData.company_id && existing.company_id) { mergedData.company_id = existing.company_id; } const editRes = await fetch('/api/contacts/' + data.contact_id, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(mergedData) }); if (!editRes.ok) { const err = await editRes.json(); throw new Error(err.error || "Failed to update contact"); } if (window.loadDataFromDb) await window.loadDataFromDb(); window.location.hash = '#/contacts'; if (window.handleNavigation) window.handleNavigation(); botReply = `Updated contact: **${mergedData.first_name} ${mergedData.last_name}**.`; } else if (data.action === 'delete_contact') { const delRes = await fetch('/api/contacts/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ids: [data.contact_id] }) }); if (!delRes.ok) { const err = await delRes.json(); throw new Error(err.error || "Failed to delete contact"); } if (window.loadDataFromDb) await window.loadDataFromDb(); window.location.hash = '#/contacts'; if (window.handleNavigation) window.handleNavigation(); botReply = `Deleted contact record.`; } else if (data.action === 'add_task') { const currentTasks = JSON.parse(localStorage.getItem('prospectiq-tasks') || '[]'); const dateVal = data.date || new Date().toISOString().split('T')[0]; const timeVal = data.time || '09:00'; const notesVal = data.notes || ''; const newTask = { id: Date.now(), title: data.task_title, completed: false, date: dateVal, time: timeVal, notes: notesVal }; currentTasks.unshift(newTask); localStorage.setItem('prospectiq-tasks', JSON.stringify(currentTasks)); if (window.addNotification) { window.addNotification("Task Scheduled", `Scheduled: "${data.task_title}"`); } window.location.hash = '#/'; if (window.handleNavigation) window.handleNavigation(); botReply = `Scheduled task: **${data.task_title}** on ${dateVal} at ${timeVal}.`; } else if (data.action === 'edit_task') { let currentTasks = JSON.parse(localStorage.getItem('prospectiq-tasks') || '[]'); currentTasks = currentTasks.map(t => { if (t.id === parseInt(data.task_id)) { const updated = { ...t }; if (data.task_title !== undefined) updated.title = data.task_title; if (data.date !== undefined) updated.date = data.date; if (data.time !== undefined) updated.time = data.time; if (data.notes !== undefined) updated.notes = data.notes; if (data.completed !== undefined) updated.completed = data.completed; return updated; } return t; }); localStorage.setItem('prospectiq-tasks', JSON.stringify(currentTasks)); window.location.hash = '#/'; if (window.handleNavigation) window.handleNavigation(); let changeMsgs = []; if (data.task_title !== undefined) changeMsgs.push(`title to "${data.task_title}"`); if (data.date !== undefined) changeMsgs.push(`date to ${data.date}`); if (data.time !== undefined) changeMsgs.push(`time to ${data.time}`); if (changeMsgs.length > 0) { botReply = `Updated task: ${changeMsgs.join(', ')}.`; } else { botReply = `Updated task details.`; } } else if (data.action === 'complete_task') { let currentTasks = JSON.parse(localStorage.getItem('prospectiq-tasks') || '[]'); currentTasks = currentTasks.map(t => t.id === parseInt(data.task_id) ? { ...t, completed: true } : t); localStorage.setItem('prospectiq-tasks', JSON.stringify(currentTasks)); window.location.hash = '#/'; if (window.handleNavigation) window.handleNavigation(); botReply = `Marked task ID ${data.task_id} as complete.`; } else if (data.action === 'delete_task') { let currentTasks = JSON.parse(localStorage.getItem('prospectiq-tasks') || '[]'); currentTasks = currentTasks.filter(t => t.id !== parseInt(data.task_id)); localStorage.setItem('prospectiq-tasks', JSON.stringify(currentTasks)); window.location.hash = '#/'; if (window.handleNavigation) window.handleNavigation(); botReply = `Deleted task ID ${data.task_id}.`; } else if (data.action === 'answer' && data.reply) { botReply = data.reply; } else if (data.reply) { botReply = data.reply; } else { botReply = `CRM operation completed successfully.`; } appendMessageAndSave('bot', botReply); } catch (err) { loadingDiv.remove(); if (err.name === 'AbortError') { appendMessageAndSave('bot', `Request stopped by user.`); } else { appendMessageAndSave('bot', `Error: Could not connect to assistant endpoint.`); } } finally { chatInput.disabled = false; chatInput.focus(); chatFeed.scrollTop = chatFeed.scrollHeight; } }; if (chatSend) { chatSend.addEventListener('click', sendInlineMessage); } if (chatInput) { chatInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendInlineMessage(); } }); chatInput.addEventListener('input', function() { this.style.height = 'auto'; this.style.height = (this.scrollHeight) + 'px'; }); } // Setup Speech-To-Text const micBtn = document.getElementById('inline-ai-mic-btn'); if (micBtn && chatInput && typeof initSpeechToText === 'function') { initSpeechToText(chatInput, micBtn); } // Expose a helper globally so app.js can inject command bar replies directly into history window.appendInlineChatHistory = (userText, botText) => { chatMessages.push({ role: 'user', content: userText }); chatMessages.push({ role: 'bot', content: botText }); saveLocalHistory(); renderChatFeed(); }; // Load initial feed loadLocalHistory(); });