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 = `
`;
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();
});