Spaces:
Running
Running
File size: 10,621 Bytes
3d06096 8a42480 3d06096 f4fff4e b3b23c0 3d06096 5b0d8fb 3d06096 8a42480 f4fff4e 3d06096 f4fff4e 3d06096 f4fff4e 8a42480 b3b23c0 8a42480 b3b23c0 8a42480 3d06096 8a42480 3d06096 8a42480 f4fff4e 8a42480 3d06096 5b0d8fb 3d06096 5b0d8fb 3d06096 5b0d8fb 3d06096 f4fff4e 3d06096 5b0d8fb 3d06096 5b0d8fb 3d06096 5b0d8fb 3d06096 b3b23c0 3d06096 b3b23c0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 | 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; |