import React, { useState, useRef, useEffect } from 'react'; import axios from 'axios'; import { Send, ChevronDown, ChevronRight, CheckCircle2, Circle } from 'lucide-react'; import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, LineChart, Line, PieChart, Pie, Cell } from 'recharts'; const guardrails = ["who is", "poem", "joke", "weather", "translate"]; const suggestions = [ "Top billed products", "Trace billing doc 91150187", "Explain transaction 91150187", "Find fraud", "Incomplete flows", "Customer revenue", "Journal entries" ]; const ChatPanel = ({ cyInstance, sendRef, width }) => { const [query, setQuery] = useState(""); const [messages, setMessages] = useState([]); const [isThinking, setIsThinking] = useState(false); const endRef = useRef(null); useEffect(() => { if (document.startViewTransition) { document.startViewTransition(() => { endRef.current?.scrollIntoView({ behavior: 'smooth' }); }); } else { endRef.current?.scrollIntoView({ behavior: 'smooth' }); } }, [messages, isThinking]); const getBadgeColor = (intent) => { if (intent.includes('Trace')) return '#3b82f6'; if (intent.includes('Error') || intent.includes('Anomaly')) return '#f87171'; if (intent.includes('Aggregat')) return '#22c55e'; if (intent.includes('Lookup')) return '#f59e0b'; if (intent.includes('Scope')) return '#f87171'; if (intent.includes('Fraud')) return '#ef4444'; if (intent.includes('Explain')) return '#8b5cf6'; return '#1a1a1a'; }; // Backend API URL (Dynamic for Vercel/Render) const API_BASE = '/api'; const handleSend = async (overrideQ) => { const text = overrideQ || query; if (!text.trim() || isThinking) return; setQuery(""); setMessages(prev => [...prev, { role: 'user', content: text }]); setIsThinking(true); const qlow = text.toLowerCase(); // Guardrails if (guardrails.some(g => qlow.includes(g))) { setTimeout(() => { setIsThinking(false); setMessages(prev => [...prev, { role: 'system', intent: '🚫 Out of Scope', intentColor: '#f87171', content: "This system only answers questions about the Order to Cash dataset. Please ask about orders, deliveries, billing, or payments." }]); }, 800); return; } try { const res = await axios.post(`${API_BASE}/query`, { query: text }); const data = res.data; // Update Graph if (cyInstance) { if (data.highlights && data.highlights.length > 0) { cyInstance.elements().addClass('dimmed').removeClass('highlighted'); let collection = cyInstance.collection(); data.highlights.forEach(h => { cyInstance.nodes().forEach(n => { if (n.data('id') === h || n.data('label').toLowerCase().includes(h.toLowerCase().replace('s',''))) { n.removeClass('dimmed').addClass('highlighted'); collection = collection.union(n); n.connectedEdges().removeClass('dimmed'); } }); }); if (collection.length > 0) { cyInstance.animate({ fit: { eles: collection, padding: 50 } }, { duration: 800 }); } } else { // STEP 5: Clear highlights if not a graph intent cyInstance.elements().removeClass('dimmed').removeClass('highlighted'); } } const botMsg = { role: 'system', intent: data.intent || "🔍 Lookup", intentColor: getBadgeColor(data.intent || ""), content: data.answer || "Found matching records from your ERP system.", result: data.result, type: data.type }; setIsThinking(false); setMessages(prev => [...prev, botMsg]); } catch (error) { // Offline mock data setTimeout(() => { setIsThinking(false); if(qlow.includes("trace") || qlow.includes("91150187")) { if (cyInstance) { cyInstance.elements().addClass('dimmed').removeClass('highlighted'); let c = cyInstance.collection(); ['91150187', '80737721', '740506', '9400635958', '310000108'].forEach(id => { const n = cyInstance.getElementById(id); if(n.length) { n.removeClass('dimmed').addClass('highlighted'); n.connectedEdges().removeClass('dimmed'); c = c.union(n); } }); cyInstance.animate({ fit: { eles: c, padding: 50 } }, { duration: 800 }); } setMessages(prev => [...prev, { role: 'system', intent: '🔗 Flow Trace', intentColor: '#3b82f6', content: "Tracing billing document **91150187**. It originates from Sales Order 740506 via Delivery 80737721 and posts to Journal Entry 9400635958.", planSteps: ["Lookup BillingDoc 91150187", "Traverse to predecessor (Delivery)", "Traverse to root (Order)"], sql: "SELECT * FROM billing_document_headers WHERE billingDocument='91150187'" }]); } else { setMessages(prev => [...prev, { role: 'system', intent: '⚠️ API Error', intentColor: '#f87171', content: "Backend API is unreachable. Showing mocked offline response." }]); } }, 1000); } }; // Expose handleSend to parent via sendRef (for GraphPanel Explain button) useEffect(() => { if (sendRef) sendRef.current = handleSend; }, [sendRef]); // Reasoning and Table components removed per user request for a cleaner natural language UI. const renderFraudList = (data) => { if (!data || data.length === 0) return null; return (
{data.slice(0, 10).map((item, i) => (
🔴
{item.id || item.salesOrder || item.billingDocument || item.journalEntry} {item.issue}
))} {data.length > 10 &&
...and {data.length - 10} more
}
); }; const renderFlowTrace = (data) => { if (!data || data.length === 0) return null; const r = data[0]; // Take the first row for the main flow const steps = [ { id: r.salesOrder, label: 'Order', icon: '📦', color: '#3b82f6' }, { id: r.deliveryDocument, label: 'Delivery', icon: '🚚', color: '#22c55e' }, { id: r.billingDocument, label: 'Billing', icon: '📄', color: '#f59e0b' }, { id: r.journalEntry, label: 'Journal', icon: '📒', color: '#8b5cf6' }, { id: r.paymentDoc, label: 'Payment', icon: '💰', color: '#f87171' } ].filter(s => s.id && s.id !== 'nan' && String(s.id).toLowerCase() !== 'null'); return (
Transaction Stream
{steps.map((step, i) => (
{ if (cyInstance) { const n = cyInstance.getElementById(String(step.id)); if (n.length) { cyInstance.elements().addClass('dimmed').removeClass('highlighted'); n.removeClass('dimmed').addClass('highlighted'); n.connectedEdges().removeClass('dimmed').addClass('highlighted'); cyInstance.animate({ fit: { eles: n.union(n.connectedEdges()), padding: 50 } }, { duration: 600 }); } } }} > {step.icon}
{step.label}
{step.id}
{i < steps.length - 1 && (
)}
))}
); }; const COLORS = ['#3b82f6', '#22c55e', '#f59e0b', '#f87171', '#8b5cf6']; const renderDynamicChart = (type, data) => { if (!data || data.length === 0 || type === 'table' || type === 'flow') return null; const keys = Object.keys(data[0]); if (keys.length < 2) return null; const xAxisKey = keys[0]; const yAxisKey = keys[1]; if (type === 'bar') { return (
); } if (type === 'distribution') { return (
{data.map((entry, index) => )}
); } return null; }; const formatText = (txt) => { const parts = txt.split(/(\*\*.*?\*\*)/g); return parts.map((p, i) => { if (p.startsWith('**') && p.endsWith('**')) return {p.slice(2,-2)}; return p; }); }; return (
{/* Header */}
Chat with Graph
Order to Cash
{/* Intro */}
D
Dodge AI
Graph Agent
Hi! I can help you analyze the Order to Cash process.
{/* Suggestion Chips */}
{suggestions.map(s => ( ))}
{/* Messages */}
{messages.map((m, i) => ( m.role === 'user' ? (
{m.content}
) : (
D
{m.intent}
{formatText(m.content)}
{(m.type === 'flow' || m.type === 'explanation') && m.result && renderFlowTrace(m.result)} {m.result && m.result.length > 0 && m.type && renderDynamicChart(m.type, m.result)} {m.type === 'fraud' && m.result && renderFraudList(m.result)}
) ))} {isThinking && (
D
)}
{/* Footer Input */}
{isThinking ? 'Dodge AI is thinking...' : 'Dodge AI is awaiting instructions'}