194 lines
6.4 KiB
TypeScript
194 lines
6.4 KiB
TypeScript
|
|
'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<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
|
||
|
|
className="min-bs-screen pb-24"
|
||
|
|
sx={{
|
||
|
|
minHeight: '100vh',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<Box className="max-w-2xl mx-auto px-4 py-6 sm:py-8">
|
||
|
|
{/* Header */}
|
||
|
|
<Box className="mb-8">
|
||
|
|
<Typography
|
||
|
|
variant="h4"
|
||
|
|
className="font-bold tracking-tight"
|
||
|
|
sx={{
|
||
|
|
background: 'linear-gradient(135deg, #22c55e 0%, #16a34a 50%, #15803d 100%)',
|
||
|
|
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',
|
||
|
|
background: 'linear-gradient(145deg, #ffffff 0%, #f8fcf8 100%)',
|
||
|
|
boxShadow: '0 4px 24px rgba(34, 197, 94, 0.08), 0 1px 3px rgba(0,0,0,0.04)',
|
||
|
|
border: '1px solid rgba(34, 197, 94, 0.12)',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<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 ? (
|
||
|
|
<CircularProgress size={20} color="inherit" sx={{ color: 'white' }} />
|
||
|
|
) : (
|
||
|
|
<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={{
|
||
|
|
background: 'linear-gradient(135deg, #22c55e 0%, #16a34a 50%, #15803d 100%)',
|
||
|
|
boxShadow: '0 4px 20px rgba(34, 197, 94, 0.35)',
|
||
|
|
'&:hover': {
|
||
|
|
background: 'linear-gradient(135deg, #4ade80 0%, #22c55e 50%, #16a34a 100%)',
|
||
|
|
boxShadow: '0 6px 24px rgba(34, 197, 94, 0.45)',
|
||
|
|
},
|
||
|
|
'&: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',
|
||
|
|
background: 'linear-gradient(160deg, #ffffff 0%, #f0fdf4 100%)',
|
||
|
|
boxShadow: '0 8px 32px rgba(34, 197, 94, 0.1)',
|
||
|
|
border: '1px solid rgba(34, 197, 94, 0.12)',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<CardContent className="p-12 flex flex-col items-center gap-4">
|
||
|
|
<CircularProgress size={48} sx={{ color: '#22c55e' }} />
|
||
|
|
<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>
|
||
|
|
)
|
||
|
|
}
|