FairRelay / brain /frontend /visualization.html
MouleeswaranM's picture
Upload folder using huggingface_hub
fcf8749 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Agent Visualization | Fair Dispatch System</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-primary: #0a0f1a;
--bg-secondary: #0d1929;
--bg-card: #0f1e2e;
--bg-card-hover: #142639;
--accent-primary: #00d4aa;
--accent-secondary: #00b894;
--accent-glow: rgba(0, 212, 170, 0.4);
--accent-blue: #3b82f6;
--accent-yellow: #fbbf24;
--accent-purple: #a855f7;
--accent-orange: #f97316;
--accent-pink: #ec4899;
--text-primary: #ffffff;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--border-primary: #1e3a5f;
--status-active: #00d4aa;
--status-processing: #fbbf24;
--status-idle: #64748b;
--status-error: #ef4444;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
overflow-x: hidden;
}
.header {
height: 60px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-primary);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
}
.header-left { display: flex; align-items: center; gap: 16px; }
.logo {
width: 40px;
height: 40px;
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 20px var(--accent-glow);
}
.logo svg { width: 24px; height: 24px; color: var(--bg-primary); }
.header-title h1 { font-size: 18px; font-weight: 600; }
.header-title .subtitle { font-size: 12px; color: var(--text-muted); }
.header-nav { display: flex; gap: 8px; }
.header-nav a {
color: var(--text-secondary);
text-decoration: none;
padding: 8px 16px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
transition: all 0.2s;
border: 1px solid transparent;
}
.header-nav a:hover { color: var(--accent-primary); background: rgba(0, 212, 170, 0.1); }
.header-nav a.active { color: var(--accent-primary); background: rgba(0, 212, 170, 0.15); border-color: var(--accent-primary); }
.status-indicator { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 500; color: var(--status-idle); }
.status-indicator.active { color: var(--status-active); }
.status-indicator.processing { color: var(--status-processing); }
.status-dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; }
.status-indicator.processing .status-dot { animation: pulse 1s infinite; }
@keyframes pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.5; transform: scale(1.3); } }
.main-container { margin-top: 60px; padding: 20px; display: flex; flex-direction: column; gap: 20px; }
.io-section { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.io-panel { background: var(--bg-card); border: 1px solid var(--border-primary); border-radius: 12px; overflow: hidden; }
.io-panel-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: var(--bg-secondary); border-bottom: 1px solid var(--border-primary); }
.io-panel-title { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600; }
.io-panel-title svg { width: 16px; height: 16px; color: var(--accent-primary); }
.io-panel textarea {
width: 100%;
height: 350px;
background: var(--bg-primary);
border: none;
color: var(--accent-primary);
font-family: 'Monaco', 'Menlo', monospace;
font-size: 11px;
padding: 16px;
resize: none;
line-height: 1.6;
}
.io-panel textarea:focus { outline: none; }
.io-panel pre { margin: 0; padding: 16px; font-family: 'Monaco', monospace; font-size: 11px; color: var(--accent-primary); white-space: pre-wrap; max-height: 350px; overflow-y: auto; background: var(--bg-primary); }
.btn-run {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 24px;
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
border: none;
border-radius: 8px;
color: var(--bg-primary);
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 4px 20px var(--accent-glow);
}
.btn-run:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 8px 30px var(--accent-glow); }
.btn-run:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-run svg { width: 18px; height: 18px; }
.agent-network-section { background: var(--bg-card); border: 1px solid var(--border-primary); border-radius: 12px; padding: 24px; }
.section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24px; }
.section-title { font-size: 16px; font-weight: 600; display: flex; align-items: center; gap: 10px; }
.section-title svg { color: var(--accent-primary); }
.agent-network { position: relative; min-height: 380px; }
.connections-svg { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 1; }
.orchestrator-node { position: relative; margin: 0 auto 40px; width: 280px; z-index: 10; }
.agent-node {
background: var(--bg-secondary);
border: 2px solid var(--border-primary);
border-radius: 12px;
padding: 16px 20px;
cursor: pointer;
transition: all 0.3s ease;
}
.agent-node:hover { background: var(--bg-card-hover); border-color: var(--accent-primary); box-shadow: 0 0 30px var(--accent-glow); transform: translateY(-4px); }
.agent-node.orchestrator { background: linear-gradient(135deg, rgba(0, 212, 170, 0.15), rgba(0, 184, 148, 0.1)); border-color: var(--accent-primary); box-shadow: 0 0 40px var(--accent-glow); }
.agent-node.processing { border-color: var(--accent-yellow); box-shadow: 0 0 30px rgba(251, 191, 36, 0.4); animation: processingPulse 1.5s ease-in-out infinite; }
.agent-node.completed { border-color: var(--accent-primary); }
.agent-node.error { border-color: var(--status-error); box-shadow: 0 0 30px rgba(239, 68, 68, 0.4); }
@keyframes processingPulse { 0%, 100% { box-shadow: 0 0 20px rgba(251, 191, 36, 0.3); } 50% { box-shadow: 0 0 40px rgba(251, 191, 36, 0.6); } }
.agent-icon { width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center; justify-content: center; margin-bottom: 12px; }
.agent-icon svg { width: 22px; height: 22px; color: var(--bg-primary); }
.agent-icon.orchestrator { background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); }
.agent-icon.database { background: linear-gradient(135deg, var(--accent-blue), #2563eb); }
.agent-icon.planner { background: linear-gradient(135deg, var(--accent-purple), #9333ea); }
.agent-icon.fairness { background: linear-gradient(135deg, var(--accent-yellow), #f59e0b); }
.agent-icon.liaison { background: linear-gradient(135deg, var(--accent-orange), #ea580c); }
.agent-icon.explain { background: linear-gradient(135deg, var(--accent-pink), #db2777); }
.agent-icon.learning { background: linear-gradient(135deg, #06b6d4, #0891b2); }
.agent-name { font-size: 14px; font-weight: 600; margin-bottom: 4px; }
.agent-description { font-size: 11px; color: var(--text-muted); margin-bottom: 12px; line-height: 1.4; }
.agent-footer { display: flex; align-items: center; justify-content: space-between; padding-top: 12px; border-top: 1px solid var(--border-primary); }
.agent-status { display: flex; align-items: center; gap: 6px; font-size: 11px; font-weight: 500; }
.agent-status.idle { color: var(--text-muted); }
.agent-status.processing { color: var(--status-processing); }
.agent-status.completed { color: var(--status-active); }
.agent-status .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.agent-status.processing .dot { animation: statusPulse 0.8s ease-in-out infinite; }
@keyframes statusPulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
.agent-meta { font-size: 11px; color: var(--text-muted); }
.agent-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 16px; z-index: 10; }
.agent-grid .agent-node { min-height: 180px; }
.connection-line { fill: none; stroke: var(--accent-primary); stroke-width: 2; opacity: 0.6; stroke-dasharray: 8, 4; animation: flow 1s linear infinite; }
@keyframes flow { from { stroke-dashoffset: 12; } to { stroke-dashoffset: 0; } }
.results-section { background: var(--bg-card); border: 1px solid var(--border-primary); border-radius: 12px; overflow: hidden; }
.results-header { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px; background: var(--bg-secondary); border-bottom: 1px solid var(--border-primary); }
.results-title { font-size: 15px; font-weight: 600; display: flex; align-items: center; gap: 10px; }
.results-title svg { color: var(--accent-primary); }
.results-metrics { display: flex; gap: 20px; }
.metric-item { display: flex; flex-direction: column; align-items: center; padding: 8px 16px; background: var(--bg-card); border-radius: 8px; border: 1px solid var(--border-primary); }
.metric-value { font-size: 18px; font-weight: 700; color: var(--accent-primary); }
.metric-label { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
.results-table-wrapper { max-height: 350px; overflow-y: auto; }
.results-table { width: 100%; border-collapse: collapse; }
.results-table th { position: sticky; top: 0; background: var(--bg-secondary); padding: 12px 16px; text-align: left; font-size: 12px; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 1px solid var(--border-primary); }
.results-table td { padding: 14px 16px; font-size: 13px; border-bottom: 1px solid rgba(30, 58, 95, 0.5); }
.results-table tr:hover td { background: var(--bg-card-hover); }
.driver-cell { display: flex; align-items: center; gap: 10px; }
.driver-avatar { width: 32px; height: 32px; border-radius: 8px; background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600; color: white; }
.driver-info { display: flex; flex-direction: column; }
.driver-name { font-weight: 500; }
.driver-id { font-size: 11px; color: var(--text-muted); }
.vehicle-badge { display: inline-flex; align-items: center; gap: 4px; padding: 4px 10px; border-radius: 20px; font-size: 11px; font-weight: 500; }
.vehicle-badge.bike { background: rgba(168, 85, 247, 0.15); color: var(--accent-purple); border: 1px solid rgba(168, 85, 247, 0.3); }
.vehicle-badge.van { background: rgba(59, 130, 246, 0.15); color: var(--accent-blue); border: 1px solid rgba(59, 130, 246, 0.3); }
.score-cell { display: flex; flex-direction: column; }
.score-value { font-weight: 600; color: var(--accent-primary); }
.score-bar { width: 80px; height: 4px; background: var(--bg-primary); border-radius: 2px; margin-top: 4px; overflow: hidden; }
.score-bar-fill { height: 100%; background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary)); border-radius: 2px; transition: width 0.5s ease; }
.fairness-badge { display: inline-flex; align-items: center; padding: 4px 10px; border-radius: 20px; font-size: 11px; font-weight: 600; }
.fairness-badge.excellent { background: rgba(0, 212, 170, 0.15); color: var(--accent-primary); border: 1px solid rgba(0, 212, 170, 0.3); }
.fairness-badge.good { background: rgba(59, 130, 246, 0.15); color: var(--accent-blue); border: 1px solid rgba(59, 130, 246, 0.3); }
.fairness-badge.fair { background: rgba(251, 191, 36, 0.15); color: var(--accent-yellow); border: 1px solid rgba(251, 191, 36, 0.3); }
.route-info { display: flex; flex-direction: column; gap: 2px; }
.route-stops { font-weight: 500; }
.route-details { font-size: 11px; color: var(--text-muted); }
.modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.8); display: none; align-items: center; justify-content: center; z-index: 1000; backdrop-filter: blur(8px); }
.modal.active { display: flex; }
.modal-content { background: #0d0d0d; border: 1px solid var(--border-primary); border-radius: 12px; width: 90%; max-width: 800px; max-height: 80vh; overflow: hidden; box-shadow: 0 25px 60px rgba(0, 0, 0, 0.5); }
.terminal-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: #1a1a1a; border-bottom: 1px solid #333; }
.terminal-dots { display: flex; gap: 8px; }
.terminal-dot { width: 12px; height: 12px; border-radius: 50%; }
.terminal-dot.red { background: #ff5f56; }
.terminal-dot.yellow { background: #ffbd2e; }
.terminal-dot.green { background: #27c93f; }
.terminal-title { font-size: 13px; font-weight: 500; color: var(--text-secondary); display: flex; align-items: center; gap: 8px; }
.terminal-title svg { width: 16px; height: 16px; color: var(--accent-primary); }
.close-btn { width: 28px; height: 28px; background: none; border: none; color: var(--text-muted); font-size: 20px; cursor: pointer; display: flex; align-items: center; justify-content: center; border-radius: 4px; }
.close-btn:hover { background: var(--bg-card); color: var(--text-primary); }
.terminal-body { padding: 20px; max-height: 500px; overflow-y: auto; background: #0d0d0d; }
.terminal-content { font-family: 'Monaco', monospace; font-size: 12px; line-height: 1.8; color: #00d4aa; }
.log-entry { margin-bottom: 16px; padding-bottom: 16px; border-bottom: 1px solid #222; }
.log-entry:last-child { border-bottom: none; margin-bottom: 0; padding-bottom: 0; }
.log-timestamp { color: #666; margin-right: 8px; }
.log-step { color: var(--accent-yellow); font-weight: 500; }
.log-message { color: var(--accent-primary); margin-top: 4px; }
.log-json { background: #111; padding: 12px; border-radius: 6px; margin-top: 8px; white-space: pre-wrap; font-size: 11px; color: #aaa; max-height: 200px; overflow-y: auto; }
.loading-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(10, 15, 26, 0.95); display: none; align-items: center; justify-content: center; z-index: 2000; flex-direction: column; gap: 30px; }
.loading-overlay.active { display: flex; }
.loading-spinner { position: relative; width: 100px; height: 100px; }
.spinner-ring { position: absolute; width: 100%; height: 100%; border-radius: 50%; border: 3px solid transparent; }
.spinner-ring:nth-child(1) { border-top-color: var(--accent-primary); animation: spin 1s linear infinite; }
.spinner-ring:nth-child(2) { width: 80%; height: 80%; top: 10%; left: 10%; border-right-color: var(--accent-blue); animation: spin 1.5s linear infinite reverse; }
.spinner-ring:nth-child(3) { width: 60%; height: 60%; top: 20%; left: 20%; border-bottom-color: var(--accent-yellow); animation: spin 2s linear infinite; }
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.loading-text { font-size: 16px; color: var(--text-secondary); }
.loading-agent { font-size: 14px; color: var(--accent-primary); font-weight: 500; margin-top: -15px; }
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: var(--bg-primary); }
::-webkit-scrollbar-thumb { background: var(--border-primary); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: var(--accent-primary); }
.empty-state { text-align: center; padding: 60px 20px; color: var(--text-muted); }
.empty-state svg { width: 64px; height: 64px; margin-bottom: 16px; opacity: 0.5; }
.empty-state h3 { font-size: 16px; color: var(--text-secondary); margin-bottom: 8px; }
@media (max-width: 1200px) { .agent-grid { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 768px) { .io-section { grid-template-columns: 1fr; } .agent-grid { grid-template-columns: repeat(2, 1fr); } .results-metrics { display: none; } }
</style>
</head>
<body>
<header class="header">
<div class="header-left">
<div class="logo">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
</svg>
</div>
<div class="header-title">
<h1>Fair Dispatch System</h1>
<span class="subtitle">Multi-Agent Visualization</span>
</div>
</div>
<nav class="header-nav">
<a href="/demo/allocate">Allocate</a>
<a href="/demo/visualization" class="active">Visualization</a>
<a href="/docs">API Docs</a>
</nav>
<div class="header-right">
<div class="status-indicator" id="systemStatus">
<span class="status-dot"></span>
<span>Ready</span>
</div>
</div>
</header>
<main class="main-container">
<section class="io-section">
<div class="io-panel">
<div class="io-panel-header">
<div class="io-panel-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
Request JSON
</div>
<button class="btn-run" id="btnRun" onclick="runAllocation()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>
Run LangGraph Allocation
</button>
</div>
<div class="io-panel-body">
<textarea id="requestJson" spellcheck="false">{
"allocation_date": "2026-03-10",
"warehouse": {
"lat": 13.0827,
"lng": 80.2707
},
"drivers": [
{"id": "driver_001", "name": "Raju", "vehicle_capacity_kg": 50},
{"id": "driver_002", "name": "Priya", "vehicle_capacity_kg": 200},
{"id": "driver_003", "name": "Kumar", "vehicle_capacity_kg": 50},
{"id": "driver_004", "name": "Lakshmi", "vehicle_capacity_kg": 50},
{"id": "driver_005", "name": "Arjun", "vehicle_capacity_kg": 200}
],
"packages": [
{"id": "pkg_001", "weight_kg": 2.5, "latitude": 13.0850, "longitude": 80.2750, "address": "12 Anna Salai, Chennai", "priority": "NORMAL"},
{"id": "pkg_002", "weight_kg": 5.0, "latitude": 13.0870, "longitude": 80.2780, "address": "45 Mount Rd, Chennai", "priority": "EXPRESS"},
{"id": "pkg_003", "weight_kg": 1.5, "latitude": 13.0800, "longitude": 80.2680, "address": "78 Triplicane, Chennai", "priority": "NORMAL"},
{"id": "pkg_004", "weight_kg": 8.0, "latitude": 13.0900, "longitude": 80.2800, "address": "23 Adyar, Chennai", "priority": "NORMAL"},
{"id": "pkg_005", "weight_kg": 3.0, "latitude": 13.0750, "longitude": 80.2650, "address": "56 T Nagar, Chennai", "priority": "EXPRESS"},
{"id": "pkg_006", "weight_kg": 4.5, "latitude": 13.0880, "longitude": 80.2720, "address": "90 Mylapore, Chennai", "priority": "NORMAL"},
{"id": "pkg_007", "weight_kg": 2.0, "latitude": 13.0820, "longitude": 80.2690, "address": "34 Egmore, Chennai", "priority": "NORMAL"},
{"id": "pkg_008", "weight_kg": 6.0, "latitude": 13.0860, "longitude": 80.2780, "address": "67 Nungambakkam, Chennai", "priority": "HIGH"}
]
}</textarea>
</div>
</div>
<div class="io-panel">
<div class="io-panel-header">
<div class="io-panel-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>
Response
</div>
</div>
<div class="io-panel-body">
<pre id="responseJson">Allocation results will appear here...</pre>
</div>
</div>
</section>
<section class="agent-network-section">
<div class="section-header">
<div class="section-title">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 1v4M12 19v4M4.22 4.22l2.83 2.83M16.95 16.95l2.83 2.83M1 12h4M19 12h4M4.22 19.78l2.83-2.83M16.95 7.05l2.83-2.83"/></svg>
Multi-Agent Workflow
</div>
</div>
<div class="agent-network">
<svg class="connections-svg" id="connectionsSvg"></svg>
<div class="orchestrator-node">
<div class="agent-node orchestrator" id="agent-orchestrator" onclick="openTerminal('ORCHESTRATOR')">
<div class="agent-icon orchestrator">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 1v4M12 19v4M4.22 4.22l2.83 2.83M16.95 16.95l2.83 2.83M1 12h4M19 12h4M4.22 19.78l2.83-2.83M16.95 7.05l2.83-2.83"/></svg>
</div>
<div class="agent-name">Central Orchestrator</div>
<div class="agent-description">Coordinates all agents and manages workflow state</div>
<div class="agent-footer">
<div class="agent-status idle" id="status-orchestrator"><span class="dot"></span><span>Idle</span></div>
<span class="agent-meta" id="meta-orchestrator">6 agents connected</span>
</div>
</div>
</div>
<div class="agent-grid">
<div class="agent-node" id="agent-ml_effort" onclick="openTerminal('ML_EFFORT')">
<div class="agent-icon database"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg></div>
<div class="agent-name">ML Effort Agent</div>
<div class="agent-description">Computes effort matrix using ML models</div>
<div class="agent-footer"><div class="agent-status idle" id="status-ml_effort"><span class="dot"></span><span>Idle</span></div><span class="agent-meta" id="meta-ml_effort">--</span></div>
</div>
<div class="agent-node" id="agent-route_planner" onclick="openTerminal('ROUTE_PLANNER')">
<div class="agent-icon planner"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3h18v18H3zM9 3v18M15 3v18M3 9h18M3 15h18"/></svg></div>
<div class="agent-name">Route Planner Agent</div>
<div class="agent-description">Generates optimal route assignments</div>
<div class="agent-footer"><div class="agent-status idle" id="status-route_planner"><span class="dot"></span><span>Idle</span></div><span class="agent-meta" id="meta-route_planner">0 proposals</span></div>
</div>
<div class="agent-node" id="agent-fairness_manager" onclick="openTerminal('FAIRNESS_MANAGER')">
<div class="agent-icon fairness"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3v18M5 8h14l-2 8H7L5 8z"/><circle cx="7" cy="20" r="2"/><circle cx="17" cy="20" r="2"/></svg></div>
<div class="agent-name">Fairness Manager</div>
<div class="agent-description">Evaluates workload fairness across drivers</div>
<div class="agent-footer"><div class="agent-status idle" id="status-fairness_manager"><span class="dot"></span><span>Idle</span></div><span class="agent-meta" id="meta-fairness_manager">Gini: --</span></div>
</div>
<div class="agent-node" id="agent-driver_liaison" onclick="openTerminal('DRIVER_LIAISON')">
<div class="agent-icon liaison"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg></div>
<div class="agent-name">Driver Liaison</div>
<div class="agent-description">Negotiates with drivers and handles feedback</div>
<div class="agent-footer"><div class="agent-status idle" id="status-driver_liaison"><span class="dot"></span><span>Idle</span></div><span class="agent-meta" id="meta-driver_liaison">0 interactions</span></div>
</div>
<div class="agent-node" id="agent-explainability" onclick="openTerminal('EXPLAINABILITY')">
<div class="agent-icon explain"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></div>
<div class="agent-name">Explainability Agent</div>
<div class="agent-description">Generates human-readable explanations</div>
<div class="agent-footer"><div class="agent-status idle" id="status-explainability"><span class="dot"></span><span>Idle</span></div><span class="agent-meta" id="meta-explainability">0 explanations</span></div>
</div>
<div class="agent-node" id="agent-learning" onclick="openTerminal('LEARNING')">
<div class="agent-icon learning"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg></div>
<div class="agent-name">Learning Agent</div>
<div class="agent-description">Continuous improvement from feedback</div>
<div class="agent-footer"><div class="agent-status idle" id="status-learning"><span class="dot"></span><span>Idle</span></div><span class="agent-meta" id="meta-learning">Improving</span></div>
</div>
</div>
</div>
</section>
<section class="results-section" id="resultsSection" style="display: none;">
<div class="results-header">
<div class="results-title">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="9" y1="21" x2="9" y2="9"/></svg>
Driver Route Allocation Results
</div>
<div class="results-metrics">
<div class="metric-item"><span class="metric-value" id="metricAvgWorkload">--</span><span class="metric-label">Avg Workload</span></div>
<div class="metric-item"><span class="metric-value" id="metricGini">--</span><span class="metric-label">Gini Index</span></div>
<div class="metric-item"><span class="metric-value" id="metricStdDev">--</span><span class="metric-label">Std Dev</span></div>
</div>
</div>
<div class="results-table-wrapper">
<table class="results-table">
<thead><tr><th>Driver</th><th>Vehicle</th><th>Route</th><th>Workload Score</th><th>Fairness Score</th><th>Explanation</th></tr></thead>
<tbody id="resultsTableBody"></tbody>
</table>
</div>
</section>
</main>
<div class="modal" id="terminalModal">
<div class="modal-content">
<div class="terminal-header">
<div class="terminal-dots"><span class="terminal-dot red"></span><span class="terminal-dot yellow"></span><span class="terminal-dot green"></span></div>
<div class="terminal-title"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg><span id="terminalTitle">Agent Logs</span></div>
<button class="close-btn" onclick="closeTerminal()">&times;</button>
</div>
<div class="terminal-body">
<div class="terminal-content" id="terminalContent">
<div class="empty-state"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg><h3>No logs yet</h3><p>Run an allocation to see agent decision logs</p></div>
</div>
</div>
</div>
</div>
<div class="loading-overlay" id="loadingOverlay">
<div class="loading-spinner"><div class="spinner-ring"></div><div class="spinner-ring"></div><div class="spinner-ring"></div></div>
<div class="loading-text">Processing allocation...</div>
<div class="loading-agent" id="loadingAgent">Initializing agents...</div>
</div>
<script>
let allocationRunId = null;
let agentLogs = {};
let currentResponse = null;
const AGENT_NAMES = { 'ORCHESTRATOR': 'Central Orchestrator', 'ML_EFFORT': 'ML Effort Agent', 'ROUTE_PLANNER': 'Route Planner Agent', 'FAIRNESS_MANAGER': 'Fairness Manager', 'DRIVER_LIAISON': 'Driver Liaison', 'EXPLAINABILITY': 'Explainability Agent', 'LEARNING': 'Learning Agent' };
function generateMockResponse(requestData) {
const drivers = requestData.drivers || [];
const packages = requestData.packages || [];
const numDrivers = drivers.length || 1;
const pkgsPerDriver = Math.ceil(packages.length / numDrivers);
const vehicleTypes = ['bike', 'van'];
const assignments = drivers.map((driver, i) => {
const myPkgs = packages.slice(i * pkgsPerDriver, (i + 1) * pkgsPerDriver);
const totalWeight = myPkgs.reduce((s, p) => s + (p.weight_kg || 2), 0);
const workloadScore = totalWeight * 1.0 + myPkgs.length * 0.5 + Math.random() * 3;
const vType = driver.vehicle_capacity_kg > 100 ? 'van' : 'bike';
return {
driver_external_id: driver.id || driver.external_id || `driver_${i+1}`,
driver_name: driver.name || `Driver ${i+1}`,
vehicle_type: vType,
workload_score: parseFloat(workloadScore.toFixed(2)),
fairness_score: parseFloat((0.82 + Math.random() * 0.17).toFixed(3)),
route_summary: {
num_stops: myPkgs.length,
num_packages: myPkgs.length,
total_weight_kg: parseFloat(totalWeight.toFixed(1)),
estimated_time_minutes: myPkgs.length * 12 + Math.floor(Math.random() * 15),
},
explanation: `${driver.name || 'Driver'} assigned ${myPkgs.length} packages (${totalWeight.toFixed(1)} kg) via ${vType}. Route optimized for minimal travel distance and balanced workload distribution.`,
packages: myPkgs.map(p => p.id || p.external_id || 'pkg'),
};
});
const workloads = assignments.map(a => a.workload_score);
const avgWorkload = workloads.reduce((s, v) => s + v, 0) / workloads.length;
const sortedW = [...workloads].sort((a, b) => a - b);
let giniNum = 0;
for (let i = 0; i < sortedW.length; i++) for (let j = 0; j < sortedW.length; j++) giniNum += Math.abs(sortedW[i] - sortedW[j]);
const giniIndex = giniNum / (2 * sortedW.length * sortedW.reduce((s, v) => s + v, 0)) || 0;
const stdDev = Math.sqrt(workloads.reduce((s, v) => s + (v - avgWorkload) ** 2, 0) / workloads.length);
return {
allocation_run_id: 'mock-' + crypto.randomUUID().slice(0, 8),
status: 'completed',
assignments,
global_fairness: {
avg_workload: parseFloat(avgWorkload.toFixed(2)),
gini_index: parseFloat(giniIndex.toFixed(4)),
std_dev: parseFloat(stdDev.toFixed(2)),
max_gap: parseFloat((Math.max(...workloads) - Math.min(...workloads)).toFixed(2)),
},
metadata: {
num_drivers: numDrivers,
num_packages: packages.length,
allocation_date: requestData.allocation_date || new Date().toISOString().slice(0, 10),
engine: 'langgraph_multi_agent',
mode: 'demo',
},
};
}
function generateMockAgentLogs(requestData, response) {
const now = new Date();
const ts = (offsetMs) => new Date(now.getTime() + offsetMs).toISOString();
const nd = requestData.drivers?.length || 0;
const np = requestData.packages?.length || 0;
return {
'ORCHESTRATOR': [
{ timestamp: ts(0), step_type: 'INIT', short_message: `Received allocation request: ${nd} drivers, ${np} packages` },
{ timestamp: ts(300), step_type: 'DISPATCH', short_message: 'Dispatching to ML_EFFORT agent for effort matrix computation' },
{ timestamp: ts(2400), step_type: 'COMPLETE', short_message: `All agents finished. Gini index: ${response.global_fairness.gini_index}` },
],
'ML_EFFORT': [
{ timestamp: ts(400), step_type: 'COMPUTE', short_message: `Computing effort matrix for ${nd}x${np} driver-package pairs` },
{ timestamp: ts(800), step_type: 'FEATURES', short_message: 'Extracted features: distance, weight, vehicle capacity, priority' },
{ timestamp: ts(1000), step_type: 'RESULT', short_message: `Computed effort matrix for ${nd} drivers x ${np} packages`, output_snapshot: { matrix_shape: [nd, np], avg_effort: 12.4, max_effort: 28.7 } },
],
'ROUTE_PLANNER': [
{ timestamp: ts(1100), step_type: 'PROPOSAL_1', short_message: `Generated initial route proposal using K-Means clustering (k=${nd})` },
{ timestamp: ts(1400), step_type: 'OPTIMIZE', short_message: 'Applied 2-opt local search optimization on each route' },
{ timestamp: ts(1600), step_type: 'PROPOSAL_2', short_message: `Refined proposal: swapped 2 packages between drivers for better balance` },
],
'FAIRNESS_MANAGER': [
{ timestamp: ts(1650), step_type: 'EVALUATE', short_message: `Evaluating fairness: Gini=${response.global_fairness.gini_index}, StdDev=${response.global_fairness.std_dev}` },
{ timestamp: ts(1800), step_type: 'VERDICT', short_message: response.global_fairness.gini_index < 0.15 ? 'Fairness check PASSED - allocation accepted' : 'Fairness marginal - requesting re-optimization' },
],
'DRIVER_LIAISON': [
{ timestamp: ts(1850), step_type: 'NOTIFY', short_message: `Simulated driver notifications sent to ${nd} drivers` },
{ timestamp: ts(2000), step_type: 'RESPONSE', short_message: `${nd} ACCEPT, 0 REJECT, 0 PENDING` },
],
'EXPLAINABILITY': [
{ timestamp: ts(2050), step_type: 'GENERATE', short_message: `Generated ${nd} human-readable explanations for driver assignments` },
{ timestamp: ts(2200), step_type: 'SUMMARY', short_message: `Fairness summary: workload range ${response.global_fairness.max_gap.toFixed(1)}, all drivers within acceptable bounds` },
],
'LEARNING': [
{ timestamp: ts(2250), step_type: 'RECORD', short_message: 'Recorded allocation outcome for continuous improvement' },
{ timestamp: ts(2350), step_type: 'UPDATE', short_message: 'Updated driver preference weights based on historical acceptance rates' },
],
};
}
async function runAllocation() {
const btn = document.getElementById('btnRun');
let requestData;
try { requestData = JSON.parse(document.getElementById('requestJson').value); } catch (e) { alert('Invalid JSON: ' + e.message); return; }
resetAgentStates(); agentLogs = {}; allocationRunId = null;
document.getElementById('resultsSection').style.display = 'none';
document.getElementById('responseJson').textContent = 'Processing...';
showLoading(true); btn.disabled = true; updateSystemStatus('processing', 'Processing...');
try {
await simulateAgentProgress();
let useMock = false;
try {
const response = await fetch('/api/v1/allocate/langgraph', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestData) });
if (!response.ok) {
const errBody = await response.json().catch(() => ({}));
const detail = errBody.detail;
const msg = typeof detail === 'string' ? detail : Array.isArray(detail) ? detail.map(d => d.msg || JSON.stringify(d)).join('; ') : 'Allocation failed';
throw new Error(msg);
}
currentResponse = await response.json();
} catch (apiError) {
console.warn('API unavailable, using mock data:', apiError.message);
useMock = true;
currentResponse = generateMockResponse(requestData);
}
allocationRunId = currentResponse.allocation_run_id;
document.getElementById('responseJson').textContent = JSON.stringify(currentResponse, null, 2);
if (useMock) {
agentLogs = generateMockAgentLogs(requestData, currentResponse);
updateAgentMetaFromMock(currentResponse);
} else {
await fetchAgentTimeline(allocationRunId);
}
displayResultsTable(currentResponse);
completeAllAgents(); updateSystemStatus('active', 'Completed' + (useMock ? ' (Demo)' : ''));
} catch (error) {
console.error('Allocation error:', error);
document.getElementById('responseJson').textContent = 'Error: ' + error.message;
setAgentError(); updateSystemStatus('idle', 'Error');
} finally { showLoading(false); btn.disabled = false; }
}
function updateAgentMetaFromMock(response) {
const n = response.assignments?.length || 0;
document.getElementById('meta-orchestrator').textContent = `${n} drivers allocated`;
document.getElementById('meta-ml_effort').textContent = `${n}x${response.metadata?.num_packages || '?'} matrix`;
document.getElementById('meta-route_planner').textContent = '2 proposals';
document.getElementById('meta-fairness_manager').textContent = `Gini: ${response.global_fairness?.gini_index?.toFixed(3) || '--'}`;
document.getElementById('meta-driver_liaison').textContent = `${n} accepted`;
document.getElementById('meta-explainability').textContent = `${n} explanations`;
document.getElementById('meta-learning').textContent = 'Weights updated';
}
async function fetchAgentTimeline(runId) {
try {
const response = await fetch(`/api/v1/admin/agent_timeline?allocation_run_id=${runId}`);
if (!response.ok) return;
const data = await response.json();
agentLogs = { 'ORCHESTRATOR': [], 'ML_EFFORT': [], 'ROUTE_PLANNER': [], 'FAIRNESS_MANAGER': [], 'DRIVER_LIAISON': [], 'EXPLAINABILITY': [], 'LEARNING': [] };
if (data.timeline) { data.timeline.forEach(event => { if (agentLogs[event.agent_name]) agentLogs[event.agent_name].push(event); }); }
if (data.steps) { data.steps.forEach(step => { if (agentLogs[step.agent_name]) { const existing = agentLogs[step.agent_name].find(e => e.step_type === step.step_type); if (existing) { existing.input_snapshot = step.input_snapshot; existing.output_snapshot = step.output_snapshot; } } }); }
updateAgentMetaFromLogs(data);
} catch (error) { console.error('Failed to fetch agent timeline:', error); }
}
function resetAgentStates() {
['orchestrator', 'ml_effort', 'route_planner', 'fairness_manager', 'driver_liaison', 'explainability', 'learning'].forEach(agent => {
const node = document.getElementById(`agent-${agent}`); const status = document.getElementById(`status-${agent}`);
if (node) node.classList.remove('active', 'processing', 'completed', 'error');
if (status) { status.className = 'agent-status idle'; status.querySelector('span:last-child').textContent = 'Idle'; }
});
}
function setAgentState(agentId, state) {
const node = document.getElementById(`agent-${agentId}`); const status = document.getElementById(`status-${agentId}`);
if (node) { node.classList.remove('active', 'processing', 'completed', 'error'); node.classList.add(state); }
if (status) { status.className = `agent-status ${state}`; status.querySelector('span:last-child').textContent = state.charAt(0).toUpperCase() + state.slice(1); }
}
async function simulateAgentProgress() {
const agents = [{ id: 'orchestrator', name: 'Central Orchestrator', delay: 200 }, { id: 'ml_effort', name: 'ML Effort Agent', delay: 400 }, { id: 'route_planner', name: 'Route Planner Agent', delay: 500 }, { id: 'fairness_manager', name: 'Fairness Manager', delay: 400 }, { id: 'driver_liaison', name: 'Driver Liaison', delay: 300 }, { id: 'explainability', name: 'Explainability Agent', delay: 300 }, { id: 'learning', name: 'Learning Agent', delay: 200 }];
for (const agent of agents) { updateLoadingAgent(agent.name); setAgentState(agent.id, 'processing'); await delay(agent.delay); }
}
function completeAllAgents() { ['orchestrator', 'ml_effort', 'route_planner', 'fairness_manager', 'driver_liaison', 'explainability', 'learning'].forEach(agent => setAgentState(agent, 'completed')); }
function setAgentError() { ['orchestrator', 'ml_effort', 'route_planner', 'fairness_manager', 'driver_liaison', 'explainability', 'learning'].forEach(agent => { const node = document.getElementById(`agent-${agent}`); if (node && node.classList.contains('processing')) setAgentState(agent, 'error'); }); }
function updateAgentMetaFromLogs(data) {
if (data.timeline) { data.timeline.forEach(event => {
const metaEl = document.getElementById(`meta-${event.agent_name.toLowerCase()}`);
if (metaEl && event.short_message) {
const msg = event.short_message;
if (event.agent_name === 'ML_EFFORT') metaEl.textContent = msg.replace('Computed effort matrix for ', '');
else if (event.agent_name === 'ROUTE_PLANNER') metaEl.textContent = event.step_type === 'PROPOSAL_1' ? '1 proposal' : '2 proposals';
else if (event.agent_name === 'FAIRNESS_MANAGER') metaEl.textContent = msg.includes('accepted') ? 'Passed ✓' : 'Re-optimize';
else if (event.agent_name === 'DRIVER_LIAISON') { const match = msg.match(/(\d+) ACCEPT/); metaEl.textContent = match ? `${match[1]} accepted` : msg; }
else if (event.agent_name === 'EXPLAINABILITY') { const match = msg.match(/Generated (\d+)/); metaEl.textContent = match ? `${match[1]} explanations` : msg; }
}
}); }
if (data.allocation_run?.global_metrics?.gini_index !== null) document.getElementById('meta-fairness_manager').textContent = `Gini: ${data.allocation_run.global_metrics.gini_index.toFixed(3)}`;
}
function openTerminal(agentName) {
const modal = document.getElementById('terminalModal'); const title = document.getElementById('terminalTitle'); const content = document.getElementById('terminalContent');
title.textContent = `${AGENT_NAMES[agentName] || agentName} - Decision Logs`;
const logs = agentLogs[agentName] || [];
if (logs.length === 0) { content.innerHTML = `<div class="empty-state"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg><h3>No logs for ${AGENT_NAMES[agentName]}</h3><p>Run an allocation to see decision logs</p></div>`; }
else {
let html = '';
logs.forEach(log => {
const timestamp = log.timestamp ? new Date(log.timestamp).toLocaleTimeString() : '--:--:--';
html += `<div class="log-entry"><div><span class="log-timestamp">[${timestamp}]</span><span class="log-step">${log.step_type || 'STEP'}</span></div><div class="log-message">${log.short_message || 'Processing...'}</div>${log.input_snapshot || log.output_snapshot ? `<div class="log-json">${log.input_snapshot ? `<strong>Input:</strong>\n${JSON.stringify(log.input_snapshot, null, 2)}\n\n` : ''}${log.output_snapshot ? `<strong>Output:</strong>\n${JSON.stringify(log.output_snapshot, null, 2)}` : ''}</div>` : ''}</div>`;
});
content.innerHTML = html;
}
modal.classList.add('active');
}
function closeTerminal() { document.getElementById('terminalModal').classList.remove('active'); }
function displayResultsTable(response) {
if (!response?.assignments?.length) return;
document.getElementById('resultsSection').style.display = 'block';
if (response.global_fairness) {
document.getElementById('metricAvgWorkload').textContent = response.global_fairness.avg_workload?.toFixed(2) || '--';
document.getElementById('metricGini').textContent = response.global_fairness.gini_index?.toFixed(4) || '--';
document.getElementById('metricStdDev').textContent = response.global_fairness.std_dev?.toFixed(2) || '--';
}
const tbody = document.getElementById('resultsTableBody');
const maxWorkload = Math.max(...response.assignments.map(a => a.workload_score));
let html = '';
response.assignments.forEach(a => {
const initials = a.driver_name.split(' ').map(n => n[0]).join('').toUpperCase();
const vehicleClass = a.route_summary ? 'van' : 'bike';
const workloadPercent = (a.workload_score / maxWorkload * 100).toFixed(0);
const fairnessClass = a.fairness_score >= 0.9 ? 'excellent' : a.fairness_score >= 0.7 ? 'good' : 'fair';
const fairnessLabel = a.fairness_score >= 0.9 ? 'Excellent' : a.fairness_score >= 0.7 ? 'Good' : 'Fair';
html += `<tr><td><div class="driver-cell"><div class="driver-avatar">${initials}</div><div class="driver-info"><span class="driver-name">${a.driver_name}</span><span class="driver-id">${a.driver_external_id}</span></div></div></td><td><span class="vehicle-badge ${vehicleClass}">${vehicleClass === 'van' ? '🚐' : '🚴'} ${vehicleClass.charAt(0).toUpperCase() + vehicleClass.slice(1)}</span></td><td><div class="route-info"><span class="route-stops">${a.route_summary?.num_stops || '?'} stops</span><span class="route-details">${a.route_summary?.num_packages || '?'} pkgs • ${a.route_summary?.total_weight_kg?.toFixed(1) || '?'} kg</span></div></td><td><div class="score-cell"><span class="score-value">${a.workload_score.toFixed(2)}</span><div class="score-bar"><div class="score-bar-fill" style="width: ${workloadPercent}%"></div></div></div></td><td><span class="fairness-badge ${fairnessClass}">${fairnessLabel} (${(a.fairness_score * 100).toFixed(0)}%)</span></td><td><span style="font-size: 12px; color: var(--text-secondary);">${truncate(a.explanation, 60)}</span></td></tr>`;
});
tbody.innerHTML = html;
}
function showLoading(show) { document.getElementById('loadingOverlay').classList.toggle('active', show); }
function updateLoadingAgent(agentName) { document.getElementById('loadingAgent').textContent = `Running: ${agentName}...`; }
function updateSystemStatus(state, text) { const indicator = document.getElementById('systemStatus'); indicator.className = `status-indicator ${state}`; indicator.querySelector('span:last-child').textContent = text; }
function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
function truncate(str, maxLen) { if (!str) return '--'; return str.length > maxLen ? str.substring(0, maxLen) + '...' : str; }
document.getElementById('terminalModal').addEventListener('click', function(e) { if (e.target === this) closeTerminal(); });
document.addEventListener('keydown', function(e) { if (e.key === 'Escape') closeTerminal(); });
function drawConnections() {
const svg = document.getElementById('connectionsSvg');
const orchestrator = document.querySelector('.orchestrator-node');
const agentNodes = document.querySelectorAll('.agent-grid .agent-node');
if (!orchestrator || agentNodes.length === 0) return;
const svgRect = svg.getBoundingClientRect();
const orchRect = orchestrator.getBoundingClientRect();
const orchCenterX = orchRect.left + orchRect.width / 2 - svgRect.left;
const orchBottomY = orchRect.bottom - svgRect.top;
let paths = '';
agentNodes.forEach(node => {
const nodeRect = node.getBoundingClientRect();
const nodeCenterX = nodeRect.left + nodeRect.width / 2 - svgRect.left;
const nodeTopY = nodeRect.top - svgRect.top;
const midY = orchBottomY + (nodeTopY - orchBottomY) / 2;
paths += `<path class="connection-line" d="M ${orchCenterX} ${orchBottomY} C ${orchCenterX} ${midY}, ${nodeCenterX} ${midY}, ${nodeCenterX} ${nodeTopY}" />`;
});
svg.innerHTML = paths;
}
window.addEventListener('load', drawConnections);
window.addEventListener('resize', drawConnections);
</script>
</body>
</html>