File size: 6,641 Bytes
be02369 |
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 |
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) { // 4MB limit
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; |