File size: 10,427 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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187


import React, { useState, useRef } from 'react';
import { FilterIcon, SparklesIcon, UploadCloudIcon, DropletIcon, AlertTriangleIcon } from '../components/icons';
import Spinner from '../components/Spinner';
import { analyzeSoilComposition, isAIConfigured } from '../services/geminiService';
import type { SoilAnalysis, View } from '../types';
import { AppStatus } from '../types';

const SoilAnalyzerView: 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 [species, setSpecies] = useState<string>('');
    const [location, setLocation] = useState<string>('');
    const [result, setResult] = useState<SoilAnalysis | 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 handleAnalyze = async () => {
        if (!image) {
            setError("Please upload an image of your soil mix.");
            return;
        }
        if (!species.trim()) {
            setError("Please enter the target species.");
            return;
        }
        if (!location.trim()) {
            setError("Please enter your location.");
            return;
        }

        setStatus(AppStatus.ANALYZING);
        setError('');
        setResult(null);

        try {
            const analysisResult = await analyzeSoilComposition(image.base64, species, location);
            if (analysisResult) {
                setResult(analysisResult);
                setStatus(AppStatus.SUCCESS);
            } else {
                throw new Error("Could not analyze the soil. The AI may be busy or the image may not be clear enough. Please try again.");
            }
        } catch (e: any) {
            setError(e.message);
            setStatus(AppStatus.ERROR);
        }
    };

    const renderResults = () => {
        if (!result) return null;

        const ratingColors = {
            Poor: 'bg-red-500',
            Average: 'bg-yellow-500',
            Good: 'bg-blue-500',
            Excellent: 'bg-green-500',
            Low: 'bg-orange-500',
            Medium: 'bg-yellow-500',
            High: 'bg-blue-500'
        };

        return (
            <div className="bg-white p-6 rounded-xl shadow-lg border border-stone-200 space-y-6">
                <h3 className="text-xl font-bold text-center text-stone-800">Soil Analysis Results</h3>
                <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
                    <div>
                        <h4 className="font-semibold text-stone-800 mb-2">Estimated Composition</h4>
                        <div className="space-y-3">
                            {result.components.map(comp => (
                                <div key={comp.name}>
                                    <div className="flex justify-between items-center mb-1">
                                        <span className="font-medium text-stone-800">{comp.name}</span>
                                        <span className="font-semibold text-amber-800">{comp.percentage}%</span>
                                    </div>
                                    <div className="w-full bg-stone-200 rounded-full h-2.5">
                                        <div className="bg-amber-600 h-2.5 rounded-full" style={{ width: `${comp.percentage}%` }}></div>
                                    </div>
                                </div>
                            ))}
                        </div>
                    </div>
                    <div className="space-y-4">
                        <h4 className="font-semibold text-stone-800 mb-2">Properties</h4>
                        <div className="flex items-center gap-3">
                            <strong className="w-32">Drainage:</strong>
                            <span className={`px-3 py-1 text-sm font-medium text-white rounded-full ${ratingColors[result.drainageRating]}`}>{result.drainageRating}</span>
                        </div>
                        <div className="flex items-center gap-3">
                             <strong className="w-32">Water Retention:</strong>
                            <span className={`px-3 py-1 text-sm font-medium text-white rounded-full ${ratingColors[result.waterRetention]}`}>{result.waterRetention}</span>
                        </div>
                    </div>
                </div>
                 <div className="mt-4 p-4 bg-stone-50 rounded-lg">
                    <h5 className="font-semibold text-stone-800">Suitability for {species}</h5>
                    <p className="text-sm text-stone-600 mt-1">{result.suitabilityAnalysis}</p>
                </div>
                <div className="mt-4 p-4 bg-green-50 rounded-lg">
                    <h5 className="font-semibold text-green-800">Improvement Suggestions</h5>
                    <p className="text-sm text-green-700 mt-1">{result.improvementSuggestions}</p>
                </div>
                 <button onClick={() => setStatus(AppStatus.IDLE)} className="w-full mt-4 bg-stone-200 text-stone-700 font-semibold py-2 px-4 rounded-lg hover:bg-stone-300 transition-colors">
                    Analyze Another Mix
                </button>
            </div>
        );
    }
    
    return (
        <div className="space-y-8 max-w-3xl 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">
                    <FilterIcon className="w-8 h-8 text-amber-700" />
                    Soil Analyzer
                </h2>
                <p className="mt-4 text-lg leading-8 text-stone-600">
                    Take a photo of your bonsai soil to get an AI-powered analysis of its composition and suitability.
                </p>
            </header>

            {status !== AppStatus.SUCCESS && (
                 <div className="bg-white p-6 rounded-xl shadow-lg border border-stone-200 space-y-4">
                    <div onClick={() => fileInputRef.current?.click()} className="flex justify-center rounded-lg border-2 border-dashed border-stone-300 px-6 py-10 hover:border-amber-600 transition-colors cursor-pointer">
                        <div className="text-center">
                            {image ? <img src={image.preview} alt="Soil 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-amber-700">Upload a close-up photo of your soil</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>
                    <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
                         <input type="text" 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-amber-600" placeholder="Target Species (e.g., Juniper)" />
                         <input type="text" 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-amber-600" placeholder="Your Location (e.g., Phoenix, AZ)" />
                    </div>
                    {error && <p className="text-sm text-red-600">{error}</p>}
                    <button onClick={handleAnalyze} disabled={!image || status === AppStatus.ANALYZING || !aiConfigured} className="w-full mt-2 flex items-center justify-center gap-2 rounded-md bg-amber-700 px-4 py-3 text-sm font-semibold text-white shadow-sm hover:bg-amber-600 disabled:bg-stone-400 disabled:cursor-not-allowed">
                       <SparklesIcon className="w-5 h-5"/> {status === AppStatus.ANALYZING ? 'Analyzing...' : 'Analyze Soil'}
                    </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">
                            Please set your Gemini API key in the{' '}
                            <button onClick={() => setActiveView('settings')} className="font-bold underline hover:text-yellow-900">
                                Settings page
                            </button>
                            {' '}to enable this feature.
                            </p>
                        </div>
                    )}
                </div>
            )}
            
            {status === AppStatus.ANALYZING && <Spinner text="Yuki is sifting through the details..." />}
            {status === AppStatus.SUCCESS && renderResults()}
            {status === AppStatus.ERROR && <p className="text-center text-red-600 p-4 bg-red-50 rounded-md">{error}</p>}

        </div>
    );
};

export default SoilAnalyzerView;