import React, { useState, useCallback, useRef, useEffect } from 'react'; import { BookUserIcon, PlusCircleIcon, ArrowLeftIcon, HistoryIcon, SparklesIcon, GalleryHorizontalIcon, ImagePlusIcon, Trash2Icon, BugIcon, WrenchIcon, BonsaiIcon, CameraIcon, CalendarIcon, SunIcon, WindIcon, SnowflakeIcon, SunriseIcon, LeafIcon, UmbrellaIcon, AlertTriangleIcon } from '../components/icons'; import type { View, BonsaiTree, DiaryLog, BonsaiAnalysis, SeasonalGuide, ProtectionProfile } from '../types'; import { LogTag } from '../types'; import { analyzeDiaryLog, analyzeGrowthProgression, analyzeFollowUp, generateSeasonalGuide, getProtectionProfile, isAIConfigured } from '../services/geminiService'; import Spinner from '../components/Spinner'; import { useLocalStorage } from '../hooks/useLocalStorage'; import AnalysisDisplay from '../components/AnalysisDisplay'; // --- Main Component --- const BonsaiDiaryView: React.FC<{ setActiveView: (view: View) => void }> = ({ setActiveView }) => { const [trees, setTrees] = useLocalStorage('bonsai-diary-trees', []); const [selectedTreeId, setSelectedTreeId] = useState(null); const [isAddTreeModalOpen, setAddTreeModalOpen] = useState(false); const [newlyAddedId, setNewlyAddedId] = useState(null); const aiConfigured = isAIConfigured(); useEffect(() => { const newId = window.localStorage.getItem('yuki-bonsai-diary-newly-added-tree-id'); if (newId) { setNewlyAddedId(newId); setSelectedTreeId(newId); window.localStorage.removeItem('yuki-bonsai-diary-newly-added-tree-id'); } }, []); const selectedTree = trees.find(t => t.id === selectedTreeId); const handleAddTree = async (newTreeData: Omit) => { const newTree: BonsaiTree = { ...newTreeData, id: `tree-${Date.now()}`, logs: [], analysisHistory: [], }; setTrees(prev => [...prev, newTree]); // Asynchronously fetch and update the protection profile if (aiConfigured) { const profile = await getProtectionProfile(newTree.species); if(profile) { const treeWithProfile = { ...newTree, protectionProfile: { ...profile, alertsEnabled: true }}; setTrees(prev => prev.map(t => t.id === newTree.id ? treeWithProfile : t)); } } }; const handleUpdateTree = (updatedTree: BonsaiTree) => { setTrees(prev => prev.map(t => t.id === updatedTree.id ? updatedTree : t)); }; const handleDeleteTree = (treeId: string) => { if (window.confirm("Are you sure you want to delete this tree and all its logs? This cannot be undone.")) { setTrees(prev => prev.filter(t => t.id !== treeId)); setSelectedTreeId(null); } }; return (

My Garden

Chronicle the life and growth of your personal trees.

{selectedTree ? ( { setSelectedTreeId(null); if (newlyAddedId) { setNewlyAddedId(null); } }} onUpdate={handleUpdateTree} onDelete={handleDeleteTree} setActiveView={setActiveView} showInitialReportOnLoad={newlyAddedId === selectedTree.id} isAIConfigured={aiConfigured} /> ) : ( <>
)} {isAddTreeModalOpen && ( setAddTreeModalOpen(false)} title="Add a New Bonsai to Your Garden"> setAddTreeModalOpen(false)} /> )}
); }; // --- Sub-components --- const TreeList: React.FC<{trees: BonsaiTree[], onSelectTree: (id: string) => void}> = ({ trees, onSelectTree }) => { if (trees.length === 0) { return (

Your Garden is Empty

