// ============================================================
// MOORE AI v13 — ROOT APP COMPONENT
// Holds all state, routing, nav shell, voice/AI engine
// No import/export — all globals loaded in order via index.html
// ============================================================
// =========================================================
// AUTH GATE — redirect to /login if not authenticated
// =========================================================
(function() {
const auth = sessionStorage.getItem('moore_auth');
if (!auth) {
window.location.href = '/login';
}
})();
const { useState, useEffect, useRef, useCallback, useMemo } = React;
// =========================================================
// ERROR BOUNDARY
// =========================================================
class ErrorBoundary extends React.Component {
constructor(props) { super(props); this.state = { hasError: false, error: null }; }
static getDerivedStateFromError(e) { return { hasError: true, error: e }; }
render() {
if (this.state.hasError) {
return (
Render Error
{this.state.error?.message || 'Unknown error'}
);
}
return this.props.children;
}
}
// =========================================================
// =========================================================
// BALANCED JSON EXTRACTOR — handles nested objects in ACTION: {...}
// Also catches raw JSON blobs the AI emits without the ACTION: prefix.
// =========================================================
const parseActionBlocks = (text) => {
const actions = [];
const ranges = [];
const ACTION_TYPES = /^(CREATE_CONTACT|UPDATE_CONTACT|ADD_NOTE|SEND_SMS|SEND_EMAIL|MAKE_CALL|SCHEDULE|UPDATE_OPP|MOVE_STAGE|ADD_TASK|CHAIN)$/;
// Walk balanced braces starting at `start` (text[start] must be '{')
const extractEnd = (src, start) => {
let depth = 0, j = start, inStr = false, esc = false;
while (j < src.length) {
const c = src[j];
if (esc) { esc = false; }
else if (c === '\\' && inStr) { esc = true; }
else if (c === '"') { inStr = !inStr; }
else if (!inStr) {
if (c === '{') depth++;
else if (c === '}') { depth--; if (depth === 0) return j; }
}
j++;
}
return -1;
};
// Pass 1 — explicit "ACTION: {" blocks
let i = 0;
while (true) {
const mi = text.indexOf('ACTION: {', i);
if (mi === -1) break;
const end = extractEnd(text, mi + 8);
if (end === -1) break;
try {
const jsonStr = text.substring(mi + 8, end + 1);
JSON.parse(jsonStr);
actions.push(jsonStr);
ranges.push([mi, end + 1]);
} catch(e) {}
i = end + 1;
}
// Remove pass-1 blocks from a working copy
let clean = text;
for (let k = ranges.length - 1; k >= 0; k--) {
clean = clean.substring(0, ranges[k][0]) + clean.substring(ranges[k][1]);
}
// Pass 2 — raw JSON blobs the AI embedded without the "ACTION: " prefix
const raw2Ranges = [];
i = 0;
while (i < clean.length) {
const brace = clean.indexOf('{', i);
if (brace === -1) break;
const end = extractEnd(clean, brace);
if (end === -1) { i = brace + 1; continue; }
const candidate = clean.substring(brace, end + 1);
try {
const parsed = JSON.parse(candidate);
if (parsed && typeof parsed.type === 'string' && ACTION_TYPES.test(parsed.type)) {
actions.push(candidate);
raw2Ranges.push([brace, end + 1]);
}
} catch(e) {}
i = end + 1;
}
for (let k = raw2Ranges.length - 1; k >= 0; k--) {
clean = clean.substring(0, raw2Ranges[k][0]) + clean.substring(raw2Ranges[k][1]);
}
// Tidy up leftover punctuation artefacts (trailing colons, double spaces, blank lines)
clean = clean.replace(/:\s*$/gm, '').replace(/[ \t]{2,}/g, ' ').replace(/\n{3,}/g, '\n\n').trim();
return { actions, clean };
};
const stripActionBlocks = (text) => parseActionBlocks(text).clean;
// =========================================================
// ACTION APPROVAL HELPERS
// =========================================================
const ACTION_META = {
SEND_EMAIL: { icon: 'mail', label: 'Send Email', color: '#06b6d4' },
SEND_SMS: { icon: 'message-circle', label: 'Send SMS', color: '#10b981' },
MAKE_CALL: { icon: 'phone-call', label: 'Call', color: '#3b82f6' },
CREATE_CONTACT: { icon: 'user-plus', label: 'Create Contact', color: '#3b82f6' },
UPDATE_CONTACT: { icon: 'pencil', label: 'Update Contact', color: '#64748b' },
ADD_NOTE: { icon: 'file-text', label: 'Add Note', color: '#ec4899' },
ADD_TASK: { icon: 'check-square', label: 'Create Task', color: '#10b981' },
SCHEDULE: { icon: 'calendar-check', label: 'Schedule', color: '#8b5cf6' },
UPDATE_OPP: { icon: 'trophy', label: 'Update Deal', color: '#f59e0b' },
MOVE_STAGE: { icon: 'git-merge', label: 'Move Stage', color: '#6366f1' },
CHAIN: { icon: 'zap', label: 'Chain', color: '#f59e0b' },
};
const describeAction = (action) => {
const name = action.name || (action.data ? `${action.data.firstName||''} ${action.data.lastName||''}`.trim() : '') || '';
switch (action.type) {
case 'SEND_EMAIL': return `${name}${action.subject ? ` — "${action.subject.substring(0,45)}"` : ''}`;
case 'SEND_SMS': return `${name}${action.message ? ` — "${action.message.substring(0,45)}..."` : ''}`;
case 'MAKE_CALL': return `${name}${action.objective ? ` — ${action.objective.substring(0,45)}` : ''}`;
case 'CREATE_CONTACT': return name || 'new contact';
case 'ADD_NOTE': return `${name}${action.body ? ` — "${action.body.substring(0,45)}"` : ''}`;
case 'ADD_TASK': return action.title ? `"${action.title.substring(0,55)}"` : '';
case 'SCHEDULE': return `${name}${action.title ? ` — ${action.title}` : ''}`;
case 'UPDATE_OPP': return `mark ${action.status}`;
case 'MOVE_STAGE': return 'move to new stage';
default: return '';
}
};
const ActionApprovalCard = ({ m, approveActions, cancelActions }) => (
{m.actions.length} Action{m.actions.length !== 1 ? 's' : ''} Queued — Approve to Execute
{m.actions.map((action, ai) => {
const meta = ACTION_META[action.type] || { icon: 'zap', label: action.type, color: '#94a3b8' };
const desc = describeAction(action);
return (
{action.type}
{desc &&
{desc}}
);
})}
);
// MAIN APP
// =========================================================
const App = () => {
// ── Theme ──
const [theme, setTheme] = useState('dark');
// ── Core state ──
const [activeTab, setActiveTab] = useState('dashboard');
const [loading, setLoading] = useState(true);
const [crmData, setCrmData] = useState({ stats: { pipelineValue: 0, totalLeads: 0, winRate: 0, aiActions: 0 }, opportunities: [] });
const [pipelines, setPipelines] = useState([]);
const [contacts, setContacts] = useState([]);
const [calEvents, setCalEvents] = useState([]);
const [conversations, setConversations] = useState([]);
const [analytics, setAnalytics] = useState(null);
const [selectedLead, setSelectedLead] = useState(null);
const [selectedContact, setSelectedContact] = useState(null);
const [searchQuery, setSearchQuery] = useState('');
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [notifications, setNotifications] = useState([]);
const [loadingTabs, setLoadingTabs] = useState({});
// ── Tasks (localStorage) ──
const [tasks, setTasks] = useState(() => {
try { return JSON.parse(localStorage.getItem('moore_tasks') || '[]'); } catch(e) { return []; }
});
// ── AI Chat state ──
const [chatOpen, setChatOpen] = useState(false);
const [chatInput, setChatInput] = useState('');
const [messages, setMessages] = useState([
{ role: 'ai', text: "Systems online. Registry synced. I'm ready to architect your pipeline — type a command or use voice." }
]);
const [isTyping, setIsTyping] = useState(false);
const [isListening, setIsListening] = useState(false);
const [chatMode, setChatMode] = useState('minimized');
// ── Pipeline view ──
const [pipelineView, setPipelineView] = useState('kanban');
const [pipelineFilter, setPipelineFilter] = useState('all');
const [selectedPipelineId, setSelectedPipelineId] = useState('');
// ── Calendar state ──
const [calView, setCalView] = useState('month');
const [calDate, setCalDate] = useState(new Date());
const [calendars, setCalendars] = useState([]);
const [selectedCalendarId, setSelectedCalendarId] = useState('');
// ── Contact form ──
const [showContactForm, setShowContactForm] = useState(false);
const [contactForm, setContactForm] = useState({ firstName: '', lastName: '', email: '', phone: '' });
// ── Notes ──
const [selectedLeadNotes, setSelectedLeadNotes] = useState([]);
const [newNote, setNewNote] = useState('');
// ── Refs ──
const audioRef = useRef(null);
const recognitionRef = useRef(null);
const chatEndRef = useRef(null);
const dragItemRef = useRef(null);
const handleSendMsgRef = useRef(null);
const pendingActionsRef = useRef({});
const transcriptRef = useRef('');
const silenceTimerRef = useRef(null);
const isVoiceEnabledRef = useRef(true);
const [isVoiceEnabled, setIsVoiceEnabled] = useState(true);
const [geminiKey, setGeminiKey] = useState('');
const [blandKey, setBlandKey] = useState('');
const [locationName, setLocationName] = useState('BigBlueCollar');
const [liveTranscript, setLiveTranscript] = useState('');
const [actionItems, setActionItems] = useState([]);
const [activeCalls, setActiveCalls] = useState([]);
// ── Theme persistence ──
useEffect(() => { localStorage.setItem('moore_theme', theme); }, [theme]);
// ── AUDIO: Gemini TTS + browser Speech Synthesis fallback ──
const stopAudio = () => {
if (audioRef.current) { audioRef.current.pause(); audioRef.current.currentTime = 0; }
window.speechSynthesis?.cancel();
};
const speakBrowser = (text) => {
if (!isVoiceEnabledRef.current) return;
const clean = text.replace(/[*_#`~]/g, '').trim();
const utterance = new SpeechSynthesisUtterance(clean);
const voices = window.speechSynthesis?.getVoices?.() || [];
const maleVoice = voices.find(v =>
v.name.includes('Google US English Male') || v.name.includes('Google UK English Male') ||
v.name.includes('Daniel') || v.name.includes('Arthur') || v.name.includes('Alex') ||
(v.name.toLowerCase().includes('male') && v.lang.startsWith('en'))
);
if (maleVoice) utterance.voice = maleVoice;
utterance.rate = 1.05;
window.speechSynthesis.speak(utterance);
};
const speak = async (text) => {
if (!isVoiceEnabledRef.current || !text || text.length < 5) return;
stopAudio();
const clean = stripActionBlocks(text).replace(/[*_#`~]/g, '').trim().substring(0, 1000);
if (!clean) return;
if (geminiKey) {
try {
const res = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-tts:generateContent?key=${geminiKey}`,
{
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: 'Say professionally and conversationally: ' + clean }] }],
generationConfig: {
responseModalities: ['AUDIO'],
speechConfig: { voiceConfig: { prebuiltVoiceConfig: { voiceName: 'Algieba' } } }
},
model: 'gemini-2.5-flash-preview-tts'
})
}
);
const data = await res.json();
if (data.error) throw new Error(data.error.message);
const audioPart = data.candidates?.[0]?.content?.parts?.[0]?.inlineData;
if (audioPart?.data) {
const sampleRateMatch = (audioPart.mimeType || '').match(/rate=(\d+)/);
const sampleRate = sampleRateMatch ? parseInt(sampleRateMatch[1]) : 24000;
const binaryStr = window.atob(audioPart.data);
const bytes = new Uint8Array(binaryStr.length);
for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
const wavBlob = pcmToWav(bytes.buffer, sampleRate);
const url = URL.createObjectURL(wavBlob);
const audio = new Audio(url);
audioRef.current = audio;
audio.play();
return;
}
} catch (e) {
console.warn('Gemini TTS failed, using browser voice:', e.message);
}
}
speakBrowser(clean);
};
// ── VOICE RECOGNITION — continuous with silence timer ──
useEffect(() => {
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
if (SR) {
const rec = new SR();
rec.continuous = true;
rec.interimResults = true;
rec.lang = 'en-US';
rec.onresult = (event) => {
let finalTranscript = '';
let interimTranscript = '';
for (let i = 0; i < event.results.length; ++i) {
if (event.results[i].isFinal) finalTranscript += event.results[i][0].transcript;
else interimTranscript += event.results[i][0].transcript;
}
const currentText = finalTranscript + interimTranscript;
transcriptRef.current = currentText;
setLiveTranscript(currentText);
setChatInput(currentText);
clearTimeout(silenceTimerRef.current);
silenceTimerRef.current = setTimeout(() => {
if (transcriptRef.current.trim()) {
rec.stop();
setIsListening(false);
if (handleSendMsgRef.current) {
handleSendMsgRef.current(null, transcriptRef.current);
}
transcriptRef.current = '';
setLiveTranscript('');
setChatInput('');
}
}, 2000);
};
rec.onerror = (e) => {
console.warn('Speech error:', e.error);
setIsListening(false);
clearTimeout(silenceTimerRef.current);
};
rec.onend = () => {
setIsListening(false);
clearTimeout(silenceTimerRef.current);
};
recognitionRef.current = rec;
}
}, []);
useEffect(() => { isVoiceEnabledRef.current = isVoiceEnabled; }, [isVoiceEnabled]);
const toggleListening = () => {
if (!recognitionRef.current) {
alert('Voice input requires Chrome or Edge browser.');
return;
}
if (isListening) {
recognitionRef.current.stop();
setIsListening(false);
clearTimeout(silenceTimerRef.current);
if (transcriptRef.current.trim() && handleSendMsgRef.current) {
handleSendMsgRef.current(null, transcriptRef.current);
transcriptRef.current = '';
setLiveTranscript('');
setChatInput('');
}
} else {
try {
setChatInput('');
transcriptRef.current = '';
setLiveTranscript('');
if (audioRef.current) { audioRef.current.pause(); audioRef.current.currentTime = 0; }
window.speechSynthesis?.cancel();
recognitionRef.current.start();
setIsListening(true);
} catch(e) { console.error('Mic start error:', e); }
}
};
// ── DATA FETCHING ──
const syncOpportunities = async (pipelineId = '') => {
try {
const data = await API.syncOpportunities(pipelineId);
if (data) setCrmData(data);
} catch (e) { console.warn('Sync failed', e); }
};
const fetchPipelines = async () => {
try {
const data = await API.fetchPipelines();
if (data.success) setPipelines(data.pipelines);
} catch (e) {}
};
const fetchContacts = async () => {
setLoadingTabs(p => ({ ...p, contacts: true }));
try {
const data = await API.fetchContacts();
if (data.success) setContacts(data.contacts);
} catch (e) {}
setLoadingTabs(p => ({ ...p, contacts: false }));
};
const fetchCalendar = async (calId) => {
setLoadingTabs(p => ({ ...p, calendar: true }));
try {
const data = await API.fetchCalendar(calId || selectedCalendarId);
if (data.success) setCalEvents(data.events);
} catch (e) {}
setLoadingTabs(p => ({ ...p, calendar: false }));
};
const fetchCalendars = async () => {
try {
const [calsData, settingsData] = await Promise.all([API.fetchCalendars(), API.fetchSettings()]);
if (calsData.success && calsData.calendars.length > 0) {
setCalendars(calsData.calendars);
const saved = settingsData.calendarId || calsData.calendars[0].id;
setSelectedCalendarId(saved);
return saved;
}
} catch (e) {}
return null;
};
const fetchConversations = async () => {
setLoadingTabs(p => ({ ...p, inbox: true }));
try {
const data = await API.fetchConversations();
if (data.success) setConversations(data.conversations);
} catch (e) {}
setLoadingTabs(p => ({ ...p, inbox: false }));
};
const fetchAnalytics = async () => {
setLoadingTabs(p => ({ ...p, analytics: true }));
try {
const data = await API.fetchAnalytics();
if (data.success) setAnalytics(data);
} catch (e) {}
setLoadingTabs(p => ({ ...p, analytics: false }));
};
const fetchLeadNotes = async (contactId) => {
if (!contactId) return;
try {
const data = await API.fetchContactNotes(contactId);
if (data.success) setSelectedLeadNotes(data.notes);
} catch (e) {}
};
useEffect(() => {
(async () => {
try {
const cfg = await API.fetchConfig();
if (cfg.geminiKey) setGeminiKey(cfg.geminiKey);
if (cfg.blandKey) setBlandKey(cfg.blandKey);
if (cfg.locationName) setLocationName(cfg.locationName);
} catch(e) {}
await Promise.all([syncOpportunities(), fetchPipelines()]);
setLoading(false);
})();
}, []);
useEffect(() => {
if (activeTab === 'contacts' && contacts.length === 0) fetchContacts();
if (activeTab === 'calendar') { fetchCalendars().then(calId => fetchCalendar(calId)); }
if (activeTab === 'inbox' && conversations.length === 0) fetchConversations();
if (activeTab === 'analytics' && !analytics) fetchAnalytics();
}, [activeTab]);
useEffect(() => {
if (window.lucide) window.lucide.createIcons();
});
useEffect(() => {
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages, isTyping]);
useEffect(() => {
if (selectedLead?.contactId) fetchLeadNotes(selectedLead.contactId);
else setSelectedLeadNotes([]);
}, [selectedLead]);
// ── AI CHAT — calls Gemini directly from browser ──
const searchContactByName = async (name) => {
try {
const data = await API.searchContacts(name);
return (data.contacts || [])[0] || null;
} catch(e) { return null; }
};
const pollBlandCall = async (callId, contactId, contactName) => {
const done = ['completed','no-answer','failed','busy','canceled'];
for (let i = 0; i < 60; i++) {
await new Promise(r => setTimeout(r, 5000));
try {
const data = await API.pollCall(callId);
if (done.includes(data.status)) {
if (data.status === 'completed' && (data.transcript || data.summary)) {
await API.addNote(contactId, `[AI CALL TRANSCRIPT]\n${data.transcript}\n\n[SUMMARY]: ${data.summary}`);
const aiFollowUp = await callGeminiDirect(
`[SYSTEM] Call with ${contactName} completed.\nSUMMARY: ${data.summary}\nTRANSCRIPT: ${data.transcript}\n\nExtract 1-3 action items as a JSON array: [{"title":"...","priority":"high|medium|low","contact":"${contactName}"}]. Respond ONLY with the JSON array.`,
geminiKey
);
try {
const items = JSON.parse(aiFollowUp.replace(/```json|```/g,'').trim());
setActionItems(prev => [...items.map(it => ({ ...it, id: Date.now() + Math.random(), done: false, createdAt: new Date().toISOString() })), ...prev]);
} catch(e) {}
}
return data;
}
setActiveCalls(prev => prev.map(c => c.callId === callId ? { ...c, status: data.status } : c));
} catch(e) { break; }
}
return null;
};
const callGeminiDirect = async (prompt, key) => {
const models = ['gemini-2.5-flash', 'gemini-2.0-flash-001', 'gemini-2.0-flash', 'gemini-1.5-flash'];
for (const model of models) {
try {
const res = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${key}`,
{
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: prompt }] }],
generationConfig: { maxOutputTokens: 4096, temperature: 0.85 }
})
}
);
const data = await res.json();
if (data.error) throw new Error(data.error.message);
const text = data.candidates?.[0]?.content?.parts?.[0]?.text;
if (text) { console.log(`[Gemini] ${model} OK`); return text; }
} catch (e) { console.warn(`[Gemini] ${model}:`, e.message); }
}
throw new Error('All Gemini models failed — check API key');
};
const executeCrmAction = async (action, allContacts) => {
const type = action.type;
if (type === 'ADD_TASK') {
const newTask = {
id: Date.now(),
title: action.title,
priority: action.priority || 'medium',
contact: action.contact || '',
status: 'todo',
source: 'AI Generated',
createdAt: new Date().toISOString()
};
setTasks(prev => {
const updated = [newTask, ...prev];
localStorage.setItem('moore_tasks', JSON.stringify(updated));
return updated;
});
return { ok: true, msg: `✓ Task added to Tasks board: **${action.title}**` };
}
if (type === 'CREATE_CONTACT') {
const data = await API.createContact(action.data);
if (!data.success) throw new Error(data.error);
setTimeout(fetchContacts, 1500);
return { ok: true, msg: `✓ Contact **${action.data.firstName} ${action.data.lastName||''}** added to CRM (ID: ${data.id})` };
}
if (type === 'UPDATE_CONTACT') {
let contactId = action.contactId;
if (!contactId && action.name) {
const found = await searchContactByName(action.name);
contactId = found?.id;
if (!contactId) throw new Error(`Contact "${action.name}" not found`);
}
if (action.data && Object.keys(action.data).length) {
await API.updateContact(contactId, action.data);
}
if (action.note) {
await API.addNote(contactId, action.note);
}
setTimeout(fetchContacts, 1500);
return { ok: true, msg: `✓ Contact updated` };
}
if (type === 'ADD_NOTE') {
let contactId = action.contactId;
if (!contactId && action.name) {
const found = await searchContactByName(action.name);
contactId = found?.id;
if (!contactId) throw new Error(`Contact "${action.name}" not found`);
}
await API.addNote(contactId, action.body);
return { ok: true, msg: `✓ Note added to contact` };
}
if (type === 'SEND_SMS' || type === 'SEND_EMAIL') {
let contactId = action.contactId;
let contactName = action.name || 'Contact';
if (!contactId && action.name) {
const found = await searchContactByName(action.name);
contactId = found?.id;
if (!contactId) throw new Error(`Contact "${action.name}" not found`);
}
const msgType = type === 'SEND_EMAIL' ? 'Email' : 'SMS';
const data = await API.sendMessage(contactId, msgType, action.message, action.subject);
if (!data.success) throw new Error(data.error);
return { ok: true, msg: `✓ ${msgType} sent to **${contactName}**` };
}
if (type === 'MAKE_CALL') {
let contactId = action.contactId;
let phone = action.phone;
let contactName = action.name || 'Contact';
if (!contactId && action.name) {
const found = await searchContactByName(action.name);
contactId = found?.id;
phone = phone || found?.phone;
contactName = found?.name || contactName;
}
if (!phone) throw new Error(`No phone number for "${action.name}". Add one in Contacts.`);
if (!blandKey) throw new Error('BLAND_API_KEY not set in .env');
const data = await API.makeCall(contactId, phone, contactName, action.objective || 'follow up');
if (!data.success) throw new Error(data.error);
setActiveCalls(prev => [...prev, { callId: data.callId, contactName, status: 'ringing', startedAt: new Date() }]);
pollBlandCall(data.callId, contactId, contactName).then(result => {
if (result) {
setMessages(prev => [...prev, { role: 'ai', text: `📞 Call with **${contactName}** ${result.status}. ${result.summary ? 'Summary: ' + result.summary : ''} Transcript saved to contact notes.`, type: 'call-result' }]);
setActiveCalls(prev => prev.filter(c => c.callId !== data.callId));
}
});
return { ok: true, msg: `📞 Calling **${contactName}** now — transcript will be saved to their notes when done` };
}
if (type === 'SCHEDULE') {
let contactId = action.contactId;
if (!contactId && action.name) {
const found = await searchContactByName(action.name);
contactId = found?.id;
if (!contactId) throw new Error(`Contact "${action.name}" not found`);
}
const data = await API.scheduleAppointment({ contactId, startTime: action.startTime, endTime: action.endTime, title: action.title, notes: action.notes });
if (!data.success) throw new Error(data.error);
setTimeout(fetchCalendar, 1500);
return { ok: true, msg: `✓ Appointment scheduled: **${action.title}**` };
}
if (type === 'UPDATE_OPP') {
await API.updateOpportunity(action.id, { status: action.status });
setTimeout(() => syncOpportunities(selectedPipelineId), 1200);
return { ok: true, msg: `✓ Deal status → **${action.status}**` };
}
if (type === 'MOVE_STAGE') {
await API.moveStage(action.id, action.stageId);
setTimeout(() => syncOpportunities(selectedPipelineId), 1200);
return { ok: true, msg: `✓ Deal moved to new stage` };
}
return { ok: false, msg: `Unknown action: ${type}` };
};
const approveActions = async (approvalId) => {
const actionStrs = pendingActionsRef.current[approvalId];
if (!actionStrs) return;
delete pendingActionsRef.current[approvalId];
setMessages(prev => prev.filter(m => !(m.type === 'action-approval' && m.id === approvalId)));
for (const actionStr of actionStrs) {
try {
const action = JSON.parse(actionStr);
const pendingId = Date.now() + Math.random();
setMessages(prev => [...prev, { role: 'ai', text: `⏳ Executing: ${action.type.replace(/_/g,' ')}...`, id: pendingId, type: 'action-pending' }]);
const result = await executeCrmAction(action, contacts);
setMessages(prev => prev.map(m => m.id === pendingId ? { ...m, text: result.msg, type: result.ok ? 'action-success' : 'action-error' } : m));
} catch (ae) {
setMessages(prev => [...prev, { role: 'ai', text: `✗ Action failed: ${ae.message}`, type: 'action-error' }]);
}
}
};
const cancelActions = (approvalId) => {
delete pendingActionsRef.current[approvalId];
setMessages(prev => prev.filter(m => !(m.type === 'action-approval' && m.id === approvalId)));
setMessages(prev => [...prev, { role: 'ai', text: 'Got it — actions cancelled.' }]);
};
const handleSendMessage = async (e, voiceText = null) => {
if (e) e.preventDefault();
const userMsg = voiceText || chatInput;
if (!userMsg.trim()) return;
setChatInput('');
setLiveTranscript('');
setMessages(prev => [...prev, { role: 'user', text: userMsg }]);
setIsTyping(true);
stopAudio();
const key = geminiKey;
if (!key) {
setMessages(prev => [...prev, { role: 'ai', text: 'No Gemini API key loaded. Check Settings → Test Gemini AI.' }]);
setIsTyping(false);
return;
}
try {
const leadCtx = selectedLead ? `\nFOCUSED LEAD: ${selectedLead.name} | ${selectedLead.status} | $${selectedLead.value} | Phone: ${selectedLead.phone} | Contact ID: ${selectedLead.contactId}` : '';
const now = new Date();
const systemPrompt = `You are Moore AI (Jake Moore), sharp Pipeline Architect for BigBlueCollar.
CURRENT TIME: ${now.toLocaleString()}
PIPELINE: ${crmData.stats.totalLeads} leads · $${crmData.stats.pipelineValue} · ${crmData.stats.winRate}% win rate${leadCtx}
TOP OPPORTUNITIES:
${JSON.stringify((crmData.opportunities||[]).slice(0,25).map(o=>({id:o.id,name:o.name,status:o.status,stage:o.stageName,value:o.value,contact:o.contact,contactId:o.contactId,email:o.email,phone:o.phone})))}
CAPABILITIES — output ACTION blocks at the END of your response, each on its OWN LINE:
ACTION: {"type":"CREATE_CONTACT","data":{"firstName":"","lastName":"","email":"","phone":""}}
ACTION: {"type":"UPDATE_CONTACT","name":"Contact Name","contactId":"","data":{"email":"","phone":"","tags":[]},"note":"note text"}
ACTION: {"type":"ADD_NOTE","name":"Contact Name","contactId":"","body":"note text"}
ACTION: {"type":"SEND_SMS","name":"Contact Name","contactId":"","message":"..."}
ACTION: {"type":"SEND_EMAIL","name":"Contact Name","contactId":"","subject":"...","message":"..."}
ACTION: {"type":"MAKE_CALL","name":"Contact Name","contactId":"","phone":"","objective":"what to accomplish on the call"}
ACTION: {"type":"SCHEDULE","name":"Contact Name","contactId":"","title":"Appointment title","startTime":"ISO datetime","endTime":"ISO datetime","notes":""}
ACTION: {"type":"UPDATE_OPP","id":"EXACT_ID","status":"won|lost|open"}
ACTION: {"type":"MOVE_STAGE","id":"EXACT_ID","stageId":"STAGE_ID"}
ACTION: {"type":"ADD_TASK","title":"Task title","priority":"high|medium|low","contact":"Contact Name"}
RULE: ACTION lines must start with exactly "ACTION: {" — never inline JSON inside sentences. Write your human-readable summary first, then list all ACTION lines after.
PERSONA: Sharp, decisive. Say "Yo", "Lock it in", "Done." Keep it tight. State what action you took. If you can't find a contact ID, use "name" field — system will search.`;
let responseText = await callGeminiDirect(`${systemPrompt}\n\nUser: ${userMsg}`, key);
const { actions: matches, clean: displayText } = parseActionBlocks(responseText);
if (displayText) {
setMessages(prev => [...prev, { role: 'ai', text: displayText }]);
speak(displayText);
}
if (matches.length > 0) {
const approvalId = Date.now() + Math.random();
const parsedActions = matches.map(s => { try { return JSON.parse(s); } catch(e) { return null; } }).filter(Boolean);
pendingActionsRef.current[approvalId] = matches;
setMessages(prev => [...prev, { role: 'ai', type: 'action-approval', id: approvalId, actions: parsedActions }]);
} else if (!displayText) {
setMessages(prev => [...prev, { role: 'ai', text: 'Done.' }]);
}
} catch (err) {
const errMsg = `Neural link error: ${err.message}`;
setMessages(prev => [...prev, { role: 'ai', text: errMsg }]);
speakBrowser('Error connecting to AI.');
} finally {
setIsTyping(false);
}
};
// Keep ref updated for voice recognition stale closure fix
useEffect(() => { handleSendMsgRef.current = handleSendMessage; });
// ── CRM ACTIONS ──
const updateOppStatus = async (id, status) => {
await API.updateOpportunity(id, { status });
syncOpportunities();
addNotification(`Deal status updated to ${status}`, 'success');
};
const addNote = async () => {
if (!newNote.trim() || !selectedLead?.contactId) return;
await API.addNote(selectedLead.contactId, newNote);
setSelectedLeadNotes(prev => [{ body: newNote, dateAdded: new Date().toISOString() }, ...prev]);
setNewNote('');
addNotification('Note added', 'success');
};
const createContact = async () => {
if (!contactForm.firstName) return;
await API.createContact(contactForm);
setShowContactForm(false);
setContactForm({ firstName: '', lastName: '', email: '', phone: '' });
fetchContacts();
addNotification('Contact created', 'success');
};
const addNotification = (msg, type = 'info') => {
const id = Date.now();
setNotifications(prev => [...prev, { id, msg, type }]);
setTimeout(() => setNotifications(prev => prev.filter(n => n.id !== id)), 4000);
};
const filteredOpps = useMemo(() => {
let opps = crmData.opportunities;
if (searchQuery && activeTab === 'pipeline') {
opps = opps.filter(o =>
(o.name || '').toLowerCase().includes(searchQuery.toLowerCase()) ||
(o.contact || '').toLowerCase().includes(searchQuery.toLowerCase())
);
}
return opps;
}, [crmData.opportunities, searchQuery, activeTab]);
const openTaskCount = tasks.filter(t => t.status !== 'done').length;
// ── NAV ITEMS ──
const navItems = [
{ group: 'CORE', items: [
{ id: 'dashboard', icon: 'layout-dashboard', label: 'Dashboard' },
{ id: 'tasks', icon: 'kanban', label: 'Tasks', badge: openTaskCount || null },
{ id: 'contacts', icon: 'users', label: 'Contacts' },
{ id: 'pipeline', icon: 'git-merge', label: 'Pipeline', badge: crmData.stats.totalLeads || null },
{ id: 'calendar', icon: 'calendar-days', label: 'Calendar' },
{ id: 'inbox', icon: 'inbox', label: 'Inbox', badge: conversations.filter(c => c.unreadCount > 0).length || null },
]},
{ group: 'INTELLIGENCE', items: [
{ id: 'ai-command', icon: 'brain-circuit', label: 'AI Strategy', accent: true },
{ id: 'action-items', icon: 'bell', label: 'Reminders', badge: actionItems.filter(a=>!a.done).length || null },
{ id: 'action-reference', icon: 'zap', label: 'Action Reference' },
{ id: 'analytics', icon: 'bar-chart-2', label: 'Analytics' },
]},
{ group: 'SYSTEM', items: [
{ id: 'settings', icon: 'settings', label: 'Settings' },
]}
];
// ── LOADING SCREEN ──
if (loading) return (
Moore AI
INITIALIZING NEURAL CORE...
);
// ── RENDER ──
return (
{/* NOTIFICATIONS */}
{notifications.map(n => (
{n.msg}
))}
{/* ========== SIDEBAR ========== */}
{/* Brand */}
setActiveTab('dashboard')}>
{!sidebarCollapsed && (
)}
{!sidebarCollapsed && (
)}
{/* Nav */}
{sidebarCollapsed && (
)}
{!sidebarCollapsed && (
GHL CONNECTED
Depth: 100 records
)}
{/* ========== MAIN ========== */}
{/* Header */}
Secure Node // {activeTab.toUpperCase()}
|
{new Date().toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' })}
{/* Search */}
setSearchQuery(e.target.value)} placeholder="Search pipeline..."
className="bg-slate-900/60 border border-slate-800 rounded-lg pl-8 pr-4 py-1.5 text-xs text-slate-300 focus:border-blue-500/50 outline-none w-52 placeholder:text-slate-700 transition-colors" />
{/* Sync */}
{/* Theme toggle */}
{/* Avatar */}
{ setChatOpen(true); setChatMode('open'); }}>
{/* Tab Content */}
{activeTab === 'dashboard' && }
{activeTab === 'tasks' && }
{activeTab === 'pipeline' && }
{activeTab === 'contacts' && }
{activeTab === 'calendar' && }
{activeTab === 'inbox' && }
{activeTab === 'ai-command' && { const nv = !isVoiceEnabled; setIsVoiceEnabled(nv); isVoiceEnabledRef.current = nv; stopAudio(); }} liveTranscript={liveTranscript} activeCalls={activeCalls} approveActions={approveActions} cancelActions={cancelActions} />}
{activeTab === 'analytics' && }
{activeTab === 'action-items' && }
{activeTab === 'action-reference' && }
{activeTab === 'settings' && }
{/* ========== LEAD DETAIL PANEL ========== */}
{selectedLead && (
Lead Detail
{selectedLead.name}
{selectedLead.contact}
{[
{ label: 'Value', value: fmt$(selectedLead.value) },
{ label: 'Status', value:
},
{ label: 'Stage', value: selectedLead.stageName || '—' },
{ label: 'Source', value: selectedLead.source || 'Direct' },
].map((s, i) => (
))}
Contact Info
{selectedLead.email && (
)}
{selectedLead.phone && (
)}
{selectedLead.id}
Quick Actions
{[
{ label: 'Mark Won', icon: 'trophy', color: '#10b981', action: () => updateOppStatus(selectedLead.id, 'won') },
{ label: 'Mark Lost', icon: 'x-circle', color: '#ef4444', action: () => updateOppStatus(selectedLead.id, 'lost') },
{ label: 'AI Analyze', icon: 'brain-circuit', color: '#8b5cf6', action: () => { setChatOpen(true); setChatMode('open'); handleSendMessage(null, `Analyze this lead: ${selectedLead.name}. Value: $${selectedLead.value}. Status: ${selectedLead.status}. Give me a 3-point strategic recommendation.`); } },
].map((btn, i) => (
))}
Notes
setNewNote(e.target.value)}
onKeyDown={e => e.key === 'Enter' && addNote()}
placeholder="Add a note..."
className="flex-1 bg-slate-900/60 border border-slate-800 rounded-lg px-3 py-2 text-xs text-white outline-none focus:border-blue-500/50 placeholder:text-slate-700" />
{selectedLeadNotes.length === 0 ? (
No notes yet
) : selectedLeadNotes.map((note, i) => (
{note.body}
{fmtDate(note.dateAdded || note.createdAt)}
))}
)}
{/* ========== FLOATING AI CHAT ========== */}
{chatOpen && (
{messages.map((m, i) => {
if (m.type === 'action-approval') {
return (
);
}
return (
{m.role === 'ai' && (
)}
{m.role === 'ai' && window.marked
?
: m.text
}
);
})}
{isTyping && (
{[0, 150, 300].map(d => (
))}
)}
{isListening && liveTranscript && (
🎤 {liveTranscript}
)}
)}
);
};
// =========================================================
// MOUNT
// =========================================================
const container = document.getElementById('root');
const root = ReactDOM.createRoot(container);
root.render();