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