Add error handling and new API methods for farm assistant features
- Introduced error messages in Persian for image analysis and chat functionalities in FarmAiAssistantChat and PlantPestDetection components. - Implemented a new postFormData method in ApiClient for handling file uploads. - Enhanced SmartFertilizationRecommendation and SmartIrrigationRecommendation components to fetch configuration data from the API, improving user experience with loading states and error handling. - Refactored components to utilize updated API services for better data management and responsiveness.
This commit is contained in:
@@ -0,0 +1,223 @@
|
|||||||
|
# مستندات APIهای توصیه و تشخیص
|
||||||
|
|
||||||
|
این سند سه گروه API را شرح میدهد: **توصیه آبیاری**، **تشخیص آفت** و **توصیه کوددهی**. همهٔ پاسخها در حال حاضر از دادهٔ ثابت (mock) برگردانده میشوند و پارامترهای ورودی در پاسخ استفاده نمیشوند.
|
||||||
|
|
||||||
|
**پایهٔ آدرس API:** `/api/`
|
||||||
|
|
||||||
|
**قالب کلی پاسخ:**
|
||||||
|
`{"status": "success", "data": <payload>}` — فقط با کد وضعیت HTTP 200.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۱. توصیه آبیاری (Irrigation Recommendation)
|
||||||
|
|
||||||
|
**پیشوند:** `api/irrigation-recommendation/`
|
||||||
|
|
||||||
|
### ۱.۱ دریافت تنظیمات (Config)
|
||||||
|
|
||||||
|
- **متد:** `GET`
|
||||||
|
- **آدرس:** `api/irrigation-recommendation/config/`
|
||||||
|
- **هدف:** برگرداندن اطلاعات مزرعه و لیست گزینههای محصول برای فرم توصیه آبیاری (هنگام بارگذاری صفحه).
|
||||||
|
- **ورودی:** ندارد. پارامترهای query خوانده یا استفاده نمیشوند.
|
||||||
|
|
||||||
|
**نمونه پاسخ:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"farmInfo": {
|
||||||
|
"soilType": "Loamy",
|
||||||
|
"waterQuality": "Medium EC",
|
||||||
|
"climateZone": "Temperate"
|
||||||
|
},
|
||||||
|
"cropOptions": [
|
||||||
|
{"id": "wheat", "labelKey": "wheat", "icon": "tabler-wheat"},
|
||||||
|
{"id": "corn", "labelKey": "corn", "icon": "tabler-plant-2"},
|
||||||
|
{"id": "cotton", "labelKey": "cotton", "icon": "tabler-flower"},
|
||||||
|
{"id": "saffron", "labelKey": "saffron", "icon": "tabler-flower-2"},
|
||||||
|
{"id": "canola", "labelKey": "canola", "icon": "tabler-leaf"},
|
||||||
|
{"id": "vegetables", "labelKey": "vegetables", "icon": "tabler-carrot"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| فیلد | نوع | توضیح |
|
||||||
|
|------|-----|--------|
|
||||||
|
| `farmInfo.soilType` | string | نوع خاک |
|
||||||
|
| `farmInfo.waterQuality` | string | کیفیت آب (مثلاً EC) |
|
||||||
|
| `farmInfo.climateZone` | string | منطقه اقلیمی |
|
||||||
|
| `cropOptions` | array | لیست محصولات: `id`, `labelKey`, `icon` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ۱.۲ دریافت توصیه آبیاری (Recommend)
|
||||||
|
|
||||||
|
- **متد:** `POST`
|
||||||
|
- **آدرس:** `api/irrigation-recommendation/recommend/`
|
||||||
|
- **هدف:** برگرداندن یک برنامهٔ آبیاری ثابت (تعداد در هفته، مدت، بهترین زمان، رطوبت، هشدار).
|
||||||
|
- **ورودی (بدن درخواست، اختیاری):** میتوانید JSON با فیلدهایی مثل `crop_id`, `soilType`, `waterQuality`, `climateZone` بفرستید؛ در پاسخ فعلی استفاده نمیشوند.
|
||||||
|
- **CSRF:** این endpoint از CSRF معاف است (برای فراخوانی از فرانت بدون توکن).
|
||||||
|
|
||||||
|
**نمونه پاسخ:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"plan": {
|
||||||
|
"frequencyPerWeek": 4,
|
||||||
|
"durationMinutes": 45,
|
||||||
|
"bestTimeOfDay": "05:00 - 07:00",
|
||||||
|
"moistureLevel": 72,
|
||||||
|
"warning": "Avoid irrigation during midday hours in the coming week due to forecasted high temperatures."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| فیلد | نوع | توضیح |
|
||||||
|
|------|-----|--------|
|
||||||
|
| `plan.frequencyPerWeek` | number | تعداد آبیاری در هفته |
|
||||||
|
| `plan.durationMinutes` | number | مدت هر نوبت (دقیقه) |
|
||||||
|
| `plan.bestTimeOfDay` | string | بهترین بازه زمانی روز |
|
||||||
|
| `plan.moistureLevel` | number | سطح رطوبت هدف (درصد) |
|
||||||
|
| `plan.warning` | string | هشدار یا توصیه اضافه |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۲. تشخیص آفت (Pest Detection)
|
||||||
|
|
||||||
|
**پیشوند:** `api/pest-detection/`
|
||||||
|
|
||||||
|
### ۲.۱ تحلیل تصویر (Analyze)
|
||||||
|
|
||||||
|
- **متد:** `POST`
|
||||||
|
- **آدرس:** `api/pest-detection/analyze/`
|
||||||
|
- **هدف:** برگرداندن نتیجهٔ ثابت تشخیص آفت (نام آفت، اطمینان، توضیح، درمان) — برای زمانی که کاربر تصویر گیاه را آپلود و درخواست تحلیل میکند.
|
||||||
|
- **ورودی (بدن درخواست، اختیاری):** JSON یا form-data (مثلاً شامل `image` یا `file`). در پاسخ فعلی استفاده نمیشود.
|
||||||
|
- **CSRF:** این endpoint از CSRF معاف است.
|
||||||
|
|
||||||
|
**نمونه پاسخ:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"pest": "شپشک",
|
||||||
|
"confidence": 92,
|
||||||
|
"description": "حشرات کوچک مکنده شیره که باعث پیچ خوردگی برگ میشوند.",
|
||||||
|
"treatment": "یک بار در هفته از اسپری روغن نیم استفاده کنید."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| فیلد | نوع | توضیح |
|
||||||
|
|------|-----|--------|
|
||||||
|
| `pest` | string | نام آفت |
|
||||||
|
| `confidence` | number | درصد اطمینان (۰–۱۰۰) |
|
||||||
|
| `description` | string | توضیح کوتاه آفت |
|
||||||
|
| `treatment` | string | توصیه درمان |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۳. توصیه کوددهی (Fertilization Recommendation)
|
||||||
|
|
||||||
|
**پیشوند:** `api/fertilization-recommendation/`
|
||||||
|
|
||||||
|
### ۳.۱ دریافت تنظیمات (Config)
|
||||||
|
|
||||||
|
- **متد:** `GET`
|
||||||
|
- **آدرس:** `api/fertilization-recommendation/config/`
|
||||||
|
- **هدف:** برگرداندن دادهٔ مزرعه، مراحل رشد و گزینههای محصول برای فرم توصیه کوددهی (هنگام بارگذاری صفحه).
|
||||||
|
- **ورودی:** ندارد.
|
||||||
|
|
||||||
|
**نمونه پاسخ:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"farmData": {
|
||||||
|
"soilType": "Loamy",
|
||||||
|
"organicMatter": "Medium (2.5%)",
|
||||||
|
"waterEC": "1.2 dS/m"
|
||||||
|
},
|
||||||
|
"growthStages": [
|
||||||
|
{"id": "prePlanting", "icon": "tabler-seedling"},
|
||||||
|
{"id": "earlyGrowth", "icon": "tabler-leaf"},
|
||||||
|
{"id": "flowering", "icon": "tabler-flower"},
|
||||||
|
{"id": "fruiting", "icon": "tabler-apple"},
|
||||||
|
{"id": "postHarvest", "icon": "tabler-basket"}
|
||||||
|
],
|
||||||
|
"cropOptions": [
|
||||||
|
{"id": "wheat", "labelKey": "wheat", "icon": "tabler-wheat"},
|
||||||
|
{"id": "corn", "labelKey": "corn", "icon": "tabler-plant-2"},
|
||||||
|
{"id": "cotton", "labelKey": "cotton", "icon": "tabler-flower"},
|
||||||
|
{"id": "saffron", "labelKey": "saffron", "icon": "tabler-flower-2"},
|
||||||
|
{"id": "canola", "labelKey": "canola", "icon": "tabler-leaf"},
|
||||||
|
{"id": "vegetables", "labelKey": "vegetables", "icon": "tabler-carrot"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| فیلد | نوع | توضیح |
|
||||||
|
|------|-----|--------|
|
||||||
|
| `farmData.soilType` | string | نوع خاک |
|
||||||
|
| `farmData.organicMatter` | string | ماده آلی خاک |
|
||||||
|
| `farmData.waterEC` | string | EC آب |
|
||||||
|
| `growthStages` | array | مراحل رشد: `id`, `icon` |
|
||||||
|
| `cropOptions` | array | لیست محصولات: `id`, `labelKey`, `icon` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ۳.۲ دریافت توصیه کوددهی (Recommend)
|
||||||
|
|
||||||
|
- **متد:** `POST`
|
||||||
|
- **آدرس:** `api/fertilization-recommendation/recommend/`
|
||||||
|
- **هدف:** برگرداندن یک برنامهٔ کوددهی ثابت (نسبت NPK، مقدار در هکتار، روش و فاصله مصرف، استدلال).
|
||||||
|
- **ورودی (بدن درخواست، اختیاری):** JSON با فیلدهایی مثل `crop_id`, `growth_stage`, `soilType`, `organicMatter`, `waterEC`. در پاسخ فعلی استفاده نمیشوند.
|
||||||
|
- **CSRF:** این endpoint از CSRF معاف است.
|
||||||
|
|
||||||
|
**نمونه پاسخ:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"plan": {
|
||||||
|
"npkRatio": "20-20-20 (NPK)",
|
||||||
|
"amountPerHectare": "150 kg/ha",
|
||||||
|
"applicationMethod": "Foliar spray + soil broadcast",
|
||||||
|
"applicationInterval": "Every 14 days",
|
||||||
|
"reasoning": "Your loamy soil with medium organic matter (2.5%) provides good nutrient retention. Water EC of 1.2 dS/m indicates low salinity—suitable for most crops. At the flowering stage, increased phosphorus supports bloom development. We recommend a balanced NPK to maintain nitrogen for vegetative growth while boosting phosphorous for flowering."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| فیلد | نوع | توضیح |
|
||||||
|
|------|-----|--------|
|
||||||
|
| `plan.npkRatio` | string | نسبت NPK پیشنهادی |
|
||||||
|
| `plan.amountPerHectare` | string | مقدار مصرف در هکتار |
|
||||||
|
| `plan.applicationMethod` | string | روش مصرف (مثلاً محلولپاشی، خاکی) |
|
||||||
|
| `plan.applicationInterval` | string | فاصله بین مصرف |
|
||||||
|
| `plan.reasoning` | string | توضیح/استدلال توصیه |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## خلاصه Endpointها
|
||||||
|
|
||||||
|
| ماژول | متد | Endpoint |
|
||||||
|
|--------|------|----------|
|
||||||
|
| Irrigation | GET | `/api/irrigation-recommendation/config/` |
|
||||||
|
| Irrigation | POST | `/api/irrigation-recommendation/recommend/` |
|
||||||
|
| Pest Detection | POST | `/api/pest-detection/analyze/` |
|
||||||
|
| Fertilization | GET | `/api/fertilization-recommendation/config/` |
|
||||||
|
| Fertilization | POST | `/api/fertilization-recommendation/recommend/` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**توجه:** در نسخهٔ فعلی هیچ پردازش، اعتبارسنجی یا استفاده از پارامترهای ورودی در پاسخ انجام نمیشود؛ همهٔ خروجیها از دادهٔ ثابت (mock) هستند.
|
||||||
@@ -687,6 +687,7 @@
|
|||||||
},
|
},
|
||||||
"analyze": "تحلیل",
|
"analyze": "تحلیل",
|
||||||
"analyzing": "در حال تحلیل...",
|
"analyzing": "در حال تحلیل...",
|
||||||
|
"analyzeError": "خطا در تحلیل تصویر. دوباره تلاش کنید.",
|
||||||
"reset": "بازنشانی",
|
"reset": "بازنشانی",
|
||||||
"resultTitle": "نتیجه تشخیص",
|
"resultTitle": "نتیجه تشخیص",
|
||||||
"resultCard": {
|
"resultCard": {
|
||||||
@@ -732,6 +733,10 @@
|
|||||||
"amount": "مقدار",
|
"amount": "مقدار",
|
||||||
"timing": "زمانبندی",
|
"timing": "زمانبندی",
|
||||||
"whyThis": "چرا این توصیه؟"
|
"whyThis": "چرا این توصیه؟"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"contextLoad": "بارگذاری زمینه مزرعه ناموفق بود.",
|
||||||
|
"chatSend": "ارسال پیام ناموفق بود. دوباره تلاش کنید."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
# مستندات APIهای دستیار هوشمند مزرعه (Farm AI Assistant)
|
||||||
|
|
||||||
|
این سند تمام APIهای مورد نیاز برای صفحه **Farm AI Assistant** را شرح میدهد: ورودیها، خروجیها و استفاده در UI.
|
||||||
|
|
||||||
|
**مسیر صفحه:** `(dashboard)/(private)/farm-ai-assistant`
|
||||||
|
**کامپوننت اصلی:** `FarmAiAssistantChat`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## نمای کلی
|
||||||
|
|
||||||
|
دستیار هوشمند مزرعه برای کار به موارد زیر نیاز دارد:
|
||||||
|
|
||||||
|
| ردیف | API | هدف |
|
||||||
|
|------|-----|------|
|
||||||
|
| ۱ | **ارسال پیام به دستیار (Chat/Complete)** | دریافت پاسخ ساختیافته (توصیه، لیست، هشدار) بر اساس پیام کاربر و زمینه مزرعه |
|
||||||
|
| ۲ | **دریافت زمینه مزرعه (Farm Context)** | پر کردن نوار «زمینه مزرعه» (نوع خاک، EC آب، محصول، مرحله رشد، آخرین آبیاری) |
|
||||||
|
| ۳ | **توصیه آبیاری** | در صورت درخواست کاربر یا تصمیم دستیار برای توصیه آبیاری |
|
||||||
|
| ۴ | **توصیه کوددهی** | در صورت درخواست کاربر یا توصیه کود |
|
||||||
|
| ۵ | **تشخیص آفت از تصویر** | وقتی کاربر تصویر گیاه را ارسال میکند |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۱. API ارسال پیام به دستیار (Farm AI Chat)
|
||||||
|
|
||||||
|
این API هسته اصلی دستیار است و در حال حاضر در فرانت با پاسخ دمو شبیهسازی شده است؛ باید با API واقعی جایگزین شود.
|
||||||
|
|
||||||
|
### ۱.۱ مشخصات
|
||||||
|
|
||||||
|
- **متد:** `POST`
|
||||||
|
- **آدرس پیشنهادی:** `POST /api/farm-ai-assistant/chat/` یا `POST /api/farm-ai-assistant/messages/`
|
||||||
|
- **هدف:** ارسال پیام کاربر (و در صورت وجود تصویر) به همراه زمینه مزرعه و دریافت پاسخ ساختیافته دستیار.
|
||||||
|
|
||||||
|
### ۱.۲ ورودی (Request Body)
|
||||||
|
|
||||||
|
| فیلد | نوع | اجباری | توضیح |
|
||||||
|
|------|-----|--------|--------|
|
||||||
|
| `content` | string | بله | متن پیام کاربر |
|
||||||
|
| `images` | string[] یا base64[] | خیر | آرایه آدرس تصاویر یا داده base64 (در صورت استفاده از آپلود تصویر دوربین در چت) |
|
||||||
|
| `conversation_id` | string | خیر | شناسه مکالمه برای ادامه گفتگو؛ در اولین پیام ارسال نشود |
|
||||||
|
| `farm_context` | object | توصیه | زمینه مزرعه برای پاسخ شخصیسازیشده (در صورت نبودن، بکاند میتواند از پیشفرض استفاده کند) |
|
||||||
|
|
||||||
|
ساختار پیشنهادی `farm_context` (همخوان با `FarmContext` در فرانت):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"content": "برنامه آبیاری برای گوجه در مرحله گلدهی چطور باشد؟",
|
||||||
|
"farm_context": {
|
||||||
|
"soilType": "Loamy",
|
||||||
|
"waterEC": "1.2 dS/m",
|
||||||
|
"selectedCrop": "Tomato",
|
||||||
|
"growthStage": "Flowering",
|
||||||
|
"lastIrrigationStatus": "2 days ago"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
اگر از **تصویر** استفاده شود (دکمه دوربین در input):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"content": "این برگ زرد شده، چه مشکلی داره؟",
|
||||||
|
"images": ["data:image/jpeg;base64,..."],
|
||||||
|
"farm_context": { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ۱.۳ خروجی (Response Body)
|
||||||
|
|
||||||
|
پاسخ باید شامل **بخشهای ساختیافته** (sections) باشد تا در UI به صورت کارت (توصیه، لیست، هشدار) رندر شود.
|
||||||
|
|
||||||
|
**قالب پیشنهادی:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"message_id": "a-1739123456789",
|
||||||
|
"conversation_id": "conv-abc123",
|
||||||
|
"content": "",
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"type": "recommendation",
|
||||||
|
"title": "Irrigation recommendation",
|
||||||
|
"icon": "droplet",
|
||||||
|
"frequency": "3 times per week",
|
||||||
|
"amount": "15–20 L per plant",
|
||||||
|
"timing": "Early morning (05:00–07:00)",
|
||||||
|
"expandableExplanation": "Your loamy soil holds moisture well..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "list",
|
||||||
|
"title": "Key points",
|
||||||
|
"icon": "leaf",
|
||||||
|
"items": [
|
||||||
|
"Avoid midday watering to reduce evaporation",
|
||||||
|
"Drip irrigation preferred for root zone targeting"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "warning",
|
||||||
|
"title": "Weather advisory",
|
||||||
|
"icon": "warning",
|
||||||
|
"content": "High temps forecasted next week. Consider increasing frequency."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**ساختار هر بخش (Section) مطابق `AIResponseSection` در فرانت:**
|
||||||
|
|
||||||
|
| فیلد | نوع | اجباری | توضیح |
|
||||||
|
|------|-----|--------|--------|
|
||||||
|
| `type` | string | بله | یکی از: `text` \| `list` \| `recommendation` \| `warning` |
|
||||||
|
| `title` | string | خیر | عنوان بخش |
|
||||||
|
| `content` | string | خیر | برای `type: "text"` یا `type: "warning"` |
|
||||||
|
| `items` | string[] | خیر | برای `type: "list"` |
|
||||||
|
| `icon` | string | خیر | یکی از: `droplet` \| `leaf` \| `warning` \| `fertilizer` \| `calendar` |
|
||||||
|
| `frequency` | string | خیر | فقط برای `recommendation`: تعداد دفعات (مثلاً در هفته) |
|
||||||
|
| `amount` | string | خیر | فقط برای `recommendation`: مقدار (مثلاً لیتر یا کیلوگرم) |
|
||||||
|
| `timing` | string | خیر | فقط برای `recommendation`: زمان پیشنهادی |
|
||||||
|
| `expandableExplanation` | string | خیر | فقط برای `recommendation`: توضیح قابل گسترش «چرا این توصیه» |
|
||||||
|
|
||||||
|
- اگر `content` خالی باشد و فقط `sections` برگردد، در UI فقط کارتها نمایش داده میشوند (مطابق پیادهسازی فعلی).
|
||||||
|
- در صورت خطا انتظار میرود پاسخ با `status: "error"` و پیام مناسب برگردد.
|
||||||
@@ -123,6 +123,22 @@ export class ApiClient {
|
|||||||
return this.handleResponse<T>(response)
|
return this.handleResponse<T>(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST request with FormData (e.g. file upload). Does not set Content-Type so browser sets multipart/form-data.
|
||||||
|
*/
|
||||||
|
async postFormData<T>(endpoint: string, formData: FormData, customHeaders?: Record<string, string>): Promise<T> {
|
||||||
|
const url = `${this.baseURL}${endpoint}`
|
||||||
|
const headers = { ...this.getHeaders(customHeaders) }
|
||||||
|
delete headers['Content-Type']
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
|
||||||
|
return this.handleResponse<T>(response)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PUT request
|
* PUT request
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* Farm AI Assistant API
|
||||||
|
* GET context (farm bar data), POST chat (user message + optional farm_context/images).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { apiClient } from '../client'
|
||||||
|
import type { FarmContext } from '@views/dashboards/farm/farmAiAssistant/farmAiAssistantTypes'
|
||||||
|
|
||||||
|
const PREFIX = '/api/farm-ai-assistant'
|
||||||
|
|
||||||
|
export interface FarmContextResponse {
|
||||||
|
soilType: string
|
||||||
|
waterEC: string
|
||||||
|
selectedCrop: string
|
||||||
|
growthStage: string
|
||||||
|
lastIrrigationStatus: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatSection {
|
||||||
|
type: 'text' | 'list' | 'recommendation' | 'warning'
|
||||||
|
title?: string
|
||||||
|
content?: string
|
||||||
|
items?: string[]
|
||||||
|
icon?: 'droplet' | 'leaf' | 'warning' | 'fertilizer' | 'calendar'
|
||||||
|
frequency?: string
|
||||||
|
amount?: string
|
||||||
|
timing?: string
|
||||||
|
expandableExplanation?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatPayload {
|
||||||
|
content: string
|
||||||
|
farm_context?: FarmContext
|
||||||
|
images?: string[]
|
||||||
|
conversation_id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatResponseData {
|
||||||
|
message_id: string
|
||||||
|
conversation_id: string
|
||||||
|
content: string
|
||||||
|
sections: ChatSection[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiResponse<T> {
|
||||||
|
status: string
|
||||||
|
data: T
|
||||||
|
}
|
||||||
|
|
||||||
|
function unwrap<T>(res: ApiResponse<T>): T {
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export const farmAiAssistantService = {
|
||||||
|
/**
|
||||||
|
* Returns farm context for the context bar (soilType, waterEC, selectedCrop, growthStage, lastIrrigationStatus).
|
||||||
|
*/
|
||||||
|
getContext(): Promise<FarmContextResponse> {
|
||||||
|
return apiClient
|
||||||
|
.get<ApiResponse<FarmContextResponse>>(`${PREFIX}/context/`)
|
||||||
|
.then(unwrap)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send user message (and optional farm_context, images, conversation_id). Returns message with sections.
|
||||||
|
*/
|
||||||
|
chat(payload: ChatPayload): Promise<ChatResponseData> {
|
||||||
|
return apiClient
|
||||||
|
.post<ApiResponse<ChatResponseData>>(`${PREFIX}/chat/`, payload)
|
||||||
|
.then(unwrap)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* Fertilization Recommendation API
|
||||||
|
* @see RECOMMENDATION_APIS.md
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { apiClient } from '../client'
|
||||||
|
|
||||||
|
const PREFIX = '/api/fertilization-recommendation'
|
||||||
|
|
||||||
|
export interface FarmData {
|
||||||
|
soilType: string
|
||||||
|
organicMatter: string
|
||||||
|
waterEC: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GrowthStage {
|
||||||
|
id: string
|
||||||
|
icon: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CropOption {
|
||||||
|
id: string
|
||||||
|
labelKey: string
|
||||||
|
icon: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FertilizationConfigResponse {
|
||||||
|
farmData: FarmData
|
||||||
|
growthStages: GrowthStage[]
|
||||||
|
cropOptions: CropOption[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FertilizationPlan {
|
||||||
|
npkRatio: string
|
||||||
|
amountPerHectare: string
|
||||||
|
applicationMethod: string
|
||||||
|
applicationInterval: string
|
||||||
|
reasoning: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FertilizationRecommendPayload {
|
||||||
|
crop_id?: string
|
||||||
|
growth_stage?: string
|
||||||
|
soilType?: string
|
||||||
|
organicMatter?: string
|
||||||
|
waterEC?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiResponse<T> {
|
||||||
|
status: string
|
||||||
|
data: T
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unwrap<T>(promise: Promise<ApiResponse<T>>): Promise<T> {
|
||||||
|
const res = await promise
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fertilizationRecommendationService = {
|
||||||
|
getConfig(): Promise<FertilizationConfigResponse> {
|
||||||
|
return unwrap(apiClient.get<ApiResponse<FertilizationConfigResponse>>(`${PREFIX}/config/`))
|
||||||
|
},
|
||||||
|
|
||||||
|
recommend(payload?: FertilizationRecommendPayload): Promise<{ plan: FertilizationPlan }> {
|
||||||
|
return unwrap(apiClient.post<ApiResponse<{ plan: FertilizationPlan }>>(`${PREFIX}/recommend/`, payload ?? {}))
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* Irrigation Recommendation API
|
||||||
|
* @see RECOMMENDATION_APIS.md
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { apiClient } from '../client'
|
||||||
|
|
||||||
|
const PREFIX = '/api/irrigation-recommendation'
|
||||||
|
|
||||||
|
export interface FarmInfo {
|
||||||
|
soilType: string
|
||||||
|
waterQuality: string
|
||||||
|
climateZone: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CropOption {
|
||||||
|
id: string
|
||||||
|
labelKey: string
|
||||||
|
icon: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IrrigationConfigResponse {
|
||||||
|
farmInfo: FarmInfo
|
||||||
|
cropOptions: CropOption[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IrrigationPlan {
|
||||||
|
frequencyPerWeek: number
|
||||||
|
durationMinutes: number
|
||||||
|
bestTimeOfDay: string
|
||||||
|
moistureLevel: number
|
||||||
|
warning?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IrrigationRecommendPayload {
|
||||||
|
crop_id?: string
|
||||||
|
soilType?: string
|
||||||
|
waterQuality?: string
|
||||||
|
climateZone?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiResponse<T> {
|
||||||
|
status: string
|
||||||
|
data: T
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unwrap<T>(promise: Promise<ApiResponse<T>>): Promise<T> {
|
||||||
|
const res = await promise
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export const irrigationRecommendationService = {
|
||||||
|
getConfig(): Promise<IrrigationConfigResponse> {
|
||||||
|
return unwrap(apiClient.get<ApiResponse<IrrigationConfigResponse>>(`${PREFIX}/config/`))
|
||||||
|
},
|
||||||
|
|
||||||
|
recommend(payload?: IrrigationRecommendPayload): Promise<{ plan: IrrigationPlan }> {
|
||||||
|
return unwrap(apiClient.post<ApiResponse<{ plan: IrrigationPlan }>>(`${PREFIX}/recommend/`, payload ?? {}))
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Pest Detection API
|
||||||
|
* @see RECOMMENDATION_APIS.md
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { apiClient } from '../client'
|
||||||
|
|
||||||
|
const PREFIX = '/api/pest-detection'
|
||||||
|
|
||||||
|
export interface PestAnalyzeResult {
|
||||||
|
pest: string
|
||||||
|
confidence: number
|
||||||
|
description: string
|
||||||
|
treatment: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiResponse<T> {
|
||||||
|
status: string
|
||||||
|
data: T
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unwrap<T>(promise: Promise<ApiResponse<T>>): Promise<T> {
|
||||||
|
const res = await promise
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pestDetectionService = {
|
||||||
|
/**
|
||||||
|
* Analyze image (optional FormData with 'image' or 'file' key). Returns mock result if backend does not use image yet.
|
||||||
|
*/
|
||||||
|
analyze(formData?: FormData): Promise<PestAnalyzeResult> {
|
||||||
|
if (formData) {
|
||||||
|
return unwrap(apiClient.postFormData<ApiResponse<PestAnalyzeResult>>(`${PREFIX}/analyze/`, formData))
|
||||||
|
}
|
||||||
|
return unwrap(apiClient.post<ApiResponse<PestAnalyzeResult>>(`${PREFIX}/analyze/`, {}))
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useRef, useEffect } from 'react'
|
import { useState, useRef, useEffect } from 'react'
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslations } from 'next-intl'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
import Card from '@mui/material/Card'
|
import Card from '@mui/material/Card'
|
||||||
@@ -14,6 +15,7 @@ import classnames from 'classnames'
|
|||||||
|
|
||||||
// Util Imports
|
// Util Imports
|
||||||
import { commonLayoutClasses } from '@layouts/utils/layoutClasses'
|
import { commonLayoutClasses } from '@layouts/utils/layoutClasses'
|
||||||
|
import { farmAiAssistantService } from '@/libs/api/services/farmAiAssistantService'
|
||||||
|
|
||||||
import type { FarmContext, FarmAIMessage, AIResponseSection } from './farmAiAssistantTypes'
|
import type { FarmContext, FarmAIMessage, AIResponseSection } from './farmAiAssistantTypes'
|
||||||
|
|
||||||
@@ -34,36 +36,6 @@ const SUGGESTION_CHIPS = [
|
|||||||
{ id: 'plant-disease', labelKey: 'suggestions.plantDisease' }
|
{ id: 'plant-disease', labelKey: 'suggestions.plantDisease' }
|
||||||
]
|
]
|
||||||
|
|
||||||
// Demo structured AI response for display
|
|
||||||
const DEMO_AI_RESPONSE_SECTIONS: AIResponseSection[] = [
|
|
||||||
{
|
|
||||||
type: 'recommendation',
|
|
||||||
title: 'Irrigation recommendation',
|
|
||||||
icon: 'droplet',
|
|
||||||
frequency: '3 times per week',
|
|
||||||
amount: '15–20 L per plant',
|
|
||||||
timing: 'Early morning (05:00–07:00)',
|
|
||||||
expandableExplanation:
|
|
||||||
'Your loamy soil holds moisture well, but tomatoes during flowering need consistent moisture. Water EC of 1.2 dS/m is suitable. Last irrigation was 2 days ago—avoid overwatering to prevent blossom-end rot.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'list',
|
|
||||||
title: 'Key points',
|
|
||||||
icon: 'leaf',
|
|
||||||
items: [
|
|
||||||
'Avoid midday watering to reduce evaporation',
|
|
||||||
'Drip irrigation preferred for root zone targeting',
|
|
||||||
'Monitor soil moisture before each session'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'warning',
|
|
||||||
title: 'Weather advisory',
|
|
||||||
icon: 'warning',
|
|
||||||
content: 'High temps forecasted next week. Consider increasing frequency to 4x/week temporarily.'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
// ─── Main Component ────────────────────────────────────────────────────────
|
// ─── Main Component ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export default function FarmAiAssistantChat() {
|
export default function FarmAiAssistantChat() {
|
||||||
@@ -75,11 +47,42 @@ export default function FarmAiAssistantChat() {
|
|||||||
const [isTyping, setIsTyping] = useState(false)
|
const [isTyping, setIsTyping] = useState(false)
|
||||||
const [selectedChip, setSelectedChip] = useState<string | null>(null)
|
const [selectedChip, setSelectedChip] = useState<string | null>(null)
|
||||||
const [expandedExplanations, setExpandedExplanations] = useState<Set<string>>(new Set())
|
const [expandedExplanations, setExpandedExplanations] = useState<Set<string>>(new Set())
|
||||||
|
const [farmContext, setFarmContext] = useState<FarmContext>(DEFAULT_FARM_CONTEXT)
|
||||||
|
const [contextLoading, setContextLoading] = useState(true)
|
||||||
|
const [conversationId, setConversationId] = useState<string | null>(null)
|
||||||
const scrollRef = useRef<HTMLDivElement>(null)
|
const scrollRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const farmContext = DEFAULT_FARM_CONTEXT
|
|
||||||
const { primary, info, warning } = theme.palette
|
const { primary, info, warning } = theme.palette
|
||||||
|
|
||||||
|
// Fetch farm context on mount
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
farmAiAssistantService
|
||||||
|
.getContext()
|
||||||
|
.then(data => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setFarmContext({
|
||||||
|
soilType: data.soilType,
|
||||||
|
waterEC: data.waterEC,
|
||||||
|
selectedCrop: data.selectedCrop,
|
||||||
|
growthStage: data.growthStage,
|
||||||
|
lastIrrigationStatus: data.lastIrrigationStatus
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (!cancelled) {
|
||||||
|
toast.error(t('errors.contextLoad'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (!cancelled) setContextLoading(false)
|
||||||
|
})
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [t])
|
||||||
|
|
||||||
// Scroll to bottom on new messages
|
// Scroll to bottom on new messages
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollRef.current?.scrollTo({
|
scrollRef.current?.scrollTo({
|
||||||
@@ -110,18 +113,26 @@ export default function FarmAiAssistantChat() {
|
|||||||
setMessages(prev => [...prev, userMessage])
|
setMessages(prev => [...prev, userMessage])
|
||||||
setIsTyping(true)
|
setIsTyping(true)
|
||||||
|
|
||||||
// Simulate AI response (replace with actual API call)
|
try {
|
||||||
await new Promise(resolve => setTimeout(resolve, 1500))
|
const res = await farmAiAssistantService.chat({
|
||||||
|
content,
|
||||||
const aiMessage: FarmAIMessage = {
|
farm_context: farmContext,
|
||||||
id: `a-${Date.now()}`,
|
...(conversationId ? { conversation_id: conversationId } : {})
|
||||||
role: 'assistant',
|
})
|
||||||
content: '',
|
if (res.conversation_id) setConversationId(res.conversation_id)
|
||||||
timestamp: new Date(),
|
const aiMessage: FarmAIMessage = {
|
||||||
sections: DEMO_AI_RESPONSE_SECTIONS
|
id: res.message_id,
|
||||||
|
role: 'assistant',
|
||||||
|
content: res.content ?? '',
|
||||||
|
timestamp: new Date(),
|
||||||
|
sections: res.sections ?? []
|
||||||
|
}
|
||||||
|
setMessages(prev => [...prev, aiMessage])
|
||||||
|
} catch {
|
||||||
|
toast.error(t('errors.chatSend'))
|
||||||
|
} finally {
|
||||||
|
setIsTyping(false)
|
||||||
}
|
}
|
||||||
setMessages(prev => [...prev, aiMessage])
|
|
||||||
setIsTyping(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleExplanation = (id: string) => {
|
const toggleExplanation = (id: string) => {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import UploadBox from './components/UploadBox'
|
|||||||
import ResultCard from './components/ResultCard'
|
import ResultCard from './components/ResultCard'
|
||||||
import type { UploadedFile } from './components/UploadBox'
|
import type { UploadedFile } from './components/UploadBox'
|
||||||
import type { PestResult } from './components/ResultCard'
|
import type { PestResult } from './components/ResultCard'
|
||||||
|
import { pestDetectionService } from '@/libs/api/services/pestDetectionService'
|
||||||
|
|
||||||
export default function PlantPestDetection() {
|
export default function PlantPestDetection() {
|
||||||
const t = useTranslations('pestDetection')
|
const t = useTranslations('pestDetection')
|
||||||
@@ -36,23 +37,29 @@ export default function PlantPestDetection() {
|
|||||||
setError(null)
|
setError(null)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleAnalyze = useCallback(() => {
|
const handleAnalyze = useCallback(async () => {
|
||||||
if (!file) return
|
if (!file) return
|
||||||
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
setResult(null)
|
setResult(null)
|
||||||
|
|
||||||
const delay = 1500 + Math.random() * 1000
|
try {
|
||||||
setTimeout(() => {
|
const formData = new FormData()
|
||||||
|
formData.append('image', file.file)
|
||||||
|
const data = await pestDetectionService.analyze(formData)
|
||||||
setResult({
|
setResult({
|
||||||
pest: t('mockResult.pest'),
|
pest: data.pest,
|
||||||
confidence: 92,
|
confidence: data.confidence,
|
||||||
description: t('mockResult.description'),
|
description: data.description,
|
||||||
treatment: t('mockResult.treatment'),
|
treatment: data.treatment,
|
||||||
})
|
})
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const message = err && typeof err === 'object' && 'message' in err ? String((err as { message: unknown }).message) : t('analyzeError')
|
||||||
|
setError(message)
|
||||||
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}, delay)
|
}
|
||||||
}, [file, t])
|
}, [file, t])
|
||||||
|
|
||||||
const handleReset = useCallback(() => {
|
const handleReset = useCallback(() => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslations } from 'next-intl'
|
||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Card from '@mui/material/Card'
|
import Card from '@mui/material/Card'
|
||||||
@@ -8,42 +8,23 @@ import CardContent from '@mui/material/CardContent'
|
|||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Collapse from '@mui/material/Collapse'
|
import Collapse from '@mui/material/Collapse'
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress'
|
||||||
import { useTheme, alpha } from '@mui/material/styles'
|
import { useTheme, alpha } from '@mui/material/styles'
|
||||||
|
import type {
|
||||||
|
FarmData,
|
||||||
|
GrowthStage,
|
||||||
|
CropOption,
|
||||||
|
FertilizationPlan,
|
||||||
|
} from '@/libs/api/services/fertilizationRecommendationService'
|
||||||
|
import { fertilizationRecommendationService } from '@/libs/api/services/fertilizationRecommendationService'
|
||||||
|
|
||||||
// Types
|
|
||||||
interface FarmData {
|
|
||||||
soilType: string
|
|
||||||
organicMatter: string
|
|
||||||
waterEC: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GrowthStage {
|
|
||||||
id: string
|
|
||||||
icon: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CropOption {
|
|
||||||
id: string
|
|
||||||
labelKey: string
|
|
||||||
icon: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FertilizationPlan {
|
|
||||||
npkRatio: string
|
|
||||||
amountPerHectare: string
|
|
||||||
applicationMethod: string
|
|
||||||
applicationInterval: string
|
|
||||||
reasoning: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock farm data (from stored soil/water data - no inputs)
|
|
||||||
const DEFAULT_FARM_DATA: FarmData = {
|
const DEFAULT_FARM_DATA: FarmData = {
|
||||||
soilType: 'Loamy',
|
soilType: 'Loamy',
|
||||||
organicMatter: 'Medium (2.5%)',
|
organicMatter: 'Medium (2.5%)',
|
||||||
waterEC: '1.2 dS/m'
|
waterEC: '1.2 dS/m'
|
||||||
}
|
}
|
||||||
|
|
||||||
const GROWTH_STAGES: GrowthStage[] = [
|
const DEFAULT_GROWTH_STAGES: GrowthStage[] = [
|
||||||
{ id: 'prePlanting', icon: 'tabler-seedling' },
|
{ id: 'prePlanting', icon: 'tabler-seedling' },
|
||||||
{ id: 'earlyGrowth', icon: 'tabler-leaf' },
|
{ id: 'earlyGrowth', icon: 'tabler-leaf' },
|
||||||
{ id: 'flowering', icon: 'tabler-flower' },
|
{ id: 'flowering', icon: 'tabler-flower' },
|
||||||
@@ -51,7 +32,7 @@ const GROWTH_STAGES: GrowthStage[] = [
|
|||||||
{ id: 'postHarvest', icon: 'tabler-basket' }
|
{ id: 'postHarvest', icon: 'tabler-basket' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const CROP_OPTIONS: CropOption[] = [
|
const DEFAULT_CROP_OPTIONS: CropOption[] = [
|
||||||
{ id: 'wheat', labelKey: 'wheat', icon: 'tabler-wheat' },
|
{ id: 'wheat', labelKey: 'wheat', icon: 'tabler-wheat' },
|
||||||
{ id: 'corn', labelKey: 'corn', icon: 'tabler-plant-2' },
|
{ id: 'corn', labelKey: 'corn', icon: 'tabler-plant-2' },
|
||||||
{ id: 'cotton', labelKey: 'cotton', icon: 'tabler-flower' },
|
{ id: 'cotton', labelKey: 'cotton', icon: 'tabler-flower' },
|
||||||
@@ -60,22 +41,6 @@ const CROP_OPTIONS: CropOption[] = [
|
|||||||
{ id: 'vegetables', labelKey: 'vegetables', icon: 'tabler-carrot' }
|
{ id: 'vegetables', labelKey: 'vegetables', icon: 'tabler-carrot' }
|
||||||
]
|
]
|
||||||
|
|
||||||
// Mock plan generator (replace with API in production)
|
|
||||||
function generateFertilizationPlan(
|
|
||||||
_cropId: string,
|
|
||||||
_growthStageId: string,
|
|
||||||
_farmData: FarmData
|
|
||||||
): FertilizationPlan {
|
|
||||||
return {
|
|
||||||
npkRatio: '20-20-20 (NPK)',
|
|
||||||
amountPerHectare: '150 kg/ha',
|
|
||||||
applicationMethod: 'Foliar spray + soil broadcast',
|
|
||||||
applicationInterval: 'Every 14 days',
|
|
||||||
reasoning:
|
|
||||||
'Your loamy soil with medium organic matter (2.5%) provides good nutrient retention. Water EC of 1.2 dS/m indicates low salinity—suitable for most crops. At the flowering stage, increased phosphorus supports bloom development. We recommend a balanced NPK to maintain nitrogen for vegetative growth while boosting phosphorous for flowering.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SmartFertilizationRecommendation() {
|
export default function SmartFertilizationRecommendation() {
|
||||||
const t = useTranslations('fertilization')
|
const t = useTranslations('fertilization')
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
@@ -83,27 +48,56 @@ export default function SmartFertilizationRecommendation() {
|
|||||||
const primaryLight = theme.palette.primary.light
|
const primaryLight = theme.palette.primary.light
|
||||||
const primaryDark = theme.palette.primary.dark
|
const primaryDark = theme.palette.primary.dark
|
||||||
const paperBg = theme.palette.background.paper
|
const paperBg = theme.palette.background.paper
|
||||||
const [farmData] = useState<FarmData>(DEFAULT_FARM_DATA)
|
const [farmData, setFarmData] = useState<FarmData>(DEFAULT_FARM_DATA)
|
||||||
const [growthStage, setGrowthStage] = useState<string>(GROWTH_STAGES[0].id)
|
const [growthStages, setGrowthStages] = useState<GrowthStage[]>(DEFAULT_GROWTH_STAGES)
|
||||||
|
const [cropOptions, setCropOptions] = useState<CropOption[]>(DEFAULT_CROP_OPTIONS)
|
||||||
|
const [configLoading, setConfigLoading] = useState(true)
|
||||||
|
const [configError, setConfigError] = useState<string | null>(null)
|
||||||
|
const [growthStage, setGrowthStage] = useState<string>(DEFAULT_GROWTH_STAGES[0].id)
|
||||||
const [selectedCrop, setSelectedCrop] = useState<string | null>(null)
|
const [selectedCrop, setSelectedCrop] = useState<string | null>(null)
|
||||||
const [plan, setPlan] = useState<FertilizationPlan | null>(null)
|
const [plan, setPlan] = useState<FertilizationPlan | null>(null)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [reasoningExpanded, setReasoningExpanded] = useState(false)
|
const [reasoningExpanded, setReasoningExpanded] = useState(false)
|
||||||
|
|
||||||
const handleGenerate = () => {
|
useEffect(() => {
|
||||||
|
fertilizationRecommendationService
|
||||||
|
.getConfig()
|
||||||
|
.then(({ farmData: farm, growthStages: stages, cropOptions: crops }) => {
|
||||||
|
if (farm) setFarmData(farm)
|
||||||
|
if (stages?.length) {
|
||||||
|
setGrowthStages(stages)
|
||||||
|
setGrowthStage(stages[0].id)
|
||||||
|
}
|
||||||
|
if (crops?.length) setCropOptions(crops)
|
||||||
|
})
|
||||||
|
.catch((err: { message?: string }) => {
|
||||||
|
setConfigError(err?.message ?? 'Failed to load config')
|
||||||
|
})
|
||||||
|
.finally(() => setConfigLoading(false))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleGenerate = async () => {
|
||||||
if (!selectedCrop) return
|
if (!selectedCrop) return
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setPlan(null)
|
setPlan(null)
|
||||||
setReasoningExpanded(false)
|
setReasoningExpanded(false)
|
||||||
setTimeout(() => {
|
try {
|
||||||
setPlan(
|
const { plan: nextPlan } = await fertilizationRecommendationService.recommend({
|
||||||
generateFertilizationPlan(selectedCrop, growthStage, farmData)
|
crop_id: selectedCrop,
|
||||||
)
|
growth_stage: growthStage,
|
||||||
|
soilType: farmData.soilType,
|
||||||
|
organicMatter: farmData.organicMatter,
|
||||||
|
waterEC: farmData.waterEC,
|
||||||
|
})
|
||||||
|
setPlan(nextPlan)
|
||||||
|
} catch {
|
||||||
|
setPlan(null)
|
||||||
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}, 1400)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const stageIndex = GROWTH_STAGES.findIndex(s => s.id === growthStage)
|
const stageIndex = growthStages.findIndex(s => s.id === growthStage)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -184,7 +178,7 @@ export default function SmartFertilizationRecommendation() {
|
|||||||
{t('growthStage.title')}
|
{t('growthStage.title')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box className='flex gap-2 mb-6 overflow-x-auto pb-2 -mx-1 px-1 scrollbar-hide'>
|
<Box className='flex gap-2 mb-6 overflow-x-auto pb-2 -mx-1 px-1 scrollbar-hide'>
|
||||||
{GROWTH_STAGES.map((stage, idx) => {
|
{growthStages.map((stage, idx) => {
|
||||||
const isSelected = growthStage === stage.id
|
const isSelected = growthStage === stage.id
|
||||||
const isPast = idx < stageIndex
|
const isPast = idx < stageIndex
|
||||||
return (
|
return (
|
||||||
@@ -245,8 +239,17 @@ export default function SmartFertilizationRecommendation() {
|
|||||||
<Typography variant='subtitle2' fontWeight={600} color='text.secondary' className='mbe-3'>
|
<Typography variant='subtitle2' fontWeight={600} color='text.secondary' className='mbe-3'>
|
||||||
{t('plantSelection.title')}
|
{t('plantSelection.title')}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
{configLoading ? (
|
||||||
|
<Box className='flex justify-center py-8 mb-6'>
|
||||||
|
<CircularProgress size={32} sx={{ color: 'primary.main' }} />
|
||||||
|
</Box>
|
||||||
|
) : configError ? (
|
||||||
|
<Typography variant='body2' color='error' className='mb-6'>
|
||||||
|
{configError}
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
<Box className='flex flex-wrap gap-3 mb-6'>
|
<Box className='flex flex-wrap gap-3 mb-6'>
|
||||||
{CROP_OPTIONS.map(crop => (
|
{cropOptions.map(crop => (
|
||||||
<CropCard
|
<CropCard
|
||||||
key={crop.id}
|
key={crop.id}
|
||||||
crop={crop}
|
crop={crop}
|
||||||
@@ -258,13 +261,14 @@ export default function SmartFertilizationRecommendation() {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 5) Primary CTA Button - End of form */}
|
{/* 5) Primary CTA Button - End of form */}
|
||||||
<Box className='mb-8'>
|
<Box className='mb-8'>
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
variant='contained'
|
variant='contained'
|
||||||
disabled={!selectedCrop || loading}
|
disabled={!selectedCrop || loading || configLoading}
|
||||||
onClick={handleGenerate}
|
onClick={handleGenerate}
|
||||||
startIcon={<i className='tabler-sparkles text-xl' />}
|
startIcon={<i className='tabler-sparkles text-xl' />}
|
||||||
className='rounded-2xl py-3.5 text-base font-semibold shadow-lg transition-all duration-300 hover:shadow-xl hover:scale-[1.01] active:scale-[0.99]'
|
className='rounded-2xl py-3.5 text-base font-semibold shadow-lg transition-all duration-300 hover:shadow-xl hover:scale-[1.01] active:scale-[0.99]'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslations } from 'next-intl'
|
||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Card from '@mui/material/Card'
|
import Card from '@mui/material/Card'
|
||||||
@@ -10,77 +10,57 @@ import Button from '@mui/material/Button'
|
|||||||
import IconButton from '@mui/material/IconButton'
|
import IconButton from '@mui/material/IconButton'
|
||||||
import CircularProgress from '@mui/material/CircularProgress'
|
import CircularProgress from '@mui/material/CircularProgress'
|
||||||
import { useTheme, alpha } from '@mui/material/styles'
|
import { useTheme, alpha } from '@mui/material/styles'
|
||||||
|
import type { FarmInfo, CropOption, IrrigationPlan } from '@/libs/api/services/irrigationRecommendationService'
|
||||||
|
import { irrigationRecommendationService } from '@/libs/api/services/irrigationRecommendationService'
|
||||||
|
|
||||||
// Types
|
|
||||||
interface FarmInfo {
|
|
||||||
soilType: string
|
|
||||||
waterQuality: string
|
|
||||||
climateZone: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CropOption {
|
|
||||||
id: string
|
|
||||||
labelKey: string
|
|
||||||
icon: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IrrigationPlan {
|
|
||||||
frequencyPerWeek: number
|
|
||||||
durationMinutes: number
|
|
||||||
bestTimeOfDay: string
|
|
||||||
moistureLevel: number // 0-100
|
|
||||||
warning?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock farm data (replace with API/store in production)
|
|
||||||
const DEFAULT_FARM_INFO: FarmInfo = {
|
const DEFAULT_FARM_INFO: FarmInfo = {
|
||||||
soilType: 'Loamy',
|
soilType: 'Loamy',
|
||||||
waterQuality: 'Medium EC',
|
waterQuality: 'Medium EC',
|
||||||
climateZone: 'Temperate'
|
climateZone: 'Temperate'
|
||||||
}
|
}
|
||||||
|
|
||||||
const CROP_OPTIONS: CropOption[] = [
|
|
||||||
{ id: 'wheat', labelKey: 'wheat', icon: 'tabler-wheat' },
|
|
||||||
{ id: 'corn', labelKey: 'corn', icon: 'tabler-plant-2' },
|
|
||||||
{ id: 'cotton', labelKey: 'cotton', icon: 'tabler-flower' },
|
|
||||||
{ id: 'saffron', labelKey: 'saffron', icon: 'tabler-flower-2' },
|
|
||||||
{ id: 'canola', labelKey: 'canola', icon: 'tabler-leaf' },
|
|
||||||
{ id: 'vegetables', labelKey: 'vegetables', icon: 'tabler-carrot' }
|
|
||||||
]
|
|
||||||
|
|
||||||
// Mock plan generator (replace with API in production)
|
|
||||||
function generateIrrigationPlan(_cropId: string, _farmInfo: FarmInfo): IrrigationPlan {
|
|
||||||
return {
|
|
||||||
frequencyPerWeek: 4,
|
|
||||||
durationMinutes: 45,
|
|
||||||
bestTimeOfDay: '05:00 - 07:00',
|
|
||||||
moistureLevel: 72,
|
|
||||||
warning: 'Avoid irrigation during midday hours in the coming week due to forecasted high temperatures.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SmartIrrigationRecommendation() {
|
export default function SmartIrrigationRecommendation() {
|
||||||
const t = useTranslations('irrigation')
|
const t = useTranslations('irrigation')
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const [farmInfo] = useState<FarmInfo>(DEFAULT_FARM_INFO)
|
const [farmInfo, setFarmInfo] = useState<FarmInfo>(DEFAULT_FARM_INFO)
|
||||||
|
const [cropOptions, setCropOptions] = useState<CropOption[]>([])
|
||||||
|
const [configLoading, setConfigLoading] = useState(true)
|
||||||
|
const [configError, setConfigError] = useState<string | null>(null)
|
||||||
const [selectedCrop, setSelectedCrop] = useState<string | null>(null)
|
const [selectedCrop, setSelectedCrop] = useState<string | null>(null)
|
||||||
const [plan, setPlan] = useState<IrrigationPlan | null>(null)
|
const [plan, setPlan] = useState<IrrigationPlan | null>(null)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const primaryMain = theme.palette.primary.main
|
const primaryMain = theme.palette.primary.main
|
||||||
const primaryLight = theme.palette.primary.light
|
const primaryLight = theme.palette.primary.light
|
||||||
const primaryDark = theme.palette.primary.dark
|
const primaryDark = theme.palette.primary.dark
|
||||||
const bgDefault = theme.palette.background.default
|
|
||||||
const paperBg = theme.palette.background.paper
|
const paperBg = theme.palette.background.paper
|
||||||
|
|
||||||
const handleGenerate = () => {
|
useEffect(() => {
|
||||||
|
irrigationRecommendationService
|
||||||
|
.getConfig()
|
||||||
|
.then(({ farmInfo: info, cropOptions: crops }) => {
|
||||||
|
setFarmInfo(info)
|
||||||
|
setCropOptions(crops.length > 0 ? crops : [])
|
||||||
|
})
|
||||||
|
.catch((err: { message?: string }) => {
|
||||||
|
setConfigError(err?.message ?? 'Failed to load config')
|
||||||
|
})
|
||||||
|
.finally(() => setConfigLoading(false))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleGenerate = async () => {
|
||||||
if (!selectedCrop) return
|
if (!selectedCrop) return
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setPlan(null)
|
setPlan(null)
|
||||||
// Simulate API delay
|
try {
|
||||||
setTimeout(() => {
|
const { plan: nextPlan } = await irrigationRecommendationService.recommend({
|
||||||
setPlan(generateIrrigationPlan(selectedCrop, farmInfo))
|
crop_id: selectedCrop,
|
||||||
|
})
|
||||||
|
setPlan(nextPlan)
|
||||||
|
} catch {
|
||||||
|
setPlan(null)
|
||||||
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}, 1200)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -160,8 +140,17 @@ export default function SmartIrrigationRecommendation() {
|
|||||||
<Typography variant='subtitle2' fontWeight={600} color='text.secondary' className='mbe-3'>
|
<Typography variant='subtitle2' fontWeight={600} color='text.secondary' className='mbe-3'>
|
||||||
{t('plantSelection.title')}
|
{t('plantSelection.title')}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
{configLoading ? (
|
||||||
|
<Box className='flex justify-center py-8'>
|
||||||
|
<CircularProgress size={32} sx={{ color: 'primary.main' }} />
|
||||||
|
</Box>
|
||||||
|
) : configError ? (
|
||||||
|
<Typography variant='body2' color='error' className='mb-6'>
|
||||||
|
{configError}
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
<Box className='flex flex-wrap gap-3 mb-6'>
|
<Box className='flex flex-wrap gap-3 mb-6'>
|
||||||
{CROP_OPTIONS.map(crop => (
|
{(cropOptions.length > 0 ? cropOptions : []).map(crop => (
|
||||||
<CropCard
|
<CropCard
|
||||||
key={crop.id}
|
key={crop.id}
|
||||||
crop={crop}
|
crop={crop}
|
||||||
@@ -171,13 +160,14 @@ export default function SmartIrrigationRecommendation() {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 4) Primary CTA Button - End of form */}
|
{/* 4) Primary CTA Button - End of form */}
|
||||||
<Box className='mb-8'>
|
<Box className='mb-8'>
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
variant='contained'
|
variant='contained'
|
||||||
disabled={!selectedCrop || loading}
|
disabled={!selectedCrop || loading || configLoading}
|
||||||
onClick={handleGenerate}
|
onClick={handleGenerate}
|
||||||
startIcon={<i className='tabler-sparkles text-xl' />}
|
startIcon={<i className='tabler-sparkles text-xl' />}
|
||||||
className='rounded-2xl py-3.5 text-base font-semibold shadow-lg transition-all duration-300 hover:shadow-xl hover:scale-[1.01] active:scale-[0.99]'
|
className='rounded-2xl py-3.5 text-base font-semibold shadow-lg transition-all duration-300 hover:shadow-xl hover:scale-[1.01] active:scale-[0.99]'
|
||||||
|
|||||||
Reference in New Issue
Block a user