Untitled

 avatar
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