File size: 6,060 Bytes
403f212 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 | import React, { useState, useRef } from "react";
/**
* CropGuard.jsx — React frontend for the crop disease detection system.
* Implements the four-step farmer flow from §3.10.4:
* home -> preview -> loading -> result
* Talks to the FastAPI backend's POST /predict endpoint.
*
* Set the API base URL via Vite env: VITE_API_URL=http://localhost:8000
*/
const API = import.meta.env.VITE_API_URL || "http://localhost:8000";
const SEVERITY = {
early: { label: "Early stage", urgency: "Routine", color: "#3fa34d",
desc: "Symptoms are small and localised. You have time, but act soon." },
moderate: { label: "Moderate stage", urgency: "Urgent", color: "#e9a625",
desc: "The disease covers a good part of the leaf and may spread fast. Treat this week." },
severe: { label: "Severe stage", urgency: "Emergency", color: "#cf3b2f",
desc: "Most of the leaf or plant is affected. Act today to save the rest of your crop." },
};
export default function CropGuard() {
const [screen, setScreen] = useState("home"); // home | preview | loading | result
const [imgUrl, setImgUrl] = useState(null);
const [file, setFile] = useState(null);
const [result, setResult] = useState(null);
const [error, setError] = useState(null);
const camRef = useRef(null);
const galRef = useRef(null);
function pick(e) {
const f = e.target.files?.[0];
if (!f) return;
setFile(f);
setImgUrl(URL.createObjectURL(f));
setScreen("preview");
e.target.value = "";
}
async function analyse() {
setScreen("loading");
setError(null);
try {
const fd = new FormData();
fd.append("file", file);
const res = await fetch(`${API}/predict`, { method: "POST", body: fd });
if (!res.ok) throw new Error("Server error");
setResult(await res.json());
setScreen("result");
} catch (err) {
setError("Could not reach the analysis server. Check your connection and try again.");
setScreen("preview");
}
}
function reset() { setScreen("home"); setResult(null); setImgUrl(null); setFile(null); }
return (
<div className="cg">
<header className="cg-head">
<div className="cg-logo">🌿</div>
<div><h1>CropGuard GH</h1><span>Snap a leaf. Know the disease.</span></div>
</header>
{screen === "home" && (
<section>
<div className="cg-hero">
<h2>Diagnose crop disease in seconds</h2>
<p>Photograph a sick leaf and get the disease, its severity, and what to do — free.</p>
</div>
{error && <p className="cg-err">{error}</p>}
<button className="cg-cta" onClick={() => camRef.current.click()}>📷 Take a photo of a leaf</button>
<button className="cg-ghost" onClick={() => galRef.current.click()}>🖼️ Choose from gallery</button>
<p className="cg-crops">Detects: Maize · Tomato · Cassava</p>
</section>
)}
{screen === "preview" && (
<section>
<img className="cg-preview" src={imgUrl} alt="leaf" />
<h3>Is this the right photo?</h3>
<p>Make sure the diseased leaf fills the frame and is in focus.</p>
{error && <p className="cg-err">{error}</p>}
<div className="cg-row">
<button className="cg-ghost" onClick={reset}>← Retake</button>
<button className="cg-cta" onClick={analyse}>🔍 Analyse crop</button>
</div>
</section>
)}
{screen === "loading" && (
<section className="cg-loading">
<div className="cg-spinner" />
<h3>Analysing the leaf…</h3>
<p>Checking colour, spots and damage</p>
</section>
)}
{screen === "result" && result && (
<Result result={result} onReset={reset} />
)}
<input ref={camRef} type="file" accept="image/*" capture="environment" hidden onChange={pick} />
<input ref={galRef} type="file" accept="image/*" hidden onChange={pick} />
</div>
);
}
function Result({ result, onReset }) {
const d = result.disease || {};
const healthy = !!d.healthy;
const sev = !healthy && result.severity ? SEVERITY[result.severity] : null;
return (
<section>
<div className={`cg-rhero ${healthy ? "ok" : "bad"}`}>
<div className="cg-rlabel">Detected disease</div>
<div className="cg-rname">{d.name}</div>
<div className="cg-rcrop">🌿 {d.crop}</div>
<div className="cg-conf">
<span>Confidence</span><b>{Math.round(result.confidence * 100)}%</b>
</div>
</div>
{!healthy && sev && (
<div className="cg-badges">
<div className="cg-badge">
<small>Severity</small>
<b style={{ color: sev.color }}>● {sev.label}</b>
</div>
<div className="cg-badge">
<small>Urgency</small>
<b style={{ color: sev.color }}>{sev.urgency}</b>
</div>
</div>
)}
{sev && <p className="cg-sevdesc">{sev.desc}</p>}
{!healthy && (
<div className="cg-card">
<h4>✅ What to do now</h4>
<ol>{(d.treatment || []).map((t, i) => <li key={i}>{t}</li>)}</ol>
<div className="cg-products">
{(d.products || []).map((p, i) => <span key={i}>🧪 {p}</span>)}
</div>
</div>
)}
{!healthy && (
<div className="cg-card">
<h4>ℹ️ About this disease</h4>
<p>{d.cause}</p>
</div>
)}
{healthy && (
<div className="cg-card cg-ok">
<h4>✅ No disease detected</h4>
<p>This leaf looks healthy. Keep monitoring your field weekly.</p>
</div>
)}
<p className="cg-disc">
⚠️ This is a diagnostic aid, not a replacement for an extension officer.
For unusual or severe cases, consult MoFA.
</p>
<button className="cg-cta" onClick={onReset}>🍃 Scan another leaf</button>
</section>
);
}
|