Spaces:
Running
Running
| import React from 'react'; | |
| import { Node } from 'reactflow'; | |
| import { NodeData, LayerDefinition, LogEntry } from '../types'; | |
| import { LAYER_DEFINITIONS } from '../constants'; | |
| import { X, Trash2, Activity, Info, CheckCircle, AlertTriangle, AlertOctagon, ChevronDown } from 'lucide-react'; | |
| import GoogleAd from './GoogleAd'; | |
| interface PropertiesPanelProps { | |
| selectedNode: Node<NodeData> | null; | |
| onChange: (id: string, newData: Partial<NodeData>) => void; | |
| onDelete: (id: string) => void; | |
| onClose: () => void; | |
| logs?: LogEntry[]; | |
| isOpen: boolean; // Mobile visibility state | |
| } | |
| const PropertiesPanel: React.FC<PropertiesPanelProps> = ({ selectedNode, onChange, onDelete, onClose, logs = [], isOpen }) => { | |
| const containerClasses = ` | |
| bg-slate-900 flex flex-col transition-all duration-300 z-30 shadow-2xl | |
| fixed inset-x-0 bottom-0 h-[60vh] w-full border-t border-slate-700 rounded-t-xl | |
| ${isOpen ? 'translate-y-0' : 'translate-y-full'} | |
| md:relative md:inset-auto md:h-full md:w-80 md:border-l md:border-t-0 md:rounded-none md:translate-y-0 md:shadow-none | |
| `; | |
| if (!selectedNode) { | |
| return ( | |
| <div className={containerClasses}> | |
| <div className="p-4 border-b border-slate-800 flex items-center justify-between"> | |
| <div className="flex items-center gap-2"> | |
| <Activity size={18} className="text-blue-400"/> | |
| <h2 className="text-sm font-bold text-slate-200 uppercase tracking-wider">System Activity</h2> | |
| </div> | |
| {/* Mobile Close Handle */} | |
| <button onClick={onClose} className="md:hidden text-slate-500 hover:text-white"> | |
| <ChevronDown size={20} /> | |
| </button> | |
| </div> | |
| <div className="flex-1 overflow-y-auto p-4 space-y-3 scrollbar-thin scrollbar-thumb-slate-700"> | |
| {logs.length === 0 ? ( | |
| <div className="flex flex-col items-center justify-center text-slate-500 h-64"> | |
| <div className="w-12 h-12 rounded-full bg-slate-800 mb-3 flex items-center justify-center"> | |
| <Activity size={24} className="opacity-20"/> | |
| </div> | |
| <p className="text-xs text-center">No activity recorded yet.</p> | |
| </div> | |
| ) : ( | |
| logs.map(log => ( | |
| <div key={log.id} className="bg-slate-800/50 rounded border border-slate-800 p-3 animate-in fade-in slide-in-from-top-1 duration-300"> | |
| <div className="flex justify-between items-start mb-1"> | |
| <div className="flex items-center gap-2"> | |
| {log.type === 'info' && <Info size={12} className="text-blue-400" />} | |
| {log.type === 'success' && <CheckCircle size={12} className="text-emerald-400" />} | |
| {log.type === 'warning' && <AlertTriangle size={12} className="text-amber-400" />} | |
| {log.type === 'error' && <AlertOctagon size={12} className="text-red-400" />} | |
| <span className="text-[10px] font-bold text-slate-500 uppercase">{log.type}</span> | |
| </div> | |
| <span className="text-[10px] text-slate-600 font-mono"> | |
| {log.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })} | |
| </span> | |
| </div> | |
| <p className="text-xs text-slate-300 leading-relaxed">{log.message}</p> | |
| </div> | |
| )) | |
| )} | |
| <div className="pt-4 text-center"> | |
| <p className="text-[10px] text-slate-600 mb-4"> | |
| Select a node on the canvas to configure parameters. | |
| </p> | |
| <div className="border-t border-slate-800/50 pt-2"> | |
| <GoogleAd className="min-h-[100px]" /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| const definition: LayerDefinition | undefined = LAYER_DEFINITIONS[selectedNode.data.type]; | |
| if (!definition) { | |
| return ( | |
| <div className={containerClasses}> | |
| <div className="p-4 border-b border-slate-800 flex justify-between items-center"> | |
| <h2 className="text-lg font-bold text-slate-100">Unknown Layer</h2> | |
| <button onClick={onClose}><X size={18} className="text-slate-500 hover:text-white"/></button> | |
| </div> | |
| <div className="p-4 flex-1"> | |
| <div className="bg-red-500/10 border border-red-500/20 text-red-400 p-3 rounded text-sm mb-4"> | |
| Error: Layer definition not found for type "{selectedNode.data.type}". | |
| This may happen if an imported template uses deprecated types. | |
| </div> | |
| <button | |
| onClick={() => onDelete(selectedNode.id)} | |
| className="w-full flex items-center justify-center gap-2 bg-red-500/10 hover:bg-red-500/20 text-red-500 py-2 rounded transition-colors text-sm font-medium border border-red-500/20" | |
| > | |
| <Trash2 size={16} /> | |
| Delete Node | |
| </button> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| const handleParamChange = (name: string, value: any, type: string) => { | |
| let parsedValue = value; | |
| if (type === 'number') parsedValue = Number(value); | |
| if (type === 'boolean') parsedValue = value === 'true'; | |
| // Update only the params object within data | |
| onChange(selectedNode.id, { | |
| params: { | |
| ...selectedNode.data.params, | |
| [name]: parsedValue | |
| } | |
| }); | |
| }; | |
| const handleLabelChange = (newLabel: string) => { | |
| onChange(selectedNode.id, { label: newLabel }); | |
| }; | |
| return ( | |
| <div className={containerClasses}> | |
| <div className="p-4 border-b border-slate-800 flex justify-between items-center"> | |
| <div> | |
| <h2 className="text-lg font-bold text-slate-100">{definition.label}</h2> | |
| <p className="text-xs text-slate-500 font-mono">{selectedNode.id}</p> | |
| </div> | |
| <button onClick={onClose} className="text-slate-500 hover:text-slate-300"> | |
| <X size={18} /> | |
| </button> | |
| </div> | |
| <div className="flex-1 overflow-y-auto p-4 space-y-5"> | |
| <div className="text-sm text-slate-400 italic bg-slate-800/50 p-3 rounded border border-slate-800"> | |
| {definition.description} | |
| </div> | |
| {/* Node Label Renaming */} | |
| <div className="space-y-1.5"> | |
| <label className="block text-xs font-semibold text-slate-300 uppercase tracking-wide"> | |
| Node Label (Name) | |
| </label> | |
| <input | |
| type="text" | |
| className="w-full bg-slate-950 border border-slate-700 rounded px-3 py-2 text-sm text-slate-200 focus:ring-1 focus:ring-blue-500 outline-none placeholder-slate-700 font-medium" | |
| value={selectedNode.data.label} | |
| onChange={(e) => handleLabelChange(e.target.value)} | |
| /> | |
| </div> | |
| <div className="h-px bg-slate-800 my-4" /> | |
| <div className="space-y-4"> | |
| {definition.parameters.map((param) => ( | |
| <div key={param.name} className="space-y-1.5"> | |
| <label className="block text-xs font-semibold text-slate-300 uppercase tracking-wide"> | |
| {param.label} | |
| </label> | |
| {param.type === 'select' ? ( | |
| <select | |
| className="w-full bg-slate-950 border border-slate-700 rounded px-3 py-2 text-sm text-slate-200 focus:ring-1 focus:ring-blue-500 outline-none" | |
| value={selectedNode.data.params[param.name] || param.default} | |
| onChange={(e) => handleParamChange(param.name, e.target.value, param.type)} | |
| > | |
| {param.options?.map(opt => ( | |
| <option key={opt} value={opt}>{opt}</option> | |
| ))} | |
| </select> | |
| ) : param.type === 'boolean' ? ( | |
| <select | |
| className="w-full bg-slate-950 border border-slate-700 rounded px-3 py-2 text-sm text-slate-200 focus:ring-1 focus:ring-blue-500 outline-none" | |
| value={String(selectedNode.data.params[param.name] ?? param.default)} | |
| onChange={(e) => handleParamChange(param.name, e.target.value === 'true', param.type)} | |
| > | |
| <option value="true">True</option> | |
| <option value="false">False</option> | |
| </select> | |
| ) : param.type === 'text' ? ( | |
| <textarea | |
| className="w-full h-32 bg-slate-950 border border-slate-700 rounded px-3 py-2 text-xs font-mono text-slate-300 focus:ring-1 focus:ring-blue-500 outline-none placeholder-slate-700 resize-y" | |
| value={selectedNode.data.params[param.name] ?? param.default} | |
| onChange={(e) => handleParamChange(param.name, e.target.value, param.type)} | |
| placeholder={param.description} | |
| spellCheck={false} | |
| /> | |
| ) : ( | |
| <input | |
| type={param.type === 'number' ? 'number' : 'text'} | |
| className="w-full bg-slate-950 border border-slate-700 rounded px-3 py-2 text-sm text-slate-200 focus:ring-1 focus:ring-blue-500 outline-none placeholder-slate-700" | |
| value={selectedNode.data.params[param.name] ?? param.default} | |
| onChange={(e) => handleParamChange(param.name, e.target.value, param.type)} | |
| placeholder={param.description} | |
| /> | |
| )} | |
| </div> | |
| ))} | |
| {definition.parameters.length === 0 && ( | |
| <p className="text-sm text-slate-500 text-center py-4">This layer has no configurable parameters.</p> | |
| )} | |
| {/* Properties Panel Ad Spot */} | |
| <div className="pt-4 border-t border-slate-800/50 mt-4"> | |
| <GoogleAd /> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="p-4 border-t border-slate-800"> | |
| <button | |
| onClick={() => onDelete(selectedNode.id)} | |
| className="w-full flex items-center justify-center gap-2 bg-red-500/10 hover:bg-red-500/20 text-red-500 py-2 rounded transition-colors text-sm font-medium border border-red-500/20" | |
| > | |
| <Trash2 size={16} /> | |
| Delete Node | |
| </button> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default PropertiesPanel; |