new-dynamo / index.html
MoShow's picture
fix app and all issues - Follow Up Deployment
25a1817 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RAD-X v10.0 • Fused Intelligence Briefing</title>
<!-- Mapbox CSS -->
<link href="https://api.mapbox.com/mapbox-gl-js/v3.4.0/mapbox-gl.css" rel="stylesheet" />
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome for icons -->
<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>
:root {
--ticker-height: 54px;
}
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: var(--ticker-height);
z-index: 1;
}
#chat-panel {
position: fixed;
top: 16px;
bottom: calc(var(--ticker-height) + 16px);
right: 16px;
width: 380px;
display: flex;
flex-direction: column;
z-index: 80;
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);
transition: transform 0.35s ease, opacity 0.25s ease;
}
#chat-panel.minimized {
transform: translateX(calc(100% + 16px));
}
#chat-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 8px;
}
#chat-header h2 {
margin: 0;
font-size: 16px;
color: #7ee0ff;
letter-spacing: 0.04em;
}
.chat-btn {
cursor: pointer;
border: 1px solid rgba(0, 179, 255, 0.35);
background: transparent;
color: #9ee7ff;
font-size: 12px;
border-radius: 999px;
padding: 6px 10px;
}
#chat-log {
flex: 1;
overflow-y: auto;
padding-right: 6px;
scrollbar-width: thin;
scrollbar-color: rgba(0, 179, 255, 0.3) transparent;
}
#chat-log::-webkit-scrollbar {
width: 6px;
}
#chat-log::-webkit-scrollbar-track {
background: transparent;
}
#chat-log::-webkit-scrollbar-thumb {
background-color: rgba(0, 179, 255, 0.3);
border-radius: 3px;
}
.msg {
font-size: 13px;
line-height: 1.5;
margin: 8px 0;
color: #ced8e4;
}
.msg.user {
color: #7ee0ff;
}
.msg.sys {
color: #a3e635;
}
#chat-form {
display: flex;
gap: 8px;
margin-top: 8px;
}
#chat-input {
flex: 1;
padding: 0.7rem 0.9rem;
color: #fff;
background: rgba(0, 0, 0, 0.35);
border: 1px solid rgba(0, 179, 255, 0.45);
border-radius: 8px;
outline: none;
transition: border 0.2s, box-shadow 0.2s;
}
#chat-input:focus {
border-color: #00b3ff;
box-shadow: 0 0 8px rgba(0, 179, 255, 0.3);
}
#chat-send {
padding: 0.7rem 0.9rem;
color: #001018;
background: #7ee0ff;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
}
#story-panel {
position: fixed;
top: 16px;
left: 16px;
width: 420px;
max-height: calc(100vh - var(--ticker-height) - 32px);
overflow-y: auto;
z-index: 70;
background: #0b0f14;
border: 1px solid rgba(0, 179, 255, 0.18);
border-radius: 12px;
transition: transform 0.35s ease, opacity 0.25s ease;
opacity: 0;
transform: translateX(-450px);
pointer-events: none;
}
#story-panel.visible {
opacity: 1;
transform: translateX(0);
pointer-events: all;
}
#story-header {
position: sticky;
top: 0;
background: #0b0f14;
z-index: 1;
padding: 12px 16px;
border-bottom: 1px solid rgba(0, 179, 255, 0.12);
display: flex;
justify-content: space-between;
align-items: center;
}
#story-header h3 {
margin: 0;
color: #7ee0ff;
font-size: 14px;
letter-spacing: 0.08em;
text-transform: uppercase;
}
#story-close {
cursor: pointer;
background: transparent;
border: 1px solid rgba(0, 179, 255, 0.35);
color: #9ee7ff;
padding: 6px 10px;
border-radius: 999px;
font-size: 12px;
}
.chapter {
padding: 20px 22px;
border-bottom: 1px solid rgba(0, 179, 255, 0.12);
opacity: 0.3;
transition: opacity 0.25s ease;
}
.chapter.active {
opacity: 1;
}
.chapter h4 {
margin: 0 0 6px 0;
color: #f9b4b4;
letter-spacing: 0.03em;
}
.chapter p {
margin: 0;
color: #c6d3e0;
font-size: 13px;
line-height: 1.5;
}
.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);
}
#ticker {
position: fixed;
left: 0;
right: 0;
bottom: 0;
height: var(--ticker-height);
display: flex;
align-items: center;
gap: 12px;
background: rgba(10, 12, 16, 0.96);
border-top: 1px solid rgba(0, 179, 255, 0.25);
z-index: 50;
overflow: hidden;
pointer-events: none;
}
#ticker .label {
color: #00b3ff;
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
padding: 0 14px;
border-right: 1px solid rgba(0, 179, 255, 0.2);
}
#ticker .marquee {
position: relative;
white-space: nowrap;
will-change: transform;
}
.tick-item {
display: inline-flex;
gap: 8px;
align-items: center;
margin-right: 36px;
color: #c9d6df;
font-size: 13px;
}
.badge {
font-size: 11px;
padding: 2px 6px;
border-radius: 6px;
background: rgba(0, 179, 255, 0.12);
border: 1px solid rgba(0, 179, 255, 0.35);
color: #9ee7ff;
}
#train-panel {
position: fixed;
inset: 0;
z-index: 90;
display: none;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.66);
backdrop-filter: blur(4px);
}
#train-card {
width: min(900px, 92vw);
max-height: 88vh;
overflow: hidden;
background: rgba(15, 20, 25, 0.97);
border: 1px solid rgba(0, 179, 255, 0.3);
border-radius: 14px;
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.6);
display: flex;
flex-direction: column;
}
#tooltip {
position: fixed;
transform: translate(-50%, -110%);
pointer-events: none;
display: none;
z-index: 60;
min-width: 320px;
}
#tooltip-title {
color: #ff9999;
font-weight: 700;
font-size: 14px;
letter-spacing: 0.08em;
text-transform: uppercase;
}
#tooltip-info {
font-size: 13px;
margin-top: 6px;
color: #cfe2f0;
white-space: pre-line;
}
#tooltip-coords {
margin-top: 8px;
padding-top: 6px;
border-top: 1px solid rgba(0, 179, 255, 0.15);
font-size: 12px;
color: #89a;
}
#tooltip .row {
margin-top: 8px;
}
#tooltip .pill {
display: inline-block;
padding: 2px 6px;
border: 1px solid rgba(0, 179, 255, 0.35);
border-radius: 999px;
font-size: 11px;
color: #9ee7ff;
margin-right: 6px;
margin-top: 4px;
}
#tooltip .kv {
font-size: 12px;
color: #cfe2f0;
}
#tooltip .meter-s {
height: 6px;
background: #06202a;
border-radius: 999px;
overflow: hidden;
margin-top: 6px;
}
#tooltip .meter-s > span {
display: block;
height: 100%;
background: linear-gradient(90deg, #ff4d4d, #ffa500, #00ffa3);
width: 0;
}
.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;
}
.map-overlay {
position: absolute;
top: 20px;
left: 20px;
z-index: 20;
background: rgba(15, 20, 25, 0.8);
border: 1px solid rgba(0, 179, 255, 0.3);
border-radius: 8px;
padding: 12px;
color: white;
}
.legend-item {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.legend-color {
width: 16px;
height: 16px;
border-radius: 50%;
margin-right: 8px;
}
.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; }
}
</style>
</head>
<body>
<!-- Map container -->
<div id="map"></div>
<!-- Map overlay legend -->
<div class="map-overlay hud-panel">
<h3 class="text-sm font-bold text-radx-highlight mb-2">INTELLIGENCE OVERLAY</h3>
<div class="legend-item">
<div class="legend-color bg-radx-danger"></div>
<span class="text-xs">Contamination Source</span>
</div>
<div class="legend-item">
<div class="legend-color bg-radx-warning"></div>
<span class="text-xs">Infrastructure Weakness</span>
</div>
<div class="legend-item">
<div class="legend-color bg-yellow-300"></div>
<span class="text-xs">Informal Vendor Hub</span>
</div>
<div class="legend-item">
<div class="legend-color bg-radx-danger" style="opacity: 0.3;"></div>
<span class="text-xs">High-Risk Zone</span>
</div>
</div>
<!-- Story panel -->
<div id="story-panel">
<div id="story-header">
<h3>CHOLERA BRIEFING - ROOT CAUSE</h3>
<button id="story-close" class="story-close-btn">
<i class="fas fa-times"></i>
</button>
</div>
<div id="story-body">
<div class="chapter active">
<h4>Nairobi Cholera Outbreak Analysis</h4>
<p>This briefing examines the primary factors contributing to the cholera outbreak in Nairobi, focusing on contamination sources, infrastructure vulnerabilities, and high-risk zones.</p>
</div>
<div class="chapter">
<h4>Mathare River Junction</h4>
<p>A leaky sewer-water junction in Mathare Valley has been identified as a high-probability contamination source. Heavy rainfall during recent months has exacerbated this issue.</p>
</div>
<div class="chapter">
<h4>Aged Pipeline Corridor</h4>
<p>A structurally compromised pipeline segment crossing untreated drainage poses a risk of infiltration in the informal settlements near Kibera.</p>
</div>
<div class="chapter">
<h4>Kibera Borehole Cluster</h4>
<p>A high-density vendor area with decentralized quality control systems has been identified as vulnerable to single-source outbreaks.</p>
</div>
<div class="chapter">
<h4>Mathare-Mukuru Corridor</h4>
<p>Persistent infrastructure gaps in this area create a permanent high-risk environment for waterborne diseases.</p>
</div>
</div>
</div>
<!-- Chat panel -->
<div id="chat-panel">
<div id="chat-header">
<h2>RAD-X <span class="text-xs">v10.0</span></h2>
<button class="chat-btn" id="chat-toggle">
<i class="fas fa-minus"></i>
</button>
</div>
<div id="chat-log">
<div class="msg sys">RAD-X Matrix initialized. Ready for intelligence briefing.</div>
<div class="msg sys">Issue your target query. I will animate, explain, and simulate in 3D.</div>
<div class="msg user">> Trace cholera origin in Kibera, Nairobi</div>
<div class="msg sys">Analyzing cholera outbreak patterns in Kibera zone. Primary contamination sources identified...</div>
</div>
<form id="chat-form">
<input type="text" id="chat-input" placeholder="Enter query...">
<button type="submit" id="chat-send">
<i class="fas fa-paper-plane"></i>
</button>
</form>
</div>
<!-- Ticker -->
<div id="ticker">
<div class="label">INTELLIGENCE FEED</div>
<div class="marquee">
<div class="tick-item">
<span class="badge">CHOLERA</span>
<span>Kibera: 142 cases reported in last 24h</span>
</div>
<div class="tick-item">
<span class="badge">OUTBREAK</span>
<span>Mathare: Contamination source confirmed</span>
</div>
<div class="tick-item">
<span class="badge">INFRASTRUCTURE</span>
<span>Pipeline repair in progress near Kibera</span>
</div>
<div class="tick-item">
<span class="badge">PREVENTIVE</span>
<span>Water treatment units deployed in Mukuru</span>
</div>
</div>
</div>
<!-- Tooltip -->
<div id="tooltip" class="hud-panel">
<div id="tooltip-title">Mathare River Junction</div>
<div id="tooltip-info">Leaky sewer-water junction identified as a high-probability contamination source.</div>
<div id="tooltip-coords">36.8565, -1.2820</div>
<div class="row">
<div class="kv">Category: <span class="text-radx-highlight">Contamination Source</span></div>
</div>
<div class="row">
<div class="kv">Severity:</div>
<div class="meter-s">
<span style="width: 90%"></span>
</div>
</div>
<div class="row">
<div class="kv">Confidence:</div>
<div class="meter-s">
<span style="width: 85%"></span>
</div>
</div>
<div class="row">
<div class="kv">Contributors:</div>
<span class="pill">sewer leak</span>
<span class="pill">heavy rainfall</span>
</div>
</div>
<!-- Mapbox GL JS -->
<script src="https://api.mapbox.com/mapbox-gl-js/v3.14.0/mapbox-gl.js"></script>
<!-- Turf.js for geospatial calculations -->
<script src='https://npmcdn.com/@turf/turf/turf.min.js'></script>
<!-- Speech Synthesis -->
<script>
// Speech synthesis implementation
const speechSynthesis = {
supported: typeof window !== "undefined" && "speechSynthesis" in window,
voices: [],
selectedVoice: null,
isSpeaking: false,
populateVoiceList() {
if (!this.supported) return;
const availableVoices = window.speechSynthesis.getVoices();
if (availableVoices.length > 0) {
this.voices = availableVoices;
// Voice selection logic
const voicePriority = [
(v) => v.lang === "en-ZA" && /male/i.test(v.name),
(v) => v.lang === "en-NG" && /male/i.test(v.name),
(v) => v.lang.startsWith("en-") && /male/i.test(v.name),
(v) => /male/i.test(v.name) && v.default,
(v) => v.default,
];
let foundVoice = null;
for (const check of voicePriority) {
const voice = availableVoices.find(check);
if (voice) {
foundVoice = voice;
break;
}
}
this.selectedVoice = foundVoice || availableVoices[0] || null;
}
},
speak(text) {
if (!this.supported || !text || !this.selectedVoice) return;
if (window.speechSynthesis.speaking) {
window.speechSynthesis.cancel();
}
const utterance = new SpeechSynthesisUtterance(text);
utterance.voice = this.selectedVoice;
utterance.pitch = 0.9;
utterance.rate = 1.05;
utterance.onstart = () => this.isSpeaking = true;
utterance.onend = () => this.isSpeaking = false;
utterance.onerror = (e) => {
console.error("Speech synthesis error:", e);
this.isSpeaking = false;
};
window.speechSynthesis.speak(utterance);
},
cancel() {
if (!this.supported) return;
window.speechSynthesis.cancel();
this.isSpeaking = false;
}
};
// Initialize speech synthesis
if (speechSynthesis.supported) {
speechSynthesis.populateVoiceList();
if (window.speechSynthesis.onvoiceschanged !== undefined) {
window.speechSynthesis.onvoiceschanged = speechSynthesis.populateVoiceList;
}
}
</script>
<script>
// Mapbox access token - use your own token here
mapboxgl.accessToken = 'pk.eyJ1IjoiYWthbmltbzEiLCJhIjoiY2x4czNxbjU2MWM2eTJqc2gwNGIwaWhkMSJ9.jSwZdyaPa1dOHepNU5P71g';
// Initialize map
const map = new mapboxgl.Map({
container: 'map',
antialias: true,
attributionControl: false,
style: 'mapbox://styles/mapbox/satellite-streets-v12',
center: [18.4, 8.8], // Africa view
zoom: 2.5,
pitch: 0,
bearing: 0,
antialias: true
});
// Ensure map container has proper dimensions
document.getElementById('map').style.width = '100%';
document.getElementById('map').style.height = '100vh';
// Add terrain and fog
map.on('load', () => {
// Add terrain
map.addSource('mapbox-dem', {
type: 'raster-dem',
url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
tileSize: 512,
maxzoom: 14
});
map.setTerrain({ 'source': 'mapbox-dem', 'exaggeration': 1.6 });
// Add fog for better 3D effect
map.setFog({
'range': [0.5, 10],
'color': '#0b0f14',
'horizon-blend': 0.03
});
// Add sky layer for realistic atmosphere
map.addLayer({
'id': 'sky',
'type': 'sky',
'paint': {
'sky-type': 'atmosphere',
'sky-atmosphere-sun': [0.0, 0.0],
'sky-atmosphere-sun-intensity': 15
}
});
// Initialize live data sources
const diseaseDataSources = {
cholera: 'https://api.example.com/feed/cholera',
mpox: 'https://api.example.com/feed/mpox',
malaria: 'https://api.example.com/feed/malaria',
dengue: 'https://api.example.com/feed/dengue',
lassa: 'https://api.example.com/feed/lassa'
};
// Fetch and update disease data
async function fetchDiseaseData(disease) {
try {
const response = await fetch(diseaseDataSources[disease]);
const data = await response.json();
return data;
} catch (error) {
console.error(`Error fetching ${disease} data:`, error);
return null;
}
}
// Add live intelligence data source
map.addSource('live_disease_data', {
'type': 'geojson',
'data': { type: 'FeatureCollection', features: [] }
});
=======
{
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [
[36.8400, -1.3120],
[36.8445, -1.3100],
[36.8490, -1.3080]
]
},
"properties": {
"id": "I1",
"category": "Infrastructure Weakness",
"title": "Aged Pipeline Corridor",
"description": "Structurally compromised pipeline segment crossing untreated drainage.",
"severity": 0.6,
"confidence": 0.9
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [36.8260, -1.3175]
},
"properties": {
"id": "V1",
"category": "Informal Vendor Hub",
"title": "Kibera Borehole Cluster",
"description": "High-density vendor area with decentralized quality control.",
"severity": 0.7,
"confidence": 0.8
}
},
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[
[36.8520, -1.2600],
[36.8580, -1.2830],
[36.8600, -1.3000],
[36.8550, -1.3170],
[36.8300, -1.3200],
[36.8350, -1.2800],
[36.8450, -1.2650],
[36.8520, -1.2600]
]]
},
"properties": {
"id": "HN2",
"category": "Zone of Historical Neglect",
"title": "Mathare-Mukuru Corridor",
"description": "Persistent infrastructure gaps create a permanent high-risk environment.",
"severity": 0.8,
"confidence": 0.95
}
}
]
};
map.addSource('radx_data', {
'type': 'geojson',
'data': intelligenceData
});
// Add layers
map.addLayer({
'id': 'zones-fill',
'type': 'fill',
'source': 'radx_data',
'filter': ['==', ['geometry-type'], 'Polygon'],
'paint': {
'fill-color': '#ff4d4d',
'fill-opacity': 0.15
}
});
map.addLayer({
'id': 'zones-outline',
'type': 'line',
'source': 'radx_data',
'filter': ['==', ['geometry-type'], 'Polygon'],
'paint': {
'line-color': '#ff4d4d',
'line-width': 1,
'line-dasharray': [2, 1]
}
});
map.addLayer({
'id': 'infra-lines',
'type': 'line',
'source': 'radx_data',
'filter': ['==', ['geometry-type'], 'LineString'],
'paint': {
'line-color': '#ffa500',
'line-width': 2.5,
'line-dasharray': [1, 1.5]
}
});
map.addLayer({
'id': 'cause-points',
'type': 'circle',
'source': 'radx_data',
'filter': ['==', ['geometry-type'], 'Point'],
'paint': {
'circle-radius': 7,
'circle-stroke-width': 1.5,
'circle-stroke-color': '#ffffff',
'circle-color': [
'match',
['get', 'category'],
'Contamination Source', '#ff4d4d',
'Informal Vendor Hub', '#ffff66',
'#cccccc'
]
}
});
// Add individual layers for each feature to enable highlighting
intelligenceData.features.forEach(feature => {
const id = feature.properties.id;
if (feature.geometry.type === 'Point') {
map.addLayer({
'id': `cause-points-${id}`,
'type': 'circle',
'source': 'radx_data',
'filter': ['==', ['get', 'id'], id],
'paint': {
'circle-radius': 7,
'circle-stroke-width': 1.5,
'circle-stroke-color': '#ffffff',
'circle-color': [
'match',
['get', 'category'],
'Contamination Source', '#ff4d4d',
'Informal Vendor Hub', '#ffff66',
'#cccccc'
]
}
});
}
});
// Add markers for high-risk points
intelligenceData.features
.filter(f => f.geometry.type === 'Point')
.forEach(feature => {
const el = document.createElement('div');
el.className = 'hotspot-marker';
el.innerHTML = '<div class="pulse-circle"></div>';
new mapboxgl.Marker(el)
.setLngLat(feature.geometry.coordinates)
.setPopup(
new mapboxgl.Popup({ offset: 25 }) // add popups
.setHTML(
`<h3>${feature.properties.title}</h3><p>${feature.properties.description}</p>`
)
)
.addTo(map);
});
});
// Tooltip functionality
const tooltip = document.getElementById('tooltip');
let hoverFeatureData = null;
let tooltipLngLat = null;
const layers = ['zones-fill', 'infra-lines', 'cause-points'];
layers.forEach(layerId => {
map.on('mousemove', layerId, (e) => {
const f = (e.features || [])[0];
if (!f) return;
map.getCanvas().style.cursor = 'pointer';
tooltipLngLat = e.lngLat;
hoverFeatureData = f.properties;
showTooltip();
});
map.on('mouseleave', layerId, () => {
map.getCanvas().style.cursor = '';
hoverFeatureData = null;
tooltip.style.display = 'none';
});
});
map.on('move', showTooltip);
function showTooltip() {
if (!hoverFeatureData || !tooltipLngLat) return;
tooltip.style.display = 'block';
tooltip.style.left = `${tooltipLngLat.x}px`;
tooltip.style.top = `${tooltipLngLat.y}px`;
document.getElementById('tooltip-title').textContent = hoverFeatureData.title;
document.getElementById('tooltip-info').textContent = hoverFeatureData.description;
document.getElementById('tooltip-coords').textContent =
`${tooltipLngLat.lng.toFixed(4)}, ${tooltipLngLat.lat.toFixed(4)}`;
}
// Chat panel functionality
const chatPanel = document.getElementById('chat-panel');
const chatToggle = document.getElementById('chat-toggle');
const chatLog = document.getElementById('chat-log');
const chatForm = document.getElementById('chat-form');
const chatInput = document.getElementById('chat-input');
const chatSend = document.getElementById('chat-send');
chatToggle.addEventListener('click', () => {
chatPanel.classList.toggle('minimized');
chatToggle.innerHTML = chatPanel.classList.contains('minimized') ?
'<i class="fas fa-plus"></i>' : '<i class="fas fa-minus"></i>';
});
chatForm.addEventListener('submit', async (e) => {
e.preventDefault();
const message = chatInput.value.trim();
if (!message) return;
// Add user message
addMessage(message, 'user');
chatInput.value = '';
// Detect disease from query
const detectedDisease = detectDiseaseFromQuery(message);
if (detectedDisease) {
addMessage(`Fetching live ${detectedDisease} outbreak data...`, 'sys');
try {
const diseaseData = await fetchDiseaseData(detectedDisease);
if (diseaseData) {
// Update map with live data
map.getSource('live_disease_data').setData(diseaseData);
// Build dynamic story from live data
buildStoryFromLiveData(detectedDisease, diseaseData);
addMessage(`Live ${detectedDisease} data loaded. ${diseaseData.features.length} hotspots detected.`, 'sys');
} else {
addMessage(`Could not fetch live ${detectedDisease} data. Trying cached information...`, 'sys');
}
} catch (error) {
addMessage(`Error processing ${detectedDisease} query: ${error.message}`, 'sys');
}
} else {
addMessage('Please specify a disease (cholera, mpox, malaria, dengue, lassa) and location.', 'sys');
}
});
function detectDiseaseFromQuery(query) {
const diseases = ['cholera', 'mpox', 'malaria', 'dengue', 'lassa'];
const queryLower = query.toLowerCase();
return diseases.find(d => queryLower.includes(d));
}
function buildStoryFromLiveData(disease, geojsonData) {
const storyPanel = document.getElementById('story-panel');
const storyBody = document.getElementById('story-body');
storyBody.innerHTML = '';
storyPanel.style.display = 'block';
// Create chapters from live data features
geojsonData.features.forEach((feature, index) => {
const chapter = document.createElement('div');
chapter.className = 'chapter' + (index === 0 ? ' active' : '');
chapter.id = `chapter-${feature.id}`;
const h4 = document.createElement('h4');
h4.textContent = feature.properties.title || `${disease} Hotspot ${index + 1}`;
chapter.appendChild(h4);
const p = document.createElement('p');
p.textContent = feature.properties.description || `Location identified as ${disease} risk area.`;
chapter.appendChild(p);
storyBody.appendChild(chapter);
// Add marker for this feature
const [lon, lat] = feature.geometry.coordinates;
const el = document.createElement('div');
el.className = 'hotspot-marker';
el.style.backgroundColor = getDiseaseColor(disease);
new mapboxgl.Marker(el)
.setLngLat([lon, lat])
.setPopup(new mapboxgl.Popup({ offset: 25 })
.setHTML(`<h3>${feature.properties.title}</h3><p>${feature.properties.description}</p>`))
.addTo(map);
if (index === 0) {
map.flyTo({
center: [lon, lat],
zoom: 12,
pitch: 45,
bearing: 0
});
}
});
// Set up intersection observer for chapters
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const chapterId = entry.target.id.replace('chapter-', '');
const feature = geojsonData.features.find(f => f.id === chapterId);
if (feature) {
document.querySelectorAll('.chapter').forEach(c => c.classList.remove('active'));
entry.target.classList.add('active');
const [lon, lat] = feature.geometry.coordinates;
map.flyTo({
center: [lon, lat],
zoom: 12,
pitch: 45,
bearing: 0
});
}
}
});
}, { threshold: 0.7 });
document.querySelectorAll('.chapter').forEach(chapter => {
observer.observe(chapter);
});
}
function getDiseaseColor(disease) {
switch(disease) {
case 'cholera': return '#ff0033';
case 'mpox': return '#ffa500';
case 'malaria': return '#00ff88';
case 'dengue': return '#bb66ff';
case 'lassa': return '#a020f0';
default: return '#ffa500';
}
}
=======
function addMessage(text, type) {
const msgEl = document.createElement('div');
msgEl.className = `msg ${type}`;
msgEl.textContent = text === 'user' ? `> ${text}` : text;
chatLog.appendChild(msgEl);
chatLog.scrollTop = chatLog.scrollHeight;
}
// Story builder function
function buildStory(disease, keys) {
const storyPanel = document.getElementById('story-panel');
const storyBody = document.getElementById('story-body');
storyBody.innerHTML = '';
storyPanel.style.display = 'block';
const knowledge = {
cholera: {
kibera: {
key: 'kibera',
place: 'Nairobi',
center: [36.7876, -1.3146],
title: 'Kibera — River Intake',
info: 'ROOT CAUSE: Sanitation + contaminated tributaries.\nTurbidity ~180 NTU; residual chlorine ~0.05 mg/L.'
},
mathare: {
key: 'mathare',
place: 'Nairobi',
center: [36.8706, -1.2600],
title: 'Mathare Valley',
info: 'ROOT CAUSE: Drainage overflow → wells.\nSpike after rainfall; inspect upstream drains.'
}
},
mpox: {
gombe: {
key: 'gombe',
place: 'Kinshasa',
center: [15.31, -4.322],
title: 'Kinshasa — Gombe Zone',
info: 'Syndromic cluster flagged; confirm lab & contact tracing routes.'
}
}
};
const dict = knowledge[disease] || {};
const toActivate = [];
keys.forEach((k, i) => {
const cfg = dict[k];
if (!cfg) return;
const sec = document.createElement('div');
sec.className = 'chapter' + (i === 0 ? ' active' : '');
sec.id = `chap-${cfg.key}`;
const h4 = document.createElement('h4');
h4.textContent = cfg.title;
sec.appendChild(h4);
const p = document.createElement('p');
p.textContent = cfg.info;
sec.appendChild(p);
storyBody.appendChild(sec);
toActivate.push(cfg);
});
toActivate.forEach((cfg, i) => {
const [lon, lat] = cfg.center;
const el = document.createElement('div');
el.className = 'hotspot-marker';
new mapboxgl.Marker(el)
.setLngLat([lon, lat])
.setPopup(new mapboxgl.Popup({ offset: 25 })
.setHTML(`<h3>${cfg.title}</h3><p>${cfg.info}</p>`))
.addTo(map);
if (i === 0) {
map.flyTo({
center: [lon, lat],
zoom: 14,
pitch: 50,
bearing: -20,
speed: 1.4,
curve: 1.2
});
}
});
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const id = entry.target.id.replace('chap-', '');
const cfg = Object.values(dict).find(c => c.key === id);
if (!cfg) return;
document.querySelectorAll('.chapter').forEach(s => s.classList.remove('active'));
entry.target.classList.add('active');
map.flyTo({
center: cfg.center,
zoom: 14,
pitch: 50,
bearing: -20,
speed: 1.4,
curve: 1.2
});
}
});
}, { root: storyPanel, threshold: 0.7 });
document.querySelectorAll('.chapter').forEach(sec => observer.observe(sec));
}
// Story panel functionality
const storyPanel = document.getElementById('story-panel');
const storyClose = document.getElementById('story-close');
const chapters = document.querySelectorAll('.chapter');
// Show story panel on load
setTimeout(() => {
storyPanel.classList.add('visible');
}, 1000);
storyClose.addEventListener('click', () => {
storyPanel.classList.remove('visible');
});
// Intersection observer for story chapters
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('active');
} else {
entry.target.classList.remove('active');
}
});
}, { threshold: 0.7 });
chapters.forEach(chapter => {
observer.observe(chapter);
});
// Live ticker with disease updates
async function updateLiveTicker() {
try {
const response = await fetch('https://api.example.com/live-updates');
const updates = await response.json();
const tickerMarquee = document.querySelector('#ticker .marquee');
tickerMarquee.innerHTML = '';
updates.forEach(update => {
const item = document.createElement('div');
item.className = 'tick-item';
const badge = document.createElement('span');
badge.className = 'badge';
badge.textContent = update.disease.toUpperCase();
badge.style.backgroundColor = getDiseaseColor(update.disease);
const text = document.createElement('span');
text.textContent = `${update.location}: ${update.message}`;
item.appendChild(badge);
item.appendChild(text);
tickerMarquee.appendChild(item);
// Clone for infinite scroll
const clone = item.cloneNode(true);
tickerMarquee.appendChild(clone);
});
// Animate ticker
let position = 0;
function animate() {
position--;
if (position < -tickerMarquee.scrollWidth / 2) {
position = 0;
}
tickerMarquee.style.transform = `translateX(${position}px)`;
requestAnimationFrame(animate);
}
animate();
} catch (error) {
console.error('Error fetching live updates:', error);
}
// Refresh every 5 minutes
setTimeout(updateLiveTicker, 300000);
}
// Initial ticker load
updateLiveTicker();
=======
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=MoShow/new-dynamo" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>