Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport"content="width=device-width, initial-scale=1.0"> | |
<title>RAD-X Training Console</title> | |
<link href="https://api.mapbox.com/mapbox-gl-js/v3.4.0/mapbox-gl.css" rel="stylesheet" /> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
<script> | |
tailwind.config = { | |
theme: { | |
extend: { | |
colors: { | |
'radx-blue': '#00b3ff', | |
'radx-dark': '#0b0f14', | |
'radx-panel': 'rgba(15,20,25,0.92)', | |
'radx-highlight': '#7ee0ff', | |
'radx-danger': '#ff4d4d', | |
'radx-warning': '#ffa500', | |
'radx-success': '#00ffa3' | |
} | |
} | |
} | |
} | |
</script> | |
<style> | |
body { | |
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; | |
background-color: #000; | |
color: #e7f3ff; | |
overflow: hidden; | |
} | |
#map { | |
position: fixed; | |
top: 0; | |
left: 0; | |
right: 0; | |
bottom: 0; | |
z-index: 1; | |
} | |
.hud-panel { | |
background: rgba(15, 20, 25, 0.92); | |
border: 1px solid rgba(0, 179, 255, 0.3); | |
border-radius: 12px; | |
padding: 14px; | |
backdrop-filter: blur(8px); | |
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5); | |
} | |
.hotspot-marker { | |
width: 20px; | |
height: 20px; | |
background-color: #e00; | |
border-radius: 50%; | |
box-shadow: 0 0 10px #f00; | |
border: 2px solid #fff; | |
position: relative; | |
animation: pulse 2s infinite; | |
z-index: 10; | |
} | |
@keyframes pulse { | |
0% { transform: scale(1); opacity: 1; } | |
50% { transform: scale(1.2); opacity: 0.7; } | |
100% { transform: scale(1); opacity: 1; } | |
} | |
.pulse-circle { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
width: 20px; | |
height: 20px; | |
border-radius: 50%; | |
background: rgba(255, 0, 0, 0.5); | |
animation: pulseRing 2s infinite; | |
} | |
@keyframes pulseRing { | |
0% { transform: translate(-50%, -50%) scale(0.5); opacity: 0.8; } | |
100% { transform: translate(-50%, -50%) scale(2); opacity: 0; } | |
} | |
.training-panel { | |
position: fixed; | |
top: 20px; | |
left: 20px; | |
width: 400px; | |
max-height: calc(100vh - 40px); | |
z-index: 10; | |
background: rgba(15, 20, 25, 0.92); | |
border: 1px solid rgba(0, 179, 255, 0.3); | |
border-radius: 12px; | |
padding: 14px; | |
backdrop-filter: blur(10px); | |
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); | |
overflow-y: auto; | |
} | |
.json-editor { | |
width: 100%; | |
height: 200px; | |
background: rgba(0, 0, 0, 0.35); | |
color: #fff; | |
border: 1px solid rgba(0, 179, 255, 0.45); | |
border-radius: 8px; | |
padding: 10px; | |
font-family: monospace; | |
font-size: 12px; | |
resize: vertical; | |
} | |
.status-indicator { | |
display: inline-block; | |
width: 10px; | |
height: 10px; | |
border-radius: 50%; | |
margin-right: 6px; | |
} | |
.status-active { | |
background-color: #00ffa3; | |
box-shadow: 0 0 5px #00ffa3; | |
} | |
.status-warning { | |
background-color: #ffa500; | |
box-shadow: 0 0 5px #ffa500; | |
} | |
.status-critical { | |
background-color: #ff4d4d; | |
box-shadow: 0 0 5px #ff4d4d; | |
} | |
</style> | |
</head> | |
<body> | |
<!-- Map container --> | |
<div id="map"></div> | |
<!-- Training Panel --> | |
<div class="training-panel"> | |
<div class="flex justify-between items-center mb-4"> | |
<h2 class="text-xl font-bold text-radx-highlight">RAD-X TRAINING CONSOLE</h2> | |
<button id="close-training" class="text-radx-highlight hover:text-white"> | |
<i class="fas fa-times"></i> | |
</button> | |
</div> | |
<div class="mb-4"> | |
<h3 class="text-lg font-semibold text-radx-blue mb-2">DATABASE CONNECTION</h3> | |
<div class="flex items-center text-sm"> | |
<span class="status-indicator status-active"></span> | |
<span>NeonDB Connected</span> | |
</div> | |
<div class="text-xs text-gray-400 mt-1">Status: Live | Tables: 8 | Records: 1,247</div> | |
</div> | |
<div class="mb-4"> | |
<h3 class="text-lg font-semibold text-radx-blue mb-2">DATA INGESTION</h3> | |
<textarea id="json-input" class="json-editor" placeholder='[ | |
{ | |
"disease": "cholera", | |
"key": "sample_site", | |
"place": "Sample City", | |
"center": [36.80, -1.30], | |
"title": "Sample City — River Intake", | |
"info": "ROOT CAUSE: Demo import.\nTurbidity ~160 NTU; residual chlorine ~0.06 mg/L." | |
} | |
]'>[ | |
{ | |
"disease": "cholera", | |
"key": "lusaka_demo", | |
"place": "Lusaka", | |
"center": [28.2540, -15.4380], | |
"title": "Lusaka — Kanyama", | |
"info": "ROOT CAUSE: High water table + pit latrines → borehole infiltration." | |
}, | |
{ | |
"disease": "malaria", | |
"key": "kisumu_demo", | |
"place": "Kisumu", | |
"center": [34.7617, -0.0917], | |
"title": "Kisumu — Lakeside", | |
"info": "Vector proliferation near shoreline + wetlands (DEMO)." | |
} | |
]</textarea> | |
<div class="flex gap-2 mt-2"> | |
<button id="ingest-btn" class="flex-1 bg-radx-blue text-black py-2 px-4 rounded font-semibold hover:bg-cyan-300 transition"> | |
INGEST DATA | |
</button> | |
<button id="clear-btn" class="flex-1 bg-gray-700 text-white py-2 px-4 rounded hover:bg-gray-600 transition"> | |
CLEAR | |
</button> | |
</div> | |
</div> | |
<div class="mb-4"> | |
<h3 class="text-lg font-semibold text-radx-blue mb-2">TRAINING METRICS</h3> | |
<div class="grid grid-cols-2 gap-2 text-sm"> | |
<div class="bg-gray-800/50 p-2 rounded"> | |
<div class="text-gray-400">Index Size</div> | |
<div id="metric-index" class="text-radx-highlight">127</div> | |
</div> | |
<div class="bg-gray-800/50 p-2 rounded"> | |
<div class="text-gray-400">Samples</div> | |
<div id="metric-samples" class="text-radx-highlight">0</div> | |
</div> | |
<div class="bg-gray-800/50 p-2 rounded"> | |
<div class="text-gray-400">Last Ingest</div> | |
<div id="metric-last" class="text-radx-highlight">—</div> | |
</div> | |
<div class="bg-gray-800/50 p-2 rounded"> | |
<div class="text-gray-400">Status</div> | |
<div class="text-radx-success">Ready</div> | |
</div> | |
</div> | |
<div class="mt-2"> | |
<div class="text-gray-400 text-sm mb-1">Progress</div> | |
<div class="h-2 bg-gray-700 rounded-full overflow-hidden"> | |
<div id="progress-bar" class="h-full bg-radx-blue" style="width: 0%"></div> | |
</div> | |
</div> | |
</div> | |
<div> | |
<h3 class="text-lg font-semibold text-radx-blue mb-2">TRAINING COMMANDS</h3> | |
<div class="bg-gray-800/50 p-3 rounded text-sm"> | |
<div class="mb-2">• <span class="font-mono">train cholera</span> — open pipeline</div> | |
<div class="mb-2">• <span class="font-mono">deploy model</span> — pretend-deploy (demo)</div> | |
<div>• <span class="font-mono">list diseases</span> — show supported keys</div> | |
</div> | |
</div> | |
</div> | |
<!-- Mapbox GL JS --> | |
<script src="https://api.mapbox.com/mapbox-gl-js/v3.14.0/mapbox-gl.js"></script> | |
<script> | |
// Mapbox access token | |
mapboxgl.accessToken = 'pk.eyJ1IjoiZGVja3VzZXIiLCJhIjoiY20xcjF5d2JxMGQzcTJqcHg1c2U5aG90cyJ9.TFJ7V8BYkx15my8fX6BM5A'; | |
// Initialize map | |
const map = new mapboxgl.Map({ | |
container: 'map', | |
style: 'mapbox://styles/mapbox/satellite-streets-v12', | |
center: [20, 0], | |
zoom: 2, | |
pitch: 0, | |
bearing: 0 | |
}); | |
// Add terrain and fog | |
map.on('style.load', () => { | |
// Add DEM source and terrain | |
map.addSource('mapbox-dem', { | |
'type': 'raster-dem', | |
'url': 'mapbox://mapbox.mapbox-terrain-dem-v1', | |
'tileSize': 512 | |
}); | |
map.setTerrain({ 'source': 'mapbox-dem', 'exaggeration': 1.5 }); | |
// Add fog | |
map.setFog({ | |
'range': [0.8, 8], | |
'color': '#0b0f14', | |
'horizon-blend': 0.05 | |
}); | |
// Add intelligence data source | |
map.addSource('training_data', { | |
'type': 'geojson', | |
'data': { | |
"type": "FeatureCollection", | |
"features": [] | |
} | |
}); | |
// Add layers for training data | |
map.addLayer({ | |
'id': 'training-points', | |
'type': 'circle', | |
'source': 'training_data', | |
'paint': { | |
'circle-radius': 8, | |
'circle-color': [ | |
'match', | |
['get', 'disease'], | |
'cholera', '#ff0033', | |
'malaria', '#00ff88', | |
'dengue', '#bb66ff', | |
'#ffa500' | |
], | |
'circle-stroke-width': 2, | |
'circle-stroke-color': '#ffffff', | |
'circle-opacity': 0.8 | |
} | |
}); | |
// Add 3D buildings layer | |
map.addLayer({ | |
'id': '3d-buildings', | |
'source': 'composite', | |
'source-layer': 'building', | |
'filter': ['==', 'extrude', 'true'], | |
'type': 'fill-extrusion', | |
'minzoom': 15, | |
'paint': { | |
'fill-extrusion-color': '#aaa', | |
'fill-extrusion-height': [ | |
'interpolate', | |
['linear'], | |
['zoom'], | |
15, | |
0, | |
15.05, | |
['get', 'height'] | |
], | |
'fill-extrusion-base': [ | |
'interpolate', | |
['linear'], | |
['zoom'], | |
15, | |
0, | |
15.05, | |
['get', 'min_height'] | |
], | |
'fill-extrusion-opacity': 0.6 | |
} | |
}); | |
}); | |
// Training functionality | |
const jsonInput = document.getElementById('json-input'); | |
const ingestBtn = document.getElementById('ingest-btn'); | |
const clearBtn = document.getElementById('clear-btn'); | |
const closeBtn = document.getElementById('close-training'); | |
const metricIndex = document.getElementById('metric-index'); | |
const metricSamples = document.getElementById('metric-samples'); | |
const metricLast = document.getElementById('metric-last'); | |
const progressBar = document.getElementById('progress-bar'); | |
// Close training panel | |
closeBtn.addEventListener('click', () => { | |
window.location.href = 'index.html'; | |
}); | |
// Clear JSON input | |
clearBtn.addEventListener('click', () => { | |
jsonInput.value = '[]'; | |
}); | |
// Ingest data | |
ingestBtn.addEventListener('click', () => { | |
try { | |
const data = JSON.parse(jsonInput.value); | |
if (!Array.isArray(data)) { | |
throw new Error('Data must be an array'); | |
} | |
// Process data | |
processData(data); | |
} catch (error) { | |
alert('Invalid JSON: ' + error.message); | |
} | |
}); | |
// Process training data | |
function processData(data) { | |
const features = []; | |
let processed = 0; | |
const total = data.length; | |
// Reset metrics | |
metricSamples.textContent = '0'; | |
progressBar.style.width = '0%'; | |
// Process each item | |
data.forEach((item, index) => { | |
setTimeout(() => { | |
// Create GeoJSON feature | |
const feature = { | |
type: 'Feature', | |
geometry: { | |
type: 'Point', | |
coordinates: item.center | |
}, | |
properties: { | |
disease: item.disease, | |
title: item.title, | |
info: item.info, | |
place: item.place, | |
key: item.key | |
} | |
}; | |
features.push(feature); | |
// Add marker to map | |
const el = document.createElement('div'); | |
el.className = 'hotspot-marker'; | |
el.innerHTML = '<div class="pulse-circle"></div>'; | |
new mapboxgl.Marker(el) | |
.setLngLat(item.center) | |
.setPopup( | |
new mapboxgl.Popup({ offset: 25 }) | |
.setHTML( | |
`<h3>${item.title}</h3><p>${item.info}</p>` | |
) | |
) | |
.addTo(map); | |
// Update metrics | |
processed++; | |
metricSamples.textContent = processed; | |
progressBar.style.width = `${(processed / total) * 100}%`; | |
// Update last ingest time | |
if (processed === total) { | |
metricLast.textContent = new Date().toLocaleTimeString(); | |
metricIndex.textContent = parseInt(metricIndex.textContent) + total; | |
// Update map source | |
map.getSource('training_data').setData({ | |
type: 'FeatureCollection', | |
features: features | |
}); | |
// Fly to first point | |
if (features.length > 0) { | |
const firstPoint = features[0].geometry.coordinates; | |
map.flyTo({ | |
center: firstPoint, | |
zoom: 12, | |
pitch: 45, | |
bearing: 0, | |
duration: 3000 | |
}); | |
} | |
} | |
}, index * 500); // Simulate processing delay | |
}); | |
} | |
// Initialize with sample data | |
window.addEventListener('load', () => { | |
// Fly to Africa | |
map.flyTo({ | |
center: [20, 0], | |
zoom: 2, | |
pitch: 0, | |
bearing: 0, | |
duration: 0 | |
}); | |
}); | |
</script> | |
</body> | |
</html> |