'use client' /* eslint-disable @next/next/no-img-element */ import { useState } from 'react' import { ArrowLeftRight, Loader2, TrendingDown, TrendingUp, Minus } from 'lucide-react' import ImageUpload from '@/components/ImageUpload' import { compareHealth, trendLabel, trendDetail, type HealthTrend, type HealthComparison, } from '@/lib/healthComparison' import type { PredictionPayload } from '@/lib/stateDiseaseMap' type Props = { crop: string applyRegionalFilter: (raw: PredictionPayload) => PredictionPayload } interface PredictResult { payload: PredictionPayload crop_mismatch: boolean suggested_crop: string | null } async function runPredict(file: File, crop: string): Promise { const formData = new FormData() formData.append('image', file) formData.append('crop', crop) const response = await fetch('/api/predict', { method: 'POST', body: formData }) if (!response.ok) { const err = await response.json().catch(() => ({})) throw new Error(err.error || 'Prediction failed') } const data = await response.json() return { payload: { disease: data.disease, confidence: data.confidence, is_healthy: data.is_healthy, meets_threshold: data.meets_threshold, all_predictions: data.all_predictions, }, crop_mismatch: !!data.crop_mismatch, suggested_crop: data.suggested_crop ?? null, } } const cap = (s: string) => (s ? s.charAt(0).toUpperCase() + s.slice(1) : s) function trendStyles(t: HealthTrend) { switch (t) { case 'improving': return { border: 'border-primary-300', bg: 'bg-primary-50', text: 'text-primary-900', Icon: TrendingUp, } case 'worsening': return { border: 'border-rose-300', bg: 'bg-rose-50', text: 'text-rose-900', Icon: TrendingDown, } default: return { border: 'border-slate-300', bg: 'bg-slate-50', text: 'text-slate-800', Icon: Minus, } } } export default function HealthComparisonPanel({ crop, applyRegionalFilter }: Props) { const [pastFile, setPastFile] = useState(null) const [currentFile, setCurrentFile] = useState(null) const [pastUrl, setPastUrl] = useState(null) const [currentUrl, setCurrentUrl] = useState(null) const [pastPred, setPastPred] = useState(null) const [currentPred, setCurrentPred] = useState(null) // Scored from the RAW (unfiltered) distributions so the health estimate stays // region-independent; the cards still display the region-consistent label. const [comparison, setComparison] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const onPastSelect = (file: File | null) => { setPastFile(file) setPastUrl((prev) => { if (prev) URL.revokeObjectURL(prev) return file ? URL.createObjectURL(file) : null }) setPastPred(null) setComparison(null) } const onCurrentSelect = (file: File | null) => { setCurrentFile(file) setCurrentUrl((prev) => { if (prev) URL.revokeObjectURL(prev) return file ? URL.createObjectURL(file) : null }) setCurrentPred(null) setComparison(null) } const clearPast = () => onPastSelect(null) const clearCurrent = () => onCurrentSelect(null) const handleCompare = async () => { if (!pastFile || !currentFile) { setError('Please upload both a past and a current photo.') return } setLoading(true) setError(null) setPastPred(null) setCurrentPred(null) setComparison(null) try { const [past, current] = await Promise.all([ runPredict(pastFile, crop), runPredict(currentFile, crop), ]) // If a photo isn't this crop, comparing diagnoses is meaningless. const wrong = past.crop_mismatch ? past : current.crop_mismatch ? current : null if (wrong) { const which = past.crop_mismatch ? 'earlier' : "today's" setError( wrong.suggested_crop ? `The ${which} photo looks more like a ${cap(wrong.suggested_crop)} leaf, not ${cap(crop)}. Use two ${cap(crop)} photos to compare.` : `The ${which} photo doesn't look like a ${cap(crop)} leaf. Use two clear ${cap(crop)} photos to compare.`, ) return } // Score from the raw distributions (full softmax, incl. Healthy); display // the regionally-filtered label. setComparison(compareHealth(past.payload, current.payload, crop)) setPastPred(applyRegionalFilter(past.payload)) setCurrentPred(applyRegionalFilter(current.payload)) } catch (e: unknown) { setError(e instanceof Error ? e.message : 'Comparison failed') } finally { setLoading(false) } } const trend = comparison?.trend ?? null const ts = trend ? trendStyles(trend) : null return (

Compare field change

Add an older photo and a current photo from the same crop to see whether the issue appears to be improving or spreading.

Earlier photo

Today's photo

{error && (
{error}
)} {pastPred && currentPred && comparison && trend && ts && (

Comparison

{trendLabel(trend)} {comparison.delta !== 0 && ( {comparison.delta > 0 ? '+' : ''} {comparison.delta} pts )}

{trendDetail(comparison)}

{/* Health score read-out: earlier → today */}
{comparison.pastScore} {comparison.currentScore} /100 health
)}
) } function ComparisonCard({ label, imageUrl, prediction, score, accent, }: { label: string imageUrl: string | null prediction: PredictionPayload score: number accent: string }) { return (
{label} Health {score}/100
{imageUrl && (
)}

Likely issue

{prediction.disease}

Match {typeof prediction.confidence === 'number' ? prediction.confidence.toFixed(1) : '—'}%

) }