demeter

Autonomous Hydroponic Intelligence

frontend/src/pages/LandingPage.jsx (18658B)


  1import React, { useEffect, useState } from "react";
  2import { useNavigate } from "react-router-dom";
  3import {
  4  Leaf,
  5  ArrowRight,
  6  Cpu,
  7  Database,
  8  Eye,
  9  Zap,
 10  Activity,
 11  Sparkles,
 12} from "lucide-react";
 13import { useFarmData } from "../hooks/useFarmData";
 14import { extractSensors, deriveCropStatus } from "../utils/dataUtils";
 15import { useT } from "../hooks/useTranslation";
 16
 17function computeFleetStats(dashData) {
 18  if (!dashData?.length) return null;
 19  const sensors = dashData.map((d) => extractSensors(d.payload));
 20  const avg = (arr) =>
 21    arr.reduce((s, v) => s + parseFloat(v || 0), 0) / arr.length;
 22
 23  const ph = avg(sensors.map((s) => s.ph));
 24  const ec = avg(sensors.map((s) => s.ec));
 25  const temp = avg(sensors.map((s) => s.temp));
 26  const humidity = avg(sensors.map((s) => s.humidity));
 27
 28  const totalSeqs = dashData.reduce(
 29    (s, d) => s + (d.payload?.sequence_number || 0),
 30    0,
 31  );
 32  const cropTypes = [
 33    ...new Set(dashData.map((d) => d.payload?.crop).filter(Boolean)),
 34  ];
 35
 36  const alerts = dashData.filter((d) => {
 37    const status = deriveCropStatus(d.payload);
 38    return status === "Critical" || status === "Attention";
 39  }).length;
 40
 41  return {
 42    ph: ph.toFixed(2),
 43    ec: ec.toFixed(2),
 44    temp: temp.toFixed(1),
 45    humidity: humidity.toFixed(1),
 46    cropCount: dashData.length,
 47    totalSeqs,
 48    cropTypes,
 49    alerts,
 50  };
 51}
 52
 53function buildActivityLog(historyData) {
 54  if (!historyData?.length) return [];
 55  return historyData
 56    .slice(-5)
 57    .reverse()
 58    .map((d, i) => {
 59      const p = d.payload || {};
 60      const agent = (p.strategic_intent || "SUPERVISOR")
 61        .replace(/_/g, " ")
 62        .split(" ")[0];
 63      const msg = p.strategic_intent
 64        ? `Strategy: ${p.strategic_intent.replace(/_/g, " ")}`
 65        : `Monitoring ${p.crop || "crop"} - seq #${p.sequence_number || 1}`;
 66      const ts = p.timestamp
 67        ? new Date(p.timestamp).toLocaleTimeString([], {
 68            hour: "2-digit",
 69            minute: "2-digit",
 70          })
 71        : `0${i}:0${i}`;
 72      return { agent, msg, time: ts };
 73    });
 74}
 75
 76export default function LandingPage() {
 77  const navigate = useNavigate();
 78  const { t, td } = useT();
 79  const [mounted, setMounted] = useState(false);
 80  const { dashboard, history, loading } = useFarmData();
 81
 82  const [stats, setStats] = useState(null);
 83  const [log, setLog] = useState([]);
 84
 85  useEffect(() => {
 86    setMounted(true);
 87  }, []);
 88
 89  useEffect(() => {
 90    if (!loading) {
 91      setStats(computeFleetStats(dashboard));
 92      setLog(buildActivityLog(history));
 93    }
 94  }, [dashboard, history, loading]);
 95
 96  const FEATURES = [
 97    {
 98      icon: Cpu,
 99      label: t("landing_feature_rl"),
100      desc: t("landing_feature_rl_desc"),
101    },
102    {
103      icon: Eye,
104      label: t("landing_feature_cv"),
105      desc: t("landing_feature_cv_desc"),
106    },
107    {
108      icon: Database,
109      label: t("landing_feature_vector"),
110      desc: t("landing_feature_vector_desc"),
111    },
112    {
113      icon: Zap,
114      label: t("landing_feature_sim"),
115      desc: t("landing_feature_sim_desc"),
116    },
117  ];
118
119  const headlineStats = stats
120    ? [
121        { val: stats.cropCount.toString(), label: t("landing_active_crops") },
122        {
123          val: stats.cropTypes.length.toString(),
124          label: t("landing_crop_types"),
125        },
126        { val: stats.totalSeqs.toString(), label: t("landing_total_cycles") },
127        {
128          val: stats.alerts > 0 ? stats.alerts.toString() : "0",
129          label: t("landing_active_alerts"),
130        },
131      ]
132    : [
133        { val: "-", label: t("landing_active_crops") },
134        { val: "-", label: t("landing_crop_types") },
135        { val: "-", label: t("landing_total_cycles") },
136        { val: "-", label: t("landing_active_alerts") },
137      ];
138
139  // Live sensor readings
140  const readings = stats
141    ? [
142        {
143          label: t("analytics_avg_ph"),
144          value: stats.ph,
145          ok: parseFloat(stats.ph) >= 5.5 && parseFloat(stats.ph) <= 6.5,
146        },
147        {
148          label: t("analytics_avg_ec"),
149          value: `${stats.ec}`,
150          ok: parseFloat(stats.ec) <= 2.5,
151        },
152        {
153          label: t("sensor_temp").toUpperCase(),
154          value: `${stats.temp}°C`,
155          ok: parseFloat(stats.temp) >= 18 && parseFloat(stats.temp) <= 30,
156        },
157        {
158          label: t("sensor_humidity").toUpperCase(),
159          value: `${stats.humidity}%`,
160          ok:
161            parseFloat(stats.humidity) >= 40 &&
162            parseFloat(stats.humidity) <= 80,
163        },
164      ]
165    : [
166        { label: t("analytics_avg_ph"), value: "-", ok: true },
167        { label: t("analytics_avg_ec"), value: "-", ok: true },
168        { label: t("sensor_temp").toUpperCase(), value: "-", ok: true },
169        { label: t("sensor_humidity").toUpperCase(), value: "-", ok: true },
170      ];
171
172  return (
173    <div
174      className="min-h-screen grid-bg relative overflow-hidden"
175      style={{ background: "var(--bg)" }}
176    >
177      <div className="scanline" />
178
179      {/* Ambient glow */}
180      <div
181        className="absolute top-0 left-1/2 -translate-x-1/2 w-[800px] h-[400px] pointer-events-none"
182        style={{
183          background:
184            "radial-gradient(ellipse at center, rgba(74,222,128,0.06) 0%, transparent 70%)",
185        }}
186      />
187
188      {/* Nav */}
189      <nav
190        className="relative z-10 flex items-center justify-between px-8 py-5 border-b"
191        style={{ borderColor: "rgba(74,222,128,0.1)" }}
192      >
193        <div className="flex items-center gap-3">
194          <div
195            className="w-9 h-9 rounded-xl flex items-center justify-center"
196            style={{ background: "linear-gradient(135deg, #1a5c2d, #4ade80)" }}
197          >
198            <Leaf size={18} fill="white" color="white" />
199          </div>
200          <div>
201            <div
202              className="font-bold text-base tracking-tight"
203              style={{ color: "var(--text)" }}
204            >
205              DEMETER
206            </div>
207            <div
208              className="text-[9px] font-mono tracking-[0.2em]"
209              style={{ color: "var(--text-3)" }}
210            >
211              {td("AUTONOMOUS FARM INTELLIGENCE")}
212            </div>
213          </div>
214        </div>
215
216        <div className="flex items-center gap-4">
217          <div
218            className="flex items-center gap-2 text-[11px] font-mono px-3 py-1.5 rounded-full"
219            style={{
220              border: "1px solid rgba(74,222,128,0.3)",
221              color: "var(--green)",
222              background: "rgba(74,222,128,0.05)",
223            }}
224          >
225            <span
226              className="status-dot w-1.5 h-1.5 rounded-full"
227              style={{ background: "var(--green)" }}
228            />
229            {loading
230              ? t("sidebar_connecting")
231              : stats
232                ? t("sidebar_farm_online")
233                : t("sidebar_no_data")}
234          </div>
235          <button
236            onClick={() => navigate("/dashboard")}
237            className="px-4 py-2 rounded-lg text-sm font-semibold transition-all"
238            style={{
239              background: "var(--surface)",
240              color: "var(--text-2)",
241              border: "1px solid var(--border)",
242            }}
243          >
244            {t("landing_enter_dash")}
245          </button>
246        </div>
247      </nav>
248
249      {/* Hero */}
250      <div className="relative z-10 max-w-7xl mx-auto px-8 pt-24 pb-16">
251        <div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
252          {/* Left */}
253          <div
254            className={`space-y-8 ${mounted ? "animate-fade-up" : "opacity-0"}`}
255          >
256            <div
257              className="inline-flex items-center gap-2 text-[11px] font-mono px-3 py-1.5 rounded-full"
258              style={{
259                border: "1px solid rgba(245,158,11,0.4)",
260                color: "var(--amber)",
261                background: "rgba(245,158,11,0.06)",
262              }}
263            >
264              <Zap size={10} fill="currentColor" />
265              {td("MULTI-AGENT SYSTEM · LANGGRAPH · AZURE")}
266            </div>
267
268            <h1
269              className="text-6xl lg:text-7xl font-bold leading-[1.0] tracking-tight"
270              style={{ color: "var(--text)" }}
271            >
272              {t("landing_hero_1")}
273              <br />
274              <span
275                className="font-serif italic"
276                style={{ color: "var(--green)" }}
277              >
278                {t("landing_hero_2")}
279              </span>
280              <br />
281              {t("landing_hero_3")}
282            </h1>
283
284            <p
285              className="text-lg leading-relaxed max-w-md"
286              style={{ color: "var(--text-2)", fontWeight: 300 }}
287            >
288              {t("landing_hero_sub")}
289            </p>
290
291            <div className="flex gap-4">
292              <button
293                onClick={() => navigate("/dashboard")}
294                className="group flex items-center gap-3 px-7 py-3.5 rounded-xl font-semibold text-sm transition-all glow-green"
295                style={{ background: "var(--green)", color: "#0c1a0e" }}
296              >
297                {t("landing_enter_dash")}{" "}
298                <ArrowRight
299                  size={16}
300                  className="group-hover:translate-x-1 transition-transform"
301                />
302              </button>
303              <button
304                onClick={() => navigate("/intelligence")}
305                className="flex items-center gap-3 px-7 py-3.5 rounded-xl font-semibold text-sm transition-all"
306                style={{
307                  border: "1px solid var(--border)",
308                  color: "var(--text-2)",
309                  background: "var(--surface)",
310                }}
311              >
312                <Sparkles size={16} /> {t("landing_intelligence")}
313              </button>
314            </div>
315
316            {/* Live headline stats from DB */}
317            <div
318              className="grid grid-cols-4 gap-4 pt-4 border-t"
319              style={{ borderColor: "var(--border)" }}
320            >
321              {headlineStats.map(({ val, label }) => (
322                <div key={label}>
323                  <div
324                    className="text-2xl font-bold font-mono"
325                    style={{ color: "var(--green)" }}
326                  >
327                    {loading ? (
328                      <span className="inline-block w-8 h-6 rounded shimmer" />
329                    ) : (
330                      val
331                    )}
332                  </div>
333                  <div
334                    className="text-[11px] mt-0.5"
335                    style={{ color: "var(--text-3)" }}
336                  >
337                    {label}
338                  </div>
339                </div>
340              ))}
341            </div>
342          </div>
343
344          {/* Right - live HUD */}
345          <div
346            className={`relative ${mounted ? "animate-fade-in" : "opacity-0"}`}
347          >
348            <div
349              className="rounded-2xl overflow-hidden"
350              style={{
351                border: "1px solid var(--border)",
352                background: "var(--bg-2)",
353              }}
354            >
355              {/* Terminal header */}
356              <div
357                className="flex items-center gap-2 px-4 py-3 border-b"
358                style={{
359                  borderColor: "var(--border)",
360                  background: "var(--bg-3)",
361                }}
362              >
363                <div
364                  className="w-3 h-3 rounded-full"
365                  style={{ background: "#ff5f57" }}
366                />
367                <div
368                  className="w-3 h-3 rounded-full"
369                  style={{ background: "#ffbd2e" }}
370                />
371                <div
372                  className="w-3 h-3 rounded-full"
373                  style={{ background: "#28ca41" }}
374                />
375                <span
376                  className="ml-2 text-[11px] font-mono"
377                  style={{ color: "var(--text-3)" }}
378                >
379                  {t("landing_live_feed")}
380                </span>
381                <Activity
382                  size={11}
383                  className="ml-auto"
384                  style={{ color: "var(--green)" }}
385                />
386              </div>
387
388              <div className="p-6 space-y-5">
389                {/* Live sensor grid */}
390                <div className="grid grid-cols-2 gap-3">
391                  {readings.map(({ label, value, ok }) => (
392                    <div
393                      key={label}
394                      className="rounded-xl p-4"
395                      style={{
396                        background: "var(--surface)",
397                        border: "1px solid var(--border)",
398                      }}
399                    >
400                      <div
401                        className="text-[10px] font-mono mb-2"
402                        style={{ color: "var(--text-3)" }}
403                      >
404                        {label}
405                      </div>
406                      {loading ? (
407                        <div className="h-7 w-16 rounded shimmer" />
408                      ) : (
409                        <div
410                          className="text-2xl font-mono font-bold"
411                          style={{ color: ok ? "var(--green)" : "var(--red)" }}
412                        >
413                          {value}
414                        </div>
415                      )}
416                      <div
417                        className="text-[9px] font-mono mt-1"
418                        style={{
419                          color: ok
420                            ? "rgba(74,222,128,0.6)"
421                            : "rgba(248,113,113,0.6)",
422                        }}
423                      >
424                        {ok ? t("landing_optimal") : t("landing_alert")}
425                      </div>
426                    </div>
427                  ))}
428                </div>
429
430                {/* Agent activity feed */}
431                <div
432                  className="rounded-xl p-4"
433                  style={{
434                    background: "var(--surface)",
435                    border: "1px solid var(--border)",
436                  }}
437                >
438                  <div
439                    className="text-[10px] font-mono mb-3"
440                    style={{ color: "var(--text-3)" }}
441                  >
442                    {t("landing_recent_activity")}
443                  </div>
444                  <div className="space-y-2">
445                    {loading ? (
446                      [1, 2, 3].map((i) => (
447                        <div key={i} className="h-4 rounded shimmer" />
448                      ))
449                    ) : log.length > 0 ? (
450                      log.slice(0, 3).map(({ agent, msg, time }, i) => (
451                        <div
452                          key={i}
453                          className="flex items-start gap-2 text-[11px] font-mono"
454                        >
455                          <span style={{ color: "var(--green)", opacity: 0.6 }}>
456                            {time}
457                          </span>
458                          <span
459                            className="px-1.5 py-0.5 rounded text-[9px]"
460                            style={{
461                              background: "rgba(74,222,128,0.12)",
462                              color: "var(--green)",
463                              whiteSpace: "nowrap",
464                            }}
465                          >
466                            {td(agent.substring(0, 12))}
467                          </span>
468                          <span style={{ color: "var(--text-2)" }}>
469                            {td(msg.substring(0, 40))}
470                            {msg.length > 40 ? "…" : ""}
471                          </span>
472                        </div>
473                      ))
474                    ) : (
475                      <div
476                        className="text-[11px] font-mono"
477                        style={{ color: "var(--text-3)" }}
478                      >
479                        {t("landing_no_cycles")}
480                      </div>
481                    )}
482                    <div
483                      className="flex items-center gap-1 text-[11px] font-mono cursor-blink"
484                      style={{ color: "var(--green)" }}
485                    >
486                      <span style={{ opacity: 0.4 }}>→ </span>
487                    </div>
488                  </div>
489                </div>
490              </div>
491            </div>
492
493            {/* Live crop count chip */}
494            {stats && (
495              <div
496                className="absolute -top-4 -right-4 px-3 py-2 rounded-lg text-[11px] font-mono"
497                style={{
498                  background: "var(--surface-2)",
499                  border: "1px solid var(--border)",
500                  color: "var(--amber)",
501                }}
502              >
503                ⬆ {stats.cropCount} {t("common_crop").toLowerCase()}
504                {stats.cropCount !== 1 ? "s" : ""}
505              </div>
506            )}
507          </div>
508        </div>
509
510        {/* Feature grid */}
511        <div
512          className="mt-24 pt-12 border-t"
513          style={{ borderColor: "var(--border)" }}
514        >
515          <div
516            className="text-[11px] font-mono mb-8"
517            style={{ color: "var(--text-3)" }}
518          >
519            {t("landing_capabilities")}
520          </div>
521          <div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
522            {FEATURES.map(({ icon: Icon, label, desc }) => (
523              <div
524                key={label}
525                className="p-5 rounded-xl card-hover"
526                style={{
527                  background: "var(--surface)",
528                  border: "1px solid var(--border)",
529                }}
530              >
531                <div
532                  className="w-9 h-9 rounded-lg flex items-center justify-center mb-4"
533                  style={{
534                    background: "rgba(74,222,128,0.1)",
535                    border: "1px solid rgba(74,222,128,0.2)",
536                  }}
537                >
538                  <Icon size={18} style={{ color: "var(--green)" }} />
539                </div>
540                <div
541                  className="font-semibold text-sm mb-1"
542                  style={{ color: "var(--text)" }}
543                >
544                  {label}
545                </div>
546                <div
547                  className="text-xs leading-relaxed"
548                  style={{ color: "var(--text-3)" }}
549                >
550                  {desc}
551                </div>
552              </div>
553            ))}
554          </div>
555        </div>
556      </div>
557    </div>
558  );
559}