| import React, { useState, useRef } from "react"; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| 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"); |
| 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> |
| ); |
| } |
|
|