Untitled
unknown
plain_text
a year ago
22 kB
26
Indexable
import React, { useEffect, useMemo, useRef, useState } from "react";
import { motion } from "framer-motion";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { Badge } from "@/components/ui/badge";
import { Progress } from "@/components/ui/progress";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import { Separator } from "@/components/ui/separator";
import { Slider } from "@/components/ui/slider";
import {
MapPin,
Recycle,
CloudRain,
Route,
BatteryFull,
Brain,
Clock4,
Upload,
Leaf,
Truck,
ShieldCheck,
ThermometerSnowflake,
} from "lucide-react";
import {
ResponsiveContainer,
LineChart,
Line,
CartesianGrid,
XAxis,
YAxis,
Tooltip,
BarChart,
Bar,
PieChart,
Pie,
Cell,
} from "recharts";
import mapboxgl from "mapbox-gl";
import "mapbox-gl/dist/mapbox-gl.css";
const distKm = (a, b) => {
const R = 6371;
const dLat = ((b.lat - a.lat) * Math.PI) / 180;
const dLng = ((b.lng - a.lng) * Math.PI) / 180;
const lat1 = (a.lat * Math.PI) / 180;
const lat2 = (b.lat * Math.PI) / 180;
const x =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.sin(dLng / 2) * Math.sin(dLng / 2) * Math.cos(lat1) * Math.cos(lat2);
const c = 2 * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));
return R * c;
};
function predictHoursToOverflow(bin) {
const base = Math.max(1, (100 - bin.fill) / 3.2);
const heatBoost = (bin.tempC - 25) * 0.15;
const crowdBoost = bin.lastCollectedHrs > 24 ? 2 : 0;
const organicBoost = (bin.organic / 100) * 1.5;
const humidityBoost = (bin.humidity - 60) * 0.07;
const est = base - heatBoost - crowdBoost - organicBoost - humidityBoost;
return Math.max(1, Math.round(est));
}
function riskLevel(bin) {
const hours = predictHoursToOverflow(bin);
if (bin.fill >= 90 || hours <= 6) return { tag: "Critical", tone: "bg-red-500" };
if (bin.fill >= 70 || hours <= 12) return { tag: "High", tone: "bg-orange-500" };
if (bin.fill >= 50 || hours <= 24) return { tag: "Medium", tone: "bg-amber-500" };
return { tag: "Low", tone: "bg-emerald-500" };
}
function planRoute(bins, start = { lat: 12.9716, lng: 77.5946 }) {
const todo = [...bins];
const path = [start];
let current = start;
while (todo.length) {
let bestIdx = 0;
let bestDist = Infinity;
for (let i = 0; i < todo.length; i++) {
const d = distKm(current, todo[i]);
if (d < bestDist) {
bestDist = d;
bestIdx = i;
}
}
current = todo[bestIdx];
path.push(current);
todo.splice(bestIdx, 1);
}
return path;
}
mapboxgl.accessToken = import.meta.env.VITE_MAPBOX_TOKEN || "YOUR_MAPBOX_ACCESS_TOKEN";
function WasteMap({ bins, prioritized }) {
const mapContainer = useRef(null);
const mapRef = useRef(null);
const markersRef = useRef([]);
const routeCoords = useMemo(() => {
const pts = prioritized.length ? planRoute(prioritized) : [];
return pts.map((p) => [p.lng, p.lat]);
}, [prioritized]);
useEffect(() => {
if (mapRef.current) return;
const map = new mapboxgl.Map({
container: mapContainer.current,
style: "mapbox://styles/mapbox/streets-v12",
center: [77.5946, 12.9716],
zoom: 12.7,
});
map.addControl(new mapboxgl.NavigationControl(), "top-right");
mapRef.current = map;
return () => {
markersRef.current.forEach((m) => m.remove());
markersRef.current = [];
if (mapRef.current) {
if (mapRef.current.getLayer("route-layer")) mapRef.current.removeLayer("route-layer");
if (mapRef.current.getSource("route-source")) mapRef.current.removeSource("route-source");
mapRef.current.remove();
mapRef.current = null;
}
};
}, []);
useEffect(() => {
if (!mapRef.current) return;
markersRef.current.forEach((m) => m.remove());
markersRef.current = [];
bins.forEach((b) => {
const risk = riskLevel(b);
const el = document.createElement("div");
el.style.width = "14px";
el.style.height = "14px";
el.style.borderRadius = "50%";
el.style.boxShadow = "0 0 0 2px white";
el.style.background =
risk.tag === "Critical" ? "#ef4444" :
risk.tag === "High" ? "#f97316" :
risk.tag === "Medium" ? "#f59e0b" : "#10b981";
const marker = new mapboxgl.Marker(el)
.setLngLat([b.lng, b.lat])
.setPopup(new mapboxgl.Popup({ offset: 12 }).setHTML(
`<div style="font-family: ui-sans-serif, system-ui; font-size:12px">
<strong>${b.location}</strong><br/>
Bin ${b.id}<br/>
Fill: ${b.fill}% — Risk: ${risk.tag}<br/>
ETA overflow: ~${predictHoursToOverflow(b)}h
</div>`
))
.addTo(mapRef.current);
markersRef.current.push(marker);
});
}, [bins]);
useEffect(() => {
if (!mapRef.current) return;
const map = mapRef.current;
const sourceId = "route-source";
const layerId = "route-layer";
if (map.getLayer(layerId)) map.removeLayer(layerId);
if (map.getSource(sourceId)) map.removeSource(sourceId);
if (routeCoords.length >= 2) {
map.addSource(sourceId, {
type: "geojson",
data: {
type: "Feature",
geometry: { type: "LineString", coordinates: routeCoords },
properties: {},
},
});
map.addLayer({
id: layerId,
type: "line",
source: sourceId,
layout: { "line-join": "round", "line-cap": "round" },
paint: { "line-color": "#2563eb", "line-width": 4, "line-opacity": 0.7 },
});
const bounds = new mapboxgl.LngLatBounds();
routeCoords.forEach((c) => bounds.extend(c));
map.fitBounds(bounds, { padding: 60, duration: 500 });
}
}, [routeCoords]);
return <div ref={mapContainer} className="w-full h-80 rounded-2xl" />;
}
export default function SmartWasteAI() {
const [bins, setBins] = useState([]);
const [threshold, setThreshold] = useState(70);
const [aiEnabled, setAiEnabled] = useState(true);
const [search, setSearch] = useState("");
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
useEffect(() => {
let alive = true;
setLoading(true);
fetch("/api/bins")
.then((r) => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
})
.then((data) => {
if (!alive) return;
setBins(Array.isArray(data) ? data : []);
setError("");
})
.catch((e) => setError(e.message || "API error"))
.finally(() => {
if (alive) setLoading(false);
});
return () => {
alive = false;
};
}, []);
const filtered = useMemo(() => {
return bins.filter((b) =>
`${b.id} ${b.location}`.toLowerCase().includes(search.toLowerCase())
);
}, [bins, search]);
const prioritized = useMemo(() => {
return filtered
.map((b) => ({ ...b, hoursToOverflow: predictHoursToOverflow(b) }))
.filter((b) => b.fill >= threshold || (aiEnabled && b.hoursToOverflow <= 12))
.sort((a, z) => z.fill - a.fill);
}, [filtered, threshold, aiEnabled]);
const composition = useMemo(() => {
const totalOrganic = bins.reduce((s, b) => s + (b.fill * (b.organic || 0)) / 100, 0);
const totalRecyclable = bins.reduce((s, b) => s + (b.fill * (b.recyclable || 0)) / 100, 0);
const totalLandfill = bins.reduce((s, b) => s + (b.fill * (b.landfill || 0)) / 100, 0);
return [
{ name: "Organic", value: Math.round(totalOrganic) },
{ name: "Recyclable", value: Math.round(totalRecyclable) },
{ name: "Landfill", value: Math.round(totalLandfill) },
];
}, [bins]);
const uploadCSV = (e) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (evt) => {
const text = String(evt.target?.result || "");
const rows = text.split(/\r?\n/).filter(Boolean);
const parsed = rows.map((r) => {
const [id, location, fill, organic, recyclable, landfill, tempC, humidity, lastCollectedHrs, battery, lat, lng] = r.split(",");
return {
id: id?.trim() || (self.crypto?.randomUUID ? self.crypto.randomUUID().slice(0, 6) : `${Date.now()}`.slice(-6)),
location: location?.trim() || "Unknown",
fill: Number(fill) || 0,
organic: Number(organic) || 0,
recyclable: Number(recyclable) || 0,
landfill: Number(landfill) || 0,
tempC: Number(tempC) || 28,
humidity: Number(humidity) || 60,
lastCollectedHrs: Number(lastCollectedHrs) || 0,
battery: Number(battery) || 100,
lat: Number(lat) || 12.9716,
lng: Number(lng) || 77.5946,
};
});
setBins(parsed);
};
reader.readAsText(file);
};
const trendData = [
{ day: "Mon", organic: 18, recyclable: 11, landfill: 4, total: 33 },
{ day: "Tue", organic: 22, recyclable: 12, landfill: 5, total: 39 },
{ day: "Wed", organic: 19, recyclable: 13, landfill: 4, total: 36 },
{ day: "Thu", organic: 25, recyclable: 15, landfill: 6, total: 46 },
{ day: "Fri", organic: 27, recyclable: 17, landfill: 7, total: 51 },
{ day: "Sat", organic: 29, recyclable: 19, landfill: 7, total: 55 },
{ day: "Sun", organic: 24, recyclable: 14, landfill: 5, total: 43 },
];
return (
<div className="min-h-screen bg-gradient-to-b from-white to-slate-50 text-slate-900">
<header className="max-w-7xl mx-auto px-4 pt-10 pb-6">
<motion.div initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5 }} className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
<div>
<h1 className="text-3xl md:text-5xl font-bold tracking-tight flex items-center gap-3">
<Recycle className="h-8 w-8" /> Smart Waste Management
</h1>
<p className="mt-2 max-w-2xl text-slate-600">AI-assisted monitoring, routing, and analytics to keep your city clean, efficient, and sustainable.</p>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<Brain className="h-5 w-5" />
<span className="text-sm">AI</span>
<Switch checked={aiEnabled} onCheckedChange={setAiEnabled} />
</div>
<Button className="rounded-2xl relative overflow-hidden">
<Upload className="h-4 w-4 mr-2" /> Import CSV
<input type="file" accept=".csv" onChange={uploadCSV} className="absolute inset-0 opacity-0 cursor-pointer" />
</Button>
</div>
</motion.div>
</header>
<main className="max-w-7xl mx-auto px-4 pb-14 space-y-6">
<Section title="Operations Console" icon={ShieldCheck}>
<div className="grid md:grid-cols-3 gap-6">
<div className="space-y-2">
<label className="text-sm text-slate-500">Search bins</label>
<Input value={search} onChange={(e) => setSearch(e.target.value)} placeholder="Try 'MG Road' or 'B-101'" className="rounded-2xl" />
</div>
<div className="space-y-2">
<label className="text-sm text-slate-500">Collection threshold (% full)</label>
<div className="px-1">
<Slider value={[threshold]} onValueChange={(v) => setThreshold(v[0])} min={30} max={95} step={1} className="max-w-sm" />
<div className="text-xs text-slate-500 mt-1">Current: <span className="font-semibold">{threshold}%</span></div>
</div>
</div>
<div className="space-y-2">
<label className="text-sm text-slate-500">Recommendations</label>
<div className="text-sm text-slate-700">
{aiEnabled ? (
<ul className="list-disc pl-5 space-y-1">
<li>Prioritize <span className="font-medium">High/Critical</span> risk bins.</li>
<li>Dispatch 1 truck to East cluster.</li>
<li>Increase recycling pickup on weekends.</li>
</ul>
) : (
<div className="text-slate-500">Enable AI to see proactive insights.</div>
)}
</div>
</div>
</div>
</Section>
<div className="grid lg:grid-cols-3 gap-6">
<Section title="Live Bins" icon={MapPin} className="lg:col-span-2">
{loading ? (
<div className="text-sm text-slate-500">Loading bins…</div>
) : error ? (
<div className="text-sm text-red-600">Failed to load: {error}</div>
) : (
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
{filtered.map((b) => {
const risk = riskLevel(b);
const hours = predictHoursToOverflow(b);
return (
<Card key={b.id} className="rounded-2xl border shadow-sm">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-base flex items-center gap-2">
<MapPin className="h-4 w-4" /> {b.location}
</CardTitle>
<Badge className={`${risk.tone} text-white`}>{risk.tag}</Badge>
</div>
<div className="text-xs text-slate-500 mt-1">Bin {b.id}</div>
</CardHeader>
<CardContent className="space-y-3">
<div>
<div className="flex items-center justify-between text-sm">
<span>Fill level</span>
<span className="font-medium">{b.fill}%</span>
</div>
<Progress value={b.fill} className="h-2 rounded-full" />
</div>
<div className="grid grid-cols-3 gap-2 text-xs">
<div className="flex items-center gap-1"><ThermometerSnowflake className="h-3 w-3" /> {b.tempC}°C</div>
<div className="flex items-center gap-1"><CloudRain className="h-3 w-3" /> {b.humidity}%</div>
<div className="flex items-center gap-1"><BatteryFull className="h-3 w-3" /> {b.battery}%</div>
</div>
<Separator />
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-1"><Clock4 className="h-4 w-4" /> Overflow in</div>
<div className="font-semibold">~{hours}h</div>
</div>
<div className="text-xs text-slate-500">Last collected: {b.lastCollectedHrs}h ago</div>
</CardContent>
</Card>
);
})}
</div>
)}
</Section>
<Section title="Optimized Route" icon={Route}>
{prioritized.length === 0 ? (
<div className="text-sm text-slate-500">No bins exceed the threshold. You're all set ✨</div>
) : (
<div className="space-y-3">
<div className="text-xs text-slate-500">Stops (nearest-first):</div>
<ol className="list-decimal pl-5 space-y-1">
<li key="HQ" className="font-medium">Depot / HQ</li>
{prioritized.map((p) => (
<li key={p.id} className="flex items-center justify-between">
<span>{p.location} <span className="text-xs text-slate-500">({p.id})</span></span>
<span className="text-xs">{p.fill}%</span>
</li>
))}
</ol>
<Button className="w-full rounded-2xl mt-2"><Truck className="h-4 w-4 mr-2" /> Dispatch</Button>
</div>
)}
</Section>
</div>
<Section title="City Analytics" icon={Brain}>
<Tabs defaultValue="trends" className="w-full">
<TabsList className="rounded-2xl">
<TabsTrigger value="trends">Weekly Trends</TabsTrigger>
<TabsTrigger value="composition">Composition</TabsTrigger>
<TabsTrigger value="capacity">Capacity vs Collection</TabsTrigger>
</TabsList>
<TabsContent value="trends" className="mt-4">
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={trendData} margin={{ top: 10, right: 20, bottom: 0, left: 0 }}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="day" />
<YAxis />
<Tooltip />
<Line type="monotone" dataKey="organic" strokeWidth={2} />
<Line type="monotone" dataKey="recyclable" strokeWidth={2} />
<Line type="monotone" dataKey="landfill" strokeWidth={2} />
</LineChart>
</ResponsiveContainer>
</div>
</TabsContent>
<TabsContent value="composition" className="mt-4">
<div className="h-64 grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie dataKey="value" data={composition} outerRadius={90} label>
{composition.map((_, i) => (
<Cell key={`cell-${i}`} />
))}
</Pie>
<Tooltip />
</PieChart>
</ResponsiveContainer>
</div>
<div className="space-y-3">
{composition.map((c) => (
<div key={c.name} className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Leaf className="h-4 w-4" /> {c.name}
</div>
<div className="font-semibold">{c.value}</div>
</div>
))}
</div>
</div>
</TabsContent>
<TabsContent value="capacity" className="mt-4">
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={trendData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="day" />
<YAxis />
<Tooltip />
<Bar dataKey="total" />
</BarChart>
</ResponsiveContainer>
</div>
</TabsContent>
</Tabs>
</Section>
<Section title="Geo View (Live Map)" icon={MapPin}>
<WasteMap bins={filtered} prioritized={prioritized} />
<p className="text-xs text-slate-500 mt-2">Powered by Mapbox. Use the threshold & AI toggle to change the route in real time.</p>
</Section>
<Section title="How It Works" icon={Brain}>
<div className="grid md:grid-cols-3 gap-6 text-sm">
<div className="space-y-2">
<h3 className="font-semibold flex items-center gap-2"><MapPin className="h-4 w-4" /> Sensor Telemetry</h3>
<p>Smart bins stream fill level, temperature, humidity, and battery status. Data lands in a time-series store.</p>
</div>
<div className="space-y-2">
<h3 className="font-semibold flex items-center gap-2"><Brain className="h-4 w-4" /> AI Forecasting</h3>
<p>Predictive models estimate time-to-overflow and contamination risk, prioritizing pickups proactively.</p>
</div>
<div className="space-y-2">
<h3 className="font-semibold flex items-center gap-2"><Route className="h-4 w-4" /> Route Optimization</h3>
<p>Greedy/heuristic solvers create near-optimal truck routes, reducing fuel cost and emissions.</p>
</div>
</div>
</Section>
<footer className="text-center text-xs text-slate-500 pt-6">
Built with ❤️ — API + Mapbox demo. Replace mock API with your backend.
</footer>
</main>
</div>
);
}
const Section = ({ title, icon: Icon, children, className = "" }) => (
<Card className={`rounded-2xl shadow-md ${className}`}>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="flex items-center gap-2 text-xl">
{Icon ? <Icon className="h-5 w-5" /> : null}
{title}
</CardTitle>
</CardHeader>
<CardContent>{children}</CardContent>
</Card>
);
Editor is loading...
Leave a Comment