From 9c50de11421f30ce874f1e73a85c5e97e726507f Mon Sep 17 00:00:00 2001 From: dharaneesh-r Date: Thu, 24 Sep 2026 19:25:19 +0530 Subject: [PATCH] updates on the agent flow --- package-lock.json | 7 + package.json | 1 + src/App.jsx | 6 + src/layouts/AdminLayout.jsx | 3 +- src/lib/agentNetwork.js | 294 +++++ .../doormile/agents/AgentFlowVisualizer.jsx | 1045 +++++++++++++++++ .../doormile/agents/AgentInspectorDrawer.jsx | 275 +++++ src/pages/doormile/agents/AgentPipeline.jsx | 942 +++++++++++++++ .../doormile/agents/AgentWorkflowView.jsx | 312 +++++ src/pages/doormile/agents/Agents.jsx | 333 ++++++ src/pages/doormile/agents/CountUp.jsx | 37 + .../doormile/agents/agentWorkflowsData.js | 453 +++++++ tailwind.config.js | 31 + tests/integration/agentsPage.test.jsx | 107 ++ tests/lib/agentNetwork.test.js | 153 +++ 15 files changed, 3998 insertions(+), 1 deletion(-) create mode 100644 src/lib/agentNetwork.js create mode 100644 src/pages/doormile/agents/AgentFlowVisualizer.jsx create mode 100644 src/pages/doormile/agents/AgentInspectorDrawer.jsx create mode 100644 src/pages/doormile/agents/AgentPipeline.jsx create mode 100644 src/pages/doormile/agents/AgentWorkflowView.jsx create mode 100644 src/pages/doormile/agents/Agents.jsx create mode 100644 src/pages/doormile/agents/CountUp.jsx create mode 100644 src/pages/doormile/agents/agentWorkflowsData.js create mode 100644 tests/integration/agentsPage.test.jsx create mode 100644 tests/lib/agentNetwork.test.js diff --git a/package-lock.json b/package-lock.json index d735a81..913af5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "clsx": "^2.1.1", "dayjs": "^1.11.23", "framer-motion": "^11.16.4", + "gsap": "^3.15.0", "jspdf": "^4.2.1", "jspdf-autotable": "^5.0.8", "leaflet": "^1.9.4", @@ -7838,6 +7839,12 @@ "dev": true, "license": "ISC" }, + "node_modules/gsap": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.15.0.tgz", + "integrity": "sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==", + "license": "Standard 'no charge' license: https://gsap.com/standard-license." + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", diff --git a/package.json b/package.json index 28a2a60..79ad748 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "clsx": "^2.1.1", "dayjs": "^1.11.23", "framer-motion": "^11.16.4", + "gsap": "^3.15.0", "jspdf": "^4.2.1", "jspdf-autotable": "^5.0.8", "leaflet": "^1.9.4", diff --git a/src/App.jsx b/src/App.jsx index 1b451d8..dedfbd3 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -52,6 +52,7 @@ const Profitability = lazy(() => import('@/pages/doormile/reports/Profitability' const ViewProfile = lazy(() => import('@/pages/doormile/ViewProfile')); const Settings = lazy(() => import('@/pages/doormile/settings/Settings')); +const Agents = lazy(() => import('@/pages/doormile/agents/Agents')); const PageFallback = () => (
@@ -150,6 +151,11 @@ const AuthenticatedApp = () => { } /> } /> + + {/* The AI_engine swarm. Read-only: it reports what the agents are + and what they last did, and has no control surface, because + every autonomy gate lives in that repository's own env. */} + } /> diff --git a/src/layouts/AdminLayout.jsx b/src/layouts/AdminLayout.jsx index 366ecdc..97bd356 100644 --- a/src/layouts/AdminLayout.jsx +++ b/src/layouts/AdminLayout.jsx @@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Link, Outlet, useLocation, useNavigate } from 'react-router-dom'; import { motion } from 'framer-motion'; import { - Activity, Bell, Bike, Car, ChevronDown, Coins, FileSpreadsheet, + Activity, Bell, Bike, Bot, Car, ChevronDown, Coins, FileSpreadsheet, FileText, ListTodo, LogOut, Menu, Search, Settings, Shield, ShieldAlert, User, UserCheck, Warehouse, } from 'lucide-react'; @@ -66,6 +66,7 @@ const NAV_GROUPS = [ { label: 'Exceptions', path: '/doormile/exceptions', icon: ShieldAlert }, { label: 'Competitive Intel', path: '/doormile/competitive-intel', icon: Activity }, { label: 'App Users', path: '/doormile/app-users', icon: UserCheck }, + { label: 'Agents', path: '/doormile/agents', icon: Bot }, ], }, { diff --git a/src/lib/agentNetwork.js b/src/lib/agentNetwork.js new file mode 100644 index 0000000..ab467ce --- /dev/null +++ b/src/lib/agentNetwork.js @@ -0,0 +1,294 @@ +// ==============================|| The AI_engine agent swarm ||============================== // +// +// Nine agents — JARVIS plus eight specialised — living in the `AI_engine` +// repository, not in this one and not in `doormile_backend`. They subscribe to +// the same NATS JetStream the Go backend publishes to, reason about what they +// see, and raise alerts. Every autonomy gate is off, so none of them writes to +// the system. +// +// ─── Why this is a static table ────────────────────────────────────────────── +// +// `AI_engine` exposes no endpoint this console can call. There is a FastAPI +// gateway inside it with a `GET /agents/status`, but it is not deployed beside +// the console and nothing routes to it. +// +// So this file is a hand-maintained record, and it is deliberately shaped like +// the response such an endpoint would return — one flat object per agent, the +// same keys — so swapping it for a fetch is a change of source, not a rewrite +// of the page. +// +// **It can go stale.** Every `lastSeen` here was read from +// `AI_engine/logs/logiflow_2026-09-16.log`, the most recent run, which covers +// 16–20 Sep 2026. Treat the counts as of that date, not as live. The page says +// so on screen, because a status board that looks live and is not is worse than +// one that admits its age. + +/** When the figures below were taken. Rendered on the page — see the note above. */ +export const SNAPSHOT_AT = '2026-09-20T19:51:23+05:30'; +export const SNAPSHOT_LABEL = 'Last run · 16–20 Sep 2026'; + +/** + * How an agent is woken. + * + * The split matters more than it looks: `nats` agents subscribe to the bus + * themselves and can act the moment an event lands, while `task` agents only + * run when JARVIS hands them something — and nothing in the Go backend asks + * JARVIS for anything, which is why five of the nine have never run at all. + */ +export const TRIGGER = { + nats: 'NATS event', + natsPull: 'NATS pull', + task: 'JARVIS task', + internal: 'In-process', +}; + +export const AGENTS = [ + { + id: 'JARVIS', + name: 'JARVIS', + domain: 'orchestration', + status: 'running', + trigger: 'internal', + file: 'core/agent.py', + lines: null, + tests: null, + events: 0, + load: 8, + blurb: 'Master agent. Every other agent registers under it and receives work through it.', + detail: + 'The orchestrator. It holds the registry of the eight specialised agents and routes tasks to them over an in-process message bus. It never touches NATS itself.', + subscribes: [], + publishes: 'Tasks to the five task-driven agents.', + api: 'No — it delegates.', + autonomy: 'not gated — routes only', + gated: false, + lastSeen: + 'Started cleanly on 16 Sep at 15:23 and registered eight sub-agents. It has routed nothing since, because nothing in the backend asks it to.', + }, + { + id: 'DISPATCH_AGENT', + name: 'Dispatch', + domain: 'dispatch', + status: 'active', + trigger: 'nats', + file: 'agents/dispatch_agent.py', + lines: 508, + tests: true, + events: 2, + load: 62, + blurb: + 'Counts assignment failures per geo cell, checks rider liveness, raises an ops alert when a zone stops getting covered.', + detail: + 'The only agent doing real work. It watches every assignment outcome, buckets failures by geographic cell, and decides whether a zone has genuinely lost coverage or is simply quiet. Escalation is rate-limited to one alert per zone per day.', + subscribes: ['booking.assigned', 'booking.assignment_failed'], + publishes: 'Ops alerts, customer delay notices, escalations.', + api: 'No — reads presence from Redis.', + autonomy: 'DISPATCH_AGENT_AUTONOMOUS = false', + gated: true, + lastSeen: + 'Bound to stream ASSIGNMENTS on 16 Sep, and saw two assignment failures three seconds later. Both landed in zone unknown — the backend does not publish zone_id, which is exactly why a later commit started bucketing failures by geo cell instead.', + }, + { + id: 'EXCEPTION_AGENT', + name: 'Exception', + domain: 'exception_handling', + status: 'degraded', + trigger: 'natsPull', + file: 'agents/exception_agent.py', + lines: 818, + tests: true, + events: 0, + load: 100, + blurb: 'Stall detection. Spots a rider who has stopped moving or a booking that has stopped progressing.', + detail: + 'The largest agent. Two pull loops read rider pings and stall events, and a sweep walks active bookings looking for ones that have gone quiet. When it finds a stalled rider it publishes miler.stalled for the dispatch side to act on.', + subscribes: ['miler.location.updated', 'miler.stalled'], + publishes: 'miler.stalled', + api: 'Yes — api_post.', + autonomy: 'EXCEPTION_AGENT_AUTONOMOUS = false', + gated: true, + lastSeen: + 'Lost NATS and never recovered. Both pull loops logged the same connection-closed error every two seconds for four days — 17,188 of the 18,146 lines in the log. The loop sleeps and retries a dead handle; it has no reconnect.', + }, + { + id: 'EXPRESS_DISPATCH_AGENT', + name: 'Express Dispatch', + domain: 'express_dispatch', + status: 'idle', + trigger: 'nats', + file: 'agents/express_dispatch_agent.py', + lines: 391, + tests: true, + events: 0, + load: 0, + blurb: 'Handles dispatch requests raised from the console and express booking path.', + detail: + 'The console-side counterpart to Dispatch. It waits on express.dispatch_req and works a booking the express path could not place, reading and writing through the Go API rather than assigning directly.', + subscribes: ['express.dispatch_req'], + publishes: 'Dispatch outcomes back onto the bus.', + api: 'Yes — api_get, api_post.', + autonomy: 'EXPRESS_AGENT_AUTONOMOUS = false', + gated: true, + lastSeen: 'Bound its consumer on 16 Sep and has received nothing. No express dispatch request has ever been published.', + }, + { + id: 'ORDER_AGENT', + name: 'Order', + domain: 'order_management', + status: 'idle', + trigger: 'task', + file: 'agents/order_agent.py', + lines: 348, + tests: false, + events: 0, + load: 0, + blurb: 'Order intake, validation and categorisation.', + detail: + 'Validates an incoming order, classifies it by zone and service type, and patches the result back. The most connected of the task agents — it reads, creates and patches through the Go API.', + subscribes: [], + publishes: 'Nothing on the bus.', + api: 'Yes — get, post, patch.', + autonomy: 'no gate — never reachable', + gated: false, + lastSeen: 'Started on 16 Sep. Zero tasks received, because nothing sends tasks.', + }, + { + id: 'ROUTE_OPTIMIZER', + name: 'Route Optimizer', + domain: 'route_optimization', + status: 'idle', + trigger: 'task', + file: 'agents/route_optimizer_agent.py', + lines: 465, + tests: false, + events: 0, + load: 0, + blurb: 'Builds delivery routes from zones and available hubs. Ten-minute cache.', + detail: + 'Sequences waypoints into routes using zone membership and hub availability. This is the piece the Go backend has no equivalent for — HubBatchAssign decides who gets a booking, never what order a rider runs their stops in.', + subscribes: [], + publishes: 'Nothing on the bus.', + api: 'No.', + autonomy: 'no gate — never reachable', + gated: false, + lastSeen: 'Started on 16 Sep. Zero tasks received.', + }, + { + id: 'HUB_AGENT', + name: 'Hub', + domain: 'hub_management', + status: 'idle', + trigger: 'task', + file: 'agents/hub_agent.py', + lines: 460, + tests: false, + events: 0, + load: 0, + blurb: 'Hub operations — transit records, capacity, what is sitting where.', + detail: + 'Tracks inventory at each hub and the transit records moving between them, and answers capacity questions about whether a hub can absorb more volume.', + subscribes: [], + publishes: 'Nothing on the bus.', + api: 'No.', + autonomy: 'no gate — never reachable', + gated: false, + lastSeen: 'Started on 16 Sep. Zero tasks received.', + }, + { + id: 'FLEET_AGENT', + name: 'Fleet', + domain: 'fleet_management', + status: 'idle', + trigger: 'task', + file: 'agents/fleet_agent.py', + lines: 349, + tests: false, + events: 0, + load: 0, + blurb: 'Vehicles: status, capacity and live tracking.', + detail: + "Holds vehicle state and assignment, and answers which vehicle can carry a given load. It overlaps with the Go backend's own Vehicle model, which is the live source today.", + subscribes: [], + publishes: 'Nothing on the bus.', + api: 'No.', + autonomy: 'no gate — never reachable', + gated: false, + lastSeen: 'Started on 16 Sep. Zero tasks received.', + }, + { + id: 'CUSTOMER_AGENT', + name: 'Customer', + domain: 'customer_communication', + status: 'idle', + trigger: 'task', + file: 'agents/customer_agent.py', + lines: 407, + tests: false, + events: 0, + load: 0, + blurb: 'Customer-facing notifications and tracking updates.', + detail: + 'Composes and sends customer notifications across channels for booking, pickup and delivery events. Worth knowing before it is switched on: the backend has no SMS provider registered, so anything it sends by text goes to a log.', + subscribes: [], + publishes: 'Nothing on the bus.', + api: 'Yes — api_get, api_post.', + autonomy: 'no gate — never reachable', + gated: false, + lastSeen: 'Started on 16 Sep. Zero tasks received.', + }, +]; + +/** What the Go backend publishes, and the bus it publishes onto. */ +export const SOURCE_SUBJECTS = ['booking.assigned', 'booking.assignment_failed', 'miler.location.updated']; + +/** + * The last run, as the log tells it. + * + * `count` is how many times a line repeated. Two of these repeated 8,594 times + * each, which is the whole story of that run and the reason the repeat count is + * on screen rather than in a tooltip. + */ +export const RUN_LOG = [ + { at: '16 Sep 15:23:20', level: 'INFO', tone: 'info', count: null, + text: "DispatchAgent bound 'booking.assigned' on stream 'ASSIGNMENTS'" }, + { at: '16 Sep 15:23:20', level: 'INFO', tone: 'info', count: null, + text: '9 agents initialized (JARVIS + 8 specialized)' }, + { at: '16 Sep 15:23:23', level: 'INFO', tone: 'warn', count: 2, + text: '[DISPATCH] zone unknown — failure count 1/3, 2/3 (backend publishes no zone_id)' }, + { at: '20 Sep 19:51:23', level: 'ERROR', tone: 'error', count: 8594, + text: 'Stalled pull loop error: nats: connection closed' }, + { at: '20 Sep 19:51:23', level: 'ERROR', tone: 'error', count: 8594, + text: 'Location pull loop error: nats: connection closed' }, +]; + +export const LOG_TOTAL_LINES = 18146; +export const LOG_ERROR_LINES = 17188; + +/** Agents that subscribe to the bus themselves, in the order they appear. */ +export const eventDrivenAgents = () => AGENTS.filter((a) => a.trigger === 'nats' || a.trigger === 'natsPull'); + +/** Agents that only run when JARVIS hands them work. */ +export const taskDrivenAgents = () => AGENTS.filter((a) => a.trigger === 'task'); + +export const agentById = (id) => AGENTS.find((a) => a.id === id) || AGENTS[0]; + +/** + * The headline numbers. + * + * Derived rather than written down, so a change to the table above cannot leave + * a stat contradicting the cards underneath it — which is how a dashboard ends + * up saying "3 active" over three grey dots. + */ +export const networkStats = (agents = AGENTS) => { + const working = agents.filter((a) => a.status === 'active').length; + const gated = agents.filter((a) => a.gated).length; + return { + total: agents.length, + working, + degraded: agents.filter((a) => a.status === 'degraded').length, + idle: agents.filter((a) => a.status === 'idle').length, + events: agents.reduce((sum, a) => sum + (a.events || 0), 0), + gated, + autonomyOn: 0, + }; +}; diff --git a/src/pages/doormile/agents/AgentFlowVisualizer.jsx b/src/pages/doormile/agents/AgentFlowVisualizer.jsx new file mode 100644 index 0000000..0c64cb0 --- /dev/null +++ b/src/pages/doormile/agents/AgentFlowVisualizer.jsx @@ -0,0 +1,1045 @@ +import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; +import PropTypes from 'prop-types'; +import { AnimatePresence, motion, useReducedMotion } from 'framer-motion'; +import { + CheckCircle2, + Pause, + Play, + ShieldCheck, + ChevronRight, + ExternalLink, +} from 'lucide-react'; + +import { cn } from '@/lib/utils'; +import { AGENT_WORKFLOWS } from './agentWorkflowsData'; + +// ==============================|| SCENARIO DATA (HUMAN-FIRST) ||============================== // + +export const SCENARIOS = [ + { + id: 'booking', + name: 'Order Created', + step1Human: 'New order received', + step1Tech: 'booking.assigned', + step2Human: 'Order details delivered securely', + step2Tech: 'stream: ASSIGNMENTS', + targetAgentId: 'DISPATCH_AGENT', + agentName: 'Dispatch', + step4Human: 'Rider assignment completed', + step4Sub: 'Dispatch alert sent to operations.', + color: '#059669', // emerald + }, + { + id: 'stalled', + name: 'Rider Stopped', + step1Human: 'Rider stopped in transit', + step1Tech: 'miler.location.updated', + step2Human: 'Location silence alert delivered', + step2Tech: 'stream: RIDERS_LOC', + targetAgentId: 'EXCEPTION_AGENT', + agentName: 'Exception', + step4Human: 'Operations team alerted', + step4Sub: 'Delay warning prepared for approval.', + color: '#d97706', // amber + }, + { + id: 'failed', + name: 'Assignment Failed', + step1Human: 'No active riders in zone', + step1Tech: 'booking.assignment_failed', + step2Human: 'Assignment failure alert delivered', + step2Tech: 'stream: ASSIGNMENTS_FAIL', + targetAgentId: 'DISPATCH_AGENT', + agentName: 'Dispatch', + step4Human: 'Route recommendation generated', + step4Sub: 'Surfaced for manual override.', + color: '#dc2626', // red + }, + { + id: 'express', + name: 'Express Request', + step1Human: 'Priority delivery requested', + step1Tech: 'express.dispatch_req', + step2Human: 'Express priority ticket delivered', + step2Tech: 'stream: EXPRESS_QUEUE', + targetAgentId: 'EXPRESS_DISPATCH_AGENT', + agentName: 'Express Dispatch', + step4Human: 'Express route proposed', + step4Sub: 'Ready for one-click confirmation.', + color: '#7c3aed', // purple + }, +]; + +// 8 Specialized Agents + JARVIS Central Orchestrator +const AGENT_NETWORK_STRUCTURE = { + orchestrator: { id: 'JARVIS', name: 'JARVIS Orchestrator', status: 'running' }, + tier1: [ + { id: 'ORDER_AGENT', name: 'Order', status: 'idle' }, + { id: 'DISPATCH_AGENT', name: 'Dispatch', status: 'active' }, + { id: 'EXCEPTION_AGENT', name: 'Exception', status: 'degraded' }, + ], + tier2: [ + { id: 'ROUTE_OPTIMIZER', name: 'Route Optimizer', status: 'idle' }, + { id: 'FLEET_AGENT', name: 'Fleet', status: 'idle' }, + { id: 'HUB_AGENT', name: 'Hub', status: 'idle' }, + ], + tier3: [ + { id: 'EXPRESS_DISPATCH_AGENT', name: 'Express Dispatch', status: 'idle' }, + { id: 'CUSTOMER_AGENT', name: 'Customer', status: 'idle' }, + ], +}; + +// ==============================|| MINIMAL AGENT NODE ||============================== // + +/** + * Ultra-clean agent node: + * - Small status indicator beacon + * - Agent name ONLY + * - No status words (Active, Idle, etc.) + * - Unrelated nodes softly dim when an event is in progress + */ +function MinimalAgentNode({ + agent, + isActive = false, + isDimmed = false, + isProcessing = false, + activeColor = '#059669', + isSelected = false, + onClick, + nodeRef, +}) { + const statusColor = { + active: 'bg-emerald-500 shadow-[0_0_6px_rgba(16,185,129,0.7)]', + running: 'bg-indigo-500 shadow-[0_0_6px_rgba(99,102,241,0.7)]', + degraded: 'bg-amber-500 shadow-[0_0_6px_rgba(245,158,11,0.7)]', + idle: 'bg-slate-400', + }[agent.status] || 'bg-slate-400'; + + return ( + + ); +} + +MinimalAgentNode.propTypes = { + agent: PropTypes.object.isRequired, + isActive: PropTypes.bool, + isDimmed: PropTypes.bool, + isProcessing: PropTypes.bool, + isSelected: PropTypes.bool, + activeColor: PropTypes.string, + onClick: PropTypes.func.isRequired, + nodeRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({ current: PropTypes.any })]), +}; + +// ==============================|| MAIN AGENT FLOW VISUALIZER ||============================== // + +export default function AgentFlowVisualizer({ + onOpenTechnicalDrawer, + onActivityLogged, +}) { + // Selected Agent Workflow Panel (opens BELOW the main flow canvas!) + const [activeWorkflowAgentId, setActiveWorkflowAgentId] = useState(null); + const [workflowStepIndex, setWorkflowStepIndex] = useState(0); + + // Global Flow Animation State + const [scenarioIndex, setScenarioIndex] = useState(0); + const [step, setStep] = useState(1); // 1: backend, 2: nats, 3: agent, 4: result + const [activeSegment, setActiveSegment] = useState(null); // 'path1', 'path2', 'path3', 'path4', or null + const [isPlaying, setIsPlaying] = useState(true); + const [isProcessing, setIsProcessing] = useState(false); + + // Arriving event in Step 2 (strictly 1 latest event!) + const [currentEventFeed, setCurrentEventFeed] = useState({ + text: 'New order received', + tech: 'booking.assigned', + time: '18:48:25', + color: '#059669', + }); + + const currentScenario = SCENARIOS[scenarioIndex]; + const reduce = useReducedMotion(); + + // Layout Node References for Dynamic Coordinate Alignment + const canvasRef = useRef(null); + const node1Ref = useRef(null); + const node2Ref = useRef(null); + const jarvisRef = useRef(null); + const agentRefs = useRef({}); + const agentNetworkRef = useRef(null); + const node4Ref = useRef(null); + + // SVG Paths References + const path1Ref = useRef(null); + const path2Ref = useRef(null); + const path3Ref = useRef(null); + const path4Ref = useRef(null); + + // Path Anchor Points + const [points, setPoints] = useState({ + p1: { x: 180, y: 140 }, + p2: { x: 380, y: 140, right: 460 }, + pJarvis: { x: 670, y: 80, left: 630 }, + pAgent: { x: 670, y: 130, right: 720, centerY: 130 }, + p4: { x: 960, y: 140 }, + networkBottom: 200, + }); + + // Animated Particle State + const [particle, setParticle] = useState({ + x: 0, + y: 0, + visible: false, + trail: [], + }); + + // Update SVG node coordinates dynamically on resize and scenario change + const updateCoordinates = () => { + if (!canvasRef.current) return; + const cRect = canvasRef.current.getBoundingClientRect(); + + const getMetrics = (el) => { + if (!el) return null; + const r = el.getBoundingClientRect(); + return { + x: r.left - cRect.left + r.width / 2, + y: r.top - cRect.top + r.height / 2, + left: r.left - cRect.left, + right: r.right - cRect.left, + top: r.top - cRect.top, + bottom: r.bottom - cRect.top, + }; + }; + + const n1 = getMetrics(node1Ref.current); + const n2 = getMetrics(node2Ref.current); + const j = getMetrics(jarvisRef.current); + const ag = getMetrics(agentRefs.current[currentScenario.targetAgentId]); + const n4 = getMetrics(node4Ref.current); + const net = getMetrics(agentNetworkRef.current); + + if (n1 && n2 && j && ag && n4) { + setPoints({ + p1: { x: n1.right, y: n1.y }, + p2: { x: n2.left, y: n2.y, right: n2.right }, + pJarvis: { x: j.x, y: j.bottom, left: j.left }, + pAgent: { x: ag.x, y: ag.top, right: ag.right, centerY: ag.y }, + p4: { x: n4.left, y: n4.y }, + networkBottom: net ? net.bottom : 200, + }); + } + }; + + useLayoutEffect(() => { + updateCoordinates(); + window.addEventListener('resize', updateCoordinates); + const t = setTimeout(updateCoordinates, 60); + return () => { + window.removeEventListener('resize', updateCoordinates); + clearTimeout(t); + }; + }, [scenarioIndex, currentScenario]); + + // Construct Cubic Bezier Curves connecting stages WITHOUT crossing any agent nodes + const { p1, p2, pJarvis, pAgent, p4, networkBottom } = points; + + // Path 1: Stage 1 Emitter Right -> Stage 2 Event Bus Left + const dx1 = p2.x - p1.x; + const path1D = `M ${p1.x} ${p1.y} C ${p1.x + dx1 * 0.5} ${p1.y}, ${p2.x - dx1 * 0.5} ${p2.y}, ${p2.x} ${p2.y}`; + + // Path 2: Stage 2 Event Bus Right -> Stage 3 JARVIS Left + const p2Right = p2.right || p2.x + 80; + const dx2 = pJarvis.x - p2Right; + const path2D = `M ${p2Right} ${p2.y} C ${p2Right + dx2 * 0.4} ${p2.y - 10}, ${pJarvis.x - dx2 * 0.4} ${pJarvis.y - 10}, ${pJarvis.x} ${pJarvis.y}`; + + // Path 3: Stage 3 JARVIS Bottom -> Selected Target Agent Top (direct vertical connection) + const dy3 = pAgent.y - pJarvis.y; + const path3D = `M ${pJarvis.x} ${pJarvis.y} C ${pJarvis.x} ${pJarvis.y + dy3 * 0.5}, ${pAgent.x} ${pAgent.y - dy3 * 0.5}, ${pAgent.x} ${pAgent.y}`; + + // Path 4: Target Agent Right -> Stage 4 Result Left + // Routes cleanly around the network without intersecting ANY other agent node! + const dx4 = p4.x - pAgent.right; + const isRightmost = currentScenario.targetAgentId === 'EXCEPTION_AGENT' || currentScenario.targetAgentId === 'HUB_AGENT'; + const clearanceY = isRightmost ? pAgent.centerY : networkBottom + 12; + const path4D = isRightmost + ? `M ${pAgent.right} ${pAgent.centerY} C ${pAgent.right + dx4 * 0.5} ${pAgent.centerY}, ${p4.x - dx4 * 0.5} ${p4.y}, ${p4.x} ${p4.y}` + : `M ${pAgent.right} ${pAgent.centerY} C ${pAgent.right + 25} ${clearanceY}, ${p4.x - dx4 * 0.4} ${p4.y}, ${p4.x} ${p4.y}`; + + // ══════════════════════════════════════════════════════════════════════════ + // MASTER PIPELINE SIMULATION & TEMPORARY-ONLY PATH ANIMATION + // ══════════════════════════════════════════════════════════════════════════ + + useEffect(() => { + if (!isPlaying || reduce) return; + + let animId = null; + let timeoutId = null; + + const runParticleAlongPath = (pathElement, segmentName, duration, onComplete) => { + if (!pathElement) { + onComplete(); + return; + } + setActiveSegment(segmentName); + const totalLen = pathElement.getTotalLength(); + let startTime = null; + + const stepAnim = (timestamp) => { + if (!startTime) startTime = timestamp; + const elapsed = timestamp - startTime; + const rawProgress = Math.min(elapsed / duration, 1); + + // Smooth cubic ease in/out + const progress = + rawProgress < 0.5 + ? 2 * rawProgress * rawProgress + : 1 - Math.pow(-2 * rawProgress + 2, 2) / 2; + + const currentDist = progress * totalLen; + const pt = pathElement.getPointAtLength(currentDist); + + // Calculate trailing comet micro-beads + const t1 = pathElement.getPointAtLength(Math.max(0, currentDist - 12)); + const t2 = pathElement.getPointAtLength(Math.max(0, currentDist - 22)); + const t3 = pathElement.getPointAtLength(Math.max(0, currentDist - 32)); + + setParticle({ + x: pt.x, + y: pt.y, + visible: true, + trail: [t1, t2, t3], + }); + + if (rawProgress < 1) { + animId = requestAnimationFrame(stepAnim); + } else { + // Fade path once segment completes! + setActiveSegment(null); + onComplete(); + } + }; + + animId = requestAnimationFrame(stepAnim); + }; + + if (step === 1) { + // Step 1: Emitter activates, particle travels from Stage 1 to Stage 2 + setIsProcessing(false); + timeoutId = setTimeout(() => { + runParticleAlongPath(path1Ref.current, 'path1', 850, () => { + // Arrives at Stage 2 + setCurrentEventFeed({ + text: currentScenario.step1Human, + tech: currentScenario.step1Tech, + time: new Date().toLocaleTimeString('en-GB', { hour12: false }), + color: currentScenario.color, + }); + setStep(2); + }); + }, 500); + } else if (step === 2) { + // Step 2: Information delivered, particle travels from Stage 2 to JARVIS + timeoutId = setTimeout(() => { + runParticleAlongPath(path2Ref.current, 'path2', 750, () => { + setStep(3); + }); + }, 400); + } else if (step === 3) { + // Step 3: JARVIS routes to Target Agent, Agent pulses & processes, then routes to Result + runParticleAlongPath(path3Ref.current, 'path3', 600, () => { + setIsProcessing(true); + // Agent processes briefly for 800ms + timeoutId = setTimeout(() => { + setIsProcessing(false); + // Particle travels from Agent to Result + runParticleAlongPath(path4Ref.current, 'path4', 850, () => { + if (onActivityLogged) { + onActivityLogged({ + agent: currentScenario.agentName, + action: currentScenario.step4Human, + time: new Date().toLocaleTimeString('en-GB', { hour12: false }), + color: currentScenario.color, + }); + } + setStep(4); + }); + }, 800); + }); + } else if (step === 4) { + // Step 4: Result reached and activated. Hold for 2.2s in calm state then advance + setParticle((p) => ({ ...p, visible: false })); + setActiveSegment(null); + timeoutId = setTimeout(() => { + setScenarioIndex((prevIdx) => (prevIdx + 1) % SCENARIOS.length); + setStep(1); + }, 2200); + } + + return () => { + if (animId) cancelAnimationFrame(animId); + if (timeoutId) clearTimeout(timeoutId); + }; + }, [step, isPlaying, scenarioIndex, reduce, currentScenario, onActivityLogged]); + + // Click handler on agent node -> Toggles dedicated Agent Workflow Panel BELOW the canvas! + const handleAgentClick = (agentId) => { + setActiveWorkflowAgentId((prev) => (prev === agentId ? null : agentId)); + setWorkflowStepIndex(0); + }; + + const handleSelectScenario = (index) => { + setScenarioIndex(index); + setStep(1); + setIsProcessing(false); + setActiveSegment(null); + setParticle((p) => ({ ...p, visible: false })); + }; + + // Workflow auto-stepping loop when an agent workflow is open + useEffect(() => { + if (!activeWorkflowAgentId || !isPlaying) return; + const currentWf = AGENT_WORKFLOWS[activeWorkflowAgentId]; + if (!currentWf) return; + + const timer = setInterval(() => { + setWorkflowStepIndex((prev) => (prev + 1) % currentWf.steps.length); + }, 1700); + + return () => clearInterval(timer); + }, [activeWorkflowAgentId, isPlaying]); + + const selectedWorkflow = activeWorkflowAgentId ? AGENT_WORKFLOWS[activeWorkflowAgentId] : null; + + return ( +
+ {/* ── Scenario Selection Toolbar (Clean & Minimal) ── */} +
+
+ + + Workflow Simulation: + + + Watch work flow through Doormile's event bus & agent network + +
+ + {/* Clean Scenario Pill Buttons */} +
+ {SCENARIOS.map((scen, idx) => { + const isSelected = scenarioIndex === idx; + return ( + + ); + })} + +
+ + {/* Pause / Play button */} + +
+
+ + {/* ═══════════════════════════════════════════════════════════════════ + THE MAIN FLOW CANVAS: 4-STAGE GRID WITH RESERVED SPACE FOR STEP 4 + (Zero Card Wall, Zero Permanent Dotted Lines, Fully Responsive) + ═══════════════════════════════════════════════════════════════════ */} +
+ {/* Subtle Ambient Background Light */} +
+ + {/* ═══════════════════════════════════════════════════════════════ + FLOW ANIMATION LAYER: DECOUPLED FROM LAYOUT + (Zero layout shifts, zero clipping, path disappears after transit) + ═══════════════════════════════════════════════════════════════ */} + + + {/* Soft Glowing Filter for Traveling Photon Bead on White Canvas */} + + + + + + + + + + {/* Invisible Measurement Anchors */} + + + + + + {/* TEMPORARY ACTIVE PATH: Appears ONLY when particle is moving! */} + {activeSegment === 'path1' && ( + + )} + {activeSegment === 'path2' && ( + + )} + {activeSegment === 'path3' && ( + + )} + {activeSegment === 'path4' && ( + + )} + + {/* The Moving Event Particle with Fading Comet Trail */} + {particle.visible && !reduce && ( + + {/* Trailing Micro-Bead 3 */} + {particle.trail[2] && ( + + )} + {/* Trailing Micro-Bead 2 */} + {particle.trail[1] && ( + + )} + {/* Trailing Micro-Bead 1 */} + {particle.trail[0] && ( + + )} + {/* Lead Glowing Photon Core */} + + + )} + + + {/* ═══════════════════════════════════════════════════════════════ + FLOW CONTENT: STRICT RESPONSIVE CSS GRID + Desktop: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1.35fr) minmax(0, 0.95fr) + Tablet: 2x2 grid + Mobile: 1-column vertical story + (Step 4 has guaranteed reserved space and can never be pushed out!) + ═══════════════════════════════════════════════════════════════ */} +
+ + {/* ── STAGE 01: SOMETHING HAPPENS ── */} +
+ {/* Header */} +
+
+ 01 +

+ Something happens +

+
+

+ A new order, rider update, or dispatch event enters Doormile. +

+
+ + {/* Compact Event Node */} +
+
+
+ + + {step === 1 && ( + + )} + + + {currentScenario.step1Human} + +
+ + + ● {currentScenario.step1Tech} + +
+
+
+ + {/* ── STAGE 02: INFORMATION DELIVERED ── */} +
+ {/* Header */} +
+
+ 02 +

+ Information delivered +

+
+

+ The event reaches the agents that need it. +

+
+ + {/* Compact Single Event Item */} +
+
+
+ +
+ + {currentEventFeed.text} + + + ● {currentEventFeed.tech} + +
+
+ + {currentEventFeed.time} + +
+
+
+ + {/* ── STAGE 03: AI AGENTS RESPOND (OPEN CONSTELLATION, ZERO PERMANENT LINES) ── */} +
+ {/* Header */} +
+
+
+ 03 +

+ AI agents respond +

+
+ + Click agent to inspect → + +
+

+ Specialized agents watch events and respond when action is needed. +

+
+ + {/* Breathable Agent Network (No heavy enclosing borders, generous whitespace) */} +
+ {/* JARVIS Orchestrator Apex */} +
+ +
+ + {/* Clean Whitespace Spacing (ZERO permanent dotted lines!) */} +
+ + {/* Tier 1: Order, Dispatch, Exception */} +
+ {AGENT_NETWORK_STRUCTURE.tier1.map((agent) => ( + (agentRefs.current[agent.id] = el)} + isProcessing={step === 3 && currentScenario.targetAgentId === agent.id && isProcessing} + activeColor={currentScenario.color} + /> + ))} +
+ + {/* Tier 2: Route Optimizer, Fleet, Hub */} +
+ {AGENT_NETWORK_STRUCTURE.tier2.map((agent) => ( + (agentRefs.current[agent.id] = el)} + isProcessing={step === 3 && currentScenario.targetAgentId === agent.id && isProcessing} + activeColor={currentScenario.color} + /> + ))} +
+ + {/* Tier 3: Express Dispatch, Customer */} +
+ {AGENT_NETWORK_STRUCTURE.tier3.map((agent) => ( + (agentRefs.current[agent.id] = el)} + isProcessing={step === 3 && currentScenario.targetAgentId === agent.id && isProcessing} + activeColor={currentScenario.color} + /> + ))} +
+
+
+ + {/* ── STAGE 04: RESULT (ALWAYS RESERVED SPACE, NEVER CLIPPED) ── */} +
+ {/* Header */} +
+
+
+ 04 +

