diff --git a/messages/fa.json b/messages/fa.json index f62f745..208804b 100644 --- a/messages/fa.json +++ b/messages/fa.json @@ -38,6 +38,10 @@ "soilData": "اطلاعات خاک", "cropZoning": "زون‌بندی کشت", "plantSimulator": "شبیه‌ساز رشد گیاه", + "irrigationRecommendation": "توصیه آبیاری", + "fertilizationRecommendation": "توصیه کوددهی", + "farmAiAssistant": "دستیار هوشمند مزرعه", + "pestDetection": "تشخیص آفات گیاهی", "dataSection": "بخش داده‌ها", "crm": "مدیریت ارتباط با مشتری", "analytics": "تحلیل‌ها", @@ -592,5 +596,139 @@ "waterStatus": "وضعیت آب", "yieldStatus": "محصول دهی", "description": "این شبیه‌ساز رشد گیاه را بر اساس سرعت پایه، میزان نور خورشید و آب دریافتی محاسبه می‌کند. هر برگ به صورت تدریجی روی ساقه ظاهر شده و با حرکت طبیعی در باد نمایش داده می‌شود. محصول‌دهی (g) پس از ۲۰٪ رشد شروع شده و با تعداد برگ، نور و آب شتاب می‌گیرد. سرعت محصول (g/s) نشان‌دهنده نرخ لحظه‌ای تولید است. نمودار تغییرات همه شاخص‌ها را در طول زمان ثبت می‌کند." + }, + "irrigation": { + "title": "توصیه هوشمند آبیاری", + "subtitle": "بر اساس داده‌های ثبت شده مزرعه شما", + "farmInfo": { + "title": "اطلاعات مزرعه", + "soilType": "نوع خاک", + "waterQuality": "کیفیت آب", + "climateZone": "منطقه اقلیمی" + }, + "verifiedBadge": "داده تأیید شده", + "editFarmInfo": "ویرایش اطلاعات مزرعه", + "plantSelection": { + "title": "انتخاب محصول" + }, + "crops": { + "wheat": "گندم", + "corn": "ذرت", + "cotton": "پنبه", + "saffron": "زعفران", + "canola": "کلزا", + "vegetables": "سبزیجات" + }, + "generateCta": "تولید برنامه آبیاری", + "generating": "در حال تحلیل و تولید برنامه آبیاری...", + "result": { + "moistureLevel": "سطح رطوبت هدف", + "frequency": "تناوب آبیاری", + "timesPerWeek": "بار در هفته", + "duration": "مدت هر نوبت", + "minutes": "دقیقه", + "bestTime": "بهترین زمان آبیاری", + "smartWarning": "هشدار هوشمند" + } + }, + "fertilization": { + "title": "برنامه هوشمند کوددهی", + "subtitle": "شخصی‌سازی شده برای پروفایل خاک شما", + "farmData": { + "title": "خلاصه داده‌های مزرعه", + "soilType": "نوع خاک", + "organicMatter": "میزان ماده آلی", + "waterEC": "هدایت الکتریکی آب" + }, + "verifiedBadge": "داده تأیید کارشناس", + "growthStage": { + "title": "مرحله رشد", + "prePlanting": "قبل از کشت", + "earlyGrowth": "رشد اولیه", + "flowering": "گلدهی", + "fruiting": "میوه‌دهی", + "postHarvest": "پس از برداشت" + }, + "plantSelection": { + "title": "انتخاب محصول" + }, + "crops": { + "wheat": "گندم", + "corn": "ذرت", + "cotton": "پنبه", + "saffron": "زعفران", + "canola": "کلزا", + "vegetables": "سبزیجات" + }, + "generateCta": "تولید برنامه کوددهی", + "generating": "در حال تحلیل و تولید نسخه تغذیه‌ای...", + "result": { + "title": "نسخه تغذیه گیاه", + "fertilizerType": "نوع کود توصیه‌شده", + "amountPerHectare": "مقدار در هکتار", + "applicationMethod": "روش مصرف", + "applicationInterval": "فواصل مصرف", + "whyRecommendation": "چرا این توصیه؟" + } + }, + "pestDetection": { + "title": "تشخیص آفات گیاهی با هوش مصنوعی", + "subtitle": "تصویر گیاه را آپلود کرده و آفات را فوراً تشخیص دهید", + "upload": { + "dragDrop": "کشیدن و رها کردن یا کلیک برای آپلود", + "dropHere": "تصویر را اینجا رها کنید", + "fileFormats": "PNG، JPG، GIF یا WebP (حداکثر ۱۰ مگابایت)", + "remove": "حذف", + "ariaLabel": "آپلود تصویر گیاه", + "invalidFile": "فایل نامعتبر. لطفاً یک تصویر (PNG، JPG، GIF، WebP، حداکثر ۱۰ مگابایت) آپلود کنید." + }, + "analyze": "تحلیل", + "analyzing": "در حال تحلیل...", + "reset": "بازنشانی", + "resultTitle": "نتیجه تشخیص", + "resultCard": { + "detectedPest": "آفت تشخیص داده شده", + "confidence": "اعتماد", + "description": "توضیحات", + "recommendedTreatment": "درمان پیشنهادی" + }, + "mockResult": { + "pest": "شپشک", + "description": "حشرات کوچک مکنده شیره که باعث پیچ خوردگی برگ می‌شوند.", + "treatment": "یک بار در هفته از اسپری روغن نیم استفاده کنید." + } + }, + "farmAiAssistant": { + "header": { + "title": "دستیار هوشمند مزرعه", + "subtitle": "پاسخ بر اساس داده‌های ثبت‌شده مزرعه شما" + }, + "context": { + "title": "زمینه مزرعه", + "soilType": "نوع خاک", + "waterEC": "EC آب / کیفیت", + "crop": "محصول انتخابی", + "growthStage": "مرحله رشد", + "lastIrrigation": "وضعیت آخرین آبیاری" + }, + "emptyState": { + "title": "سوال خود را بپرسید", + "subtitle": "دستیار بر اساس خاک، آب و محصول شما پاسخ می‌دهد" + }, + "suggestions": { + "yellowLeaves": "چرا برگ‌های گوجه زرد می‌شوند؟", + "irrigationPlan": "برنامه آبیاری امروز چیست؟", + "fertilizerFlowering": "در گلدهی چه کودی بدهم؟", + "plantDisease": "این بیماری گیاهی را تشخیص بده" + }, + "input": { + "placeholder": "سوال مزرعه‌ای خود را بنویسید..." + }, + "recommendation": { + "frequency": "تناوب", + "amount": "مقدار", + "timing": "زمان‌بندی", + "whyThis": "چرا این توصیه؟" + } } } diff --git a/package-lock.json b/package-lock.json index 3e6e073..08e57e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,11 +56,13 @@ "cmdk": "1.0.4", "date-fns": "4.1.0", "emoji-mart": "5.6.0", + "framer-motion": "^12.34.3", "fs-extra": "11.2.0", "input-otp": "1.4.1", "keen-slider": "6.8.6", "leaflet": "^1.9.4", "leaflet-draw": "^1.0.4", + "lucide-react": "^0.575.0", "mapbox-gl": "3.9.0", "negotiator": "1.0.0", "next": "15.1.2", @@ -6323,6 +6325,33 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "12.34.3", + "resolved": "https://mirror-npm.runflare.com/framer-motion/-/framer-motion-12.34.3.tgz", + "integrity": "sha512-v81ecyZKYO/DfpTwHivqkxSUBzvceOpoI+wLfgCgoUIKxlFKEXdg0oR9imxwXumT4SFy8vRk9xzJ5l3/Du/55Q==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.34.3", + "motion-utils": "^12.29.2", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fs-extra": { "version": "11.2.0", "license": "MIT", @@ -7633,6 +7662,15 @@ "dev": true, "license": "ISC" }, + "node_modules/lucide-react": { + "version": "0.575.0", + "resolved": "https://mirror-npm.runflare.com/lucide-react/-/lucide-react-0.575.0.tgz", + "integrity": "sha512-VuXgKZrk0uiDlWjGGXmKV6MSk9Yy4l10qgVvzGn2AWBx1Ylt0iBexKOAoA6I7JO3m+M9oeovJd3yYENfkUbOeg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/mapbox-gl": { "version": "3.9.0", "license": "SEE LICENSE IN LICENSE.txt", @@ -7839,6 +7877,21 @@ "ufo": "^1.5.4" } }, + "node_modules/motion-dom": { + "version": "12.34.3", + "resolved": "https://mirror-npm.runflare.com/motion-dom/-/motion-dom-12.34.3.tgz", + "integrity": "sha512-sYgFe+pR9aIM7o4fhs2aXtOI+oqlUd33N9Yoxcgo1Fv7M20sRkHtCmzE/VRNIcq7uNJ+qio+Xubt1FXH3pQ+eQ==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.29.2" + } + }, + "node_modules/motion-utils": { + "version": "12.29.2", + "resolved": "https://mirror-npm.runflare.com/motion-utils/-/motion-utils-12.29.2.tgz", + "integrity": "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "license": "MIT" diff --git a/package.json b/package.json index f3aca02..a247d0b 100644 --- a/package.json +++ b/package.json @@ -61,11 +61,13 @@ "cmdk": "1.0.4", "date-fns": "4.1.0", "emoji-mart": "5.6.0", + "framer-motion": "^12.34.3", "fs-extra": "11.2.0", "input-otp": "1.4.1", "keen-slider": "6.8.6", "leaflet": "^1.9.4", "leaflet-draw": "^1.0.4", + "lucide-react": "^0.575.0", "mapbox-gl": "3.9.0", "negotiator": "1.0.0", "next": "15.1.2", diff --git a/src/app/(dashboard)/(private)/dashboard/farm-ai-assistant/page.tsx b/src/app/(dashboard)/(private)/dashboard/farm-ai-assistant/page.tsx new file mode 100644 index 0000000..363a67b --- /dev/null +++ b/src/app/(dashboard)/(private)/dashboard/farm-ai-assistant/page.tsx @@ -0,0 +1,8 @@ +// Components Imports +import { FarmAiAssistantChat } from '@views/dashboards/farm/farmAiAssistant' + +const FarmAiAssistantPage = () => { + return +} + +export default FarmAiAssistantPage diff --git a/src/app/(dashboard)/(private)/dashboard/fertilization-recommendation/page.tsx b/src/app/(dashboard)/(private)/dashboard/fertilization-recommendation/page.tsx new file mode 100644 index 0000000..ed9a4f4 --- /dev/null +++ b/src/app/(dashboard)/(private)/dashboard/fertilization-recommendation/page.tsx @@ -0,0 +1,8 @@ +// Components Imports +import SmartFertilizationRecommendation from '@views/dashboards/farm/smartFertilization/SmartFertilizationRecommendation' + +const FertilizationRecommendationPage = async () => { + return +} + +export default FertilizationRecommendationPage diff --git a/src/app/(dashboard)/(private)/dashboard/irrigation-recommendation/page.tsx b/src/app/(dashboard)/(private)/dashboard/irrigation-recommendation/page.tsx new file mode 100644 index 0000000..cea1ddf --- /dev/null +++ b/src/app/(dashboard)/(private)/dashboard/irrigation-recommendation/page.tsx @@ -0,0 +1,8 @@ +// Components Imports +import SmartIrrigationRecommendation from '@views/dashboards/farm/smartIrrigation/SmartIrrigationRecommendation' + +const IrrigationRecommendationPage = async () => { + return +} + +export default IrrigationRecommendationPage diff --git a/src/app/(dashboard)/(private)/dashboard/pest-detection/page.tsx b/src/app/(dashboard)/(private)/dashboard/pest-detection/page.tsx new file mode 100644 index 0000000..18fe776 --- /dev/null +++ b/src/app/(dashboard)/(private)/dashboard/pest-detection/page.tsx @@ -0,0 +1,8 @@ +// Components Imports +import PlantPestDetection from '@views/dashboards/farm/pestDetection/PlantPestDetection' + +const PestDetectionPage = async () => { + return +} + +export default PestDetectionPage diff --git a/src/app/globals.css b/src/app/globals.css index 31c6d3c..15037be 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -146,3 +146,40 @@ code { background-color: rgb(var(--mui-palette-info-mainChannel) / 0.08); border: 0; } + +/* Smart Irrigation - fade-in animation */ +@keyframes fade-in { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.animate-fade-in { + animation: fade-in 0.5s ease-out forwards; +} + +/* Hide horizontal scrollbar for growth stage stepper */ +.scrollbar-hide { + -ms-overflow-style: none; + scrollbar-width: none; +} +.scrollbar-hide::-webkit-scrollbar { + display: none; +} + +/* Farm AI Assistant - typing indicator bounce */ +@keyframes typing-bounce { + 0%, 80%, 100% { + transform: scale(0.6); + opacity: 0.5; + } + 40% { + transform: scale(1); + opacity: 1; + } +} diff --git a/src/components/layout/vertical/VerticalMenu.tsx b/src/components/layout/vertical/VerticalMenu.tsx index bc535d0..2a09bd5 100644 --- a/src/components/layout/vertical/VerticalMenu.tsx +++ b/src/components/layout/vertical/VerticalMenu.tsx @@ -108,6 +108,18 @@ const VerticalMenu = ({ scrollMenu }: Props) => { }> {t('plantSimulator')} + }> + {t('irrigationRecommendation')} + + }> + {t('fertilizationRecommendation')} + + }> + {t('farmAiAssistant')} + + }> + {t('pestDetection')} + diff --git a/src/views/dashboards/farm/farmAiAssistant/FarmAiAssistantChat.tsx b/src/views/dashboards/farm/farmAiAssistant/FarmAiAssistantChat.tsx new file mode 100644 index 0000000..40513ad --- /dev/null +++ b/src/views/dashboards/farm/farmAiAssistant/FarmAiAssistantChat.tsx @@ -0,0 +1,671 @@ +'use client' + +import { useState, useRef, useEffect } from 'react' +import { useTranslations } from 'next-intl' +import Box from '@mui/material/Box' +import Typography from '@mui/material/Typography' +import Card from '@mui/material/Card' +import IconButton from '@mui/material/IconButton' +import TextField from '@mui/material/TextField' +import Collapse from '@mui/material/Collapse' +import classnames from 'classnames' +import type { FarmContext, FarmAIMessage, AIResponseSection } from './farmAiAssistantTypes' + +// ─── Constants ───────────────────────────────────────────────────────────── + +const DEFAULT_FARM_CONTEXT: FarmContext = { + soilType: 'Loamy', + waterEC: '1.2 dS/m', + selectedCrop: 'Tomato', + growthStage: 'Flowering', + lastIrrigationStatus: '2 days ago' +} + +const SUGGESTION_CHIPS = [ + { id: 'yellow-leaves', labelKey: 'suggestions.yellowLeaves' }, + { id: 'irrigation-plan', labelKey: 'suggestions.irrigationPlan' }, + { id: 'fertilizer-flowering', labelKey: 'suggestions.fertilizerFlowering' }, + { 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() { + const t = useTranslations('farmAiAssistant') + const [messages, setMessages] = useState([]) + const [inputValue, setInputValue] = useState('') + const [isContextExpanded, setIsContextExpanded] = useState(true) + const [isTyping, setIsTyping] = useState(false) + const [selectedChip, setSelectedChip] = useState(null) + const [expandedExplanations, setExpandedExplanations] = useState>(new Set()) + const scrollRef = useRef(null) + + const farmContext = DEFAULT_FARM_CONTEXT + + // Scroll to bottom on new messages + useEffect(() => { + scrollRef.current?.scrollTo({ + top: scrollRef.current.scrollHeight, + behavior: 'smooth' + }) + }, [messages, isTyping]) + + const handleSuggestionClick = (chipId: string, labelKey: string) => { + const label = t(labelKey) + setSelectedChip(prev => (prev === chipId ? null : chipId)) + handleSend(label) + } + + const handleSend = async (text?: string) => { + const content = (text || inputValue).trim() + if (!content) return + + setInputValue('') + setSelectedChip(null) + + const userMessage: FarmAIMessage = { + id: `u-${Date.now()}`, + role: 'user', + content, + timestamp: new Date() + } + 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 + } + setMessages(prev => [...prev, aiMessage]) + setIsTyping(false) + } + + const toggleExplanation = (id: string) => { + setExpandedExplanations(prev => { + const next = new Set(prev) + if (next.has(id)) next.delete(id) + else next.add(id) + return next + }) + } + + return ( + + {/* 1) Smart Header */} + + + + + + + {t('header.title')} + + + {t('header.subtitle')} + + + + + {/* 2) Expandable Farm Context Bar */} + + setIsContextExpanded(!isContextExpanded)} + className='w-full flex items-center justify-between px-4 py-3 text-start' + sx={{ + '&:hover': { bgcolor: 'rgba(34, 197, 94, 0.04)' }, + transition: 'background 0.2s' + }} + > + + {t('context.title')} + + + + + + + + + + + + + + + {/* 3) Chat Area */} + + {messages.length === 0 && !isTyping && ( + + + + + + {t('emptyState.title')} + + + {t('emptyState.subtitle')} + + + )} + + {messages.map(msg => ( + + ))} + + {/* Typing indicator */} + {isTyping && ( + + + + + + + + + )} + + + {/* 4) Suggestion Chips */} + + + {SUGGESTION_CHIPS.map(chip => ( + handleSuggestionClick(chip.id, chip.labelKey)} + className={classnames( + 'px-4 py-2 rounded-2xl text-sm font-medium whitespace-nowrap transition-all duration-200 shrink-0', + 'hover:scale-[1.02] active:scale-[0.98]' + )} + sx={{ + background: + selectedChip === chip.id + ? 'linear-gradient(135deg, #22c55e 0%, #16a34a 100%)' + : 'linear-gradient(145deg, #ffffff 0%, rgba(34, 197, 94, 0.08) 100%)', + color: selectedChip === chip.id ? '#ffffff' : 'text.primary', + border: selectedChip === chip.id ? 'none' : '1px solid rgba(34, 197, 94, 0.2)', + boxShadow: + selectedChip === chip.id + ? '0 4px 12px rgba(34, 197, 94, 0.35)' + : '0 2px 8px rgba(0,0,0,0.04)' + }} + > + {t(chip.labelKey)} + + ))} + + + + {/* 5) Input Area - Sticky Bottom */} + + + + + + + + + + setInputValue(e.target.value)} + onKeyDown={e => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault() + handleSend() + } + }} + variant='standard' + InputProps={{ + disableUnderline: true, + sx: { + px: 1.5, + py: 1, + fontSize: '0.95rem' + } + }} + sx={{ flex: 1, minWidth: 0 }} + /> + handleSend()} + disabled={!inputValue.trim()} + className='shrink-0' + sx={{ + background: inputValue.trim() + ? 'linear-gradient(135deg, #22c55e 0%, #16a34a 100%)' + : 'action.disabledBackground', + color: inputValue.trim() ? '#ffffff' : 'action.disabled', + '&:hover': inputValue.trim() + ? { + background: 'linear-gradient(135deg, #16a34a 0%, #15803d 100%)', + boxShadow: '0 4px 12px rgba(34, 197, 94, 0.4)' + } + : {}, + '&.Mui-disabled': { background: 'action.disabledBackground', color: 'action.disabled' } + }} + > + + + + + + ) +} + +// ─── Sub-components ──────────────────────────────────────────────────────── + +function ContextBadge({ + icon, + label, + value, + colSpan = 1 +}: { + icon: string + label: string + value: string + colSpan?: number +}) { + return ( + + + + + {label} + + + {value} + + + + ) +} + +function MessageBubble({ + message, + expandedExplanations, + onToggleExplanation, + t +}: { + message: FarmAIMessage + expandedExplanations: Set + onToggleExplanation: (id: string) => void + t: (key: string) => string +}) { + const isUser = message.role === 'user' + + if (isUser) { + return ( + + + + {message.content} + + + + ) + } + + // AI message - structured cards + return ( + + + + + + {message.sections?.map((section, idx) => ( + + ))} + + + ) +} + +function AISectionCard({ + section, + expandedExplanations, + onToggleExplanation, + messageId, + idx, + t +}: { + section: AIResponseSection + expandedExplanations: Set + onToggleExplanation: (id: string) => void + messageId: string + idx: number + t: (key: string) => string +}) { + const expId = `${messageId}-exp-${idx}` + + const iconMap = { + droplet: 'tabler-droplet', + leaf: 'tabler-leaf', + warning: 'tabler-alert-triangle', + fertilizer: 'tabler-atom-2', + calendar: 'tabler-calendar' + } + const iconClass = section.icon ? iconMap[section.icon] : 'tabler-leaf' + + if (section.type === 'recommendation') { + return ( + + + + + + {section.title} + + + + {section.frequency && ( + + + {t('recommendation.frequency')} + + + {section.frequency} + + + )} + {section.amount && ( + + + {t('recommendation.amount')} + + + {section.amount} + + + )} + {section.timing && ( + + + {t('recommendation.timing')} + + + {section.timing} + + + )} + + {section.expandableExplanation && ( + + onToggleExplanation(expId)} + className='flex items-center gap-1 text-sm font-medium' + sx={{ color: '#22c55e', '&:hover': { color: '#16a34a' } }} + > + {t('recommendation.whyThis')} + + + + + {section.expandableExplanation} + + + + )} + + + ) + } + + if (section.type === 'warning') { + return ( + + + + + {section.title && ( + + {section.title} + + )} + + {section.content} + + + + + ) + } + + if (section.type === 'list') { + return ( + + {section.title && ( + + + + {section.title} + + + )} + {section.items && ( + + {section.items.map((item, i) => ( + + {item} + + ))} + + )} + + ) + } + + return null +} + +function TypingIndicator() { + return ( + + {[0, 1, 2].map(i => ( + + ))} + + ) +} diff --git a/src/views/dashboards/farm/farmAiAssistant/farmAiAssistantTypes.ts b/src/views/dashboards/farm/farmAiAssistant/farmAiAssistantTypes.ts new file mode 100644 index 0000000..cab231a --- /dev/null +++ b/src/views/dashboards/farm/farmAiAssistant/farmAiAssistantTypes.ts @@ -0,0 +1,36 @@ +export interface FarmContext { + soilType: string + waterEC: string + selectedCrop: string + growthStage: string + lastIrrigationStatus: string +} + +export interface SuggestionChip { + id: string + label: string +} + +// Structured AI response sections for card-based rendering +export interface AIResponseSection { + type: 'text' | 'list' | 'recommendation' | 'warning' + title?: string + content?: string + items?: string[] + icon?: 'droplet' | 'leaf' | 'warning' | 'fertilizer' | 'calendar' + // Recommendation-specific + frequency?: string + amount?: string + timing?: string + expandableExplanation?: string +} + +export interface FarmAIMessage { + id: string + role: 'user' | 'assistant' + content: string + timestamp: Date + images?: string[] + // For structured AI responses + sections?: AIResponseSection[] +} diff --git a/src/views/dashboards/farm/farmAiAssistant/index.ts b/src/views/dashboards/farm/farmAiAssistant/index.ts new file mode 100644 index 0000000..2e2b004 --- /dev/null +++ b/src/views/dashboards/farm/farmAiAssistant/index.ts @@ -0,0 +1,2 @@ +export { default as FarmAiAssistantChat } from './FarmAiAssistantChat' +export * from './farmAiAssistantTypes' diff --git a/src/views/dashboards/farm/pestDetection/PlantPestDetection.tsx b/src/views/dashboards/farm/pestDetection/PlantPestDetection.tsx new file mode 100644 index 0000000..f82db01 --- /dev/null +++ b/src/views/dashboards/farm/pestDetection/PlantPestDetection.tsx @@ -0,0 +1,193 @@ +'use client' + +import { useState, useCallback } from 'react' +import { useTranslations } from 'next-intl' +import Box from '@mui/material/Box' +import Card from '@mui/material/Card' +import CardContent from '@mui/material/CardContent' +import Typography from '@mui/material/Typography' +import Button from '@mui/material/Button' +import CircularProgress from '@mui/material/CircularProgress' +import Collapse from '@mui/material/Collapse' +import UploadBox from './components/UploadBox' +import ResultCard from './components/ResultCard' +import type { UploadedFile } from './components/UploadBox' +import type { PestResult } from './components/ResultCard' + +export default function PlantPestDetection() { + const t = useTranslations('pestDetection') + const [file, setFile] = useState(null) + const [result, setResult] = useState(null) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + const handleFileSelect = useCallback((newFile: UploadedFile | null) => { + setFile(newFile) + setResult(null) + setError(null) + }, []) + + const handleAnalyze = useCallback(() => { + if (!file) return + + setLoading(true) + setError(null) + setResult(null) + + const delay = 1500 + Math.random() * 1000 + setTimeout(() => { + setResult({ + pest: t('mockResult.pest'), + confidence: 92, + description: t('mockResult.description'), + treatment: t('mockResult.treatment'), + }) + setLoading(false) + }, delay) + }, [file, t]) + + const handleReset = useCallback(() => { + if (file) { + URL.revokeObjectURL(file.preview) + } + setFile(null) + setResult(null) + setError(null) + setLoading(false) + }, [file]) + + return ( + + + {/* Header */} + + + {t('title')} + + + {t('subtitle')} + + + + {/* Upload card */} + + + + + {/* Action buttons */} + + + + {file && ( + + )} + + + + + {/* Loading state */} + + + + + + {t('analyzing')} + + + + + + {/* Result card */} + + {result && !loading && ( + + + {t('resultTitle')} + + + + )} + + + + ) +} diff --git a/src/views/dashboards/farm/pestDetection/components/ResultCard.tsx b/src/views/dashboards/farm/pestDetection/components/ResultCard.tsx new file mode 100644 index 0000000..9c06e41 --- /dev/null +++ b/src/views/dashboards/farm/pestDetection/components/ResultCard.tsx @@ -0,0 +1,118 @@ +'use client' + +import { useTranslations } from 'next-intl' +import Box from '@mui/material/Box' +import Card from '@mui/material/Card' +import CardContent from '@mui/material/CardContent' +import Typography from '@mui/material/Typography' +import LinearProgress from '@mui/material/LinearProgress' +import CustomAvatar from '@core/components/mui/Avatar' + +export interface PestResult { + pest: string + confidence: number + description: string + treatment: string +} + +interface ResultCardProps { + result: PestResult +} + +function getConfidenceColor(confidence: number): 'success' | 'warning' | 'error' { + if (confidence > 80) return 'success' + if (confidence >= 50) return 'warning' + return 'error' +} + +export default function ResultCard({ result }: ResultCardProps) { + const t = useTranslations('pestDetection.resultCard') + const color = getConfidenceColor(result.confidence) + + return ( + + `linear-gradient(160deg, ${theme.palette.background.paper} 0%, ${theme.palette[color].lighterOpacity} 100%)`, + boxShadow: '0 8px 32px rgba(0,0,0,0.08), 0 2px 8px rgba(0,0,0,0.04)', + }} + > + + {/* Pest name & icon */} + + + + + + + {result.pest} + + + {t('detectedPest')} + + + + + {/* Confidence bar */} + + + + {t('confidence')} + + + {result.confidence}% + + + + + + {/* Description */} + + + + + {t('description')} + + + + {result.description} + + + + {/* Treatment */} + + + + + {t('recommendedTreatment')} + + + + {result.treatment} + + + + + ) +} diff --git a/src/views/dashboards/farm/pestDetection/components/UploadBox.tsx b/src/views/dashboards/farm/pestDetection/components/UploadBox.tsx new file mode 100644 index 0000000..abf0ef2 --- /dev/null +++ b/src/views/dashboards/farm/pestDetection/components/UploadBox.tsx @@ -0,0 +1,176 @@ +'use client' + +import { useDropzone } from 'react-dropzone' +import { useTranslations } from 'next-intl' +import Box from '@mui/material/Box' +import Typography from '@mui/material/Typography' +import Button from '@mui/material/Button' +import { styled } from '@mui/material/styles' +import type { BoxProps } from '@mui/material/Box' +import CustomAvatar from '@core/components/mui/Avatar' +import AppReactDropzone from '@/libs/styles/AppReactDropzone' + +export interface UploadedFile { + file: File + preview: string +} + +interface UploadBoxProps { + file: UploadedFile | null + onFileSelect: (file: UploadedFile | null) => void + onError?: (message: string | null) => void + error?: string +} + +const DropzoneWrapper = styled(AppReactDropzone)(({ theme }) => ({ + '& .dropzone': { + minHeight: 200, + padding: theme.spacing(4, 3), + borderRadius: '16px', + border: '2px dashed', + borderColor: 'var(--mui-palette-divider)', + transition: 'all 0.3s ease', + '&:hover': { + borderColor: 'var(--mui-palette-primary-main)', + backgroundColor: 'var(--mui-palette-primary-lighterOpacity)', + }, + }, + '& .dropzone.active': { + borderColor: 'var(--mui-palette-primary-main)', + backgroundColor: 'var(--mui-palette-primary-lighterOpacity)', + }, + '& .dropzone.hasFile': { + borderColor: 'var(--mui-palette-primary-main)', + minHeight: 'unset', + padding: theme.spacing(3), + }, + '& .dropzone.error': { + borderColor: 'var(--mui-palette-error-main)', + backgroundColor: 'var(--mui-palette-error-lighterOpacity)', + }, +})) + +export default function UploadBox({ file, onFileSelect, onError, error }: UploadBoxProps) { + const t = useTranslations('pestDetection.upload') + const { getRootProps, getInputProps, isDragActive } = useDropzone({ + onDrop: (acceptedFiles) => { + onError?.(null) + const validFile = acceptedFiles[0] + if (validFile) { + onFileSelect({ + file: validFile, + preview: URL.createObjectURL(validFile), + }) + } else { + onFileSelect(null) + } + }, + onDropRejected: () => { + onError?.(t('invalidFile')) + onFileSelect(null) + }, + accept: { + 'image/*': ['.jpeg', '.jpg', '.png', '.gif', '.webp'], + }, + maxFiles: 1, + maxSize: 10 * 1024 * 1024, // 10MB + }) + + const handleRemove = (e: React.MouseEvent) => { + e.stopPropagation() + if (file) { + URL.revokeObjectURL(file.preview) + onFileSelect(null) + } + } + + const dropzoneClass = [ + 'dropzone', + isDragActive && 'active', + file && 'hasFile', + error && 'error', + ] + .filter(Boolean) + .join(' ') + + return ( + +
+ + {file ? ( + + + + + + + {file.file.name} + + + {(file.file.size / 1024).toFixed(1)} KB + + + + + ) : ( + + + + + + + {isDragActive ? t('dropHere') : t('dragDrop')} + + + {t('fileFormats')} + + + + + )} +
+ {error && ( + + {error} + + )} +
+ ) +} diff --git a/src/views/dashboards/farm/smartFertilization/SmartFertilizationRecommendation.tsx b/src/views/dashboards/farm/smartFertilization/SmartFertilizationRecommendation.tsx new file mode 100644 index 0000000..053a4d8 --- /dev/null +++ b/src/views/dashboards/farm/smartFertilization/SmartFertilizationRecommendation.tsx @@ -0,0 +1,542 @@ +'use client' + +import { useState } from 'react' +import { useTranslations } from 'next-intl' +import Box from '@mui/material/Box' +import Card from '@mui/material/Card' +import CardContent from '@mui/material/CardContent' +import Typography from '@mui/material/Typography' +import Button from '@mui/material/Button' +import Collapse from '@mui/material/Collapse' + +// 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[] = [ + { 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' } +] + +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 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 [farmData] = useState(DEFAULT_FARM_DATA) + const [growthStage, setGrowthStage] = useState(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 = () => { + if (!selectedCrop) return + setLoading(true) + setPlan(null) + setReasoningExpanded(false) + setTimeout(() => { + setPlan( + generateFertilizationPlan(selectedCrop, growthStage, farmData) + ) + setLoading(false) + }, 1400) + } + + const stageIndex = GROWTH_STAGES.findIndex(s => s.id === growthStage) + + return ( + + + {/* 1) Header */} + + + {t('title')} + + + {t('subtitle')} + + + + {/* 2) Farm Data Card */} + + + + + {t('farmData.title')} + + + + {t('verifiedBadge')} + + + + + + + + + + + {/* 3) Growth Stage Selector */} + + {t('growthStage.title')} + + + {GROWTH_STAGES.map((stage, idx) => { + const isSelected = growthStage === stage.id + const isPast = idx < stageIndex + return ( + setGrowthStage(stage.id)} + className='flex flex-col items-center gap-1.5 px-4 py-3 rounded-2xl shrink-0 transition-all duration-300 ease-out border-2 cursor-pointer min-w-[72px]' + sx={{ + borderColor: isSelected ? '#22c55e' : 'transparent', + background: isSelected + ? 'linear-gradient(145deg, rgba(34, 197, 94, 0.15) 0%, rgba(124, 58, 237, 0.06) 100%)' + : 'linear-gradient(145deg, #ffffff 0%, #faf5ff 100%)', + boxShadow: isSelected + ? '0 4px 20px rgba(34, 197, 94, 0.2), inset 0 1px 0 rgba(255,255,255,0.8)' + : '0 2px 8px rgba(0,0,0,0.04)', + '&:hover': { + transform: 'translateY(-2px)', + boxShadow: isSelected + ? '0 6px 24px rgba(34, 197, 94, 0.25)' + : '0 4px 16px rgba(124, 58, 237, 0.1)' + } + }} + > + + + + + {t(`growthStage.${stage.id}`)} + + + ) + })} + + + {/* 4) Plant Selection */} + + {t('plantSelection.title')} + + + {CROP_OPTIONS.map(crop => ( + + setSelectedCrop(prev => (prev === crop.id ? null : crop.id)) + } + /> + ))} + + + {/* 6) Result Section - Prescription style */} + {plan && ( + + + + + + + {t('result.title')} + + + + + + + + + + + {/* Expandable "Why this recommendation?" */} + + setReasoningExpanded(!reasoningExpanded)} + className='w-full flex items-center justify-between px-4 py-3 text-start cursor-pointer' + sx={{ '&:hover': { bgcolor: 'rgba(34, 197, 94, 0.06)' } }} + > + + + + {t('result.whyRecommendation')} + + + + + + + + {plan.reasoning} + + + + + + + + )} + + {/* Loading state */} + {loading && ( + + + + + + + {t('generating')} + + + + )} + + + {/* 5) Primary CTA Button - Sticky */} + + + + + ) +} + +// ─── Sub-components ────────────────────────────────────────────────────────── + +function FarmBadge({ + icon, + label, + value +}: { + icon: string + label: string + value: string +}) { + return ( + + + + + {label} + + + {value} + + + + ) +} + +function CropCard({ + crop, + label, + selected, + onClick +}: { + crop: CropOption + label: string + selected: boolean + onClick: () => void +}) { + return ( + + + + + + {label} + + {selected && ( + + )} + + ) +} + +function PrescriptionRow({ + icon, + label, + value +}: { + icon: string + label: string + value: string +}) { + return ( + + + + + {label} + + + {value} + + + + ) +} diff --git a/src/views/dashboards/farm/smartIrrigation/SmartIrrigationRecommendation.tsx b/src/views/dashboards/farm/smartIrrigation/SmartIrrigationRecommendation.tsx new file mode 100644 index 0000000..826eed7 --- /dev/null +++ b/src/views/dashboards/farm/smartIrrigation/SmartIrrigationRecommendation.tsx @@ -0,0 +1,436 @@ +'use client' + +import { useState } from 'react' +import { useTranslations } from 'next-intl' +import Box from '@mui/material/Box' +import Card from '@mui/material/Card' +import CardContent from '@mui/material/CardContent' +import Typography from '@mui/material/Typography' +import Button from '@mui/material/Button' +import IconButton from '@mui/material/IconButton' +import CircularProgress from '@mui/material/CircularProgress' + +// 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 = { + soilType: 'Loamy', + waterQuality: 'Medium EC', + 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() { + const t = useTranslations('irrigation') + const [farmInfo] = useState(DEFAULT_FARM_INFO) + const [selectedCrop, setSelectedCrop] = useState(null) + const [plan, setPlan] = useState(null) + const [loading, setLoading] = useState(false) + + const handleGenerate = () => { + if (!selectedCrop) return + setLoading(true) + setPlan(null) + // Simulate API delay + setTimeout(() => { + setPlan(generateIrrigationPlan(selectedCrop, farmInfo)) + setLoading(false) + }, 1200) + } + + return ( + + + {/* 1) Dynamic Header */} + + + {t('title')} + + + {t('subtitle')} + + + + {/* 2) Farm Info Card */} + + + + + {t('farmInfo.title')} + + + + + {t('verifiedBadge')} + + + + + + + + + + + + + + + {/* 3) Plant Selection Section */} + + {t('plantSelection.title')} + + + {CROP_OPTIONS.map(crop => ( + setSelectedCrop(prev => (prev === crop.id ? null : crop.id))} + /> + ))} + + + {/* 5) Result Card (after click) */} + {plan && ( + + + + {/* Circular moisture indicator */} + + + + + + + + + + + + + + + + {plan.moistureLevel}% + + + {t('result.moistureLevel')} + + + + + + + + + + + + {plan.warning && ( + + + + + + {t('result.smartWarning')} + + + {plan.warning} + + + + + )} + + + + )} + + {/* Loading state */} + {loading && ( + + + + + {t('generating')} + + + + )} + + + {/* 4) Primary CTA Button - Sticky */} + + + + + ) +} + +// ─── Sub-components ────────────────────────────────────────────────────────── + +function FarmBadge({ + icon, + label, + value +}: { + icon: string + label: string + value: string +}) { + return ( + + + + + {label} + + + {value} + + + + ) +} + +function CropCard({ + crop, + label, + selected, + onClick +}: { + crop: CropOption + label: string + selected: boolean + onClick: () => void +}) { + return ( + + + + + + {label} + + {selected && ( + + )} + + ) +} + +function ResultRow({ + icon, + label, + value +}: { + icon: string + label: string + value: string +}) { + return ( + + + + + {label} + + + {value} + + + + ) +}