import React, { useState, useEffect, useRef } from 'react'; import axios from 'axios'; import { motion, AnimatePresence } from 'framer-motion'; import { Send, Bot, User, Info, Wifi, AlertTriangle, CheckCircle2 } from 'lucide-react'; import ReactMarkdown from 'react-markdown'; const API_BASE = import.meta.env.VITE_API_BASE || ''; const OrchestrationLog = ({ steps }) => { if (!steps || !Array.isArray(steps)) return null; return (
Agent Reasoning & Path
); }; const ResultCards = ({ data }) => { if (!data || !Array.isArray(data)) return null; return (
{data.map((item, idx) => { const status = item.status || (item.anomaly === 1 ? 'Critical' : 'Normal'); let pillClass = 'pill-green'; let Icon = CheckCircle2; if (status === 'Critical') { pillClass = 'pill-red'; Icon = AlertTriangle; } else if (status === 'Warning') { pillClass = 'pill-yellow'; Icon = Info; } else if (status === 'No Data') { pillClass = 'pill-white'; Icon = Wifi; } return (
{item.name || item.asset || item.asset_name || 'Asset'} {status}
{item.value !== undefined && Value: {item.value}} {item.latest_mse !== undefined && MSE: {item.latest_mse.toFixed(5)}} {item.mse !== undefined && MSE: {item.mse.toFixed(5)}}
); })}
); }; const TrendChartRenderer = ({ chart }) => { if (!chart || !chart.series || chart.series.length === 0) return null; const [hoveredPoint, setHoveredPoint] = useState(null); // Dimensions const width = 520; const height = 240; const paddingLeft = 45; const paddingRight = 20; const paddingTop = 25; const paddingBottom = 35; // Get all values to compute range let allValues = []; chart.series.forEach(s => { if (s.points) { s.points.forEach(p => allValues.push(p.value)); } }); if (allValues.length === 0) return null; let minVal = Math.min(...allValues); let maxVal = Math.max(...allValues); if (minVal === maxVal) { minVal = minVal - 1; maxVal = maxVal + 1; } const valRange = maxVal - minVal; const yMin = minVal - valRange * 0.1; const yMax = maxVal + valRange * 0.1; // Helper functions for scaling const getX = (index, total) => { if (total <= 1) return paddingLeft + (width - paddingLeft - paddingRight) / 2; return paddingLeft + (index / (total - 1)) * (width - paddingLeft - paddingRight); }; const getY = (value) => { return paddingTop + (1 - (value - yMin) / (yMax - yMin)) * (height - paddingTop - paddingBottom); }; // Gridlines (Y-axis) const gridCount = 4; const gridLines = []; for (let i = 0; i <= gridCount; i++) { gridLines.push(yMin + (i / gridCount) * (yMax - yMin)); } // X-axis labels (render up to 5 labels to avoid clutter) const maxLabelCount = 5; const totalPoints = chart.series[0]?.points?.length || 0; const labelIndices = []; if (totalPoints > 0) { const step = Math.max(1, Math.floor(totalPoints / maxLabelCount)); for (let i = 0; i < totalPoints; i += step) { labelIndices.push(i); } if (labelIndices[labelIndices.length - 1] !== totalPoints - 1 && totalPoints > 1) { labelIndices.push(totalPoints - 1); } } const isLine = chart.chart_type === 'line'; return (

{chart.title || 'Telemetry Trend'}

{chart.y_label || 'Values'} over {chart.x_label || 'Time'}

{chart.series.map((s, sIdx) => ( ))} {/* Horizontal Gridlines & Y Labels */} {gridLines.map((val, i) => { const y = getY(val); return ( {val.toFixed(1)} ); })} {/* X Axis Base Line */} {/* X Axis Labels */} {chart.series[0]?.points?.map((p, pIdx) => { if (!labelIndices.includes(pIdx)) return null; const x = getX(pIdx, totalPoints); return ( {p.label} ); })} {/* Plot Series */} {chart.series.map((s, sIdx) => { if (!s.points || s.points.length === 0) return null; const pointsCount = s.points.length; // 1. Line Chart Path if (isLine) { let pathD = ''; s.points.forEach((p, pIdx) => { const x = getX(pIdx, pointsCount); const y = getY(p.value); pathD += `${pIdx === 0 ? 'M' : 'L'} ${x} ${y}`; }); const bottomY = height - paddingBottom; const firstX = getX(0, pointsCount); const lastX = getX(pointsCount - 1, pointsCount); const areaD = `${pathD} L ${lastX} ${bottomY} L ${firstX} ${bottomY} Z`; return ( {/* Shaded Area Under Line */} {/* Glow Stroke Line */} {/* Data Points */} {s.points.map((p, pIdx) => { const x = getX(pIdx, pointsCount); const y = getY(p.value); return ( { const rect = e.target.getBoundingClientRect(); const containerRect = e.target.ownerDocument.querySelector('.trend-chart-svg-container').getBoundingClientRect(); setHoveredPoint({ label: p.label, value: p.value, seriesLabel: s.label, color: s.color, x: rect.left - containerRect.left + rect.width / 2, y: rect.top - containerRect.top }); }} onMouseLeave={() => setHoveredPoint(null)} /> ); })} ); } else { // 2. Bar Chart Columns const barWidth = Math.max(4, ((width - paddingLeft - paddingRight) / pointsCount) * 0.55); const spacing = ((width - paddingLeft - paddingRight) / pointsCount) * 0.45; return ( {s.points.map((p, pIdx) => { const x = paddingLeft + pIdx * (barWidth + spacing) + spacing / 2; const y = getY(p.value); const bottomY = height - paddingBottom; const barHeight = Math.max(2, bottomY - y); return ( { const rect = e.target.getBoundingClientRect(); const containerRect = e.target.ownerDocument.querySelector('.trend-chart-svg-container').getBoundingClientRect(); setHoveredPoint({ label: p.label, value: p.value, seriesLabel: s.label, color: s.color, x: rect.left - containerRect.left + rect.width / 2, y: rect.top - containerRect.top }); }} onMouseLeave={() => setHoveredPoint(null)} /> ); })} ); } })} {/* Tooltip Overlay */} {hoveredPoint && (
{hoveredPoint.seriesLabel} {hoveredPoint.value.toFixed(2)} {hoveredPoint.label}
)}
{/* Legend */} {chart.series.length > 0 && (
{chart.series.map((s, sIdx) => (
{s.label}
))}
)}
); }; const MessageBubble = ({ message }) => { const isAI = message.sender === 'ai'; const results = message.result; const analysis = message.analysis; const trendChart = analysis?.trend_chart || (analysis?.trend_charts && analysis.trend_charts[0]); return (
{isAI ? : }
{isAI ? (
{message.text} {message.orchestration && } {trendChart && } {results && }
) : (

{message.text}

)}
); }; const ChatPanel = () => { const [messages, setMessages] = useState([ { id: 1, text: "Hello! I am **The Foreman**, your Industrial Multi-Agent Supervisor. I can help with real-time telemetry, historical analysis, and anomaly detection. How can I assist you today?", sender: 'ai' } ]); const [input, setInput] = useState(''); const [loading, setLoading] = useState(false); const scrollRef = useRef(null); useEffect(() => { if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }, [messages, loading]); const handleSend = async (e) => { e.preventDefault(); if (!input.trim()) return; const userMsg = { id: Date.now(), text: input, sender: 'user' }; setMessages(prev => [...prev, userMsg]); setInput(''); setLoading(true); try { const { data } = await axios.post(`${API_BASE}/api/v1/chat/message`, { message: input }); const aiMsg = { id: Date.now() + 1, text: data.answer || data.reply, sender: 'ai', result: data.data, intent: data.intent, analysis: data.analysis, orchestration: data.orchestration }; setMessages(prev => [...prev, aiMsg]); } catch (err) { console.error("Gateway error:", err); const errorDetail = err.response ? ` (${err.response.status}: ${JSON.stringify(err.response.data)})` : ` (${err.message})`; setMessages(prev => [...prev, { id: Date.now(), text: `The Foreman is currently unable to reach the gateway${errorDetail}. Please check your network connection.`, sender: 'ai' }]); } finally { setLoading(false); } }; return (
{/* Topbar */}

The Foreman

Supervising Industrial Systems
All systems reporting
{/* Messages */}
{/* Guide Card */}

Operational Guide

To analyze specific systems, use these naming conventions:

  • Coldrooms: "coldroom 1" through "coldroom 9"
  • Refinery: "tank 1" to "tank 13", no tank 10
{messages.map((m) => ( ))} {loading && (
)}
{/* Input */}
setInput(e.target.value)} />
); }; export default ChatPanel;