FairRelay / brain /frontend /demo.html
MouleeswaranM's picture
Upload folder using huggingface_hub
fcf8749 verified
raw
history blame
26.4 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API Demo | 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;
--accent-primary: #00d4aa;
--accent-secondary: #00b894;
--accent-glow: rgba(0, 212, 170, 0.4);
--text-primary: #ffffff;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--border-primary: #1e3a5f;
--status-success: #00d4aa;
--status-error: #ef4444;
--status-warning: #fbbf24;
}
* {
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;
}
.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) 0%, var(--accent-secondary) 100%);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
}
.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-right {
display: flex;
align-items: center;
gap: 12px;
}
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
border: none;
transition: all 0.2s ease;
}
.btn-primary {
background: linear-gradient(135deg, var(--accent-primary) 0%, var(--accent-secondary) 100%);
color: var(--bg-primary);
box-shadow: 0 4px 16px var(--accent-glow);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 24px var(--accent-glow);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn-secondary {
background: var(--bg-card);
color: var(--text-secondary);
border: 1px solid var(--border-primary);
}
.btn-secondary:hover {
background: var(--bg-secondary);
color: var(--text-primary);
border-color: var(--accent-primary);
}
.main-content {
margin-top: 60px;
padding: 24px;
display: flex;
flex-direction: column;
gap: 20px;
min-height: calc(100vh - 60px);
}
.metrics-bar {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
.metric-card {
background: var(--bg-card);
border: 1px solid var(--border-primary);
border-radius: 12px;
padding: 16px 20px;
display: flex;
flex-direction: column;
gap: 8px;
transition: all 0.3s ease;
}
.metric-card:hover {
border-color: var(--accent-primary);
box-shadow: 0 4px 16px rgba(0, 212, 170, 0.1);
}
.metric-label {
font-size: 12px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.metric-value {
font-size: 28px;
font-weight: 700;
color: var(--accent-primary);
}
.metric-value.pending {
color: var(--text-muted);
}
.panels-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
flex: 1;
}
.panel {
background: var(--bg-card);
border: 1px solid var(--border-primary);
border-radius: 12px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.panel-header {
padding: 16px 20px;
border-bottom: 1px solid var(--border-primary);
display: flex;
align-items: center;
justify-content: space-between;
}
.panel-title {
font-size: 14px;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
.panel-title svg {
width: 18px;
height: 18px;
color: var(--accent-primary);
}
.panel-actions {
display: flex;
gap: 8px;
}
.panel-btn {
padding: 6px 12px;
font-size: 12px;
border-radius: 6px;
background: var(--bg-secondary);
color: var(--text-secondary);
border: 1px solid var(--border-primary);
cursor: pointer;
transition: all 0.2s ease;
}
.panel-btn:hover {
color: var(--text-primary);
border-color: var(--accent-primary);
}
.panel-body {
flex: 1;
padding: 0;
position: relative;
}
.json-textarea {
width: 100%;
height: 100%;
min-height: 400px;
background: transparent;
border: none;
color: var(--text-primary);
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 13px;
line-height: 1.6;
padding: 16px 20px;
resize: none;
outline: none;
}
.json-textarea:focus {
background: rgba(0, 212, 170, 0.02);
}
.json-textarea.readonly {
color: var(--text-secondary);
}
.json-textarea::placeholder {
color: var(--text-muted);
}
.status-indicator {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--text-secondary);
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--status-success);
}
.status-dot.loading {
animation: pulse 1s infinite;
background: var(--status-warning);
}
.status-dot.error {
background: var(--status-error);
}
@keyframes pulse {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.5;
transform: scale(1.2);
}
}
.alert {
padding: 16px 20px;
border-radius: 8px;
font-size: 14px;
display: none;
}
.alert.error {
display: block;
background: rgba(239, 68, 68, 0.1);
border: 1px solid var(--status-error);
color: var(--status-error);
}
.alert.success {
display: block;
background: rgba(0, 212, 170, 0.1);
border: 1px solid var(--status-success);
color: var(--status-success);
}
.curl-section {
background: var(--bg-card);
border: 1px solid var(--border-primary);
border-radius: 12px;
padding: 20px;
}
.curl-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.curl-title {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
}
.curl-code {
background: var(--bg-secondary);
border-radius: 8px;
padding: 16px;
overflow-x: auto;
}
.curl-code pre {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
line-height: 1.6;
color: var(--accent-primary);
margin: 0;
white-space: pre-wrap;
}
@media (max-width: 1024px) {
.panels-container {
grid-template-columns: 1fr;
}
.metrics-bar {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 640px) {
.metrics-bar {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<header class="header">
<div class="header-left">
<div class="logo">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
</svg>
</div>
<div class="header-title">
<h1>API Demo</h1>
<span class="subtitle">Fair Dispatch System</span>
</div>
</div>
<div class="header-right">
<div class="status-indicator" id="status-indicator">
<span class="status-dot" id="status-dot"></span>
<span id="status-text">Ready</span>
</div>
<button class="btn btn-primary" id="run-btn" onclick="runAllocation()">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Start Allocation
</button>
<a href="/" class="btn btn-secondary">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
Back to Dashboard
</a>
</div>
</header>
<main class="main-content">
<div id="alert-container"></div>
<div class="metrics-bar">
<div class="metric-card">
<span class="metric-label">Gini Index</span>
<span class="metric-value pending" id="metric-gini"></span>
</div>
<div class="metric-card">
<span class="metric-label">Std Deviation</span>
<span class="metric-value pending" id="metric-stddev"></span>
</div>
<div class="metric-card">
<span class="metric-label">Avg Workload</span>
<span class="metric-value pending" id="metric-avg"></span>
</div>
<div class="metric-card">
<span class="metric-label">Assignments</span>
<span class="metric-value pending" id="metric-assignments"></span>
</div>
</div>
<div class="panels-container">
<div class="panel">
<div class="panel-header">
<span class="panel-title">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
Input JSON (AllocationRequest)
</span>
<div class="panel-actions">
<button class="panel-btn" onclick="formatInput()">Format</button>
<button class="panel-btn" onclick="resetInput()">Reset</button>
</div>
</div>
<div class="panel-body">
<textarea id="input-json" class="json-textarea"
placeholder="Paste your AllocationRequest JSON here..."></textarea>
</div>
</div>
<div class="panel">
<div class="panel-header">
<span class="panel-title">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Output JSON (AllocationResponse)
</span>
<div class="panel-actions">
<button class="panel-btn" onclick="copyOutput()">Copy</button>
</div>
</div>
<div class="panel-body">
<textarea id="output-json" class="json-textarea readonly" readonly
placeholder="Response will appear here..."></textarea>
</div>
</div>
</div>
<!-- Supervision Link - Persistent after allocation -->
<div id="supervision-link-container"
style="display: none; margin: 20px; padding: 16px; background: linear-gradient(135deg, rgba(0, 212, 170, 0.15) 0%, rgba(0, 184, 148, 0.15) 100%); border: 1px solid #00d4aa; border-radius: 12px; text-align: center;">
<div style="display: flex; align-items: center; justify-content: center; gap: 16px; flex-wrap: wrap;">
<span style="color: #00d4aa; font-size: 14px;">
<strong>✓ Allocation Complete</strong> — Run ID: <code id="run-id-display"
style="background: rgba(0,0,0,0.3); padding: 2px 8px; border-radius: 4px;"></code>
</span>
<a id="supervision-link" href="#" target="_blank"
style="display: inline-flex; align-items: center; gap: 8px; padding: 10px 24px; background: linear-gradient(135deg, #00d4aa 0%, #00b894 100%); color: #0a0f1a; text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 14px; transition: all 0.2s ease;">
🎯 Open Live Supervision Dashboard
</a>
</div>
</div>
<div class="curl-section">
<div class="curl-header">
<span class="curl-title">📋 cURL Example</span>
<button class="panel-btn" onclick="copyCurl()">Copy cURL</button>
</div>
<div class="curl-code">
<pre id="curl-example">curl -X POST "http://localhost:8000/api/v1/allocate" \
-H "Content-Type: application/json" \
-d '{
"allocation_date": "2026-02-05",
"warehouse": {"lat": 12.9716, "lng": 77.5946},
"drivers": [
{"id": "driver_001", "name": "Raju", "vehicle_capacity_kg": 150, "preferred_language": "en"},
{"id": "driver_002", "name": "Priya", "vehicle_capacity_kg": 120, "preferred_language": "ta"}
],
"packages": [
{"id": "pkg_001", "weight_kg": 2.5, "fragility_level": 3, "address": "123 Main St", "latitude": 12.97, "longitude": 77.60, "priority": "NORMAL"},
{"id": "pkg_002", "weight_kg": 8.0, "fragility_level": 2, "address": "456 Oak Ave", "latitude": 12.98, "longitude": 77.61, "priority": "HIGH"}
]
}'</pre>
</div>
</div>
</main>
<script>
// Default allocation request with 10 packages and 3 drivers
const DEFAULT_REQUEST = {
allocation_date: new Date().toISOString().split('T')[0],
warehouse: { lat: 12.9716, lng: 77.5946 },
drivers: [
{ id: "driver_001", name: "Raju", vehicle_capacity_kg: 150, preferred_language: "en" },
{ id: "driver_002", name: "Priya", vehicle_capacity_kg: 120, preferred_language: "ta" },
{ id: "driver_003", name: "Kumar", vehicle_capacity_kg: 180, preferred_language: "en" },
{ id: "driver_004", name: "Lakshmi", vehicle_capacity_kg: 100, preferred_language: "hi" },
{ id: "driver_005", name: "Arjun", vehicle_capacity_kg: 160, preferred_language: "te" }
],
packages: [
{ id: "pkg_001", weight_kg: 2.5, fragility_level: 3, address: "4A, Ruby Apartment, 3rd Main Road, Koramangala", latitude: 12.9352, longitude: 77.6245, priority: "NORMAL" },
{ id: "pkg_002", weight_kg: 8.0, fragility_level: 2, address: "No. 12, 2nd Cross Street, Indiranagar", latitude: 12.9716, longitude: 77.6411, priority: "HIGH" },
{ id: "pkg_003", weight_kg: 1.5, fragility_level: 1, address: "45, Brigade Road, Ashok Nagar", latitude: 12.9719, longitude: 77.6074, priority: "NORMAL" },
{ id: "pkg_004", weight_kg: 5.0, fragility_level: 4, address: "78, MG Road, Near Trinity Metro", latitude: 12.9756, longitude: 77.6066, priority: "EXPRESS" },
{ id: "pkg_005", weight_kg: 3.2, fragility_level: 2, address: "23, Residency Road, Richmond Circle", latitude: 12.9682, longitude: 77.5973, priority: "NORMAL" },
{ id: "pkg_006", weight_kg: 6.8, fragility_level: 3, address: "101, Commercial Street, Shivaji Nagar", latitude: 12.9833, longitude: 77.6072, priority: "HIGH" },
{ id: "pkg_007", weight_kg: 2.1, fragility_level: 1, address: "55, Cunningham Road, Vasanth Nagar", latitude: 12.9927, longitude: 77.5855, priority: "NORMAL" },
{ id: "pkg_008", weight_kg: 4.5, fragility_level: 5, address: "88, Lavelle Road, Langford Town", latitude: 12.9644, longitude: 77.5957, priority: "EXPRESS" },
{ id: "pkg_009", weight_kg: 7.2, fragility_level: 2, address: "32, Vittal Mallya Road, Cubbon Park", latitude: 12.9738, longitude: 77.5956, priority: "NORMAL" },
{ id: "pkg_010", weight_kg: 1.8, fragility_level: 1, address: "67, Infantry Road, Shivaji Nagar", latitude: 12.9864, longitude: 77.5961, priority: "HIGH" }
]
};
// Initialize
document.getElementById('input-json').value = JSON.stringify(DEFAULT_REQUEST, null, 2);
function setStatus(status, text) {
const dot = document.getElementById('status-dot');
const textEl = document.getElementById('status-text');
dot.className = 'status-dot';
if (status === 'loading') dot.classList.add('loading');
if (status === 'error') dot.classList.add('error');
textEl.textContent = text;
}
function showAlert(type, message) {
const container = document.getElementById('alert-container');
container.innerHTML = `<div class="alert ${type}">${message}</div>`;
setTimeout(() => container.innerHTML = '', 5000);
}
function updateMetrics(data) {
const gf = data.global_fairness || {};
document.getElementById('metric-gini').textContent =
gf.gini_index !== undefined ? gf.gini_index.toFixed(3) : '—';
document.getElementById('metric-stddev').textContent =
gf.std_dev !== undefined ? gf.std_dev.toFixed(2) : '—';
document.getElementById('metric-avg').textContent =
gf.avg_workload !== undefined ? gf.avg_workload.toFixed(2) : '—';
document.getElementById('metric-assignments').textContent =
data.assignments ? data.assignments.length : '—';
// Remove pending class
document.querySelectorAll('.metric-value').forEach(el => el.classList.remove('pending'));
}
function resetMetrics() {
document.getElementById('metric-gini').textContent = '—';
document.getElementById('metric-stddev').textContent = '—';
document.getElementById('metric-avg').textContent = '—';
document.getElementById('metric-assignments').textContent = '—';
document.querySelectorAll('.metric-value').forEach(el => el.classList.add('pending'));
}
async function runAllocation() {
const inputEl = document.getElementById('input-json');
const outputEl = document.getElementById('output-json');
const runBtn = document.getElementById('run-btn');
// Parse input
let payload;
try {
payload = JSON.parse(inputEl.value);
} catch (e) {
showAlert('error', `Invalid JSON: ${e.message}`);
setStatus('error', 'JSON Parse Error');
return;
}
// Disable button and show loading
runBtn.disabled = true;
setStatus('loading', 'Processing...');
resetMetrics();
outputEl.value = 'Sending request...';
try {
const startTime = performance.now();
// Use LangGraph endpoint for real-time agent events
const response = await fetch('/api/v1/allocate/langgraph', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const duration = ((performance.now() - startTime) / 1000).toFixed(2);
const data = await response.json();
outputEl.value = JSON.stringify(data, null, 2);
if (response.ok) {
setStatus('success', `Completed in ${duration}s`);
updateMetrics(data);
// Show allocation_run_id and supervision link
const runId = data.allocation_run_id;
if (runId) {
const shortId = runId.substring(0, 8);
const supervisionUrl = `http://localhost:8090/?run_id=${encodeURIComponent(runId)}`;
// Show persistent supervision link container
const linkContainer = document.getElementById('supervision-link-container');
const runIdDisplay = document.getElementById('run-id-display');
const supervisionLink = document.getElementById('supervision-link');
runIdDisplay.textContent = shortId + '...';
supervisionLink.href = supervisionUrl;
linkContainer.style.display = 'block';
// Also show toast alert (will disappear)
showAlert('success', `Allocation completed in ${duration}s`);
} else {
showAlert('success', `Allocation completed successfully in ${duration}s`);
}
} else {
setStatus('error', `Error ${response.status}`);
showAlert('error', `API Error: ${data.detail || data.message || 'Unknown error'}`);
}
} catch (e) {
setStatus('error', 'Connection Error');
outputEl.value = JSON.stringify({ error: e.message }, null, 2);
showAlert('error', `Connection error: ${e.message}`);
} finally {
runBtn.disabled = false;
}
}
function formatInput() {
const el = document.getElementById('input-json');
try {
const parsed = JSON.parse(el.value);
el.value = JSON.stringify(parsed, null, 2);
} catch (e) {
showAlert('error', `Cannot format: ${e.message}`);
}
}
function resetInput() {
document.getElementById('input-json').value = JSON.stringify(DEFAULT_REQUEST, null, 2);
document.getElementById('output-json').value = '';
resetMetrics();
setStatus('success', 'Ready');
}
function copyOutput() {
const el = document.getElementById('output-json');
navigator.clipboard.writeText(el.value).then(() => {
showAlert('success', 'Output copied to clipboard');
});
}
function copyCurl() {
const el = document.getElementById('curl-example');
navigator.clipboard.writeText(el.textContent).then(() => {
showAlert('success', 'cURL command copied to clipboard');
});
}
</script>
</body>
</html>