commit 6bd43d625c709b1856cc8f8008fb85785f5e608d
parent 50d8ab6e001d505abdc8b1be10b5ca132244616a
Author: maydayv7 <maydayv7@gmail.com>
Date: Sat, 28 Mar 2026 12:26:16 +0530
Misc frontend fixes
Diffstat:
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>