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