|
import React, { useState, useEffect, useCallback } from 'react'; |
|
import { WrenchIcon, PlusCircleIcon, ToolboxIcon, InfoIcon, Trash2Icon, SparklesIcon, BookOpenIcon, CheckCircleIcon } from '../components/icons'; |
|
import Spinner from '../components/Spinner'; |
|
import { generateToolGuide, generateMaintenanceTips, isAIConfigured } from '../services/geminiService'; |
|
import type { ToolRecommendation, UserTool, MaintenanceTips } from '../types'; |
|
import { AppStatus, ToolCondition } from '../types'; |
|
import { useLocalStorage } from '../hooks/useLocalStorage'; |
|
|
|
|
|
const ToolGuideView: React.FC = () => { |
|
const [status, setStatus] = useState<AppStatus>(AppStatus.IDLE); |
|
const [toolData, setToolData] = useState<ToolRecommendation[] | null>(null); |
|
const [myToolkit, setMyToolkit] = useLocalStorage<UserTool[]>('user-toolkit', []); |
|
const [error, setError] = useState<string>(''); |
|
const [activeTab, setActiveTab] = useState<'encyclopedia' | 'toolkit'>('encyclopedia'); |
|
|
|
const [modalOpen, setModalOpen] = useState(false); |
|
const [modalContent, setModalContent] = useState<{toolName: string, tips: MaintenanceTips} | null>(null); |
|
const [modalLoading, setModalLoading] = useState(false); |
|
const aiConfigured = isAIConfigured(); |
|
|
|
useEffect(() => { |
|
const fetchToolGuide = async () => { |
|
if (!aiConfigured) { |
|
setStatus(AppStatus.ERROR); |
|
setError('Please set your Gemini API key in Settings to load the tool encyclopedia.'); |
|
return; |
|
} |
|
setStatus(AppStatus.ANALYZING); |
|
try { |
|
const result = await generateToolGuide(); |
|
if (result) { |
|
setToolData(result); |
|
setStatus(AppStatus.SUCCESS); |
|
} else { |
|
throw new Error('Failed to load tool encyclopedia. The AI may be busy. Please try again.'); |
|
} |
|
} catch (e: any) { |
|
setError(e.message); |
|
setStatus(AppStatus.ERROR); |
|
} |
|
}; |
|
fetchToolGuide(); |
|
}, [aiConfigured]); |
|
|
|
const handleAddToToolkit = (tool: ToolRecommendation) => { |
|
const newUserTool: UserTool = { |
|
...tool, |
|
id: `${tool.name}-${Date.now()}`, |
|
condition: ToolCondition.GOOD, |
|
}; |
|
setMyToolkit(prev => [...prev, newUserTool]); |
|
}; |
|
|
|
const handleRemoveFromToolkit = (toolId: string) => { |
|
setMyToolkit(prev => prev.filter(t => t.id !== toolId)); |
|
}; |
|
|
|
const handleUpdateTool = (toolId: string, updatedProps: Partial<UserTool>) => { |
|
setMyToolkit(prev => prev.map(t => t.id === toolId ? { ...t, ...updatedProps } : t)); |
|
} |
|
|
|
const handleShowMaintenanceTips = async (tool: UserTool) => { |
|
setModalOpen(true); |
|
setModalLoading(true); |
|
try { |
|
const tips = await generateMaintenanceTips(tool.name); |
|
if (tips) { |
|
setModalContent({ toolName: tool.name, tips }); |
|
} else { |
|
throw new Error("Could not retrieve tips."); |
|
} |
|
} catch(e: any) { |
|
setModalContent({ |
|
toolName: tool.name, |
|
tips: { sharpening: e.message, cleaning: "Could not retrieve tips.", storage: "Could not retrieve tips."} |
|
}); |
|
} finally { |
|
setModalLoading(false); |
|
} |
|
}; |
|
|
|
const isToolInKit = (toolName: string) => myToolkit.some(t => t.name === toolName); |
|
|
|
const conditionColors: Record<ToolCondition, { bg: string, text: string, ring: string }> = { |
|
[ToolCondition.EXCELLENT]: { bg: 'bg-green-100', text: 'text-green-800', ring: 'ring-green-600' }, |
|
[ToolCondition.GOOD]: { bg: 'bg-blue-100', text: 'text-blue-800', ring: 'ring-blue-600' }, |
|
[ToolCondition.NEEDS_SHARPENING]: { bg: 'bg-yellow-100', text: 'text-yellow-800', ring: 'ring-yellow-600' }, |
|
[ToolCondition.NEEDS_OILING]: { bg: 'bg-orange-100', text: 'text-orange-800', ring: 'ring-orange-600' }, |
|
[ToolCondition.DAMAGED]: { bg: 'bg-red-100', text: 'text-red-800', ring: 'ring-red-600' }, |
|
}; |
|
|
|
const renderEncyclopedia = () => { |
|
if (status === AppStatus.ANALYZING) return <Spinner text="Yuki is organizing the tool shed..." />; |
|
if (status === AppStatus.ERROR) return <p className="text-center text-red-600 p-4 bg-red-50 rounded-md">{error}</p>; |
|
if (!toolData) return null; |
|
|
|
const groupedTools = toolData.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]) => ( |
|
<div key={category} className="bg-white rounded-xl shadow-md border border-stone-200 p-6"> |
|
<h3 className="text-xl font-semibold text-stone-800 mb-4">{category} Tools</h3> |
|
<div className="divide-y divide-stone-100"> |
|
{tools.sort((a,b) => { |
|
const levels = { 'Essential': 1, 'Recommended': 2, 'Advanced': 3 }; |
|
return levels[a.level] - levels[b.level]; |
|
}).map(tool => ( |
|
<div key={tool.name} className="py-3"> |
|
<div className="flex justify-between items-start gap-4"> |
|
<div className="flex-1"> |
|
<p className="font-semibold text-stone-800">{tool.name}</p> |
|
<p className="text-sm text-stone-600">{tool.description}</p> |
|
</div> |
|
<div className="flex flex-col items-end gap-2"> |
|
<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> |
|
<button |
|
onClick={() => handleAddToToolkit(tool)} |
|
disabled={isToolInKit(tool.name)} |
|
className="flex items-center gap-1 text-xs font-semibold text-green-700 hover:text-green-600 disabled:text-stone-400 disabled:cursor-not-allowed transition-colors" |
|
> |
|
{isToolInKit(tool.name) ? <CheckCircleIcon className="w-4 h-4" /> : <PlusCircleIcon className="w-4 h-4" />} |
|
{isToolInKit(tool.name) ? 'In Toolkit' : 'Add to Toolkit'} |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
))} |
|
</div> |
|
</div> |
|
))} |
|
</div> |
|
); |
|
}; |
|
|
|
const renderMyToolkit = () => { |
|
if (myToolkit.length === 0) { |
|
return ( |
|
<div className="text-center bg-white rounded-xl shadow-md border border-stone-200 p-12"> |
|
<ToolboxIcon className="mx-auto h-16 w-16 text-stone-400" /> |
|
<h3 className="mt-4 text-xl font-semibold text-stone-800">Your Toolkit is Empty</h3> |
|
<p className="mt-2 text-stone-600">Add tools from the "Tool Encyclopedia" tab to start managing your collection.</p> |
|
</div> |
|
) |
|
} |
|
|
|
return ( |
|
<div className="space-y-4"> |
|
{myToolkit.map(tool => ( |
|
<div key={tool.id} className="bg-white rounded-xl shadow-md border border-stone-200 p-4 transition-shadow hover:shadow-lg"> |
|
<div className="flex flex-col sm:flex-row gap-4"> |
|
<div className="flex-1"> |
|
<p className="font-bold text-lg text-stone-900">{tool.name}</p> |
|
<p className="text-sm text-stone-500">{tool.description}</p> |
|
</div> |
|
<div className="flex flex-col sm:items-end gap-2"> |
|
<select |
|
value={tool.condition} |
|
onChange={(e) => handleUpdateTool(tool.id, { condition: e.target.value as ToolCondition })} |
|
className={`w-full sm:w-auto text-sm font-medium border-0 rounded-md shadow-sm focus:ring-2 focus:ring-offset-2 ${conditionColors[tool.condition].bg} ${conditionColors[tool.condition].text} ${conditionColors[tool.condition].ring}`} |
|
> |
|
{Object.values(ToolCondition).map(c => <option key={c} value={c}>{c}</option>)} |
|
</select> |
|
<p className="text-xs text-stone-500"> |
|
Last Maintained: {tool.lastMaintained ? new Date(tool.lastMaintained).toLocaleDateString() : 'N/A'} |
|
</p> |
|
</div> |
|
</div> |
|
<div className="mt-4 pt-4 border-t border-stone-100 flex flex-col sm:flex-row items-center gap-4"> |
|
<div className="flex-1 w-full"> |
|
<label htmlFor={`notes-${tool.id}`} className="text-xs font-bold text-stone-500 uppercase">Notes</label> |
|
<textarea |
|
id={`notes-${tool.id}`} |
|
rows={2} |
|
placeholder="Add notes about this tool..." |
|
value={tool.notes || ''} |
|
onChange={(e) => handleUpdateTool(tool.id, { notes: e.target.value })} |
|
className="block mt-1 w-full rounded-md border-stone-300 shadow-sm focus:border-green-500 focus:ring-green-500 sm:text-sm text-stone-800" |
|
/> |
|
</div> |
|
<div className="flex-shrink-0 flex sm:flex-col items-center gap-2 w-full sm:w-auto"> |
|
<button onClick={() => handleUpdateTool(tool.id, { lastMaintained: new Date().toISOString() })} className="w-full sm:w-auto flex items-center justify-center gap-2 text-sm font-semibold bg-blue-100 text-blue-700 hover:bg-blue-200 px-3 py-2 rounded-md transition-colors">Log Maintenance</button> |
|
<button onClick={() => handleShowMaintenanceTips(tool)} disabled={!aiConfigured} className="w-full sm:w-auto flex items-center justify-center gap-2 text-sm font-semibold bg-stone-100 text-stone-700 hover:bg-stone-200 px-3 py-2 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"><InfoIcon className="w-4 h-4"/>Tips</button> |
|
<button onClick={() => handleRemoveFromToolkit(tool.id)} className="w-full sm:w-auto flex items-center justify-center gap-2 text-sm font-semibold bg-red-100 text-red-700 hover:bg-red-200 px-3 py-2 rounded-md transition-colors"><Trash2Icon className="w-4 h-4"/>Remove</button> |
|
</div> |
|
</div> |
|
</div> |
|
))} |
|
</div> |
|
) |
|
}; |
|
|
|
const renderModal = () => ( |
|
<div className={`fixed inset-0 z-50 flex items-center justify-center transition-opacity ${modalOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'}`}> |
|
<div className="absolute inset-0 bg-black/50" onClick={() => setModalOpen(false)}></div> |
|
<div className="bg-white rounded-2xl shadow-xl w-full max-w-lg m-4 p-6 relative transform transition-transform scale-95" style={{transform: modalOpen ? 'scale(1)' : 'scale(0.95)'}}> |
|
{modalLoading ? <Spinner text="Yuki is fetching maintenance tips..." /> : ( |
|
modalContent && <> |
|
<h3 className="text-2xl font-bold text-stone-900">Maintenance for {modalContent.toolName}</h3> |
|
<div className="mt-4 space-y-4 text-stone-600"> |
|
<div> |
|
<h4 className="font-semibold text-stone-800">Cleaning</h4> |
|
<p>{modalContent.tips.cleaning}</p> |
|
</div> |
|
<div> |
|
<h4 className="font-semibold text-stone-800">Sharpening</h4> |
|
<p>{modalContent.tips.sharpening}</p> |
|
</div> |
|
<div> |
|
<h4 className="font-semibold text-stone-800">Storage</h4> |
|
<p>{modalContent.tips.storage}</p> |
|
</div> |
|
</div> |
|
<button onClick={() => setModalOpen(false)} className="mt-6 w-full bg-green-700 text-white font-semibold py-2 px-4 rounded-lg hover:bg-green-600 transition-colors"> |
|
Close |
|
</button> |
|
</>)} |
|
</div> |
|
</div> |
|
); |
|
|
|
return ( |
|
<div className="space-y-8 max-w-5xl mx-auto"> |
|
<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"> |
|
<WrenchIcon className="w-8 h-8 text-gray-600" /> |
|
Bonsai Tool Shed |
|
</h2> |
|
<p className="mt-4 text-lg leading-8 text-stone-600"> |
|
Explore the encyclopedia of bonsai tools and manage your personal toolkit. |
|
</p> |
|
</header> |
|
|
|
<div className="bg-white p-2 rounded-xl shadow-lg border border-stone-200"> |
|
<div className="flex gap-2"> |
|
<button onClick={() => setActiveTab('encyclopedia')} className={`flex-1 flex items-center justify-center gap-2 px-4 py-2 rounded-lg text-sm font-semibold transition-colors ${activeTab === 'encyclopedia' ? 'bg-green-700 text-white shadow' : 'bg-transparent text-stone-600 hover:bg-stone-100'}`}> |
|
<BookOpenIcon className="w-5 h-5" /> Tool Encyclopedia |
|
</button> |
|
<button onClick={() => setActiveTab('toolkit')} className={`flex-1 flex items-center justify-center gap-2 px-4 py-2 rounded-lg text-sm font-semibold transition-colors ${activeTab === 'toolkit' ? 'bg-green-700 text-white shadow' : 'bg-transparent text-stone-600 hover:bg-stone-100'}`}> |
|
<ToolboxIcon className="w-5 h-5" /> My Toolkit ({myToolkit.length}) |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<div> |
|
{activeTab === 'encyclopedia' ? renderEncyclopedia() : renderMyToolkit()} |
|
</div> |
|
|
|
{renderModal()} |
|
</div> |
|
); |
|
}; |
|
|
|
export default ToolGuideView; |