Yuki / views /SpeciesIdentifierView.tsx
Severian's picture
Upload 43 files
be02369 verified
import React, { useState, useRef } from 'react';
import { ScanIcon, SparklesIcon, UploadCloudIcon, CheckCircleIcon, AlertTriangleIcon } from '../components/icons';
import Spinner from '../components/Spinner';
import { identifyBonsaiSpecies, isAIConfigured } from '../services/geminiService';
import type { View, SpeciesIdentificationResult } from '../types';
import { AppStatus } from '../types';
const SpeciesIdentifierView: React.FC<{ setActiveView: (view: View) => void }> = ({ setActiveView }) => {
const [status, setStatus] = useState<AppStatus>(AppStatus.IDLE);
const [image, setImage] = useState<{ preview: string; base64: string } | null>(null);
const [result, setResult] = useState<SpeciesIdentificationResult | null>(null);
const [error, setError] = useState<string>('');
const fileInputRef = useRef<HTMLInputElement>(null);
const aiConfigured = isAIConfigured();
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];
setImage({ preview: reader.result as string, base64: base64String });
setError('');
setStatus(AppStatus.IDLE);
setResult(null);
};
reader.onerror = () => setError("Failed to read the file.");
reader.readAsDataURL(file);
}
};
const handleIdentify = async () => {
if (!image) {
setError("Please upload an image to identify.");
return;
}
setStatus(AppStatus.ANALYZING);
setError('');
setResult(null);
try {
const idResult = await identifyBonsaiSpecies(image.base64);
if (idResult && idResult.identifications.length > 0) {
setResult(idResult);
setStatus(AppStatus.SUCCESS);
} else {
throw new Error("Could not identify the species. The AI may be busy, or the image may not be clear enough. Please try a different photo.");
}
} catch (e: any) {
setError(e.message);
setStatus(AppStatus.ERROR);
}
};
const startFullAnalysis = (species: string) => {
// A bit of a hack to pass data to another view; for a larger app, a state manager (Context, Redux) would be better.
window.sessionStorage.setItem('prefilled-species', species);
setActiveView('steward');
}
const renderContent = () => {
switch (status) {
case AppStatus.ANALYZING:
return <Spinner text="Yuki is examining the leaves..." />;
case AppStatus.SUCCESS:
if (!result) return null;
return (
<div className="space-y-4">
<h3 className="text-xl font-bold text-center text-stone-800">Identification Results</h3>
{result.identifications.map((id, index) => (
<div key={index} className="bg-white p-6 rounded-xl shadow-md border border-stone-200">
<div className="flex justify-between items-baseline mb-2">
<h4 className="text-lg font-bold text-stone-900">{id.commonName}</h4>
<span className={`px-2 py-0.5 text-xs font-medium rounded-full ${id.confidence === 'High' ? 'bg-green-100 text-green-800' : id.confidence === 'Medium' ? 'bg-yellow-100 text-yellow-800' : 'bg-red-100 text-red-800'}`}>{id.confidence} Confidence</span>
</div>
<p className="text-sm font-mono text-stone-500 mb-3">{id.scientificName}</p>
<p className="text-sm text-stone-700"><strong className="font-medium text-stone-800">Reasoning:</strong> {id.reasoning}</p>
<div className="mt-4 p-4 bg-stone-50 rounded-lg">
<h5 className="font-semibold text-stone-800">General Care Summary</h5>
<p className="text-sm text-stone-600 mt-1">{id.generalCareSummary}</p>
</div>
{id.confidence === 'High' && index === 0 && (
<button
onClick={() => startFullAnalysis(id.commonName)}
className="mt-4 w-full flex items-center justify-center gap-2 rounded-md bg-green-700 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-green-600"
>
<SparklesIcon className="w-5 h-5" />
Get Full Analysis for {id.commonName}
</button>
)}
</div>
))}
</div>
);
case AppStatus.ERROR:
return <p className="text-center text-red-600 p-4 bg-red-50 rounded-md">{error}</p>;
case AppStatus.IDLE:
default:
return (
<div className="bg-white p-6 rounded-xl shadow-lg border border-stone-200">
<div onClick={() => fileInputRef.current?.click()} className="flex justify-center rounded-lg border-2 border-dashed border-stone-300 px-6 py-10 hover:border-green-600 transition-colors cursor-pointer">
<div className="text-center">
{image ? <img src={image.preview} 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" />
<p className="mt-2 text-sm font-semibold text-green-700">Upload a photo to identify</p>
<p className="text-xs text-stone-500">PNG, JPG up to 4MB</p>
</> )}
</div>
<input ref={fileInputRef} type="file" className="sr-only" onChange={handleFileChange} accept="image/png, image/jpeg" />
</div>
{error && <p className="text-sm text-red-600 mt-2">{error}</p>}
<button onClick={handleIdentify} disabled={!image || !aiConfigured} className="w-full mt-6 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 disabled:bg-stone-400 disabled:cursor-not-allowed">
<CheckCircleIcon className="w-5 h-5"/> Identify Species
</button>
{!aiConfigured && (
<div className="mt-4 p-3 bg-yellow-50 text-yellow-800 rounded-lg border border-yellow-200 text-center">
<p className="text-sm">
AI features are disabled. Please set your Gemini API key in the{' '}
<button onClick={() => setActiveView('settings')} className="font-bold underline hover:text-yellow-900">
Settings page
</button>.
</p>
</div>
)}
</div>
);
}
}
return (
<div className="space-y-8 max-w-2xl mx-auto">
<header className="text-center">
<h2 className="text-3xl font-bold tracking-tight text-stone-900 sm:text-4xl flex items-center justify-center gap-3">
<ScanIcon className="w-8 h-8 text-green-600" />
Species Identifier
</h2>
<p className="mt-4 text-lg leading-8 text-stone-600">
Don't know what kind of tree you have? Upload a photo and let our AI identify it for you.
</p>
</header>
<div>
{renderContent()}
</div>
</div>
);
};
export default SpeciesIdentifierView;