|
|
|
|
|
|
|
import React, { useState, useRef, useEffect } from 'react'; |
|
import { UploadCloudIcon, SparklesIcon } from './icons'; |
|
|
|
interface ImageUploaderProps { |
|
onAnalyze: (imageBase64: string, species: string, location: string) => void; |
|
isAnalyzing: boolean; |
|
defaultSpecies?: string; |
|
defaultLocation?: string; |
|
disabled?: boolean; |
|
} |
|
|
|
const ImageUploader: React.FC<ImageUploaderProps> = ({ onAnalyze, isAnalyzing, defaultSpecies = '', defaultLocation = '', disabled = false }) => { |
|
const [imagePreview, setImagePreview] = useState<string | null>(null); |
|
const [imageBase64, setImageBase64] = useState<string>(''); |
|
const [species, setSpecies] = useState<string>(defaultSpecies); |
|
const [location, setLocation] = useState<string>(defaultLocation); |
|
const [error, setError] = useState<string>(''); |
|
const fileInputRef = useRef<HTMLInputElement>(null); |
|
|
|
useEffect(() => { |
|
setSpecies(defaultSpecies); |
|
setLocation(defaultLocation); |
|
}, [defaultSpecies, defaultLocation]); |
|
|
|
|
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => { |
|
const file = event.target.files?.[0]; |
|
if (file) { |
|
if (file.size > 4 * 1024 * 1024) { |
|
setError("File size exceeds 4MB. Please upload a smaller image."); |
|
return; |
|
} |
|
const reader = new FileReader(); |
|
reader.onloadend = () => { |
|
const base64String = (reader.result as string).split(',')[1]; |
|
setImagePreview(reader.result as string); |
|
setImageBase64(base64String); |
|
setError(''); |
|
}; |
|
reader.onerror = () => { |
|
setError("Failed to read the file."); |
|
} |
|
reader.readAsDataURL(file); |
|
} |
|
}; |
|
|
|
const handleAnalyzeClick = () => { |
|
if (!imageBase64) { |
|
setError('Please upload an image of your bonsai.'); |
|
return; |
|
} |
|
if (!species.trim()) { |
|
setError('Please enter the bonsai species.'); |
|
return; |
|
} |
|
if (!location.trim()) { |
|
setError('Please enter your city or region for climate data.'); |
|
return; |
|
} |
|
setError(''); |
|
onAnalyze(imageBase64, species, location); |
|
}; |
|
|
|
const triggerFileSelect = () => fileInputRef.current?.click(); |
|
|
|
return ( |
|
<div className="w-full max-w-2xl mx-auto bg-white p-8 rounded-2xl shadow-lg border border-stone-200"> |
|
<div className="space-y-6"> |
|
<div> |
|
<h2 className="text-lg font-medium text-stone-900">Provide Tree Details</h2> |
|
<p className="mt-1 text-sm text-stone-600"> |
|
Upload a clear photo and tell us about your tree for the most accurate AI analysis. |
|
</p> |
|
</div> |
|
|
|
<div |
|
className="mt-2 flex justify-center rounded-lg border-2 border-dashed border-stone-300 px-6 py-10 hover:border-green-600 transition-colors cursor-pointer" |
|
onClick={triggerFileSelect} |
|
onDragOver={(e) => e.preventDefault()} |
|
onDrop={(e) => { |
|
e.preventDefault(); |
|
if (e.dataTransfer.files) { |
|
const mockEvent = { target: { files: e.dataTransfer.files } } as unknown as React.ChangeEvent<HTMLInputElement>; |
|
handleFileChange(mockEvent); |
|
} |
|
}} |
|
> |
|
<div className="text-center"> |
|
{imagePreview ? ( |
|
<img src={imagePreview} alt="Bonsai preview" className="mx-auto h-40 w-auto rounded-md object-cover" /> |
|
) : ( |
|
<> |
|
<UploadCloudIcon className="mx-auto h-12 w-12 text-stone-400" aria-hidden="true" /> |
|
<div className="mt-4 flex text-sm leading-6 text-stone-600"> |
|
<span className="relative font-semibold text-green-700 focus-within:outline-none focus-within:ring-2 focus-within:ring-green-600 focus-within:ring-offset-2 hover:text-green-500"> |
|
Upload a file |
|
</span> |
|
<input ref={fileInputRef} id="file-upload" name="file-upload" type="file" className="sr-only" onChange={handleFileChange} accept="image/png, image/jpeg" /> |
|
<p className="pl-1">or drag and drop</p> |
|
</div> |
|
<p className="text-xs leading-5 text-stone-500">PNG, JPG up to 4MB</p> |
|
</> |
|
)} |
|
</div> |
|
</div> |
|
|
|
<div className="grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-2"> |
|
<div> |
|
<label htmlFor="species" className="block text-sm font-medium leading-6 text-stone-900"> |
|
Bonsai Species |
|
</label> |
|
<div className="mt-2"> |
|
<input |
|
type="text" |
|
name="species" |
|
id="species" |
|
value={species} |
|
onChange={(e) => setSpecies(e.target.value)} |
|
className="block w-full rounded-md border-0 py-2 px-3 text-stone-900 shadow-sm ring-1 ring-inset ring-stone-300 placeholder:text-stone-400 focus:ring-2 focus:ring-inset focus:ring-green-600 sm:text-sm sm:leading-6" |
|
placeholder="e.g., Japanese Maple, Ficus" |
|
/> |
|
</div> |
|
</div> |
|
<div> |
|
<label htmlFor="location" className="block text-sm font-medium leading-6 text-stone-900"> |
|
Your Location (City/Region) |
|
</label> |
|
<div className="mt-2"> |
|
<input |
|
type="text" |
|
name="location" |
|
id="location" |
|
value={location} |
|
onChange={(e) => setLocation(e.target.value)} |
|
className="block w-full rounded-md border-0 py-2 px-3 text-stone-900 shadow-sm ring-1 ring-inset ring-stone-300 placeholder:text-stone-400 focus:ring-2 focus:ring-inset focus:ring-green-600 sm:text-sm sm:leading-6" |
|
placeholder="e.g., San Francisco, CA" |
|
/> |
|
</div> |
|
</div> |
|
</div> |
|
{error && <p className="text-sm text-red-600">{error}</p>} |
|
<div className="mt-6"> |
|
<button |
|
type="button" |
|
onClick={handleAnalyzeClick} |
|
disabled={isAnalyzing || disabled} |
|
className="w-full flex items-center justify-center gap-2 rounded-md bg-green-700 px-4 py-3 text-sm font-semibold text-white shadow-sm hover:bg-green-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-700 disabled:bg-stone-400 disabled:cursor-not-allowed" |
|
> |
|
<SparklesIcon className="-ml-0.5 h-5 w-5" aria-hidden="true" /> |
|
{isAnalyzing ? 'Analyzing...' : 'Get AI Analysis'} |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
); |
|
}; |
|
|
|
export default ImageUploader; |