File size: 19,581 Bytes
52d6ced
 
8f0f112
 
52d6ced
8f0f112
 
 
 
 
52d6ced
 
 
 
8f0f112
52d6ced
 
8f0f112
 
 
 
 
 
 
 
 
 
 
 
 
 
52d6ced
 
8f0f112
 
52d6ced
 
 
8f0f112
 
 
 
 
 
 
 
 
 
 
 
52d6ced
 
 
 
 
8f0f112
 
 
52d6ced
 
 
7edf946
 
52d6ced
 
 
 
 
8f0f112
52d6ced
 
 
 
8f0f112
 
 
 
 
 
 
 
 
 
 
 
 
52d6ced
 
 
 
 
 
 
 
 
 
8f0f112
 
 
52d6ced
 
 
8f0f112
 
 
52d6ced
 
 
 
8f0f112
 
52d6ced
 
 
 
 
 
8f0f112
52d6ced
 
 
 
 
 
 
8f0f112
52d6ced
8f0f112
 
52d6ced
8f0f112
 
 
52d6ced
 
 
 
 
 
8f0f112
52d6ced
 
 
 
 
 
 
8f0f112
52d6ced
 
 
 
 
 
 
 
 
 
 
 
8f0f112
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52d6ced
8f0f112
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52d6ced
8f0f112
52d6ced
8f0f112
 
 
 
 
 
 
 
52d6ced
 
 
 
8f0f112
52d6ced
8f0f112
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52d6ced
8f0f112
52d6ced
 
8f0f112
 
 
 
 
 
52d6ced
 
8f0f112
 
52d6ced
 
 
8f0f112
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
import { useState, useRef } from "react";

export interface PredictionResult {
    filename?: string;
    success: boolean;
    error?: string;
    prediction?: string;
    confidence?: number;
    probabilities?: { name: string; probability: number }[];
    details?: {
        gram_stain: string;
        shape: string;
        pathogenicity: string;
    };
    previewUrl?: string;
}

const PRECAUTIONS: Record<string, string> = {
    "Escherichia coli": "Indicator of fecal contamination. \n\nPrecautions/Actions: Boil water immediately before consumption. Source trace to find sewage leaks. Do not use for washing open wounds.",
    "Pseudomonas aeruginosa": "Opportunistic pathogen resistant to many sanitizers. \n\nPrecautions/Actions: Ensure water chlorination levels are adequate. Can cause severe infections in immunocompromised individuals. Avoid contact with eyes or ears.",
    "Enterococcus faecalis": "Indicates prolonged fecal contamination. Very resilient. \n\nPrecautions/Actions: Shock chlorinate the water system. Discontinue use for drinking until negative tests are returned.",
    "Clostridium perfringens": "Spore-forming bacteria, highly resistant to standard disinfection. \n\nPrecautions/Actions: Indicates remote or past fecal contamination. UV filtration or extreme heat treatment may be required.",
    "Listeria monocytogenes": "Dangerous to pregnant women and immunocompromised individuals. \n\nPrecautions/Actions: Do not use water for food preparation or drinking. Pasteurization/boiling is required."
};

interface UploadZoneProps {
    onResultsGenerated?: (results: PredictionResult[] | null) => void;
}

