import React, { useState, useEffect, useRef } from 'react'; import { createPortal } from 'react-dom'; import Groq from 'groq-sdk'; import ReactMarkdown from 'react-markdown'; import { useAuth } from '../context/AuthContext'; import { useMockStore } from '../data/mockStore'; import { MessageCircle, Send, X, Minimize2, Maximize2, Loader2, Calendar, Trophy } from 'lucide-react'; import { logger } from '../utils/logger'; import { toast } from 'sonner'; import { config } from '../config/env'; const groq = new Groq({ apiKey: config.groqApiKey, dangerouslyAllowBrowser: true // Allowed for client-side demo }); // --- CONTEXT GENERATOR (Deep, role-aware with cross-role visibility) --- const generateRoleContext = (user, store) => { const { meetings, properties, salesHistory, users, vendors, personnel, documents, projects, owners, orders, vendorInvoices } = store; const TODAY = '2026-02-18'; const todayDate = new Date(TODAY); const BASE = `You are the LynkedUp Pro AI Assistant — an intelligent operations concierge for a construction & roofing management platform. Company: LynkedUp Pro | Phone: 866-259-6533 | 20+ years experience | Free first 3 diagnostic exams | 15% off Veterans & Seniors. Today's Date: ${TODAY}. Tone: Professional, data-driven, concise. Use markdown formatting. Never fabricate data — only reference what's provided below. `; if (!user) { // Guest / unauthenticated return BASE + `ROLE: EXPERT ROOFING CONSULTANT (Guest) KNOWLEDGE BASE: - Active Leak: Water saturates insulation/decking → mold in 24-48h. Urgency: map moisture path immediately. - Missing Shingles: Underlayment exposed to UV → destroyed in days. Risk: insurance may deny for "neglect". - Hail Dents: Fractured fiberglass mat → reduces roof life 3-5 years. Document now or insurance calls it "wear and tear". - Granule Loss: Asphalt baking in Texas sun → roof failure in 12-18 months. - Ice Dams: Heat loss from attic → assess intake vents, not just patch. - Sagging: Structural fatigue or long-term water intrusion → do NOT walk on area. STRATEGY: Validate observation → explain hidden consequence of waiting → offer zero-risk solution (free inspection). HOOK: "Insurance adjusters use specific criteria. We know exactly what they look for." | "We're in Plano — project manager can stop by in 30 minutes for free moisture check." Contact: 866-259-6533`; } // ──────────────── OWNER ──────────────── if (user.role === 'OWNER') { const ownerProfile = (owners || []).find(o => o.id === user.id); const myProjects = projects.filter(p => p.ownerId === user.id); const activeProj = myProjects.filter(p => p.status === 'active'); const completedProj = myProjects.filter(p => p.status === 'completed'); const onHoldProj = myProjects.filter(p => p.status === 'on_hold'); const delayedProj = myProjects.filter(p => p.status === 'delayed'); const totalBudget = myProjects.reduce((s, p) => s + (p.budget || 0), 0); const totalSpent = myProjects.reduce((s, p) => s + (p.spent || 0), 0); const pendingInvoices = myProjects.reduce((s, p) => s + (p.invoices || []).filter(i => i.status === 'pending').reduce((is, i) => is + i.amount, 0), 0); // Vendor compliance overview (owners see all vendors) const activeVendors = vendors.filter(v => v.status === 'active'); const nonCompliant = vendors.filter(v => v.status !== 'active'); const vendorSummary = vendors.slice(0, 6).map(v => { const coiStatus = v.compliance?.coi?.status || 'unknown'; const w9Status = v.compliance?.w9?.status || 'unknown'; return ` - ${v.vendorName}: COI=${coiStatus}, W9=${w9Status}, Rating=${v.performance?.rating || 'N/A'}/5, Spend=$${(v.spend?.totalSpend || 0).toLocaleString()}`; }).join('\n'); // Documents overview const pendingDocs = documents.filter(d => d.status === 'pending_review').length; const expiredDocs = documents.filter(d => d.status === 'expired').length; const expiringSoon = documents.filter(d => { if (!d.expirationDate || d.status === 'expired') return false; const diff = (new Date(d.expirationDate) - todayDate) / (1000 * 60 * 60 * 24); return diff > 0 && diff <= 30; }).length; // Personnel overview const activeStaff = personnel.filter(p => p.status === 'active').length; const totalStaff = personnel.length; // Project details const projDetails = myProjects.map(p => { const variance = p.budget > 0 ? Math.round(((p.spent - p.budget) / p.budget) * 100) : 0; const nextMs = (p.milestones || []).find(m => m.status !== 'completed'); return ` - ${p.address} (${p.projectType}): ${p.status.toUpperCase()}, ${p.completionPercentage || 0}% done, Budget $${p.budget.toLocaleString()}, Spent $${p.spent.toLocaleString()} (${variance > 0 ? '+' : ''}${variance}%), Next: ${nextMs ? `${nextMs.name} by ${nextMs.dueDate}` : 'N/A'}`; }).join('\n'); // Upcoming milestones across all owner projects const upcomingMs = myProjects.flatMap(p => (p.milestones || []).filter(m => { const diff = (new Date(m.dueDate) - todayDate) / (1000 * 60 * 60 * 24); return diff >= -3 && diff <= 30 && m.status !== 'completed'; }).map(m => ({ ...m, address: p.address }))).sort((a, b) => new Date(a.dueDate) - new Date(b.dueDate)); const msText = upcomingMs.slice(0, 8).map(m => ` - ${m.name} @ ${m.address} — due ${m.dueDate} (${m.status})`).join('\n'); const dashSummary = ownerProfile?.dashboardSummary; const riskScore = dashSummary?.riskScore ?? ownerProfile?.riskScore ?? 'N/A'; const roiProjection = dashSummary?.roiProjection ?? 'N/A'; return BASE + `ROLE: OWNER — ${user.name} (${ownerProfile?.companyName || user.companyName || 'N/A'}) EXECUTIVE SUMMARY: - Total Projects: ${myProjects.length} (Active: ${activeProj.length}, Completed: ${completedProj.length}, On Hold: ${onHoldProj.length}, Delayed: ${delayedProj.length}) - Total Budget: $${totalBudget.toLocaleString()} | Total Spent: $${totalSpent.toLocaleString()} | Remaining: $${(totalBudget - totalSpent).toLocaleString()} - Pending Invoice Payouts: $${pendingInvoices.toLocaleString()} - Risk Score: ${riskScore}/100 (lower is better) | ROI Projection: ${roiProjection}% - Active Staff: ${activeStaff}/${totalStaff} MY PROJECTS: ${projDetails || ' None.'} UPCOMING MILESTONES (Next 30 Days): ${msText || ' None due.'} VENDOR COMPLIANCE (Cross-Role: Owner sees ALL vendors): - Active: ${activeVendors.length} | Non-Compliant: ${nonCompliant.length} ${vendorSummary} DOCUMENT STATUS: - Pending Review: ${pendingDocs} | Expired: ${expiredDocs} | Expiring Soon (30d): ${expiringSoon} INSTRUCTIONS: - You are the Business Intelligence Architect for this owner. - Answer questions about projects, budgets, vendor compliance, documents, personnel, and strategic risks. - When asked "which vendor is non-compliant?", list them by name with specific issues (COI/W9 status). - When asked about a specific project, provide detailed breakdown from the data above. - PRIORITIZE RISK ALERTS when compliance issues or over-budget projects exist.`; } // ──────────────── ADMIN ──────────────── if (user.role === 'ADMIN') { // Urgent action center const unassigned = properties.filter(p => !p.assignedAgentId && p.canvassingStatus === 'Lead').length; const hotLeadCount = properties.filter(p => p.canvassingStatus === 'Hot Lead').length; const rescheduleNeeded = meetings.filter(m => m.status === 'Cancelled' || m.status === 'Missed').length; const pendingSigs = properties.filter(p => p.pendingSignature).length; // Leaderboard const agentStats = {}; salesHistory.forEach(tx => { if (tx.status === 'closed_won') { if (!agentStats[tx.agentId]) agentStats[tx.agentId] = { revenue: 0, volume: 0 }; agentStats[tx.agentId].revenue += tx.amount; agentStats[tx.agentId].volume += 1; } }); const leaderboard = Object.entries(agentStats).map(([id, stats]) => { const agent = users.find(u => u.id === id); return { name: agent?.name || id, ...stats }; }).sort((a, b) => b.revenue - a.revenue).slice(0, 5); const lbText = leaderboard.map((s, i) => ` ${i + 1}. ${s.name} — $${s.revenue.toLocaleString()} (${s.volume} deals)`).join('\n'); // Pipeline const totalProps = properties.length; const statusBreakdown = {}; properties.forEach(p => { statusBreakdown[p.canvassingStatus] = (statusBreakdown[p.canvassingStatus] || 0) + 1; }); const pipelineText = Object.entries(statusBreakdown).map(([k, v]) => ` - ${k}: ${v}`).join('\n'); // Meetings today const todayMeetings = meetings.filter(m => m.date === TODAY); // All projects overview (admin sees everything) const allActive = projects.filter(p => p.status === 'active').length; const allBudget = projects.reduce((s, p) => s + (p.budget || 0), 0); const allSpent = projects.reduce((s, p) => s + (p.spent || 0), 0); // Vendor summary (admin sees all) const nonCompliant = vendors.filter(v => v.status !== 'active'); // Personnel const activeStaff = personnel.filter(p => p.status === 'active'); // Documents const pendingDocs = documents.filter(d => d.status === 'pending_review').length; return BASE + `ROLE: ADMIN — ${user.name} URGENT ACTION CENTER: - Unassigned Leads: ${unassigned} - Hot Leads Needing Follow-Up: ${hotLeadCount} - Reschedule Needed (Cancelled/Missed): ${rescheduleNeeded} - Pending Signatures: ${pendingSigs} - Documents Pending Review: ${pendingDocs} SALES PIPELINE (${totalProps} properties): ${pipelineText} LEADERBOARD (All-Time Closed Won): ${lbText || ' No data.'} TODAY'S MEETINGS (${todayMeetings.length}): ${todayMeetings.slice(0, 5).map(m => ` - ${m.time} with ${m.customerName} (${m.status})`).join('\n') || ' None scheduled.'} PROJECTS OVERVIEW (Cross-Role: Admin sees ALL): - Active: ${allActive}/${projects.length} | Total Budget: $${allBudget.toLocaleString()} | Spent: $${allSpent.toLocaleString()} VENDOR COMPLIANCE: - Total Vendors: ${vendors.length} | Non-Compliant: ${nonCompliant.length} ${nonCompliant.map(v => ` - ${v.vendorName}: Status=${v.status}`).join('\n') || ' All compliant.'} STAFF: ${activeStaff.length} active / ${personnel.length} total INSTRUCTIONS: - You manage the entire operation. Answer questions about pipeline, agents, revenue, meetings, and compliance. - When asked "who's the top performer?", use the leaderboard data. - When asked about a specific agent, cross-reference meetings + salesHistory. - You can also answer questions about projects, vendors, and documents (full cross-role access).`; } // ──────────────── FIELD AGENT ──────────────── if (user.role === 'FIELD_AGENT') { const myMeetings = meetings.filter(m => m.agentId === user.id).sort((a, b) => new Date(a.date) - new Date(b.date)); const todayMeetings = myMeetings.filter(m => m.date === TODAY); const upcomingMeetings = myMeetings.filter(m => new Date(m.date) >= todayDate); const agendaText = upcomingMeetings.slice(0, 8).map(m => { const isToday = m.date === TODAY; return ` - [${m.date} @ ${m.time}] ${m.status} with ${m.customerName}${isToday ? ' **(TODAY)**' : ''} — ${m.issueDescription || 'General'}`; }).join('\n'); const hotLeads = properties.filter(p => p.canvassingStatus === 'Hot Lead').slice(0, 5) .map(p => ` - ${p.propertyData?.propertyAddress || p.address || 'N/A'} (Value: $${(p.propertyData?.currentEstimatedMarketValue || 0).toLocaleString()})`).join('\n'); const goldenLeads = properties.filter(p => p.propertyData?.yearBuilt < 2000 && p.propertyData?.currentEstimatedMarketValue > 400000).slice(0, 3) .map(p => ` - ${p.propertyData.propertyAddress} (Built: ${p.propertyData.yearBuilt})`).join('\n'); const pendingSigs = properties.filter(p => p.pendingSignature || p.propertyData?.pendingSignature) .map(p => ` - ${p.propertyData?.propertyAddress || 'N/A'} (Owner: ${p.ownerData?.fullName || 'N/A'})`).join('\n'); const myWins = meetings.filter(m => m.agentId === user.id && m.status === 'Converted'); const myCompleted = meetings.filter(m => m.agentId === user.id && (m.status === 'Converted' || m.status === 'Completed')); const totalRevenue = myWins.reduce((s, m) => s + (m.dealValue || 0), 0); const winRate = myCompleted.length > 0 ? Math.round((myWins.length / myCompleted.length) * 100) : 0; return BASE + `ROLE: FIELD AGENT — ${user.name} MY AGENDA (${todayMeetings.length} today, ${upcomingMeetings.length} upcoming): ${agendaText || ' No upcoming meetings.'} HOT LEADS (Priority): ${hotLeads || ' None.'} GOLDEN LEADS (High Value / Older Homes): ${goldenLeads || ' None.'} PENDING SIGNATURES: ${pendingSigs || ' None.'} MY PERFORMANCE: - Revenue (YTD): $${totalRevenue.toLocaleString()} - Win Rate: ${winRate}% (${myWins.length} wins / ${myCompleted.length} completed) TERRITORY: ${properties.length} properties tracked in DFW. INSTRUCTIONS: - Help with agenda, leads, performance stats, and territory insights. - When asked "what's next?", show the next upcoming meeting. - When asked about leads, suggest Hot or Golden leads. - Be efficient and action-oriented.`; } // ──────────────── CONTRACTOR ──────────────── if (user.role === 'CONTRACTOR') { const myProjects = projects.filter(p => p.contractorId === user.id || p.contractorId === 'con_001'); const activeProj = myProjects.filter(p => p.status === 'active'); const totalBudget = myProjects.reduce((s, p) => s + (p.budget || 0), 0); const totalSpent = myProjects.reduce((s, p) => s + (p.spent || 0), 0); const pendingInvoices = myProjects.reduce((s, p) => s + (p.invoices || []).filter(i => i.status === 'pending').reduce((is, i) => is + i.amount, 0), 0); const projDetails = myProjects.map(p => { const nextMs = (p.milestones || []).find(m => m.status !== 'completed'); return ` - ${p.address} (${p.projectType}): ${p.status.toUpperCase()}, ${p.completionPercentage || 0}% done, Budget $${p.budget.toLocaleString()}, Spent $${p.spent.toLocaleString()}, Next: ${nextMs ? `${nextMs.name} by ${nextMs.dueDate}` : 'All complete'}`; }).join('\n'); // Upcoming milestones const upcomingMs = myProjects.flatMap(p => (p.milestones || []).filter(m => { const diff = (new Date(m.dueDate) - todayDate) / (1000 * 60 * 60 * 24); return diff >= -3 && diff <= 14 && m.status !== 'completed'; }).map(m => ({ ...m, address: p.address }))).sort((a, b) => new Date(a.dueDate) - new Date(b.dueDate)); const msText = upcomingMs.map(m => ` - ${m.name} @ ${m.address} — due ${m.dueDate} (${m.status})`).join('\n'); // My documents / compliance const myDocs = documents.filter(d => d.entityId === user.id || d.entityId === 'con_001'); const expiredDocs = myDocs.filter(d => d.status === 'expired'); // Cross-role: Contractor sees subcontractors on their projects const subIds = [...new Set(myProjects.flatMap(p => p.subcontractorIds || []))]; const subUsers = users.filter(u => subIds.includes(u.id)); // Cross-role: Contractor sees vendors delivering to their projects const vendorNames = [...new Set(myProjects.flatMap(p => (p.milestones || []).filter(m => m.vendorId).map(m => { const v = vendors.find(vv => vv.id === m.vendorId); return v ? v.vendorName : m.vendorId; })))]; return BASE + `ROLE: CONTRACTOR — ${user.name} MY PROJECTS (${myProjects.length} total, ${activeProj.length} active): ${projDetails || ' None assigned.'} CRITICAL TASKS (Next 14 Days): ${msText || ' No critical tasks due.'} FINANCIALS: - Total Budget: $${totalBudget.toLocaleString()} | Spent: $${totalSpent.toLocaleString()} - Pending Invoice Payouts: $${pendingInvoices.toLocaleString()} COMPLIANCE: ${expiredDocs.length > 0 ? `- WARNING: ${expiredDocs.length} expired documents need renewal: ${expiredDocs.map(d => d.name).join(', ')}` : '- All documents current.'} CROSS-ROLE VISIBILITY: - My Subcontractors: ${subUsers.map(s => s.name).join(', ') || 'None assigned'} - Vendors on My Projects: ${vendorNames.join(', ') || 'None'} INSTRUCTIONS: - Help track project progress, deadlines, invoices, and crew coordination. - Only discuss THIS contractor's projects — never reveal other contractors' data. - When asked about a subcontractor, reference their tasks on shared projects.`; } // ──────────────── SUBCONTRACTOR ──────────────── if (user.role === 'SUBCONTRACTOR') { const myId = user.id || 'sub_001'; const myProjects = projects.filter(p => (p.subcontractorIds || []).includes(myId)); // Only milestones assigned to this sub const myTasks = myProjects.flatMap(p => (p.milestones || []).filter(m => m.assignedTo === myId).map(m => ({ ...m, address: p.address, projectId: p.id }))); const activeTasks = myTasks.filter(t => t.status === 'in_progress'); const pendingTasks = myTasks.filter(t => t.status === 'pending'); const completedTasks = myTasks.filter(t => t.status === 'completed'); const taskDetails = myTasks.filter(t => t.status !== 'completed').map(t => ` - ${t.name} @ ${t.address} — ${t.status} (due ${t.dueDate})`).join('\n'); // Pending payouts (from invoices on projects I'm on) const pendingPay = myProjects.reduce((s, p) => s + (p.invoices || []).filter(i => i.status === 'pending' && (i.payeeTo === myId || i.payeeTo === 'sub_001')).reduce((is, i) => is + i.amount, 0), 0); // My compliance docs const myDocs = documents.filter(d => d.entityId === myId); const expiredDocs = myDocs.filter(d => d.status === 'expired'); // Cross-role: Sub sees the contractor managing their projects const contractorIds = [...new Set(myProjects.map(p => p.contractorId))]; const contractorUsers = users.filter(u => contractorIds.includes(u.id)); return BASE + `ROLE: SUBCONTRACTOR — ${user.name} MY TASKS (${activeTasks.length} active, ${pendingTasks.length} pending, ${completedTasks.length} completed): ${taskDetails || ' No open tasks.'} ASSIGNED PROJECTS: ${myProjects.map(p => `${p.address} (${p.status})`).join(', ') || 'None'} FINANCIALS: - Pending Payouts: $${pendingPay.toLocaleString()} COMPLIANCE: ${expiredDocs.length > 0 ? `- WARNING: ${expiredDocs.length} expired docs: ${expiredDocs.map(d => d.name).join(', ')}` : '- All documents current.'} CROSS-ROLE VISIBILITY: - My General Contractor: ${contractorUsers.map(c => c.name).join(', ') || 'N/A'} INSTRUCTIONS: - Help track task progress, deadlines, and payouts. - Only show THIS subcontractor's tasks — never reveal other subs' data or full project budgets. - When asked "what's next?", show the nearest pending/in-progress task by due date.`; } // ──────────────── VENDOR ──────────────── if (user.role === 'VENDOR') { // Map user to vendor record const vendorMap = { 'ven_001': 'v3', 'ven_002': 'v1' }; const vendorId = vendorMap[user.id] || 'v1'; const myVendor = vendors.find(v => v.id === vendorId) || vendors[0]; const coiStatus = myVendor.compliance?.coi?.status || 'unknown'; const coiExpiry = myVendor.compliance?.coi?.expirationDate || 'N/A'; const w9Status = myVendor.compliance?.w9?.status || 'unknown'; const rating = myVendor.performance?.rating || 'N/A'; const onTime = myVendor.performance?.onTimeDelivery || 'N/A'; const defectRate = myVendor.performance?.defectRate || 'N/A'; // Orders for this vendor const myOrders = (orders || []).filter(o => o.vendorId === vendorId); const activeOrders = myOrders.filter(o => o.status === 'shipped' || o.status === 'processing'); const orderText = myOrders.slice(0, 5).map(o => ` - PO#${o.id}: ${o.items?.map(i => i.name).join(', ') || 'Items'} — ${o.status.toUpperCase()}, $${(o.total || 0).toLocaleString()}, ETA: ${o.estimatedDelivery || 'N/A'}`).join('\n'); // Invoices for this vendor const myInvoices = (vendorInvoices || []).filter(i => i.vendorId === vendorId); const pendingInv = myInvoices.filter(i => i.status === 'pending'); const totalPending = pendingInv.reduce((s, i) => s + (i.amount || 0), 0); const invText = myInvoices.slice(0, 5).map(i => ` - INV#${i.id}: $${(i.amount || 0).toLocaleString()} — ${i.status.toUpperCase()} (${i.dueDate || 'N/A'})`).join('\n'); // Spend const totalSpend = myVendor.spend?.totalSpend || 0; const ytdSpend = myVendor.spend?.ytdSpend || totalSpend; return BASE + `ROLE: VENDOR — ${user.name} (${myVendor.vendorName}) COMPANY PROFILE: - Category: ${myVendor.category || 'N/A'} | Status: ${myVendor.status || 'N/A'} - Rating: ${rating}/5.0 | On-Time Delivery: ${onTime}% | Defect Rate: ${defectRate}% COMPLIANCE: - COI: ${coiStatus} (expires ${coiExpiry}) - W-9: ${w9Status} ${coiStatus === 'expired' || w9Status === 'expired' ? '⚠️ WARNING: Expired documents — upload renewed versions to avoid order holds.' : ''} ORDERS (${myOrders.length} total, ${activeOrders.length} active): ${orderText || ' No orders.'} INVOICES: - Pending: ${pendingInv.length} ($${totalPending.toLocaleString()}) ${invText || ' No invoices.'} SPEND: - Total All-Time: $${totalSpend.toLocaleString()} - YTD: $${ytdSpend.toLocaleString()} INSTRUCTIONS: - Help track orders, invoices, compliance status, and deliveries. - Only show THIS vendor's data — never reveal other vendors' info or full project financials. - When asked about compliance, explain exactly what needs renewal and urgency. - When asked "what orders are pending?", list active orders with ETAs.`; } // ──────────────── CUSTOMER ──────────────── if (user.role === 'CUSTOMER') { const myProperty = properties.find(p => p.propertyData?.propertyId === user.propertyId) || properties.find(p => p.ownerData?.fullName === user.name); let propDetails = ' Property details not found.'; let tips = 'Standard seasonal maintenance recommended.'; let warranty = 'Standard Manufacturer Warranty (check status with your rep)'; if (myProperty) { const pd = myProperty.propertyData; propDetails = ` - Address: ${pd.propertyAddress} - Roof Type: ${pd.roofMaterial} | Condition: ${pd.roofCondition} - Storm Risk: ${pd.neighborhoodRating}/10 - Est. Value: $${pd.currentEstimatedMarketValue?.toLocaleString() || 'N/A'} - Last Inspection: ${pd.lastInspectionDate || 'Unknown'}`; if (pd.roofCondition === 'Needs Repair') tips = 'URGENT: Schedule an inspection immediately to prevent water damage.'; else if (pd.treeCoverage > 50) tips = 'Consider trimming overhanging branches to protect shingles.'; if (pd.yearBuilt > 2015) warranty = 'Active Workmanship Warranty (Valid until 2030)'; } const myMeetings = meetings.filter(m => m.customerId === user.id || m.customerName === user.name) .sort((a, b) => new Date(b.date) - new Date(a.date)); const meetingText = myMeetings.slice(0, 5).map(m => { const isFuture = new Date(m.date) > todayDate; return ` - [${m.date}] ${isFuture ? 'UPCOMING' : 'COMPLETED'} (${m.status}) — ${m.notes || 'No notes'}`; }).join('\n'); const totalSpend = myMeetings.reduce((s, m) => s + (m.dealValue || 0), 0); return BASE + `ROLE: HOMEOWNER — ${user.name} MY PROPERTY: ${propDetails} SERVICE HISTORY: ${meetingText || ' No records found.'} FINANCIALS: - Total Spend: $${totalSpend.toLocaleString()} - Warranty: ${warranty} ADVICE: ${tips} INSTRUCTIONS: - Answer questions about "my roof", "my appointment", "warranty", or "spending". - Be polite, reassuring, and helpful. - Offer to schedule a free inspection if concerns arise: 866-259-6533.`; } // ──────────────── FALLBACK ──────────────── return BASE + `ROLE: ${user.role} — ${user.name} You are logged in but your role context is being set up. I can help with general questions about LynkedUp Pro.`; }; const Chatbot = (props) => { const { user, isAuthenticated } = useAuth(); // Pull ALL data needed for context generation const storeData = useMockStore(); // Destructure specifically for local usage if needed, but we pass the whole object to context functions const { addMeeting, meetings, properties, salesHistory, users, vendors, personnel, documents, projects, owners, orders, vendorInvoices } = storeData; const [isOpen, setIsOpen] = useState(false); const [isMinimized, setIsMinimized] = useState(false); const [input, setInput] = useState(''); const [isLoading, setIsLoading] = useState(false); const [isDemoMode, setIsDemoMode] = useState(false); const [messages, setMessages] = useState([ { role: 'assistant', content: 'Hello! Welcome to LynkedUp Pro. How can I help you with your roofing needs today?' } ]); const messagesEndRef = useRef(null); // Initial check for API Key useEffect(() => { if (config.isDemoMode(config.groqApiKey)) { setIsDemoMode(true); setMessages(prev => [...prev, { role: 'system', content: '⚠️ Demo Mode: Default API Key detected. Responses will be simulated.' }]); } }, []); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); // Update greeting and context when user changes useEffect(() => { let greeting = 'Hello! Welcome to LynkedUp Pro. How can I help you with your roofing needs today?'; if (user) { greeting = `Welcome back, ${user.name}! `; if (user.role === 'ADMIN') { greeting += "Operations HQ is live. Ask me about pipeline, agents, compliance, or revenue."; } else if (user.role === 'FIELD_AGENT') { greeting += "I have your territory data and agenda ready. What do you need?"; } else if (user.role === 'CUSTOMER') { greeting += "I have your property records open. How can I assist?"; } else if (user.role === 'OWNER') { greeting += "Executive Dashboard is live. Ask me about projects, budgets, vendors, or compliance."; } else if (user.role === 'CONTRACTOR') { greeting += "Command Center loaded. Need project status, deadlines, or invoice info?"; } else if (user.role === 'SUBCONTRACTOR') { greeting += "Your tasks are loaded. Ask about deadlines, payouts, or compliance."; } else if (user.role === 'VENDOR') { greeting += "Your orders and compliance data are ready. Ask about invoices, deliveries, or documents."; } } setMessages([{ role: 'assistant', content: greeting }]); }, [user]); const handleSend = async () => { if (!input.trim()) return; const originalInput = input; // Backup input const newMessages = [...messages, { role: 'user', content: input }]; setMessages(newMessages); setInput(''); setIsLoading(true); try { if (isDemoMode) { // ... existing demo logic ... (Simulated for brevity, keeping existing structure if needed, but replacing with smart demo response) setTimeout(() => { const responses = [ "I can help with that! At LynkedUp Pro, we've been in business for 20+ years. I can't access real-time data in demo mode, but I can help you navigate.", "Please call us at 866-259-6533 for a real quote!", "Would you like to schedule a callback? Please log in first." ]; const randomResponse = responses[Math.floor(Math.random() * responses.length)]; setMessages(prev => [...prev, { role: 'assistant', content: randomResponse }]); setIsLoading(false); }, 1000); return; } // --- REAL AI REQUEST --- // --- Generate deep, role-aware context --- // 1. Build Dynamic Context let systemContext = generateRoleContext(user, storeData); // 2. Call Groq const chatCompletion = await groq.chat.completions.create({ messages: [ { role: 'system', content: systemContext }, ...newMessages ], model: 'qwen/qwen3-32b', temperature: 0.7, max_tokens: 1024, }); let responseContent = chatCompletion.choices[0]?.message?.content || "I'm having trouble connecting right now."; // Filter out blocks if present responseContent = responseContent.replace(/[\s\S]*?<\/think>/gi, '').trim(); // Check for Function Call Pattern (Legacy Callback) const callbackMatch = responseContent.match(/:::CALLBACK_REQUEST({.*?}):::/); if (callbackMatch) { const jsonStr = callbackMatch[1]; try { const data = JSON.parse(jsonStr); // Create Meeting / Callback const mockDate = new Date(); mockDate.setDate(mockDate.getDate() + 1); // Next day default const meeting = { agentId: user ? user.id : 'e1', // Use logged in user ID customerName: data.name || 'Potential Client', propertyId: 'Callback Request', date: mockDate.toISOString().split('T')[0], time: data.time || '10:00', status: 'Pending', notes: `Callback requested for ${data.phone}` }; addMeeting(meeting); // Clean the response to hide the JSON responseContent = responseContent.replace(callbackMatch[0], '').trim(); if (!responseContent) responseContent = "I've successfully scheduled a callback for you! We will contact you shortly."; else responseContent += "\n\n✅ Callback scheduled successfully!"; } catch (e) { console.error("Failed to parse callback JSON", e); } } setMessages(prev => [...prev, { role: 'assistant', content: responseContent }]); } catch (error) { logger.error("Groq API Error", error); setInput(originalInput); // <--- RESTORE INPUT ON ERROR let errorMsg = "Sorry, I'm having trouble connecting to the Groq server."; if (error.message) { if (error.message.includes('401')) { errorMsg += " (Error 401: Invalid API Key)"; toast.error("AI Configuration Error", { description: "Invalid API Key." }); } else if (error.message.includes('404')) { errorMsg += " (Error 404: Model not found)"; toast.error("AI Model Error", { description: "Model not found." }); } else errorMsg += ` (${error.message})`; } setMessages(prev => [...prev, { role: 'assistant', content: errorMsg }]); } finally { setIsLoading(false); } }; const handleKeyPress = (e) => { if (e.key === 'Enter') handleSend(); }; // --- INLINE MODE RENDER --- if (props.inline) { return (
{/* Header (Simplified) */}

LynkedUp AI Concierge

{/* Messages */}
{messages.map((msg, i) => (
{msg.role === 'system' ? ( {msg.content} ) : (
)}
))} {isLoading && (
)}
{/* Input */}
setInput(e.target.value)} onKeyPress={handleKeyPress} placeholder="Type a message..." className="flex-1 bg-transparent text-sm focus:outline-none text-slate-900 dark:text-white" />

Powered by Groq LPU™ & Qwen

); } // --- WIDGET MODE (DEFAULT) --- if (!isOpen) { return createPortal( , document.body ); } return createPortal(
{/* Header */}
!isMinimized && setIsMinimized(!isMinimized)}>

LynkedUp AI

{/* Messages */} {!isMinimized && ( <>
, document.body ); }; export default Chatbot;