Yuki / views /AiStewardView.tsx
Severian's picture
Upload 43 files
be02369 verified
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; // User cancelled
// Fetch the protection profile for the new tree
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;