Yuki / views /ToolGuideView.tsx
Severian's picture
Upload 43 files
be02369 verified
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;