import React, { useState, useRef, useEffect } from 'react'; import { ChatMessage, User } from '../types'; import { Send, Bot, User as UserIcon, X, Maximize2, Minimize2, Loader2, Sparkles } from 'lucide-react'; import ReactMarkdown from 'react-markdown'; import { motion, AnimatePresence } from 'framer-motion'; interface LocalAssistantProps { currentUser: User; projectContext?: any; } const LocalAssistant: React.FC = ({ currentUser, projectContext }) => { const [isOpen, setIsOpen] = useState(false); const [isMinimized, setIsMinimized] = useState(false); const [input, setInput] = useState(''); const [messages, setMessages] = useState([ { role: 'model', parts: [{ text: "Hello! I'm BuildTrack Local Assistant. I can summarize project data without using any external AI service." }] } ]); const [isLoading, setIsLoading] = useState(false); const scrollRef = useRef(null); useEffect(() => { if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }, [messages]); const buildLocalResponse = (question: string) => { const project = projectContext; const normalized = question.toLowerCase(); if (!project) { return "No project is selected yet. Open a project and I can summarize progress, risks, BOQ status, bills, documents, and pending suggestions from the local project data."; } const boq = project.boq || []; const bills = project.bills || []; const liabilities = project.liabilities || []; const dprs = project.dprs || []; const documents = project.documents || []; const suggestions = project.aiSuggestions || []; const planned = boq.reduce((sum: number, item: any) => sum + (item.plannedQty || 0) * (item.rate || 0), 0); const executed = boq.reduce((sum: number, item: any) => sum + (item.executedQty || 0) * (item.rate || 0), 0); const progress = planned > 0 ? ((executed / planned) * 100).toFixed(1) : "0.0"; const pendingHigh = boq.filter((item: any) => item.priority === 'HIGH' && (item.executedQty || 0) < (item.plannedQty || 0)); const money = (value: number) => new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format(value || 0); if (normalized.includes('risk')) { const risk = project.riskAssessment; if (risk?.risks?.length) { return `Current local risk score is **${risk.overallRiskScore}/100**.\n\n${risk.risks.slice(0, 3).map((r: any) => `- **${r.category} (${r.impact})**: ${r.description} Mitigation: ${r.mitigation}`).join('\n')}`; } return `No saved risk assessment is available yet. Based on local project data, I would first review ${pendingHigh.length} high-priority pending BOQ item(s), ${liabilities.length} liability record(s), and upcoming milestones.`; } if (normalized.includes('bill') || normalized.includes('finance') || normalized.includes('money')) { const clientBilled = bills.filter((b: any) => b.type === 'CLIENT_RA').reduce((sum: number, b: any) => sum + (b.amount || 0), 0); const expenses = bills.filter((b: any) => b.type !== 'CLIENT_RA').reduce((sum: number, b: any) => sum + (b.amount || 0), 0); const liabilityTotal = liabilities.reduce((sum: number, l: any) => sum + (l.amount || 0), 0); return `Financial snapshot:\n\n- Client billed: **${money(clientBilled)}**\n- Recorded expenses: **${money(expenses)}**\n- Open liabilities: **${money(liabilityTotal)}**\n- Executed value: **${money(executed)}**`; } if (normalized.includes('document') || normalized.includes('file')) { return `This project has **${documents.length}** document(s). Pending local suggestions: **${suggestions.filter((s: any) => s.status === 'PENDING').length}**. Use the document manager's scan action to create local suggestions from file names and available text.`; } if (normalized.includes('boq') || normalized.includes('progress') || normalized.includes('status')) { const pendingText = pendingHigh.length ? pendingHigh.slice(0, 3).map((item: any) => `- ${item.description}: ${(item.plannedQty || 0) - (item.executedQty || 0)} ${item.unit} pending`).join('\n') : "- No high-priority BOQ items are pending."; return `Project status for **${project.name}**:\n\n- Progress: **${progress}%** by value\n- Planned value: **${money(planned)}**\n- Executed value: **${money(executed)}**\n- DPR entries: **${dprs.length}**\n\n${pendingText}`; } return `I am running fully locally for ${currentUser.name} (${currentUser.role}). For **${project.name}**, I can summarize progress, BOQ, risks, bills, DPRs, documents, and pending suggestions using data already stored in this app.`; }; const handleSend = async () => { if (!input.trim() || isLoading) return; const question = input.trim(); const userMessage: ChatMessage = { role: 'user', parts: [{ text: question }] }; setMessages(prev => [...prev, userMessage]); setInput(''); setIsLoading(true); try { await new Promise(resolve => setTimeout(resolve, 250)); const modelResponse: ChatMessage = { role: 'model', parts: [{ text: buildLocalResponse(question) }] }; setMessages(prev => [...prev, modelResponse]); } catch (error) { console.error("Local assistant error:", error); setMessages(prev => [...prev, { role: 'model', parts: [{ text: "I could not process that local request. Please try a shorter question about progress, risk, bills, documents, or BOQ." }] }]); } finally { setIsLoading(false); } }; return (
{isOpen && ( {/* Header */}

Local Assistant

Local & Ready
{!isMinimized && ( <> {/* Messages */}
{messages.map((m, i) => (
{m.role === 'user' ? : }

{children}

, ul: ({ children }) =>
    {children}
, ol: ({ children }) =>
    {children}
, }} > {m.parts[0].text}
))} {isLoading && (
)}
{/* Input */}
setInput(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleSend()} className="w-full pl-4 pr-12 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-600 outline-none text-sm transition-all" />

Local assistant uses project data stored in this app.

)}
)}
{ if (!isOpen) setIsOpen(true); setIsMinimized(false); }} className={`w-14 h-14 rounded-full flex items-center justify-center shadow-2xl transition-colors ${ isOpen ? 'bg-white text-blue-600 border border-blue-100' : 'bg-blue-600 text-white' }`} >
); }; export default LocalAssistant;