feat: Wire LiveDemo to real backend API + add API_URL env config for Vercel
#3
by MouleeswaranM - opened
landing/src/components/LiveDemo.tsx
CHANGED
|
@@ -1,17 +1,31 @@
|
|
| 1 |
import { useState, useRef } from 'react'
|
| 2 |
import './LiveDemo.css'
|
| 3 |
|
|
|
|
|
|
|
|
|
|
| 4 |
const DEMO_DRIVERS = [
|
| 5 |
-
{ id: 'drv_001', name: 'Rajesh Kumar', hours_today: 4.5, hours_since_rest: 2.1, is_ill: false, gender: 'M' },
|
| 6 |
-
{ id: 'drv_002', name: 'Priya Sharma', hours_today: 8.1, hours_since_rest: 0.5, is_ill: false, gender: 'F' },
|
| 7 |
-
{ id: 'drv_003', name: 'Amit Patel', hours_today: 3.2, hours_since_rest: 3.0, is_ill: false, gender: 'M' },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
]
|
|
|
|
| 9 |
const DEMO_ROUTES = [
|
| 10 |
{ id: 'rt_mumbai_pune', distance_km: 148, difficulty: 'medium', is_night_route: false },
|
| 11 |
{ id: 'rt_pune_nashik', distance_km: 215, difficulty: 'hard', is_night_route: true },
|
| 12 |
{ id: 'rt_nashik_aurangabad', distance_km: 98, difficulty: 'easy', is_night_route: false },
|
| 13 |
]
|
| 14 |
|
|
|
|
| 15 |
const MOCK_RESULT = {
|
| 16 |
success: true,
|
| 17 |
data: {
|
|
@@ -27,20 +41,108 @@ const MOCK_RESULT = {
|
|
| 27 |
carbon_kg: 87.4,
|
| 28 |
latency_ms: 312,
|
| 29 |
explanation: 'Priya has worked 8.1h today — assigned shorter route. Night route routed to Amit (highest wellness). Gini = 0.12 — excellent fairness.',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
}
|
| 31 |
}
|
| 32 |
|
| 33 |
export default function LiveDemo() {
|
| 34 |
const [status, setStatus] = useState<'idle' | 'loading' | 'done'>('idle')
|
| 35 |
-
const [result, setResult] = useState<
|
|
|
|
| 36 |
const resultRef = useRef<HTMLDivElement>(null)
|
| 37 |
|
| 38 |
const runDemo = async () => {
|
| 39 |
setStatus('loading')
|
| 40 |
setResult(null)
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
setStatus('done')
|
| 43 |
-
setResult(MOCK_RESULT)
|
| 44 |
setTimeout(() => resultRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }), 100)
|
| 45 |
}
|
| 46 |
|
|
@@ -102,7 +204,7 @@ export default function LiveDemo() {
|
|
| 102 |
{status === 'loading' ? (
|
| 103 |
<>
|
| 104 |
<span className="demo__spinner" />
|
| 105 |
-
|
| 106 |
</>
|
| 107 |
) : status === 'done' ? (
|
| 108 |
<>✓ Run again</>
|
|
@@ -112,6 +214,11 @@ export default function LiveDemo() {
|
|
| 112 |
</>
|
| 113 |
)}
|
| 114 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
</div>
|
| 116 |
|
| 117 |
{status === 'done' && result && (
|
|
|
|
| 1 |
import { useState, useRef } from 'react'
|
| 2 |
import './LiveDemo.css'
|
| 3 |
|
| 4 |
+
// API URL - set via VITE_API_URL env var (points to Render backend)
|
| 5 |
+
const API_URL = import.meta.env.VITE_API_URL || 'https://fairrelay-brain.onrender.com'
|
| 6 |
+
|
| 7 |
const DEMO_DRIVERS = [
|
| 8 |
+
{ id: 'drv_001', name: 'Rajesh Kumar', hours_today: 4.5, hours_since_rest: 2.1, is_ill: false, gender: 'M', vehicle_capacity_kg: 500, preferred_language: 'hi' },
|
| 9 |
+
{ id: 'drv_002', name: 'Priya Sharma', hours_today: 8.1, hours_since_rest: 0.5, is_ill: false, gender: 'F', vehicle_capacity_kg: 400, preferred_language: 'hi' },
|
| 10 |
+
{ id: 'drv_003', name: 'Amit Patel', hours_today: 3.2, hours_since_rest: 3.0, is_ill: false, gender: 'M', vehicle_capacity_kg: 600, preferred_language: 'en' },
|
| 11 |
+
]
|
| 12 |
+
|
| 13 |
+
const DEMO_PACKAGES = [
|
| 14 |
+
{ id: 'pkg_001', weight_kg: 12.5, fragility_level: 2, address: 'Andheri West, Mumbai', latitude: 19.1364, longitude: 72.8296, priority: 'normal' },
|
| 15 |
+
{ id: 'pkg_002', weight_kg: 8.0, fragility_level: 1, address: 'Bandra East, Mumbai', latitude: 19.0596, longitude: 72.8495, priority: 'high' },
|
| 16 |
+
{ id: 'pkg_003', weight_kg: 22.0, fragility_level: 3, address: 'Powai, Mumbai', latitude: 19.1176, longitude: 72.9060, priority: 'normal' },
|
| 17 |
+
{ id: 'pkg_004', weight_kg: 5.5, fragility_level: 1, address: 'Goregaon East, Mumbai', latitude: 19.1663, longitude: 72.8526, priority: 'express' },
|
| 18 |
+
{ id: 'pkg_005', weight_kg: 15.0, fragility_level: 2, address: 'Juhu, Mumbai', latitude: 19.0883, longitude: 72.8264, priority: 'normal' },
|
| 19 |
+
{ id: 'pkg_006', weight_kg: 9.8, fragility_level: 1, address: 'Malad West, Mumbai', latitude: 19.1874, longitude: 72.8484, priority: 'normal' },
|
| 20 |
]
|
| 21 |
+
|
| 22 |
const DEMO_ROUTES = [
|
| 23 |
{ id: 'rt_mumbai_pune', distance_km: 148, difficulty: 'medium', is_night_route: false },
|
| 24 |
{ id: 'rt_pune_nashik', distance_km: 215, difficulty: 'hard', is_night_route: true },
|
| 25 |
{ id: 'rt_nashik_aurangabad', distance_km: 98, difficulty: 'easy', is_night_route: false },
|
| 26 |
]
|
| 27 |
|
| 28 |
+
// Fallback mock result for when backend is unavailable
|
| 29 |
const MOCK_RESULT = {
|
| 30 |
success: true,
|
| 31 |
data: {
|
|
|
|
| 41 |
carbon_kg: 87.4,
|
| 42 |
latency_ms: 312,
|
| 43 |
explanation: 'Priya has worked 8.1h today — assigned shorter route. Night route routed to Amit (highest wellness). Gini = 0.12 — excellent fairness.',
|
| 44 |
+
source: 'mock',
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
interface DemoResult {
|
| 49 |
+
success: boolean
|
| 50 |
+
data: {
|
| 51 |
+
allocations: Array<{
|
| 52 |
+
driver: string
|
| 53 |
+
driver_name: string
|
| 54 |
+
route: string
|
| 55 |
+
route_label: string
|
| 56 |
+
wellness_score: number
|
| 57 |
+
}>
|
| 58 |
+
}
|
| 59 |
+
meta: {
|
| 60 |
+
gini_index: number
|
| 61 |
+
fairness_grade: string
|
| 62 |
+
carbon_kg: number
|
| 63 |
+
latency_ms: number
|
| 64 |
+
explanation: string
|
| 65 |
+
source?: string
|
| 66 |
}
|
| 67 |
}
|
| 68 |
|
| 69 |
export default function LiveDemo() {
|
| 70 |
const [status, setStatus] = useState<'idle' | 'loading' | 'done'>('idle')
|
| 71 |
+
const [result, setResult] = useState<DemoResult | null>(null)
|
| 72 |
+
const [apiSource, setApiSource] = useState<'live' | 'fallback'>('live')
|
| 73 |
const resultRef = useRef<HTMLDivElement>(null)
|
| 74 |
|
| 75 |
const runDemo = async () => {
|
| 76 |
setStatus('loading')
|
| 77 |
setResult(null)
|
| 78 |
+
|
| 79 |
+
const startTime = Date.now()
|
| 80 |
+
|
| 81 |
+
try {
|
| 82 |
+
// Try calling real backend API
|
| 83 |
+
const response = await fetch(`${API_URL}/api/v1/allocate`, {
|
| 84 |
+
method: 'POST',
|
| 85 |
+
headers: { 'Content-Type': 'application/json' },
|
| 86 |
+
body: JSON.stringify({
|
| 87 |
+
drivers: DEMO_DRIVERS.map(d => ({
|
| 88 |
+
id: d.id,
|
| 89 |
+
name: d.name,
|
| 90 |
+
vehicle_capacity_kg: d.vehicle_capacity_kg,
|
| 91 |
+
preferred_language: d.preferred_language,
|
| 92 |
+
})),
|
| 93 |
+
packages: DEMO_PACKAGES,
|
| 94 |
+
warehouse: { lat: 19.0760, lng: 72.8777 },
|
| 95 |
+
allocation_date: new Date().toISOString().split('T')[0],
|
| 96 |
+
}),
|
| 97 |
+
signal: AbortSignal.timeout(15000), // 15s timeout
|
| 98 |
+
})
|
| 99 |
+
|
| 100 |
+
if (response.ok) {
|
| 101 |
+
const data = await response.json()
|
| 102 |
+
const latency = Date.now() - startTime
|
| 103 |
+
|
| 104 |
+
// Transform real API response into demo format
|
| 105 |
+
const allocations = (data.assignments || []).map((a: any) => ({
|
| 106 |
+
driver: a.driver_external_id || a.driver_id,
|
| 107 |
+
driver_name: a.driver_name || a.driver_external_id,
|
| 108 |
+
route: a.route_id,
|
| 109 |
+
route_label: `Route ${a.route_summary?.num_stops || '?'} stops · ${a.route_summary?.total_weight_kg?.toFixed(1) || '?'}kg · ${a.route_summary?.estimated_time_minutes?.toFixed(0) || '?'}min`,
|
| 110 |
+
wellness_score: Math.round((a.fairness_score || 0.8) * 100),
|
| 111 |
+
}))
|
| 112 |
+
|
| 113 |
+
setResult({
|
| 114 |
+
success: true,
|
| 115 |
+
data: { allocations },
|
| 116 |
+
meta: {
|
| 117 |
+
gini_index: data.global_fairness?.gini_index ?? 0.15,
|
| 118 |
+
fairness_grade: (data.global_fairness?.gini_index ?? 0.15) < 0.2 ? 'A' : (data.global_fairness?.gini_index ?? 0.3) < 0.35 ? 'B' : 'C',
|
| 119 |
+
carbon_kg: allocations.length * 28.5,
|
| 120 |
+
latency_ms: latency,
|
| 121 |
+
explanation: `Real-time allocation via FairRelay API. ${allocations.length} drivers assigned with Gini = ${(data.global_fairness?.gini_index ?? 0.15).toFixed(2)}. Fairness-optimized in ${latency}ms.`,
|
| 122 |
+
source: 'live',
|
| 123 |
+
}
|
| 124 |
+
})
|
| 125 |
+
setApiSource('live')
|
| 126 |
+
} else {
|
| 127 |
+
throw new Error(`API returned ${response.status}`)
|
| 128 |
+
}
|
| 129 |
+
} catch (err) {
|
| 130 |
+
// Fallback to mock with realistic delay
|
| 131 |
+
console.warn('[LiveDemo] Backend unavailable, using fallback:', err)
|
| 132 |
+
await new Promise(r => setTimeout(r, 800))
|
| 133 |
+
|
| 134 |
+
setResult({
|
| 135 |
+
...MOCK_RESULT,
|
| 136 |
+
meta: {
|
| 137 |
+
...MOCK_RESULT.meta,
|
| 138 |
+
latency_ms: Date.now() - startTime,
|
| 139 |
+
source: 'fallback',
|
| 140 |
+
}
|
| 141 |
+
})
|
| 142 |
+
setApiSource('fallback')
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
setStatus('done')
|
|
|
|
| 146 |
setTimeout(() => resultRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }), 100)
|
| 147 |
}
|
| 148 |
|
|
|
|
| 204 |
{status === 'loading' ? (
|
| 205 |
<>
|
| 206 |
<span className="demo__spinner" />
|
| 207 |
+
Calling FairRelay API…
|
| 208 |
</>
|
| 209 |
) : status === 'done' ? (
|
| 210 |
<>✓ Run again</>
|
|
|
|
| 214 |
</>
|
| 215 |
)}
|
| 216 |
</button>
|
| 217 |
+
{status === 'done' && (
|
| 218 |
+
<div style={{ marginTop: '0.5rem', fontSize: '0.7rem', color: apiSource === 'live' ? '#10b981' : '#f59e0b', textAlign: 'center' }}>
|
| 219 |
+
{apiSource === 'live' ? '● Connected to live API' : '● Using demo fallback (backend warming up)'}
|
| 220 |
+
</div>
|
| 221 |
+
)}
|
| 222 |
</div>
|
| 223 |
|
| 224 |
{status === 'done' && result && (
|