Spaces:
Running
Running
| 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; | |