| 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; |