Yuki / components /AnalysisDisplay.tsx
Severian's picture
Upload 43 files
be02369 verified
import React, { useState } from 'react';
import type { BonsaiAnalysis, ToolRecommendation } from '../types';
import {
CheckCircleIcon, AlertTriangleIcon, SparklesIcon, ZapIcon, BookOpenIcon,
ClipboardListIcon, ScissorsIcon, SunIcon, DropletIcon, ThermometerIcon,
CalendarIcon, BonsaiIcon, LayersIcon, FlaskConicalIcon, GalleryVerticalEndIcon,
WindIcon, SnowflakeIcon, SunriseIcon, LeafIcon, StethoscopeIcon, BugIcon, WrenchIcon,
BookUserIcon, PaletteIcon, DownloadIcon
} from './icons';
import Spinner from './Spinner';
import { generateBonsaiImage } from '../services/geminiService';
type Tab = 'Overview' | 'Care Plan' | 'Health and Pests' | 'Styling' | 'Fertilizer and Soil' | 'Seasonal Guide' | 'Diagnostics' | 'Pest Library' | 'Tools & Supplies' | 'Knowledge';
interface AnalysisDisplayProps {
analysis: BonsaiAnalysis;
onReset?: () => void;
onSaveToDiary?: () => void;
isReadonly?: boolean;
treeImageBase64?: string;
}
const TABS: { name: Tab, icon: React.FC<React.SVGProps<SVGSVGElement>> }[] = [
{ name: 'Overview', icon: CheckCircleIcon },
{ name: 'Care Plan', icon: CalendarIcon },
{ name: 'Diagnostics', icon: StethoscopeIcon },
{ name: 'Health and Pests', icon: AlertTriangleIcon },
{ name: 'Pest Library', icon: BugIcon },
{ name: 'Styling', icon: ScissorsIcon },
{ name: 'Fertilizer and Soil', icon: LayersIcon },
{ name: 'Seasonal Guide', icon: LeafIcon },
{ name: 'Tools & Supplies', icon: WrenchIcon },
{ name: 'Knowledge', icon: BookOpenIcon },
];
const InfoCard: React.FC<{ title: string; children: React.ReactNode; icon: React.ReactNode; className?: string }> = ({ title, children, icon, className = '' }) => (
<div className={`bg-white rounded-xl shadow-md border border-stone-200 p-6 ${className}`}>
<div className="flex items-center gap-3 mb-4">
{icon}
<h3 className="text-xl font-semibold text-stone-800">{title}</h3>
</div>
<div className="space-y-3 text-stone-600">
{children}
</div>
</div>
);
const HealthGauge: React.FC<{ score: number }> = ({ score }) => {
const circumference = 2 * Math.PI * 52;
const offset = circumference - (score / 100) * circumference;
const color = score > 80 ? 'text-green-600' : score > 50 ? 'text-yellow-500' : 'text-red-600';
return (
<div className="relative w-32 h-32 flex items-center justify-center">
<svg className="absolute w-full h-full transform -rotate-90">
<circle className="text-stone-200" strokeWidth="10" stroke="currentColor" fill="transparent" r="52" cx="64" cy="64" />
<circle className={color} strokeWidth="10" strokeDasharray={circumference} strokeDashoffset={offset}
strokeLinecap="round" stroke="currentColor" fill="transparent" r="52" cx="64" cy="64" />
</svg>
<span className={`text-3xl font-bold ${color}`}>{score}</span>
</div>
);
};
const TabButton: React.FC<{ name: Tab, icon: React.ReactNode, isActive: boolean, onClick: () => void }> = ({ name, icon, isActive, onClick }) => (
<button
onClick={onClick}
className={`flex-shrink-0 flex items-center justify-center sm:justify-start gap-2 px-4 py-3 text-sm font-medium rounded-t-lg border-b-2 transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2
${isActive
? 'border-green-600 text-green-700 bg-white'
: 'border-transparent text-stone-500 hover:text-green-600 hover:bg-stone-100'
}`}
>
{icon}
<span className="hidden sm:inline">{name}</span>
</button>
);
const PotVisualizerModal: React.FC<{
isOpen: boolean;
onClose: () => void;
analysis: BonsaiAnalysis;
}> = ({ isOpen, onClose, analysis }) => {
const [isLoading, setIsLoading] = useState(false);
const [generatedImage, setGeneratedImage] = useState<string | null>(null);
const [error, setError] = useState('');
const [promptUsed, setPromptUsed] = useState('');
const potStyles = [
"A shallow, rectangular, unglazed, dark brown ceramic pot.",
"A round, blue-glazed ceramic pot with a soft patina.",
"An oval, cream-colored pot with delicate feet.",
"A modern, minimalist, square, grey concrete pot.",
"A classic, hexagonal, deep red pot.",
"A natural-looking pot carved from rock with rough texture."
];
const handleGenerate = async (potStyle: string) => {
setIsLoading(true);
setError('');
setGeneratedImage(null);
const treeDescription = `A photorealistic image of a healthy ${analysis.species} bonsai tree. ${analysis.healthAssessment.observations.join(' ')}. The trunk is ${analysis.healthAssessment.trunkAndNebariHealth.toLowerCase()}.`;
const fullPrompt = `${treeDescription} The tree is in ${potStyle}`;
setPromptUsed(fullPrompt);
const result = await generateBonsaiImage(fullPrompt);
if (result) {
setGeneratedImage(result);
} else {
setError("Sorry, the AI couldn't generate the image. Please try a different style.");
}
setIsLoading(false);
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60" onClick={onClose}>
<div className="bg-white rounded-2xl shadow-xl w-full max-w-2xl m-4 p-6 relative" onClick={e => e.stopPropagation()}>
<h3 className="text-2xl font-bold text-stone-900 mb-4">AI Pot Visualizer</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h4 className="font-semibold text-stone-800 mb-2">Choose a Pot Style:</h4>
<div className="space-y-2">
{potStyles.map(style => (
<button key={style} onClick={() => handleGenerate(style)} disabled={isLoading} className="w-full text-left p-3 bg-stone-100 hover:bg-green-100 hover:text-green-800 rounded-lg text-sm transition-colors disabled:opacity-50">
{style}
</button>
))}
</div>
</div>
<div className="flex flex-col items-center justify-center bg-stone-100 rounded-lg p-4 min-h-[256px]">
{isLoading ? <Spinner text="Yuki is at the potter's wheel..." /> :
generatedImage ? (
<div className="space-y-2 text-center">
<img src={`data:image/jpeg;base64,${generatedImage}`} alt="Generated bonsai" className="rounded-lg shadow-md"/>
<a href={`data:image/jpeg;base64,${generatedImage}`} download="bonsai-pot-visualization.jpg" className="inline-flex items-center gap-2 text-xs text-green-700 hover:underline">
<DownloadIcon className="w-4 h-4" />
Download Image
</a>
</div>
) :
error ? <p className="text-red-600 text-center">{error}</p> : <p className="text-stone-500 text-center">Your generated image will appear here.</p>
}
</div>
</div>
<button onClick={onClose} className="mt-6 w-full bg-stone-200 text-stone-700 font-semibold py-2 px-4 rounded-lg hover:bg-stone-300 transition-colors">
Close
</button>
</div>
</div>
);
};
const AnalysisDisplay: React.FC<AnalysisDisplayProps> = ({ analysis, onReset, onSaveToDiary, isReadonly = false, treeImageBase64 }) => {
const [activeTab, setActiveTab] = useState<Tab>('Overview');
const [isPotVisualizerOpen, setPotVisualizerOpen] = useState(false);
const {
healthAssessment, careSchedule, pestAndDiseaseAlerts, stylingSuggestions,
environmentalFactors, estimatedAge, species, wateringAnalysis, knowledgeNuggets,
fertilizerRecommendations, soilRecipe, potSuggestion, seasonalGuide,
diagnostics, pestLibrary, toolRecommendations
} = analysis;
const seasonIcons: { [key: string]: React.FC<React.SVGProps<SVGSVGElement>> } = {
Spring: SunriseIcon,
Summer: SunIcon,
Autumn: WindIcon,
Winter: SnowflakeIcon,
};
const renderContent = () => {
switch (activeTab) {
case 'Overview':
return (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<InfoCard title="At a Glance" icon={<BonsaiIcon className="w-7 h-7 text-green-700" />} className="lg:col-span-1 flex flex-col items-center text-center">
<HealthGauge score={healthAssessment.healthScore} />
<p className="text-lg font-medium mt-4"><strong className="text-stone-900">Overall Health:</strong> {healthAssessment.overallHealth}</p>
<p><strong className="text-stone-900">Species:</strong> {species}</p>
<p><strong className="text-stone-900">Estimated Age:</strong> {estimatedAge}</p>
</InfoCard>
<InfoCard title="Key Observations" icon={<CheckCircleIcon className="w-7 h-7 text-green-600" />} className="lg:col-span-2">
<ul className="list-disc list-inside space-y-2">
{healthAssessment.observations.map((obs, i) => <li key={i}>{obs}</li>)}
</ul>
</InfoCard>
<InfoCard title="Ideal Environment" icon={<SunIcon className="w-7 h-7 text-yellow-500" />} className="lg:col-span-3 grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="flex items-center gap-3">
<SunIcon className="w-8 h-8 text-yellow-500"/>
<div><strong className="block text-stone-800">Light</strong>{environmentalFactors.idealLight}</div>
</div>
<div className="flex items-center gap-3">
<DropletIcon className="w-8 h-8 text-blue-500"/>
<div><strong className="block text-stone-800">Humidity</strong>{environmentalFactors.idealHumidity}</div>
</div>
<div className="flex items-center gap-3">
<ThermometerIcon className="w-8 h-8 text-red-500"/>
<div><strong className="block text-stone-800">Temperature</strong>{environmentalFactors.temperatureRange}</div>
</div>
</InfoCard>
</div>
);
case 'Care Plan':
return (
<div className="space-y-6">
<InfoCard title="Watering Analysis" icon={<DropletIcon className="w-7 h-7 text-blue-500" />}>
<p><strong className="text-stone-900">Frequency:</strong> {wateringAnalysis.frequency}</p>
<p><strong className="text-stone-900">Method:</strong> {wateringAnalysis.method}</p>
<p><strong className="text-stone-900">Notes:</strong> {wateringAnalysis.notes}</p>
</InfoCard>
<div>
<h3 className="text-2xl font-semibold text-stone-800 text-center mb-6">4-Week Personalized Care Schedule</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5">
{careSchedule.sort((a,b) => a.week - b.week).map((item, i) => (
<div key={i} className="bg-white p-5 rounded-lg border border-stone-200 shadow-sm flex flex-col">
<p className="font-bold text-green-800">Week {item.week}</p>
<p className="font-semibold text-stone-900 mt-1">{item.task}</p>
<p className="text-sm text-stone-600 mt-2 flex-grow">{item.details}</p>
{item.toolsNeeded && item.toolsNeeded.length > 0 && (
<div className="mt-3 pt-3 border-t border-stone-200">
<h4 className="text-xs font-bold text-stone-500 uppercase">Tools</h4>
<div className="flex flex-wrap gap-2 mt-1">
{item.toolsNeeded.map(tool => <span key={tool} className="text-xs bg-stone-100 text-stone-700 px-2 py-1 rounded-full">{tool}</span>)}
</div>
</div>
)}
</div>
))}
</div>
</div>
</div>
);
case 'Diagnostics':
return (
<InfoCard title="Advanced Diagnostics" icon={<StethoscopeIcon className="w-7 h-7 text-blue-700" />}>
<div className="space-y-4">
{diagnostics.map((diag, i) => (
<div key={i} className="p-4 bg-stone-50 rounded-lg border border-stone-200">
<div className="flex justify-between items-baseline">
<h4 className="font-semibold text-stone-800">{diag.issue}</h4>
<span className={`px-2 py-0.5 text-xs font-medium rounded-full ${diag.confidence === 'High' ? 'bg-red-100 text-red-800' : diag.confidence === 'Medium' ? 'bg-yellow-100 text-yellow-800' : 'bg-green-100 text-green-800'}`}>{diag.confidence} Confidence</span>
</div>
<p className="mt-2"><strong className="font-medium text-stone-700">Symptoms to watch for:</strong> {diag.symptoms}</p>
<p className="mt-1"><strong className="font-medium text-stone-700">Solution/Prevention:</strong> {diag.solution}</p>
</div>
))}
</div>
</InfoCard>
);
case 'Health and Pests':
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<InfoCard title="Active Pest & Disease Alerts" icon={<AlertTriangleIcon className="w-7 h-7 text-amber-600" />}>
{pestAndDiseaseAlerts.length > 0 ? (
pestAndDiseaseAlerts.map((alert, i) => (
<div key={i} className="py-2 border-b border-stone-200 last:border-b-0">
<div className="flex justify-between items-baseline">
<p className="font-semibold text-stone-800">{alert.pestOrDisease}</p>
<span className={`px-2 py-0.5 text-xs font-medium rounded-full ${alert.severity === 'High' ? 'bg-red-100 text-red-800' : alert.severity === 'Medium' ? 'bg-yellow-100 text-yellow-800' : 'bg-green-100 text-green-800'}`}>{alert.severity}</span>
</div>
<p><strong className="font-medium">Symptoms:</strong> {alert.symptoms}</p>
<p><strong className="font-medium">Treatment:</strong> {alert.treatment}</p>
</div>
))
) : <p>No active threats detected. Check the 'Pest Library' tab for preventative knowledge on common threats in your area.</p>}
</InfoCard>
<InfoCard title="Detailed Health Breakdown" icon={<CheckCircleIcon className="w-7 h-7 text-green-600" />}>
<p><strong className="font-medium text-stone-900">Foliage:</strong> {healthAssessment.foliageHealth}</p>
<p><strong className="font-medium text-stone-900">Trunk & Nebari:</strong> {healthAssessment.trunkAndNebariHealth}</p>
<p><strong className="font-medium text-stone-900">Pot & Soil:</strong> {healthAssessment.potAndSoilHealth}</p>
</InfoCard>
</div>
);
case 'Pest Library':
return (
<InfoCard title="Regional Pest & Disease Library" icon={<BugIcon className="w-7 h-7 text-red-700" />}>
<p className="text-sm mb-4">A reference for the most common threats to a {species} in your region.</p>
<div className="space-y-4">
{pestLibrary.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">
<p>{pest.description}</p>
<div>
<strong className="font-medium text-stone-700">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-700">Organic Treatment:</strong>
<p>{pest.treatment.organic}</p>
</div>
<div>
<strong className="font-medium text-stone-700">Chemical Treatment:</strong>
<p>{pest.treatment.chemical}</p>
</div>
</div>
</details>
))}
</div>
</InfoCard>
);
case 'Styling':
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<InfoCard title="Styling & Shaping Advice" icon={<ScissorsIcon className="w-7 h-7 text-indigo-600" />} className="lg:col-span-1">
{stylingSuggestions.length > 0 ? (
stylingSuggestions.map((suggestion, i) => (
<div key={i} className="py-3 border-b border-stone-200 last:border-b-0">
<p className="font-semibold text-stone-800">{suggestion.technique} ({suggestion.area})</p>
<p className="mt-1">{suggestion.description}</p>
</div>
))
) : <p>No immediate styling is recommended. Focus on health first.</p>}
</InfoCard>
<InfoCard title="Pot Recommendation" icon={<GalleryVerticalEndIcon className="w-7 h-7 text-orange-700" />} className="lg:col-span-1">
<p><strong className="font-medium text-stone-900">Style:</strong> {potSuggestion.style}</p>
<p><strong className="font-medium text-stone-900">Size:</strong> {potSuggestion.size}</p>
<p><strong className="font-medium text-stone-900">Color Palette:</strong> {potSuggestion.colorPalette}</p>
<p className="mt-3 pt-3 border-t border-stone-200"><strong className="font-medium text-stone-900">Rationale:</strong> {potSuggestion.rationale}</p>
<button onClick={() => setPotVisualizerOpen(true)} className="mt-4 w-full flex items-center justify-center gap-2 rounded-md bg-orange-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-orange-500">
<PaletteIcon className="w-5 h-5"/>
Visualize Pot Pairings
</button>
</InfoCard>
</div>
);
case 'Fertilizer and Soil':
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<InfoCard title="Fertilizer Schedule" icon={<FlaskConicalIcon className="w-7 h-7 text-cyan-600" />}>
{fertilizerRecommendations.map(rec => (
<div key={rec.phase} className="py-2 border-b border-stone-200 last:border-b-0">
<p className="font-semibold text-stone-800">{rec.phase}</p>
<p><strong className="font-medium">Type:</strong> {rec.type}</p>
<p><strong className="font-medium">Frequency:</strong> {rec.frequency}</p>
<p className="text-sm italic">Notes: {rec.notes}</p>
</div>
))}
</InfoCard>
<InfoCard title="Recommended Soil Mix" icon={<LayersIcon className="w-7 h-7 text-amber-700" />}>
<div className="space-y-3">
{soilRecipe.components.map(comp => (
<div key={comp.name}>
<div className="flex justify-between items-center mb-1">
<span className="font-medium text-stone-800">{comp.name}</span>
<span className="font-semibold text-amber-800">{comp.percentage}%</span>
</div>
<div className="w-full bg-stone-200 rounded-full h-2.5">
<div className="bg-amber-600 h-2.5 rounded-full" style={{ width: `${comp.percentage}%` }}></div>
</div>
<p className="text-xs italic text-stone-500 mt-1">{comp.notes}</p>
</div>
))}
</div>
<p className="mt-4 pt-4 border-t border-stone-200"><strong className="font-medium text-stone-900">Rationale:</strong> {soilRecipe.rationale}</p>
</InfoCard>
</div>
);
case 'Seasonal Guide':
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{seasonalGuide.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 (
<InfoCard key={season.season} title={season.season} icon={<Icon className="w-7 h-7 text-green-700" />}>
<p className="italic text-stone-600 mb-4">{season.summary}</p>
<ul className="space-y-2">
{season.tasks.map(task => (
<li key={task.task} className="flex items-center justify-between text-sm">
<span>{task.task}</span>
<span className={`px-2 py-0.5 text-xs font-medium rounded-full ${task.importance === 'High' ? 'bg-red-100 text-red-800' : task.importance === 'Medium' ? 'bg-yellow-100 text-yellow-800' : 'bg-blue-100 text-blue-800'}`}>{task.importance}</span>
</li>
))}
</ul>
</InfoCard>
)
})}
</div>
);
case 'Tools & Supplies':
const groupedTools = toolRecommendations.reduce((acc, tool) => {
acc[tool.category] = acc[tool.category] || [];
acc[tool.category].push(tool);
return acc;
}, {} as Record<ToolRecommendation['category'], ToolRecommendation[]>);
return (
<div className="space-y-6">
{Object.entries(groupedTools).map(([category, tools]) => (
<InfoCard key={category} title={category} icon={<WrenchIcon className="w-7 h-7 text-gray-700" />}>
<div className="space-y-3">
{tools.map(tool => (
<div key={tool.name} className="py-2 border-b border-stone-100 last:border-0">
<div className="flex justify-between items-baseline">
<p className="font-semibold text-stone-800">{tool.name}</p>
<span className={`px-2 py-0.5 text-xs font-medium rounded-full ${tool.level === 'Essential' ? 'bg-green-100 text-green-800' : tool.level === 'Recommended' ? 'bg-blue-100 text-blue-800' : 'bg-orange-100 text-orange-800'}`}>{tool.level}</span>
</div>
<p className="text-sm">{tool.description}</p>
</div>
))}
</div>
</InfoCard>
))}
</div>
);
case 'Knowledge':
return (
<InfoCard title={`Master's Wisdom: ${species}`} icon={<BookOpenIcon className="w-7 h-7 text-purple-600" />}>
<ul className="space-y-4">
{knowledgeNuggets.map((nugget, i) => (
<li key={i} className="flex items-start gap-3">
<SparklesIcon className="w-5 h-5 text-yellow-500 mt-1 flex-shrink-0" />
<span>{nugget}</span>
</li>
))}
</ul>
</InfoCard>
);
default:
return null;
}
};
return (
<div className="w-full max-w-6xl mx-auto space-y-8">
<PotVisualizerModal isOpen={isPotVisualizerOpen} onClose={() => setPotVisualizerOpen(false)} analysis={analysis} />
<div className="text-center">
<h2 className="text-3xl font-bold tracking-tight text-stone-900 sm:text-4xl">Your Bonsai Analysis is Ready</h2>
<p className="mt-4 text-lg leading-8 text-stone-600">
Master Yuki has assessed your <span className="font-semibold text-green-700">{species}</span>. Here is your report.
</p>
</div>
<div className="bg-stone-50 rounded-xl p-1 sm:p-2 sticky top-2 z-20 shadow-sm border border-stone-200">
<div className="flex flex-nowrap items-center justify-start -mb-px border-b border-stone-200 overflow-x-auto">
{TABS.map(({name, icon: Icon}) => (
<TabButton key={name} name={name} icon={<Icon className="w-5 h-5" />} isActive={activeTab === name} onClick={() => setActiveTab(name)} />
))}
</div>
</div>
<div className="bg-stone-100 p-4 sm:p-6 lg:p-8 rounded-2xl">
{renderContent()}
</div>
{!isReadonly && (
<div className="text-center pt-6 flex flex-col sm:flex-row justify-center items-center gap-4">
<button
onClick={onReset}
className="w-full sm:w-auto bg-stone-200 text-stone-700 font-semibold py-3 px-6 rounded-lg hover:bg-stone-300 transition-colors"
>
Analyze Another Tree
</button>
{onSaveToDiary && (
<button
onClick={onSaveToDiary}
className="flex items-center gap-2 w-full sm:w-auto justify-center rounded-md bg-green-700 px-6 py-3 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"
>
<BookUserIcon className="w-5 h-5"/>
Save Tree to My Garden
</button>
)}
</div>
)}
</div>
);
};
export default AnalysisDisplay;