export const UploadZone = ({ onResultsGenerated }: UploadZoneProps) => {
    const [files, setFiles] = useState<File[]>([]);
    const [isUploading, setIsUploading] = useState(false);
    const [progress, setProgress] = useState(0);
    const [localResults, setLocalResults] = useState<PredictionResult[] | null>(null);
    const [selectedResult, setSelectedResult] = useState<PredictionResult | null>(null);
    const [error, setError] = useState<string | null>(null);
    const fileInputRef = useRef<HTMLInputElement>(null);

    const handleFiles = async (selectedFiles: FileList | File[]) => {
        const fileArray = Array.from(selectedFiles);
        if (fileArray.length === 0) return;
        
        setFiles(fileArray);
        
        // Generate preview URLs
        const previewUrls = fileArray.map(f => URL.createObjectURL(f));
        
        setLocalResults(null);
        if (onResultsGenerated) onResultsGenerated(null);
        setSelectedResult(null);
        setError(null);
        setIsUploading(true);
        setProgress(20);

        const formData = new FormData();
        fileArray.forEach(f => {
            formData.append("files", f);
        });

        try {
            setProgress(60);
            const apiUrl = import.meta.env.VITE_API_URL || "http://localhost:5000";
            const response = await fetch(`${apiUrl}/predict_batch`, {
                method: "POST",
                body: formData,
            });

            if (!response.ok) {
                throw new Error("Failed to analyze images");
            }
            
            setProgress(90);
            const data = await response.json();
            
            // Attach preview URLs to results for display
            if (data.results && Array.isArray(data.results)) {
                const resultsWithPreviews = data.results.map((r: any) => {
                    const matchIndex = fileArray.findIndex(f => f.name === r.filename);
                    return {
                        ...r,
                        previewUrl: matchIndex >= 0 ? previewUrls[matchIndex] : null
                    };
                });
                setLocalResults(resultsWithPreviews);
                if (onResultsGenerated) onResultsGenerated(resultsWithPreviews);
            }
            setProgress(100);
        } catch (err: any) {
            setError(err.message || "An error occurred");
        } finally {
            setIsUploading(false);
        }
    };

    const onFileDrop = (e: React.DragEvent<HTMLDivElement>) => {
        e.preventDefault();
        if (e.dataTransfer.files.length > 0) {
            handleFiles(e.dataTransfer.files);
        }
    };

    const onFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        if (e.target.files && e.target.files.length > 0) {
            handleFiles(e.target.files);
        }
    };

    return (
        <section className="max-w-7xl mx-auto px-6 mb-24 relative z-10" id="upload">
            <div className={`grid ${localResults ? 'grid-cols-1 lg:grid-cols-3' : 'grid-cols-1 max-w-2xl mx-auto'} gap-8`}>
                <div className="space-y-6 lg:col-span-1">
                    <h3 className="text-3xl font-display font-bold">Upload Zone</h3>
                    
                    <div 
                        onDragOver={(e) => e.preventDefault()}
                        onDrop={onFileDrop}
                        onClick={() => fileInputRef.current?.click()}
                        className="cursor-target border-2 border-dashed border-primary/30 rounded-3xl p-12 flex flex-col items-center justify-center text-center bg-background-light/50 dark:bg-background-dark/50 backdrop-blur-md hover:bg-primary/10 transition-colors group cursor-pointer h-64"
                    >
                        <input 
                            type="file" 
                            ref={fileInputRef} 
                            onChange={onFileChange} 
                            className="hidden" 
                            accept="image/*"
                            multiple
                        />
                        <div className="w-16 h-16 rounded-full bg-primary/20 flex items-center justify-center mb-4 group-hover:scale-110 transition-transform">
                            <span className="material-symbols-outlined text-primary text-3xl">cloud_upload</span>
                        </div>
                        <h4 className="text-lg font-bold mb-2 text-slate-900 dark:text-white">Drag & drop images</h4>
                        <p className="text-sm text-slate-500 dark:text-slate-400 mb-4">Supports JPG, PNG up to 20MB.</p>
                        <button className="cursor-target bg-primary text-white px-5 py-2 rounded-lg font-bold hover:scale-105 transition-transform text-sm">Browse</button>
                    </div>

                    {/* Upload Progress */}
                    {isUploading && (
                        <div className="bg-slate-100 dark:bg-slate-800/80 p-6 rounded-2xl border border-slate-200 dark:border-slate-700 backdrop-blur-md">
                            <div className="flex items-center justify-between mb-2">
                                <span className="text-sm font-semibold text-slate-900 dark:text-slate-100">Processing {files.length} images</span>
                                <span className="text-sm font-bold text-primary">{progress}%</span>
                            </div>
                            <div className="w-full h-2 bg-slate-200 dark:bg-slate-700 rounded-full overflow-hidden">
                                <div className="h-full bg-primary rounded-full transition-all duration-300" style={{ width: `${progress}%` }}></div>
                            </div>
                            <p className="text-xs text-slate-500 mt-3 flex items-center gap-1">
                                <span className="material-symbols-outlined text-[14px] animate-spin">sync</span>
                                Analyzing morphological features...
                            </p>
                        </div>
                    )}
                    
                    {error && (
                        <div className="bg-red-500/10 border border-red-500/20 text-red-500 p-4 rounded-xl flex items-center gap-2">
                            <span className="material-symbols-outlined">error</span>
                            {error}
                        </div>
                    )}
                </div>

                {localResults && (
                    <div className="space-y-6 lg:col-span-2">
                        <div className="flex items-center justify-between">
                            <h3 className="text-3xl font-display font-bold">Analysis Results</h3>
                            <span className="bg-primary/10 text-primary px-3 py-1 rounded-full text-sm font-bold">{localResults.length} processed</span>
                        </div>
                        
                        <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
                            {localResults.map((res, index) => (
                                <div 
                                    key={index} 
                                    onClick={() => res.success && setSelectedResult(res)}
                                    className={`bg-white/80 dark:bg-slate-800/80 backdrop-blur-md p-6 rounded-3xl border border-slate-200 dark:border-slate-700 shadow-xl flex flex-col ${res.success ? 'cursor-pointer hover:scale-[1.02] hover:border-primary/50 transition-all' : ''}`}
                                    title={res.success ? "Click for detailed summary" : ""}
                                >
                                    <div className="flex gap-4 mb-4">
                                        <div className="w-24 h-24 rounded-2xl overflow-hidden border border-primary/20 flex-shrink-0 bg-black">
                                            {res.previewUrl ? (
                                                <img className="w-full h-full object-cover" alt={`Upload ${index}`} src={res.previewUrl} />
                                            ) : (
                                                <div className="w-full h-full flex items-center justify-center bg-slate-800"><span className="material-symbols-outlined text-slate-500">image</span></div>
                                            )}
                                        </div>
                                        <div className="flex-1 min-w-0">
                                            <p className="text-xs text-slate-500 dark:text-slate-400 truncate mb-1" title={res.filename}>{res.filename}</p>
                                            
                                            {res.success && res.prediction ? (
                                                <>
                                                    <h4 className="text-lg font-bold italic text-slate-900 dark:text-slate-100 leading-tight mb-2 truncate" title={res.prediction}>{res.prediction}</h4>
                                                    <div className={`inline-flex items-center gap-1 text-xs font-bold px-2 py-1 rounded-full whitespace-nowrap ${(res.confidence || 0) > 80 ? 'bg-green-500/10 text-green-500' : 'bg-yellow-500/10 text-yellow-500'}`}>
                                                        <span className="material-symbols-outlined text-[14px]">{(res.confidence || 0) > 80 ? 'verified' : 'warning'}</span>
                                                        {(res.confidence || 0).toFixed(1)}% Conf
                                                    </div>
                                                </>
                                            ) : (
                                                <div className="text-red-500 text-sm font-bold flex items-center gap-1">
                                                    <span className="material-symbols-outlined text-sm">error</span> Failed
                                                </div>
                                            )}
                                        </div>
                                    </div>

                                    {res.success && res.details && (
                                        <div className="flex-1 flex flex-col justify-end">
                                            <div className="grid grid-cols-2 gap-2 p-3 bg-slate-50 dark:bg-slate-900/50 rounded-xl">
                                                <div className="text-xs text-slate-900 dark:text-slate-100">
                                                    <span className="text-slate-500 block mb-0.5 mt-0">Morphology</span>
                                                    <span className="font-semibold">{res.details.shape}</span>
                                                </div>
                                                <div className="text-xs text-slate-900 dark:text-slate-100">
                                                    <span className="text-slate-500 block mb-0.5 mt-0">Stain</span>
                                                    <span className="font-semibold">{res.details.gram_stain}</span>
                                                </div>
                                            </div>
                                        </div>
                                    )}
                                </div>
                            ))}
                        </div>
                    </div>
                )}
            </div>

            {/* Modal Dialog Box */}
            {selectedResult && (
                <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm" onClick={() => setSelectedResult(null)}>
                    <div 
                        className="bg-white dark:bg-slate-900 rounded-3xl max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-2xl border border-slate-200 dark:border-slate-700"
                        onClick={e => e.stopPropagation()}
                    >
                        <div className="p-6 md:p-8 flex flex-col gap-6">
                            <div className="flex items-start justify-between">
                                <h3 className="text-2xl font-bold font-display text-slate-900 dark:text-white">Detailed Classification Summary</h3>
                                <button onClick={() => setSelectedResult(null)} className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
                                    <span className="material-symbols-outlined">close</span>
                                </button>
                            </div>

                            <div className="flex flex-col md:flex-row gap-6">
                                <div className="w-full md:w-48 h-48 rounded-2xl overflow-hidden bg-black flex-shrink-0 border-4 border-slate-100 dark:border-slate-800">
                                    {selectedResult.previewUrl ? (
                                        <img src={selectedResult.previewUrl} alt="Analyzed bacteria" className="w-full h-full object-cover" />
                                    ) : (
                                        <div className="w-full h-full flex items-center justify-center text-slate-500"><span className="material-symbols-outlined text-4xl">image</span></div>
                                    )}
                                </div>
                                <div className="flex-1 space-y-4">
                                    <div>
                                        <span className="text-sm font-bold text-slate-500 uppercase tracking-wider">Predicted Species</span>
                                        <h4 className="text-3xl font-bold italic text-primary mt-1">{selectedResult.prediction}</h4>
                                    </div>
                                    
                                    <div className="grid grid-cols-2 gap-4">
                                        <div className="bg-slate-50 dark:bg-slate-800/50 p-3 rounded-xl">
                                            <span className="text-xs text-slate-500 block mb-1">Confidence</span>
                                            <span className="text-lg font-bold text-slate-900 dark:text-white">{(selectedResult.confidence || 0).toFixed(1)}%</span>
                                        </div>
                                        <div className="bg-slate-50 dark:bg-slate-800/50 p-3 rounded-xl">
                                            <span className="text-xs text-slate-500 block mb-1">Risk Level</span>
                                            <span className="text-lg font-bold text-slate-900 dark:text-white">{selectedResult.details?.pathogenicity || 'Unknown'}</span>
                                        </div>
                                    </div>
                                </div>
                            </div>

                            <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-2">
                                <div className="space-y-3">
                                    <h5 className="font-bold text-slate-900 dark:text-white border-b border-slate-200 dark:border-slate-700 pb-2">Microbiology Specifics</h5>
                                    <ul className="space-y-2 text-sm text-slate-600 dark:text-slate-300">
                                        <li className="flex justify-between"><span>Gram Stain:</span> <strong className="text-slate-900 dark:text-white">{selectedResult.details?.gram_stain}</strong></li>
                                        <li className="flex justify-between"><span>Morphology:</span> <strong className="text-slate-900 dark:text-white">{selectedResult.details?.shape}</strong></li>
                                        <li className="flex justify-between"><span>Pathogenicity:</span> <strong className="text-slate-900 dark:text-white">{selectedResult.details?.pathogenicity}</strong></li>
                                    </ul>
                                </div>

                                <div className="space-y-3">
                                    <h5 className="font-bold text-slate-900 dark:text-white border-b border-slate-200 dark:border-slate-700 pb-2">Precautions & Actions</h5>
                                    <div className="bg-yellow-50 dark:bg-yellow-500/10 border border-yellow-200 dark:border-yellow-500/20 rounded-xl p-4 text-sm text-yellow-800 dark:text-yellow-200 shadow-inner">
                                        <div className="flex gap-2 items-start whitespace-pre-line">
                                            <span className="material-symbols-outlined text-[18px] mt-0.5">warning</span>
                                            <p className="leading-relaxed m-0">
                                                {selectedResult.prediction ? (PRECAUTIONS[selectedResult.prediction] || "Standard water safety protocols apply. Heat above 70°C before any consumption.") : "Unknown"}
                                            </p>
                                        </div>
                                    </div>
                                </div>
                            </div>
                            
                            <div className="mt-4 flex justify-end">
                                <button className="px-6 py-2 bg-slate-900 dark:bg-white text-white dark:text-slate-900 font-bold rounded-xl hover:scale-105 transition-transform" onClick={() => setSelectedResult(null)}>
                                    Close Details
                                </button>
                            </div>
                        </div>
                    </div>
                </div>
            )}
        </section>
    );
};