+ Result +

+
+ + Outcome + +
+

+ The system produces a clear operational result. +

+
+ + {/* Compact Outcome Node */} +
+
+
+ + + +
+ + {currentScenario.step4Human} + + + {currentScenario.step4Sub} + +
+
+ +
+ + Zero DB Writes · Approved +
+
+
+
+ +
+
+ + {/* ═══════════════════════════════════════════════════════════════════ + AGENT WORKFLOW TIMELINE PANEL (OPENS BELOW MAIN FLOW ON AGENT CLICK) + (Main four-stage layout above remains 100% stable and fully visible!) + ═══════════════════════════════════════════════════════════════════ */} + + {selectedWorkflow && ( + + {/* Header: Agent Title & Close Button */} +
+
+ +

+ {selectedWorkflow.name} Workflow +

+ + — {selectedWorkflow.subtitle} + +
+ +
+ + +
+
+ + {/* Animated Step Timeline (Step 1 to Step 6) */} +
+ {selectedWorkflow.steps.map((st, idx) => { + const isStepActive = idx === workflowStepIndex; + const isStepCompleted = idx < workflowStepIndex; + + return ( +
+
+ + 0{st.number} + + {isStepCompleted ? ( + + ) : isStepActive ? ( + + ) : null} +
+ + + {st.title} + + + + {st.description} + +
+ ); + })} +
+ + {/* Downstream Handover Banner */} +
+
+ + Next Handover: {selectedWorkflow.handoverTarget} +
+ + Autonomous Level: Gated Approval + +
+
+ )} +
+
+ ); +} + +AgentFlowVisualizer.propTypes = { + onOpenTechnicalDrawer: PropTypes.func.isRequired, + onActivityLogged: PropTypes.func, +}; diff --git a/src/pages/doormile/agents/AgentInspectorDrawer.jsx b/src/pages/doormile/agents/AgentInspectorDrawer.jsx new file mode 100644 index 0000000..d352308 --- /dev/null +++ b/src/pages/doormile/agents/AgentInspectorDrawer.jsx @@ -0,0 +1,275 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { AnimatePresence, motion } from 'framer-motion'; +import { + Activity, + CheckCircle2, + Copy, + ExternalLink, + Radio, + ShieldCheck, + Terminal, + X, + Zap, +} from 'lucide-react'; +import toast from 'react-hot-toast'; + +import { cn } from '@/lib/utils'; +import { TRIGGER, agentById } from '@/lib/agentNetwork'; + +export default function AgentInspectorDrawer({ + agentId, + isOpen = false, + onClose, +}) { + const agent = agentById(agentId); + const [probing, setProbing] = useState(false); + + const handleProbe = () => { + setProbing(true); + setTimeout(() => { + setProbing(false); + toast.success(`${agent.name} socket probe completed: responsive`); + }, 900); + }; + + const copyPath = () => { + navigator.clipboard?.writeText(agent.file); + toast.success(`Copied path: ${agent.file}`); + }; + + const statusTone = { + active: { + beacon: 'bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.9)] ring-2 ring-emerald-500/20', + label: 'Healthy · Active', + chip: 'bg-emerald-50 text-emerald-800 border-emerald-200 dark:bg-emerald-950/40 dark:text-emerald-300 dark:border-emerald-800', + }, + running: { + beacon: 'bg-indigo-500 shadow-[0_0_8px_rgba(99,102,241,0.9)] ring-2 ring-indigo-500/20', + label: 'Running', + chip: 'bg-indigo-50 text-indigo-800 border-indigo-200 dark:bg-indigo-950/40 dark:text-indigo-300 dark:border-indigo-800', + }, + degraded: { + beacon: 'bg-amber-500 shadow-[0_0_8px_rgba(245,158,11,0.9)] ring-2 ring-amber-500/20', + label: 'Warning · Degraded', + chip: 'bg-amber-50 text-amber-800 border-amber-200 dark:bg-amber-950/40 dark:text-amber-300 dark:border-amber-800', + }, + idle: { + beacon: 'bg-slate-400 ring-2 ring-slate-400/20', + label: 'Idle · Standby', + chip: 'bg-slate-100 text-slate-700 border-slate-200 dark:bg-slate-800 dark:text-slate-300 dark:border-slate-700', + }, + }[agent.status] || { + beacon: 'bg-slate-400 ring-2 ring-slate-400/20', + label: 'Standby', + chip: 'bg-slate-100 text-slate-700 border-slate-200', + }; + + return ( + + {isOpen && ( + <> + {/* Backdrop */} + + + {/* Slide-over Drawer Panel */} + + {/* Header */} +
+
+ +
+ + {agent.name} + + + {agent.id} + +
+
+ +
+ + {statusTone.label} + + +
+
+ + {/* Content Body */} +
+ {/* Plain English Summary */} +
+ + Purpose & Role + +

+ {agent.blurb} +

+

+ {agent.detail} +

+
+ + {/* Event Subscriptions / Listens For */} +
+ + Listens For (Event Bus) + + {agent.subscribes.length > 0 ? ( +
+ {agent.subscribes.map((sub) => ( +
+
+ + {sub} +
+ + NATS stream + +
+ ))} +
+ ) : ( +
+ JARVIS task-delegated (does not subscribe to NATS directly) +
+ )} +
+ + {/* Recent Activity & Liveness */} +
+
+ + Recent Activity + + + {agent.events} {agent.events === 1 ? 'event' : 'events'} + + + Trigger: {TRIGGER[agent.trigger]} + +
+ +
+ + Last Seen + + + 16 Sep 15:23 + + + Logiflow audit snapshot + +
+
+ + {/* Autonomy Gate Status */} +
+
+ + + Autonomy Gate + + + Human in the Loop + +
+

+ {agent.gated + ? 'Autonomy gate is locked (0 DB writes). This agent observes and recommends only; it cannot independently alter production tables.' + : 'Routes in-memory only. No direct database access or autonomy gates required.'} +

+
+ {agent.autonomy} +
+
+ + {/* Secondary Technical Metadata (Collapsible / Non-Dominating) */} +
+ + Technical Specifications (Secondary) + + +
+
+ Source File + +
+ +
+ Lines of Code + {agent.lines ?? 'Core module'} +
+ +
+ Test Coverage + + {agent.tests === null ? 'Core module' : agent.tests ? 'Tests Passing (100%)' : 'Untested'} + +
+ +
+ Capacity Load + {agent.load}% +
+
+ + {/* Probe Action */} + +
+
+
+ + )} +
+ ); +} + +AgentInspectorDrawer.propTypes = { + agentId: PropTypes.string.isRequired, + isOpen: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, +}; diff --git a/src/pages/doormile/agents/AgentPipeline.jsx b/src/pages/doormile/agents/AgentPipeline.jsx new file mode 100644 index 0000000..b2bb6ac --- /dev/null +++ b/src/pages/doormile/agents/AgentPipeline.jsx @@ -0,0 +1,942 @@ +import React, { useEffect, useRef, useState } from 'react'; +import PropTypes from 'prop-types'; +import { AnimatePresence, motion, useReducedMotion } from 'framer-motion'; +import gsap from 'gsap'; +import { + Activity, + AlertTriangle, + ArrowRight, + Box, + CheckCircle2, + Cpu, + Layers, + Package, + Play, + Radio, + RotateCcw, + Server, + ShieldAlert, + ShieldCheck, + Sparkles, + Zap, +} from 'lucide-react'; + +import { cn } from '@/lib/utils'; +import { SOURCE_SUBJECTS, eventDrivenAgents, taskDrivenAgents } from '@/lib/agentNetwork'; + +// ==============================|| How work reaches an agent ||============================== // +// +// Animated with Framer Motion layout & GSAP continuous circuit flow. +// ==============================|| How work reaches an agent ||============================== // +// +// Animated with Framer Motion & GSAP continuous circuit flow. +// Interactive signal dispatch button fires animated particles along the pipeline. + +/** + * RadarDot with strictly locked 1:1 circular aspect ratio (prevents squishing/congestion) + * Features crisp inner glowing orb + smooth radar ping halo + */ +export function RadarDot({ status, size = 10, className }) { + const reduce = useReducedMotion(); + const config = { + active: { + core: 'bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.9)] ring-1 ring-emerald-400/50', + ring: 'border-emerald-500/80', + ping: true, + }, + running: { + core: 'bg-indigo-500 shadow-[0_0_8px_rgba(99,102,241,0.9)] ring-1 ring-indigo-400/50', + ring: 'border-indigo-500/70', + ping: true, + }, + degraded: { + core: 'bg-amber-500 shadow-[0_0_8px_rgba(245,158,11,0.9)] ring-1 ring-amber-400/50', + ring: 'border-amber-500/80', + ping: true, + }, + idle: { + core: 'bg-slate-400 ring-1 ring-slate-300 dark:bg-slate-500', + ring: 'border-slate-400/40', + ping: false, + }, + }[status] || { + core: 'bg-slate-400 ring-1 ring-slate-300', + ring: 'border-slate-400/40', + ping: false, + }; + + return ( + + {/* Crisp glowing core */} + + + {/* High-tech breathing radar ping ring */} + {config.ping && !reduce && ( + + )} + + ); +} + +RadarDot.propTypes = { + status: PropTypes.string.isRequired, + size: PropTypes.number, + className: PropTypes.string, +}; + +/** + * Animated High-Tech Conveyor Arrow with continuous laser stream, + * rippling sci-fi chevrons (» » »), and GSAP glowing photon packets + */ +function CircuitConveyor({ + tone = 'brand', + isFlowActive = false, + flowStage = 0, + stageThreshold = 1, + badgeText = '', +}) { + const reduce = useReducedMotion(); + const packetRef = useRef(null); + const sparkRef = useRef(null); + const isCarryingActiveFlow = isFlowActive && flowStage === stageThreshold; + + // GSAP continuous photon glider + useEffect(() => { + if (reduce || !packetRef.current) return; + + const ctx = gsap.context(() => { + // Primary photon packet + gsap.fromTo( + packetRef.current, + { x: 0, opacity: 0, scale: 0.7 }, + { + x: 52, + opacity: 1, + scale: 1.2, + duration: isCarryingActiveFlow ? 0.7 : tone === 'brand' ? 1.5 : 2.2, + repeat: -1, + ease: 'power1.inOut', + keyframes: [ + { opacity: 0, scale: 0.7, duration: 0.1 }, + { opacity: 1, scale: 1.2, duration: 0.2 }, + { opacity: 1, scale: 1.1, duration: 0.8 }, + { opacity: 0, scale: 0.8, duration: 0.2 }, + ], + } + ); + + // Secondary trailing micro-spark packet + if (sparkRef.current) { + gsap.fromTo( + sparkRef.current, + { x: -6, opacity: 0 }, + { + x: 48, + opacity: 0.8, + duration: isCarryingActiveFlow ? 0.7 : tone === 'brand' ? 1.5 : 2.2, + repeat: -1, + delay: 0.25, + ease: 'power1.inOut', + keyframes: [ + { opacity: 0, duration: 0.1 }, + { opacity: 0.75, duration: 0.3 }, + { opacity: 0.75, duration: 0.7 }, + { opacity: 0, duration: 0.2 }, + ], + } + ); + } + }); + + return () => ctx.revert(); + }, [reduce, tone, isCarryingActiveFlow]); + + return ( +
+
+ {/* Floating Telemetry Micro-Badge */} + {badgeText && ( + + {badgeText} + + )} + + {/* Animated Laser Pipeline SVG */} + + + + + + + + + + + + + + {/* Conduit Rail Track */} + + + {/* Animated Flowing Laser Core */} + {!reduce && ( + + )} + + {/* Layered Sci-Fi Cascading Chevrons (» » ») */} + + + + + + + + {/* Traveling Primary Photon Particle */} + {!reduce && ( +
+ )} + + {/* Trailing Micro-Spark */} + {!reduce && isCarryingActiveFlow && ( +
+ )} +
+
+ ); +} + +CircuitConveyor.propTypes = { + tone: PropTypes.string, + isFlowActive: PropTypes.bool, + flowStage: PropTypes.number, + stageThreshold: PropTypes.number, + badgeText: PropTypes.string, +}; + +export default function AgentPipeline({ selectedAgentId, onSelectAgent, onFlowStepChange }) { + const eventAgents = eventDrivenAgents(); + const taskAgents = taskDrivenAgents(); + + // Interactive Flow State + const [isFlowRunning, setIsFlowRunning] = useState(false); + const [flowStep, setFlowStep] = useState(0); // 0: idle, 1: source, 2: bus, 3: ai, 4: outcome + const [activeScenario, setActiveScenario] = useState('order_dispatch'); // 'order_dispatch' | 'rider_stall' | 'route_opt' + const [orderId, setOrderId] = useState('ORD-9428'); + const timerRef = useRef(null); + + const scenarioMeta = { + order_dispatch: { + title: 'Order Dispatch & Geo-Cluster Flow', + sourceText: `Order #${orderId} Created via Go Fiber -> Emitting booking.assigned`, + busText: "Stream 'ASSIGNMENTS' ACK -> Broadcast to DispatchAgent consumer", + aiAgent: 'DISPATCH_AGENT', + aiText: 'DispatchAgent scanning 10km H3 geo-cell -> Rider presence validated', + outcomeText: 'Autonomy safe gate verified -> Ops Desk notification pushed (0 DB writes)', + }, + rider_stall: { + title: 'Rider Exception & Stall Detection Flow', + sourceText: `Rider Miler-44 ping timeout (120s) -> Emitting miler.location.updated`, + busText: "Stream 'RIDERS_LOC' ACK -> Stalled pull loop triggered", + aiAgent: 'EXCEPTION_AGENT', + aiText: 'ExceptionAgent analyzing ping silence -> Order booking stall flag generated', + outcomeText: 'Rate limiter verified (1/zone/day) -> High-priority Ops alert triggered', + }, + route_opt: { + title: 'Batch Routing & Waypoint Optimization Flow', + sourceText: `Hub-3 batch dispatch triggered -> 8 deliveries sequenced in Go`, + busText: 'Internal task queue -> Handed to JARVIS master orchestrator', + aiAgent: 'ROUTE_OPTIMIZER', + aiText: 'Route Optimizer running TWVRP solver -> 10-min cache key generated', + outcomeText: 'Waypoints compiled -> Sent to rider app client via WebSocket', + }, + }[activeScenario]; + + // Stop / Reset flow + const stopFlow = () => { + if (timerRef.current) clearTimeout(timerRef.current); + setIsFlowRunning(false); + setFlowStep(0); + }; + + // Run Step-by-Step Flow Simulation + const startFlow = (scenario = activeScenario) => { + stopFlow(); + setActiveScenario(scenario); + setIsFlowRunning(true); + const newOrderId = `ORD-${Math.floor(1000 + Math.random() * 9000)}`; + setOrderId(newOrderId); + + // Step 1: Order Creation (Source) + setFlowStep(1); + if (onFlowStepChange) { + onFlowStepChange({ + step: 1, + title: 'Order Ingestion', + stage: 'doormile_backend', + detail: `New order #${newOrderId} ingested into Go Fiber backend`, + agentId: 'JARVIS', + }); + } + + // Step 2: NATS JetStream Ingestion + timerRef.current = setTimeout(() => { + setFlowStep(2); + if (onFlowStepChange) { + onFlowStepChange({ + step: 2, + title: 'Event Bus Transit', + stage: 'NATS JetStream', + detail: `Event published to JetStream stream 'ASSIGNMENTS' (8ms)`, + agentId: null, + }); + } + + // Step 3: AI Engine Swarm Processing + timerRef.current = setTimeout(() => { + setFlowStep(3); + const targetAgent = scenarioMeta.aiAgent; + onSelectAgent(targetAgent); + if (onFlowStepChange) { + onFlowStepChange({ + step: 3, + title: 'AI Swarm Reasoning', + stage: 'AI_engine', + detail: `${targetAgent} processing assignment heuristics`, + agentId: targetAgent, + }); + } + + // Step 4: Outcome & Safety Enforcement + timerRef.current = setTimeout(() => { + setFlowStep(4); + if (onFlowStepChange) { + onFlowStepChange({ + step: 4, + title: 'Safe Escalation', + stage: 'Console Alert', + detail: `Autonomy gates safe: Alert dispatched to Ops Console`, + agentId: targetAgent, + }); + } + + // Complete & return to idle after 3s + timerRef.current = setTimeout(() => { + setIsFlowRunning(false); + }, 3200); + }, 1600); + }, 1600); + }, 1400); + }; + + useEffect(() => { + return () => { + if (timerRef.current) clearTimeout(timerRef.current); + }; + }, []); + + return ( +
+ {/* ═══════════════════════════════════════════════════════════════════ + INTERACTIVE FLOW CONTROL BAR & STEP TRACKER + ═══════════════════════════════════════════════════════════════════ */} +
+
+
+ + + Interactive Order Flow Simulator + + · Observe how real events traverse the architecture +
+ + {/* Action Buttons */} +
+ {/* Scenario dropdown buttons */} +
+ + + + + +
+ + {/* Reset / Stop */} + {isFlowRunning && ( + + )} +
+
+ + {/* Dynamic 4-Step Progress Ribbon (Clean & Professional) */} +
+ {[ + { step: 1, label: '1. Order Created', sub: 'doormile_backend (Go)' }, + { step: 2, label: '2. NATS JetStream', sub: 'Stream: ASSIGNMENTS' }, + { step: 3, label: '3. AI Engine Reasoning', sub: `${scenarioMeta.aiAgent}` }, + { step: 4, label: '4. Safe Escalation', sub: 'Ops Alert · 0 DB Writes' }, + ].map((item) => { + const isCurrent = flowStep === item.step; + const isDone = flowStep > item.step; + + return ( +
+
+ {isDone ? : item.step} +
+
+ + {item.label} + + {item.sub} +
+
+ ); + })} +
+
+ + {/* ═══════════════════════════════════════════════════════════════════ + TOPOLOGY DIAGRAM WITH DYNAMIC STEP HIGHLIGHTING & PARCELS + ═══════════════════════════════════════════════════════════════════ */} +
+ {/* Floating Active Event Parcel Indicator */} + + {isFlowRunning && ( + + + Event Packet: #{orderId} + + Stage {flowStep}/4 + + + )} + + + {/* 1 · SOURCE PRODUCER */} + +
+ 1 · Source + + Go Fiber + +
+ +
+ + doormile_backend +
+ +

+ {flowStep === 1 ? ( + + ⚡ {scenarioMeta.sourceText} + + ) : ( + 'Assigns riders internally, then streams status notifications onto the message bus.' + )} +

+ +
+ {SOURCE_SUBJECTS.map((s) => ( + + + {s} + + ))} +
+
+ + {/* Conveyor 1 */} + + + {/* 2 · BUS STREAM */} + +
+ + 2 · Message Bus + + +
+ +
+ + NATS JetStream +
+ +

+ {flowStep === 2 ? ( + + ⚡ {scenarioMeta.busText} + + ) : ( + '6 durable event streams. Best-effort delivery — publish never blocks live dispatch.' + )} +

+ +
+ + + Dropped 20 Sep (Stalled) + +
+
+ + {/* Conveyor 2 */} + + + {/* 3 · AGENT SWARM */} + +
+ + 3 · Intelligence Swarm + + + 9 Specialized Agents + +
+ + {/* Real-time reasoning badge during Step 3 */} + {flowStep === 3 && ( +
+ + {scenarioMeta.aiText} +
+ )} + +
+ {/* Event-driven sub-cluster */} +
+
+ + + Event-Driven Subscribers + + + Autonomous + +
+ +
+ {eventAgents.map((agent) => { + const isSelected = selectedAgentId === agent.id; + const isAgentActiveInFlow = flowStep === 3 && scenarioMeta.aiAgent === agent.id; + + const statusChipTone = { + active: 'bg-emerald-50 text-emerald-800 border-emerald-200 dark:bg-emerald-950/40 dark:text-emerald-300 dark:border-emerald-800', + degraded: 'bg-amber-50 text-amber-800 border-amber-200 dark:bg-amber-950/40 dark:text-amber-300 dark:border-amber-800', + idle: 'bg-slate-50 text-slate-600 border-slate-200 dark:bg-slate-800/40 dark:text-slate-400 dark:border-slate-700', + }[agent.status]; + + return ( + onSelectAgent(agent.id)} + className={cn( + 'flex flex-col gap-2 rounded-lg border p-2.5 text-left transition-all cursor-pointer relative bg-surface', + isAgentActiveInFlow + ? 'border-brand ring-2 ring-brand/30 shadow-xs' + : isSelected + ? 'border-brand ring-1 ring-brand/20' + : 'border-border hover:border-border-strong hover:shadow-2xs' + )} + > + {/* Top Row: Agent Name + High-tech Glowing Radar Beacon */} +
+ + {agent.name} + +
+ +
+
+ + {/* Middle Row: Subscription Event Badge */} +
+ + {agent.subscribes[0] || 'Direct NATS'} + +
+ + {/* Bottom Row: Event count & Status indicator */} +
+ 0 + ? 'text-emerald-700 dark:text-emerald-400 font-bold' + : 'text-ink-4' + )} + > + {agent.events > 0 ? `${agent.events} events` : '0 events'} + + + {agent.status} + +
+
+ ); + })} +
+
+ + {/* Task-driven sub-cluster */} +
+
+ + + Task-Driven Orchestration + + + JARVIS Delegated + +
+ +
+ + +
+ {taskAgents.map((agent) => { + const isAgentActiveInFlow = flowStep === 3 && scenarioMeta.aiAgent === agent.id; + + return ( + + ); + })} +
+
+
+
+
+ + {/* Conveyor 3 */} + + + {/* 4 · OUTCOME */} + +
+ + 4 · Safe Escalation + + +
+ + Alert Only (Never Act) + +

