|
import React, { useState, useCallback } from 'react'; |
|
import { BugIcon, SparklesIcon, AlertTriangleIcon } from '../components/icons'; |
|
import Spinner from '../components/Spinner'; |
|
import { generatePestLibrary, isAIConfigured } from '../services/geminiService'; |
|
import type { PestLibraryEntry, View } from '../types'; |
|
import { AppStatus } from '../types'; |
|
|
|
interface PestLibraryViewProps { |
|
setActiveView: (view: View) => void; |
|
} |
|
|
|
const PestLibraryView: React.FC<PestLibraryViewProps> = ({ setActiveView }) => { |
|
const [status, setStatus] = useState<AppStatus>(AppStatus.IDLE); |
|
const [location, setLocation] = useState<string>(''); |
|
const [pestData, setPestData] = useState<PestLibraryEntry[] | null>(null); |
|
const [error, setError] = useState<string>(''); |
|
const aiConfigured = isAIConfigured(); |
|
|
|
const handleGenerate = useCallback(async () => { |
|
if (!location.trim()) { |
|
setError('Please enter your city or region.'); |
|
return; |
|
} |
|
setStatus(AppStatus.ANALYZING); |
|
setError(''); |
|
setPestData(null); |
|
|
|
try { |
|
const result = await generatePestLibrary(location); |
|
if (result) { |
|
setPestData(result); |
|
setStatus(AppStatus.SUCCESS); |
|
} else { |
|
throw new Error('Failed to generate the pest library. The AI may be busy. Please try again.'); |
|
} |
|
} catch (e: any) { |
|
setError(e.message); |
|
setStatus(AppStatus.ERROR); |
|
} |
|
}, [location]); |
|
|
|
return ( |
|
<div className="space-y-8 max-w-4xl 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"> |
|
<BugIcon className="w-8 h-8 text-red-600" /> |
|
Regional Pest Library |
|
</h2> |
|
<p className="mt-4 text-lg leading-8 text-stone-600"> |
|
Discover common pests and diseases for bonsai in your area to stay one step ahead. |
|
</p> |
|
</header> |
|
|
|
<div className="bg-white p-6 rounded-xl shadow-lg border border-stone-200"> |
|
<div className="flex flex-col sm:flex-row gap-4"> |
|
<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-green-600 sm:text-sm sm:leading-6" |
|
placeholder="e.g., Portland, Oregon" |
|
disabled={status === AppStatus.ANALYZING} |
|
/> |
|
<button |
|
onClick={handleGenerate} |
|
disabled={status === AppStatus.ANALYZING || !aiConfigured} |
|
className="flex items-center justify-center gap-2 w-full sm:w-auto rounded-md bg-green-700 px-4 py-2.5 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="w-5 h-5" /> |
|
{status === AppStatus.ANALYZING ? 'Generating...' : 'Generate Library'} |
|
</button> |
|
</div> |
|
{error && <p className="text-sm text-red-600 mt-2">{error}</p>} |
|
{!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> |
|
|
|
{status === AppStatus.ANALYZING && <Spinner text="Yuki is consulting the archives..." />} |
|
{status === AppStatus.SUCCESS && pestData && ( |
|
<div className="bg-white p-6 rounded-xl shadow-lg border border-stone-200 space-y-4"> |
|
<h3 className="text-xl font-bold text-stone-800">Pest & Disease Guide for {location}</h3> |
|
{pestData.map((pest, i) => ( |
|
<details key={i} className="p-4 bg-stone-50 rounded-lg border border-stone-200 group"> |
|
<summary className="font-semibold text-stone-800 cursor-pointer flex justify-between items-center"> |
|
{pest.name} ({pest.type}) |
|
<span className="text-xs text-stone-500 group-open:hidden">Show Details</span> |
|
<span className="text-xs text-stone-500 hidden group-open:inline">Hide Details</span> |
|
</summary> |
|
<div className="mt-4 space-y-3 text-sm text-stone-700"> |
|
<p>{pest.description}</p> |
|
<div> |
|
<strong className="font-medium text-stone-800">Symptoms:</strong> |
|
<ul className="list-disc list-inside ml-2"> |
|
{pest.symptoms.map((s, idx) => <li key={idx}>{s}</li>)} |
|
</ul> |
|
</div> |
|
<div> |
|
<strong className="font-medium text-stone-800">Organic Treatment:</strong> |
|
<p>{pest.treatment.organic}</p> |
|
</div> |
|
<div> |
|
<strong className="font-medium text-stone-800">Chemical Treatment:</strong> |
|
<p>{pest.treatment.chemical}</p> |
|
</div> |
|
</div> |
|
</details> |
|
))} |
|
</div> |
|
)} |
|
{status === AppStatus.ERROR && ( |
|
<div className="text-center p-8 bg-white rounded-lg shadow-lg border border-red-200 max-w-md mx-auto"> |
|
<h3 className="text-xl font-semibold text-red-700">An Error Occurred</h3> |
|
<p className="text-stone-600 mt-2">{error}</p> |
|
</div> |
|
)} |
|
</div> |
|
); |
|
}; |
|
|
|
export default PestLibraryView; |