demeter

Autonomous Hydroponic Intelligence
commit 6bd43d625c709b1856cc8f8008fb85785f5e608d
parent 50d8ab6e001d505abdc8b1be10b5ca132244616a
Author: maydayv7 <maydayv7@gmail.com>
Date:   Sat, 28 Mar 2026 12:26:16 +0530

Misc frontend fixes

Diffstat:
Mbackend/node_server/controllers/cropController.js | 11++++++-----
Mbackend/server/functions.py | 6+++---
Mfrontend/src/index.css | 2+-
Mfrontend/src/pages/AddCrop.jsx | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mfrontend/src/pages/CropDetails.jsx | 12+++++++++++-
Mfrontend/src/pages/Dashboard.jsx | 6+++---
Mfrontend/src/pages/Help.jsx | 10++++------
Mfrontend/src/pages/RunCycle.jsx | 51++++++++++++++++++++++++++++++++-------------------
Mfrontend/src/pages/Settings.jsx | 1-
Mfrontend/src/utils/translations.js | 4++--
Mreadme.md | 5++++-
11 files changed, 144 insertions(+), 42 deletions(-)

diff --git a/backend/node_server/controllers/cropController.js b/backend/node_server/controllers/cropController.js @@ -61,12 +61,13 @@ const createCrop = async (req, res) => { humidity: [60.0], }, - // Auto-generate fake sensor IDs + // Fallback to auto-generated fake sensor IDs sensor_ids: { - ph_sensor: fakeSensorId("PH"), - ec_sensor: fakeSensorId("EC"), - temp_sensor: fakeSensorId("TMP"), - humidity_sensor: fakeSensorId("HUM"), + ph_sensor: req.body.sensor_ids?.ph_sensor || fakeSensorId("PH"), + ec_sensor: req.body.sensor_ids?.ec_sensor || fakeSensorId("EC"), + temp_sensor: req.body.sensor_ids?.temp_sensor || fakeSensorId("TMP"), + humidity_sensor: + req.body.sensor_ids?.humidity_sensor || fakeSensorId("HUM"), }, // Auto-set cycle_duration_hours based on crop type diff --git a/backend/server/functions.py b/backend/server/functions.py @@ -279,7 +279,7 @@ async def process_cycle_stream(file: UploadFile, sensors_str: str, builder): image_b64 = base64.b64encode(file_content).decode("utf-8") abs_image_path = os.path.abspath(temp_filename) - yield f"data: {json.dumps({'agent': 'FETCHER', 'text': '[Fetcher] ЁЯУб Requesting data from simulator...'})}\n\n" + yield f"data: {json.dumps({'agent': 'FETCHER', 'text': 'ЁЯУб Requesting data from simulator...'})}\n\n" await asyncio.sleep(0.5) # --- 2. DATA --- @@ -293,7 +293,7 @@ async def process_cycle_stream(file: UploadFile, sensors_str: str, builder): seq_num = get_next_sequence_number(target_crop_id) - yield f"data: {json.dumps({'agent': 'FETCHER', 'text': f'[Fetcher] ЁЯФв Sequence for {target_crop_id}: {seq_num}'})}\n\n" + yield f"data: {json.dumps({'agent': 'FETCHER', 'text': f'ЁЯФв Sequence for {target_crop_id}: {seq_num}'})}\n\n" await asyncio.sleep(0.3) metadata = { @@ -309,7 +309,7 @@ async def process_cycle_stream(file: UploadFile, sensors_str: str, builder): query_fmu = builder.create_fmu(abs_image_path, clean_sensors, metadata=metadata) store_fmu(query_fmu) - yield f"data: {json.dumps({'agent': 'FETCHER', 'text': f'[Fetcher] ЁЯза FMU Created (ID: {query_fmu.id}) тАФ Handing off to specialists.'})}\n\n" + yield f"data: {json.dumps({'agent': 'FETCHER', 'text': f'ЁЯза FMU Created (ID: {query_fmu.id}) тАФ Handing off to specialists.'})}\n\n" await asyncio.sleep(0.5) # --- 3. RESEARCH --- diff --git a/frontend/src/index.css b/frontend/src/index.css @@ -1,4 +1,4 @@ -@import url("https://fonts.googleapis.com/css2?family=Mozilla+Headline:wght@200..700&family=Roboto:ital,wght@0,100..900;1,100..900&family=TASA+Explorer:wght@400..800&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=Syne:wght@400;500;600;700;800&family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300&family=Instrument+Serif:ital@0;1&family=Noto+Sans+Devanagari:wght@400;500;600;700&display=swap"); @tailwind base; @tailwind components; diff --git a/frontend/src/pages/AddCrop.jsx b/frontend/src/pages/AddCrop.jsx @@ -128,6 +128,14 @@ export default function AddCrop() { location: "", notes: "", }); + + const [sensorIds, setSensorIds] = useState({ + ph_sensor: "", + ec_sensor: "", + temp_sensor: "", + humidity_sensor: "", + }); + const [imageFile, setImageFile] = useState(null); const [imagePreview, setImagePreview] = useState(null); @@ -188,6 +196,7 @@ export default function AddCrop() { crop: form.crop, location: form.location, notes: form.notes, + sensor_ids: sensorIds, // image_url: upload to storage }; @@ -451,6 +460,75 @@ export default function AddCrop() { )} </SectionCard> + {/* Sensor Assignment */} + <SectionCard> + <div className="section-label" style={{ marginBottom: 12 }}> + Sensor Assignment + </div> + <div + style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }} + > + {[ + { key: "ph_sensor", label: "pH Sensor", color: "var(--green)" }, + { key: "ec_sensor", label: "EC Sensor", color: "var(--amber)" }, + { key: "temp_sensor", label: "Temp Sensor", color: "var(--red)" }, + { + key: "humidity_sensor", + label: "Humidity Sensor", + color: "#a78bfa", + }, + ].map(({ key, label, color }) => ( + <div + key={key} + style={{ + padding: "10px 14px", + borderRadius: 10, + background: "var(--bg-3)", + border: "1px solid var(--border)", + }} + > + <div + style={{ + fontSize: 10, + fontFamily: "DM Mono, monospace", + color: "var(--text-3)", + marginBottom: 6, + }} + > + {label} + </div> + <input + type="text" + value={sensorIds[key]} + onChange={(e) => + setSensorIds((s) => ({ ...s, [key]: e.target.value })) + } + style={{ + width: "100%", + background: "none", + border: "none", + outline: "none", + color, + fontFamily: "DM Mono, monospace", + fontSize: 13, + fontWeight: 700, + }} + /> + </div> + ))} + </div> + <p + style={{ + fontSize: 11, + color: "var(--text-3)", + marginTop: 8, + fontFamily: "DM Mono, monospace", + }} + > + Leave blank to auto-assign, or enter your physical sensor labels. + </p> + </SectionCard> + {/* Auto: cycle duration + stage */} <SectionCard> <div className="section-label" style={{ marginBottom: 12 }}> diff --git a/frontend/src/pages/CropDetails.jsx b/frontend/src/pages/CropDetails.jsx @@ -960,6 +960,7 @@ export default function CropDetails() { const [latest, setLatest] = useState(null); const [cropDoc, setCropDoc] = useState(null); const [loading, setLoading] = useState(true); + const [lastExplainedLog, setLastExplainedLog] = useState(null); const [activeTab, setActiveTab] = useState("details_tab_info"); useEffect(() => { @@ -987,6 +988,15 @@ export default function CropDetails() { })); setHistory(processed); setLatest(processed[processed.length - 1]); + const lastExplained = [...processed] + .reverse() + .find( + (h) => + h.payload?.explanation_log && + h.payload.explanation_log !== "PENDING_ANALYSIS" && + h.payload.explanation_log.trim().length > 0, + ); + setLastExplainedLog(lastExplained?.payload?.explanation_log || null); } if (mongoDoc) setCropDoc(mongoDoc); @@ -1206,7 +1216,7 @@ export default function CropDetails() { strategicIntent={p.strategic_intent} /> )} - <ExplanationLogBlock log={p.explanation_log} t={t} td={td} /> + <ExplanationLogBlock log={lastExplainedLog} t={t} td={td} /> <div style={{ borderRadius: 14, diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx @@ -399,11 +399,11 @@ export default function Dashboard() { if (!name) return null; const n = name.toLowerCase(); if (n.includes("tomato")) - return "https://images.unsplash.com/photo-1591857177580-dc82b9e4e5c9?q=80&w=400"; + return "https://images.unsplash.com/photo-1571680322279-a226e6a4cc2a?q=80&w=400"; if (n.includes("basil")) - return "https://images.unsplash.com/photo-1618164436241-4473940d1f5c?q=80&w=400"; + return "https://images.unsplash.com/photo-1618375569909-3c8616cf7733?q=80&w=400"; if (n.includes("strawberry")) - return "https://images.unsplash.com/photo-1464965911861-746a04b4bca6?q=80&w=400"; + return "https://images.unsplash.com/photo-1677694682771-f2a1eaa7b8d9?q=80&w=400"; // Default: lettuce return "https://images.unsplash.com/photo-1622206151226-18ca2c9ab4a1?q=80&w=400"; }; diff --git a/frontend/src/pages/Help.jsx b/frontend/src/pages/Help.jsx @@ -1085,7 +1085,6 @@ function ManualCard({ scenario, dict }) { function StepCard({ step, dict, index }) { return ( <div - className="card-hover" style={{ padding: "14px 16px", borderRadius: 12, @@ -1099,8 +1098,8 @@ function StepCard({ step, dict, index }) { > <div style={{ - width: 28, - height: 28, + width: 20, + height: 20, borderRadius: 8, background: `${step.color}18`, border: `1px solid ${step.color}40`, @@ -1118,10 +1117,10 @@ function StepCard({ step, dict, index }) { </div> <span style={{ - fontSize: 13, + fontSize: 11, color: "var(--text-2)", lineHeight: 1.6, - paddingTop: 4, + paddingTop: 1, }} > {dict[step.key]} @@ -1133,7 +1132,6 @@ function StepCard({ step, dict, index }) { function CropTypeCard({ crop, dict }) { return ( <div - className="card-hover" style={{ padding: "16px", borderRadius: 14, diff --git a/frontend/src/pages/RunCycle.jsx b/frontend/src/pages/RunCycle.jsx @@ -325,7 +325,7 @@ export default function RunCycle() { ); pushLog( - `[FETCHER] Starting cycle for ${cropIdVal} (${cropName} - ${stage})`, + `Starting cycle for ${cropIdVal} (${cropName} - ${stage})`, "FETCHER", ); @@ -358,11 +358,17 @@ export default function RunCycle() { } try { const parsed = JSON.parse(raw); - if (parsed.log) pushLog(parsed.log); - if (parsed.action) { - setFinalAction(parsed.action); + const msg = parsed.log || parsed.text; + if (msg) pushLog(msg, parsed.agent?.toUpperCase?.()); + if (parsed.action || parsed.final_action) { + setFinalAction(parsed.action || parsed.final_action); pushLog("тЬЕ Final action dispatched to hardware", "SUPERVISOR"); } + if (parsed.phase === "done") { + setPhase("done"); + setCycles((c) => c + 1); + showToast(t("add_cycle_done")); + } } catch { pushLog(raw); } @@ -454,27 +460,33 @@ export default function RunCycle() { {t("run_for_crop", { crop: cropName })} </p> </div> - {phase === "done" && ( - <div + </PageHeader> + + {phase === "done" && ( + <div + style={{ + display: "flex", + alignItems: "center", + justifyContent: "center", + gap: 8, + padding: "8px 16px", + borderBottom: "1px solid rgba(74,222,128,0.15)", + background: "rgba(74,222,128,0.06)", + }} + > + <CheckCircle2 size={13} style={{ color: "var(--green)" }} /> + <span style={{ - marginLeft: "auto", - display: "flex", - alignItems: "center", - gap: 6, - padding: "4px 12px", - borderRadius: 20, - background: "rgba(74,222,128,0.1)", - border: "1px solid rgba(74,222,128,0.3)", - fontSize: 11, + fontSize: 12, fontFamily: "DM Mono, monospace", color: "var(--green)", + fontWeight: 600, }} > - <CheckCircle2 size={12} /> {cycles} cycle{cycles !== 1 ? "s" : ""} completed - </div> - )} - </PageHeader> + </span> + </div> + )} <div style={{ @@ -1011,6 +1023,7 @@ export default function RunCycle() { color: "var(--red)", border: "1px solid rgba(248,113,113,0.3)", animation: "pulse 1.5s ease-in-out infinite", + alignSelf: "center", }} > STREAMING diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx @@ -285,7 +285,6 @@ export default function SettingsPage() { <div style={{ flex: 1, overflowY: "auto", padding: 28 }}> <div style={{ - maxWidth: 720, display: "flex", flexDirection: "column", gap: 24, diff --git a/frontend/src/utils/translations.js b/frontend/src/utils/translations.js @@ -145,7 +145,7 @@ const en = { add_subtitle: "Register a new crop batch in the system", add_plant_image: "PLANT IMAGE", add_drop_image: "Drop crop image here", - add_image_hint: "PNG or JPG ┬╖ optional but helps AI detect disease", + add_image_hint: "PNG or JPG ┬╖ helps AI detect disease", add_sensor_params: "SENSOR READINGS", add_start: "Start Monitoring", add_running: "AI RunningтАж", @@ -626,7 +626,7 @@ const hi = { add_subtitle: "рд╕рд┐рд╕реНрдЯрдо рдореЗрдВ рдПрдХ рдирдИ рдлрд╕рд▓ рджрд░реНрдЬ рдХрд░реЗрдВ", add_plant_image: "рдкреМрдзреЗ рдХреА рддрд╕реНрд╡реАрд░", add_drop_image: "рдлрд╕рд▓ рдХреА рддрд╕реНрд╡реАрд░ рдпрд╣рд╛рдБ рдбрд╛рд▓реЗрдВ", - add_image_hint: "PNG рдпрд╛ JPG ┬╖ рд╡реИрдХрд▓реНрдкрд┐рдХ, рдмреАрдорд╛рд░реА рдкрд╣рдЪрд╛рди рдореЗрдВ рдорджрдж рдХрд░рддрд╛ рд╣реИ", + add_image_hint: "PNG рдпрд╛ JPG ┬╖ рдмреАрдорд╛рд░реА рдкрд╣рдЪрд╛рди рдореЗрдВ рдорджрдж рдХрд░рддрд╛ рд╣реИ", add_sensor_params: "рд╕реЗрдВрд╕рд░ рд░реАрдбрд┐рдВрдЧ", add_start: "рдирд┐рдЧрд░рд╛рдиреА рд╢реБрд░реВ рдХрд░реЗрдВ", add_running: "AI рдХрд╛рдо рдХрд░ рд░рд╣рд╛ рд╣реИтАж", diff --git a/readme.md b/readme.md @@ -185,6 +185,7 @@ The frontend is a highly responsive React 19 SPA featuring bilingual support (En | Dashboard | `/dashboard` | Crop card grid with health/maturity/stage filters and harvest banner | | Crop Details | `/crop/:id` | Per-crop sensor charts, agent reasoning log, event timeline, actuator commands | | Add Crop | `/add-crop` | Form to initialize a crop + live agent pipeline log with 6-phase progress tracker | +| Add Crop | `/add-crop` | Form to initialize a crop + live agent pipeline log with 6-phase progress tracker | | Farm Intelligence | `/intelligence` | Natural-language RAG search against the agronomic knowledge base | | Analytics | `/analytics` | Multi-chart analytics: pH/EC/temp traces, daily sequences, parameter health scores, per-crop table | | Alerts | `/alerts` | Categorized alert system (CRITICAL/WARNING/INFO/HARVEST) with acknowledge workflow | @@ -398,4 +399,6 @@ python agent/main_agent.py --- -<center>Made with тЭдя╕П for the future of sustainable agriculture</center> +<div align="center"> +Made with тЭдя╕П for the future of sustainable agriculture +</div>