Akash-Dragon commited on
Commit
bfc8b81
·
1 Parent(s): a9bb92b

feat: replace legacy frontend, integrate agriculture-ai-frontend with backend, update CORS, and clean project structure

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +9 -14
  2. README.md +21 -23
  3. agriculture-ai-frontend/.env.example +2 -0
  4. agriculture-ai-frontend/.gitignore +32 -0
  5. agriculture-ai-frontend/.prettierignore +8 -0
  6. agriculture-ai-frontend/.prettierrc +6 -0
  7. agriculture-ai-frontend/AGENTS.md +10 -0
  8. agriculture-ai-frontend/DEPLOY.md +69 -0
  9. agriculture-ai-frontend/README.md +29 -0
  10. agriculture-ai-frontend/bun.lock +0 -0
  11. agriculture-ai-frontend/bunfig.toml +7 -0
  12. agriculture-ai-frontend/components.json +22 -0
  13. agriculture-ai-frontend/eslint.config.js +40 -0
  14. agriculture-ai-frontend/package-lock.json +0 -0
  15. agriculture-ai-frontend/package.json +87 -0
  16. backend/test1.jpg → agriculture-ai-frontend/public/favicon.ico +2 -2
  17. agriculture-ai-frontend/public/favicon.svg +5 -0
  18. agriculture-ai-frontend/src/components/agri/AdviceTab.tsx +311 -0
  19. agriculture-ai-frontend/src/components/agri/ConfidenceBar.tsx +29 -0
  20. agriculture-ai-frontend/src/components/agri/CropTab.tsx +196 -0
  21. agriculture-ai-frontend/src/components/agri/DiseaseTab.tsx +108 -0
  22. agriculture-ai-frontend/src/components/agri/ImageDrop.tsx +110 -0
  23. agriculture-ai-frontend/src/components/agri/Shell.tsx +190 -0
  24. agriculture-ai-frontend/src/components/agri/SoilTab.tsx +105 -0
  25. agriculture-ai-frontend/src/components/ui/accordion.tsx +51 -0
  26. agriculture-ai-frontend/src/components/ui/alert-dialog.tsx +115 -0
  27. agriculture-ai-frontend/src/components/ui/alert.tsx +49 -0
  28. agriculture-ai-frontend/src/components/ui/aspect-ratio.tsx +5 -0
  29. agriculture-ai-frontend/src/components/ui/avatar.tsx +47 -0
  30. agriculture-ai-frontend/src/components/ui/badge.tsx +32 -0
  31. agriculture-ai-frontend/src/components/ui/breadcrumb.tsx +101 -0
  32. agriculture-ai-frontend/src/components/ui/button.tsx +49 -0
  33. agriculture-ai-frontend/src/components/ui/calendar.tsx +177 -0
  34. agriculture-ai-frontend/src/components/ui/card.tsx +55 -0
  35. agriculture-ai-frontend/src/components/ui/carousel.tsx +240 -0
  36. agriculture-ai-frontend/src/components/ui/chart.tsx +331 -0
  37. agriculture-ai-frontend/src/components/ui/checkbox.tsx +26 -0
  38. agriculture-ai-frontend/src/components/ui/collapsible.tsx +11 -0
  39. agriculture-ai-frontend/src/components/ui/command.tsx +143 -0
  40. agriculture-ai-frontend/src/components/ui/context-menu.tsx +187 -0
  41. agriculture-ai-frontend/src/components/ui/dialog.tsx +104 -0
  42. agriculture-ai-frontend/src/components/ui/drawer.tsx +98 -0
  43. agriculture-ai-frontend/src/components/ui/dropdown-menu.tsx +188 -0
  44. agriculture-ai-frontend/src/components/ui/form.tsx +171 -0
  45. agriculture-ai-frontend/src/components/ui/hover-card.tsx +27 -0
  46. agriculture-ai-frontend/src/components/ui/input-otp.tsx +69 -0
  47. agriculture-ai-frontend/src/components/ui/input.tsx +22 -0
  48. agriculture-ai-frontend/src/components/ui/label.tsx +21 -0
  49. agriculture-ai-frontend/src/components/ui/menubar.tsx +229 -0
  50. agriculture-ai-frontend/src/components/ui/navigation-menu.tsx +120 -0
.gitignore CHANGED
@@ -1,8 +1,10 @@
1
- # OS / editor
2
  .DS_Store
3
  Thumbs.db
4
  .vscode/
5
  .idea/
 
 
6
 
7
  # Python
8
  __pycache__/
@@ -31,6 +33,11 @@ env/
31
  # Build / packaging
32
  build/
33
  dist/
 
 
 
 
 
34
  *.egg-info/
35
  .eggs/
36
 
