Yuki / components /ImageUploader.tsx
Severian's picture
Upload 43 files
be02369 verified
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;