Click "Add New Tree" or use the "New Tree Analysis" to get started.

); } return (
{trees.map(tree => (
onSelectTree(tree.id)}> {tree.name}

{tree.name}

{tree.species}

Acquired: {new Date(tree.acquiredDate).toLocaleDateString()}

{tree.logs.length} log entries. {tree.analysisHistory.length} analysis reports.

))}
); }; const TreeDetail: React.FC<{tree: BonsaiTree, onBack: () => void, onUpdate: (tree: BonsaiTree) => void, onDelete: (id: string) => void, setActiveView: (view: View) => void, showInitialReportOnLoad: boolean, isAIConfigured: boolean}> = ({ tree, onBack, onUpdate, onDelete, setActiveView, showInitialReportOnLoad, isAIConfigured }) => { const [isAddLogModalOpen, setAddLogModalOpen] = useState(false); const [isGrowthModalOpen, setGrowthModalOpen] = useState(false); const [activeReport, setActiveReport] = useState(null); const [isAnalyzingFollowUp, setIsAnalyzingFollowUp] = useState(false); const followUpPhotoInputRef = useRef(null); const [followUpError, setFollowUpError] = useState(''); const [isSeasonalPlanOpen, setSeasonalPlanOpen] = useState(false); useEffect(() => { if (showInitialReportOnLoad && tree.analysisHistory.length > 0) { setActiveReport(tree.analysisHistory[0].analysis); } }, [showInitialReportOnLoad, tree.analysisHistory]); const handleAddLog = async (newLogData: Omit, analyze: boolean) => { let newLog: DiaryLog = { ...newLogData, id: `log-${Date.now()}`}; if (analyze && newLog.photos.length > 0 && isAIConfigured) { try { const previousLogWithPhoto = [...tree.logs].reverse().find(l => l.photos.length > 0); const analysis = await analyzeDiaryLog(tree.species, newLog.photos, previousLogWithPhoto?.photos[0]); if (analysis) { newLog.aiAnalysis = analysis; } } catch (e: any) { alert(`Could not get AI analysis: ${e.message}`); } } const updatedTree = { ...tree, logs: [...tree.logs, newLog].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) }; onUpdate(updatedTree); }; const handleRequestFollowUp = () => { setFollowUpError(''); followUpPhotoInputRef.current?.click(); }; const handleFollowUpPhotoSelected = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; setIsAnalyzingFollowUp(true); const reader = new FileReader(); reader.onloadend = async () => { const imageBase64 = (reader.result as string).split(',')[1]; const latestAnalysis = tree.analysisHistory[0]?.analysis; if (!latestAnalysis) { setFollowUpError("No initial analysis found. Please use the AI Steward first."); setIsAnalyzingFollowUp(false); return; } try { const result = await analyzeFollowUp(imageBase64, latestAnalysis, tree.species, tree.location); if (result) { const newAnalysisEntry = { date: new Date().toISOString(), analysis: result }; const updatedTree = { ...tree, analysisHistory: [newAnalysisEntry, ...tree.analysisHistory] }; onUpdate(updatedTree); setActiveReport(result); } else { throw new Error("Failed to get follow-up analysis. Please try again."); } } catch (e: any) { setFollowUpError(e.message); } finally { setIsAnalyzingFollowUp(false); } }; reader.readAsDataURL(file); event.target.value = ''; }; return (

{tree.name}

{tree.species}

{!isAIConfigured && (
Some AI features like Follow-up Analysis and Seasonal Plans are disabled. Please set your Gemini API key in the{' '} .
)} {followUpError &&

{followUpError}

}
{tree.analysisHistory.length > 0 && (

Analysis History

{tree.analysisHistory.map((entry, index) => ( ))}
)}
{/* Timeline */}
{tree.logs.length > 0 ? tree.logs.map(log => ) : (

No log entries yet. Add one to start the timeline!

)}
{isAddLogModalOpen && ( setAddLogModalOpen(false)} title={`New Log for ${tree.name}`}> setAddLogModalOpen(false)} isAIConfigured={isAIConfigured}/> )} {isGrowthModalOpen && ( setGrowthModalOpen(false)} title="AI Growth Visualizer"> setGrowthModalOpen(false)} /> )} {activeReport && ( setActiveReport(null)} title={`Analysis for ${tree.name}`}>
)} {isSeasonalPlanOpen && ( setSeasonalPlanOpen(false)} title={`Seasonal Plan for ${tree.name}`}> setSeasonalPlanOpen(false)} isAIConfigured={isAIConfigured} /> )}
) }; const ProtectionSettings: React.FC<{tree: BonsaiTree, onUpdate: (tree: BonsaiTree) => void, isAIConfigured: boolean}> = ({ tree, onUpdate, isAIConfigured }) => { const handleProfileChange = (field: keyof ProtectionProfile, value: any) => { const updatedProfile = { ...tree.protectionProfile, minTempC: tree.protectionProfile?.minTempC ?? 0, maxTempC: tree.protectionProfile?.maxTempC ?? 40, maxWindKph: tree.protectionProfile?.maxWindKph ?? 50, alertsEnabled: tree.protectionProfile?.alertsEnabled ?? true, [field]: value, }; onUpdate({ ...tree, protectionProfile: updatedProfile }); }; if (!tree.protectionProfile) { return (
{isAIConfigured ? :

Protection profile generation disabled. Set API key in Settings.

}
); } return (

Weather Shield Settings

handleProfileChange('alertsEnabled', e.target.checked)} className="h-5 w-5 rounded text-blue-600 focus:ring-blue-500" />
handleProfileChange('minTempC', Number(e.target.value))} className="w-20 p-1 border rounded-md text-center" />
handleProfileChange('maxTempC', Number(e.target.value))} className="w-20 p-1 border rounded-md text-center" />
handleProfileChange('maxWindKph', Number(e.target.value))} className="w-20 p-1 border rounded-md text-center" />
); }; const LogEntry: React.FC<{log: DiaryLog, setActiveView: (view: View) => void}> = ({log, setActiveView}) => { const tagLinks: Partial, view: View}>> = { [LogTag.PestControl]: { icon: BugIcon, view: 'pests' }, [LogTag.DiseaseControl]: { icon: BugIcon, view: 'pests' }, [LogTag.Wiring]: { icon: WrenchIcon, view: 'tools' }, [LogTag.Pruning]: { icon: WrenchIcon, view: 'tools' }, [LogTag.Repotting]: { icon: WrenchIcon, view: 'tools' }, }; return (

{new Date(log.date).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}

{log.title}

{log.aiAnalysis && (
Yuki's Analysis

{log.aiAnalysis.summary}

{log.aiAnalysis.suggestions && log.aiAnalysis.suggestions.map((s, i) => (

Suggestion: {s}

))}
)} {log.healthCheckResult && (
Health Check Diagnosis

Probable Cause: {log.healthCheckResult.probableCause} ({log.healthCheckResult.confidence} Confidence)

{log.healthCheckResult.explanation}

)}

{log.notes}

{log.photos.length > 0 && (
{log.photos.map((photo, i) => ( {`Log ))}
)}
{log.tags.map(tag => { const link = tagLinks[tag]; return (
{tag} {link && ( )}
) })}
); } const SeasonalPlanModal: React.FC<{ tree: BonsaiTree, onClose: () => void, isAIConfigured: boolean }> = ({ tree, onClose, isAIConfigured }) => { const [guideData, setGuideData] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(''); useEffect(() => { const fetchGuide = async () => { setIsLoading(true); setError(''); const latestAnalysis = tree.analysisHistory[0]?.analysis; // Use the guide from the analysis if it exists if (latestAnalysis && latestAnalysis.seasonalGuide && latestAnalysis.seasonalGuide.length > 0) { setGuideData(latestAnalysis.seasonalGuide); setIsLoading(false); return; } // If no cached guide, check if AI is configured before trying to generate if (!isAIConfigured) { setError("A seasonal guide was not found in your latest analysis. Please set an API key in Settings to generate a new one."); setIsLoading(false); return; } // Fallback to generating if not found in analysis try { const result = await generateSeasonalGuide(tree.species, tree.location); if (result) { setGuideData(result); } else { throw new Error("Failed to get seasonal guide from the AI."); } } catch (e: any) { setError(e.message); } finally { setIsLoading(false); } }; fetchGuide(); }, [tree, isAIConfigured]); const seasonIcons: { [key: string]: React.FC> } = { Spring: SunriseIcon, Summer: SunIcon, Autumn: WindIcon, Winter: SnowflakeIcon, }; return (
{isLoading && } {error &&

{error}

} {guideData && (
{guideData.sort((a,b) => ['Spring', 'Summer', 'Autumn', 'Winter'].indexOf(a.season) - ['Spring', 'Summer', 'Autumn', 'Winter'].indexOf(b.season)).map(season => { const Icon = seasonIcons[season.season] || LeafIcon; return (

{season.season}

{season.summary}

    {season.tasks.map(task => (
  • {task.task} {task.importance}
  • ))}
) })}
)}
); }; // --- Forms and Modals --- const AddTreeForm: React.FC<{onAddTree: (tree: Omit) => void, onClose: () => void}> = ({onAddTree, onClose}) => { const [name, setName] = useState(''); const [species, setSpecies] = useState(''); const [location, setLocation] = useState(''); const [acquiredDate, setAcquiredDate] = useState(new Date().toISOString().split('T')[0]); const [source, setSource] = useState(''); const [initialPhoto, setInitialPhoto] = useState(''); const [notes, setNotes] = useState(''); const [error, setError] = useState(''); const fileInputRef = useRef(null); const handleFileChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { const reader = new FileReader(); reader.onloadend = () => setInitialPhoto((reader.result as string).split(',')[1]); reader.readAsDataURL(file); } }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!name || !species || !acquiredDate || !initialPhoto || !location) { setError('Please fill out all required fields and upload an initial photo.'); return; } onAddTree({ name, species, acquiredDate, source, initialPhoto, notes, location }); onClose(); }; return (
setName(e.target.value)} required className="w-full p-2 border rounded-md" /> setSpecies(e.target.value)} required className="w-full p-2 border rounded-md" /> setLocation(e.target.value)} required className="w-full p-2 border rounded-md" /> setSource(e.target.value)} className="w-full p-2 border rounded-md" />
setAcquiredDate(e.target.value)} required className="w-full p-2 border rounded-md" />
fileInputRef.current?.click()} className="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-stone-300 border-dashed rounded-md cursor-pointer">
{initialPhoto ? : }

Click to upload an image

{error &&

{error}

}
); }; const AddLogForm: React.FC<{onAddLog: (log: Omit, analyze: boolean) => void, onClose: () => void, isAIConfigured: boolean}> = ({onAddLog, onClose, isAIConfigured}) => { const [date, setDate] = useState(new Date().toISOString().split('T')[0]); const [title, setTitle] = useState(''); const [notes, setNotes] = useState(''); const [photos, setPhotos] = useState([]); const [tags, setTags] = useState([]); const [requestAnalysis, setRequestAnalysis] = useState(true); const fileInputRef = useRef(null); const [error, setError] = useState(''); const handleFileChange = (e: React.ChangeEvent) => { const files = e.target.files; if (files) { const filePromises = Array.from(files).map(file => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => resolve((reader.result as string).split(',')[1]); reader.onerror = reject; reader.readAsDataURL(file); }); }); Promise.all(filePromises).then(base64Photos => setPhotos(p => [...p, ...base64Photos])); } }; const handleTagToggle = (tag: LogTag) => { setTags(prev => prev.includes(tag) ? prev.filter(t => t !== tag) : [...prev, tag]); }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!title || !date) { setError('Please add a title and date.'); return; } onAddLog({ date, title, notes, photos, tags }, requestAnalysis); onClose(); }; return (
setTitle(e.target.value)} required className="w-full p-2 border rounded-md" /> setDate(e.target.value)} required className="w-full p-2 border rounded-md" />
fileInputRef.current?.click()} className="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-stone-300 border-dashed rounded-md cursor-pointer">

Click to upload images

{photos.length > 0 &&
{photos.length} photo(s) selected.
}
{Object.values(LogTag).map(tag => ( ))}

Yuki will analyze your photos for changes and provide suggestions. (Requires at least one photo)

setRequestAnalysis(e.target.checked)} className="ml-auto h-5 w-5 rounded text-green-600 focus:ring-green-500" disabled={photos.length === 0 || !isAIConfigured} />
{error &&

{error}

}
); } const GrowthVisualizer: React.FC<{tree: BonsaiTree, onClose: () => void}> = ({ tree, onClose }) => { const logsWithPhotos = tree.logs.filter(l => l.photos.length > 0); const [startLogId, setStartLogId] = useState(logsWithPhotos.length > 1 ? logsWithPhotos[logsWithPhotos.length - 1].id : ''); const [endLogId, setEndLogId] = useState(logsWithPhotos.length > 0 ? logsWithPhotos[0].id : ''); const [analysis, setAnalysis] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(''); const handleGenerate = async () => { setError(''); const startLog = logsWithPhotos.find(l => l.id === startLogId); const endLog = logsWithPhotos.find(l => l.id === endLogId); if (!startLog || !endLog || !startLog.photos[0] || !endLog.photos[0]) return; setIsLoading(true); setAnalysis(null); const photoData = [ { date: startLog.date, image: startLog.photos[0] }, { date: endLog.date, image: endLog.photos[0] } ].sort((a,b) => new Date(a.date).getTime() - new Date(b.date).getTime()); try { const result = await analyzeGrowthProgression(tree.species, photoData); setAnalysis(result || "Could not generate analysis."); } catch (e: any) { setError(e.message); } finally { setIsLoading(false); } }; const startPhoto = logsWithPhotos.find(l => l.id === startLogId)?.photos[0]; const endPhoto = logsWithPhotos.find(l => l.id === endLogId)?.photos[0]; return (
{startPhoto && }
{endPhoto && }
{error &&

{error}

} {isLoading && } {analysis && (

Progression Analysis

{analysis}

)}
); }; const Modal: React.FC<{onClose: () => void, title: string, children: React.ReactNode}> = ({ onClose, title, children }) => { return (
e.stopPropagation()}>

{title}

{children}
); }; export default BonsaiDiaryView;