demeter

Autonomous Hydroponic Intelligence

frontend/src/pages/CropDetails.jsx (46767B)


   1import React, { useEffect, useState } from "react";
   2import { useParams, useNavigate } from "react-router-dom";
   3import { fetchCropDetails, fetchCropById } from "../api/farmApi";
   4import {
   5  ArrowLeft,
   6  Thermometer,
   7  Droplet,
   8  Wind,
   9  Activity,
  10  Brain,
  11  ChevronDown,
  12  ChevronUp,
  13  Sparkles,
  14  MapPin,
  15  Calendar,
  16  Clock,
  17  ChevronRight,
  18  Cpu,
  19  StickyNote,
  20  Play,
  21  Leaf,
  22} from "lucide-react";
  23import {
  24  AreaChart,
  25  Area,
  26  LineChart,
  27  Line,
  28  XAxis,
  29  YAxis,
  30  CartesianGrid,
  31  Tooltip,
  32  ResponsiveContainer,
  33} from "recharts";
  34import {
  35  extractSensors,
  36  parsePythonString,
  37  formatNumber,
  38  calculateMaturity,
  39  getDaysRemaining,
  40  getCurrentStage,
  41  getEffectiveElapsedHours,
  42  CROP_LIFECYCLES,
  43  CROP_CYCLE_HOURS,
  44} from "../utils/dataUtils";
  45import {
  46  AgentActionWidget,
  47  AgentOutcomeWidget,
  48} from "../components/AgentWidgets";
  49import {
  50  PageShell,
  51  PageHeader,
  52  IconButton,
  53  ChartTooltip,
  54  InlineLabel,
  55} from "../components/ui";
  56import { useSettings } from "../hooks/useSettings";
  57import { useT } from "../hooks/useTranslation";
  58
  59// Sub-components
  60
  61function StatBox({ icon: Icon, label, value, color, unit }) {
  62  return (
  63    <div
  64      className="card-hover"
  65      style={{
  66        borderRadius: 14,
  67        padding: "18px 20px",
  68        background: "var(--surface)",
  69        border: "1px solid var(--border)",
  70      }}
  71    >
  72      <div
  73        style={{
  74          display: "flex",
  75          alignItems: "center",
  76          gap: 8,
  77          marginBottom: 12,
  78        }}
  79      >
  80        <div
  81          style={{
  82            width: 32,
  83            height: 32,
  84            borderRadius: 8,
  85            background: `${color}18`,
  86            border: `1px solid ${color}30`,
  87            display: "flex",
  88            alignItems: "center",
  89            justifyContent: "center",
  90          }}
  91        >
  92          <Icon size={14} style={{ color }} />
  93        </div>
  94        <span className="sensor-label">{label}</span>
  95      </div>
  96      <div style={{ display: "flex", alignItems: "baseline", gap: 4 }}>
  97        <span className="sensor-value" style={{ color }}>
  98          {value}
  99        </span>
 100        {unit && <span className="sensor-unit">{unit}</span>}
 101      </div>
 102    </div>
 103  );
 104}
 105
 106// Severity dot for logs
 107function logDotColor(payload) {
 108  const outcome = (payload?.outcome || "").toLowerCase();
 109  if (/fail|critical|disease|error/.test(outcome)) return "var(--red)";
 110  if (/deteriorat|negative|attention/.test(outcome)) return "var(--amber)";
 111  return "var(--green)";
 112}
 113
 114function ExplanationLogBlock({ log, t, td }) {
 115  const [expanded, setExpanded] = useState(false);
 116
 117  const isPending =
 118    !log || log === "PENDING_ANALYSIS" || log.trim().length === 0;
 119
 120  if (isPending) {
 121    return (
 122      <div
 123        style={{
 124          borderRadius: 14,
 125          padding: "18px 20px",
 126          background: "var(--surface)",
 127          border: "1px solid var(--border)",
 128        }}
 129      >
 130        <div
 131          style={{
 132            display: "flex",
 133            alignItems: "center",
 134            gap: 10,
 135            marginBottom: 12,
 136          }}
 137        >
 138          <div
 139            style={{
 140              width: 32,
 141              height: 32,
 142              borderRadius: 8,
 143              background: "rgba(167,139,250,0.08)",
 144              border: "1px solid rgba(167,139,250,0.15)",
 145              display: "flex",
 146              alignItems: "center",
 147              justifyContent: "center",
 148            }}
 149          >
 150            <Brain size={14} style={{ color: "#a78bfa" }} />
 151          </div>
 152          <InlineLabel style={{ marginBottom: 0 }}>
 153            {t("details_ai_reasoning")}
 154          </InlineLabel>
 155          <span
 156            style={{
 157              fontSize: 9,
 158              fontFamily: "DM Mono, monospace",
 159              padding: "2px 8px",
 160              borderRadius: 20,
 161              background: "var(--bg-3)",
 162              border: "1px solid var(--border)",
 163              color: "var(--text-3)",
 164            }}
 165          >
 166            PENDING
 167          </span>
 168        </div>
 169        <div
 170          style={{
 171            fontSize: 12,
 172            fontFamily: "DM Mono, monospace",
 173            color: "var(--text-3)",
 174            fontStyle: "italic",
 175          }}
 176        >
 177          {t("details_pending_exp")}
 178        </div>
 179      </div>
 180    );
 181  }
 182
 183  const translatedLog = td(log);
 184  const lines = translatedLog.split("\n").filter((l) => l.trim());
 185  const isStructured = lines.some((l) => /^\d+\./.test(l.trim()));
 186
 187  // Preview: first 3 lines
 188  const previewLines = lines.slice(0, 3);
 189  const hasMore = lines.length > 3;
 190
 191  return (
 192    <div
 193      style={{
 194        borderRadius: 14,
 195        background: "var(--surface)",
 196        border: "1px solid rgba(167,139,250,0.2)",
 197        overflow: "hidden",
 198        flexShrink: 0,
 199      }}
 200    >
 201      {/* Header */}
 202      <div
 203        style={{
 204          padding: "14px 20px",
 205          borderBottom: "1px solid var(--border)",
 206          background: "rgba(167,139,250,0.05)",
 207          display: "flex",
 208          alignItems: "center",
 209          gap: 10,
 210        }}
 211      >
 212        <div
 213          style={{
 214            width: 28,
 215            height: 28,
 216            borderRadius: 7,
 217            background: "rgba(167,139,250,0.12)",
 218            border: "1px solid rgba(167,139,250,0.25)",
 219            display: "flex",
 220            alignItems: "center",
 221            justifyContent: "center",
 222            flexShrink: 0,
 223          }}
 224        >
 225          <Sparkles size={12} style={{ color: "#a78bfa" }} />
 226        </div>
 227        <div style={{ flex: 1 }}>
 228          <InlineLabel style={{ marginBottom: 0 }}>
 229            {t("details_ai_reasoning")}
 230          </InlineLabel>
 231          <div
 232            style={{
 233              fontSize: 10,
 234              fontFamily: "DM Mono, monospace",
 235              color: "var(--text-3)",
 236              marginTop: 2,
 237            }}
 238          >
 239            {t("details_chain_thought")}
 240          </div>
 241        </div>
 242        {hasMore && (
 243          <button
 244            onClick={() => setExpanded((e) => !e)}
 245            style={{
 246              display: "flex",
 247              alignItems: "center",
 248              gap: 5,
 249              padding: "4px 10px",
 250              borderRadius: 7,
 251              fontSize: 10,
 252              fontFamily: "DM Mono, monospace",
 253              cursor: "pointer",
 254              background: "var(--bg-3)",
 255              border: "1px solid var(--border)",
 256              color: "var(--text-3)",
 257            }}
 258          >
 259            {expanded ? (
 260              <>
 261                <ChevronUp size={11} /> {t("details_collapse")}
 262              </>
 263            ) : (
 264              <>
 265                <ChevronDown size={11} /> {t("details_expand")}
 266              </>
 267            )}
 268          </button>
 269        )}
 270      </div>
 271
 272      {/* Content */}
 273      <div style={{ padding: "16px 20px" }}>
 274        {isStructured ? (
 275          // Render numbered steps as styled blocks
 276          <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
 277            {(expanded ? lines : previewLines).map((line, i) => {
 278              const stepMatch = line
 279                .trim()
 280                .match(/^(\d+)\.\s+\*\*(.+?)\*\*:?\s*(.*)/);
 281
 282              if (stepMatch) {
 283                const [, num, title, body] = stepMatch;
 284                const stepColors = [
 285                  "var(--blue)",
 286                  "#a78bfa",
 287                  "var(--amber)",
 288                  "var(--green)",
 289                ];
 290                const col = stepColors[(parseInt(num) - 1) % stepColors.length];
 291                return (
 292                  <div
 293                    key={i}
 294                    style={{
 295                      display: "flex",
 296                      gap: 12,
 297                      padding: "10px 14px",
 298                      borderRadius: 10,
 299                      background: "var(--bg-3)",
 300                      border: `1px solid ${col}18`,
 301                    }}
 302                  >
 303                    <div
 304                      style={{
 305                        width: 22,
 306                        height: 22,
 307                        borderRadius: "50%",
 308                        background: `${col}18`,
 309                        border: `1px solid ${col}40`,
 310                        display: "flex",
 311                        alignItems: "center",
 312                        justifyContent: "center",
 313                        flexShrink: 0,
 314                        fontSize: 10,
 315                        fontFamily: "DM Mono, monospace",
 316                        color: col,
 317                        fontWeight: 700,
 318                      }}
 319                    >
 320                      {num}
 321                    </div>
 322                    <div>
 323                      <div
 324                        style={{
 325                          fontSize: 12,
 326                          fontWeight: 700,
 327                          color: col,
 328                          fontFamily: "DM Mono, monospace",
 329                          marginBottom: 3,
 330                        }}
 331                      >
 332                        {title}
 333                      </div>
 334                      {body && (
 335                        <div
 336                          style={{
 337                            fontSize: 12,
 338                            fontFamily: "DM Mono, monospace",
 339                            color: "var(--text-2)",
 340                            lineHeight: 1.7,
 341                          }}
 342                        >
 343                          {body}
 344                        </div>
 345                      )}
 346                    </div>
 347                  </div>
 348                );
 349              }
 350
 351              // Regular line
 352              return (
 353                <div
 354                  key={i}
 355                  style={{
 356                    fontSize: 12,
 357                    fontFamily: "DM Mono, monospace",
 358                    color: "var(--text-2)",
 359                    lineHeight: 1.7,
 360                    padding: "2px 0",
 361                  }}
 362                >
 363                  {line}
 364                </div>
 365              );
 366            })}
 367
 368            {!expanded && hasMore && (
 369              <button
 370                onClick={() => setExpanded(true)}
 371                style={{
 372                  alignSelf: "flex-start",
 373                  padding: "5px 12px",
 374                  borderRadius: 7,
 375                  fontSize: 11,
 376                  fontFamily: "DM Mono, monospace",
 377                  cursor: "pointer",
 378                  background: "rgba(167,139,250,0.08)",
 379                  border: "1px solid rgba(167,139,250,0.2)",
 380                  color: "#a78bfa",
 381                }}
 382              >
 383                {t("details_more_lines", {
 384                  n: lines.length - previewLines.length,
 385                })}
 386              </button>
 387            )}
 388          </div>
 389        ) : (
 390          // Plain text fallback
 391          <pre
 392            style={{
 393              fontSize: 12,
 394              fontFamily: "DM Mono, monospace",
 395              color: "var(--text-2)",
 396              lineHeight: 1.75,
 397              whiteSpace: "pre-wrap",
 398              margin: 0,
 399              maxHeight: expanded ? "none" : 140,
 400              overflow: expanded ? "visible" : "hidden",
 401            }}
 402          >
 403            {translatedLog}
 404          </pre>
 405        )}
 406      </div>
 407    </div>
 408  );
 409}
 410
 411function InfoTab({ cropDoc, cropId, navigate, t }) {
 412  if (!cropDoc) {
 413    return (
 414      <div
 415        style={{
 416          display: "flex",
 417          alignItems: "center",
 418          justifyContent: "center",
 419          padding: 48,
 420        }}
 421      >
 422        <span
 423          style={{
 424            fontSize: 13,
 425            fontFamily: "DM Mono, monospace",
 426            color: "var(--text-3)",
 427          }}
 428        >
 429          No metadata available yet - run the first cycle to populate data.
 430        </span>
 431      </div>
 432    );
 433  }
 434
 435  const maturity = calculateMaturity(cropDoc);
 436  const daysLeft = getDaysRemaining(cropDoc);
 437  const stage = getCurrentStage(cropDoc) || cropDoc.stage || "seedling";
 438  const cycleH =
 439    cropDoc.cycle_duration_hours ||
 440    CROP_CYCLE_HOURS[(cropDoc.crop || "").toLowerCase()] ||
 441    1;
 442  const lifecycle = CROP_LIFECYCLES[(cropDoc.crop || "").toLowerCase()];
 443
 444  const plantedDate = cropDoc.planted_at
 445    ? new Date(cropDoc.planted_at).toLocaleDateString(undefined, {
 446        year: "numeric",
 447        month: "short",
 448        day: "numeric",
 449      })
 450    : "-";
 451  const daysSince = Math.floor(getEffectiveElapsedHours(cropDoc) / 24) || null;
 452
 453  const sensorIds = cropDoc.sensor_ids || {};
 454  const SENSORS_DISPLAY = [
 455    {
 456      label: "pH Sensor",
 457      id: sensorIds.ph_sensor || "PH-SEN-????",
 458      color: "var(--green)",
 459    },
 460    {
 461      label: "EC Sensor",
 462      id: sensorIds.ec_sensor || "EC-SEN-????",
 463      color: "var(--amber)",
 464    },
 465    {
 466      label: "Temp Sensor",
 467      id: sensorIds.temp_sensor || "TMP-SEN-????",
 468      color: "var(--blue)",
 469    },
 470    {
 471      label: "Humidity Sensor",
 472      id: sensorIds.humidity_sensor || "HUM-SEN-????",
 473      color: "#a78bfa",
 474    },
 475  ];
 476
 477  return (
 478    <div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
 479      {/* Crop header */}
 480      <div
 481        style={{
 482          display: "flex",
 483          alignItems: "center",
 484          gap: 16,
 485          padding: 20,
 486          borderRadius: 16,
 487          background: "var(--surface)",
 488          border: "1px solid var(--border)",
 489        }}
 490      >
 491        <div
 492          style={{
 493            width: 64,
 494            height: 64,
 495            borderRadius: 16,
 496            background: "rgba(74,222,128,0.1)",
 497            border: "1px solid rgba(74,222,128,0.25)",
 498            display: "flex",
 499            alignItems: "center",
 500            justifyContent: "center",
 501            flexShrink: 0,
 502          }}
 503        >
 504          <Leaf size={28} style={{ color: "var(--green)" }} />
 505        </div>
 506        <div style={{ flex: 1 }}>
 507          <div
 508            style={{
 509              fontWeight: 700,
 510              fontSize: 18,
 511              color: "var(--text)",
 512              marginBottom: 4,
 513            }}
 514          >
 515            {cropDoc.crop}
 516          </div>
 517          <div
 518            style={{
 519              fontSize: 11,
 520              fontFamily: "DM Mono, monospace",
 521              color: "var(--text-3)",
 522            }}
 523          >
 524            {cropId}
 525          </div>
 526          <div
 527            style={{
 528              marginTop: 8,
 529              display: "flex",
 530              alignItems: "center",
 531              gap: 8,
 532            }}
 533          >
 534            <span
 535              style={{
 536                fontSize: 10,
 537                fontFamily: "DM Mono, monospace",
 538                padding: "2px 10px",
 539                borderRadius: 20,
 540                background: "rgba(74,222,128,0.12)",
 541                border: "1px solid rgba(74,222,128,0.3)",
 542                color: "var(--green)",
 543                textTransform: "capitalize",
 544              }}
 545            >
 546              {stage}
 547            </span>
 548            {cycleH && (
 549              <span
 550                style={{
 551                  fontSize: 10,
 552                  fontFamily: "DM Mono, monospace",
 553                  color: "var(--text-3)",
 554                }}
 555              >
 556                {t("details_hours_per_cycle", { n: cycleH })}
 557              </span>
 558            )}
 559          </div>
 560        </div>
 561      </div>
 562
 563      {/* Details grid */}
 564      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
 565        {[
 566          {
 567            icon: Calendar,
 568            label: t("details_planted"),
 569            value: plantedDate,
 570            color: "var(--blue)",
 571          },
 572          {
 573            icon: Clock,
 574            label: t("details_cycle_duration"),
 575            value: `${cycleH}h / cycle`,
 576            color: "var(--green)",
 577          },
 578          {
 579            icon: Leaf,
 580            label: t("details_lifecycle_progress"),
 581            value: `${maturity}%`,
 582            color: "var(--amber)",
 583          },
 584          {
 585            icon: MapPin,
 586            label: t("details_location"),
 587            value: cropDoc.location || "-",
 588            color: "var(--text-2)",
 589          },
 590        ].map(({ icon, label, value, color }) => (
 591          <div
 592            key={label}
 593            style={{
 594              padding: "14px 16px",
 595              borderRadius: 12,
 596              background: "var(--surface)",
 597              border: "1px solid var(--border)",
 598            }}
 599          >
 600            <div
 601              style={{
 602                display: "flex",
 603                alignItems: "center",
 604                gap: 7,
 605                marginBottom: 8,
 606              }}
 607            >
 608              {React.createElement(icon, { size: 12, style: { color } })}
 609              <span
 610                style={{
 611                  fontSize: 10,
 612                  fontFamily: "DM Mono, monospace",
 613                  color: "var(--text-3)",
 614                }}
 615              >
 616                {label}
 617              </span>
 618            </div>
 619            <div
 620              style={{
 621                fontSize: 14,
 622                fontWeight: 700,
 623                color,
 624                fontFamily: "DM Mono, monospace",
 625              }}
 626            >
 627              {value}
 628            </div>
 629          </div>
 630        ))}
 631
 632        {/* Days since / remaining */}
 633        <div
 634          style={{
 635            padding: "14px 16px",
 636            borderRadius: 12,
 637            background: "var(--surface)",
 638            border: "1px solid var(--border)",
 639          }}
 640        >
 641          <div
 642            style={{
 643              fontSize: 10,
 644              fontFamily: "DM Mono, monospace",
 645              color: "var(--text-3)",
 646              marginBottom: 8,
 647            }}
 648          >
 649            {t("details_days_since", { n: daysSince ?? "-" })}
 650          </div>
 651          <div
 652            style={{
 653              fontSize: 14,
 654              fontWeight: 700,
 655              fontFamily: "DM Mono, monospace",
 656              color: "var(--text-2)",
 657            }}
 658          >
 659            {daysLeft !== null
 660              ? t("details_days_remain", { n: daysLeft })
 661              : "-"}
 662          </div>
 663        </div>
 664
 665        <div
 666          style={{
 667            padding: "14px 16px",
 668            borderRadius: 12,
 669            background: "var(--surface)",
 670            border: "1px solid var(--border)",
 671          }}
 672        >
 673          <div
 674            style={{
 675              fontSize: 10,
 676              fontFamily: "DM Mono, monospace",
 677              color: "var(--text-3)",
 678              marginBottom: 8,
 679            }}
 680          >
 681            {t("details_total_sequences")}
 682          </div>
 683          <div
 684            style={{
 685              fontSize: 22,
 686              fontWeight: 700,
 687              fontFamily: "DM Mono, monospace",
 688              color: "var(--text)",
 689            }}
 690          >
 691            {cropDoc.sequence_number || 0}
 692          </div>
 693        </div>
 694      </div>
 695
 696      {/* Lifecycle progress bar */}
 697      {lifecycle && (
 698        <div
 699          style={{
 700            padding: 16,
 701            borderRadius: 14,
 702            background: "var(--surface)",
 703            border: "1px solid var(--border)",
 704          }}
 705        >
 706          <div
 707            style={{
 708              fontSize: 10,
 709              fontFamily: "DM Mono, monospace",
 710              color: "var(--text-3)",
 711              marginBottom: 10,
 712            }}
 713          >
 714            {t("details_lifecycle_progress")} - {lifecycle.totalDays} days total
 715          </div>
 716          <div
 717            style={{
 718              height: 8,
 719              borderRadius: 4,
 720              background: "var(--border)",
 721              overflow: "hidden",
 722              marginBottom: 8,
 723            }}
 724          >
 725            <div
 726              style={{
 727                width: `${maturity}%`,
 728                height: "100%",
 729                borderRadius: 4,
 730                background: maturity >= 80 ? "var(--amber)" : "var(--green)",
 731                transition: "width 0.4s",
 732              }}
 733            />
 734          </div>
 735          <div style={{ display: "flex", gap: 12, flexWrap: "wrap" }}>
 736            {lifecycle.stages.map((s, i) => {
 737              const colors = [
 738                "var(--text-3)",
 739                "var(--green)",
 740                "var(--amber)",
 741                "var(--red)",
 742              ];
 743              return (
 744                <div
 745                  key={s.name}
 746                  style={{ display: "flex", alignItems: "center", gap: 5 }}
 747                >
 748                  <span
 749                    style={{
 750                      width: 8,
 751                      height: 8,
 752                      borderRadius: "50%",
 753                      background: colors[i % colors.length],
 754                      display: "inline-block",
 755                    }}
 756                  />
 757                  <span
 758                    style={{
 759                      fontSize: 10,
 760                      fontFamily: "DM Mono, monospace",
 761                      color: "var(--text-3)",
 762                      textTransform: "capitalize",
 763                    }}
 764                  >
 765                    {s.name} (
 766                    {s.endH
 767                      ? `${Math.round((s.endH - s.startH) / 24)}d`
 768                      : "harvest"}
 769                    )
 770                  </span>
 771                </div>
 772              );
 773            })}
 774          </div>
 775        </div>
 776      )}
 777
 778      {/* Sensor Hardware */}
 779      <div
 780        style={{
 781          borderRadius: 14,
 782          overflow: "hidden",
 783          background: "var(--surface)",
 784          border: "1px solid var(--border)",
 785        }}
 786      >
 787        <div
 788          style={{
 789            padding: "12px 16px",
 790            borderBottom: "1px solid var(--border)",
 791            background: "var(--bg-3)",
 792            display: "flex",
 793            alignItems: "center",
 794            gap: 8,
 795          }}
 796        >
 797          <Cpu size={13} style={{ color: "var(--text-3)" }} />
 798          <span
 799            style={{
 800              fontSize: 10,
 801              fontFamily: "DM Mono, monospace",
 802              color: "var(--text-3)",
 803              letterSpacing: "0.08em",
 804            }}
 805          >
 806            {t("details_sensor_hardware")}
 807          </span>
 808        </div>
 809        <div
 810          style={{
 811            display: "grid",
 812            gridTemplateColumns: "1fr 1fr",
 813            gap: 1,
 814            background: "var(--border)",
 815          }}
 816        >
 817          {SENSORS_DISPLAY.map(({ label, id, color }) => (
 818            <div
 819              key={label}
 820              style={{ padding: "14px 16px", background: "var(--surface)" }}
 821            >
 822              <div
 823                style={{
 824                  fontSize: 10,
 825                  fontFamily: "DM Mono, monospace",
 826                  color: "var(--text-3)",
 827                  marginBottom: 6,
 828                }}
 829              >
 830                {label}
 831              </div>
 832              <div
 833                style={{
 834                  fontSize: 13,
 835                  fontFamily: "DM Mono, monospace",
 836                  fontWeight: 700,
 837                  color,
 838                  marginBottom: 4,
 839                }}
 840              >
 841                {id}
 842              </div>
 843              <div style={{ display: "flex", alignItems: "center", gap: 5 }}>
 844                <span
 845                  style={{
 846                    width: 5,
 847                    height: 5,
 848                    borderRadius: "50%",
 849                    background: "var(--green)",
 850                    display: "inline-block",
 851                  }}
 852                />
 853                <span
 854                  style={{
 855                    fontSize: 9,
 856                    fontFamily: "DM Mono, monospace",
 857                    color: "var(--green)",
 858                  }}
 859                >
 860                  {t("details_sensor_online")}
 861                </span>
 862              </div>
 863            </div>
 864          ))}
 865        </div>
 866      </div>
 867
 868      {/* Notes */}
 869      {cropDoc.notes && (
 870        <div
 871          style={{
 872            padding: 16,
 873            borderRadius: 14,
 874            background: "var(--surface)",
 875            border: "1px solid var(--border)",
 876          }}
 877        >
 878          <div
 879            style={{
 880              display: "flex",
 881              alignItems: "center",
 882              gap: 8,
 883              marginBottom: 10,
 884            }}
 885          >
 886            <StickyNote size={13} style={{ color: "var(--text-3)" }} />
 887            <span
 888              style={{
 889                fontSize: 10,
 890                fontFamily: "DM Mono, monospace",
 891                color: "var(--text-3)",
 892              }}
 893            >
 894              {t("details_notes")}
 895            </span>
 896          </div>
 897          <p
 898            style={{
 899              fontSize: 13,
 900              color: "var(--text-2)",
 901              lineHeight: 1.7,
 902              margin: 0,
 903              fontFamily: "DM Mono, monospace",
 904            }}
 905          >
 906            {cropDoc.notes}
 907          </p>
 908        </div>
 909      )}
 910
 911      {/* Run Cycle CTA */}
 912      <button
 913        onClick={() => navigate(`/run-cycle/${cropId}`)}
 914        style={{
 915          padding: "14px 20px",
 916          borderRadius: 12,
 917          background: "var(--green)",
 918          border: "none",
 919          color: "var(--btn-on-green)",
 920          fontWeight: 700,
 921          fontSize: 14,
 922          cursor: "pointer",
 923          display: "flex",
 924          alignItems: "center",
 925          justifyContent: "center",
 926          gap: 8,
 927          marginBottom: 8,
 928        }}
 929      >
 930        <Play size={15} />
 931        {t("details_run_cycle_btn")}
 932        <ChevronRight size={14} />
 933      </button>
 934    </div>
 935  );
 936}
 937
 938// Tabs
 939
 940const TABS = [
 941  "details_tab_info",
 942  "details_tab_overview",
 943  "details_tab_sensors",
 944  "details_tab_log",
 945];
 946
 947// MAIN
 948
 949export default function CropDetails() {
 950  const { cropId } = useParams();
 951  const navigate = useNavigate();
 952  const { settings } = useSettings();
 953  const { t, td } = useT();
 954  const logLimit = settings.historyLogLimit ?? 20;
 955
 956  const [history, setHistory] = useState([]);
 957  const [latest, setLatest] = useState(null);
 958  const [cropDoc, setCropDoc] = useState(null);
 959  const [loading, setLoading] = useState(true);
 960  const [lastExplainedLog, setLastExplainedLog] = useState(null);
 961  const [activeTab, setActiveTab] = useState("details_tab_info");
 962
 963  useEffect(() => {
 964    let cancelled = false;
 965    setLoading(true);
 966
 967    Promise.all([
 968      // Qdrant history
 969      fetchCropDetails(cropId),
 970      // MongoDB doc
 971      fetchCropById(cropId),
 972    ]).then(([histData, mongoDoc]) => {
 973      if (cancelled) return;
 974
 975      if (histData?.length) {
 976        const sorted = [...histData].sort(
 977          (a, b) =>
 978            (a.payload?.sequence_number || 0) -
 979            (b.payload?.sequence_number || 0),
 980        );
 981        const processed = sorted.map((item) => ({
 982          ...item,
 983          cleanSensors: extractSensors(item.payload || {}),
 984          parsedAction: parsePythonString((item.payload || {}).action_taken),
 985        }));
 986        setHistory(processed);
 987        setLatest(processed[processed.length - 1]);
 988        const lastExplained = [...processed]
 989          .reverse()
 990          .find(
 991            (h) =>
 992              h.payload?.explanation_log &&
 993              h.payload.explanation_log !== "PENDING_ANALYSIS" &&
 994              h.payload.explanation_log.trim().length > 0,
 995          );
 996        setLastExplainedLog(lastExplained?.payload?.explanation_log || null);
 997      }
 998
 999      if (mongoDoc) setCropDoc(mongoDoc);
1000      setLoading(false);
1001    });
1002
1003    return () => {
1004      cancelled = true;
1005    };
1006  }, [cropId]);
1007
1008  if (loading)
1009    return (
1010      <PageShell>
1011        <div
1012          style={{
1013            flex: 1,
1014            display: "flex",
1015            alignItems: "center",
1016            justifyContent: "center",
1017          }}
1018        >
1019          <span
1020            style={{
1021              fontSize: 13,
1022              fontFamily: "DM Mono, monospace",
1023              color: "var(--text-3)",
1024            }}
1025          >
1026            {t("common_loading")}
1027          </span>
1028        </div>
1029      </PageShell>
1030    );
1031
1032  if (!latest && !cropDoc)
1033    return (
1034      <div
1035        style={{
1036          flex: 1,
1037          display: "flex",
1038          alignItems: "center",
1039          justifyContent: "center",
1040        }}
1041      >
1042        <span style={{ color: "var(--text-3)" }}>{t("details_not_found")}</span>
1043      </div>
1044    );
1045
1046  const p = latest?.payload || {};
1047  const sensors = latest?.cleanSensors || {};
1048
1049  const chartData = history.map((h) => ({
1050    t: h.payload?.timestamp
1051      ? new Date(h.payload.timestamp).toLocaleTimeString([], {
1052          hour: "2-digit",
1053          minute: "2-digit",
1054        })
1055      : "--",
1056    temp: formatNumber(h.cleanSensors?.temp),
1057    ph: formatNumber(h.cleanSensors?.ph),
1058    ec: formatNumber(h.cleanSensors?.ec),
1059    humidity: formatNumber(h.cleanSensors?.humidity),
1060  }));
1061
1062  const displayCrop = cropDoc?.crop || p.crop || t("common_unknown");
1063  const displayStage = cropDoc?.stage || p.stage || "";
1064
1065  return (
1066    <PageShell>
1067      {/* Header */}
1068      <PageHeader>
1069        <IconButton onClick={() => navigate("/dashboard")}>
1070          <ArrowLeft size={15} />
1071        </IconButton>
1072
1073        <div>
1074          <h1 className="page-title">
1075            {td(displayCrop)}{" "}
1076            <span
1077              style={{ color: "var(--text-3)", fontWeight: 400, fontSize: 16 }}
1078            >
1079              #{p.sequence_number || 0}
1080            </span>
1081          </h1>
1082          <p className="page-subtitle">
1083            {cropId} · {td(displayStage)}
1084          </p>
1085        </div>
1086
1087        {/* Live badge */}
1088        <div
1089          style={{
1090            display: "flex",
1091            alignItems: "center",
1092            gap: 6,
1093            padding: "4px 10px",
1094            borderRadius: 20,
1095            background: "rgba(74,222,128,0.1)",
1096            border: "1px solid rgba(74,222,128,0.25)",
1097            fontSize: 11,
1098            fontFamily: "DM Mono, monospace",
1099            color: "var(--green)",
1100          }}
1101        >
1102          <span
1103            className="status-dot"
1104            style={{
1105              width: 6,
1106              height: 6,
1107              borderRadius: "50%",
1108              background: "var(--green)",
1109            }}
1110          />
1111          {t("details_live")}
1112        </div>
1113
1114        {/* Tabs */}
1115        <div
1116          style={{
1117            marginLeft: "auto",
1118            display: "flex",
1119            gap: 4,
1120            flexWrap: "wrap",
1121          }}
1122        >
1123          {TABS.map((tab) => (
1124            <button
1125              key={tab}
1126              onClick={() => setActiveTab(tab)}
1127              style={{
1128                padding: "5px 14px",
1129                borderRadius: 8,
1130                fontSize: 12,
1131                fontFamily: "DM Mono, monospace",
1132                textTransform: "capitalize",
1133                cursor: "pointer",
1134                background:
1135                  activeTab === tab ? "var(--surface-2)" : "transparent",
1136                color: activeTab === tab ? "var(--text)" : "var(--text-3)",
1137                border: `1px solid ${activeTab === tab ? "var(--border-bright)" : "transparent"}`,
1138              }}
1139            >
1140              {t(tab)}
1141            </button>
1142          ))}
1143        </div>
1144      </PageHeader>
1145
1146      {/* Content */}
1147      <div
1148        style={{
1149          flex: 1,
1150          overflowY: "auto",
1151          padding: 24,
1152          display: "flex",
1153          flexDirection: "column",
1154          gap: 20,
1155        }}
1156      >
1157        {/* Info Tab */}
1158        {activeTab === "details_tab_info" && (
1159          <InfoTab
1160            cropDoc={cropDoc}
1161            cropId={cropId}
1162            navigate={navigate}
1163            t={t}
1164          />
1165        )}
1166
1167        {/* Overview Tab */}
1168        {activeTab === "details_tab_overview" && (
1169          <>
1170            {/* Sensor stats */}
1171            <div
1172              style={{
1173                display: "grid",
1174                gridTemplateColumns: "repeat(4,1fr)",
1175                gap: 12,
1176              }}
1177            >
1178              <StatBox
1179                icon={Thermometer}
1180                label={t("sensor_temp")}
1181                value={formatNumber(sensors.temp)}
1182                unit="°C"
1183                color="var(--blue)"
1184              />
1185              <StatBox
1186                icon={Droplet}
1187                label={t("sensor_ph")}
1188                value={formatNumber(sensors.ph)}
1189                unit=""
1190                color="var(--green)"
1191              />
1192              <StatBox
1193                icon={Activity}
1194                label={t("sensor_ec")}
1195                value={formatNumber(sensors.ec)}
1196                unit="dS/m"
1197                color="var(--amber)"
1198              />
1199              <StatBox
1200                icon={Wind}
1201                label={t("sensor_humidity")}
1202                value={formatNumber(sensors.humidity)}
1203                unit="%"
1204                color="#a78bfa"
1205              />
1206            </div>
1207
1208            {/* Outcome */}
1209            {p.outcome && p.outcome !== "PENDING_OBSERVATION" && (
1210              <AgentOutcomeWidget
1211                outcome={p.outcome}
1212                rewardScore={p.reward_score}
1213                strategicIntent={p.strategic_intent}
1214              />
1215            )}
1216            <ExplanationLogBlock log={lastExplainedLog} t={t} td={td} />
1217            <div
1218              style={{
1219                borderRadius: 14,
1220                padding: 20,
1221                background: "var(--surface)",
1222                border: "1px solid var(--border)",
1223              }}
1224            >
1225              <InlineLabel>{t("details_hist_ph")}</InlineLabel>
1226              <ResponsiveContainer width="100%" height={200}>
1227                <AreaChart data={chartData}>
1228                  <defs>
1229                    <linearGradient id="phGradCD" x1="0" y1="0" x2="0" y2="1">
1230                      <stop offset="0%" stopColor="#4ade80" stopOpacity={0.3} />
1231                      <stop offset="100%" stopColor="#4ade80" stopOpacity={0} />
1232                    </linearGradient>
1233                  </defs>
1234                  <CartesianGrid
1235                    stroke="var(--border)"
1236                    strokeDasharray="3 3"
1237                    vertical={false}
1238                  />
1239                  <XAxis
1240                    dataKey="t"
1241                    tick={{
1242                      fontSize: 10,
1243                      fill: "var(--text-3)",
1244                      fontFamily: "DM Mono",
1245                    }}
1246                    axisLine={false}
1247                    tickLine={false}
1248                  />
1249                  <YAxis
1250                    domain={["auto", "auto"]}
1251                    tick={{
1252                      fontSize: 10,
1253                      fill: "var(--text-3)",
1254                      fontFamily: "DM Mono",
1255                    }}
1256                    axisLine={false}
1257                    tickLine={false}
1258                  />
1259                  <Tooltip content={<ChartTooltip />} />
1260                  <Area
1261                    type="monotone"
1262                    dataKey="ph"
1263                    stroke="var(--green)"
1264                    fill="url(#phGradCD)"
1265                    strokeWidth={2}
1266                    dot={false}
1267                    name={t("chart_ph")}
1268                  />
1269                </AreaChart>
1270              </ResponsiveContainer>
1271            </div>
1272
1273            {/* Latest Action */}
1274            {p.action_taken && p.action_taken !== "PENDING_ACTION" && (
1275              <div
1276                style={{
1277                  borderRadius: 14,
1278                  padding: 20,
1279                  background: "var(--surface)",
1280                  border: "1px solid var(--border)",
1281                }}
1282              >
1283                <InlineLabel style={{ marginBottom: 16 }}>
1284                  {t("details_latest_cmd")}
1285                </InlineLabel>
1286                <AgentActionWidget
1287                  actionTaken={p.action_taken}
1288                  compact={false}
1289                />
1290              </div>
1291            )}
1292          </>
1293        )}
1294
1295        {/* Sensors Tab */}
1296        {activeTab === "details_tab_sensors" && (
1297          <>
1298            <div
1299              style={{
1300                borderRadius: 14,
1301                padding: 20,
1302                background: "var(--surface)",
1303                border: "1px solid var(--border)",
1304              }}
1305            >
1306              <InlineLabel>{t("details_temp_hum")}</InlineLabel>
1307              <ResponsiveContainer width="100%" height={200}>
1308                <LineChart data={chartData}>
1309                  <CartesianGrid
1310                    stroke="var(--border)"
1311                    strokeDasharray="3 3"
1312                    vertical={false}
1313                  />
1314                  <XAxis
1315                    dataKey="t"
1316                    tick={{
1317                      fontSize: 10,
1318                      fill: "var(--text-3)",
1319                      fontFamily: "DM Mono",
1320                    }}
1321                    axisLine={false}
1322                    tickLine={false}
1323                  />
1324                  <YAxis
1325                    tick={{
1326                      fontSize: 10,
1327                      fill: "var(--text-3)",
1328                      fontFamily: "DM Mono",
1329                    }}
1330                    axisLine={false}
1331                    tickLine={false}
1332                  />
1333                  <Tooltip content={<ChartTooltip />} />
1334                  <Line
1335                    type="monotone"
1336                    dataKey="temp"
1337                    stroke="#60a5fa"
1338                    strokeWidth={2}
1339                    dot={false}
1340                    name={t("chart_temp")}
1341                  />
1342                  <Line
1343                    type="monotone"
1344                    dataKey="humidity"
1345                    stroke="#a78bfa"
1346                    strokeWidth={2}
1347                    dot={false}
1348                    name={t("chart_humidity")}
1349                  />
1350                </LineChart>
1351              </ResponsiveContainer>
1352            </div>
1353
1354            <div
1355              style={{
1356                borderRadius: 14,
1357                padding: 20,
1358                background: "var(--surface)",
1359                border: "1px solid var(--border)",
1360              }}
1361            >
1362              <div className="section-label">{t("details_ec_conc")}</div>
1363              <ResponsiveContainer width="100%" height={180}>
1364                <AreaChart data={chartData}>
1365                  <defs>
1366                    <linearGradient id="ecGradCD" x1="0" y1="0" x2="0" y2="1">
1367                      <stop
1368                        offset="0%"
1369                        stopColor="#f59e0b"
1370                        stopOpacity={0.25}
1371                      />
1372                      <stop offset="100%" stopColor="#f59e0b" stopOpacity={0} />
1373                    </linearGradient>
1374                  </defs>
1375                  <CartesianGrid
1376                    stroke="var(--border)"
1377                    strokeDasharray="3 3"
1378                    vertical={false}
1379                  />
1380                  <XAxis
1381                    dataKey="t"
1382                    tick={{
1383                      fontSize: 10,
1384                      fill: "var(--text-3)",
1385                      fontFamily: "DM Mono",
1386                    }}
1387                    axisLine={false}
1388                    tickLine={false}
1389                  />
1390                  <YAxis
1391                    tick={{
1392                      fontSize: 10,
1393                      fill: "var(--text-3)",
1394                      fontFamily: "DM Mono",
1395                    }}
1396                    axisLine={false}
1397                    tickLine={false}
1398                  />
1399                  <Tooltip content={<ChartTooltip />} />
1400                  <Area
1401                    type="monotone"
1402                    dataKey="ec"
1403                    stroke="var(--amber)"
1404                    fill="url(#ecGradCD)"
1405                    strokeWidth={2}
1406                    dot={false}
1407                    name={t("chart_ec")}
1408                  />
1409                </AreaChart>
1410              </ResponsiveContainer>
1411            </div>
1412          </>
1413        )}
1414
1415        {/* Log Tab */}
1416        {activeTab === "details_tab_log" && (
1417          <div
1418            style={{
1419              borderRadius: 14,
1420              overflow: "hidden",
1421              background: "var(--surface)",
1422              border: "1px solid var(--border)",
1423              flexShrink: 0,
1424            }}
1425          >
1426            {/* Header row */}
1427            <div
1428              style={{
1429                padding: "14px 20px",
1430                borderBottom: "1px solid var(--border)",
1431              }}
1432            >
1433              <div className="section-label">
1434                {t("details_event_log", {
1435                  total: history.length,
1436                  limit: logLimit,
1437                })}
1438              </div>
1439            </div>
1440
1441            {/* Log rows */}
1442            <div>
1443              {[...history]
1444                .reverse()
1445                .slice(0, logLimit)
1446                .map((h, i) => (
1447                  <LogRow
1448                    key={i}
1449                    h={h}
1450                    i={i}
1451                    logLimit={logLimit}
1452                    t={t}
1453                    td={td}
1454                    formatNumber={formatNumber}
1455                    logDotColor={logDotColor}
1456                  />
1457                ))}
1458            </div>
1459          </div>
1460        )}
1461      </div>
1462    </PageShell>
1463  );
1464}
1465
1466function LogRow({ h, i, logLimit, t, td, formatNumber, logDotColor }) {
1467  const [expanded, setExpanded] = useState(false);
1468  const hasExplanation =
1469    h.payload?.explanation_log &&
1470    h.payload.explanation_log !== "PENDING_ANALYSIS";
1471
1472  return (
1473    <div
1474      style={{
1475        borderBottom: "1px solid var(--border)",
1476        transition: "background 0.12s",
1477        cursor: "default",
1478      }}
1479      onMouseEnter={(e) =>
1480        (e.currentTarget.style.background = "rgba(74,222,128,0.04)")
1481      }
1482      onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")}
1483    >
1484      {/* Row header */}
1485      <div
1486        style={{
1487          display: "flex",
1488          alignItems: "center",
1489          gap: 10,
1490          padding: "10px 20px",
1491        }}
1492      >
1493        <span
1494          style={{
1495            width: 7,
1496            height: 7,
1497            borderRadius: "50%",
1498            background: logDotColor(h.payload),
1499            flexShrink: 0,
1500          }}
1501        />
1502        <span
1503          style={{
1504            fontSize: 11,
1505            fontFamily: "DM Mono, monospace",
1506            color: "var(--text-3)",
1507            flexShrink: 0,
1508            width: 52,
1509          }}
1510        >
1511          {h.payload?.timestamp
1512            ? new Date(h.payload.timestamp).toLocaleTimeString([], {
1513                hour: "2-digit",
1514                minute: "2-digit",
1515              })
1516            : "--"}
1517        </span>
1518        <span
1519          style={{
1520            fontSize: 11,
1521            fontFamily: "DM Mono, monospace",
1522            color: "var(--text-3)",
1523            width: 36,
1524            flexShrink: 0,
1525          }}
1526        >
1527          #{h.payload?.sequence_number || i}
1528        </span>
1529        <div style={{ display: "flex", gap: 12, flexWrap: "wrap" }}>
1530          {[
1531            { label: "pH", value: h.cleanSensors?.ph, color: "var(--green)" },
1532            { label: "EC", value: h.cleanSensors?.ec, color: "var(--amber)" },
1533            {
1534              label: "T",
1535              value: h.cleanSensors?.temp + "°",
1536              color: "var(--blue)",
1537            },
1538            {
1539              label: "H",
1540              value: h.cleanSensors?.humidity + "%",
1541              color: "#a78bfa",
1542            },
1543          ].map(({ label, value, color }) => (
1544            <span
1545              key={label}
1546              style={{
1547                fontSize: 13,
1548                fontFamily: "DM Mono, monospace",
1549                display: "flex",
1550                alignItems: "baseline",
1551                gap: 3,
1552              }}
1553            >
1554              <span style={{ color: "var(--text-3)", fontSize: 11 }}>
1555                {label}
1556              </span>
1557              <span style={{ color, fontWeight: 700 }}>
1558                {formatNumber(value)}
1559              </span>
1560            </span>
1561          ))}
1562        </div>
1563
1564        {/* Outcome badge */}
1565        {h.payload?.outcome && (
1566          <span style={{ marginLeft: "auto", flexShrink: 0 }}>
1567            <AgentOutcomeWidget
1568              outcome={h.payload.outcome}
1569              rewardScore={h.payload.reward_score}
1570            />
1571          </span>
1572        )}
1573
1574        {/* Explanation toggle */}
1575        {hasExplanation && (
1576          <button
1577            onClick={() => setExpanded((v) => !v)}
1578            style={{
1579              flexShrink: 0,
1580              display: "flex",
1581              alignItems: "center",
1582              gap: 4,
1583              padding: "3px 8px",
1584              borderRadius: 6,
1585              fontSize: 9,
1586              fontFamily: "DM Mono, monospace",
1587              cursor: "pointer",
1588              background: expanded ? "rgba(167,139,250,0.12)" : "var(--bg-3)",
1589              border: `1px solid ${expanded ? "rgba(167,139,250,0.3)" : "var(--border)"}`,
1590              color: expanded ? "#a78bfa" : "var(--text-3)",
1591            }}
1592          >
1593            <Brain size={9} /> {expanded ? t("details_hide") : t("details_why")}
1594          </button>
1595        )}
1596      </div>
1597
1598      {/* Action row */}
1599      {h.payload?.action_taken &&
1600        h.payload.action_taken !== "PENDING_ACTION" && (
1601          <div style={{ padding: "0 20px 10px 46px" }}>
1602            <AgentActionWidget actionTaken={h.payload.action_taken} compact />
1603          </div>
1604        )}
1605
1606      {/* Explanation log inline */}
1607      {hasExplanation && expanded && (
1608        <div
1609          className="animate-fade-in"
1610          style={{
1611            margin: "0 20px 12px 46px",
1612            padding: "12px 14px",
1613            borderRadius: 10,
1614            background: "rgba(167,139,250,0.05)",
1615            border: "1px solid rgba(167,139,250,0.15)",
1616          }}
1617        >
1618          <div
1619            style={{
1620              fontSize: 9,
1621              fontFamily: "DM Mono, monospace",
1622              color: "#a78bfa",
1623              marginBottom: 8,
1624            }}
1625          >
1626            <Brain size={9} style={{ display: "inline", marginRight: 5 }} />
1627            {t("details_ai_reasoning")}
1628          </div>
1629          <pre
1630            style={{
1631              fontSize: 11,
1632              fontFamily: "DM Mono, monospace",
1633              color: "var(--text-2)",
1634              lineHeight: 1.7,
1635              whiteSpace: "pre-wrap",
1636              margin: 0,
1637            }}
1638          >
1639            {td(h.payload.explanation_log)}
1640          </pre>
1641        </div>
1642      )}
1643    </div>
1644  );
1645}