File size: 8,673 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


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;