|
|
|
|
|
|
|
|
|
|
|
|
|
import React, { useState, useCallback, useEffect } from 'react'; |
|
import ImageUploader from '../components/ImageUploader'; |
|
import AnalysisDisplay from '../components/AnalysisDisplay'; |
|
import Spinner from '../components/Spinner'; |
|
import { SparklesIcon, AlertTriangleIcon } from '../components/icons'; |
|
import { analyzeBonsai, getProtectionProfile, isAIConfigured } from '../services/geminiService'; |
|
import type { BonsaiAnalysis, BonsaiTree, View } from '../types'; |
|
import { AppStatus } from '../types'; |
|
|
|
interface AiStewardViewProps { |
|
setActiveView: (view: View) => void; |
|
} |
|
|
|
const AiStewardView: React.FC<AiStewardViewProps> = ({ setActiveView }) => { |
|
const [status, setStatus] = useState<AppStatus>(AppStatus.IDLE); |
|
const [analysisResult, setAnalysisResult] = useState<BonsaiAnalysis | null>(null); |
|
const [error, setError] = useState<string>(''); |
|
const [currentImage, setCurrentImage] = useState<string>(''); |
|
const [currentLocation, setCurrentLocation] = useState(''); |
|
const [prefilledSpecies, setPrefilledSpecies] = useState(''); |
|
const aiConfigured = isAIConfigured(); |
|
|
|
useEffect(() => { |
|
const species = window.sessionStorage.getItem('prefilled-species'); |
|
if (species) { |
|
setPrefilledSpecies(species); |
|
window.sessionStorage.removeItem('prefilled-species'); |
|
} |
|
}, []); |
|
|
|
const handleAnalyze = useCallback(async (imageBase64: string, species: string, location: string) => { |
|
setStatus(AppStatus.ANALYZING); |
|
setError(''); |
|
setAnalysisResult(null); |
|
setCurrentImage(imageBase64); |
|
setCurrentLocation(location); |
|
|
|
try { |
|
const result = await analyzeBonsai(imageBase64, species, location); |
|
if (result) { |
|
setAnalysisResult(result); |
|
setStatus(AppStatus.SUCCESS); |
|
} else { |
|
throw new Error('Failed to get analysis. The AI may be busy, or there was an issue with the request. Please try again.'); |
|
} |
|
} catch (e: any) { |
|
setError(e.message); |
|
setStatus(AppStatus.ERROR); |
|
} |
|
}, []); |
|
|
|
const handleSaveToDiary = async () => { |
|
try { |
|
if (!analysisResult || !currentImage || !currentLocation) { |
|
alert("Cannot save. Analysis data is missing."); |
|
return; |
|
} |
|
|
|
const treeName = window.prompt("What would you like to name this tree in your garden?", analysisResult.species); |
|
if (!treeName) return; |
|
|
|
|
|
const protectionProfileData = await getProtectionProfile(analysisResult.species); |
|
|
|
const newTree: BonsaiTree = { |
|
id: `tree-${Date.now()}`, |
|
name: treeName, |
|
species: analysisResult.species, |
|
acquiredDate: new Date().toISOString(), |
|
source: "AI Steward Analysis", |
|
location: currentLocation, |
|
initialPhoto: currentImage, |
|
logs: [], |
|
analysisHistory: [{ date: new Date().toISOString(), analysis: analysisResult }], |
|
protectionProfile: protectionProfileData ? { ...protectionProfileData, alertsEnabled: true } : undefined, |
|
}; |
|
|
|
const storageKey = `yuki-app-bonsai-diary-trees`; |
|
const existingTreesJSON = window.localStorage.getItem(storageKey); |
|
const existingTrees: BonsaiTree[] = existingTreesJSON ? JSON.parse(existingTreesJSON) : []; |
|
const updatedTrees = [...existingTrees, newTree]; |
|
window.localStorage.setItem(storageKey, JSON.stringify(updatedTrees)); |
|
window.localStorage.setItem('yuki-bonsai-diary-newly-added-tree-id', newTree.id); |
|
setActiveView('garden'); |
|
|
|
} catch (error) { |
|
console.error("Failed to save tree to garden:", error); |
|
alert("Could not save the tree to your garden. An unexpected error occurred. Please check browser permissions for storage and try again."); |
|
} |
|
}; |
|
|
|
|
|
const handleReset = () => { |
|
setStatus(AppStatus.IDLE); |
|
setAnalysisResult(null); |
|
setError(''); |
|
setCurrentImage(''); |
|
setCurrentLocation(''); |
|
setPrefilledSpecies(''); |
|
}; |
|
|
|
const renderContent = () => { |
|
switch (status) { |
|
case AppStatus.ANALYZING: |
|
return <div className="flex justify-center items-center h-full"><Spinner /></div>; |
|
case AppStatus.SUCCESS: |
|
return analysisResult ? <AnalysisDisplay analysis={analysisResult} onReset={handleReset} onSaveToDiary={handleSaveToDiary} treeImageBase64={currentImage} /> : null; |
|
case AppStatus.ERROR: |
|
return ( |
|
<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> |
|
<button |
|
onClick={handleReset} |
|
className="mt-6 bg-green-700 text-white font-semibold py-2 px-6 rounded-lg hover:bg-green-600 transition-colors" |
|
> |
|
Try Again |
|
</button> |
|
</div> |
|
); |
|
case AppStatus.IDLE: |
|
default: |
|
return ( |
|
<> |
|
<ImageUploader onAnalyze={handleAnalyze} isAnalyzing={false} defaultSpecies={prefilledSpecies} disabled={!aiConfigured}/> |
|
{!aiConfigured && ( |
|
<div className="mt-6 p-4 bg-yellow-50 text-yellow-800 rounded-lg border border-yellow-200 text-center max-w-2xl mx-auto"> |
|
<div className="flex items-center justify-center gap-2"> |
|
<AlertTriangleIcon className="w-5 h-5"/> |
|
<h3 className="font-semibold">AI Features Disabled</h3> |
|
</div> |
|
<p className="text-sm mt-1"> |
|
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> |
|
)} |
|
</> |
|
); |
|
} |
|
}; |
|
|
|
return ( |
|
<div className="space-y-8"> |
|
<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"> |
|
<SparklesIcon className="w-8 h-8 text-green-600" /> |
|
New Tree Analysis |
|
</h2> |
|
<p className="mt-4 text-lg leading-8 text-stone-600 max-w-2xl mx-auto"> |
|
Welcome a new tree to your collection. Get an instant, expert analysis from Yuki, our AI Bonsai Sensei. |
|
</p> |
|
</header> |
|
<div className="w-full"> |
|
{renderContent()} |
|
</div> |
|
</div> |
|
); |
|
}; |
|
|
|
export default AiStewardView; |