diff --git a/RECOMMENDATION_APIS.md b/RECOMMENDATION_APIS.md new file mode 100644 index 0000000..d6f3d3a --- /dev/null +++ b/RECOMMENDATION_APIS.md @@ -0,0 +1,223 @@ +# مستندات APIهای توصیه و تشخیص + +این سند سه گروه API را شرح می‌دهد: **توصیه آبیاری**، **تشخیص آفت** و **توصیه کوددهی**. همهٔ پاسخ‌ها در حال حاضر از دادهٔ ثابت (mock) برگردانده می‌شوند و پارامترهای ورودی در پاسخ استفاده نمی‌شوند. + +**پایهٔ آدرس API:** `/api/` + +**قالب کلی پاسخ:** +`{"status": "success", "data": }` — فقط با کد وضعیت 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) هستند. diff --git a/messages/fa.json b/messages/fa.json index ac8eb1a..faec7af 100644 --- a/messages/fa.json +++ b/messages/fa.json @@ -687,6 +687,7 @@ }, "analyze": "تحلیل", "analyzing": "در حال تحلیل...", + "analyzeError": "خطا در تحلیل تصویر. دوباره تلاش کنید.", "reset": "بازنشانی", "resultTitle": "نتیجه تشخیص", "resultCard": { @@ -732,6 +733,10 @@ "amount": "مقدار", "timing": "زمان‌بندی", "whyThis": "چرا این توصیه؟" + }, + "errors": { + "contextLoad": "بارگذاری زمینه مزرعه ناموفق بود.", + "chatSend": "ارسال پیام ناموفق بود. دوباره تلاش کنید." } } } diff --git a/src/app/(dashboard)/(private)/farm-ai-assistant/FARM_AI_ASSISTANT_APIS.md b/src/app/(dashboard)/(private)/farm-ai-assistant/FARM_AI_ASSISTANT_APIS.md new file mode 100644 index 0000000..7c7fc50 --- /dev/null +++ b/src/app/(dashboard)/(private)/farm-ai-assistant/FARM_AI_ASSISTANT_APIS.md @@ -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"` و پیام مناسب برگردد. diff --git a/src/libs/api/client.ts b/src/libs/api/client.ts index 061ca67..52e1803 100644 --- a/src/libs/api/client.ts +++ b/src/libs/api/client.ts @@ -123,6 +123,22 @@ export class ApiClient { return this.handleResponse(response) } + /** + * POST request with FormData (e.g. file upload). Does not set Content-Type so browser sets multipart/form-data. + */ + async postFormData(endpoint: string, formData: FormData, customHeaders?: Record): Promise { + 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(response) + } + /** * PUT request */ diff --git a/src/libs/api/services/farmAiAssistantService.ts b/src/libs/api/services/farmAiAssistantService.ts new file mode 100644 index 0000000..41a2131 --- /dev/null +++ b/src/libs/api/services/farmAiAssistantService.ts @@ -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 { + status: string + data: T +} + +function unwrap(res: ApiResponse): T { + return res.data +} + +export const farmAiAssistantService = { + /** + * Returns farm context for the context bar (soilType, waterEC, selectedCrop, growthStage, lastIrrigationStatus). + */ + getContext(): Promise { + return apiClient + .get>(`${PREFIX}/context/`) + .then(unwrap) + }, + + /** + * Send user message (and optional farm_context, images, conversation_id). Returns message with sections. + */ + chat(payload: ChatPayload): Promise { + return apiClient + .post>(`${PREFIX}/chat/`, payload) + .then(unwrap) + } +} diff --git a/src/libs/api/services/fertilizationRecommendationService.ts b/src/libs/api/services/fertilizationRecommendationService.ts new file mode 100644 index 0000000..6889297 --- /dev/null +++ b/src/libs/api/services/fertilizationRecommendationService.ts @@ -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 { + status: string + data: T +} + +async function unwrap(promise: Promise>): Promise { + const res = await promise + return res.data +} + +export const fertilizationRecommendationService = { + getConfig(): Promise { + return unwrap(apiClient.get>(`${PREFIX}/config/`)) + }, + + recommend(payload?: FertilizationRecommendPayload): Promise<{ plan: FertilizationPlan }> { + return unwrap(apiClient.post>(`${PREFIX}/recommend/`, payload ?? {})) + }, +} diff --git a/src/libs/api/services/irrigationRecommendationService.ts b/src/libs/api/services/irrigationRecommendationService.ts new file mode 100644 index 0000000..cb182dd --- /dev/null +++ b/src/libs/api/services/irrigationRecommendationService.ts @@ -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 { + status: string + data: T +} + +async function unwrap(promise: Promise>): Promise { + const res = await promise + return res.data +} + +export const irrigationRecommendationService = { + getConfig(): Promise { + return unwrap(apiClient.get>(`${PREFIX}/config/`)) + }, + + recommend(payload?: IrrigationRecommendPayload): Promise<{ plan: IrrigationPlan }> { + return unwrap(apiClient.post>(`${PREFIX}/recommend/`, payload ?? {})) + }, +} diff --git a/src/libs/api/services/pestDetectionService.ts b/src/libs/api/services/pestDetectionService.ts new file mode 100644 index 0000000..6896e7d --- /dev/null +++ b/src/libs/api/services/pestDetectionService.ts @@ -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 { + status: string + data: T +} + +async function unwrap(promise: Promise>): Promise { + 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 { + if (formData) { + return unwrap(apiClient.postFormData>(`${PREFIX}/analyze/`, formData)) + } + return unwrap(apiClient.post>(`${PREFIX}/analyze/`, {})) + }, +} diff --git a/src/views/dashboards/farm/farmAiAssistant/FarmAiAssistantChat.tsx b/src/views/dashboards/farm/farmAiAssistant/FarmAiAssistantChat.tsx index 72efb1a..eba2fe4 100644 --- a/src/views/dashboards/farm/farmAiAssistant/FarmAiAssistantChat.tsx +++ b/src/views/dashboards/farm/farmAiAssistant/FarmAiAssistantChat.tsx @@ -2,6 +2,7 @@ import { useState, useRef, useEffect } from 'react' import { useTranslations } from 'next-intl' +import { toast } from 'react-toastify' import Box from '@mui/material/Box' import Typography from '@mui/material/Typography' import Card from '@mui/material/Card' @@ -14,6 +15,7 @@ import classnames from 'classnames' // Util Imports import { commonLayoutClasses } from '@layouts/utils/layoutClasses' +import { farmAiAssistantService } from '@/libs/api/services/farmAiAssistantService' import type { FarmContext, FarmAIMessage, AIResponseSection } from './farmAiAssistantTypes' @@ -34,36 +36,6 @@ const SUGGESTION_CHIPS = [ { 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 ──────────────────────────────────────────────────────── export default function FarmAiAssistantChat() { @@ -75,11 +47,42 @@ export default function FarmAiAssistantChat() { const [isTyping, setIsTyping] = useState(false) const [selectedChip, setSelectedChip] = useState(null) const [expandedExplanations, setExpandedExplanations] = useState>(new Set()) + const [farmContext, setFarmContext] = useState(DEFAULT_FARM_CONTEXT) + const [contextLoading, setContextLoading] = useState(true) + const [conversationId, setConversationId] = useState(null) const scrollRef = useRef(null) - const farmContext = DEFAULT_FARM_CONTEXT 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 useEffect(() => { scrollRef.current?.scrollTo({ @@ -110,18 +113,26 @@ export default function FarmAiAssistantChat() { setMessages(prev => [...prev, userMessage]) setIsTyping(true) - // Simulate AI response (replace with actual API call) - await new Promise(resolve => setTimeout(resolve, 1500)) - - const aiMessage: FarmAIMessage = { - id: `a-${Date.now()}`, - role: 'assistant', - content: '', - timestamp: new Date(), - sections: DEMO_AI_RESPONSE_SECTIONS + try { + const res = await farmAiAssistantService.chat({ + content, + farm_context: farmContext, + ...(conversationId ? { conversation_id: conversationId } : {}) + }) + if (res.conversation_id) setConversationId(res.conversation_id) + const aiMessage: FarmAIMessage = { + 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) => { diff --git a/src/views/dashboards/farm/pestDetection/PlantPestDetection.tsx b/src/views/dashboards/farm/pestDetection/PlantPestDetection.tsx index 5ba340e..2e204fb 100644 --- a/src/views/dashboards/farm/pestDetection/PlantPestDetection.tsx +++ b/src/views/dashboards/farm/pestDetection/PlantPestDetection.tsx @@ -20,6 +20,7 @@ import UploadBox from './components/UploadBox' import ResultCard from './components/ResultCard' import type { UploadedFile } from './components/UploadBox' import type { PestResult } from './components/ResultCard' +import { pestDetectionService } from '@/libs/api/services/pestDetectionService' export default function PlantPestDetection() { const t = useTranslations('pestDetection') @@ -36,23 +37,29 @@ export default function PlantPestDetection() { setError(null) }, []) - const handleAnalyze = useCallback(() => { + const handleAnalyze = useCallback(async () => { if (!file) return setLoading(true) setError(null) setResult(null) - const delay = 1500 + Math.random() * 1000 - setTimeout(() => { + try { + const formData = new FormData() + formData.append('image', file.file) + const data = await pestDetectionService.analyze(formData) setResult({ - pest: t('mockResult.pest'), - confidence: 92, - description: t('mockResult.description'), - treatment: t('mockResult.treatment'), + pest: data.pest, + confidence: data.confidence, + description: data.description, + 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) - }, delay) + } }, [file, t]) const handleReset = useCallback(() => { diff --git a/src/views/dashboards/farm/smartFertilization/SmartFertilizationRecommendation.tsx b/src/views/dashboards/farm/smartFertilization/SmartFertilizationRecommendation.tsx index 7604923..f487115 100644 --- a/src/views/dashboards/farm/smartFertilization/SmartFertilizationRecommendation.tsx +++ b/src/views/dashboards/farm/smartFertilization/SmartFertilizationRecommendation.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState } from 'react' +import { useState, useEffect } from 'react' import { useTranslations } from 'next-intl' import Box from '@mui/material/Box' import Card from '@mui/material/Card' @@ -8,42 +8,23 @@ import CardContent from '@mui/material/CardContent' import Typography from '@mui/material/Typography' import Button from '@mui/material/Button' import Collapse from '@mui/material/Collapse' +import CircularProgress from '@mui/material/CircularProgress' 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 = { soilType: 'Loamy', organicMatter: 'Medium (2.5%)', waterEC: '1.2 dS/m' } -const GROWTH_STAGES: GrowthStage[] = [ +const DEFAULT_GROWTH_STAGES: GrowthStage[] = [ { id: 'prePlanting', icon: 'tabler-seedling' }, { id: 'earlyGrowth', icon: 'tabler-leaf' }, { id: 'flowering', icon: 'tabler-flower' }, @@ -51,7 +32,7 @@ const GROWTH_STAGES: GrowthStage[] = [ { id: 'postHarvest', icon: 'tabler-basket' } ] -const CROP_OPTIONS: CropOption[] = [ +const DEFAULT_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' }, @@ -60,22 +41,6 @@ const CROP_OPTIONS: CropOption[] = [ { 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() { const t = useTranslations('fertilization') const theme = useTheme() @@ -83,27 +48,56 @@ export default function SmartFertilizationRecommendation() { const primaryLight = theme.palette.primary.light const primaryDark = theme.palette.primary.dark const paperBg = theme.palette.background.paper - const [farmData] = useState(DEFAULT_FARM_DATA) - const [growthStage, setGrowthStage] = useState(GROWTH_STAGES[0].id) + const [farmData, setFarmData] = useState(DEFAULT_FARM_DATA) + const [growthStages, setGrowthStages] = useState(DEFAULT_GROWTH_STAGES) + const [cropOptions, setCropOptions] = useState(DEFAULT_CROP_OPTIONS) + const [configLoading, setConfigLoading] = useState(true) + const [configError, setConfigError] = useState(null) + const [growthStage, setGrowthStage] = useState(DEFAULT_GROWTH_STAGES[0].id) const [selectedCrop, setSelectedCrop] = useState(null) const [plan, setPlan] = useState(null) const [loading, setLoading] = 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 setLoading(true) setPlan(null) setReasoningExpanded(false) - setTimeout(() => { - setPlan( - generateFertilizationPlan(selectedCrop, growthStage, farmData) - ) + try { + const { plan: nextPlan } = await fertilizationRecommendationService.recommend({ + crop_id: selectedCrop, + growth_stage: growthStage, + soilType: farmData.soilType, + organicMatter: farmData.organicMatter, + waterEC: farmData.waterEC, + }) + setPlan(nextPlan) + } catch { + setPlan(null) + } finally { setLoading(false) - }, 1400) + } } - const stageIndex = GROWTH_STAGES.findIndex(s => s.id === growthStage) + const stageIndex = growthStages.findIndex(s => s.id === growthStage) return ( - {GROWTH_STAGES.map((stage, idx) => { + {growthStages.map((stage, idx) => { const isSelected = growthStage === stage.id const isPast = idx < stageIndex return ( @@ -245,8 +239,17 @@ export default function SmartFertilizationRecommendation() { {t('plantSelection.title')} + {configLoading ? ( + + + + ) : configError ? ( + + {configError} + + ) : ( - {CROP_OPTIONS.map(crop => ( + {cropOptions.map(crop => ( ))} + )} {/* 5) Primary CTA Button - End of form */}