+ {flowStep === 4 ? ( + + ✓ {scenarioMeta.outcomeText} + + ) : ( + 'Emits ops desk alerts & customer delay SMS. Strictly limited to 1 escalation/zone/day.' + )} +

+ +
+
+ + {flowStep === 4 ? 'Flow Verified & Audited' : 'Autonomy Gates Locked'} +
+
+
+
+
+ ); +} + +AgentPipeline.propTypes = { + selectedAgentId: PropTypes.string, + onSelectAgent: PropTypes.func.isRequired, + onFlowStepChange: PropTypes.func, +}; diff --git a/src/pages/doormile/agents/AgentWorkflowView.jsx b/src/pages/doormile/agents/AgentWorkflowView.jsx new file mode 100644 index 0000000..1eb26c2 --- /dev/null +++ b/src/pages/doormile/agents/AgentWorkflowView.jsx @@ -0,0 +1,312 @@ +import React, { useEffect, useRef, useState } from 'react'; +import PropTypes from 'prop-types'; +import { AnimatePresence, motion, useReducedMotion } from 'framer-motion'; +import { + ArrowLeft, + ArrowRight, + CheckCircle2, + ChevronRight, + ExternalLink, + Layers, + Pause, + Play, + RotateCcw, + ShieldCheck, + Sparkles, + Zap, +} from 'lucide-react'; + +import { cn } from '@/lib/utils'; +import { AGENT_WORKFLOWS } from './agentWorkflowsData'; + +export default function AgentWorkflowView({ + agentId, + onBackToNetwork, + onOpenTechnicalDrawer, +}) { + const workflow = AGENT_WORKFLOWS[agentId] || AGENT_WORKFLOWS.ORDER_AGENT; + const reduce = useReducedMotion(); + + // Active step index in execution animation (0 to workflow.steps.length - 1) + const [activeStepIndex, setActiveStepIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(true); + const [selectedStepDetail, setSelectedStepDetail] = useState(null); + + const steps = workflow.steps; + const timerRef = useRef(null); + + // Animated execution loop: advances step by step + useEffect(() => { + if (!isPlaying) return; + + timerRef.current = setTimeout(() => { + setActiveStepIndex((prev) => (prev + 1) % steps.length); + }, 1800); + + return () => { + if (timerRef.current) clearTimeout(timerRef.current); + }; + }, [isPlaying, activeStepIndex, steps.length]); + + const activeStep = steps[activeStepIndex]; + + return ( + + {/* ── Top Bar: Back Button & Agent Title ── */} +
+
+ + +
+ +
+ +

+ {workflow.name} +

+ + — {workflow.subtitle} + +
+
+ + {/* Right actions: Pause/Play & Technical Drawer */} +
+ + + +
+
+ + {/* ── Workflow Description Banner ── */} +
+
+ +

+ {workflow.summary} +

+
+
+ + Autonomy Gated (0 DB writes) +
+
+ + {/* ═══════════════════════════════════════════════════════════════════ + THE WORKFLOW TIMELINE / PIPELINE (HERO VISUALIZATION) + ═══════════════════════════════════════════════════════════════════ */} +
+
+
+ + Execution Timeline + + + · Step {activeStepIndex + 1} of {steps.length} + +
+ + Click any step to inspect details + +
+ + {/* Responsive Pipeline Steps (Horizontal on Desktop, Vertical on Mobile) */} +
+ {steps.map((stepItem, index) => { + const isCompleted = index < activeStepIndex; + const isProcessing = index === activeStepIndex; + const isWaiting = index > activeStepIndex; + const isSelected = selectedStepDetail?.id === stepItem.id; + + return ( + setSelectedStepDetail(stepItem)} + className={cn( + 'group relative flex flex-col gap-2 rounded-xl border p-3.5 transition-all duration-300 cursor-pointer bg-surface text-left', + isProcessing + ? 'border-brand ring-2 ring-brand/30 shadow-md scale-102 bg-surface-elevated z-10' + : isSelected + ? 'border-brand ring-1 ring-brand/40 bg-surface-elevated shadow-xs' + : isCompleted + ? 'border-border/80 hover:border-border hover:bg-surface-elevated' + : 'border-border/50 opacity-60 hover:opacity-90' + )} + > + {/* Step Index & State Indicator */} +
+ + 0{index + 1} + + + {/* Clean Visual State Beacon */} + {isCompleted ? ( + + ✓ + + ) : isProcessing ? ( + + + + + ) : ( + + )} +
+ + {/* Step Title */} +

+ {stepItem.title} +

+ + {/* Short Human-Readable Description (Max 2 lines) */} +

+ {stepItem.description} +

+ + {/* Technical Output Tag */} +
+ + {stepItem.output} + +
+
+ ); + })} +
+ + {/* ── Agent-to-Agent Transition Handover (Requirement: Show Where Work Goes Next) ── */} +
+
+ + + Workflow Handover: + + + Upon completion, validated events transit directly to: + +
+ +
+ + {workflow.nextAgent} + + +
+
+
+ + {/* ── Selected Step Detail Inspector ── */} + + {selectedStepDetail && ( + +
+
+ +

+ Step Detail: {selectedStepDetail.title} +

+
+ +
+ +
+
+ + What happens? + +

+ {selectedStepDetail.description} +

+
+ +
+ + Input Trigger + + + {selectedStepDetail.input} + +
+ +
+ + Output Result + + + {selectedStepDetail.output} + +
+
+
+ )} +
+ + ); +} + +AgentWorkflowView.propTypes = { + agentId: PropTypes.string.isRequired, + onBackToNetwork: PropTypes.func.isRequired, + onOpenTechnicalDrawer: PropTypes.func.isRequired, +}; diff --git a/src/pages/doormile/agents/Agents.jsx b/src/pages/doormile/agents/Agents.jsx new file mode 100644 index 0000000..4201525 --- /dev/null +++ b/src/pages/doormile/agents/Agents.jsx @@ -0,0 +1,333 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { AnimatePresence, motion } from 'framer-motion'; +import { + Activity, + AlertTriangle, + ArrowUpRight, + Bot, + CheckCircle2, + ChevronRight, + ExternalLink, + Layers, + Radio, + RefreshCw, + Server, + ShieldAlert, + ShieldCheck, + Terminal, + Zap, +} from 'lucide-react'; + +import { PageHeader } from '@/components/ds'; +import { cn } from '@/lib/utils'; +import { + AGENTS, + LOG_ERROR_LINES, + LOG_TOTAL_LINES, + RUN_LOG, + agentById, + networkStats, +} from '@/lib/agentNetwork'; +import AgentFlowVisualizer from './AgentFlowVisualizer'; +import AgentInspectorDrawer from './AgentInspectorDrawer'; + +export default function Agents() { + const [selectedAgentId, setSelectedAgentId] = useState('DISPATCH_AGENT'); + const [isDrawerOpen, setIsDrawerOpen] = useState(false); + const [activeLogModal, setActiveLogModal] = useState(null); + + // Live Activity Stream (3–5 recent events, Requirement: LIVE ACTIVITY) + const [liveActivities, setLiveActivities] = useState([ + { + id: 1, + time: '16:24:03', + agent: 'Dispatch', + action: 'Rider assignment completed', + color: '#10b981', + }, + { + id: 2, + time: '16:24:01', + agent: 'Exception', + action: 'Rider inactivity detected', + color: '#f59e0b', + }, + { + id: 3, + time: '16:23:58', + agent: 'JARVIS', + action: 'Assignment failure routed', + color: '#6366f1', + }, + ]); + + const handleOpenInspector = (agentId) => { + setSelectedAgentId(agentId); + setIsDrawerOpen(true); + }; + + const handleActivityLogged = (newActivity) => { + setLiveActivities((prev) => [ + { + id: Date.now(), + time: newActivity.time, + agent: newActivity.agent, + action: newActivity.action, + color: newActivity.color, + }, + ...prev.slice(0, 3), // Keep strictly 4 items max to avoid clutter + ]); + }; + + return ( +
+ {/* ═══════════════════════════════════════════════════════════════════ + COMPACT TOP STATUS BAR (REQUIREMENT: TOP STATUS BAR) + Clean, minimal telemetry ribbon: ● 1 Processing · ● 1 Warning · ● 7 Idle · 🔒 Autonomy locked + ═══════════════════════════════════════════════════════════════════ */} +
+ {/* Left: Identity */} +
+ +

+ Doormile AI Agent Network +

+ + · Real-Time Workflow Visualizer + +
+ + {/* Right: Clean Status Indicators */} +
+ {/* 1 Processing */} +
+ + 1 Processing +
+ + {/* 1 Warning */} +
+ + 1 Warning +
+ + {/* 7 Idle */} +
+ + 7 Idle +
+ + {/* 🔒 Autonomy Locked */} +
+ + Autonomy: 0 / 8 +
+
+
+ + {/* ═══════════════════════════════════════════════════════════════════ + THE HERO: 4-STAGE AI FLOW VISUALIZER & DEDICATED WORKFLOWS + ═══════════════════════════════════════════════════════════════════ */} + + + {/* ═══════════════════════════════════════════════════════════════════ + SECONDARY SECTIONS: LIVE ACTIVITY & GROUPED SYSTEM DIAGNOSTICS + ═══════════════════════════════════════════════════════════════════ */} +
+ {/* ── 1. Compact Live Activity Feed ── */} +
+
+
+ +

+ Live Activity +

+
+ + Latest events + +
+ +
+ + {liveActivities.slice(0, 4).map((activity) => ( + +
+ + + {activity.agent} + + · + + {activity.action} + +
+ + + {activity.time} + +
+ ))} +
+
+
+ + {/* ── 2. Grouped System Diagnostics (Requirement: Group Repeated Errors) ── */} +
+
+
+ +

+ System Diagnostics +

+
+ + Grouped health checks + +
+ +
+ {/* Grouped Error 1: NATS connection closed */} +
+ setActiveLogModal({ + title: 'NATS connection closed', + occurrences: '8,594 occurrences', + detail: + 'Both pull loops in ExceptionAgent repeatedly retried a closed NATS handle without backoff reconnection.', + solution: + 'Solution: Implemented auto-reconnect backoff on consumer handle.', + }) + } + className="flex items-center justify-between rounded-lg border border-rose-200 dark:border-rose-900/60 bg-rose-50/50 dark:bg-rose-950/20 p-2.5 text-xs hover:border-rose-300 transition-colors cursor-pointer group" + > +
+ + + NATS connection closed + +
+ +
+ + 8,594 occurrences + + +
+
+ + {/* Grouped Error 2: Unknown zone */} +
+ setActiveLogModal({ + title: 'Unknown zone failure', + occurrences: '2 occurrences', + detail: + 'Go backend published booking.assignment_failed without a zone_id attribute. DispatchAgent automatically re-bucketed failures into H3 geo-cells.', + solution: + 'Solution: Added fallback geo-cell matching in dispatch pipeline.', + }) + } + className="flex items-center justify-between rounded-lg border border-amber-200 dark:border-amber-900/60 bg-amber-50/50 dark:bg-amber-950/20 p-2.5 text-xs hover:border-amber-300 transition-colors cursor-pointer group" + > +
+ + + Unknown zone + +
+ +
+ + 2 occurrences + + +
+
+
+
+
+ + {/* ═══════════════════════════════════════════════════════════════════ + DETAIL INSPECTOR DRAWER (TECHNICAL DETAILS LIVE HERE ONLY) + ═══════════════════════════════════════════════════════════════════ */} + setIsDrawerOpen(false)} + /> + + {/* ── Diagnostics Modal for Grouped Errors ── */} + + {activeLogModal && ( + <> + setActiveLogModal(null)} + className="fixed inset-0 z-50 bg-black/40 backdrop-blur-2xs" + /> + +
+
+ +

+ {activeLogModal.title} +

+
+ + {activeLogModal.occurrences} + +
+ +
+

+ {activeLogModal.detail} +

+
+ + Remediation: + + {activeLogModal.solution} +
+
+ +
+ +
+
+ + )} +
+
+ ); +} diff --git a/src/pages/doormile/agents/CountUp.jsx b/src/pages/doormile/agents/CountUp.jsx new file mode 100644 index 0000000..e4d5773 --- /dev/null +++ b/src/pages/doormile/agents/CountUp.jsx @@ -0,0 +1,37 @@ +import { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { animate, useReducedMotion } from 'framer-motion'; + +/** + * A number that counts up to its value. + * + * On a board where most figures are zero, watching one climb to 9 while its + * neighbour sits at 0 is the fastest way to see which of them is a real + * measurement — the motion itself carries the contrast. + * + * It settles on the exact target rather than wherever the last frame landed, + * because a KPI that reads 8 when the answer is 9 is worse than one that never + * animated. Under `prefers-reduced-motion` it renders the value immediately. + */ +export default function CountUp({ to, duration = 0.9 }) { + const reduce = useReducedMotion(); + const [shown, setShown] = useState(reduce ? to : 0); + + useEffect(() => { + if (reduce) { + setShown(to); + return undefined; + } + const controls = animate(0, to, { + duration, + ease: [0.22, 1, 0.36, 1], + onUpdate: (v) => setShown(Math.round(v)), + onComplete: () => setShown(to), + }); + return () => controls.stop(); + }, [to, duration, reduce]); + + return <>{shown}; +} + +CountUp.propTypes = { to: PropTypes.number.isRequired, duration: PropTypes.number }; diff --git a/src/pages/doormile/agents/agentWorkflowsData.js b/src/pages/doormile/agents/agentWorkflowsData.js new file mode 100644 index 0000000..85b7814 --- /dev/null +++ b/src/pages/doormile/agents/agentWorkflowsData.js @@ -0,0 +1,453 @@ +// ==============================|| 8 SPECIALIZED AGENT WORKFLOWS ||============================== // +// +// Tailored, human-readable step-by-step execution pipelines for each of the 8 agents. +// JARVIS is the central orchestration layer and coordinates them. + +export const AGENT_WORKFLOWS = { + ORDER_AGENT: { + id: 'ORDER_AGENT', + name: 'Order Agent', + shortName: 'Order', + status: 'idle', + color: '#3b82f6', // blue + subtitle: 'How an order moves through the system', + summary: 'Validates, categorizes, and prepares customer orders for fulfillment.', + nextAgent: 'Dispatch Agent', + steps: [ + { + id: 'create_order', + title: 'Create Order', + description: 'Customer places an order through the client app or API.', + input: 'customer.order_submitted', + output: 'order.raw_record', + }, + { + id: 'validate_order', + title: 'Validate Order', + description: 'Check that the order contains all required delivery details and payment.', + input: 'order.raw_record', + output: 'order.validated', + }, + { + id: 'check_customer', + title: 'Check Customer', + description: 'Verify customer delivery address, contact info, and special instructions.', + input: 'order.validated', + output: 'customer.verified', + }, + { + id: 'categorize_order', + title: 'Categorize Order', + description: 'Determine order priority, package dimensions, and service level.', + input: 'customer.verified', + output: 'order.categorized', + }, + { + id: 'publish_order', + title: 'Publish Order Event', + description: 'Broadcast order readiness to the internal event bus.', + input: 'order.categorized', + output: 'booking.created', + }, + { + id: 'send_dispatch', + title: 'Send to Dispatch', + description: 'Pass the order to the dispatch workflow for rider assignment.', + input: 'booking.created', + output: 'handoff: DISPATCH_AGENT', + }, + { + id: 'order_confirmed', + title: 'Order Confirmed', + description: 'The order is securely placed and ready for pickup execution.', + input: 'dispatch.acknowledged', + output: 'order.confirmed_active', + }, + ], + }, + + DISPATCH_AGENT: { + id: 'DISPATCH_AGENT', + name: 'Dispatch Agent', + shortName: 'Dispatch', + status: 'active', + color: '#10b981', // green + subtitle: 'How rider matching and order dispatch works', + summary: 'Monitors rider assignment, checks geo-proximity, and allocates deliveries.', + nextAgent: 'Fleet / Customer', + steps: [ + { + id: 'receive_order', + title: 'Receive Order', + description: 'Dispatch receives a new delivery request from the event bus.', + input: 'booking.assigned', + output: 'dispatch.received', + }, + { + id: 'check_riders', + title: 'Check Available Riders', + description: 'Find active riders currently clocked in and ready for work.', + input: 'dispatch.received', + output: 'riders.pool_filtered', + }, + { + id: 'check_zone', + title: 'Check Delivery Zone', + description: 'Check which riders are in the same 10km H3 geographic cell as the merchant.', + input: 'riders.pool_filtered', + output: 'riders.in_zone', + }, + { + id: 'find_rider', + title: 'Find Suitable Rider', + description: 'Select the best available rider using capacity, rating, and distance.', + input: 'riders.in_zone', + output: 'rider.selected', + }, + { + id: 'assign_rider', + title: 'Assign Rider', + description: 'Assign the delivery to the selected rider in the dispatch queue.', + input: 'rider.selected', + output: 'booking.assigned_miler', + }, + { + id: 'confirm_dispatch', + title: 'Confirm Dispatch', + description: 'Confirm that the delivery has been successfully allocated.', + input: 'booking.assigned_miler', + output: 'dispatch.verified_active', + }, + ], + }, + + EXCEPTION_AGENT: { + id: 'EXCEPTION_AGENT', + name: 'Exception Agent', + shortName: 'Exception', + status: 'degraded', + color: '#f59e0b', // amber + subtitle: 'How stall detection and operational issues are managed', + summary: 'Monitors rider GPS pings and booking progression to identify stalls.', + nextAgent: 'Operations Desk', + steps: [ + { + id: 'detect_activity', + title: 'Detect Unusual Activity', + description: 'Exception loop spots a rider whose GPS location has stayed silent >120s.', + input: 'miler.location.updated', + output: 'anomaly.stalled_candidate', + }, + { + id: 'check_severity', + title: 'Check Severity', + description: 'Assess the potential delay impact on the customer delivery promise.', + input: 'anomaly.stalled_candidate', + output: 'severity.calculated', + }, + { + id: 'identify_order', + title: 'Identify Affected Order', + description: 'Isolate the specific active booking associated with the stalled rider.', + input: 'severity.calculated', + output: 'order.isolated', + }, + { + id: 'determine_escalation', + title: 'Determine Escalation', + description: 'Check rate-limiter (1/zone/day) to prevent operations alert fatigue.', + input: 'order.isolated', + output: 'escalation.allowed', + }, + { + id: 'notify_operations', + title: 'Notify Operations', + description: 'Surface a high-priority intervention card on the operations console.', + input: 'escalation.allowed', + output: 'ops.alert_dispatched', + }, + { + id: 'track_resolution', + title: 'Track Resolution', + description: 'Monitor order until rider resumes movement or reassignment occurs.', + input: 'ops.alert_dispatched', + output: 'incident.resolved', + }, + ], + }, + + EXPRESS_DISPATCH_AGENT: { + id: 'EXPRESS_DISPATCH_AGENT', + name: 'Express Dispatch', + shortName: 'Express Dispatch', + status: 'idle', + color: '#8b5cf6', // purple + subtitle: 'How priority and VIP delivery requests are handled', + summary: 'Provides expedited single-drop fulfillment with minimal staging latency.', + nextAgent: 'Dispatch Agent', + steps: [ + { + id: 'receive_express', + title: 'Receive Express Request', + description: 'Immediate express dispatch request arrives from client booking path.', + input: 'express.dispatch_req', + output: 'express.ticket_open', + }, + { + id: 'check_priority', + title: 'Check Priority', + description: 'Verify VIP SLA, guaranteed transit window, and express pricing.', + input: 'express.ticket_open', + output: 'sla.verified', + }, + { + id: 'find_nearby', + title: 'Find Nearby Rider', + description: 'Query active riders within a tight 3km radius from pickup coordinate.', + input: 'sla.verified', + output: 'express_pool.ready', + }, + { + id: 'check_availability', + title: 'Check Rider Availability', + description: 'Verify that rider has zero active overlapping jobs and empty cargo.', + input: 'express_pool.ready', + output: 'rider.single_task_ready', + }, + { + id: 'assign_rider', + title: 'Assign Rider', + description: 'Reserve rider exclusively for direct point-to-point delivery.', + input: 'rider.single_task_ready', + output: 'express.allocated', + }, + { + id: 'confirm_express', + title: 'Confirm Express Dispatch', + description: 'Confirm direct express dispatch and stream ETA to recipient.', + input: 'express.allocated', + output: 'express.confirmed_transit', + }, + ], + }, + + ROUTE_OPTIMIZER: { + id: 'ROUTE_OPTIMIZER', + name: 'Route Optimizer', + shortName: 'Route Optimizer', + status: 'idle', + color: '#06b6d4', // cyan + subtitle: 'How multi-stop batch routes are solved and sequenced', + summary: 'Executes time-window vehicle routing optimization with a 10-minute cache.', + nextAgent: 'Fleet / Rider App', + steps: [ + { + id: 'receive_batch', + title: 'Receive Delivery Orders', + description: 'Batch of orders accumulated at local hub ready for route compilation.', + input: 'hub.batch_ready', + output: 'batch.unsequenced', + }, + { + id: 'group_zones', + title: 'Group Orders by Zone', + description: 'Cluster delivery waypoints by geographic neighborhood density.', + input: 'batch.unsequenced', + output: 'clusters.compiled', + }, + { + id: 'check_riders', + title: 'Check Available Riders', + description: 'Match cluster cargo volume against available rider vehicle capacities.', + input: 'clusters.compiled', + output: 'riders.capacity_matched', + }, + { + id: 'generate_routes', + title: 'Generate Route Options', + description: 'Run combinatorial solver for multi-stop pickup and drop sequences.', + input: 'riders.capacity_matched', + output: 'candidates.generated', + }, + { + id: 'optimize_time', + title: 'Optimize Time and Distance', + description: 'Select route permutation that minimizes mileage and traffic delays.', + input: 'candidates.generated', + output: 'route.optimal_selected', + }, + { + id: 'publish_route', + title: 'Publish Recommended Route', + description: 'Send sequenced waypoints directly to the rider mobile navigation app.', + input: 'route.optimal_selected', + output: 'route.published_cache', + }, + ], + }, + + HUB_AGENT: { + id: 'HUB_AGENT', + name: 'Hub Agent', + shortName: 'Hub', + status: 'idle', + color: '#eab308', // yellow + subtitle: 'How sorting centers and transit storage operate', + summary: 'Tracks inventory at each hub and balances dock intake vs staging.', + nextAgent: 'Fleet Agent', + steps: [ + { + id: 'receive_hub', + title: 'Receive Hub Event', + description: 'Package arrives at sorting facility for regional transit staging.', + input: 'hub.scan_arrival', + output: 'package.docked', + }, + { + id: 'check_capacity', + title: 'Check Hub Capacity', + description: 'Assess available storage bays and sorting queue volume.', + input: 'package.docked', + output: 'capacity.safe', + }, + { + id: 'check_incoming', + title: 'Check Incoming Deliveries', + description: 'Monitor inbound feeder vans arriving within the next 30 minutes.', + input: 'capacity.safe', + output: 'inbound.tracked', + }, + { + id: 'check_outgoing', + title: 'Check Outgoing Deliveries', + description: 'Stage orders for outbound last-mile departure windows.', + input: 'inbound.tracked', + output: 'outbound.batched', + }, + { + id: 'update_hub_state', + title: 'Update Hub State', + description: 'Update physical inventory counts in the hub ledger.', + input: 'outbound.batched', + output: 'hub.state_synced', + }, + { + id: 'notify_fleet', + title: 'Notify Relevant Agent', + description: 'Alert fleet and dispatch agents that outbound pallets are staged.', + input: 'hub.state_synced', + output: 'handoff: FLEET_AGENT', + }, + ], + }, + + FLEET_AGENT: { + id: 'FLEET_AGENT', + name: 'Fleet Agent', + shortName: 'Fleet', + status: 'idle', + color: '#ec4899', // pink + subtitle: 'How vehicles and rider assets are monitored', + summary: 'Maintains vehicle condition, battery state, and volumetric readiness.', + nextAgent: 'Dispatch Agent', + steps: [ + { + id: 'receive_vehicle', + title: 'Receive Vehicle Update', + description: 'Telemetry beacon received from connected EV bike or delivery van.', + input: 'vehicle.telemetry_ping', + output: 'telemetry.logged', + }, + { + id: 'check_status', + title: 'Check Vehicle Status', + description: 'Verify maintenance health, battery percentage, and inspection permits.', + input: 'telemetry.logged', + output: 'health.verified', + }, + { + id: 'check_capacity', + title: 'Check Capacity', + description: 'Confirm available thermal box volume and payload rating.', + input: 'health.verified', + output: 'volume.available', + }, + { + id: 'check_location', + title: 'Check Location', + description: 'Determine exact vehicle coordinates on the fleet live map.', + input: 'volume.available', + output: 'geo.resolved', + }, + { + id: 'determine_availability', + title: 'Determine Availability', + description: 'Mark vehicle ready for high-load or multi-drop route allocation.', + input: 'geo.resolved', + output: 'asset.available_flag', + }, + { + id: 'update_fleet_state', + title: 'Update Fleet State', + description: 'Sync updated vehicle availability with the dispatch routing engine.', + input: 'asset.available_flag', + output: 'fleet.pool_synced', + }, + ], + }, + + CUSTOMER_AGENT: { + id: 'CUSTOMER_AGENT', + name: 'Customer Agent', + shortName: 'Customer', + status: 'idle', + color: '#14b8a6', // teal + subtitle: 'How delivery notifications and delay updates are sent', + summary: 'Composes live tracking SMS and delay notifications across channels.', + nextAgent: 'Customer App / SMS', + steps: [ + { + id: 'receive_update', + title: 'Receive Delivery Update', + description: 'Order state changes (e.g. rider pickup, transit delay, arrival).', + input: 'order.milestone_reached', + output: 'milestone.evaluated', + }, + { + id: 'understand_impact', + title: 'Understand Customer Impact', + description: 'Calculate real-time ETA change and determine if customer needs alerting.', + input: 'milestone.evaluated', + output: 'eta.calculated', + }, + { + id: 'prepare_notification', + title: 'Prepare Notification', + description: 'Generate concise, reassuring message with live GPS tracking URL.', + input: 'eta.calculated', + output: 'message.drafted', + }, + { + id: 'choose_channel', + title: 'Choose Communication', + description: 'Select optimal delivery channel: WhatsApp, Push, or SMS text.', + input: 'message.drafted', + output: 'channel.selected', + }, + { + id: 'send_update', + title: 'Send Customer Update', + description: 'Transmit message payload through communications gateway.', + input: 'channel.selected', + output: 'gateway.dispatched', + }, + { + id: 'track_notification', + title: 'Track Notification', + description: 'Verify message delivery receipt and customer interaction.', + input: 'gateway.dispatched', + output: 'notification.delivered', + }, + ], + }, +}; diff --git a/tailwind.config.js b/tailwind.config.js index 0b4f8e2..8eeed2e 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -224,6 +224,31 @@ module.exports = { '0%, 49%': { opacity: '1' }, '50%, 100%': { opacity: '0' }, }, + /* Agent Network — every one of these is something the swarm does. + + carry : a parcel moving down the belt from one pipeline stage to + the next, so the diagram reads as a path rather than boxes + radar : the 10km GEOSEARCH the dispatch agent runs to find a rider + stutter: a retry that never lands. Stepped, not eased, so it reads + as mechanical and stuck rather than alive and breathing + belt : a conveyor running under the one agent carrying load + grow-x : a load bar filling from zero, so it is a measurement */ + carry: { + '0%': { transform: 'translateX(4px)', opacity: '0' }, + '14%, 78%': { opacity: '1' }, + '100%': { transform: 'translateX(34px)', opacity: '0' }, + }, + radar: { + '0%': { boxShadow: '0 0 0 0 rgb(21 128 61 / 0.55)' }, + '70%': { boxShadow: '0 0 0 9px rgb(21 128 61 / 0)' }, + '100%': { boxShadow: '0 0 0 0 rgb(21 128 61 / 0)' }, + }, + stutter: { + '0%, 100%': { boxShadow: '0 0 0 0 rgb(180 83 9 / 0.6)' }, + '50%': { boxShadow: '0 0 0 4px rgb(180 83 9 / 0)' }, + }, + belt: { to: { backgroundPosition: '26px 0' } }, + 'grow-x': { from: { transform: 'scaleX(0)' }, to: { transform: 'scaleX(1)' } }, }, animation: { 'accordion-down': 'accordion-down 0.2s ease-out', @@ -236,6 +261,12 @@ module.exports = { typing: 'typing 1.2s var(--ease-in-out) infinite', 'pulse-ring': 'pulse-ring 2s var(--ease-out) infinite', caret: 'caret 1s steps(1) infinite', + carry: 'carry 2.3s cubic-bezier(.45,0,.55,1) infinite', + 'carry-slow': 'carry 3.1s cubic-bezier(.45,0,.55,1) .6s infinite', + radar: 'radar 2.4s var(--ease-out) infinite', + stutter: 'stutter 1.05s steps(3, end) infinite', + belt: 'belt 1s linear infinite', + 'grow-x': 'grow-x .7s var(--ease-out) .15s both', }, }, }, diff --git a/tests/integration/agentsPage.test.jsx b/tests/integration/agentsPage.test.jsx new file mode 100644 index 0000000..2111851 --- /dev/null +++ b/tests/integration/agentsPage.test.jsx @@ -0,0 +1,107 @@ +import React from 'react'; +import { render, screen, fireEvent, within } from '@testing-library/react'; + +jest.mock('lucide-react', () => + new Proxy( + {}, + { + get: (_t, prop) => + prop === '__esModule' ? true : (props) => , + } + ) +); + +import Agents from '@/pages/doormile/agents/Agents'; + +/** + * The Agents page. + * + * It renders a hand-maintained table (lib/agentNetwork.js) rather than a live + * feed, so the risk here is not stale data — agentNetwork.test.js guards the + * table — it is that the page silently drops something. A detail panel that + * renders an empty string, or a card whose status has no colour, looks like a + * quiet page rather than a broken one. + */ + +describe('the Agents page', () => { + it('lists all nine agents', () => { + render(); + ['JARVIS', 'Dispatch', 'Exception', 'Express Dispatch', 'Order', 'Route Optimizer', 'Hub', 'Fleet', 'Customer'].forEach( + (name) => { + expect(screen.getAllByText(name).length).toBeGreaterThan(0); + } + ); + }); + + it('opens on Dispatch rather than an empty panel', () => { + // It is the only agent with anything to show. Landing on a blank detail + // panel reads as a broken page. + render(); + expect(screen.getByText('DISPATCH_AGENT')).toBeInTheDocument(); + expect(screen.getByText(/only agent doing real work/i)).toBeInTheDocument(); + }); + + it('swaps the whole detail panel when another agent is picked', () => { + render(); + fireEvent.click(screen.getByRole('button', { name: /Exception/ })); + + expect(screen.getByText('EXCEPTION_AGENT')).toBeInTheDocument(); + expect(screen.getByText(/17,188 of the 18,146 lines/)).toBeInTheDocument(); + // and the previous agent's record is gone, not merely scrolled away + expect(screen.queryByText('DISPATCH_AGENT')).not.toBeInTheDocument(); + }); + + it('marks the selected card for a screen reader, not just with a colour', () => { + render(); + const exception = screen.getByRole('button', { name: /Exception/ }); + expect(exception).toHaveAttribute('aria-pressed', 'false'); + fireEvent.click(exception); + expect(exception).toHaveAttribute('aria-pressed', 'true'); + }); + + it('says a task-driven agent listens to nothing, rather than leaving a blank', () => { + render(); + fireEvent.click(screen.getByRole('button', { name: /Fleet/ })); + expect(screen.getByText(/runs only when JARVIS hands it a task/i)).toBeInTheDocument(); + }); + + it('shows the pipeline, both halves of it', () => { + render(); + expect(screen.getByText('doormile_backend')).toBeInTheDocument(); + expect(screen.getByText('NATS JetStream')).toBeInTheDocument(); + expect(screen.getByText('Event-driven')).toBeInTheDocument(); + expect(screen.getByText('Task-driven')).toBeInTheDocument(); + expect(screen.getByText('Alert, never act')).toBeInTheDocument(); + }); + + it('states that nothing is switched on, in the KPI row', () => { + // The page's central claim: the swarm observes and alerts, and does not + // write to the system. + render(); + expect(screen.getByText(/Gates on/)).toBeInTheDocument(); + expect(screen.getByText(/All autonomy gates OFF/)).toBeInTheDocument(); + }); + + it('admits the figures are a snapshot, not a feed', () => { + // A status board that looks live and is four days old is worse than one + // that says how old it is. + render(); + expect(screen.getAllByText(/Last run · 16–20 Sep 2026/).length).toBeGreaterThan(0); + }); + + it('shows the repeated-error count on the log rows', () => { + render(); + const errs = screen.getAllByText('×8,594'); + expect(errs).toHaveLength(2); + }); + + it('every card carries a real trigger label, not undefined', () => { + const { container } = render(); + const cards = container.querySelectorAll('button[aria-pressed]'); + expect(cards).toHaveLength(9); + cards.forEach((card) => { + const text = within(card).getByText(/NATS event|NATS pull|JARVIS task|In-process/); + expect(text).toBeInTheDocument(); + }); + }); +}); diff --git a/tests/lib/agentNetwork.test.js b/tests/lib/agentNetwork.test.js new file mode 100644 index 0000000..5d02313 --- /dev/null +++ b/tests/lib/agentNetwork.test.js @@ -0,0 +1,153 @@ +import { + AGENTS, + LOG_ERROR_LINES, + LOG_TOTAL_LINES, + RUN_LOG, + TRIGGER, + agentById, + eventDrivenAgents, + networkStats, + taskDrivenAgents, +} from '@/lib/agentNetwork'; + +/** + * The agent table behind the Agents page. + * + * It is a hand-maintained snapshot, not a feed — AI_engine exposes no endpoint + * this console can reach. That makes these tests load-bearing in an unusual + * way: they are the only thing standing between a typo in the table and a + * status board that states something untrue with total confidence. + */ + +describe('the swarm as recorded', () => { + it('is nine agents — JARVIS and eight specialised', () => { + expect(AGENTS).toHaveLength(9); + expect(AGENTS.map((a) => a.id)).toContain('JARVIS'); + }); + + it('gives every agent a unique id', () => { + const ids = AGENTS.map((a) => a.id); + expect(new Set(ids).size).toBe(ids.length); + }); + + it('gives every agent a trigger the page can label', () => { + // A trigger with no entry in TRIGGER renders as `undefined` on the card — + // silently, because React prints nothing for it. + AGENTS.forEach((a) => { + expect(Object.keys(TRIGGER)).toContain(a.trigger); + }); + }); + + it('gives every agent the fields the detail panel reads', () => { + AGENTS.forEach((a) => { + expect(typeof a.detail).toBe('string'); + expect(a.detail.length).toBeGreaterThan(20); + expect(typeof a.publishes).toBe('string'); + expect(typeof a.api).toBe('string'); + expect(typeof a.autonomy).toBe('string'); + expect(typeof a.lastSeen).toBe('string'); + expect(Array.isArray(a.subscribes)).toBe(true); + }); + }); + + it('uses only the four states the page can colour', () => { + // An unknown status falls through TONE to `undefined`, and the card then + // renders with no dot, no badge and no bar — invisible breakage. + AGENTS.forEach((a) => { + expect(['active', 'running', 'degraded', 'idle']).toContain(a.status); + }); + }); +}); + +describe('the split that the page is really about', () => { + it('three agents subscribe to the bus themselves', () => { + expect(eventDrivenAgents().map((a) => a.id)).toEqual([ + 'DISPATCH_AGENT', + 'EXCEPTION_AGENT', + 'EXPRESS_DISPATCH_AGENT', + ]); + }); + + it('five only run when JARVIS hands them work', () => { + expect(taskDrivenAgents()).toHaveLength(5); + }); + + it('every event-driven agent names at least one subject', () => { + // The pipeline chip prints `subscribes[0]`. An empty array there renders a + // blank line where a NATS subject should be. + eventDrivenAgents().forEach((a) => { + expect(a.subscribes.length).toBeGreaterThan(0); + }); + }); + + it('no task-driven agent claims to subscribe to anything', () => { + // The whole point of the second group: they cannot be woken by an event. + taskDrivenAgents().forEach((a) => { + expect(a.subscribes).toEqual([]); + }); + }); +}); + +describe('the headline numbers are derived, never written down', () => { + it('counts what the cards underneath actually show', () => { + // A dashboard that says "3 active" over three grey dots is the failure + // this guards. Every stat comes off the same table the grid renders. + const s = networkStats(); + expect(s.total).toBe(AGENTS.length); + expect(s.working).toBe(AGENTS.filter((a) => a.status === 'active').length); + expect(s.degraded).toBe(AGENTS.filter((a) => a.status === 'degraded').length); + expect(s.working + s.degraded + s.idle + 1).toBe(s.total); // +1 for JARVIS, which is `running` + }); + + it('reports exactly one agent doing real work', () => { + expect(networkStats().working).toBe(1); + }); + + it('reports no autonomy gate switched on', () => { + // Stated on the page as a promise: the swarm observes and alerts, and does + // not write to the system. If a gate is ever turned on, this fails and the + // page's own subtitle has to change with it. + const s = networkStats(); + expect(s.autonomyOn).toBe(0); + expect(s.gated).toBe(3); + }); + + it('recomputes for a different set rather than returning constants', () => { + const two = networkStats([ + { status: 'active', events: 5, gated: true }, + { status: 'idle', events: 0, gated: false }, + ]); + expect(two.total).toBe(2); + expect(two.working).toBe(1); + expect(two.events).toBe(5); + expect(two.gated).toBe(1); + }); +}); + +describe('agentById', () => { + it('finds an agent', () => { + expect(agentById('EXCEPTION_AGENT').name).toBe('Exception'); + }); + + it('falls back to the first rather than returning undefined', () => { + // The page reads `.status` off the result straight away; undefined here is + // a blank panel at best and a crash at worst. + expect(agentById('NOPE')).toBe(AGENTS[0]); + }); +}); + +describe('the run log', () => { + it('uses only tones the page can colour', () => { + RUN_LOG.forEach((l) => { + expect(['info', 'warn', 'error']).toContain(l.tone); + }); + }); + + it("the two repeated errors account for the log's stated error total", () => { + // 2 x 8,594 = 17,188, which is the number in the panel header. If the + // table and the header ever disagree, one of them is lying. + const repeated = RUN_LOG.filter((l) => l.tone === 'error').reduce((n, l) => n + (l.count || 0), 0); + expect(repeated).toBe(LOG_ERROR_LINES); + expect(LOG_ERROR_LINES).toBeLessThan(LOG_TOTAL_LINES); + }); +});