wuhp's picture
Update components/Sidebar.tsx
5b8143b verified
import React from 'react';
import { LAYER_DEFINITIONS } from '../constants';
import { LayerType } from '../types';
import { Box, Sparkles, LayoutTemplate, Circle, Search, X, ChevronLeft, ChevronRight } from 'lucide-react';
import GoogleAd from './GoogleAd';
interface SidebarProps {
onOpenAIBuilder: () => void;
onSelectTemplate: (templateId: string) => void;
onBackToHome: () => void;
isConnected: boolean;
isOpen: boolean;
onToggle: () => void;
}
const Sidebar: React.FC<SidebarProps> = ({ onOpenAIBuilder, onSelectTemplate, onBackToHome, isConnected, isOpen, onToggle }) => {
const [searchQuery, setSearchQuery] = React.useState('');
const onDragStart = (event: React.DragEvent, layerType: LayerType) => {
event.dataTransfer.setData('application/reactflow', layerType);
event.dataTransfer.effectAllowed = 'move';
};
const categories = Array.from(new Set(Object.values(LAYER_DEFINITIONS).map(l => l.category)));
const categoryOrder = [
'Core', 'Convolution', 'Preprocessing', 'Recurrent', 'Transformer', 'Normalization', 'GenAI',
'Video', 'Audio', '3D', 'Detection', 'OCR', 'Robotics',
'Graph', 'Physics', 'Spiking', 'RL', 'Advanced',
'Utility', 'Merge'
];
categories.sort((a, b) => {
const idxA = categoryOrder.indexOf(a);
const idxB = categoryOrder.indexOf(b);
if (idxA === -1) return 1;
if (idxB === -1) return -1;
return idxA - idxB;
});
const filteredLayers = Object.values(LAYER_DEFINITIONS).filter(layer =>
layer.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
layer.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
layer.type.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<>
{/* Mobile Backdrop */}
{isOpen && (
<div
className="fixed inset-0 bg-black/60 z-30 md:hidden backdrop-blur-sm transition-opacity"
onClick={onToggle}
/>
)}
{/* Sidebar Container */}
<aside className={`
fixed inset-y-0 left-0 z-40 bg-slate-900 border-r border-slate-800 flex flex-col h-full shadow-2xl transition-all duration-300
${isOpen ? 'translate-x-0 w-64' : '-translate-x-full w-64 md:translate-x-0 md:w-0 md:border-none'}
md:relative md:shadow-none
`}>
{/* Desktop Toggle Button (Chevron) */}
<button
onClick={onToggle}
className={`
hidden md:flex absolute -right-3 top-6 bg-slate-800 border border-slate-700 text-slate-400 p-0.5 rounded-full hover:text-white hover:bg-slate-700 cursor-pointer z-50 w-6 h-6 items-center justify-center shadow-md transition-opacity duration-300
${isOpen ? 'opacity-100' : 'opacity-100 translate-x-3'}
`}
title={isOpen ? "Collapse Sidebar" : "Expand Sidebar"}
>
{isOpen ? <ChevronLeft size={14} /> : <ChevronRight size={14} />}
</button>
{/* Content Container */}
<div className={`flex flex-col h-full overflow-hidden whitespace-nowrap ${!isOpen ? 'md:opacity-0 md:invisible' : 'opacity-100 visible'} transition-all duration-200`}>
<div className="p-4 border-b border-slate-800 bg-slate-900 space-y-4 min-w-[16rem]">
<div className="flex justify-between items-start">
<div
onClick={onBackToHome}
className="cursor-pointer group"
title="Return to Home"
>
<div className="flex items-center gap-3 mb-1">
<img
src="https://huggingface.co/spaces/wuhp/testarcbuilder/resolve/main/public/logo.png"
alt="wuhp"
className="w-10 h-10 object-contain transition-transform group-hover:scale-110"
/>
<h1 className="text-xl font-bold bg-gradient-to-r from-blue-400 to-violet-400 bg-clip-text text-transparent">
wuhp
</h1>
</div>
<p className="text-xs text-slate-500 pl-8 group-hover:text-slate-400 transition-colors">Visual AI Architect</p>
</div>
{/* Mobile Close Button */}
<button onClick={onToggle} className="md:hidden text-slate-500 hover:text-white">
<X size={20} />
</button>
</div>
<div className="grid grid-cols-2 gap-2">
<button
onClick={onOpenAIBuilder}
className="flex flex-col items-center justify-center p-2 bg-purple-500/10 hover:bg-purple-500/20 border border-purple-500/30 rounded-lg text-purple-300 transition-colors group"
>
<Sparkles size={18} className="mb-1 group-hover:scale-110 transition-transform" />
<span className="text-[10px] font-bold">AI Builder</span>
</button>
<button
onClick={() => onSelectTemplate('menu')}
className="flex flex-col items-center justify-center p-2 bg-blue-500/10 hover:bg-blue-500/20 border border-blue-500/30 rounded-lg text-blue-300 transition-colors group"
>
<LayoutTemplate size={18} className="mb-1 group-hover:scale-110 transition-transform" />
<span className="text-[10px] font-bold">Templates</span>
</button>
</div>
{/* Search Bar */}
<div className="relative">
<Search className="absolute left-3 top-2.5 text-slate-500" size={14} />
<input
type="text"
placeholder="Search layers..."
className="w-full bg-slate-950 border border-slate-700 rounded-lg pl-9 pr-8 py-2 text-xs text-slate-200 focus:outline-none focus:border-blue-500 transition-colors placeholder-slate-600"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
{searchQuery && (
<button
onClick={() => setSearchQuery('')}
className="absolute right-2 top-2 text-slate-500 hover:text-slate-300"
>
<X size={14} />
</button>
)}
</div>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-6 scrollbar-thin scrollbar-thumb-slate-700 min-w-[16rem]">
{searchQuery ? (
// Search Results View
<div className="space-y-2">
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-widest mb-3">
Search Results ({filteredLayers.length})
</h3>
{filteredLayers.length > 0 ? (
filteredLayers.map(layer => (
<div
key={layer.type}
className="bg-slate-800 hover:bg-slate-750 p-3 rounded border border-slate-700 cursor-grab active:cursor-grabbing transition-colors group relative overflow-hidden"
onDragStart={(event) => onDragStart(event, layer.type)}
draggable
>
<div className="flex items-center gap-3 relative z-10">
<div className={`p-1.5 rounded transition-colors group-hover:bg-slate-900 bg-slate-900/50 shrink-0`}>
<Box size={14} className="text-slate-400 group-hover:text-blue-400" />
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-slate-200 group-hover:text-white truncate">{layer.label}</div>
<div className="text-xs text-slate-500 leading-tight group-hover:text-slate-400 whitespace-normal mt-0.5">{layer.description}</div>
</div>
</div>
</div>
))
) : (
<div className="text-center text-slate-500 py-8 text-xs italic">
No layers found matching "{searchQuery}"
</div>
)}
</div>
) : (
// Categorized View
categories.map(category => (
<div key={category}>
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-widest mb-3 flex items-center gap-2">
{category}
<div className="h-px flex-1 bg-slate-800"></div>
</h3>
<div className="grid grid-cols-1 gap-2">
{Object.values(LAYER_DEFINITIONS)
.filter(l => l.category === category)
.map(layer => (
<div
key={layer.type}
className="bg-slate-800 hover:bg-slate-750 p-3 rounded border border-slate-700 cursor-grab active:cursor-grabbing transition-colors group relative overflow-hidden"
onDragStart={(event) => onDragStart(event, layer.type)}
draggable
>
<div className="flex items-center gap-3 relative z-10">
<div className={`p-1.5 rounded transition-colors group-hover:bg-slate-900 bg-slate-900/50 shrink-0`}>
<Box size={14} className="text-slate-400 group-hover:text-blue-400" />
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-slate-200 group-hover:text-white truncate">{layer.label}</div>
<div className="text-xs text-slate-500 leading-tight group-hover:text-slate-400 whitespace-normal mt-0.5">{layer.description}</div>
</div>
</div>
</div>
))}
</div>
</div>
))
)}
{/* Sidebar Ad Spot - Only render when open to avoid 0-width errors */}
{isOpen && (
<div className="pt-4 border-t border-slate-800/50">
<div className="text-[10px] text-slate-600 mb-2 uppercase tracking-wider font-semibold text-center">Sponsored</div>
<GoogleAd />
</div>
)}
</div>
<div className="p-4 border-t border-slate-800 text-[10px] text-slate-500 text-center flex items-center justify-center gap-2 min-w-[16rem]">
<span>v1.3.0 • Powered by Gemini 2.5</span>
<Circle size={8} className={isConnected ? "fill-emerald-500 text-emerald-500" : "fill-red-500 text-red-500"} />
</div>
</div>
</aside>
</>
);
};
export default Sidebar;