2026-02-21 00:19:25 +03:30
|
|
|
'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'
|
2026-02-21 22:21:31 +03:30
|
|
|
import { useTheme } from '@mui/material/styles'
|
|
|
|
|
import { alpha } from '@mui/material/styles'
|
|
|
|
|
import classnames from 'classnames'
|
|
|
|
|
|
|
|
|
|
// Util Imports
|
|
|
|
|
import { commonLayoutClasses } from '@layouts/utils/layoutClasses'
|
|
|
|
|
|
2026-02-21 00:19:25 +03:30
|
|
|
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')
|
2026-02-21 22:21:31 +03:30
|
|
|
const theme = useTheme()
|
|
|
|
|
const primary = theme.palette.primary
|
2026-02-21 00:19:25 +03:30
|
|
|
const [file, setFile] = useState<UploadedFile | null>(null)
|
|
|
|
|
const [result, setResult] = useState<PestResult | null>(null)
|
|
|
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
|
const [error, setError] = useState<string | null>(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 (
|
|
|
|
|
<Box
|
2026-02-21 22:21:31 +03:30
|
|
|
className={classnames(commonLayoutClasses.contentHeightFixed, 'flex flex-col is-full overflow-hidden rounded')}
|
|
|
|
|
sx={{ minHeight: '100%' }}
|
2026-02-21 00:19:25 +03:30
|
|
|
>
|
2026-02-21 22:21:31 +03:30
|
|
|
<Box className="is-full py-6 sm:py-8 flex flex-col">
|
2026-02-21 00:19:25 +03:30
|
|
|
{/* Header */}
|
|
|
|
|
<Box className="mb-8">
|
|
|
|
|
<Typography
|
|
|
|
|
variant="h4"
|
|
|
|
|
className="font-bold tracking-tight"
|
|
|
|
|
sx={{
|
2026-02-21 22:21:31 +03:30
|
|
|
background: `linear-gradient(135deg, ${primary.main} 0%, ${primary.dark} 50%, ${primary.dark} 100%)`,
|
2026-02-21 00:19:25 +03:30
|
|
|
backgroundClip: 'text',
|
|
|
|
|
WebkitBackgroundClip: 'text',
|
|
|
|
|
color: 'transparent',
|
|
|
|
|
fontSize: { xs: '1.5rem', sm: '1.75rem' },
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{t('title')}
|
|
|
|
|
</Typography>
|
|
|
|
|
<Typography variant="body2" color="text.secondary" className="mt-1">
|
|
|
|
|
{t('subtitle')}
|
|
|
|
|
</Typography>
|
|
|
|
|
</Box>
|
|
|
|
|
|
|
|
|
|
{/* Upload card */}
|
|
|
|
|
<Card
|
|
|
|
|
elevation={0}
|
|
|
|
|
className="mb-6 overflow-hidden transition-all duration-300 hover:shadow-lg"
|
|
|
|
|
sx={{
|
|
|
|
|
borderRadius: '24px',
|
2026-02-21 22:21:31 +03:30
|
|
|
background: `linear-gradient(145deg, ${theme.palette.background.paper} 0%, ${alpha(primary.main, 0.04)} 100%)`,
|
|
|
|
|
boxShadow: `0 4px 24px ${alpha(primary.main, 0.08)}, ${theme.shadows[1]}`,
|
|
|
|
|
border: `1px solid ${alpha(primary.main, 0.12)}`,
|
2026-02-21 00:19:25 +03:30
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<CardContent className="p-5 sm:p-6">
|
|
|
|
|
<UploadBox
|
|
|
|
|
file={file}
|
|
|
|
|
onFileSelect={handleFileSelect}
|
|
|
|
|
onError={setError}
|
|
|
|
|
error={error ?? undefined}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* Action buttons */}
|
|
|
|
|
<Box className="mt-6 flex flex-col gap-3 sm:flex-row sm:justify-center">
|
|
|
|
|
<Button
|
|
|
|
|
variant="contained"
|
|
|
|
|
disabled={!file || loading}
|
|
|
|
|
onClick={handleAnalyze}
|
|
|
|
|
startIcon={
|
|
|
|
|
loading ? (
|
2026-02-21 22:21:31 +03:30
|
|
|
<CircularProgress size={20} color="inherit" />
|
2026-02-21 00:19:25 +03:30
|
|
|
) : (
|
|
|
|
|
<i className="tabler-scan text-xl" />
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
className="rounded-xl py-3 px-8 font-semibold shadow-md transition-all duration-300 hover:shadow-lg hover:scale-[1.02] active:scale-[0.98]"
|
|
|
|
|
sx={{
|
2026-02-21 22:21:31 +03:30
|
|
|
background: `linear-gradient(135deg, ${primary.main} 0%, ${primary.dark} 50%, ${primary.dark} 100%)`,
|
|
|
|
|
boxShadow: `0 4px 20px ${alpha(primary.main, 0.35)}`,
|
2026-02-21 00:19:25 +03:30
|
|
|
'&:hover': {
|
2026-02-21 22:21:31 +03:30
|
|
|
background: `linear-gradient(135deg, ${primary.light} 0%, ${primary.main} 50%, ${primary.dark} 100%)`,
|
|
|
|
|
boxShadow: `0 6px 24px ${alpha(primary.main, 0.45)}`,
|
2026-02-21 00:19:25 +03:30
|
|
|
},
|
|
|
|
|
'&:disabled': {
|
|
|
|
|
background: 'action.disabledBackground',
|
|
|
|
|
color: 'action.disabled',
|
|
|
|
|
},
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{loading ? t('analyzing') : t('analyze')}
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
{file && (
|
|
|
|
|
<Button
|
|
|
|
|
variant="outlined"
|
|
|
|
|
onClick={handleReset}
|
|
|
|
|
disabled={loading}
|
|
|
|
|
startIcon={<i className="tabler-rotate-2 text-lg" />}
|
|
|
|
|
className="rounded-xl py-3 px-8 font-semibold"
|
|
|
|
|
sx={{
|
|
|
|
|
borderColor: 'divider',
|
|
|
|
|
'&:hover': {
|
|
|
|
|
borderColor: 'primary.main',
|
|
|
|
|
backgroundColor: 'action.hover',
|
|
|
|
|
},
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{t('reset')}
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</Box>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* Loading state */}
|
|
|
|
|
<Collapse in={loading}>
|
|
|
|
|
<Card
|
|
|
|
|
elevation={0}
|
|
|
|
|
className="mb-6"
|
|
|
|
|
sx={{
|
|
|
|
|
borderRadius: '24px',
|
2026-02-21 22:21:31 +03:30
|
|
|
background: `linear-gradient(160deg, ${theme.palette.background.paper} 0%, ${alpha(primary.main, 0.06)} 100%)`,
|
|
|
|
|
boxShadow: `0 8px 32px ${alpha(primary.main, 0.1)}`,
|
|
|
|
|
border: `1px solid ${alpha(primary.main, 0.12)}`,
|
2026-02-21 00:19:25 +03:30
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<CardContent className="p-12 flex flex-col items-center gap-4">
|
2026-02-21 22:21:31 +03:30
|
|
|
<CircularProgress size={48} sx={{ color: 'primary.main' }} />
|
2026-02-21 00:19:25 +03:30
|
|
|
<Typography variant="body2" color="text.secondary">
|
|
|
|
|
{t('analyzing')}
|
|
|
|
|
</Typography>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</Collapse>
|
|
|
|
|
|
|
|
|
|
{/* Result card */}
|
|
|
|
|
<Collapse in={!!result && !loading}>
|
|
|
|
|
{result && !loading && (
|
|
|
|
|
<Box className="mb-6">
|
|
|
|
|
<Typography variant="subtitle2" fontWeight={600} color="text.secondary" className="mbe-3">
|
|
|
|
|
{t('resultTitle')}
|
|
|
|
|
</Typography>
|
|
|
|
|
<ResultCard result={result} />
|
|
|
|
|
</Box>
|
|
|
|
|
)}
|
|
|
|
|
</Collapse>
|
|
|
|
|
</Box>
|
|
|
|
|
</Box>
|
|
|
|
|
)
|
|
|
|
|
}
|