Spaces:
Sleeping
Sleeping
| 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 = `<p class="mb-0">${escapeHtml(msg.content).replace(/\n/g, '<br>')}</p>`; | |
| } | |
| } else { | |
| div.innerHTML = `<p class="mb-0">${escapeHtml(msg.content)}</p>`; | |
| } | |
| chatFeed.appendChild(div); | |
| }); | |
| chatFeed.scrollTop = chatFeed.scrollHeight; | |
| }; | |
| const escapeHtml = (text) => { | |
| if (!text) return ""; | |
| return text.replace(/&/g, "&").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 = ` | |
| <div class="d-flex justify-content-between align-items-center w-100 gap-3" style="min-width: 160px;"> | |
| <p class="mb-0 text-muted"><span class="thinking-text">thinking...</span></p> | |
| <button id="inline-ai-stop-btn" class="btn btn-outline-danger btn-sm py-0.5 px-2" style="font-size: 0.72rem; border-radius: 4px; font-weight: 500;">Stop</button> | |
| </div> | |
| `; | |
| 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, | |
| 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, | |
| 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 === '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(); | |
| }); | |