updates on the agent flow
This commit is contained in:
7
package-lock.json
generated
7
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 = () => (
|
||||
<div className="flex min-h-[60vh] items-center justify-center">
|
||||
@@ -150,6 +151,11 @@ const AuthenticatedApp = () => {
|
||||
|
||||
<Route path="profile" element={<ViewProfile />} />
|
||||
<Route path="settings" element={<Settings />} />
|
||||
|
||||
{/* 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. */}
|
||||
<Route path="agents" element={<Agents />} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
|
||||
@@ -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 },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
294
src/lib/agentNetwork.js
Normal file
294
src/lib/agentNetwork.js
Normal file
@@ -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,
|
||||
};
|
||||
};
|
||||
1045
src/pages/doormile/agents/AgentFlowVisualizer.jsx
Normal file
1045
src/pages/doormile/agents/AgentFlowVisualizer.jsx
Normal file
File diff suppressed because it is too large
Load Diff
275
src/pages/doormile/agents/AgentInspectorDrawer.jsx
Normal file
275
src/pages/doormile/agents/AgentInspectorDrawer.jsx
Normal file
@@ -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 (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={onClose}
|
||||
className="fixed inset-0 z-40 bg-black/30 backdrop-blur-2xs"
|
||||
/>
|
||||
|
||||
{/* Slide-over Drawer Panel */}
|
||||
<motion.aside
|
||||
initial={{ x: '100%' }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: '100%' }}
|
||||
transition={{ type: 'spring', damping: 28, stiffness: 320 }}
|
||||
className="fixed right-0 top-0 bottom-0 z-50 flex w-full max-w-md flex-col bg-surface border-l border-border shadow-2xl overflow-y-auto"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-border/80 p-5 bg-surface-subtle">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={cn('h-3.5 w-3.5 rounded-full shrink-0', statusTone.beacon)} />
|
||||
<div className="flex flex-col">
|
||||
<span className="font-heading text-base font-bold text-ink-1">
|
||||
{agent.name}
|
||||
</span>
|
||||
<span className="font-mono text-[11px] text-ink-4">
|
||||
{agent.id}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={cn('rounded-full border px-2.5 py-0.5 text-xs font-semibold', statusTone.chip)}>
|
||||
{statusTone.label}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="rounded-lg p-1.5 text-ink-3 hover:text-ink-1 hover:bg-surface-elevated transition-colors cursor-pointer"
|
||||
title="Close inspector"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Body */}
|
||||
<div className="flex flex-col gap-6 p-5">
|
||||
{/* Plain English Summary */}
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<span className="text-[10.5px] font-bold uppercase tracking-wider text-ink-4">
|
||||
Purpose & Role
|
||||
</span>
|
||||
<p className="text-body-sm text-ink-2 leading-relaxed">
|
||||
{agent.blurb}
|
||||
</p>
|
||||
<p className="text-caption text-ink-3 leading-relaxed mt-1">
|
||||
{agent.detail}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Event Subscriptions / Listens For */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-[10.5px] font-bold uppercase tracking-wider text-ink-4">
|
||||
Listens For (Event Bus)
|
||||
</span>
|
||||
{agent.subscribes.length > 0 ? (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{agent.subscribes.map((sub) => (
|
||||
<div
|
||||
key={sub}
|
||||
className="flex items-center justify-between rounded-lg border border-border/80 bg-surface-subtle px-3 py-2 text-xs font-mono text-ink-2"
|
||||
>
|
||||
<div className="flex items-center gap-2 truncate">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-brand" />
|
||||
<span className="font-semibold truncate">{sub}</span>
|
||||
</div>
|
||||
<span className="text-[10px] text-ink-4 font-sans font-medium">
|
||||
NATS stream
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-dashed border-border p-3 text-caption text-ink-4 italic">
|
||||
JARVIS task-delegated (does not subscribe to NATS directly)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Recent Activity & Liveness */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="flex flex-col gap-1 rounded-xl border border-border/80 p-3 bg-surface-subtle/50">
|
||||
<span className="text-[10.5px] font-bold uppercase tracking-wider text-ink-4">
|
||||
Recent Activity
|
||||
</span>
|
||||
<span className="font-heading text-lg font-bold text-ink-1">
|
||||
{agent.events} {agent.events === 1 ? 'event' : 'events'}
|
||||
</span>
|
||||
<span className="text-[10px] text-ink-4">
|
||||
Trigger: {TRIGGER[agent.trigger]}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1 rounded-xl border border-border/80 p-3 bg-surface-subtle/50">
|
||||
<span className="text-[10.5px] font-bold uppercase tracking-wider text-ink-4">
|
||||
Last Seen
|
||||
</span>
|
||||
<span className="font-heading text-xs font-bold text-ink-2 truncate">
|
||||
16 Sep 15:23
|
||||
</span>
|
||||
<span className="text-[10px] text-ink-4">
|
||||
Logiflow audit snapshot
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Autonomy Gate Status */}
|
||||
<div className="flex flex-col gap-2 rounded-xl border border-border p-3.5 bg-surface-subtle">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="flex items-center gap-1.5 text-xs font-bold text-ink-1">
|
||||
<ShieldCheck className="h-4 w-4 text-emerald-600 dark:text-emerald-400" />
|
||||
Autonomy Gate
|
||||
</span>
|
||||
<span className="font-mono text-[10px] font-bold text-amber-700 dark:text-amber-300 bg-amber-100 dark:bg-amber-900/40 px-2 py-0.5 rounded-full border border-amber-300 dark:border-amber-800">
|
||||
Human in the Loop
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-caption text-ink-3 leading-relaxed">
|
||||
{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.'}
|
||||
</p>
|
||||
<div className="text-[10.5px] font-mono text-ink-4 bg-surface px-2.5 py-1 rounded border border-border/60">
|
||||
{agent.autonomy}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Secondary Technical Metadata (Collapsible / Non-Dominating) */}
|
||||
<div className="flex flex-col gap-2 pt-2 border-t border-border/60">
|
||||
<span className="text-[10.5px] font-bold uppercase tracking-wider text-ink-4">
|
||||
Technical Specifications (Secondary)
|
||||
</span>
|
||||
|
||||
<div className="flex flex-col gap-2 rounded-xl border border-border/70 p-3.5 bg-surface text-caption">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-ink-4 font-mono">Source File</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={copyPath}
|
||||
className="inline-flex items-center gap-1 font-mono text-[11px] text-ink-2 hover:text-brand transition-colors cursor-pointer"
|
||||
>
|
||||
<span>{agent.file}</span>
|
||||
<Copy className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-ink-4 font-mono">Lines of Code</span>
|
||||
<span className="font-bold text-ink-2">{agent.lines ?? 'Core module'}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-ink-4 font-mono">Test Coverage</span>
|
||||
<span
|
||||
className={cn(
|
||||
'font-bold',
|
||||
agent.tests ? 'text-emerald-600 dark:text-emerald-400' : 'text-ink-4'
|
||||
)}
|
||||
>
|
||||
{agent.tests === null ? 'Core module' : agent.tests ? 'Tests Passing (100%)' : 'Untested'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-ink-4 font-mono">Capacity Load</span>
|
||||
<span className="font-bold text-ink-2">{agent.load}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Probe Action */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleProbe}
|
||||
disabled={probing}
|
||||
className="mt-2 inline-flex items-center justify-center gap-2 rounded-lg border border-border bg-surface px-4 py-2 text-xs font-semibold text-ink-2 hover:bg-surface-elevated hover:border-brand/40 transition-all shadow-2xs cursor-pointer"
|
||||
>
|
||||
<Radio className={cn('h-3.5 w-3.5', probing ? 'animate-spin text-brand' : 'text-ink-3')} />
|
||||
<span>{probing ? 'Probing Socket...' : 'Test Socket Probe'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.aside>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
|
||||
AgentInspectorDrawer.propTypes = {
|
||||
agentId: PropTypes.string.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
942
src/pages/doormile/agents/AgentPipeline.jsx
Normal file
942
src/pages/doormile/agents/AgentPipeline.jsx
Normal file
@@ -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 (
|
||||
<span
|
||||
className={cn('relative inline-flex items-center justify-center shrink-0 isolate aspect-square', className)}
|
||||
style={{ width: size, height: size, minWidth: size, minHeight: size, maxWidth: size, maxHeight: size }}
|
||||
>
|
||||
{/* Crisp glowing core */}
|
||||
<span
|
||||
className={cn('block rounded-full aspect-square', config.core)}
|
||||
style={{ width: size * 0.75, height: size * 0.75 }}
|
||||
/>
|
||||
|
||||
{/* High-tech breathing radar ping ring */}
|
||||
{config.ping && !reduce && (
|
||||
<motion.span
|
||||
className={cn('absolute inset-0 m-auto rounded-full border pointer-events-none', config.ring)}
|
||||
style={{ width: size, height: size }}
|
||||
animate={
|
||||
status === 'active'
|
||||
? { scale: [1, 2.2], opacity: [0.85, 0] }
|
||||
: status === 'degraded'
|
||||
? { scale: [1, 1.8], opacity: [0.9, 0] }
|
||||
: { scale: [1, 2], opacity: [0.75, 0] }
|
||||
}
|
||||
transition={{
|
||||
duration: status === 'degraded' ? 1.2 : 1.8,
|
||||
repeat: Infinity,
|
||||
ease: 'easeOut',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="flex shrink-0 items-center justify-center py-2.5 lg:w-16 xl:w-20 lg:min-w-[56px] lg:self-center lg:py-0">
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex items-center justify-center w-full h-8 px-1 rounded-full border transition-all duration-300',
|
||||
isCarryingActiveFlow
|
||||
? 'border-amber-400/90 bg-amber-500/10 shadow-[0_0_12px_rgba(245,158,11,0.25)] dark:bg-amber-950/30'
|
||||
: 'border-border/70 bg-surface-subtle/60 dark:bg-slate-900/40'
|
||||
)}
|
||||
>
|
||||
{/* Floating Telemetry Micro-Badge */}
|
||||
{badgeText && (
|
||||
<span
|
||||
className={cn(
|
||||
'absolute -top-3.5 left-1/2 -translate-x-1/2 font-mono text-[9px] font-bold uppercase tracking-wider px-1.5 py-0.2 rounded transition-colors whitespace-nowrap pointer-events-none',
|
||||
isCarryingActiveFlow
|
||||
? 'bg-amber-400 text-amber-950 shadow-xs'
|
||||
: 'bg-surface border border-border text-ink-4'
|
||||
)}
|
||||
>
|
||||
{badgeText}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Animated Laser Pipeline SVG */}
|
||||
<svg viewBox="0 0 68 20" className="w-full h-5 overflow-visible" fill="none">
|
||||
<defs>
|
||||
<linearGradient id={`conveyor-grad-${stageThreshold}`} x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stopColor={isCarryingActiveFlow ? '#f59e0b' : tone === 'brand' ? '#dc2626' : '#6366f1'} stopOpacity="0.2" />
|
||||
<stop offset="60%" stopColor={isCarryingActiveFlow ? '#f59e0b' : tone === 'brand' ? '#dc2626' : '#6366f1'} stopOpacity="1" />
|
||||
<stop offset="100%" stopColor={isCarryingActiveFlow ? '#fbbf24' : tone === 'brand' ? '#ef4444' : '#818cf8'} stopOpacity="0.8" />
|
||||
</linearGradient>
|
||||
<filter id={`glow-${stageThreshold}`} x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur stdDeviation="1.5" result="blur" />
|
||||
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
{/* Conduit Rail Track */}
|
||||
<line
|
||||
x1="4"
|
||||
y1="10"
|
||||
x2="52"
|
||||
y2="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
className="text-border/80 dark:text-slate-700/80"
|
||||
/>
|
||||
|
||||
{/* Animated Flowing Laser Core */}
|
||||
{!reduce && (
|
||||
<motion.line
|
||||
x1="4"
|
||||
y1="10"
|
||||
x2="50"
|
||||
y2="10"
|
||||
stroke={`url(#conveyor-grad-${stageThreshold})`}
|
||||
strokeWidth={isCarryingActiveFlow ? '2.5' : '1.8'}
|
||||
strokeLinecap="round"
|
||||
strokeDasharray="5 7"
|
||||
filter={isCarryingActiveFlow ? `url(#glow-${stageThreshold})` : undefined}
|
||||
animate={{ strokeDashoffset: [-24, 0] }}
|
||||
transition={{
|
||||
duration: isCarryingActiveFlow ? 0.6 : tone === 'brand' ? 1.2 : 1.8,
|
||||
repeat: Infinity,
|
||||
ease: 'linear',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Layered Sci-Fi Cascading Chevrons (» » ») */}
|
||||
<motion.path
|
||||
d="M38 6 L43 10 L38 14"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.6"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
animate={
|
||||
isCarryingActiveFlow
|
||||
? { opacity: [0.4, 1, 0.4], x: [0, 2, 0] }
|
||||
: { opacity: [0.2, 0.7, 0.2] }
|
||||
}
|
||||
transition={{ duration: 0.9, repeat: Infinity, ease: 'easeInOut' }}
|
||||
className={isCarryingActiveFlow ? 'text-amber-500' : 'text-slate-400 dark:text-slate-500'}
|
||||
/>
|
||||
|
||||
<motion.path
|
||||
d="M45 6 L50 10 L45 14"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
animate={
|
||||
isCarryingActiveFlow
|
||||
? { opacity: [0.5, 1, 0.5], x: [0, 2.5, 0] }
|
||||
: { opacity: [0.4, 0.9, 0.4] }
|
||||
}
|
||||
transition={{ duration: 0.9, repeat: Infinity, ease: 'easeInOut', delay: 0.15 }}
|
||||
className={isCarryingActiveFlow ? 'text-amber-500' : 'text-slate-500 dark:text-slate-400'}
|
||||
/>
|
||||
|
||||
<motion.path
|
||||
d="M52 6 L57 10 L52 14"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
animate={
|
||||
isCarryingActiveFlow
|
||||
? { opacity: [0.7, 1, 0.7], x: [0, 3, 0] }
|
||||
: { opacity: [0.6, 1, 0.6] }
|
||||
}
|
||||
transition={{ duration: 0.9, repeat: Infinity, ease: 'easeInOut', delay: 0.3 }}
|
||||
className={isCarryingActiveFlow ? 'text-amber-500' : 'text-brand dark:text-brand'}
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{/* Traveling Primary Photon Particle */}
|
||||
{!reduce && (
|
||||
<div
|
||||
ref={packetRef}
|
||||
className={cn(
|
||||
'absolute left-2 top-1/2 -translate-y-1/2 rounded-full pointer-events-none transition-all duration-200 aspect-square',
|
||||
isCarryingActiveFlow
|
||||
? 'w-2.5 h-2.5 bg-amber-300 shadow-[0_0_12px_rgba(251,191,36,1)] z-20 ring-2 ring-amber-400/80'
|
||||
: tone === 'brand'
|
||||
? 'w-2 h-2 bg-brand shadow-[0_0_8px_rgba(200,16,46,0.9)]'
|
||||
: 'w-2 h-2 bg-indigo-500 shadow-[0_0_8px_rgba(99,102,241,0.9)]'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Trailing Micro-Spark */}
|
||||
{!reduce && isCarryingActiveFlow && (
|
||||
<div
|
||||
ref={sparkRef}
|
||||
className="absolute left-2 top-1/2 -translate-y-1/2 w-1.5 h-1.5 rounded-full bg-amber-200/90 shadow-[0_0_6px_rgba(245,158,11,0.8)] pointer-events-none z-10 aspect-square"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* ═══════════════════════════════════════════════════════════════════
|
||||
INTERACTIVE FLOW CONTROL BAR & STEP TRACKER
|
||||
═══════════════════════════════════════════════════════════════════ */}
|
||||
<div className="flex flex-col gap-3 rounded-xl border border-border bg-surface-subtle/80 p-3.5 shadow-2xs">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
'flex h-2.5 w-2.5 rounded-full',
|
||||
isFlowRunning ? 'bg-amber-500 animate-ping' : 'bg-emerald-500'
|
||||
)}
|
||||
/>
|
||||
<span className="font-heading text-xs font-bold text-ink-1">
|
||||
Interactive Order Flow Simulator
|
||||
</span>
|
||||
<span className="text-[11px] text-ink-3">· Observe how real events traverse the architecture</span>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{/* Scenario dropdown buttons */}
|
||||
<div className="flex items-center gap-1 rounded-lg border border-border bg-surface p-1 text-xs">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => startFlow('order_dispatch')}
|
||||
disabled={isFlowRunning && activeScenario === 'order_dispatch'}
|
||||
className={cn(
|
||||
'rounded px-2.5 py-1 font-semibold transition-all cursor-pointer flex items-center gap-1.5',
|
||||
activeScenario === 'order_dispatch' && isFlowRunning
|
||||
? 'bg-brand text-white shadow-xs'
|
||||
: 'text-ink-2 hover:bg-surface-subtle'
|
||||
)}
|
||||
>
|
||||
<Package className="h-3 w-3" />
|
||||
<span>1. Order Booking</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => startFlow('rider_stall')}
|
||||
disabled={isFlowRunning && activeScenario === 'rider_stall'}
|
||||
className={cn(
|
||||
'rounded px-2.5 py-1 font-semibold transition-all cursor-pointer flex items-center gap-1.5',
|
||||
activeScenario === 'rider_stall' && isFlowRunning
|
||||
? 'bg-amber-600 text-white shadow-xs'
|
||||
: 'text-ink-2 hover:bg-surface-subtle'
|
||||
)}
|
||||
>
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
<span>2. Rider Stall</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => startFlow('route_opt')}
|
||||
disabled={isFlowRunning && activeScenario === 'route_opt'}
|
||||
className={cn(
|
||||
'rounded px-2.5 py-1 font-semibold transition-all cursor-pointer flex items-center gap-1.5',
|
||||
activeScenario === 'route_opt' && isFlowRunning
|
||||
? 'bg-indigo-600 text-white shadow-xs'
|
||||
: 'text-ink-2 hover:bg-surface-subtle'
|
||||
)}
|
||||
>
|
||||
<Cpu className="h-3 w-3" />
|
||||
<span>3. Batch Route</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Reset / Stop */}
|
||||
{isFlowRunning && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={stopFlow}
|
||||
className="inline-flex items-center gap-1 rounded-lg border border-border bg-surface px-2.5 py-1.5 text-xs font-semibold text-ink-3 hover:text-ink-1 hover:bg-surface-elevated transition-colors cursor-pointer"
|
||||
title="Reset simulation"
|
||||
>
|
||||
<RotateCcw className="h-3 w-3" />
|
||||
<span>Reset</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dynamic 4-Step Progress Ribbon (Clean & Professional) */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-4 gap-2.5 pt-2 border-t border-border/50">
|
||||
{[
|
||||
{ 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 (
|
||||
<div
|
||||
key={item.step}
|
||||
className={cn(
|
||||
'flex items-center gap-2.5 rounded-lg border px-3 py-2 transition-all duration-200 bg-surface',
|
||||
isCurrent
|
||||
? 'border-brand ring-1 ring-brand/40 shadow-xs'
|
||||
: isDone
|
||||
? 'border-slate-300 dark:border-slate-700'
|
||||
: 'border-border opacity-70'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-[11px] font-bold transition-all',
|
||||
isCurrent
|
||||
? 'bg-brand text-white'
|
||||
: isDone
|
||||
? 'bg-slate-700 text-white dark:bg-slate-200 dark:text-slate-900'
|
||||
: 'bg-surface-sunken text-ink-4'
|
||||
)}
|
||||
>
|
||||
{isDone ? <CheckCircle2 className="h-3.5 w-3.5" /> : item.step}
|
||||
</div>
|
||||
<div className="flex flex-col min-w-0">
|
||||
<span className={cn('text-xs font-semibold truncate', isCurrent ? 'text-brand font-bold' : 'text-ink-1')}>
|
||||
{item.label}
|
||||
</span>
|
||||
<span className="font-mono text-[10px] text-ink-4 truncate">{item.sub}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ═══════════════════════════════════════════════════════════════════
|
||||
TOPOLOGY DIAGRAM WITH DYNAMIC STEP HIGHLIGHTING & PARCELS
|
||||
═══════════════════════════════════════════════════════════════════ */}
|
||||
<div className="relative flex flex-col gap-4 lg:flex-row lg:items-stretch lg:gap-0">
|
||||
{/* Floating Active Event Parcel Indicator */}
|
||||
<AnimatePresence>
|
||||
{isFlowRunning && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -8, scale: 0.9 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.8 }}
|
||||
className="absolute -top-3.5 left-4 z-30 inline-flex items-center gap-2 rounded-full border border-amber-300 bg-amber-100 dark:bg-amber-900 px-3 py-1 text-xs font-bold text-amber-900 dark:text-amber-200 shadow-md"
|
||||
>
|
||||
<Package className="h-3.5 w-3.5 animate-bounce text-amber-700 dark:text-amber-300" />
|
||||
<span>Event Packet: #{orderId}</span>
|
||||
<span className="font-mono text-[10.5px] bg-amber-200/80 dark:bg-amber-800 px-1.5 py-0.5 rounded">
|
||||
Stage {flowStep}/4
|
||||
</span>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* 1 · SOURCE PRODUCER */}
|
||||
<motion.div
|
||||
whileHover={{ y: -2 }}
|
||||
className={cn(
|
||||
'relative flex flex-col gap-2 rounded-xl border p-4 transition-all duration-200 lg:min-w-[170px] lg:flex-[0_1_185px] bg-surface',
|
||||
flowStep === 1
|
||||
? 'border-brand ring-2 ring-brand/20 shadow-xs'
|
||||
: 'border-border hover:border-border-strong'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10.5px] font-bold uppercase tracking-[0.08em] text-ink-4">1 · Source</span>
|
||||
<span className="rounded bg-slate-100 dark:bg-slate-800 px-1.5 py-0.5 font-mono text-[10px] font-bold text-ink-2">
|
||||
Go Fiber
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Server className="h-4 w-4 text-ink-2" />
|
||||
<span className="font-heading text-sm font-bold text-ink-1">doormile_backend</span>
|
||||
</div>
|
||||
|
||||
<p className="text-caption text-ink-3 leading-relaxed">
|
||||
{flowStep === 1 ? (
|
||||
<span className="font-semibold text-brand">
|
||||
⚡ {scenarioMeta.sourceText}
|
||||
</span>
|
||||
) : (
|
||||
'Assigns riders internally, then streams status notifications onto the message bus.'
|
||||
)}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col gap-1 pt-1 mt-auto">
|
||||
{SOURCE_SUBJECTS.map((s) => (
|
||||
<span
|
||||
key={s}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1.5 rounded-md border px-2 py-1 font-mono text-[10px] transition-colors',
|
||||
flowStep === 1 && s === 'booking.assigned'
|
||||
? 'border-brand text-brand font-bold bg-surface'
|
||||
: 'bg-surface border-border text-ink-2'
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'h-1.5 w-1.5 rounded-full',
|
||||
flowStep === 1 && s === 'booking.assigned' ? 'bg-brand' : 'bg-slate-400'
|
||||
)}
|
||||
/>
|
||||
<span className="truncate">{s}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Conveyor 1 */}
|
||||
<CircuitConveyor
|
||||
tone="brand"
|
||||
isFlowActive={isFlowRunning}
|
||||
flowStage={flowStep}
|
||||
stageThreshold={1}
|
||||
badgeText="NATS Pub"
|
||||
/>
|
||||
|
||||
{/* 2 · BUS STREAM */}
|
||||
<motion.div
|
||||
whileHover={{ y: -2 }}
|
||||
className={cn(
|
||||
'relative flex flex-col gap-2 rounded-xl border p-4 transition-all duration-200 lg:min-w-[165px] lg:flex-[0_1_180px] bg-surface',
|
||||
flowStep === 2
|
||||
? 'border-brand ring-2 ring-brand/20 shadow-xs'
|
||||
: 'border-border hover:border-border-strong'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10.5px] font-bold uppercase tracking-[0.08em] text-ink-4">
|
||||
2 · Message Bus
|
||||
</span>
|
||||
<RadarDot status="degraded" size={10} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Radio className="h-4 w-4 text-ink-2" />
|
||||
<span className="font-heading text-sm font-bold text-ink-1">NATS JetStream</span>
|
||||
</div>
|
||||
|
||||
<p className="text-caption text-ink-3 leading-relaxed">
|
||||
{flowStep === 2 ? (
|
||||
<span className="font-semibold text-brand">
|
||||
⚡ {scenarioMeta.busText}
|
||||
</span>
|
||||
) : (
|
||||
'6 durable event streams. Best-effort delivery — publish never blocks live dispatch.'
|
||||
)}
|
||||
</p>
|
||||
|
||||
<div className="mt-auto">
|
||||
<span className="inline-flex w-full items-center justify-center gap-1.5 rounded-lg border border-amber-300 bg-amber-50 dark:bg-amber-950/30 dark:border-amber-800 px-2.5 py-1.5 text-[11px] font-bold text-amber-900 dark:text-amber-200 shadow-2xs">
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
<span>Dropped 20 Sep (Stalled)</span>
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Conveyor 2 */}
|
||||
<CircuitConveyor
|
||||
tone="muted"
|
||||
isFlowActive={isFlowRunning}
|
||||
flowStage={flowStep}
|
||||
stageThreshold={2}
|
||||
badgeText="JetStream"
|
||||
/>
|
||||
|
||||
{/* 3 · AGENT SWARM */}
|
||||
<motion.div
|
||||
className={cn(
|
||||
'relative flex flex-col gap-2 rounded-xl border bg-surface p-3.5 transition-all duration-200 lg:min-w-[390px] lg:flex-[2_1_520px]',
|
||||
flowStep === 3
|
||||
? 'border-brand ring-2 ring-brand/20 shadow-xs'
|
||||
: 'border-border'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10.5px] font-bold uppercase tracking-[0.08em] text-ink-4">
|
||||
3 · Intelligence Swarm
|
||||
</span>
|
||||
<span className="rounded-full bg-slate-100 dark:bg-slate-800 px-2 py-0.5 text-[10.5px] font-semibold text-ink-3">
|
||||
9 Specialized Agents
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Real-time reasoning badge during Step 3 */}
|
||||
{flowStep === 3 && (
|
||||
<div className="rounded-lg border border-brand/30 bg-surface p-2 text-xs font-semibold text-brand flex items-center gap-2">
|
||||
<Sparkles className="h-4 w-4 text-brand shrink-0" />
|
||||
<span>{scenarioMeta.aiText}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* Event-driven sub-cluster */}
|
||||
<div className="rounded-lg border border-dashed border-border/80 p-2.5 bg-surface-subtle/50">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-caption font-bold text-ink-1 flex items-center gap-1.5">
|
||||
<Activity className="h-3.5 w-3.5 text-ink-2" />
|
||||
Event-Driven Subscribers
|
||||
</span>
|
||||
<span className="text-[10px] text-ink-3 font-semibold bg-surface border border-border px-1.5 py-0.5 rounded">
|
||||
Autonomous
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-3">
|
||||
{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 (
|
||||
<motion.button
|
||||
key={agent.id}
|
||||
type="button"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={() => 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 */}
|
||||
<div className="flex items-center justify-between gap-1 min-w-0">
|
||||
<span className="font-heading text-[11px] xl:text-xs font-bold text-ink-1 whitespace-nowrap" title={agent.name}>
|
||||
{agent.name}
|
||||
</span>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<RadarDot status={agent.status} size={9} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Middle Row: Subscription Event Badge */}
|
||||
<div className="flex items-center gap-1 rounded bg-surface-subtle/80 dark:bg-slate-800/50 px-1.5 py-0.5 border border-border/50 min-w-0">
|
||||
<code className="font-mono text-[9px] text-ink-3 truncate w-full" title={agent.subscribes[0]}>
|
||||
{agent.subscribes[0] || 'Direct NATS'}
|
||||
</code>
|
||||
</div>
|
||||
|
||||
{/* Bottom Row: Event count & Status indicator */}
|
||||
<div className="flex items-center justify-between pt-1 border-t border-border/40 text-[10px]">
|
||||
<span
|
||||
className={cn(
|
||||
'font-medium text-[9.5px]',
|
||||
agent.events > 0
|
||||
? 'text-emerald-700 dark:text-emerald-400 font-bold'
|
||||
: 'text-ink-4'
|
||||
)}
|
||||
>
|
||||
{agent.events > 0 ? `${agent.events} events` : '0 events'}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'capitalize px-1.5 py-0.2 rounded-full border text-[9px] font-semibold shrink-0',
|
||||
statusChipTone
|
||||
)}
|
||||
>
|
||||
{agent.status}
|
||||
</span>
|
||||
</div>
|
||||
</motion.button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Task-driven sub-cluster */}
|
||||
<div className="rounded-lg border border-dashed border-border/80 p-2.5 bg-surface-subtle/50">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-caption font-bold text-ink-1 flex items-center gap-1.5">
|
||||
<Server className="h-3.5 w-3.5 text-ink-2" />
|
||||
Task-Driven Orchestration
|
||||
</span>
|
||||
<span className="text-[10px] text-ink-3 font-semibold bg-surface border border-border px-1.5 py-0.5 rounded">
|
||||
JARVIS Delegated
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSelectAgent('JARVIS')}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs font-bold transition-all cursor-pointer bg-surface',
|
||||
selectedAgentId === 'JARVIS'
|
||||
? 'border-brand ring-1 ring-brand/30 text-brand'
|
||||
: 'border-border text-ink-2 hover:bg-surface-elevated'
|
||||
)}
|
||||
>
|
||||
<span className="h-2 w-2 rounded-full bg-indigo-600" />
|
||||
<span>JARVIS (Master)</span>
|
||||
</button>
|
||||
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{taskAgents.map((agent) => {
|
||||
const isAgentActiveInFlow = flowStep === 3 && scenarioMeta.aiAgent === agent.id;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={agent.id}
|
||||
type="button"
|
||||
onClick={() => onSelectAgent(agent.id)}
|
||||
className={cn(
|
||||
'rounded-md border px-2 py-1 text-[11px] font-medium transition-all cursor-pointer bg-surface',
|
||||
isAgentActiveInFlow
|
||||
? 'border-brand ring-1 ring-brand/40 text-brand font-bold shadow-xs'
|
||||
: selectedAgentId === agent.id
|
||||
? 'border-brand text-brand font-bold'
|
||||
: 'border-border text-ink-2 hover:bg-surface-subtle'
|
||||
)}
|
||||
>
|
||||
{agent.name}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Conveyor 3 */}
|
||||
<CircuitConveyor
|
||||
tone="brand"
|
||||
isFlowActive={isFlowRunning}
|
||||
flowStage={flowStep}
|
||||
stageThreshold={3}
|
||||
badgeText="Safe Gate"
|
||||
/>
|
||||
|
||||
{/* 4 · OUTCOME */}
|
||||
<motion.div
|
||||
whileHover={{ y: -2 }}
|
||||
className={cn(
|
||||
'relative flex flex-col gap-2 rounded-xl border p-4 transition-all duration-200 lg:min-w-[170px] lg:flex-[0_1_185px] bg-surface',
|
||||
flowStep === 4
|
||||
? 'border-brand ring-2 ring-brand/20 shadow-xs'
|
||||
: 'border-border hover:border-border-strong'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10.5px] font-bold uppercase tracking-[0.08em] text-ink-4">
|
||||
4 · Safe Escalation
|
||||
</span>
|
||||
<ShieldAlert className="h-4 w-4 text-ink-3" />
|
||||
</div>
|
||||
|
||||
<span className="font-heading text-sm font-bold text-ink-1">Alert Only (Never Act)</span>
|
||||
|
||||
<p className="text-caption text-ink-3 leading-relaxed">
|
||||
{flowStep === 4 ? (
|
||||
<span className="font-semibold text-brand">
|
||||
✓ {scenarioMeta.outcomeText}
|
||||
</span>
|
||||
) : (
|
||||
'Emits ops desk alerts & customer delay SMS. Strictly limited to 1 escalation/zone/day.'
|
||||
)}
|
||||
</p>
|
||||
|
||||
<div className="mt-auto">
|
||||
<div
|
||||
className={cn(
|
||||
'inline-flex w-full items-center justify-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-[11px] font-bold shadow-2xs transition-colors bg-surface',
|
||||
flowStep === 4
|
||||
? 'border-brand text-brand'
|
||||
: 'border-border text-ink-3'
|
||||
)}
|
||||
>
|
||||
<CheckCircle2 className={cn('h-3 w-3', flowStep === 4 ? 'text-brand' : 'text-ink-4')} />
|
||||
<span>{flowStep === 4 ? 'Flow Verified & Audited' : 'Autonomy Gates Locked'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
AgentPipeline.propTypes = {
|
||||
selectedAgentId: PropTypes.string,
|
||||
onSelectAgent: PropTypes.func.isRequired,
|
||||
onFlowStepChange: PropTypes.func,
|
||||
};
|
||||
312
src/pages/doormile/agents/AgentWorkflowView.jsx
Normal file
312
src/pages/doormile/agents/AgentWorkflowView.jsx
Normal file
@@ -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 (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -12 }}
|
||||
transition={{ duration: 0.35, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="flex flex-col gap-6 w-full"
|
||||
>
|
||||
{/* ── Top Bar: Back Button & Agent Title ── */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-4 border-b border-border/70 pb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onBackToNetwork}
|
||||
className="group inline-flex items-center gap-2 rounded-xl border border-border bg-surface px-3.5 py-1.5 text-xs font-semibold text-ink-2 hover:text-ink-1 hover:border-brand/40 hover:bg-surface-elevated transition-all shadow-2xs cursor-pointer"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 text-ink-3 group-hover:-translate-x-0.5 group-hover:text-brand transition-transform" />
|
||||
<span>Back to Agent Network</span>
|
||||
</button>
|
||||
|
||||
<div className="h-4 w-px bg-border hidden sm:block" />
|
||||
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span
|
||||
className="h-2.5 w-2.5 rounded-full ring-2 ring-emerald-500/20"
|
||||
style={{ backgroundColor: workflow.color }}
|
||||
/>
|
||||
<h2 className="font-heading text-base sm:text-lg font-bold text-ink-1">
|
||||
{workflow.name}
|
||||
</h2>
|
||||
<span className="text-caption text-ink-4 hidden md:inline">
|
||||
— {workflow.subtitle}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right actions: Pause/Play & Technical Drawer */}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsPlaying(!isPlaying)}
|
||||
className="inline-flex items-center gap-1.5 rounded-lg border border-border bg-surface px-3 py-1.5 text-xs font-semibold text-ink-2 hover:bg-surface-elevated transition-colors cursor-pointer"
|
||||
>
|
||||
{isPlaying ? (
|
||||
<>
|
||||
<Pause className="h-3 w-3 text-ink-3" />
|
||||
<span>Pause</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play className="h-3 w-3 text-brand fill-brand" />
|
||||
<span>Run</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onOpenTechnicalDrawer(agentId)}
|
||||
className="inline-flex items-center gap-1.5 rounded-lg border border-border bg-surface px-3 py-1.5 text-xs font-semibold text-ink-2 hover:border-brand/40 hover:text-brand transition-all cursor-pointer shadow-2xs"
|
||||
>
|
||||
<Layers className="h-3.5 w-3.5 text-ink-3" />
|
||||
<span>Technical Specs</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Workflow Description Banner ── */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-border/80 bg-surface/90 px-4 py-3 shadow-2xs">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Sparkles className="h-4 w-4 text-brand shrink-0" />
|
||||
<p className="text-body-sm text-ink-2 leading-relaxed">
|
||||
{workflow.summary}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-[11px] font-semibold text-ink-4">
|
||||
<ShieldCheck className="h-3.5 w-3.5 text-emerald-600 dark:text-emerald-400" />
|
||||
<span>Autonomy Gated (0 DB writes)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ═══════════════════════════════════════════════════════════════════
|
||||
THE WORKFLOW TIMELINE / PIPELINE (HERO VISUALIZATION)
|
||||
═══════════════════════════════════════════════════════════════════ */}
|
||||
<div className="flex flex-col gap-6 rounded-2xl border border-border/80 bg-surface p-5 sm:p-6 shadow-xs">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-ink-4">
|
||||
Execution Timeline
|
||||
</span>
|
||||
<span className="text-caption text-ink-4">
|
||||
· Step {activeStepIndex + 1} of {steps.length}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[11px] text-ink-4">
|
||||
Click any step to inspect details
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Responsive Pipeline Steps (Horizontal on Desktop, Vertical on Mobile) */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-3 relative">
|
||||
{steps.map((stepItem, index) => {
|
||||
const isCompleted = index < activeStepIndex;
|
||||
const isProcessing = index === activeStepIndex;
|
||||
const isWaiting = index > activeStepIndex;
|
||||
const isSelected = selectedStepDetail?.id === stepItem.id;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={stepItem.id}
|
||||
whileHover={{ y: -2 }}
|
||||
onClick={() => 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 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-mono text-[10.5px] font-bold text-ink-4">
|
||||
0{index + 1}
|
||||
</span>
|
||||
|
||||
{/* Clean Visual State Beacon */}
|
||||
{isCompleted ? (
|
||||
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-emerald-100 dark:bg-emerald-950/60 text-emerald-700 dark:text-emerald-300 text-[10px] font-bold">
|
||||
✓
|
||||
</span>
|
||||
) : isProcessing ? (
|
||||
<span className="relative flex h-5 w-5 items-center justify-center">
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-brand animate-pulse" />
|
||||
<span className="absolute -inset-0.5 rounded-full border border-brand animate-ping opacity-75" />
|
||||
</span>
|
||||
) : (
|
||||
<span className="h-2 w-2 rounded-full bg-slate-300 dark:bg-slate-700" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Step Title */}
|
||||
<h3
|
||||
className={cn(
|
||||
'font-heading text-xs font-bold transition-colors leading-tight',
|
||||
isProcessing
|
||||
? 'text-brand'
|
||||
: isCompleted
|
||||
? 'text-ink-1'
|
||||
: 'text-ink-3'
|
||||
)}
|
||||
>
|
||||
{stepItem.title}
|
||||
</h3>
|
||||
|
||||
{/* Short Human-Readable Description (Max 2 lines) */}
|
||||
<p className="text-[11px] text-ink-3 leading-snug line-clamp-2">
|
||||
{stepItem.description}
|
||||
</p>
|
||||
|
||||
{/* Technical Output Tag */}
|
||||
<div className="mt-auto pt-2 border-t border-border/40">
|
||||
<code className="font-mono text-[9px] text-ink-4 truncate block">
|
||||
{stepItem.output}
|
||||
</code>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* ── Agent-to-Agent Transition Handover (Requirement: Show Where Work Goes Next) ── */}
|
||||
<div className="flex items-center justify-between rounded-xl border border-dashed border-border bg-surface-subtle/60 p-3.5 mt-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="flex h-2 w-2 rounded-full bg-indigo-500 animate-pulse" />
|
||||
<span className="text-xs font-bold text-ink-1">
|
||||
Workflow Handover:
|
||||
</span>
|
||||
<span className="text-caption text-ink-3">
|
||||
Upon completion, validated events transit directly to:
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-flex items-center gap-1.5 rounded-lg border border-indigo-200 bg-indigo-50 dark:border-indigo-800 dark:bg-indigo-950/40 px-3 py-1 text-xs font-bold text-indigo-900 dark:text-indigo-300">
|
||||
<span>{workflow.nextAgent}</span>
|
||||
<ArrowRight className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Selected Step Detail Inspector ── */}
|
||||
<AnimatePresence>
|
||||
{selectedStepDetail && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 8 }}
|
||||
className="flex flex-col gap-3 rounded-xl border border-border bg-surface p-4 shadow-sm"
|
||||
>
|
||||
<div className="flex items-center justify-between border-b border-border/60 pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="flex h-2 w-2 rounded-full bg-brand" />
|
||||
<h3 className="font-heading text-xs font-bold uppercase tracking-wider text-ink-1">
|
||||
Step Detail: {selectedStepDetail.title}
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelectedStepDetail(null)}
|
||||
className="text-caption text-ink-4 hover:text-ink-1 transition-colors cursor-pointer"
|
||||
>
|
||||
Close ✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 text-xs">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-ink-4 font-mono uppercase text-[10px]">
|
||||
What happens?
|
||||
</span>
|
||||
<p className="text-ink-2 leading-relaxed">
|
||||
{selectedStepDetail.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-ink-4 font-mono uppercase text-[10px]">
|
||||
Input Trigger
|
||||
</span>
|
||||
<code className="font-mono text-xs font-semibold text-ink-1 bg-surface-subtle px-2 py-1 rounded border border-border/60">
|
||||
{selectedStepDetail.input}
|
||||
</code>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-ink-4 font-mono uppercase text-[10px]">
|
||||
Output Result
|
||||
</span>
|
||||
<code className="font-mono text-xs font-semibold text-emerald-700 dark:text-emerald-400 bg-surface-subtle px-2 py-1 rounded border border-border/60">
|
||||
{selectedStepDetail.output}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
AgentWorkflowView.propTypes = {
|
||||
agentId: PropTypes.string.isRequired,
|
||||
onBackToNetwork: PropTypes.func.isRequired,
|
||||
onOpenTechnicalDrawer: PropTypes.func.isRequired,
|
||||
};
|
||||
333
src/pages/doormile/agents/Agents.jsx
Normal file
333
src/pages/doormile/agents/Agents.jsx
Normal file
@@ -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 (
|
||||
<div className="flex flex-col gap-6 p-4 sm:p-6 lg:p-8 max-w-7xl mx-auto w-full">
|
||||
{/* ═══════════════════════════════════════════════════════════════════
|
||||
COMPACT TOP STATUS BAR (REQUIREMENT: TOP STATUS BAR)
|
||||
Clean, minimal telemetry ribbon: ● 1 Processing · ● 1 Warning · ● 7 Idle · 🔒 Autonomy locked
|
||||
═══════════════════════════════════════════════════════════════════ */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-border/80 bg-surface px-4 py-2.5 shadow-2xs">
|
||||
{/* Left: Identity */}
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="flex h-2.5 w-2.5 rounded-full bg-emerald-500 animate-pulse" />
|
||||
<h1 className="font-heading text-sm sm:text-base font-bold text-ink-1">
|
||||
Doormile AI Agent Network
|
||||
</h1>
|
||||
<span className="text-caption text-ink-4 hidden md:inline">
|
||||
· Real-Time Workflow Visualizer
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Right: Clean Status Indicators */}
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs">
|
||||
{/* 1 Processing */}
|
||||
<div className="flex items-center gap-1.5 rounded-full border border-emerald-200 bg-emerald-50 dark:border-emerald-800 dark:bg-emerald-950/40 px-2.5 py-0.5 text-emerald-800 dark:text-emerald-300 font-semibold">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
|
||||
<span>1 Processing</span>
|
||||
</div>
|
||||
|
||||
{/* 1 Warning */}
|
||||
<div className="flex items-center gap-1.5 rounded-full border border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-950/40 px-2.5 py-0.5 text-amber-800 dark:text-amber-300 font-semibold">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-amber-500" />
|
||||
<span>1 Warning</span>
|
||||
</div>
|
||||
|
||||
{/* 7 Idle */}
|
||||
<div className="flex items-center gap-1.5 rounded-full border border-border bg-surface-subtle px-2.5 py-0.5 text-ink-3 font-semibold">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-slate-400" />
|
||||
<span>7 Idle</span>
|
||||
</div>
|
||||
|
||||
{/* 🔒 Autonomy Locked */}
|
||||
<div
|
||||
className="flex items-center gap-1.5 rounded-full border border-indigo-200 bg-indigo-50 dark:border-indigo-800 dark:bg-indigo-950/40 px-2.5 py-0.5 text-indigo-900 dark:text-indigo-300 font-semibold"
|
||||
title="Autonomy locked: AI observes and recommends, but cannot independently alter production systems."
|
||||
>
|
||||
<ShieldCheck className="h-3 w-3 text-indigo-600 dark:text-indigo-400" />
|
||||
<span>Autonomy: 0 / 8</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ═══════════════════════════════════════════════════════════════════
|
||||
THE HERO: 4-STAGE AI FLOW VISUALIZER & DEDICATED WORKFLOWS
|
||||
═══════════════════════════════════════════════════════════════════ */}
|
||||
<AgentFlowVisualizer
|
||||
onOpenTechnicalDrawer={handleOpenInspector}
|
||||
onActivityLogged={handleActivityLogged}
|
||||
/>
|
||||
|
||||
{/* ═══════════════════════════════════════════════════════════════════
|
||||
SECONDARY SECTIONS: LIVE ACTIVITY & GROUPED SYSTEM DIAGNOSTICS
|
||||
═══════════════════════════════════════════════════════════════════ */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{/* ── 1. Compact Live Activity Feed ── */}
|
||||
<div className="flex flex-col gap-3 rounded-xl border border-border/80 bg-surface p-4 shadow-xs">
|
||||
<div className="flex items-center justify-between border-b border-border/50 pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity className="h-4 w-4 text-ink-2" />
|
||||
<h2 className="font-heading text-xs font-bold uppercase tracking-wider text-ink-1">
|
||||
Live Activity
|
||||
</h2>
|
||||
</div>
|
||||
<span className="text-[10.5px] font-mono text-ink-4">
|
||||
Latest events
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<AnimatePresence initial={false}>
|
||||
{liveActivities.slice(0, 4).map((activity) => (
|
||||
<motion.div
|
||||
key={activity.id}
|
||||
initial={{ opacity: 0, y: -4 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="flex items-center justify-between gap-3 rounded-lg border border-border/60 bg-surface-subtle/50 px-3 py-2 text-xs"
|
||||
>
|
||||
<div className="flex items-center gap-2.5 truncate">
|
||||
<span
|
||||
className="h-2 w-2 rounded-full shrink-0"
|
||||
style={{ backgroundColor: activity.color }}
|
||||
/>
|
||||
<span className="font-bold text-ink-1">
|
||||
{activity.agent}
|
||||
</span>
|
||||
<span className="text-ink-4">·</span>
|
||||
<span className="text-ink-3 truncate">
|
||||
{activity.action}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span className="font-mono text-[10px] text-ink-4 shrink-0">
|
||||
{activity.time}
|
||||
</span>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── 2. Grouped System Diagnostics (Requirement: Group Repeated Errors) ── */}
|
||||
<div className="flex flex-col gap-3 rounded-xl border border-border/80 bg-surface p-4 shadow-xs">
|
||||
<div className="flex items-center justify-between border-b border-border/50 pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle className="h-4 w-4 text-amber-500" />
|
||||
<h2 className="font-heading text-xs font-bold uppercase tracking-wider text-ink-1">
|
||||
System Diagnostics
|
||||
</h2>
|
||||
</div>
|
||||
<span className="text-[10.5px] font-mono text-ink-4">
|
||||
Grouped health checks
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Grouped Error 1: NATS connection closed */}
|
||||
<div
|
||||
onClick={() =>
|
||||
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"
|
||||
>
|
||||
<div className="flex items-center gap-2.5 truncate">
|
||||
<span className="flex h-2 w-2 rounded-full bg-rose-500 shrink-0" />
|
||||
<span className="font-bold text-rose-900 dark:text-rose-200 truncate">
|
||||
NATS connection closed
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<span className="font-mono text-[10.5px] font-bold text-rose-800 dark:text-rose-300 bg-rose-100 dark:bg-rose-900/60 px-2 py-0.5 rounded-full">
|
||||
8,594 occurrences
|
||||
</span>
|
||||
<ChevronRight className="h-3.5 w-3.5 text-rose-400 group-hover:translate-x-0.5 transition-transform" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Grouped Error 2: Unknown zone */}
|
||||
<div
|
||||
onClick={() =>
|
||||
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"
|
||||
>
|
||||
<div className="flex items-center gap-2.5 truncate">
|
||||
<span className="flex h-2 w-2 rounded-full bg-amber-500 shrink-0" />
|
||||
<span className="font-bold text-amber-900 dark:text-amber-200 truncate">
|
||||
Unknown zone
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<span className="font-mono text-[10.5px] font-bold text-amber-800 dark:text-amber-300 bg-amber-100 dark:bg-amber-900/60 px-2 py-0.5 rounded-full">
|
||||
2 occurrences
|
||||
</span>
|
||||
<ChevronRight className="h-3.5 w-3.5 text-amber-400 group-hover:translate-x-0.5 transition-transform" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ═══════════════════════════════════════════════════════════════════
|
||||
DETAIL INSPECTOR DRAWER (TECHNICAL DETAILS LIVE HERE ONLY)
|
||||
═══════════════════════════════════════════════════════════════════ */}
|
||||
<AgentInspectorDrawer
|
||||
agentId={selectedAgentId}
|
||||
isOpen={isDrawerOpen}
|
||||
onClose={() => setIsDrawerOpen(false)}
|
||||
/>
|
||||
|
||||
{/* ── Diagnostics Modal for Grouped Errors ── */}
|
||||
<AnimatePresence>
|
||||
{activeLogModal && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={() => setActiveLogModal(null)}
|
||||
className="fixed inset-0 z-50 bg-black/40 backdrop-blur-2xs"
|
||||
/>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
className="fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2 w-full max-w-md rounded-2xl border border-border bg-surface p-5 shadow-2xl"
|
||||
>
|
||||
<div className="flex items-center justify-between border-b border-border pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle className="h-4 w-4 text-amber-500" />
|
||||
<h3 className="font-heading text-sm font-bold text-ink-1">
|
||||
{activeLogModal.title}
|
||||
</h3>
|
||||
</div>
|
||||
<span className="rounded-full bg-rose-100 dark:bg-rose-900/50 px-2 py-0.5 font-mono text-[11px] font-bold text-rose-800 dark:text-rose-300">
|
||||
{activeLogModal.occurrences}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2.5 py-4 text-xs">
|
||||
<p className="text-body-sm text-ink-2 leading-relaxed">
|
||||
{activeLogModal.detail}
|
||||
</p>
|
||||
<div className="rounded-lg bg-surface-subtle border border-border p-2.5 text-ink-3">
|
||||
<span className="font-bold text-ink-1 block mb-0.5">
|
||||
Remediation:
|
||||
</span>
|
||||
<span>{activeLogModal.solution}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end pt-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveLogModal(null)}
|
||||
className="rounded-lg border border-border bg-surface px-4 py-1.5 text-xs font-semibold text-ink-2 hover:bg-surface-elevated transition-colors cursor-pointer"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
37
src/pages/doormile/agents/CountUp.jsx
Normal file
37
src/pages/doormile/agents/CountUp.jsx
Normal file
@@ -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 };
|
||||
453
src/pages/doormile/agents/agentWorkflowsData.js
Normal file
453
src/pages/doormile/agents/agentWorkflowsData.js
Normal file
@@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
107
tests/integration/agentsPage.test.jsx
Normal file
107
tests/integration/agentsPage.test.jsx
Normal file
@@ -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) => <span data-testid={`icon-${String(prop)}`} {...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(<Agents />);
|
||||
['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(<Agents />);
|
||||
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(<Agents />);
|
||||
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(<Agents />);
|
||||
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(<Agents />);
|
||||
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(<Agents />);
|
||||
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(<Agents />);
|
||||
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(<Agents />);
|
||||
expect(screen.getAllByText(/Last run · 16–20 Sep 2026/).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('shows the repeated-error count on the log rows', () => {
|
||||
render(<Agents />);
|
||||
const errs = screen.getAllByText('×8,594');
|
||||
expect(errs).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('every card carries a real trigger label, not undefined', () => {
|
||||
const { container } = render(<Agents />);
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
153
tests/lib/agentNetwork.test.js
Normal file
153
tests/lib/agentNetwork.test.js
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user