@@ -49,29 +56,17 @@ dist/
49
  !dataset/agri_ai_dataset/models/*.pth
50
  !dataset/agri_ai_dataset/models/*.pkl
51
 
52
-
53
  # Dataset images and raw data
54
  dataset/agri_ai_dataset/crop_recommendation/
55
  dataset/agri_ai_dataset/extra_classes/
56
  dataset/agri_ai_dataset/plant_disease/
57
  dataset/agri_ai_dataset/soil_classification/
58
 
59
- # Node / React / React Native
60
  node_modules/
61
- .expo/
62
- .expo-shared/
63
  .npm/
64
  .yarn/
65
  .pnpm-store/
66
  coverage/
67
  .vite/
68
- metro-cache/
69
  *.tsbuildinfo
70
-
71
- # Android / iOS build outputs
72
- android/build/
73
- ios/build/
74
-
75
- # Misc generated files
76
- *.swp
77
- *.swo
 
1
+ # OS / Editor
2
  .DS_Store
3
  Thumbs.db
4
  .vscode/
5
  .idea/
6
+ *.swp
7
+ *.swo
8
 
9
  # Python
10
  __pycache__/
 
33
  # Build / packaging
34
  build/
35
  dist/
36
+ dist-ssr/
37
+ .output/
38
+ .nitro/
39
+ .tanstack/
40
+ .lovable/
41
  *.egg-info/
42
  .eggs/
43
 
 
56
  !dataset/agri_ai_dataset/models/*.pth
57
  !dataset/agri_ai_dataset/models/*.pkl
58
 
 
59
  # Dataset images and raw data
60
  dataset/agri_ai_dataset/crop_recommendation/
61
  dataset/agri_ai_dataset/extra_classes/
62
  dataset/agri_ai_dataset/plant_disease/
63
  dataset/agri_ai_dataset/soil_classification/
64
 
65
+ # Node / React / Vite
66
  node_modules/
 
 
67
  .npm/
68
  .yarn/
69
  .pnpm-store/
70
  coverage/
71
  .vite/
 
72
  *.tsbuildinfo
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -26,22 +26,24 @@ agri-ai/
26
  │ │ ├── preprocessing.py # Image validation + transforms
27
  │ │ └── cache.py # Redis cache with in-memory fallback
28
  │ └── rag_docs/ # Drop .txt files here for RAG knowledge base
29
- └── frontend/
30
  ├── index.html
31
- ├── vite.config.js
32
  ├── package.json
33
  └── src/
34
- ├── App.jsx # Root: header, tabs, language state
35
- ├── index.css # Full dark-earthy theme
36
- ├── i18n/
37
- ── translations.js # EN / Tamil / Hindi / Telugu strings
38
- ── components/
39
- ── DiseaseTab.jsx
40
- ── SoilTab.jsx
41
- ├── CropTab.jsx
42
- ├── AdviceTab.jsx # Streaming LLM response
43
- ├── ImageUpload.jsx
44
- ── ConfidenceBar.jsx
 
 
45
  ```
46
 
47
  ---
@@ -57,23 +59,19 @@ ls ../dataset/agri_ai_dataset/models/
57
  # crop_model.pkl disease_model_fp16.pth label_enoder_crop.pkl soil_model.pth
58
  ```
59
 
60
- ### 2. Backend
61
 
62
  ```bash
63
  cd backend
64
 
65
- # Create virtual environment
66
- python -m venv venv
67
- source venv/bin/activate # Windows: venv\Scripts\activate
68
-
69
- # Install dependencies
70
  pip install -r requirements.txt
71
 
72
  # Configure environment
73
  cp .env.example .env
74
  # Edit .env:
75
- # - Set model paths (already match your existing paths)
76
- # - Add your ANTHROPIC_API_KEY
77
  # - Optionally set REDIS_URL, JWT_SECRET_KEY
78
 
79
  # Start server
@@ -103,13 +101,13 @@ Destroy infected plant material immediately." > backend/rag_docs/tomato_diseases
103
  ### 4. Frontend
104
 
105
  ```bash
106
- cd frontend
107
 
108
  npm install
109
  npm run dev
110
 
111
  # Opens at http://localhost:5173
112
- # API calls proxy to http://localhost:8000 via vite.config.js
113
  ```
114
 
115
  ---
 
26
  │ │ ├── preprocessing.py # Image validation + transforms
27
  │ │ └── cache.py # Redis cache with in-memory fallback
28
  │ └── rag_docs/ # Drop .txt files here for RAG knowledge base
29
+ └── agriculture-ai-frontend/
30
  ├── index.html
31
+ ├── vite.config.ts
32
  ├── package.json
33
  └── src/
34
+ ├── routes/ # TanStack Router routes
35
+ ├── styles.css # Styling
36
+ ├── lib/
37
+ ── api.ts # FastAPI integration & data mapping
38
+ │ ├── agri-store.ts # Global state management
39
+ │ └── i18n.ts # Translations
40
+ ── components/agri/
41
+ ├── DiseaseTab.tsx
42
+ ├── SoilTab.tsx
43
+ ├── CropTab.tsx
44
+ ── AdviceTab.tsx
45
+ ├── ImageDrop.tsx
46
+ └── ConfidenceBar.tsx
47
  ```
48
 
49
  ---
 
59
  # crop_model.pkl disease_model_fp16.pth label_enoder_crop.pkl soil_model.pth
60
  ```
61
 
62
+ ### 2. Backend (Global Python Environment)
63
 
64
  ```bash
65
  cd backend
66
 
67
+ # Install dependencies (using global Python environment)
 
 
 
 
68
  pip install -r requirements.txt
69
 
70
  # Configure environment
71
  cp .env.example .env
72
  # Edit .env:
73
+ # - Set model paths
74
+ # - Add your OPENAI_API_KEY / LLM keys
75
  # - Optionally set REDIS_URL, JWT_SECRET_KEY
76
 
77
  # Start server
 
101
  ### 4. Frontend
102
 
103
  ```bash
104
+ cd agriculture-ai-frontend
105
 
106
  npm install
107
  npm run dev
108
 
109
  # Opens at http://localhost:5173
110
+ # API calls proxy to http://localhost:8000 via vite.config.ts / VITE_API_BASE_URL
111
  ```
112
 
113
  ---
agriculture-ai-frontend/.env.example ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # FastAPI backend URL — leave empty to use same-origin (works with vercel.json rewrite)
2
+ VITE_API_BASE_URL=http://localhost:8000
agriculture-ai-frontend/.gitignore ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ .output
14
+ .vinxi
15
+ .tanstack/**
16
+ .nitro
17
+ *.local
18
+
19
+ # Wrangler / Cloudflare
20
+ .wrangler/
21
+ .dev.vars
22
+
23
+ # Editor directories and files
24
+ .vscode/*
25
+ !.vscode/extensions.json
26
+ .idea
27
+ .DS_Store
28
+ *.suo
29
+ *.ntvs*
30
+ *.njsproj
31
+ *.sln
32
+ *.sw?
agriculture-ai-frontend/.prettierignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ node_modules
2
+ dist
3
+ .output
4
+ .vinxi
5
+ pnpm-lock.yaml
6
+ package-lock.json
7
+ bun.lock
8
+ routeTree.gen.ts
agriculture-ai-frontend/.prettierrc ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "printWidth": 100,
3
+ "semi": true,
4
+ "singleQuote": false,
5
+ "trailingComma": "all"
6
+ }
agriculture-ai-frontend/AGENTS.md ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- LOVABLE:BEGIN -->
2
+ > [!IMPORTANT]
3
+ > This project is connected to [Lovable](https://lovable.dev). Avoid rewriting
4
+ > published git history — force pushing, or rebasing/amending/squashing commits
5
+ > that are already pushed — as it rewrites history on Lovable's side and the
6
+ > user will likely lose their project history.
7
+ >
8
+ > Commits you push to the connected branch sync back to Lovable and show up in
9
+ > the editor, so keep the branch in a working state.
10
+ <!-- LOVABLE:END -->
agriculture-ai-frontend/DEPLOY.md ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Deploying to Vercel
2
+
3
+ This is the **frontend** (TanStack Start). Your FastAPI backend from
4
+ `Agriculture-AI-main/backend/` deploys separately.
5
+
6
+ ---
7
+
8
+ ## 1. Frontend on Vercel
9
+
10
+ 1. Push this project to GitHub.
11
+ 2. In Vercel → **New Project** → import the repo.
12
+ 3. Framework preset: **Other**. Vercel picks up `vercel.json`:
13
+ - Build command: `bun run build`
14
+ - Output directory: `.output/public`
15
+ 4. **Environment variables**:
16
+ - `VITE_API_BASE_URL` — full URL of your FastAPI backend
17
+ (e.g. `https://agri-api.onrender.com`). Leave empty to use the
18
+ same-origin `/api/v1/...` rewrite in `vercel.json` (edit the
19
+ `destination` in `vercel.json` first).
20
+ 5. Deploy.
21
+
22
+ ## 2. FastAPI backend
23
+
24
+ Vercel serverless (Python runtime) is only suitable for the **light**
25
+ routes. Heavy PyTorch/CatBoost models exceed the 250 MB unzipped
26
+ serverless limit — deploy the backend on a container host instead.
27
+
28
+ **Recommended hosts:**
29
+ - **Render** — free tier fits small models; add a `Procfile`
30
+ `web: uvicorn main:app --host 0.0.0.0 --port $PORT`
31
+ - **Railway / Fly.io** — Docker image, no size limit
32
+ - **Hugging Face Spaces (Docker)** — free GPU option for the CNN
33
+
34
+ **Dockerfile** you can drop into `backend/`:
35
+
36
+ ```dockerfile
37
+ FROM python:3.11-slim
38
+ WORKDIR /app
39
+ COPY requirements.txt .
40
+ RUN pip install --no-cache-dir -r requirements.txt
41
+ COPY . .
42
+ ENV PORT=8000
43
+ CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port ${PORT}"]
44
+ ```
45
+
46
+ After the backend is live, set `VITE_API_BASE_URL` in Vercel to its URL
47
+ and redeploy, or update `vercel.json` `destination` and rely on the
48
+ rewrite so the browser calls same-origin (no CORS setup needed).
49
+
50
+ ## 3. CORS (if not using the rewrite)
51
+
52
+ Add to `backend/main.py`:
53
+
54
+ ```python
55
+ from fastapi.middleware.cors import CORSMiddleware
56
+ app.add_middleware(
57
+ CORSMiddleware,
58
+ allow_origins=["https://your-vercel-app.vercel.app"],
59
+ allow_methods=["*"],
60
+ allow_headers=["*"],
61
+ )
62
+ ```
63
+
64
+ ## API contract expected by this UI
65
+
66
+ - `POST /api/v1/analyze/disease` — multipart `file`, `lang` → `{ disease, scientific_name?, confidence, treatment?, urgency? }`
67
+ - `POST /api/v1/analyze/soil` — multipart `file`, `lang` → `{ soil_type, confidence, properties? }`
68
+ - `POST /api/v1/recommend/crop` — JSON `{ N,P,K,temperature,humidity,ph,rainfall,lang }` → `{ recommendations: [{crop, probability}] }`
69
+ - `POST /api/v1/advice` — JSON `{ question?, disease?, soil?, crop?, lang }` → streaming text/plain body
agriculture-ai-frontend/README.md ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Welcome to your Lovable project
2
+
3
+ This project was built with [Lovable](https://lovable.dev).
4
+
5
+ ## Build with Lovable
6
+
7
+ Open your project in the [Lovable editor](https://lovable.dev) and keep building.
8
+
9
+ - **Ship faster**: describe what you want to build and Lovable handles the code.
10
+ - **Stay in sync**: connect the project to GitHub and every change made in Lovable is committed straight to your repository.
11
+ - **Full ownership**: this code is yours. Push to your repository and your changes sync back into Lovable, ready for your next prompt.
12
+
13
+ ## Development
14
+
15
+ Prefer working locally? You need Node.js and npm — [install with nvm](https://github.com/nvm-sh/nvm#installing-and-updating).
16
+
17
+ ```sh
18
+ git clone <this-repository-url>
19
+ cd <repository-name>
20
+ npm i
21
+ npm run dev
22
+ ```
23
+
24
+ ## Built with
25
+
26
+ - TanStack Start
27
+ - TypeScript
28
+ - React
29
+ - Tailwind CSS
agriculture-ai-frontend/bun.lock ADDED
The diff for this file is too large to render. See raw diff
 
agriculture-ai-frontend/bunfig.toml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ [install]
2
+ saveTextLockfile = true
3
+ # 24h supply-chain guard: skip package versions published less than a day ago.
4
+ minimumReleaseAge = 86400
5
+ # Each entry bypasses the 24h guard for one package — confirm with the user
6
+ # before adding any.
7
+ minimumReleaseAgeExcludes = ["@lovable.dev/vite-tanstack-config", "@lovable.dev/mcp-js", "@lovable.dev/vite-plugin-dev-server-bridge", "@lovable.dev/vite-plugin-hmr-gate", "@lovable.dev/email-js", "@lovable.dev/webhooks-js"]
agriculture-ai-frontend/components.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": false,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "css": "src/styles.css",
8
+ "baseColor": "slate",
9
+ "cssVariables": true,
10
+ "prefix": ""
11
+ },
12
+ "iconLibrary": "lucide",
13
+ "rtl": false,
14
+ "aliases": {
15
+ "components": "@/components",
16
+ "utils": "@/lib/utils",
17
+ "ui": "@/components/ui",
18
+ "lib": "@/lib",
19
+ "hooks": "@/hooks"
20
+ },
21
+ "registries": {}
22
+ }
agriculture-ai-frontend/eslint.config.js ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from "@eslint/js";
2
+ import eslintPluginPrettier from "eslint-plugin-prettier/recommended";
3
+ import globals from "globals";
4
+ import reactHooks from "eslint-plugin-react-hooks";
5
+ import reactRefresh from "eslint-plugin-react-refresh";
6
+ import tseslint from "typescript-eslint";
7
+
8
+ export default tseslint.config(
9
+ { ignores: ["dist", ".output", ".vinxi"] },
10
+ {
11
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
12
+ files: ["**/*.{ts,tsx}"],
13
+ languageOptions: {
14
+ ecmaVersion: 2020,
15
+ globals: globals.browser,
16
+ },
17
+ plugins: {
18
+ "react-hooks": reactHooks,
19
+ "react-refresh": reactRefresh,
20
+ },
21
+ rules: {
22
+ ...reactHooks.configs.recommended.rules,
23
+ "no-restricted-imports": [
24
+ "error",
25
+ {
26
+ paths: [
27
+ {
28
+ name: "server-only",
29
+ message:
30
+ "TanStack Start does not use the Next.js `server-only` package. Rename the module to `*.server.ts` or mark it with `@tanstack/react-start/server-only`.",
31
+ },
32
+ ],
33
+ },
34
+ ],
35
+ "react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
36
+ "@typescript-eslint/no-unused-vars": "off",
37
+ },
38
+ },
39
+ eslintPluginPrettier,
40
+ );
agriculture-ai-frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
agriculture-ai-frontend/package.json ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "tanstack_start_ts",
3
+ "private": true,
4
+ "sideEffects": false,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite dev",
8
+ "build": "vite build",
9
+ "build:dev": "vite build --mode development",
10
+ "preview": "vite preview",
11
+ "lint": "eslint .",
12
+ "format": "prettier --write ."
13
+ },
14
+ "dependencies": {
15
+ "@hookform/resolvers": "^5.2.2",
16
+ "@radix-ui/react-accordion": "^1.2.12",
17
+ "@radix-ui/react-alert-dialog": "^1.1.15",
18
+ "@radix-ui/react-aspect-ratio": "^1.1.8",
19
+ "@radix-ui/react-avatar": "^1.1.11",
20
+ "@radix-ui/react-checkbox": "^1.3.3",
21
+ "@radix-ui/react-collapsible": "^1.1.12",
22
+ "@radix-ui/react-context-menu": "^2.2.16",
23
+ "@radix-ui/react-dialog": "^1.1.15",
24
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
25
+ "@radix-ui/react-hover-card": "^1.1.15",
26
+ "@radix-ui/react-label": "^2.1.8",
27
+ "@radix-ui/react-menubar": "^1.1.16",
28
+ "@radix-ui/react-navigation-menu": "^1.2.14",
29
+ "@radix-ui/react-popover": "^1.1.15",
30
+ "@radix-ui/react-progress": "^1.1.8",
31
+ "@radix-ui/react-radio-group": "^1.3.8",
32
+ "@radix-ui/react-scroll-area": "^1.2.10",
33
+ "@radix-ui/react-select": "^2.2.6",
34
+ "@radix-ui/react-separator": "^1.1.8",
35
+ "@radix-ui/react-slider": "^1.3.6",
36
+ "@radix-ui/react-slot": "^1.2.4",
37
+ "@radix-ui/react-switch": "^1.2.6",
38
+ "@radix-ui/react-tabs": "^1.1.13",
39
+ "@radix-ui/react-toggle": "^1.1.10",
40
+ "@radix-ui/react-toggle-group": "^1.1.11",
41
+ "@radix-ui/react-tooltip": "^1.2.8",
42
+ "@tailwindcss/vite": "^4.2.1",
43
+ "@tanstack/react-query": "^5.101.1",
44
+ "@tanstack/react-router": "^1.170.16",
45
+ "@tanstack/react-start": "^1.168.26",
46
+ "@tanstack/router-plugin": "^1.168.18",
47
+ "class-variance-authority": "^0.7.1",
48
+ "clsx": "^2.1.1",
49
+ "cmdk": "^1.1.1",
50
+ "date-fns": "^4.1.0",
51
+ "embla-carousel-react": "^8.6.0",
52
+ "input-otp": "^1.4.2",
53
+ "lucide-react": "^0.575.0",
54
+ "react": "^19.2.0",
55
+ "react-day-picker": "^9.14.0",
56
+ "react-dom": "^19.2.0",
57
+ "react-hook-form": "^7.71.2",
58
+ "react-resizable-panels": "^4.6.5",
59
+ "recharts": "^2.15.4",
60
+ "sonner": "^2.0.7",
61
+ "tailwind-merge": "^3.5.0",
62
+ "tailwindcss": "^4.2.1",
63
+ "tw-animate-css": "^1.3.4",
64
+ "vaul": "^1.1.2",
65
+ "vite-tsconfig-paths": "^6.0.2",
66
+ "zod": "^3.24.2"
67
+ },
68
+ "devDependencies": {
69
+ "@eslint/js": "^9.32.0",
70
+ "@lovable.dev/vite-tanstack-config": "^2.7.7",
71
+ "@types/node": "^22.16.5",
72
+ "@types/react": "^19.2.0",
73
+ "@types/react-dom": "^19.2.0",
74
+ "@vitejs/plugin-react": "^5.2.0",
75
+ "eslint": "^9.32.0",
76
+ "eslint-config-prettier": "^10.1.1",
77
+ "eslint-plugin-prettier": "^5.2.6",
78
+ "eslint-plugin-react-hooks": "^5.2.0",
79
+ "eslint-plugin-react-refresh": "^0.4.20",
80
+ "globals": "^15.15.0",
81
+ "nitro": "3.0.260603-beta",
82
+ "prettier": "^3.7.3",
83
+ "typescript": "^5.8.3",
84
+ "typescript-eslint": "^8.56.1",
85
+ "vite": "^8.0.16"
86
+ }
87
+ }
backend/test1.jpg → agriculture-ai-frontend/public/favicon.ico RENAMED
File without changes
agriculture-ai-frontend/public/favicon.svg ADDED
agriculture-ai-frontend/src/components/agri/AdviceTab.tsx ADDED
@@ -0,0 +1,311 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useRef, useState } from "react";
2
+ import { toast } from "sonner";
3
+ import { Loader2, Sparkles, Square } from "lucide-react";
4
+ import { SectionHeader, TabWrapper } from "./Shell";
5
+ import { useAgri } from "@/lib/agri-store";
6
+ import { TRANSLATIONS } from "@/lib/i18n";
7
+ import { streamAdvice } from "@/lib/api";
8
+
9
+ export function AdviceTab() {
10
+ const { lang, disease, soil, crops } = useAgri();
11
+ const t = TRANSLATIONS[lang].advice;
12
+ const [question, setQuestion] = useState("");
13
+ const [text, setText] = useState("");
14
+ const [streaming, setStreaming] = useState(false);
15
+ const abortRef = useRef<AbortController | null>(null);
16
+
17
+ const hasContext = Boolean(disease || soil || (crops && crops.length));
18
+
19
+ const start = async () => {
20
+ if (streaming) return;
21
+ setStreaming(true);
22
+ setText("");
23
+ const ctrl = new AbortController();
24
+ abortRef.current = ctrl;
25
+ try {
26
+ await streamAdvice(
27
+ { question: question.trim() || undefined, disease, soil, crop: crops, lang },
28
+ (chunk) => setText((s) => s + chunk),
29
+ ctrl.signal,
30
+ );
31
+ } catch (e) {
32
+ if ((e as Error).name !== "AbortError") {
33
+ console.error(e);
34
+ toast.error(t.error);
35
+ }
36
+ } finally {
37
+ setStreaming(false);
38
+ abortRef.current = null;
39
+ }
40
+ };
41
+
42
+ const stop = () => abortRef.current?.abort();
43
+
44
+ return (
45
+ <TabWrapper>
46
+ <SectionHeader title={t.title} subtitle={t.subtitle} />
47
+
48
+ <div className="grid gap-6 lg:grid-cols-5">
49
+ <div className="space-y-4 lg:col-span-2">
50
+ <div className="rounded-3xl border border-border bg-sage-soft/50 p-5">
51
+ <h4 className="mb-3 text-xs font-bold uppercase tracking-widest opacity-60">
52
+ {t.context}
53
+ </h4>
54
+ {!hasContext ? (
55
+ <p className="text-sm text-foreground/50">{t.noContext}</p>
56
+ ) : (
57
+ <ul className="space-y-2 text-sm">
58
+ {disease && (
59
+ <ContextRow
60
+ k="Disease"
61
+ v={`${disease.disease} · ${(disease.confidence * 100).toFixed(0)}%`}
62
+ />
63
+ )}
64
+ {soil && (
65
+ <ContextRow
66
+ k="Soil"
67
+ v={`${soil.soil_type} · ${(soil.confidence * 100).toFixed(0)}%`}
68
+ />
69
+ )}
70
+ {crops && crops[0] && (
71
+ <ContextRow
72
+ k="Top crop"
73
+ v={`${crops[0].crop} · ${(crops[0].probability * 100).toFixed(0)}%`}
74
+ />
75
+ )}
76
+ </ul>
77
+ )}
78
+ </div>
79
+
80
+ <div className="rounded-3xl border border-border bg-card p-5">
81
+ <label className="mb-2 block text-xs font-bold uppercase tracking-widest opacity-60">
82
+ {t.ask}
83
+ </label>
84
+ <textarea
85
+ value={question}
86
+ onChange={(e) => setQuestion(e.target.value)}
87
+ placeholder={t.placeholder}
88
+ rows={4}
89
+ className="w-full resize-none rounded-xl border border-border bg-background px-3 py-2 text-sm outline-none transition-all focus:ring-2 focus:ring-primary/20"
90
+ />
91
+ <div className="mt-3 flex gap-2">
92
+ <button
93
+ onClick={start}
94
+ disabled={streaming}
95
+ className="flex flex-1 items-center justify-center gap-2 rounded-xl bg-primary py-3 text-sm font-semibold text-primary-foreground shadow-md shadow-primary/20 transition-transform hover:scale-[1.01] disabled:opacity-60"
96
+ >
97
+ {streaming ? (
98
+ <>
99
+ <Loader2 className="size-4 animate-spin" /> {t.streaming}
100
+ </>
101
+ ) : (
102
+ <>
103
+ <Sparkles className="size-4" /> {t.ask}
104
+ </>
105
+ )}
106
+ </button>
107
+ {streaming && (
108
+ <button
109
+ onClick={stop}
110
+ className="grid size-11 place-items-center rounded-xl border border-border bg-card text-foreground/70 transition hover:bg-sage-soft"
111
+ aria-label="Stop"
112
+ >
113
+ <Square className="size-4" />
114
+ </button>
115
+ )}
116
+ </div>
117
+ </div>
118
+ </div>
119
+
120
+ <div className="lg:col-span-3">
121
+ <div className="min-h-[400px] rounded-3xl border border-border bg-card p-6 shadow-sm">
122
+ {text ? (
123
+ <FormattedAdvice text={text} streaming={streaming} />
124
+ ) : (
125
+ <div className="grid h-full min-h-[340px] place-items-center text-center">
126
+ <div className="max-w-xs text-sm text-foreground/40">
127
+ <Sparkles className="mx-auto mb-3 size-8 text-primary/40" />
128
+ {t.placeholder}
129
+ </div>
130
+ </div>
131
+ )}
132
+ </div>
133
+ </div>
134
+ </div>
135
+ </TabWrapper>
136
+ );
137
+ }
138
+
139
+ function ContextRow({ k, v }: { k: string; v: string }) {
140
+ return (
141
+ <li className="flex items-center justify-between gap-3">
142
+ <span className="text-[11px] font-bold uppercase tracking-wider opacity-50">
143
+ {k}
144
+ </span>
145
+ <span className="truncate font-mono text-xs">{v}</span>
146
+ </li>
147
+ );
148
+ }
149
+
150
+ function FormattedAdvice({ text, streaming }: { text: string; streaming: boolean }) {
151
+ const lines = text.split("\n");
152
+
153
+ const renderInline = (str: string) => {
154
+ const parts = str.split(/(\*\*.*?\*\*)/g);
155
+ return parts.map((part, i) => {
156
+ if (part.startsWith("**") && part.endsWith("**") && part.length >= 4) {
157
+ return (
158
+ <strong key={i} className="font-semibold text-primary">
159
+ {part.slice(2, -2)}
160
+ </strong>
161
+ );
162
+ }
163
+ return part;
164
+ });
165
+ };
166
+
167
+ const blocks: React.ReactNode[] = [];
168
+ let currentList: React.ReactNode[] = [];
169
+
170
+ const flushList = () => {
171
+ if (currentList.length > 0) {
172
+ blocks.push(
173
+ <ul key={`ul-${blocks.length}`} className="my-2.5 space-y-2 pl-1">
174
+ {currentList}
175
+ </ul>,
176
+ );
177
+ currentList = [];
178
+ }
179
+ };
180
+
181
+ lines.forEach((line, index) => {
182
+ const trimmed = line.trim();
183
+ if (!trimmed) {
184
+ flushList();
185
+ return;
186
+ }
187
+
188
+ // Header standalone line e.g. **Summary:** or ## Summary
189
+ const headerMatch = trimmed.match(/^(?:#{1,3}\s*|\*\*)([^*:]+)(?:\*\*:?|:)?$/i);
190
+ const isHeaderLine =
191
+ Boolean(headerMatch) && !trimmed.startsWith("- ") && !trimmed.startsWith("* ");
192
+
193
+ if (isHeaderLine && headerMatch) {
194
+ flushList();
195
+ const headerTitle = headerMatch[1].trim();
196
+ const badge = getBadgeStyle(headerTitle);
197
+
198
+ blocks.push(
199
+ <div key={`h-${index}`} className="mt-5 mb-2 first:mt-0">
200
+ <span
201
+ className={`inline-flex items-center gap-1.5 rounded-lg border px-2.5 py-1 text-xs font-bold uppercase tracking-wider ${badge.color}`}
202
+ >
203
+ <span>{badge.icon}</span>
204
+ <span>{headerTitle}</span>
205
+ </span>
206
+ </div>,
207
+ );
208
+ return;
209
+ }
210
+
211
+ // Bullet points
212
+ const bulletMatch = trimmed.match(/^[-*•]\s+(.*)$/);
213
+ if (bulletMatch) {
214
+ currentList.push(
215
+ <li
216
+ key={`li-${index}`}
217
+ className="flex items-start gap-2.5 text-sm leading-relaxed text-foreground/90"
218
+ >
219
+ <span className="mt-2 size-1.5 shrink-0 rounded-full bg-primary/70" />
220
+ <span>{renderInline(bulletMatch[1])}</span>
221
+ </li>,
222
+ );
223
+ return;
224
+ }
225
+
226
+ // Inline header e.g. **Summary:** Rest of the sentence
227
+ const inlineHeaderMatch = trimmed.match(/^\*\*([^*:]+):\*\*\s*(.*)$/);
228
+ if (inlineHeaderMatch) {
229
+ flushList();
230
+ const title = inlineHeaderMatch[1].trim();
231
+ const rest = inlineHeaderMatch[2].trim();
232
+ const badge = getBadgeStyle(title);
233
+
234
+ blocks.push(
235
+ <div key={`inline-${index}`} className="mt-4 mb-2 first:mt-0 space-y-2">
236
+ <div>
237
+ <span
238
+ className={`inline-flex items-center gap-1.5 rounded-lg border px-2.5 py-1 text-xs font-bold uppercase tracking-wider ${badge.color}`}
239
+ >
240
+ <span>{badge.icon}</span>
241
+ <span>{title}</span>
242
+ </span>
243
+ </div>
244
+ {rest && (
245
+ <p className="text-sm leading-relaxed text-foreground/90 pl-0.5">
246
+ {renderInline(rest)}
247
+ </p>
248
+ )}
249
+ </div>,
250
+ );
251
+ return;
252
+ }
253
+
254
+ // Regular text paragraph
255
+ flushList();
256
+ blocks.push(
257
+ <p key={`p-${index}`} className="my-2 text-sm leading-relaxed text-foreground/90">
258
+ {renderInline(trimmed)}
259
+ </p>,
260
+ );
261
+ });
262
+
263
+ flushList();
264
+
265
+ return (
266
+ <div className="space-y-1">
267
+ {blocks}
268
+ {streaming && (
269
+ <span className="ml-1 inline-block h-4 w-2 animate-pulse bg-primary align-middle" />
270
+ )}
271
+ </div>
272
+ );
273
+ }
274
+
275
+ function getBadgeStyle(title: string) {
276
+ const lower = title.toLowerCase();
277
+ if (lower.includes("warning") || lower.includes("caution")) {
278
+ return {
279
+ color: "bg-amber-500/10 text-amber-700 border-amber-500/20 dark:text-amber-400",
280
+ icon: "⚠️",
281
+ };
282
+ }
283
+ if (lower.includes("action") || lower.includes("immediate")) {
284
+ return {
285
+ color: "bg-emerald-500/10 text-emerald-700 border-emerald-500/20 dark:text-emerald-400",
286
+ icon: "⚡",
287
+ };
288
+ }
289
+ if (lower.includes("crop") || lower.includes("recommend")) {
290
+ return {
291
+ color: "bg-green-500/10 text-green-700 border-green-500/20 dark:text-green-400",
292
+ icon: "🌾",
293
+ };
294
+ }
295
+ if (lower.includes("soil")) {
296
+ return {
297
+ color: "bg-amber-700/10 text-amber-800 border-amber-700/20 dark:text-amber-300",
298
+ icon: "🪨",
299
+ };
300
+ }
301
+ if (lower.includes("summary")) {
302
+ return {
303
+ color: "bg-blue-500/10 text-blue-700 border-blue-500/20 dark:text-blue-400",
304
+ icon: "📋",
305
+ };
306
+ }
307
+ return {
308
+ color: "bg-primary/10 text-primary border-primary/20",
309
+ icon: "📌",
310
+ };
311
+ }
agriculture-ai-frontend/src/components/agri/ConfidenceBar.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function ConfidenceBar({
2
+ label,
3
+ value,
4
+ valueLabel,
5
+ tone = "primary",
6
+ }: {
7
+ label: string;
8
+ value: number; // 0..1
9
+ valueLabel?: string;
10
+ tone?: "primary" | "accent";
11
+ }) {
12
+ const pct = Math.max(0, Math.min(1, value)) * 100;
13
+ return (
14
+ <div>
15
+ <div className="mb-1.5 flex justify-between text-[11px] font-medium uppercase tracking-wide opacity-60">
16
+ <span>{label}</span>
17
+ <span>{valueLabel ?? `${pct.toFixed(0)}%`}</span>
18
+ </div>
19
+ <div className="h-1.5 w-full overflow-hidden rounded-full bg-sage-soft">
20
+ <div
21
+ className={`h-full rounded-full transition-[width] duration-500 ${
22
+ tone === "accent" ? "bg-accent" : "bg-primary"
23
+ }`}
24
+ style={{ width: `${pct}%` }}
25
+ />
26
+ </div>
27
+ </div>
28
+ );
29
+ }
agriculture-ai-frontend/src/components/agri/CropTab.tsx ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import { toast } from "sonner";
3
+ import { Loader2 } from "lucide-react";
4
+ import { SectionHeader, TabWrapper } from "./Shell";
5
+ import { useAgri } from "@/lib/agri-store";
6
+ import { TRANSLATIONS } from "@/lib/i18n";
7
+ import { recommendCrop, type CropInput } from "@/lib/api";
8
+
9
+ const DEFAULTS: CropInput = {
10
+ N: 42,
11
+ P: 50,
12
+ K: 43,
13
+ temperature: 26,
14
+ humidity: 70,
15
+ ph: 6.5,
16
+ rainfall: 120,
17
+ };
18
+
19
+ export function CropTab() {
20
+ const { lang, crops, setCrops } = useAgri();
21
+ const t = TRANSLATIONS[lang].crop;
22
+ const [input, setInput] = useState<CropInput>(DEFAULTS);
23
+ const [loading, setLoading] = useState(false);
24
+
25
+ const submit = async () => {
26
+ setLoading(true);
27
+ try {
28
+ const res = await recommendCrop(input, lang);
29
+ setCrops(res.recommendations);
30
+ } catch (e) {
31
+ console.error(e);
32
+ toast.error(t.error);
33
+ } finally {
34
+ setLoading(false);
35
+ }
36
+ };
37
+
38
+ const set = (k: keyof CropInput, v: number) =>
39
+ setInput((s) => ({ ...s, [k]: Number.isFinite(v) ? v : 0 }));
40
+
41
+ return (
42
+ <TabWrapper>
43
+ <SectionHeader title={t.title} subtitle={t.subtitle} />
44
+
45
+ <div className="rounded-3xl border border-border bg-sage-soft/40 p-5 sm:p-8">
46
+ <div className="grid gap-10 md:grid-cols-2">
47
+ {/* Form */}
48
+ <div className="space-y-6">
49
+ <div className="grid grid-cols-3 gap-3 sm:gap-4">
50
+ <NumField label={t.n} value={input.N} onChange={(v) => set("N", v)} />
51
+ <NumField label={t.p} value={input.P} onChange={(v) => set("P", v)} />
52
+ <NumField label={t.k} value={input.K} onChange={(v) => set("K", v)} />
53
+ </div>
54
+
55
+ <div className="grid grid-cols-2 gap-3 sm:gap-4">
56
+ <NumField
57
+ label={t.temperature}
58
+ value={input.temperature}
59
+ step={0.1}
60
+ onChange={(v) => set("temperature", v)}
61
+ />
62
+ <NumField
63
+ label={t.humidity}
64
+ value={input.humidity}
65
+ step={0.1}
66
+ onChange={(v) => set("humidity", v)}
67
+ />
68
+ <NumField
69
+ label={t.rainfall}
70
+ value={input.rainfall}
71
+ step={0.1}
72
+ onChange={(v) => set("rainfall", v)}
73
+ />
74
+ <NumField
75
+ label={t.ph}
76
+ value={input.ph}
77
+ step={0.1}
78
+ onChange={(v) => set("ph", v)}
79
+ />
80
+ </div>
81
+
82
+ <div className="space-y-2">
83
+ <div className="flex items-center justify-between">
84
+ <label className="text-[11px] font-bold uppercase tracking-wider">
85
+ {t.ph}
86
+ </label>
87
+ <span className="font-mono text-sm font-bold">
88
+ {input.ph.toFixed(1)}
89
+ </span>
90
+ </div>
91
+ <input
92
+ type="range"
93
+ min={0}
94
+ max={14}
95
+ step={0.1}
96
+ value={input.ph}
97
+ onChange={(e) => set("ph", Number(e.target.value))}
98
+ className="w-full cursor-pointer accent-primary"
99
+ />
100
+ <div className="flex justify-between font-mono text-[9px] opacity-40">
101
+ <span>{t.acidic}</span>
102
+ <span>{t.alkaline}</span>
103
+ </div>
104
+ </div>
105
+
106
+ <button
107
+ onClick={submit}
108
+ disabled={loading}
109
+ className="flex w-full items-center justify-center gap-2 rounded-2xl bg-primary py-4 font-bold tracking-tight text-primary-foreground shadow-lg shadow-primary/20 transition-transform hover:scale-[1.01] disabled:opacity-60"
110
+ >
111
+ {loading ? (
112
+ <>
113
+ <Loader2 className="size-4 animate-spin" />
114
+ {t.submitting}
115
+ </>
116
+ ) : (
117
+ t.submit
118
+ )}
119
+ </button>
120
+ </div>
121
+
122
+ {/* Results */}
123
+ <div className="space-y-3">
124
+ <p className="text-[10px] font-bold uppercase tracking-widest text-foreground/50">
125
+ {t.ranked}
126
+ </p>
127
+ {(crops ?? []).length === 0 ? (
128
+ <div className="grid aspect-[4/3] place-items-center rounded-2xl border border-dashed border-border bg-card/50 p-6 text-center text-sm text-foreground/40">
129
+
130
+ </div>
131
+ ) : (
132
+ (crops ?? []).map((c, i) => {
133
+ const pct = Math.max(0, Math.min(1, c.probability)) * 100;
134
+ const initials = c.crop.slice(0, 2);
135
+ return (
136
+ <div
137
+ key={c.crop + i}
138
+ className="flex items-center gap-4 rounded-2xl border border-border bg-card p-4"
139
+ style={{ opacity: 1 - i * 0.15 }}
140
+ >
141
+ <div className="grid size-12 shrink-0 place-items-center rounded-xl bg-sage-soft font-bold italic text-primary">
142
+ {initials}
143
+ </div>
144
+ <div className="min-w-0 flex-1">
145
+ <div className="mb-1 flex items-end justify-between gap-2">
146
+ <h5 className="truncate text-sm font-bold capitalize">
147
+ {c.crop}
148
+ </h5>
149
+ <span className="font-mono text-[11px] font-bold">
150
+ {pct.toFixed(0)}% {t.match}
151
+ </span>
152
+ </div>
153
+ <div className="h-1 w-full rounded-full bg-sage-soft">
154
+ <div
155
+ className="h-full rounded-full bg-primary transition-[width] duration-500"
156
+ style={{ width: `${pct}%` }}
157
+ />
158
+ </div>
159
+ </div>
160
+ </div>
161
+ );
162
+ })
163
+ )}
164
+ </div>
165
+ </div>
166
+ </div>
167
+ </TabWrapper>
168
+ );
169
+ }
170
+
171
+ function NumField({
172
+ label,
173
+ value,
174
+ onChange,
175
+ step = 1,
176
+ }: {
177
+ label: string;
178
+ value: number;
179
+ onChange: (v: number) => void;
180
+ step?: number;
181
+ }) {
182
+ return (
183
+ <div className="min-w-0">
184
+ <label className="mb-2 block text-[10px] font-bold uppercase tracking-widest opacity-50">
185
+ {label}
186
+ </label>
187
+ <input
188
+ type="number"
189
+ value={value}
190
+ step={step}
191
+ onChange={(e) => onChange(Number(e.target.value))}
192
+ className="w-full rounded-xl border border-border bg-card px-3 py-2.5 font-mono text-sm outline-none transition-all focus:ring-2 focus:ring-primary/20"
193
+ />
194
+ </div>
195
+ );
196
+ }
agriculture-ai-frontend/src/components/agri/DiseaseTab.tsx ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import { toast } from "sonner";
3
+ import { SectionHeader, TabWrapper } from "./Shell";
4
+ import { ImageDrop } from "./ImageDrop";
5
+ import { ConfidenceBar } from "./ConfidenceBar";
6
+ import { useAgri } from "@/lib/agri-store";
7
+ import { TRANSLATIONS } from "@/lib/i18n";
8
+ import { analyzeDisease } from "@/lib/api";
9
+
10
+ export function DiseaseTab() {
11
+ const { lang, disease, setDisease } = useAgri();
12
+ const t = TRANSLATIONS[lang].disease;
13
+ const [loading, setLoading] = useState(false);
14
+
15
+ const urgencyLabel = (u?: string) => {
16
+ if (u === "high") return t.urgencyHigh;
17
+ if (u === "low") return t.urgencyLow;
18
+ return t.urgencyMid;
19
+ };
20
+ const urgencyPct = (u?: string) =>
21
+ u === "high" ? 0.95 : u === "low" ? 0.3 : 0.66;
22
+
23
+ const onFile = async (f: File) => {
24
+ setLoading(true);
25
+ try {
26
+ const res = await analyzeDisease(f, lang);
27
+ setDisease(res);
28
+ } catch (e) {
29
+ console.error(e);
30
+ toast.error(t.error);
31
+ } finally {
32
+ setLoading(false);
33
+ }
34
+ };
35
+
36
+ return (
37
+ <TabWrapper>
38
+ <SectionHeader
39
+ title={t.title}
40
+ subtitle={t.subtitle}
41
+ workflowId={t.workflowId}
42
+ />
43
+
44
+ <div className="grid gap-6 lg:grid-cols-5">
45
+ <div className="lg:col-span-3">
46
+ <ImageDrop
47
+ title={t.dropTitle}
48
+ hint={t.dropHint}
49
+ cta={t.select}
50
+ loading={loading}
51
+ onFile={onFile}
52
+ onClear={() => setDisease(null)}
53
+ />
54
+ </div>
55
+
56
+ <div className="space-y-4 lg:col-span-2">
57
+ <div className="rounded-3xl border border-border bg-card p-6 shadow-sm">
58
+ <div className="mb-6 flex items-start justify-between gap-3">
59
+ <span className="rounded border border-amber-200 bg-amber-50 px-2.5 py-1 text-[10px] font-bold uppercase tracking-wider text-amber-700">
60
+ {t.result}
61
+ </span>
62
+ <div className="text-right">
63
+ <span className="block font-mono text-[24px] font-bold text-primary">
64
+ {disease
65
+ ? (disease.confidence * 100).toFixed(1)
66
+ : "—"}
67
+ <span className="text-sm">%</span>
68
+ </span>
69
+ <span className="text-[10px] uppercase tracking-tighter opacity-40">
70
+ {t.confidence}
71
+ </span>
72
+ </div>
73
+ </div>
74
+
75
+ <div className="mb-8">
76
+ <h3 className="text-xl font-bold">
77
+ {disease?.disease ?? "—"}
78
+ </h3>
79
+ {disease?.scientific_name && (
80
+ <p className="mt-1 font-mono text-xs italic text-foreground/50">
81
+ {disease.scientific_name}
82
+ </p>
83
+ )}
84
+ </div>
85
+
86
+ <ConfidenceBar
87
+ label={t.urgency}
88
+ value={disease ? urgencyPct(disease.urgency) : 0}
89
+ valueLabel={disease ? urgencyLabel(disease.urgency) : "—"}
90
+ tone="accent"
91
+ />
92
+ </div>
93
+
94
+ {disease?.treatment ? (
95
+ <div className="rounded-2xl bg-primary p-5 text-primary-foreground">
96
+ <h4 className="mb-3 text-xs font-bold uppercase tracking-widest opacity-70">
97
+ {t.action}
98
+ </h4>
99
+ <p className="text-sm leading-relaxed text-sage-soft">
100
+ {disease.treatment}
101
+ </p>
102
+ </div>
103
+ ) : null}
104
+ </div>
105
+ </div>
106
+ </TabWrapper>
107
+ );
108
+ }
agriculture-ai-frontend/src/components/agri/ImageDrop.tsx ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useRef, useState, type DragEvent } from "react";
2
+ import { Loader2, UploadCloud, X } from "lucide-react";
3
+
4
+ export function ImageDrop({
5
+ title,
6
+ hint,
7
+ cta,
8
+ loading,
9
+ onFile,
10
+ onClear,
11
+ }: {
12
+ title: string;
13
+ hint: string;
14
+ cta: string;
15
+ loading?: boolean;
16
+ onFile: (f: File) => void;
17
+ onClear?: () => void;
18
+ }) {
19
+ const inputRef = useRef<HTMLInputElement>(null);
20
+ const [preview, setPreview] = useState<string | null>(null);
21
+ const [dragOver, setDragOver] = useState(false);
22
+
23
+ const handle = (f: File) => {
24
+ setPreview(URL.createObjectURL(f));
25
+ onFile(f);
26
+ };
27
+
28
+ const onDrop = (e: DragEvent<HTMLDivElement>) => {
29
+ e.preventDefault();
30
+ setDragOver(false);
31
+ const f = e.dataTransfer.files?.[0];
32
+ if (f) handle(f);
33
+ };
34
+
35
+ const clear = () => {
36
+ setPreview(null);
37
+ if (inputRef.current) inputRef.current.value = "";
38
+ onClear?.();
39
+ };
40
+
41
+ return (
42
+ <div className="group">
43
+ <div
44
+ onDragOver={(e) => {
45
+ e.preventDefault();
46
+ setDragOver(true);
47
+ }}
48
+ onDragLeave={() => setDragOver(false)}
49
+ onDrop={onDrop}
50
+ className={`relative flex aspect-[4/3] flex-col items-center justify-center overflow-hidden rounded-3xl border-2 border-dashed p-6 text-center transition-all sm:p-8 ${
51
+ dragOver
52
+ ? "border-primary bg-sage-mid"
53
+ : "border-sage-mid bg-sage-soft group-hover:border-primary/40"
54
+ }`}
55
+ >
56
+ {preview ? (
57
+ <>
58
+ <img
59
+ src={preview}
60
+ alt="preview"
61
+ className="absolute inset-0 h-full w-full object-cover"
62
+ />
63
+ {loading ? (
64
+ <div className="absolute inset-0 grid place-items-center bg-background/70 backdrop-blur-sm">
65
+ <Loader2 className="size-8 animate-spin text-primary" />
66
+ </div>
67
+ ) : (
68
+ <button
69
+ onClick={clear}
70
+ className="absolute right-3 top-3 grid size-9 place-items-center rounded-full bg-white/90 shadow-md backdrop-blur transition hover:scale-105"
71
+ aria-label="Clear"
72
+ >
73
+ <X className="size-4" />
74
+ </button>
75
+ )}
76
+ </>
77
+ ) : (
78
+ <div className="relative z-10">
79
+ <div className="mx-auto mb-4 grid size-16 place-items-center rounded-full border border-border bg-white shadow-sm transition-transform duration-300 group-hover:scale-105">
80
+ {loading ? (
81
+ <Loader2 className="size-7 animate-spin text-primary" />
82
+ ) : (
83
+ <UploadCloud className="size-7 text-primary" />
84
+ )}
85
+ </div>
86
+ <h3 className="font-semibold">{title}</h3>
87
+ <p className="mt-1 text-xs text-foreground/50">{hint}</p>
88
+ <button
89
+ type="button"
90
+ onClick={() => inputRef.current?.click()}
91
+ className="mt-6 rounded-full bg-primary px-6 py-2.5 text-sm font-medium text-primary-foreground shadow-lg shadow-primary/10 transition-colors hover:bg-primary/90"
92
+ >
93
+ {cta}
94
+ </button>
95
+ </div>
96
+ )}
97
+ <input
98
+ ref={inputRef}
99
+ type="file"
100
+ accept="image/*"
101
+ className="hidden"
102
+ onChange={(e) => {
103
+ const f = e.target.files?.[0];
104
+ if (f) handle(f);
105
+ }}
106
+ />
107
+ </div>
108
+ </div>
109
+ );
110
+ }
agriculture-ai-frontend/src/components/agri/Shell.tsx ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useMemo, useState, type ReactNode } from "react";
2
+ import { Leaf, Sprout, TestTube2, Wheat, MessagesSquare } from "lucide-react";
3
+ import { AgriContext } from "@/lib/agri-store";
4
+ import { LANGUAGES, TRANSLATIONS, type LangCode } from "@/lib/i18n";
5
+ import type { CropSuggestion, DiseaseResult, SoilResult } from "@/lib/api";
6
+ import { DiseaseTab } from "./DiseaseTab";
7
+ import { SoilTab } from "./SoilTab";
8
+ import { CropTab } from "./CropTab";
9
+ import { AdviceTab } from "./AdviceTab";
10
+
11
+ type TabId = "disease" | "soil" | "crop" | "advice";
12
+
13
+ const NAV: { id: TabId; icon: typeof Leaf }[] = [
14
+ { id: "disease", icon: Leaf },
15
+ { id: "soil", icon: TestTube2 },
16
+ { id: "crop", icon: Wheat },
17
+ { id: "advice", icon: MessagesSquare },
18
+ ];
19
+
20
+ export function Shell() {
21
+ const [lang, setLang] = useState<LangCode>("en");
22
+ const [tab, setTab] = useState<TabId>("disease");
23
+ const [disease, setDisease] = useState<DiseaseResult | null>(null);
24
+ const [soil, setSoil] = useState<SoilResult | null>(null);
25
+ const [crops, setCrops] = useState<CropSuggestion[] | null>(null);
26
+
27
+ const t = TRANSLATIONS[lang];
28
+
29
+ const store = useMemo(
30
+ () => ({ lang, setLang, disease, setDisease, soil, setSoil, crops, setCrops }),
31
+ [lang, disease, soil, crops],
32
+ );
33
+
34
+ return (
35
+ <AgriContext.Provider value={store}>
36
+ <div className="min-h-screen bg-background text-foreground selection:bg-primary/10">
37
+ {/* Desktop sidebar */}
38
+ <nav className="fixed top-0 left-0 z-50 hidden h-full w-64 flex-col border-r border-border bg-sage-soft p-6 md:flex">
39
+ <div className="mb-10 flex items-center gap-2">
40
+ <div className="grid size-9 place-items-center rounded-lg bg-primary text-primary-foreground">
41
+ <Sprout className="size-5" />
42
+ </div>
43
+ <div className="min-w-0">
44
+ <h1 className="truncate text-base font-bold uppercase italic tracking-tight text-primary">
45
+ {t.brand}
46
+ </h1>
47
+ <p className="mt-0.5 font-mono text-[10px] uppercase tracking-widest text-foreground/50">
48
+ {t.brandSub}
49
+ </p>
50
+ </div>
51
+ </div>
52
+
53
+ <div className="flex-1 space-y-1.5">
54
+ {NAV.map((n) => {
55
+ const active = tab === n.id;
56
+ const Icon = n.icon;
57
+ return (
58
+ <button
59
+ key={n.id}
60
+ onClick={() => setTab(n.id)}
61
+ className={`flex w-full items-center gap-3 rounded-xl px-4 py-3 text-sm font-medium transition-all ${
62
+ active
63
+ ? "bg-primary text-primary-foreground shadow-sm shadow-primary/20"
64
+ : "text-foreground/70 hover:bg-sage-mid"
65
+ }`}
66
+ >
67
+ <Icon className="size-4 shrink-0" />
68
+ <span className="truncate">{t.nav[n.id]}</span>
69
+ </button>
70
+ );
71
+ })}
72
+ </div>
73
+
74
+ <div className="border-t border-border pt-6">
75
+ <div className="flex items-center gap-2 rounded-lg border border-border bg-sage-mid/50 p-2">
76
+ <span className="size-2 shrink-0 animate-pulse rounded-full bg-emerald-500" />
77
+ <span className="truncate font-mono text-[11px] text-foreground/60">
78
+ {t.systemReady}
79
+ </span>
80
+ </div>
81
+ </div>
82
+ </nav>
83
+
84
+ {/* Header */}
85
+ <header className="sticky top-0 z-40 bg-background/80 backdrop-blur-md md:pl-64">
86
+ <div className="mx-auto grid h-16 max-w-7xl grid-cols-[minmax(0,1fr)_auto] items-center gap-4 px-4 sm:px-6">
87
+ <div className="flex min-w-0 items-center gap-2 md:hidden">
88
+ <Sprout className="size-4 shrink-0 text-primary" />
89
+ <span className="truncate font-bold italic text-primary">
90
+ {t.brand}
91
+ </span>
92
+ </div>
93
+ <div className="hidden md:block" />
94
+ <LanguageSelect lang={lang} onChange={setLang} />
95
+ </div>
96
+ </header>
97
+
98
+ {/* Main */}
99
+ <main className="pb-28 md:pb-12 md:pl-64">
100
+ <div className="mx-auto max-w-5xl space-y-8 p-4 sm:p-6">
101
+ {tab === "disease" && <DiseaseTab />}
102
+ {tab === "soil" && <SoilTab />}
103
+ {tab === "crop" && <CropTab />}
104
+ {tab === "advice" && <AdviceTab />}
105
+ </div>
106
+ </main>
107
+
108
+ {/* Mobile bottom nav */}
109
+ <nav className="animate-reveal fixed bottom-6 left-1/2 z-50 flex h-16 w-[calc(100%-2rem)] max-w-md -translate-x-1/2 items-center justify-around rounded-full border border-border bg-white px-2 shadow-2xl md:hidden">
110
+ {NAV.map((n) => {
111
+ const active = tab === n.id;
112
+ const Icon = n.icon;
113
+ return (
114
+ <button
115
+ key={n.id}
116
+ onClick={() => setTab(n.id)}
117
+ aria-label={t.nav[n.id]}
118
+ className={`grid place-items-center rounded-full p-3 transition-all ${
119
+ active
120
+ ? "bg-primary text-primary-foreground shadow-md shadow-primary/20"
121
+ : "text-foreground/40"
122
+ }`}
123
+ >
124
+ <Icon className="size-5" />
125
+ </button>
126
+ );
127
+ })}
128
+ </nav>
129
+ </div>
130
+ </AgriContext.Provider>
131
+ );
132
+ }
133
+
134
+ function LanguageSelect({
135
+ lang,
136
+ onChange,
137
+ }: {
138
+ lang: LangCode;
139
+ onChange: (l: LangCode) => void;
140
+ }) {
141
+ return (
142
+ <label className="relative flex shrink-0 items-center">
143
+ <select
144
+ value={lang}
145
+ onChange={(e) => onChange(e.target.value as LangCode)}
146
+ className="cursor-pointer appearance-none rounded-full border border-border bg-sage-soft px-4 py-1.5 pr-8 text-xs font-medium text-foreground outline-none transition-colors hover:border-primary/30 focus:ring-2 focus:ring-primary/20"
147
+ >
148
+ {LANGUAGES.map((l) => (
149
+ <option key={l.code} value={l.code}>
150
+ {l.native}
151
+ </option>
152
+ ))}
153
+ </select>
154
+ <span className="pointer-events-none absolute right-3 text-[8px] opacity-40">
155
+
156
+ </span>
157
+ </label>
158
+ );
159
+ }
160
+
161
+ export function SectionHeader({
162
+ title,
163
+ subtitle,
164
+ workflowId,
165
+ }: {
166
+ title: string;
167
+ subtitle: string;
168
+ workflowId?: string;
169
+ }) {
170
+ return (
171
+ <header className="mb-6 grid grid-cols-[minmax(0,1fr)_auto] items-end gap-4">
172
+ <div className="min-w-0">
173
+ <h2 className="truncate text-2xl font-bold tracking-tight">{title}</h2>
174
+ <p className="text-sm text-foreground/60">{subtitle}</p>
175
+ </div>
176
+ {workflowId ? (
177
+ <div className="hidden text-right sm:block">
178
+ <span className="font-mono text-[10px] uppercase tracking-wider opacity-40">
179
+ Workflow ID
180
+ </span>
181
+ <p className="font-mono text-xs">{workflowId}</p>
182
+ </div>
183
+ ) : null}
184
+ </header>
185
+ );
186
+ }
187
+
188
+ export function TabWrapper({ children }: { children: ReactNode }) {
189
+ return <section className="animate-reveal">{children}</section>;
190
+ }
agriculture-ai-frontend/src/components/agri/SoilTab.tsx ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import { toast } from "sonner";
3
+ import { SectionHeader, TabWrapper } from "./Shell";
4
+ import { ImageDrop } from "./ImageDrop";
5
+ import { ConfidenceBar } from "./ConfidenceBar";
6
+ import { useAgri } from "@/lib/agri-store";
7
+ import { TRANSLATIONS } from "@/lib/i18n";
8
+ import { analyzeSoil } from "@/lib/api";
9
+
10
+ export function SoilTab() {
11
+ const { lang, soil, setSoil } = useAgri();
12
+ const t = TRANSLATIONS[lang].soil;
13
+ const [loading, setLoading] = useState(false);
14
+
15
+ const onFile = async (f: File) => {
16
+ setLoading(true);
17
+ try {
18
+ const res = await analyzeSoil(f, lang);
19
+ setSoil(res);
20
+ } catch (e) {
21
+ console.error(e);
22
+ toast.error(t.error);
23
+ } finally {
24
+ setLoading(false);
25
+ }
26
+ };
27
+
28
+ const props = soil?.properties
29
+ ? Object.entries(soil.properties)
30
+ : [];
31
+
32
+ return (
33
+ <TabWrapper>
34
+ <SectionHeader
35
+ title={t.title}
36
+ subtitle={t.subtitle}
37
+ workflowId={t.workflowId}
38
+ />
39
+
40
+ <div className="grid gap-6 lg:grid-cols-5">
41
+ <div className="lg:col-span-3">
42
+ <ImageDrop
43
+ title={t.dropTitle}
44
+ hint={t.dropHint}
45
+ cta={t.select}
46
+ loading={loading}
47
+ onFile={onFile}
48
+ onClear={() => setSoil(null)}
49
+ />
50
+ </div>
51
+
52
+ <div className="space-y-4 lg:col-span-2">
53
+ <div className="rounded-3xl border border-border bg-card p-6 shadow-sm">
54
+ <div className="mb-6 flex items-start justify-between gap-3">
55
+ <span className="rounded border border-emerald-200 bg-emerald-50 px-2.5 py-1 text-[10px] font-bold uppercase tracking-wider text-emerald-700">
56
+ {t.result}
57
+ </span>
58
+ <div className="text-right">
59
+ <span className="block font-mono text-[24px] font-bold text-primary">
60
+ {soil ? (soil.confidence * 100).toFixed(1) : "—"}
61
+ <span className="text-sm">%</span>
62
+ </span>
63
+ <span className="text-[10px] uppercase tracking-tighter opacity-40">
64
+ {t.confidence}
65
+ </span>
66
+ </div>
67
+ </div>
68
+
69
+ <div className="mb-6">
70
+ <h3 className="text-xl font-bold">{soil?.soil_type ?? "—"}</h3>
71
+ </div>
72
+
73
+ <ConfidenceBar
74
+ label={t.confidence}
75
+ value={soil?.confidence ?? 0}
76
+ />
77
+ </div>
78
+
79
+ {props.length > 0 ? (
80
+ <div className="rounded-2xl border border-border bg-card p-5">
81
+ <h4 className="mb-3 text-xs font-bold uppercase tracking-widest opacity-60">
82
+ {t.properties}
83
+ </h4>
84
+ <dl className="space-y-2">
85
+ {props.map(([k, v]) => (
86
+ <div
87
+ key={k}
88
+ className="flex items-center justify-between gap-3 text-sm"
89
+ >
90
+ <dt className="truncate capitalize text-foreground/70">
91
+ {k.replace(/_/g, " ")}
92
+ </dt>
93
+ <dd className="font-mono text-xs font-semibold text-primary">
94
+ {String(v)}
95
+ </dd>
96
+ </div>
97
+ ))}
98
+ </dl>
99
+ </div>
100
+ ) : null}
101
+ </div>
102
+ </div>
103
+ </TabWrapper>
104
+ );
105
+ }
agriculture-ai-frontend/src/components/ui/accordion.tsx ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as AccordionPrimitive from "@radix-ui/react-accordion";
3
+ import { ChevronDown } from "lucide-react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const Accordion = AccordionPrimitive.Root;
8
+
9
+ const AccordionItem = React.forwardRef<
10
+ React.ElementRef<typeof AccordionPrimitive.Item>,
11
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
12
+ >(({ className, ...props }, ref) => (
13
+ <AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} />
14
+ ));
15
+ AccordionItem.displayName = "AccordionItem";
16
+
17
+ const AccordionTrigger = React.forwardRef<
18
+ React.ElementRef<typeof AccordionPrimitive.Trigger>,
19
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
20
+ >(({ className, children, ...props }, ref) => (
21
+ <AccordionPrimitive.Header className="flex">
22
+ <AccordionPrimitive.Trigger
23
+ ref={ref}
24
+ className={cn(
25
+ "flex flex-1 items-center justify-between py-4 text-sm font-medium cursor-pointer transition-all hover:underline text-left [&[data-state=open]>svg]:rotate-180",
26
+ className,
27
+ )}
28
+ {...props}
29
+ >
30
+ {children}
31
+ <ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
32
+ </AccordionPrimitive.Trigger>
33
+ </AccordionPrimitive.Header>
34
+ ));
35
+ AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
36
+
37
+ const AccordionContent = React.forwardRef<
38
+ React.ElementRef<typeof AccordionPrimitive.Content>,
39
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
40
+ >(({ className, children, ...props }, ref) => (
41
+ <AccordionPrimitive.Content
42
+ ref={ref}
43
+ className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
44
+ {...props}
45
+ >
46
+ <div className={cn("pb-4 pt-0", className)}>{children}</div>
47
+ </AccordionPrimitive.Content>
48
+ ));
49
+ AccordionContent.displayName = AccordionPrimitive.Content.displayName;
50
+
51
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
agriculture-ai-frontend/src/components/ui/alert-dialog.tsx ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
3
+
4
+ import { cn } from "@/lib/utils";
5
+ import { buttonVariants } from "@/components/ui/button";
6
+
7
+ const AlertDialog = AlertDialogPrimitive.Root;
8
+
9
+ const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
10
+
11
+ const AlertDialogPortal = AlertDialogPrimitive.Portal;
12
+
13
+ const AlertDialogOverlay = React.forwardRef<
14
+ React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
15
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
16
+ >(({ className, ...props }, ref) => (
17
+ <AlertDialogPrimitive.Overlay
18
+ className={cn(
19
+ "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
20
+ className,
21
+ )}
22
+ {...props}
23
+ ref={ref}
24
+ />
25
+ ));
26
+ AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
27
+
28
+ const AlertDialogContent = React.forwardRef<
29
+ React.ElementRef<typeof AlertDialogPrimitive.Content>,
30
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
31
+ >(({ className, ...props }, ref) => (
32
+ <AlertDialogPortal>
33
+ <AlertDialogOverlay />
34
+ <AlertDialogPrimitive.Content
35
+ ref={ref}
36
+ className={cn(
37
+ "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 sm:rounded-lg",
38
+ className,
39
+ )}
40
+ {...props}
41
+ />
42
+ </AlertDialogPortal>
43
+ ));
44
+ AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
45
+
46
+ const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
47
+ <div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
48
+ );
49
+ AlertDialogHeader.displayName = "AlertDialogHeader";
50
+
51
+ const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
52
+ <div
53
+ className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
54
+ {...props}
55
+ />
56
+ );
57
+ AlertDialogFooter.displayName = "AlertDialogFooter";
58
+
59
+ const AlertDialogTitle = React.forwardRef<
60
+ React.ElementRef<typeof AlertDialogPrimitive.Title>,
61
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
62
+ >(({ className, ...props }, ref) => (
63
+ <AlertDialogPrimitive.Title
64
+ ref={ref}
65
+ className={cn("text-lg font-semibold", className)}
66
+ {...props}
67
+ />
68
+ ));
69
+ AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
70
+
71
+ const AlertDialogDescription = React.forwardRef<
72
+ React.ElementRef<typeof AlertDialogPrimitive.Description>,
73
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
74
+ >(({ className, ...props }, ref) => (
75
+ <AlertDialogPrimitive.Description
76
+ ref={ref}
77
+ className={cn("text-sm text-muted-foreground", className)}
78
+ {...props}
79
+ />
80
+ ));
81
+ AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;
82
+
83
+ const AlertDialogAction = React.forwardRef<
84
+ React.ElementRef<typeof AlertDialogPrimitive.Action>,
85
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
86
+ >(({ className, ...props }, ref) => (
87
+ <AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
88
+ ));
89
+ AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
90
+
91
+ const AlertDialogCancel = React.forwardRef<
92
+ React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
93
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
94
+ >(({ className, ...props }, ref) => (
95
+ <AlertDialogPrimitive.Cancel
96
+ ref={ref}
97
+ className={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
98
+ {...props}
99
+ />
100
+ ));
101
+ AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
102
+
103
+ export {
104
+ AlertDialog,
105
+ AlertDialogPortal,
106
+ AlertDialogOverlay,
107
+ AlertDialogTrigger,
108
+ AlertDialogContent,
109
+ AlertDialogHeader,
110
+ AlertDialogFooter,
111
+ AlertDialogTitle,
112
+ AlertDialogDescription,
113
+ AlertDialogAction,
114
+ AlertDialogCancel,
115
+ };
agriculture-ai-frontend/src/components/ui/alert.tsx ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ const alertVariants = cva(
7
+ "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-background text-foreground",
12
+ destructive:
13
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ variant: "default",
18
+ },
19
+ },
20
+ );
21
+
22
+ const Alert = React.forwardRef<
23
+ HTMLDivElement,
24
+ React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
25
+ >(({ className, variant, ...props }, ref) => (
26
+ <div ref={ref} role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
27
+ ));
28
+ Alert.displayName = "Alert";
29
+
30
+ const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
31
+ ({ className, ...props }, ref) => (
32
+ <h5
33
+ ref={ref}
34
+ className={cn("mb-1 font-medium leading-none tracking-tight", className)}
35
+ {...props}
36
+ />
37
+ ),
38
+ );
39
+ AlertTitle.displayName = "AlertTitle";
40
+
41
+ const AlertDescription = React.forwardRef<
42
+ HTMLParagraphElement,
43
+ React.HTMLAttributes<HTMLParagraphElement>
44
+ >(({ className, ...props }, ref) => (
45
+ <div ref={ref} className={cn("text-sm [&_p]:leading-relaxed", className)} {...props} />
46
+ ));
47
+ AlertDescription.displayName = "AlertDescription";
48
+
49
+ export { Alert, AlertTitle, AlertDescription };
agriculture-ai-frontend/src/components/ui/aspect-ratio.tsx ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
2
+
3
+ const AspectRatio = AspectRatioPrimitive.Root;
4
+
5
+ export { AspectRatio };
agriculture-ai-frontend/src/components/ui/avatar.tsx ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as AvatarPrimitive from "@radix-ui/react-avatar";
5
+
6
+ import { cn } from "@/lib/utils";
7
+
8
+ const Avatar = React.forwardRef<
9
+ React.ElementRef<typeof AvatarPrimitive.Root>,
10
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
11
+ >(({ className, ...props }, ref) => (
12
+ <AvatarPrimitive.Root
13
+ ref={ref}
14
+ className={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
15
+ {...props}
16
+ />
17
+ ));
18
+ Avatar.displayName = AvatarPrimitive.Root.displayName;
19
+
20
+ const AvatarImage = React.forwardRef<
21
+ React.ElementRef<typeof AvatarPrimitive.Image>,
22
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
23
+ >(({ className, ...props }, ref) => (
24
+ <AvatarPrimitive.Image
25
+ ref={ref}
26
+ className={cn("aspect-square h-full w-full", className)}
27
+ {...props}
28
+ />
29
+ ));
30
+ AvatarImage.displayName = AvatarPrimitive.Image.displayName;
31
+
32
+ const AvatarFallback = React.forwardRef<
33
+ React.ElementRef<typeof AvatarPrimitive.Fallback>,
34
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
35
+ >(({ className, ...props }, ref) => (
36
+ <AvatarPrimitive.Fallback
37
+ ref={ref}
38
+ className={cn(
39
+ "flex h-full w-full items-center justify-center rounded-full bg-muted",
40
+ className,
41
+ )}
42
+ {...props}
43
+ />
44
+ ));
45
+ AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
46
+
47
+ export { Avatar, AvatarImage, AvatarFallback };
agriculture-ai-frontend/src/components/ui/badge.tsx ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ const badgeVariants = cva(
7
+ "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
12
+ secondary:
13
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
14
+ destructive:
15
+ "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
16
+ outline: "text-foreground",
17
+ },
18
+ },
19
+ defaultVariants: {
20
+ variant: "default",
21
+ },
22
+ },
23
+ );
24
+
25
+ export interface BadgeProps
26
+ extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
27
+
28
+ function Badge({ className, variant, ...props }: BadgeProps) {
29
+ return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
30
+ }
31
+
32
+ export { Badge, badgeVariants };
agriculture-ai-frontend/src/components/ui/breadcrumb.tsx ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { ChevronRight, MoreHorizontal } from "lucide-react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const Breadcrumb = React.forwardRef<
8
+ HTMLElement,
9
+ React.ComponentPropsWithoutRef<"nav"> & {
10
+ separator?: React.ReactNode;
11
+ }
12
+ >(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
13
+ Breadcrumb.displayName = "Breadcrumb";
14
+
15
+ const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<"ol">>(
16
+ ({ className, ...props }, ref) => (
17
+ <ol
18
+ ref={ref}
19
+ className={cn(
20
+ "flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
21
+ className,
22
+ )}
23
+ {...props}
24
+ />
25
+ ),
26
+ );
27
+ BreadcrumbList.displayName = "BreadcrumbList";
28
+
29
+ const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<"li">>(
30
+ ({ className, ...props }, ref) => (
31
+ <li ref={ref} className={cn("inline-flex items-center gap-1.5", className)} {...props} />
32
+ ),
33
+ );
34
+ BreadcrumbItem.displayName = "BreadcrumbItem";
35
+
36
+ const BreadcrumbLink = React.forwardRef<
37
+ HTMLAnchorElement,
38
+ React.ComponentPropsWithoutRef<"a"> & {
39
+ asChild?: boolean;
40
+ }
41
+ >(({ asChild, className, ...props }, ref) => {
42
+ const Comp = asChild ? Slot : "a";
43
+
44
+ return (
45
+ <Comp
46
+ ref={ref}
47
+ className={cn("transition-colors hover:text-foreground", className)}
48
+ {...props}
49
+ />
50
+ );
51
+ });
52
+ BreadcrumbLink.displayName = "BreadcrumbLink";
53
+
54
+ const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWithoutRef<"span">>(
55
+ ({ className, ...props }, ref) => (
56
+ <span
57
+ ref={ref}
58
+ role="link"
59
+ aria-disabled="true"
60
+ aria-current="page"
61
+ className={cn("font-normal text-foreground", className)}
62
+ {...props}
63
+ />
64
+ ),
65
+ );
66
+ BreadcrumbPage.displayName = "BreadcrumbPage";
67
+
68
+ const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => (
69
+ <li
70
+ role="presentation"
71
+ aria-hidden="true"
72
+ className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
73
+ {...props}
74
+ >
75
+ {children ?? <ChevronRight />}
76
+ </li>
77
+ );
78
+ BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
79
+
80
+ const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
81
+ <span
82
+ role="presentation"
83
+ aria-hidden="true"
84
+ className={cn("flex h-9 w-9 items-center justify-center", className)}
85
+ {...props}
86
+ >
87
+ <MoreHorizontal className="h-4 w-4" />
88
+ <span className="sr-only">More</span>
89
+ </span>
90
+ );
91
+ BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
92
+
93
+ export {
94
+ Breadcrumb,
95
+ BreadcrumbList,
96
+ BreadcrumbItem,
97
+ BreadcrumbLink,
98
+ BreadcrumbPage,
99
+ BreadcrumbSeparator,
100
+ BreadcrumbEllipsis,
101
+ };
agriculture-ai-frontend/src/components/ui/button.tsx ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
13
+ destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
14
+ outline:
15
+ "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
16
+ secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
17
+ ghost: "hover:bg-accent hover:text-accent-foreground",
18
+ link: "text-primary underline-offset-4 hover:underline",
19
+ },
20
+ size: {
21
+ default: "h-9 px-4 py-2",
22
+ sm: "h-8 rounded-md px-3 text-xs",
23
+ lg: "h-10 rounded-md px-8",
24
+ icon: "h-9 w-9",
25
+ },
26
+ },
27
+ defaultVariants: {
28
+ variant: "default",
29
+ size: "default",
30
+ },
31
+ },
32
+ );
33
+
34
+ export interface ButtonProps
35
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
36
+ asChild?: boolean;
37
+ }
38
+
39
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
40
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
41
+ const Comp = asChild ? Slot : "button";
42
+ return (
43
+ <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
44
+ );
45
+ },
46
+ );
47
+ Button.displayName = "Button";
48
+
49
+ export { Button, buttonVariants };
agriculture-ai-frontend/src/components/ui/calendar.tsx ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
5
+ import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker";
6
+
7
+ import { cn } from "@/lib/utils";
8
+ import { Button, buttonVariants } from "@/components/ui/button";
9
+
10
+ function Calendar({
11
+ className,
12
+ classNames,
13
+ showOutsideDays = true,
14
+ captionLayout = "label",
15
+ buttonVariant = "ghost",
16
+ formatters,
17
+ components,
18
+ ...props
19
+ }: React.ComponentProps<typeof DayPicker> & {
20
+ buttonVariant?: React.ComponentProps<typeof Button>["variant"];
21
+ }) {
22
+ const defaultClassNames = getDefaultClassNames();
23
+
24
+ return (
25
+ <DayPicker
26
+ showOutsideDays={showOutsideDays}
27
+ className={cn(
28
+ "bg-background group/calendar p-3 [--cell-size:2rem] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
29
+ String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
30
+ String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
31
+ className,
32
+ )}
33
+ captionLayout={captionLayout}
34
+ formatters={{
35
+ formatMonthDropdown: (date) => date.toLocaleString("default", { month: "short" }),
36
+ ...formatters,
37
+ }}
38
+ classNames={{
39
+ root: cn("w-fit", defaultClassNames.root),
40
+ months: cn("relative flex flex-col gap-4 md:flex-row", defaultClassNames.months),
41
+ month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
42
+ nav: cn(
43
+ "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
44
+ defaultClassNames.nav,
45
+ ),
46
+ button_previous: cn(
47
+ buttonVariants({ variant: buttonVariant }),
48
+ "h-(--cell-size) w-(--cell-size) select-none p-0 aria-disabled:opacity-50",
49
+ defaultClassNames.button_previous,
50
+ ),
51
+ button_next: cn(
52
+ buttonVariants({ variant: buttonVariant }),
53
+ "h-(--cell-size) w-(--cell-size) select-none p-0 aria-disabled:opacity-50",
54
+ defaultClassNames.button_next,
55
+ ),
56
+ month_caption: cn(
57
+ "flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)",
58
+ defaultClassNames.month_caption,
59
+ ),
60
+ dropdowns: cn(
61
+ "flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium",
62
+ defaultClassNames.dropdowns,
63
+ ),
64
+ dropdown_root: cn(
65
+ "has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border",
66
+ defaultClassNames.dropdown_root,
67
+ ),
68
+ dropdown: cn("bg-popover absolute inset-0 opacity-0", defaultClassNames.dropdown),
69
+ caption_label: cn(
70
+ "select-none font-medium",
71
+ captionLayout === "label"
72
+ ? "text-sm"
73
+ : "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5",
74
+ defaultClassNames.caption_label,
75
+ ),
76
+ table: "w-full border-collapse",
77
+ weekdays: cn("flex", defaultClassNames.weekdays),
78
+ weekday: cn(
79
+ "text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal",
80
+ defaultClassNames.weekday,
81
+ ),
82
+ week: cn("mt-2 flex w-full", defaultClassNames.week),
83
+ week_number_header: cn("w-(--cell-size) select-none", defaultClassNames.week_number_header),
84
+ week_number: cn(
85
+ "text-muted-foreground select-none text-[0.8rem]",
86
+ defaultClassNames.week_number,
87
+ ),
88
+ day: cn(
89
+ "group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md",
90
+ defaultClassNames.day,
91
+ ),
92
+ range_start: cn("bg-accent rounded-l-md", defaultClassNames.range_start),
93
+ range_middle: cn("rounded-none", defaultClassNames.range_middle),
94
+ range_end: cn("bg-accent rounded-r-md", defaultClassNames.range_end),
95
+ today: cn(
96
+ "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
97
+ defaultClassNames.today,
98
+ ),
99
+ outside: cn(
100
+ "text-muted-foreground aria-selected:text-muted-foreground",
101
+ defaultClassNames.outside,
102
+ ),
103
+ disabled: cn("text-muted-foreground opacity-50", defaultClassNames.disabled),
104
+ hidden: cn("invisible", defaultClassNames.hidden),
105
+ ...classNames,
106
+ }}
107
+ components={{
108
+ Root: ({ className, rootRef, ...props }) => {
109
+ return <div data-slot="calendar" ref={rootRef} className={cn(className)} {...props} />;
110
+ },
111
+ Chevron: ({ className, orientation, ...props }) => {
112
+ if (orientation === "left") {
113
+ return <ChevronLeftIcon className={cn("size-4", className)} {...props} />;
114
+ }
115
+
116
+ if (orientation === "right") {
117
+ return <ChevronRightIcon className={cn("size-4", className)} {...props} />;
118
+ }
119
+
120
+ return <ChevronDownIcon className={cn("size-4", className)} {...props} />;
121
+ },
122
+ DayButton: CalendarDayButton,
123
+ WeekNumber: ({ children, ...props }) => {
124
+ return (
125
+ <td {...props}>
126
+ <div className="flex size-(--cell-size) items-center justify-center text-center">
127
+ {children}
128
+ </div>
129
+ </td>
130
+ );
131
+ },
132
+ ...components,
133
+ }}
134
+ {...props}
135
+ />
136
+ );
137
+ }
138
+
139
+ function CalendarDayButton({
140
+ className,
141
+ day,
142
+ modifiers,
143
+ ...props
144
+ }: React.ComponentProps<typeof DayButton>) {
145
+ const defaultClassNames = getDefaultClassNames();
146
+
147
+ const ref = React.useRef<HTMLButtonElement>(null);
148
+ React.useEffect(() => {
149
+ if (modifiers.focused) ref.current?.focus();
150
+ }, [modifiers.focused]);
151
+
152
+ return (
153
+ <Button
154
+ ref={ref}
155
+ variant="ghost"
156
+ size="icon"
157
+ data-day={day.date.toLocaleDateString()}
158
+ data-selected-single={
159
+ modifiers.selected &&
160
+ !modifiers.range_start &&
161
+ !modifiers.range_end &&
162
+ !modifiers.range_middle
163
+ }
164
+ data-range-start={modifiers.range_start}
165
+ data-range-end={modifiers.range_end}
166
+ data-range-middle={modifiers.range_middle}
167
+ className={cn(
168
+ "data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 flex aspect-square h-auto w-full min-w-(--cell-size) flex-col gap-1 font-normal leading-none data-[range-end=true]:rounded-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] [&>span]:text-xs [&>span]:opacity-70",
169
+ defaultClassNames.day,
170
+ className,
171
+ )}
172
+ {...props}
173
+ />
174
+ );
175
+ }
176
+
177
+ export { Calendar, CalendarDayButton };
agriculture-ai-frontend/src/components/ui/card.tsx ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
6
+ ({ className, ...props }, ref) => (
7
+ <div
8
+ ref={ref}
9
+ className={cn("rounded-xl border bg-card text-card-foreground shadow", className)}
10
+ {...props}
11
+ />
12
+ ),
13
+ );
14
+ Card.displayName = "Card";
15
+
16
+ const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
17
+ ({ className, ...props }, ref) => (
18
+ <div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
19
+ ),
20
+ );
21
+ CardHeader.displayName = "CardHeader";
22
+
23
+ const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
24
+ ({ className, ...props }, ref) => (
25
+ <div
26
+ ref={ref}
27
+ className={cn("font-semibold leading-none tracking-tight", className)}
28
+ {...props}
29
+ />
30
+ ),
31
+ );
32
+ CardTitle.displayName = "CardTitle";
33
+
34
+ const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
35
+ ({ className, ...props }, ref) => (
36
+ <div ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
37
+ ),
38
+ );
39
+ CardDescription.displayName = "CardDescription";
40
+
41
+ const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
42
+ ({ className, ...props }, ref) => (
43
+ <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
44
+ ),
45
+ );
46
+ CardContent.displayName = "CardContent";
47
+
48
+ const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
49
+ ({ className, ...props }, ref) => (
50
+ <div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
51
+ ),
52
+ );
53
+ CardFooter.displayName = "CardFooter";
54
+
55
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
agriculture-ai-frontend/src/components/ui/carousel.tsx ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import useEmblaCarousel, { type UseEmblaCarouselType } from "embla-carousel-react";
3
+ import { ArrowLeft, ArrowRight } from "lucide-react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+ import { Button } from "@/components/ui/button";
7
+
8
+ type CarouselApi = UseEmblaCarouselType[1];
9
+ type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
10
+ type CarouselOptions = UseCarouselParameters[0];
11
+ type CarouselPlugin = UseCarouselParameters[1];
12
+
13
+ type CarouselProps = {
14
+ opts?: CarouselOptions;
15
+ plugins?: CarouselPlugin;
16
+ orientation?: "horizontal" | "vertical";
17
+ setApi?: (api: CarouselApi) => void;
18
+ };
19
+
20
+ type CarouselContextProps = {
21
+ carouselRef: ReturnType<typeof useEmblaCarousel>[0];
22
+ api: ReturnType<typeof useEmblaCarousel>[1];
23
+ scrollPrev: () => void;
24
+ scrollNext: () => void;
25
+ canScrollPrev: boolean;
26
+ canScrollNext: boolean;
27
+ } & CarouselProps;
28
+
29
+ const CarouselContext = React.createContext<CarouselContextProps | null>(null);
30
+
31
+ function useCarousel() {
32
+ const context = React.useContext(CarouselContext);
33
+
34
+ if (!context) {
35
+ throw new Error("useCarousel must be used within a <Carousel />");
36
+ }
37
+
38
+ return context;
39
+ }
40
+
41
+ const Carousel = React.forwardRef<
42
+ HTMLDivElement,
43
+ React.HTMLAttributes<HTMLDivElement> & CarouselProps
44
+ >(({ orientation = "horizontal", opts, setApi, plugins, className, children, ...props }, ref) => {
45
+ const [carouselRef, api] = useEmblaCarousel(
46
+ {
47
+ ...opts,
48
+ axis: orientation === "horizontal" ? "x" : "y",
49
+ },
50
+ plugins,
51
+ );
52
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false);
53
+ const [canScrollNext, setCanScrollNext] = React.useState(false);
54
+
55
+ const onSelect = React.useCallback((api: CarouselApi) => {
56
+ if (!api) {
57
+ return;
58
+ }
59
+
60
+ setCanScrollPrev(api.canScrollPrev());
61
+ setCanScrollNext(api.canScrollNext());
62
+ }, []);
63
+
64
+ const scrollPrev = React.useCallback(() => {
65
+ api?.scrollPrev();
66
+ }, [api]);
67
+
68
+ const scrollNext = React.useCallback(() => {
69
+ api?.scrollNext();
70
+ }, [api]);
71
+
72
+ const handleKeyDown = React.useCallback(
73
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
74
+ if (event.key === "ArrowLeft") {
75
+ event.preventDefault();
76
+ scrollPrev();
77
+ } else if (event.key === "ArrowRight") {
78
+ event.preventDefault();
79
+ scrollNext();
80
+ }
81
+ },
82
+ [scrollPrev, scrollNext],
83
+ );
84
+
85
+ React.useEffect(() => {
86
+ if (!api || !setApi) {
87
+ return;
88
+ }
89
+
90
+ setApi(api);
91
+ }, [api, setApi]);
92
+
93
+ React.useEffect(() => {
94
+ if (!api) {
95
+ return;
96
+ }
97
+
98
+ onSelect(api);
99
+ api.on("reInit", onSelect);
100
+ api.on("select", onSelect);
101
+
102
+ return () => {
103
+ api?.off("select", onSelect);
104
+ };
105
+ }, [api, onSelect]);
106
+
107
+ return (
108
+ <CarouselContext.Provider
109
+ value={{
110
+ carouselRef,
111
+ api: api,
112
+ opts,
113
+ orientation: orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
114
+ scrollPrev,
115
+ scrollNext,
116
+ canScrollPrev,
117
+ canScrollNext,
118
+ }}
119
+ >
120
+ <div
121
+ ref={ref}
122
+ onKeyDownCapture={handleKeyDown}
123
+ className={cn("relative", className)}
124
+ role="region"
125
+ aria-roledescription="carousel"
126
+ {...props}
127
+ >
128
+ {children}
129
+ </div>
130
+ </CarouselContext.Provider>
131
+ );
132
+ });
133
+ Carousel.displayName = "Carousel";
134
+
135
+ const CarouselContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
136
+ ({ className, ...props }, ref) => {
137
+ const { carouselRef, orientation } = useCarousel();
138
+
139
+ return (
140
+ <div ref={carouselRef} className="overflow-hidden">
141
+ <div
142
+ ref={ref}
143
+ className={cn(
144
+ "flex",
145
+ orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
146
+ className,
147
+ )}
148
+ {...props}
149
+ />
150
+ </div>
151
+ );
152
+ },
153
+ );
154
+ CarouselContent.displayName = "CarouselContent";
155
+
156
+ const CarouselItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
157
+ ({ className, ...props }, ref) => {
158
+ const { orientation } = useCarousel();
159
+
160
+ return (
161
+ <div
162
+ ref={ref}
163
+ role="group"
164
+ aria-roledescription="slide"
165
+ className={cn(
166
+ "min-w-0 shrink-0 grow-0 basis-full",
167
+ orientation === "horizontal" ? "pl-4" : "pt-4",
168
+ className,
169
+ )}
170
+ {...props}
171
+ />
172
+ );
173
+ },
174
+ );
175
+ CarouselItem.displayName = "CarouselItem";
176
+
177
+ const CarouselPrevious = React.forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>(
178
+ ({ className, variant = "outline", size = "icon", ...props }, ref) => {
179
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel();
180
+
181
+ return (
182
+ <Button
183
+ ref={ref}
184
+ variant={variant}
185
+ size={size}
186
+ className={cn(
187
+ "absolute h-8 w-8 rounded-full",
188
+ orientation === "horizontal"
189
+ ? "-left-12 top-1/2 -translate-y-1/2"
190
+ : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
191
+ className,
192
+ )}
193
+ disabled={!canScrollPrev}
194
+ onClick={scrollPrev}
195
+ {...props}
196
+ >
197
+ <ArrowLeft className="h-4 w-4" />
198
+ <span className="sr-only">Previous slide</span>
199
+ </Button>
200
+ );
201
+ },
202
+ );
203
+ CarouselPrevious.displayName = "CarouselPrevious";
204
+
205
+ const CarouselNext = React.forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>(
206
+ ({ className, variant = "outline", size = "icon", ...props }, ref) => {
207
+ const { orientation, scrollNext, canScrollNext } = useCarousel();
208
+
209
+ return (
210
+ <Button
211
+ ref={ref}
212
+ variant={variant}
213
+ size={size}
214
+ className={cn(
215
+ "absolute h-8 w-8 rounded-full",
216
+ orientation === "horizontal"
217
+ ? "-right-12 top-1/2 -translate-y-1/2"
218
+ : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
219
+ className,
220
+ )}
221
+ disabled={!canScrollNext}
222
+ onClick={scrollNext}
223
+ {...props}
224
+ >
225
+ <ArrowRight className="h-4 w-4" />
226
+ <span className="sr-only">Next slide</span>
227
+ </Button>
228
+ );
229
+ },
230
+ );
231
+ CarouselNext.displayName = "CarouselNext";
232
+
233
+ export {
234
+ type CarouselApi,
235
+ Carousel,
236
+ CarouselContent,
237
+ CarouselItem,
238
+ CarouselPrevious,
239
+ CarouselNext,
240
+ };
agriculture-ai-frontend/src/components/ui/chart.tsx ADDED
@@ -0,0 +1,331 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as RechartsPrimitive from "recharts";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ // Format: { THEME_NAME: CSS_SELECTOR }
7
+ const THEMES = { light: "", dark: ".dark" } as const;
8
+
9
+ export type ChartConfig = {
10
+ [k in string]: {
11
+ label?: React.ReactNode;
12
+ icon?: React.ComponentType;
13
+ } & (
14
+ | { color?: string; theme?: never }
15
+ | { color?: never; theme: Record<keyof typeof THEMES, string> }
16
+ );
17
+ };
18
+
19
+ type ChartContextProps = {
20
+ config: ChartConfig;
21
+ };
22
+
23
+ const ChartContext = React.createContext<ChartContextProps | null>(null);
24
+
25
+ function useChart() {
26
+ const context = React.useContext(ChartContext);
27
+
28
+ if (!context) {
29
+ throw new Error("useChart must be used within a <ChartContainer />");
30
+ }
31
+
32
+ return context;
33
+ }
34
+
35
+ const ChartContainer = React.forwardRef<
36
+ HTMLDivElement,
37
+ React.ComponentProps<"div"> & {
38
+ config: ChartConfig;
39
+ children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["children"];
40
+ }
41
+ >(({ id, className, children, config, ...props }, ref) => {
42
+ const uniqueId = React.useId();
43
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
44
+
45
+ return (
46
+ <ChartContext.Provider value={{ config }}>
47
+ <div
48
+ data-chart={chartId}
49
+ ref={ref}
50
+ className={cn(
51
+ "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
52
+ className,
53
+ )}
54
+ {...props}
55
+ >
56
+ <ChartStyle id={chartId} config={config} />
57
+ <RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer>
58
+ </div>
59
+ </ChartContext.Provider>
60
+ );
61
+ });
62
+ ChartContainer.displayName = "Chart";
63
+
64
+ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
65
+ const colorConfig = Object.entries(config).filter(([, config]) => config.theme || config.color);
66
+
67
+ if (!colorConfig.length) {
68
+ return null;
69
+ }
70
+
71
+ return (
72
+ <style
73
+ dangerouslySetInnerHTML={{
74
+ __html: Object.entries(THEMES)
75
+ .map(
76
+ ([theme, prefix]) => `
77
+ ${prefix} [data-chart=${id}] {
78
+ ${colorConfig
79
+ .map(([key, itemConfig]) => {
80
+ const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color;
81
+ return color ? ` --color-${key}: ${color};` : null;
82
+ })
83
+ .join("\n")}
84
+ }
85
+ `,
86
+ )
87
+ .join("\n"),
88
+ }}
89
+ />
90
+ );
91
+ };
92
+
93
+ const ChartTooltip = RechartsPrimitive.Tooltip;
94
+
95
+ const ChartTooltipContent = React.forwardRef<
96
+ HTMLDivElement,
97
+ React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
98
+ React.ComponentProps<"div"> & {
99
+ hideLabel?: boolean;
100
+ hideIndicator?: boolean;
101
+ indicator?: "line" | "dot" | "dashed";
102
+ nameKey?: string;
103
+ labelKey?: string;
104
+ }
105
+ >(
106
+ (
107
+ {
108
+ active,
109
+ payload,
110
+ className,
111
+ indicator = "dot",
112
+ hideLabel = false,
113
+ hideIndicator = false,
114
+ label,
115
+ labelFormatter,
116
+ labelClassName,
117
+ formatter,
118
+ color,
119
+ nameKey,
120
+ labelKey,
121
+ },
122
+ ref,
123
+ ) => {
124
+ const { config } = useChart();
125
+
126
+ const tooltipLabel = React.useMemo(() => {
127
+ if (hideLabel || !payload?.length) {
128
+ return null;
129
+ }
130
+
131
+ const [item] = payload;
132
+ const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
133
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
134
+ const value =
135
+ !labelKey && typeof label === "string"
136
+ ? config[label as keyof typeof config]?.label || label
137
+ : itemConfig?.label;
138
+
139
+ if (labelFormatter) {
140
+ return (
141
+ <div className={cn("font-medium", labelClassName)}>{labelFormatter(value, payload)}</div>
142
+ );
143
+ }
144
+
145
+ if (!value) {
146
+ return null;
147
+ }
148
+
149
+ return <div className={cn("font-medium", labelClassName)}>{value}</div>;
150
+ }, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]);
151
+
152
+ if (!active || !payload?.length) {
153
+ return null;
154
+ }
155
+
156
+ const nestLabel = payload.length === 1 && indicator !== "dot";
157
+
158
+ return (
159
+ <div
160
+ ref={ref}
161
+ className={cn(
162
+ "grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
163
+ className,
164
+ )}
165
+ >
166
+ {!nestLabel ? tooltipLabel : null}
167
+ <div className="grid gap-1.5">
168
+ {payload
169
+ .filter((item) => item.type !== "none")
170
+ .map((item, index) => {
171
+ const key = `${nameKey || item.name || item.dataKey || "value"}`;
172
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
173
+ const indicatorColor = color || item.payload.fill || item.color;
174
+
175
+ return (
176
+ <div
177
+ key={item.dataKey}
178
+ className={cn(
179
+ "flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
180
+ indicator === "dot" && "items-center",
181
+ )}
182
+ >
183
+ {formatter && item?.value !== undefined && item.name ? (
184
+ formatter(item.value, item.name, item, index, item.payload)
185
+ ) : (
186
+ <>
187
+ {itemConfig?.icon ? (
188
+ <itemConfig.icon />
189
+ ) : (
190
+ !hideIndicator && (
191
+ <div
192
+ className={cn(
193
+ "shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
194
+ {
195
+ "h-2.5 w-2.5": indicator === "dot",
196
+ "w-1": indicator === "line",
197
+ "w-0 border-[1.5px] border-dashed bg-transparent":
198
+ indicator === "dashed",
199
+ "my-0.5": nestLabel && indicator === "dashed",
200
+ },
201
+ )}
202
+ style={
203
+ {
204
+ "--color-bg": indicatorColor,
205
+ "--color-border": indicatorColor,
206
+ } as React.CSSProperties
207
+ }
208
+ />
209
+ )
210
+ )}
211
+ <div
212
+ className={cn(
213
+ "flex flex-1 justify-between leading-none",
214
+ nestLabel ? "items-end" : "items-center",
215
+ )}
216
+ >
217
+ <div className="grid gap-1.5">
218
+ {nestLabel ? tooltipLabel : null}
219
+ <span className="text-muted-foreground">
220
+ {itemConfig?.label || item.name}
221
+ </span>
222
+ </div>
223
+ {item.value && (
224
+ <span className="font-mono font-medium tabular-nums text-foreground">
225
+ {item.value.toLocaleString()}
226
+ </span>
227
+ )}
228
+ </div>
229
+ </>
230
+ )}
231
+ </div>
232
+ );
233
+ })}
234
+ </div>
235
+ </div>
236
+ );
237
+ },
238
+ );
239
+ ChartTooltipContent.displayName = "ChartTooltip";
240
+
241
+ const ChartLegend = RechartsPrimitive.Legend;
242
+
243
+ const ChartLegendContent = React.forwardRef<
244
+ HTMLDivElement,
245
+ React.ComponentProps<"div"> &
246
+ Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
247
+ hideIcon?: boolean;
248
+ nameKey?: string;
249
+ }
250
+ >(({ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, ref) => {
251
+ const { config } = useChart();
252
+
253
+ if (!payload?.length) {
254
+ return null;
255
+ }
256
+
257
+ return (
258
+ <div
259
+ ref={ref}
260
+ className={cn(
261
+ "flex items-center justify-center gap-4",
262
+ verticalAlign === "top" ? "pb-3" : "pt-3",
263
+ className,
264
+ )}
265
+ >
266
+ {payload
267
+ .filter((item) => item.type !== "none")
268
+ .map((item) => {
269
+ const key = `${nameKey || item.dataKey || "value"}`;
270
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
271
+
272
+ return (
273
+ <div
274
+ key={item.value}
275
+ className={cn(
276
+ "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground",
277
+ )}
278
+ >
279
+ {itemConfig?.icon && !hideIcon ? (
280
+ <itemConfig.icon />
281
+ ) : (
282
+ <div
283
+ className="h-2 w-2 shrink-0 rounded-[2px]"
284
+ style={{
285
+ backgroundColor: item.color,
286
+ }}
287
+ />
288
+ )}
289
+ {itemConfig?.label}
290
+ </div>
291
+ );
292
+ })}
293
+ </div>
294
+ );
295
+ });
296
+ ChartLegendContent.displayName = "ChartLegend";
297
+
298
+ // Helper to extract item config from a payload.
299
+ function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
300
+ if (typeof payload !== "object" || payload === null) {
301
+ return undefined;
302
+ }
303
+
304
+ const payloadPayload =
305
+ "payload" in payload && typeof payload.payload === "object" && payload.payload !== null
306
+ ? payload.payload
307
+ : undefined;
308
+
309
+ let configLabelKey: string = key;
310
+
311
+ if (key in payload && typeof payload[key as keyof typeof payload] === "string") {
312
+ configLabelKey = payload[key as keyof typeof payload] as string;
313
+ } else if (
314
+ payloadPayload &&
315
+ key in payloadPayload &&
316
+ typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
317
+ ) {
318
+ configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;
319
+ }
320
+
321
+ return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
322
+ }
323
+
324
+ export {
325
+ ChartContainer,
326
+ ChartTooltip,
327
+ ChartTooltipContent,
328
+ ChartLegend,
329
+ ChartLegendContent,
330
+ ChartStyle,
331
+ };
agriculture-ai-frontend/src/components/ui/checkbox.tsx ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
3
+ import { Check } from "lucide-react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const Checkbox = React.forwardRef<
8
+ React.ElementRef<typeof CheckboxPrimitive.Root>,
9
+ React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
10
+ >(({ className, ...props }, ref) => (
11
+ <CheckboxPrimitive.Root
12
+ ref={ref}
13
+ className={cn(
14
+ "grid place-content-center peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
15
+ className,
16
+ )}
17
+ {...props}
18
+ >
19
+ <CheckboxPrimitive.Indicator className={cn("grid place-content-center text-current")}>
20
+ <Check className="h-4 w-4" />
21
+ </CheckboxPrimitive.Indicator>
22
+ </CheckboxPrimitive.Root>
23
+ ));
24
+ Checkbox.displayName = CheckboxPrimitive.Root.displayName;
25
+
26
+ export { Checkbox };
agriculture-ai-frontend/src/components/ui/collapsible.tsx ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
4
+
5
+ const Collapsible = CollapsiblePrimitive.Root;
6
+
7
+ const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
8
+
9
+ const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
10
+
11
+ export { Collapsible, CollapsibleTrigger, CollapsibleContent };
agriculture-ai-frontend/src/components/ui/command.tsx ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { type DialogProps } from "@radix-ui/react-dialog";
5
+ import { Command as CommandPrimitive } from "cmdk";
6
+ import { Search } from "lucide-react";
7
+
8
+ import { cn } from "@/lib/utils";
9
+ import { Dialog, DialogContent } from "@/components/ui/dialog";
10
+
11
+ const Command = React.forwardRef<
12
+ React.ElementRef<typeof CommandPrimitive>,
13
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive>
14
+ >(({ className, ...props }, ref) => (
15
+ <CommandPrimitive
16
+ ref={ref}
17
+ className={cn(
18
+ "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
19
+ className,
20
+ )}
21
+ {...props}
22
+ />
23
+ ));
24
+ Command.displayName = CommandPrimitive.displayName;
25
+
26
+ const CommandDialog = ({ children, ...props }: DialogProps) => {
27
+ return (
28
+ <Dialog {...props}>
29
+ <DialogContent className="overflow-hidden p-0">
30
+ <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
31
+ {children}
32
+ </Command>
33
+ </DialogContent>
34
+ </Dialog>
35
+ );
36
+ };
37
+
38
+ const CommandInput = React.forwardRef<
39
+ React.ElementRef<typeof CommandPrimitive.Input>,
40
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
41
+ >(({ className, ...props }, ref) => (
42
+ <div className="flex items-center border-b px-3" cmdk-input-wrapper="">
43
+ <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
44
+ <CommandPrimitive.Input
45
+ ref={ref}
46
+ className={cn(
47
+ "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
48
+ className,
49
+ )}
50
+ {...props}
51
+ />
52
+ </div>
53
+ ));
54
+
55
+ CommandInput.displayName = CommandPrimitive.Input.displayName;
56
+
57
+ const CommandList = React.forwardRef<
58
+ React.ElementRef<typeof CommandPrimitive.List>,
59
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
60
+ >(({ className, ...props }, ref) => (
61
+ <CommandPrimitive.List
62
+ ref={ref}
63
+ className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
64
+ {...props}
65
+ />
66
+ ));
67
+
68
+ CommandList.displayName = CommandPrimitive.List.displayName;
69
+
70
+ const CommandEmpty = React.forwardRef<
71
+ React.ElementRef<typeof CommandPrimitive.Empty>,
72
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
73
+ >((props, ref) => (
74
+ <CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />
75
+ ));
76
+
77
+ CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
78
+
79
+ const CommandGroup = React.forwardRef<
80
+ React.ElementRef<typeof CommandPrimitive.Group>,
81
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
82
+ >(({ className, ...props }, ref) => (
83
+ <CommandPrimitive.Group
84
+ ref={ref}
85
+ className={cn(
86
+ "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
87
+ className,
88
+ )}
89
+ {...props}
90
+ />
91
+ ));
92
+
93
+ CommandGroup.displayName = CommandPrimitive.Group.displayName;
94
+
95
+ const CommandSeparator = React.forwardRef<
96
+ React.ElementRef<typeof CommandPrimitive.Separator>,
97
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
98
+ >(({ className, ...props }, ref) => (
99
+ <CommandPrimitive.Separator
100
+ ref={ref}
101
+ className={cn("-mx-1 h-px bg-border", className)}
102
+ {...props}
103
+ />
104
+ ));
105
+ CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
106
+
107
+ const CommandItem = React.forwardRef<
108
+ React.ElementRef<typeof CommandPrimitive.Item>,
109
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
110
+ >(({ className, ...props }, ref) => (
111
+ <CommandPrimitive.Item
112
+ ref={ref}
113
+ className={cn(
114
+ "relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
115
+ className,
116
+ )}
117
+ {...props}
118
+ />
119
+ ));
120
+
121
+ CommandItem.displayName = CommandPrimitive.Item.displayName;
122
+
123
+ const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
124
+ return (
125
+ <span
126
+ className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
127
+ {...props}
128
+ />
129
+ );
130
+ };
131
+ CommandShortcut.displayName = "CommandShortcut";
132
+
133
+ export {
134
+ Command,
135
+ CommandDialog,
136
+ CommandInput,
137
+ CommandList,
138
+ CommandEmpty,
139
+ CommandGroup,
140
+ CommandItem,
141
+ CommandShortcut,
142
+ CommandSeparator,
143
+ };
agriculture-ai-frontend/src/components/ui/context-menu.tsx ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
3
+ import { Check, ChevronRight, Circle } from "lucide-react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const ContextMenu = ContextMenuPrimitive.Root;
8
+
9
+ const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
10
+
11
+ const ContextMenuGroup = ContextMenuPrimitive.Group;
12
+
13
+ const ContextMenuPortal = ContextMenuPrimitive.Portal;
14
+
15
+ const ContextMenuSub = ContextMenuPrimitive.Sub;
16
+
17
+ const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
18
+
19
+ const ContextMenuSubTrigger = React.forwardRef<
20
+ React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
21
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
22
+ inset?: boolean;
23
+ }
24
+ >(({ className, inset, children, ...props }, ref) => (
25
+ <ContextMenuPrimitive.SubTrigger
26
+ ref={ref}
27
+ className={cn(
28
+ "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
29
+ inset && "pl-8",
30
+ className,
31
+ )}
32
+ {...props}
33
+ >
34
+ {children}
35
+ <ChevronRight className="ml-auto h-4 w-4" />
36
+ </ContextMenuPrimitive.SubTrigger>
37
+ ));
38
+ ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
39
+
40
+ const ContextMenuSubContent = React.forwardRef<
41
+ React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
42
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
43
+ >(({ className, ...props }, ref) => (
44
+ <ContextMenuPrimitive.SubContent
45
+ ref={ref}
46
+ className={cn(
47
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-context-menu-content-transform-origin)",
48
+ className,
49
+ )}
50
+ {...props}
51
+ />
52
+ ));
53
+ ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
54
+
55
+ const ContextMenuContent = React.forwardRef<
56
+ React.ElementRef<typeof ContextMenuPrimitive.Content>,
57
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
58
+ >(({ className, ...props }, ref) => (
59
+ <ContextMenuPrimitive.Portal>
60
+ <ContextMenuPrimitive.Content
61
+ ref={ref}
62
+ className={cn(
63
+ "z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-context-menu-content-transform-origin)",
64
+ className,
65
+ )}
66
+ {...props}
67
+ />
68
+ </ContextMenuPrimitive.Portal>
69
+ ));
70
+ ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
71
+
72
+ const ContextMenuItem = React.forwardRef<
73
+ React.ElementRef<typeof ContextMenuPrimitive.Item>,
74
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
75
+ inset?: boolean;
76
+ }
77
+ >(({ className, inset, ...props }, ref) => (
78
+ <ContextMenuPrimitive.Item
79
+ ref={ref}
80
+ className={cn(
81
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
82
+ inset && "pl-8",
83
+ className,
84
+ )}
85
+ {...props}
86
+ />
87
+ ));
88
+ ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
89
+
90
+ const ContextMenuCheckboxItem = React.forwardRef<
91
+ React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
92
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
93
+ >(({ className, children, checked, ...props }, ref) => (
94
+ <ContextMenuPrimitive.CheckboxItem
95
+ ref={ref}
96
+ className={cn(
97
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
98
+ className,
99
+ )}
100
+ checked={checked}
101
+ {...props}
102
+ >
103
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
104
+ <ContextMenuPrimitive.ItemIndicator>
105
+ <Check className="h-4 w-4" />
106
+ </ContextMenuPrimitive.ItemIndicator>
107
+ </span>
108
+ {children}
109
+ </ContextMenuPrimitive.CheckboxItem>
110
+ ));
111
+ ContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName;
112
+
113
+ const ContextMenuRadioItem = React.forwardRef<
114
+ React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
115
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
116
+ >(({ className, children, ...props }, ref) => (
117
+ <ContextMenuPrimitive.RadioItem
118
+ ref={ref}
119
+ className={cn(
120
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
121
+ className,
122
+ )}
123
+ {...props}
124
+ >
125
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
126
+ <ContextMenuPrimitive.ItemIndicator>
127
+ <Circle className="h-4 w-4 fill-current" />
128
+ </ContextMenuPrimitive.ItemIndicator>
129
+ </span>
130
+ {children}
131
+ </ContextMenuPrimitive.RadioItem>
132
+ ));
133
+ ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
134
+
135
+ const ContextMenuLabel = React.forwardRef<
136
+ React.ElementRef<typeof ContextMenuPrimitive.Label>,
137
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
138
+ inset?: boolean;
139
+ }
140
+ >(({ className, inset, ...props }, ref) => (
141
+ <ContextMenuPrimitive.Label
142
+ ref={ref}
143
+ className={cn("px-2 py-1.5 text-sm font-semibold text-foreground", inset && "pl-8", className)}
144
+ {...props}
145
+ />
146
+ ));
147
+ ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
148
+
149
+ const ContextMenuSeparator = React.forwardRef<
150
+ React.ElementRef<typeof ContextMenuPrimitive.Separator>,
151
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
152
+ >(({ className, ...props }, ref) => (
153
+ <ContextMenuPrimitive.Separator
154
+ ref={ref}
155
+ className={cn("-mx-1 my-1 h-px bg-border", className)}
156
+ {...props}
157
+ />
158
+ ));
159
+ ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
160
+
161
+ const ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
162
+ return (
163
+ <span
164
+ className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
165
+ {...props}
166
+ />
167
+ );
168
+ };
169
+ ContextMenuShortcut.displayName = "ContextMenuShortcut";
170
+
171
+ export {
172
+ ContextMenu,
173
+ ContextMenuTrigger,
174
+ ContextMenuContent,
175
+ ContextMenuItem,
176
+ ContextMenuCheckboxItem,
177
+ ContextMenuRadioItem,
178
+ ContextMenuLabel,
179
+ ContextMenuSeparator,
180
+ ContextMenuShortcut,
181
+ ContextMenuGroup,
182
+ ContextMenuPortal,
183
+ ContextMenuSub,
184
+ ContextMenuSubContent,
185
+ ContextMenuSubTrigger,
186
+ ContextMenuRadioGroup,
187
+ };
agriculture-ai-frontend/src/components/ui/dialog.tsx ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
5
+ import { X } from "lucide-react";
6
+
7
+ import { cn } from "@/lib/utils";
8
+
9
+ const Dialog = DialogPrimitive.Root;
10
+
11
+ const DialogTrigger = DialogPrimitive.Trigger;
12
+
13
+ const DialogPortal = DialogPrimitive.Portal;
14
+
15
+ const DialogClose = DialogPrimitive.Close;
16
+
17
+ const DialogOverlay = React.forwardRef<
18
+ React.ElementRef<typeof DialogPrimitive.Overlay>,
19
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
20
+ >(({ className, ...props }, ref) => (
21
+ <DialogPrimitive.Overlay
22
+ ref={ref}
23
+ className={cn(
24
+ "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
25
+ className,
26
+ )}
27
+ {...props}
28
+ />
29
+ ));
30
+ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
31
+
32
+ const DialogContent = React.forwardRef<
33
+ React.ElementRef<typeof DialogPrimitive.Content>,
34
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
35
+ >(({ className, children, ...props }, ref) => (
36
+ <DialogPortal>
37
+ <DialogOverlay />
38
+ <DialogPrimitive.Content
39
+ ref={ref}
40
+ className={cn(
41
+ "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 sm:rounded-lg",
42
+ className,
43
+ )}
44
+ {...props}
45
+ >
46
+ {children}
47
+ <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background cursor-pointer transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
48
+ <X className="h-4 w-4" />
49
+ <span className="sr-only">Close</span>
50
+ </DialogPrimitive.Close>
51
+ </DialogPrimitive.Content>
52
+ </DialogPortal>
53
+ ));
54
+ DialogContent.displayName = DialogPrimitive.Content.displayName;
55
+
56
+ const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
57
+ <div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
58
+ );
59
+ DialogHeader.displayName = "DialogHeader";
60
+
61
+ const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
62
+ <div
63
+ className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
64
+ {...props}
65
+ />
66
+ );
67
+ DialogFooter.displayName = "DialogFooter";
68
+
69
+ const DialogTitle = React.forwardRef<
70
+ React.ElementRef<typeof DialogPrimitive.Title>,
71
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
72
+ >(({ className, ...props }, ref) => (
73
+ <DialogPrimitive.Title
74
+ ref={ref}
75
+ className={cn("text-lg font-semibold leading-none tracking-tight", className)}
76
+ {...props}
77
+ />
78
+ ));
79
+ DialogTitle.displayName = DialogPrimitive.Title.displayName;
80
+
81
+ const DialogDescription = React.forwardRef<
82
+ React.ElementRef<typeof DialogPrimitive.Description>,
83
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
84
+ >(({ className, ...props }, ref) => (
85
+ <DialogPrimitive.Description
86
+ ref={ref}
87
+ className={cn("text-sm text-muted-foreground", className)}
88
+ {...props}
89
+ />
90
+ ));
91
+ DialogDescription.displayName = DialogPrimitive.Description.displayName;
92
+
93
+ export {
94
+ Dialog,
95
+ DialogPortal,
96
+ DialogOverlay,
97
+ DialogTrigger,
98
+ DialogClose,
99
+ DialogContent,
100
+ DialogHeader,
101
+ DialogFooter,
102
+ DialogTitle,
103
+ DialogDescription,
104
+ };
agriculture-ai-frontend/src/components/ui/drawer.tsx ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { Drawer as DrawerPrimitive } from "vaul";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ const Drawer = ({
7
+ shouldScaleBackground = true,
8
+ ...props
9
+ }: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
10
+ <DrawerPrimitive.Root shouldScaleBackground={shouldScaleBackground} {...props} />
11
+ );
12
+ Drawer.displayName = "Drawer";
13
+
14
+ const DrawerTrigger = DrawerPrimitive.Trigger;
15
+
16
+ const DrawerPortal = DrawerPrimitive.Portal;
17
+
18
+ const DrawerClose = DrawerPrimitive.Close;
19
+
20
+ const DrawerOverlay = React.forwardRef<
21
+ React.ElementRef<typeof DrawerPrimitive.Overlay>,
22
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
23
+ >(({ className, ...props }, ref) => (
24
+ <DrawerPrimitive.Overlay
25
+ ref={ref}
26
+ className={cn("fixed inset-0 z-50 bg-black/80", className)}
27
+ {...props}
28
+ />
29
+ ));
30
+ DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
31
+
32
+ const DrawerContent = React.forwardRef<
33
+ React.ElementRef<typeof DrawerPrimitive.Content>,
34
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
35
+ >(({ className, children, ...props }, ref) => (
36
+ <DrawerPortal>
37
+ <DrawerOverlay />
38
+ <DrawerPrimitive.Content
39
+ ref={ref}
40
+ className={cn(
41
+ "fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
42
+ className,
43
+ )}
44
+ {...props}
45
+ >
46
+ <div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
47
+ {children}
48
+ </DrawerPrimitive.Content>
49
+ </DrawerPortal>
50
+ ));
51
+ DrawerContent.displayName = "DrawerContent";
52
+
53
+ const DrawerHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
54
+ <div className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)} {...props} />
55
+ );
56
+ DrawerHeader.displayName = "DrawerHeader";
57
+
58
+ const DrawerFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
59
+ <div className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} />
60
+ );
61
+ DrawerFooter.displayName = "DrawerFooter";
62
+
63
+ const DrawerTitle = React.forwardRef<
64
+ React.ElementRef<typeof DrawerPrimitive.Title>,
65
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
66
+ >(({ className, ...props }, ref) => (
67
+ <DrawerPrimitive.Title
68
+ ref={ref}
69
+ className={cn("text-lg font-semibold leading-none tracking-tight", className)}
70
+ {...props}
71
+ />
72
+ ));
73
+ DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
74
+
75
+ const DrawerDescription = React.forwardRef<
76
+ React.ElementRef<typeof DrawerPrimitive.Description>,
77
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
78
+ >(({ className, ...props }, ref) => (
79
+ <DrawerPrimitive.Description
80
+ ref={ref}
81
+ className={cn("text-sm text-muted-foreground", className)}
82
+ {...props}
83
+ />
84
+ ));
85
+ DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
86
+
87
+ export {
88
+ Drawer,
89
+ DrawerPortal,
90
+ DrawerOverlay,
91
+ DrawerTrigger,
92
+ DrawerClose,
93
+ DrawerContent,
94
+ DrawerHeader,
95
+ DrawerFooter,
96
+ DrawerTitle,
97
+ DrawerDescription,
98
+ };
agriculture-ai-frontend/src/components/ui/dropdown-menu.tsx ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
5
+ import { Check, ChevronRight, Circle } from "lucide-react";
6
+
7
+ import { cn } from "@/lib/utils";
8
+
9
+ const DropdownMenu = DropdownMenuPrimitive.Root;
10
+
11
+ const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
12
+
13
+ const DropdownMenuGroup = DropdownMenuPrimitive.Group;
14
+
15
+ const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
16
+
17
+ const DropdownMenuSub = DropdownMenuPrimitive.Sub;
18
+
19
+ const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
20
+
21
+ const DropdownMenuSubTrigger = React.forwardRef<
22
+ React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
23
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
24
+ inset?: boolean;
25
+ }
26
+ >(({ className, inset, children, ...props }, ref) => (
27
+ <DropdownMenuPrimitive.SubTrigger
28
+ ref={ref}
29
+ className={cn(
30
+ "flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
31
+ inset && "pl-8",
32
+ className,
33
+ )}
34
+ {...props}
35
+ >
36
+ {children}
37
+ <ChevronRight className="ml-auto" />
38
+ </DropdownMenuPrimitive.SubTrigger>
39
+ ));
40
+ DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
41
+
42
+ const DropdownMenuSubContent = React.forwardRef<
43
+ React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
44
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
45
+ >(({ className, ...props }, ref) => (
46
+ <DropdownMenuPrimitive.SubContent
47
+ ref={ref}
48
+ className={cn(
49
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-dropdown-menu-content-transform-origin)",
50
+ className,
51
+ )}
52
+ {...props}
53
+ />
54
+ ));
55
+ DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
56
+
57
+ const DropdownMenuContent = React.forwardRef<
58
+ React.ElementRef<typeof DropdownMenuPrimitive.Content>,
59
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
60
+ >(({ className, sideOffset = 4, ...props }, ref) => (
61
+ <DropdownMenuPrimitive.Portal>
62
+ <DropdownMenuPrimitive.Content
63
+ ref={ref}
64
+ sideOffset={sideOffset}
65
+ className={cn(
66
+ "z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
67
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-dropdown-menu-content-transform-origin)",
68
+ className,
69
+ )}
70
+ {...props}
71
+ />
72
+ </DropdownMenuPrimitive.Portal>
73
+ ));
74
+ DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
75
+
76
+ const DropdownMenuItem = React.forwardRef<
77
+ React.ElementRef<typeof DropdownMenuPrimitive.Item>,
78
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
79
+ inset?: boolean;
80
+ }
81
+ >(({ className, inset, ...props }, ref) => (
82
+ <DropdownMenuPrimitive.Item
83
+ ref={ref}
84
+ className={cn(
85
+ "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
86
+ inset && "pl-8",
87
+ className,
88
+ )}
89
+ {...props}
90
+ />
91
+ ));
92
+ DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
93
+
94
+ const DropdownMenuCheckboxItem = React.forwardRef<
95
+ React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
96
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
97
+ >(({ className, children, checked, ...props }, ref) => (
98
+ <DropdownMenuPrimitive.CheckboxItem
99
+ ref={ref}
100
+ className={cn(
101
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
102
+ className,
103
+ )}
104
+ checked={checked}
105
+ {...props}
106
+ >
107
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
108
+ <DropdownMenuPrimitive.ItemIndicator>
109
+ <Check className="h-4 w-4" />
110
+ </DropdownMenuPrimitive.ItemIndicator>
111
+ </span>
112
+ {children}
113
+ </DropdownMenuPrimitive.CheckboxItem>
114
+ ));
115
+ DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
116
+
117
+ const DropdownMenuRadioItem = React.forwardRef<
118
+ React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
119
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
120
+ >(({ className, children, ...props }, ref) => (
121
+ <DropdownMenuPrimitive.RadioItem
122
+ ref={ref}
123
+ className={cn(
124
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
125
+ className,
126
+ )}
127
+ {...props}
128
+ >
129
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
130
+ <DropdownMenuPrimitive.ItemIndicator>
131
+ <Circle className="h-2 w-2 fill-current" />
132
+ </DropdownMenuPrimitive.ItemIndicator>
133
+ </span>
134
+ {children}
135
+ </DropdownMenuPrimitive.RadioItem>
136
+ ));
137
+ DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
138
+
139
+ const DropdownMenuLabel = React.forwardRef<
140
+ React.ElementRef<typeof DropdownMenuPrimitive.Label>,
141
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
142
+ inset?: boolean;
143
+ }
144
+ >(({ className, inset, ...props }, ref) => (
145
+ <DropdownMenuPrimitive.Label
146
+ ref={ref}
147
+ className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
148
+ {...props}
149
+ />
150
+ ));
151
+ DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
152
+
153
+ const DropdownMenuSeparator = React.forwardRef<
154
+ React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
155
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
156
+ >(({ className, ...props }, ref) => (
157
+ <DropdownMenuPrimitive.Separator
158
+ ref={ref}
159
+ className={cn("-mx-1 my-1 h-px bg-muted", className)}
160
+ {...props}
161
+ />
162
+ ));
163
+ DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
164
+
165
+ const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
166
+ return (
167
+ <span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />
168
+ );
169
+ };
170
+ DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
171
+
172
+ export {
173
+ DropdownMenu,
174
+ DropdownMenuTrigger,
175
+ DropdownMenuContent,
176
+ DropdownMenuItem,
177
+ DropdownMenuCheckboxItem,
178
+ DropdownMenuRadioItem,
179
+ DropdownMenuLabel,
180
+ DropdownMenuSeparator,
181
+ DropdownMenuShortcut,
182
+ DropdownMenuGroup,
183
+ DropdownMenuPortal,
184
+ DropdownMenuSub,
185
+ DropdownMenuSubContent,
186
+ DropdownMenuSubTrigger,
187
+ DropdownMenuRadioGroup,
188
+ };
agriculture-ai-frontend/src/components/ui/form.tsx ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as LabelPrimitive from "@radix-ui/react-label";
3
+ import { Slot } from "@radix-ui/react-slot";
4
+ import {
5
+ Controller,
6
+ FormProvider,
7
+ useFormContext,
8
+ type ControllerProps,
9
+ type FieldPath,
10
+ type FieldValues,
11
+ } from "react-hook-form";
12
+
13
+ import { cn } from "@/lib/utils";
14
+ import { Label } from "@/components/ui/label";
15
+
16
+ const Form = FormProvider;
17
+
18
+ type FormFieldContextValue<
19
+ TFieldValues extends FieldValues = FieldValues,
20
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
21
+ > = {
22
+ name: TName;
23
+ };
24
+
25
+ const FormFieldContext = React.createContext<FormFieldContextValue | null>(null);
26
+
27
+ const FormField = <
28
+ TFieldValues extends FieldValues = FieldValues,
29
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
30
+ >({
31
+ ...props
32
+ }: ControllerProps<TFieldValues, TName>) => {
33
+ return (
34
+ <FormFieldContext.Provider value={{ name: props.name }}>
35
+ <Controller {...props} />
36
+ </FormFieldContext.Provider>
37
+ );
38
+ };
39
+
40
+ const useFormField = () => {
41
+ const fieldContext = React.useContext(FormFieldContext);
42
+ const itemContext = React.useContext(FormItemContext);
43
+ const { getFieldState, formState } = useFormContext();
44
+
45
+ if (!fieldContext) {
46
+ throw new Error("useFormField should be used within <FormField>");
47
+ }
48
+
49
+ if (!itemContext) {
50
+ throw new Error("useFormField should be used within <FormItem>");
51
+ }
52
+
53
+ const fieldState = getFieldState(fieldContext.name, formState);
54
+
55
+ const { id } = itemContext;
56
+
57
+ return {
58
+ id,
59
+ name: fieldContext.name,
60
+ formItemId: `${id}-form-item`,
61
+ formDescriptionId: `${id}-form-item-description`,
62
+ formMessageId: `${id}-form-item-message`,
63
+ ...fieldState,
64
+ };
65
+ };
66
+
67
+ type FormItemContextValue = {
68
+ id: string;
69
+ };
70
+
71
+ const FormItemContext = React.createContext<FormItemContextValue | null>(null);
72
+
73
+ const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
74
+ ({ className, ...props }, ref) => {
75
+ const id = React.useId();
76
+
77
+ return (
78
+ <FormItemContext.Provider value={{ id }}>
79
+ <div ref={ref} className={cn("space-y-2", className)} {...props} />
80
+ </FormItemContext.Provider>
81
+ );
82
+ },
83
+ );
84
+ FormItem.displayName = "FormItem";
85
+
86
+ const FormLabel = React.forwardRef<
87
+ React.ElementRef<typeof LabelPrimitive.Root>,
88
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
89
+ >(({ className, ...props }, ref) => {
90
+ const { error, formItemId } = useFormField();
91
+
92
+ return (
93
+ <Label
94
+ ref={ref}
95
+ className={cn(error && "text-destructive", className)}
96
+ htmlFor={formItemId}
97
+ {...props}
98
+ />
99
+ );
100
+ });
101
+ FormLabel.displayName = "FormLabel";
102
+
103
+ const FormControl = React.forwardRef<
104
+ React.ElementRef<typeof Slot>,
105
+ React.ComponentPropsWithoutRef<typeof Slot>
106
+ >(({ ...props }, ref) => {
107
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
108
+
109
+ return (
110
+ <Slot
111
+ ref={ref}
112
+ id={formItemId}
113
+ aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
114
+ aria-invalid={!!error}
115
+ {...props}
116
+ />
117
+ );
118
+ });
119
+ FormControl.displayName = "FormControl";
120
+
121
+ const FormDescription = React.forwardRef<
122
+ HTMLParagraphElement,
123
+ React.HTMLAttributes<HTMLParagraphElement>
124
+ >(({ className, ...props }, ref) => {
125
+ const { formDescriptionId } = useFormField();
126
+
127
+ return (
128
+ <p
129
+ ref={ref}
130
+ id={formDescriptionId}
131
+ className={cn("text-[0.8rem] text-muted-foreground", className)}
132
+ {...props}
133
+ />
134
+ );
135
+ });
136
+ FormDescription.displayName = "FormDescription";
137
+
138
+ const FormMessage = React.forwardRef<
139
+ HTMLParagraphElement,
140
+ React.HTMLAttributes<HTMLParagraphElement>
141
+ >(({ className, children, ...props }, ref) => {
142
+ const { error, formMessageId } = useFormField();
143
+ const body = error ? String(error?.message ?? "") : children;
144
+
145
+ if (!body) {
146
+ return null;
147
+ }
148
+
149
+ return (
150
+ <p
151
+ ref={ref}
152
+ id={formMessageId}
153
+ className={cn("text-[0.8rem] font-medium text-destructive", className)}
154
+ {...props}
155
+ >
156
+ {body}
157
+ </p>
158
+ );
159
+ });
160
+ FormMessage.displayName = "FormMessage";
161
+
162
+ export {
163
+ useFormField,
164
+ Form,
165
+ FormItem,
166
+ FormLabel,
167
+ FormControl,
168
+ FormDescription,
169
+ FormMessage,
170
+ FormField,
171
+ };
agriculture-ai-frontend/src/components/ui/hover-card.tsx ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ const HoverCard = HoverCardPrimitive.Root;
7
+
8
+ const HoverCardTrigger = HoverCardPrimitive.Trigger;
9
+
10
+ const HoverCardContent = React.forwardRef<
11
+ React.ElementRef<typeof HoverCardPrimitive.Content>,
12
+ React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
13
+ >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
14
+ <HoverCardPrimitive.Content
15
+ ref={ref}
16
+ align={align}
17
+ sideOffset={sideOffset}
18
+ className={cn(
19
+ "z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-hover-card-content-transform-origin)",
20
+ className,
21
+ )}
22
+ {...props}
23
+ />
24
+ ));
25
+ HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
26
+
27
+ export { HoverCard, HoverCardTrigger, HoverCardContent };
agriculture-ai-frontend/src/components/ui/input-otp.tsx ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { OTPInput, OTPInputContext } from "input-otp";
3
+ import { Minus } from "lucide-react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const InputOTP = React.forwardRef<
8
+ React.ElementRef<typeof OTPInput>,
9
+ React.ComponentPropsWithoutRef<typeof OTPInput>
10
+ >(({ className, containerClassName, ...props }, ref) => (
11
+ <OTPInput
12
+ ref={ref}
13
+ containerClassName={cn(
14
+ "flex items-center gap-2 has-[:disabled]:opacity-50",
15
+ containerClassName,
16
+ )}
17
+ className={cn("disabled:cursor-not-allowed", className)}
18
+ {...props}
19
+ />
20
+ ));
21
+ InputOTP.displayName = "InputOTP";
22
+
23
+ const InputOTPGroup = React.forwardRef<
24
+ React.ElementRef<"div">,
25
+ React.ComponentPropsWithoutRef<"div">
26
+ >(({ className, ...props }, ref) => (
27
+ <div ref={ref} className={cn("flex items-center", className)} {...props} />
28
+ ));
29
+ InputOTPGroup.displayName = "InputOTPGroup";
30
+
31
+ const InputOTPSlot = React.forwardRef<
32
+ React.ElementRef<"div">,
33
+ React.ComponentPropsWithoutRef<"div"> & { index: number }
34
+ >(({ index, className, ...props }, ref) => {
35
+ const inputOTPContext = React.useContext(OTPInputContext);
36
+ const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
37
+
38
+ return (
39
+ <div
40
+ ref={ref}
41
+ className={cn(
42
+ "relative flex h-9 w-9 items-center justify-center border-y border-r border-input text-sm shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
43
+ isActive && "z-10 ring-1 ring-ring",
44
+ className,
45
+ )}
46
+ {...props}
47
+ >
48
+ {char}
49
+ {hasFakeCaret && (
50
+ <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
51
+ <div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
52
+ </div>
53
+ )}
54
+ </div>
55
+ );
56
+ });
57
+ InputOTPSlot.displayName = "InputOTPSlot";
58
+
59
+ const InputOTPSeparator = React.forwardRef<
60
+ React.ElementRef<"div">,
61
+ React.ComponentPropsWithoutRef<"div">
62
+ >(({ ...props }, ref) => (
63
+ <div ref={ref} role="separator" {...props}>
64
+ <Minus />
65
+ </div>
66
+ ));
67
+ InputOTPSeparator.displayName = "InputOTPSeparator";
68
+
69
+ export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
agriculture-ai-frontend/src/components/ui/input.tsx ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
6
+ ({ className, type, ...props }, ref) => {
7
+ return (
8
+ <input
9
+ type={type}
10
+ className={cn(
11
+ "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
12
+ className,
13
+ )}
14
+ ref={ref}
15
+ {...props}
16
+ />
17
+ );
18
+ },
19
+ );
20
+ Input.displayName = "Input";
21
+
22
+ export { Input };
agriculture-ai-frontend/src/components/ui/label.tsx ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as LabelPrimitive from "@radix-ui/react-label";
5
+ import { cva, type VariantProps } from "class-variance-authority";
6
+
7
+ import { cn } from "@/lib/utils";
8
+
9
+ const labelVariants = cva(
10
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
11
+ );
12
+
13
+ const Label = React.forwardRef<
14
+ React.ElementRef<typeof LabelPrimitive.Root>,
15
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
16
+ >(({ className, ...props }, ref) => (
17
+ <LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
18
+ ));
19
+ Label.displayName = LabelPrimitive.Root.displayName;
20
+
21
+ export { Label };
agriculture-ai-frontend/src/components/ui/menubar.tsx ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as MenubarPrimitive from "@radix-ui/react-menubar";
3
+ import { Check, ChevronRight, Circle } from "lucide-react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ function MenubarMenu({ ...props }: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
8
+ return <MenubarPrimitive.Menu {...props} />;
9
+ }
10
+
11
+ function MenubarGroup({ ...props }: React.ComponentProps<typeof MenubarPrimitive.Group>) {
12
+ return <MenubarPrimitive.Group {...props} />;
13
+ }
14
+
15
+ function MenubarPortal({ ...props }: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
16
+ return <MenubarPrimitive.Portal {...props} />;
17
+ }
18
+
19
+ function MenubarRadioGroup({ ...props }: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
20
+ return <MenubarPrimitive.RadioGroup {...props} />;
21
+ }
22
+
23
+ function MenubarSub({ ...props }: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
24
+ return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />;
25
+ }
26
+
27
+ const Menubar = React.forwardRef<
28
+ React.ElementRef<typeof MenubarPrimitive.Root>,
29
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
30
+ >(({ className, ...props }, ref) => (
31
+ <MenubarPrimitive.Root
32
+ ref={ref}
33
+ className={cn(
34
+ "flex h-9 items-center space-x-1 rounded-md border bg-background p-1 shadow-sm",
35
+ className,
36
+ )}
37
+ {...props}
38
+ />
39
+ ));
40
+ Menubar.displayName = MenubarPrimitive.Root.displayName;
41
+
42
+ const MenubarTrigger = React.forwardRef<
43
+ React.ElementRef<typeof MenubarPrimitive.Trigger>,
44
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
45
+ >(({ className, ...props }, ref) => (
46
+ <MenubarPrimitive.Trigger
47
+ ref={ref}
48
+ className={cn(
49
+ "flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
50
+ className,
51
+ )}
52
+ {...props}
53
+ />
54
+ ));
55
+ MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName;
56
+
57
+ const MenubarSubTrigger = React.forwardRef<
58
+ React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
59
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
60
+ inset?: boolean;
61
+ }
62
+ >(({ className, inset, children, ...props }, ref) => (
63
+ <MenubarPrimitive.SubTrigger
64
+ ref={ref}
65
+ className={cn(
66
+ "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
67
+ inset && "pl-8",
68
+ className,
69
+ )}
70
+ {...props}
71
+ >
72
+ {children}
73
+ <ChevronRight className="ml-auto h-4 w-4" />
74
+ </MenubarPrimitive.SubTrigger>
75
+ ));
76
+ MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName;
77
+
78
+ const MenubarSubContent = React.forwardRef<
79
+ React.ElementRef<typeof MenubarPrimitive.SubContent>,
80
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
81
+ >(({ className, ...props }, ref) => (
82
+ <MenubarPrimitive.SubContent
83
+ ref={ref}
84
+ className={cn(
85
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-menubar-content-transform-origin)",
86
+ className,
87
+ )}
88
+ {...props}
89
+ />
90
+ ));
91
+ MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName;
92
+
93
+ const MenubarContent = React.forwardRef<
94
+ React.ElementRef<typeof MenubarPrimitive.Content>,
95
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
96
+ >(({ className, align = "start", alignOffset = -4, sideOffset = 8, ...props }, ref) => (
97
+ <MenubarPrimitive.Portal>
98
+ <MenubarPrimitive.Content
99
+ ref={ref}
100
+ align={align}
101
+ alignOffset={alignOffset}
102
+ sideOffset={sideOffset}
103
+ className={cn(
104
+ "z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-menubar-content-transform-origin)",
105
+ className,
106
+ )}
107
+ {...props}
108
+ />
109
+ </MenubarPrimitive.Portal>
110
+ ));
111
+ MenubarContent.displayName = MenubarPrimitive.Content.displayName;
112
+
113
+ const MenubarItem = React.forwardRef<
114
+ React.ElementRef<typeof MenubarPrimitive.Item>,
115
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
116
+ inset?: boolean;
117
+ }
118
+ >(({ className, inset, ...props }, ref) => (
119
+ <MenubarPrimitive.Item
120
+ ref={ref}
121
+ className={cn(
122
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
123
+ inset && "pl-8",
124
+ className,
125
+ )}
126
+ {...props}
127
+ />
128
+ ));
129
+ MenubarItem.displayName = MenubarPrimitive.Item.displayName;
130
+
131
+ const MenubarCheckboxItem = React.forwardRef<
132
+ React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
133
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
134
+ >(({ className, children, checked, ...props }, ref) => (
135
+ <MenubarPrimitive.CheckboxItem
136
+ ref={ref}
137
+ className={cn(
138
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
139
+ className,
140
+ )}
141
+ checked={checked}
142
+ {...props}
143
+ >
144
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
145
+ <MenubarPrimitive.ItemIndicator>
146
+ <Check className="h-4 w-4" />
147
+ </MenubarPrimitive.ItemIndicator>
148
+ </span>
149
+ {children}
150
+ </MenubarPrimitive.CheckboxItem>
151
+ ));
152
+ MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName;
153
+
154
+ const MenubarRadioItem = React.forwardRef<
155
+ React.ElementRef<typeof MenubarPrimitive.RadioItem>,
156
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
157
+ >(({ className, children, ...props }, ref) => (
158
+ <MenubarPrimitive.RadioItem
159
+ ref={ref}
160
+ className={cn(
161
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
162
+ className,
163
+ )}
164
+ {...props}
165
+ >
166
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
167
+ <MenubarPrimitive.ItemIndicator>
168
+ <Circle className="h-4 w-4 fill-current" />
169
+ </MenubarPrimitive.ItemIndicator>
170
+ </span>
171
+ {children}
172
+ </MenubarPrimitive.RadioItem>
173
+ ));
174
+ MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName;
175
+
176
+ const MenubarLabel = React.forwardRef<
177
+ React.ElementRef<typeof MenubarPrimitive.Label>,
178
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
179
+ inset?: boolean;
180
+ }
181
+ >(({ className, inset, ...props }, ref) => (
182
+ <MenubarPrimitive.Label
183
+ ref={ref}
184
+ className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
185
+ {...props}
186
+ />
187
+ ));
188
+ MenubarLabel.displayName = MenubarPrimitive.Label.displayName;
189
+
190
+ const MenubarSeparator = React.forwardRef<
191
+ React.ElementRef<typeof MenubarPrimitive.Separator>,
192
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
193
+ >(({ className, ...props }, ref) => (
194
+ <MenubarPrimitive.Separator
195
+ ref={ref}
196
+ className={cn("-mx-1 my-1 h-px bg-muted", className)}
197
+ {...props}
198
+ />
199
+ ));
200
+ MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName;
201
+
202
+ const MenubarShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
203
+ return (
204
+ <span
205
+ className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
206
+ {...props}
207
+ />
208
+ );
209
+ };
210
+ MenubarShortcut.displayname = "MenubarShortcut";
211
+
212
+ export {
213
+ Menubar,
214
+ MenubarMenu,
215
+ MenubarTrigger,
216
+ MenubarContent,
217
+ MenubarItem,
218
+ MenubarSeparator,
219
+ MenubarLabel,
220
+ MenubarCheckboxItem,
221
+ MenubarRadioGroup,
222
+ MenubarRadioItem,
223
+ MenubarPortal,
224
+ MenubarSubContent,
225
+ MenubarSubTrigger,
226
+ MenubarGroup,
227
+ MenubarSub,
228
+ MenubarShortcut,
229
+ };
agriculture-ai-frontend/src/components/ui/navigation-menu.tsx ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
3
+ import { cva } from "class-variance-authority";
4
+ import { ChevronDown } from "lucide-react";
5
+
6
+ import { cn } from "@/lib/utils";
7
+
8
+ const NavigationMenu = React.forwardRef<
9
+ React.ElementRef<typeof NavigationMenuPrimitive.Root>,
10
+ React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
11
+ >(({ className, children, ...props }, ref) => (
12
+ <NavigationMenuPrimitive.Root
13
+ ref={ref}
14
+ className={cn("relative z-10 flex max-w-max flex-1 items-center justify-center", className)}
15
+ {...props}
16
+ >
17
+ {children}
18
+ <NavigationMenuViewport />
19
+ </NavigationMenuPrimitive.Root>
20
+ ));
21
+ NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
22
+
23
+ const NavigationMenuList = React.forwardRef<
24
+ React.ElementRef<typeof NavigationMenuPrimitive.List>,
25
+ React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
26
+ >(({ className, ...props }, ref) => (
27
+ <NavigationMenuPrimitive.List
28
+ ref={ref}
29
+ className={cn("group flex flex-1 list-none items-center justify-center space-x-1", className)}
30
+ {...props}
31
+ />
32
+ ));
33
+ NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
34
+
35
+ const NavigationMenuItem = NavigationMenuPrimitive.Item;
36
+
37
+ const navigationMenuTriggerStyle = cva(
38
+ "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium cursor-pointer transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed data-[state=open]:text-accent-foreground data-[state=open]:bg-accent/50 data-[state=open]:hover:bg-accent data-[state=open]:focus:bg-accent",
39
+ );
40
+
41
+ const NavigationMenuTrigger = React.forwardRef<
42
+ React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
43
+ React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
44
+ >(({ className, children, ...props }, ref) => (
45
+ <NavigationMenuPrimitive.Trigger
46
+ ref={ref}
47
+ className={cn(navigationMenuTriggerStyle(), "group", className)}
48
+ {...props}
49
+ >
50
+ {children}{" "}
51
+ <ChevronDown
52
+ className="relative top-[1px] ml-1 h-3 w-3 transition duration-300 group-data-[state=open]:rotate-180"
53
+ aria-hidden="true"
54
+ />
55
+ </NavigationMenuPrimitive.Trigger>
56
+ ));
57
+ NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
58
+
59
+ const NavigationMenuContent = React.forwardRef<
60
+ React.ElementRef<typeof NavigationMenuPrimitive.Content>,
61
+ React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
62
+ >(({ className, ...props }, ref) => (
63
+ <NavigationMenuPrimitive.Content
64
+ ref={ref}
65
+ className={cn(
66
+ "left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
67
+ className,
68
+ )}
69
+ {...props}
70
+ />
71
+ ));
72
+ NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
73
+
74
+ const NavigationMenuLink = NavigationMenuPrimitive.Link;
75
+
76
+ const NavigationMenuViewport = React.forwardRef<
77
+ React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
78
+ React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
79
+ >(({ className, ...props }, ref) => (
80
+ <div className={cn("absolute left-0 top-full flex justify-center")}>
81
+ <NavigationMenuPrimitive.Viewport
82
+ className={cn(
83
+ "origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
84
+ className,
85
+ )}
86
+ ref={ref}
87
+ {...props}
88
+ />
89
+ </div>
90
+ ));
91
+ NavigationMenuViewport.displayName = NavigationMenuPrimitive.Viewport.displayName;
92
+
93
+ const NavigationMenuIndicator = React.forwardRef<
94
+ React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
95
+ React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
96
+ >(({ className, ...props }, ref) => (
97
+ <NavigationMenuPrimitive.Indicator
98
+ ref={ref}
99
+ className={cn(
100
+ "top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
101
+ className,
102
+ )}
103
+ {...props}
104
+ >
105
+ <div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
106
+ </NavigationMenuPrimitive.Indicator>
107
+ ));
108
+ NavigationMenuIndicator.displayName = NavigationMenuPrimitive.Indicator.displayName;
109
+
110
+ export {
111
+ navigationMenuTriggerStyle,
112
+ NavigationMenu,
113
+ NavigationMenuList,
114
+ NavigationMenuItem,
115
+ NavigationMenuContent,
116
+ NavigationMenuTrigger,
117
+ NavigationMenuLink,
118
+ NavigationMenuIndicator,
119
+ NavigationMenuViewport,
120
+ };