pynode-flow 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. pynode/__init__.py +37 -0
  2. pynode/__main__.py +9 -0
  3. pynode/_perf.py +65 -0
  4. pynode/_version.py +24 -0
  5. pynode/api/__init__.py +15 -0
  6. pynode/api/helpers.py +79 -0
  7. pynode/api/nodes.py +423 -0
  8. pynode/api/services.py +124 -0
  9. pynode/api/sse.py +58 -0
  10. pynode/api/uploads.py +59 -0
  11. pynode/api/workflows.py +466 -0
  12. pynode/config.py +93 -0
  13. pynode/install_nodes.py +143 -0
  14. pynode/main.py +118 -0
  15. pynode/node_registry.py +156 -0
  16. pynode/nodes/AutoSyncNode/auto_sync_node.py +151 -0
  17. pynode/nodes/BBoxMetricsNode/__init__.py +3 -0
  18. pynode/nodes/BBoxMetricsNode/bbox_metrics_node.py +250 -0
  19. pynode/nodes/BBoxMetricsNode/requirements.txt +1 -0
  20. pynode/nodes/BatchNode/__init__.py +3 -0
  21. pynode/nodes/BatchNode/batch_node.py +86 -0
  22. pynode/nodes/BatchNode/requirements.txt +1 -0
  23. pynode/nodes/CameraNode/__init__.py +4 -0
  24. pynode/nodes/CameraNode/camera_node.py +204 -0
  25. pynode/nodes/CameraNode/requirements.txt +2 -0
  26. pynode/nodes/ChangeNode/__init__.py +1 -0
  27. pynode/nodes/ChangeNode/change_node.py +255 -0
  28. pynode/nodes/ChangeNode/requirements.txt +1 -0
  29. pynode/nodes/ConfidenceFilterNode/__init__.py +4 -0
  30. pynode/nodes/ConfidenceFilterNode/confidence_filter_node.py +208 -0
  31. pynode/nodes/ConfidenceFilterNode/requirements.txt +1 -0
  32. pynode/nodes/CounterNode/__init__.py +3 -0
  33. pynode/nodes/CounterNode/counter_node.py +191 -0
  34. pynode/nodes/CropNode/__init__.py +4 -0
  35. pynode/nodes/CropNode/crop_node.py +317 -0
  36. pynode/nodes/CropNode/requirements.txt +2 -0
  37. pynode/nodes/DebugNode/__init__.py +4 -0
  38. pynode/nodes/DebugNode/debug_node.py +169 -0
  39. pynode/nodes/DebugNode/requirements.txt +1 -0
  40. pynode/nodes/DelayNode/delay_node.py +218 -0
  41. pynode/nodes/DelayNode/requirements.txt +1 -0
  42. pynode/nodes/DrawPredictionsNode/__init__.py +3 -0
  43. pynode/nodes/DrawPredictionsNode/draw_predictions_node.py +365 -0
  44. pynode/nodes/DrawPredictionsNode/requirements.txt +2 -0
  45. pynode/nodes/ErrorNode/error_node.py +94 -0
  46. pynode/nodes/ErrorNode/requirements.txt +1 -0
  47. pynode/nodes/FilterNode/__init__.py +3 -0
  48. pynode/nodes/FilterNode/filter_node.py +102 -0
  49. pynode/nodes/FilterNode/requirements.txt +1 -0
  50. pynode/nodes/FrameSourceNode/__init__.py +4 -0
  51. pynode/nodes/FrameSourceNode/frame_source_node.py +319 -0
  52. pynode/nodes/FrameSourceNode/requirements.txt +1 -0
  53. pynode/nodes/FunctionNode/__init__.py +3 -0
  54. pynode/nodes/FunctionNode/function_node.py +168 -0
  55. pynode/nodes/FunctionNode/requirements.txt +1 -0
  56. pynode/nodes/GateNode/gate_node.py +60 -0
  57. pynode/nodes/GateNode/requirements.txt +1 -0
  58. pynode/nodes/GetiUploadNode/__init__.py +3 -0
  59. pynode/nodes/GetiUploadNode/geti_upload_node.py +278 -0
  60. pynode/nodes/GetiUploadNode/requirements.txt +1 -0
  61. pynode/nodes/ImageFormatNode/__init__.py +4 -0
  62. pynode/nodes/ImageFormatNode/image_format_node.py +310 -0
  63. pynode/nodes/ImageFormatNode/requirements.txt +2 -0
  64. pynode/nodes/ImageUploadNode/image_upload_node.py +96 -0
  65. pynode/nodes/ImageViewerNode/__init__.py +4 -0
  66. pynode/nodes/ImageViewerNode/image_viewer_node.py +162 -0
  67. pynode/nodes/ImageViewerNode/requirements.txt +2 -0
  68. pynode/nodes/ImageWriterNode/__init__.py +3 -0
  69. pynode/nodes/ImageWriterNode/imagewriter_node.py +352 -0
  70. pynode/nodes/ImageWriterNode/requirements.txt +1 -0
  71. pynode/nodes/InferenceNode/InferenceEngine/__init__.py +15 -0
  72. pynode/nodes/InferenceNode/InferenceEngine/device_detection.py +149 -0
  73. pynode/nodes/InferenceNode/InferenceEngine/engines/base_engine.py +155 -0
  74. pynode/nodes/InferenceNode/InferenceEngine/engines/example_engine_template.py +272 -0
  75. pynode/nodes/InferenceNode/InferenceEngine/engines/geti_engine.py +309 -0
  76. pynode/nodes/InferenceNode/InferenceEngine/engines/onnx_engine.py +346 -0
  77. pynode/nodes/InferenceNode/InferenceEngine/engines/pass_engine.py +86 -0
  78. pynode/nodes/InferenceNode/InferenceEngine/engines/ultralytics_engine.py +863 -0
  79. pynode/nodes/InferenceNode/InferenceEngine/inference_engine_factory.py +533 -0
  80. pynode/nodes/InferenceNode/InferenceEngine/readme.md +107 -0
  81. pynode/nodes/InferenceNode/InferenceEngine/result_converters.py +121 -0
  82. pynode/nodes/InferenceNode/inference_node.py +451 -0
  83. pynode/nodes/InferenceNode/requirements.txt +13 -0
  84. pynode/nodes/InjectNode/inject_node.py +245 -0
  85. pynode/nodes/InjectNode/requirements.txt +1 -0
  86. pynode/nodes/JoinNode/__init__.py +3 -0
  87. pynode/nodes/JoinNode/join_node.py +156 -0
  88. pynode/nodes/JoinNode/requirements.txt +1 -0
  89. pynode/nodes/LabelFilterNode/__init__.py +3 -0
  90. pynode/nodes/LabelFilterNode/label_filter_node.py +188 -0
  91. pynode/nodes/LabelFilterNode/requirements.txt +1 -0
  92. pynode/nodes/MQTTNode/__init__.py +8 -0
  93. pynode/nodes/MQTTNode/mqtt_in_node.py +148 -0
  94. pynode/nodes/MQTTNode/mqtt_out_node.py +157 -0
  95. pynode/nodes/MQTTNode/mqtt_service.py +367 -0
  96. pynode/nodes/MQTTNode/requirements.txt +1 -0
  97. pynode/nodes/MessageWriterNode/__init__.py +7 -0
  98. pynode/nodes/MessageWriterNode/messagereader_node.py +568 -0
  99. pynode/nodes/MessageWriterNode/messagewriter_node.py +505 -0
  100. pynode/nodes/OmronCameraNode/__init__.py +3 -0
  101. pynode/nodes/OmronCameraNode/omron_camera_node.py +652 -0
  102. pynode/nodes/OmronCameraNode/requirements.txt +4 -0
  103. pynode/nodes/OpenCV/__init__.py +1 -0
  104. pynode/nodes/OpenCV/arithmetic_node.py +376 -0
  105. pynode/nodes/OpenCV/bitwise_node.py +127 -0
  106. pynode/nodes/OpenCV/blend_node.py +127 -0
  107. pynode/nodes/OpenCV/blob_detector_node.py +233 -0
  108. pynode/nodes/OpenCV/blur_node.py +141 -0
  109. pynode/nodes/OpenCV/color_space_node.py +139 -0
  110. pynode/nodes/OpenCV/colormap_node.py +262 -0
  111. pynode/nodes/OpenCV/contours_node.py +258 -0
  112. pynode/nodes/OpenCV/denormalize_coords_node.py +365 -0
  113. pynode/nodes/OpenCV/draw_node.py +312 -0
  114. pynode/nodes/OpenCV/edge_detector_node.py +177 -0
  115. pynode/nodes/OpenCV/equirectangular_node.py +447 -0
  116. pynode/nodes/OpenCV/fft_node.py +102 -0
  117. pynode/nodes/OpenCV/focus_estimation_node.py +189 -0
  118. pynode/nodes/OpenCV/frequency_filter_node.py +241 -0
  119. pynode/nodes/OpenCV/histogram_node.py +160 -0
  120. pynode/nodes/OpenCV/hough_circles_node.py +224 -0
  121. pynode/nodes/OpenCV/hough_lines_node.py +243 -0
  122. pynode/nodes/OpenCV/in_range_node.py +186 -0
  123. pynode/nodes/OpenCV/morphology_node.py +136 -0
  124. pynode/nodes/OpenCV/normalize_coords_node.py +344 -0
  125. pynode/nodes/OpenCV/paste_node.py +244 -0
  126. pynode/nodes/OpenCV/perspective_node.py +174 -0
  127. pynode/nodes/OpenCV/realsense_depth_node.py +253 -0
  128. pynode/nodes/OpenCV/requirements.txt +2 -0
  129. pynode/nodes/OpenCV/resize_node.py +175 -0
  130. pynode/nodes/OpenCV/rotate_node.py +159 -0
  131. pynode/nodes/OpenCV/template_match_node.py +227 -0
  132. pynode/nodes/OpenCV/threshold_node.py +167 -0
  133. pynode/nodes/PointInShapeNode/__init__.py +3 -0
  134. pynode/nodes/PointInShapeNode/point_in_shape_node.py +313 -0
  135. pynode/nodes/PointInShapeNode/requirements.txt +2 -0
  136. pynode/nodes/PolygonMetricsNode/__init__.py +3 -0
  137. pynode/nodes/PolygonMetricsNode/polygon_metrics_node.py +331 -0
  138. pynode/nodes/PolygonMetricsNode/requirements.txt +2 -0
  139. pynode/nodes/QueueLengthProbeNode/__init__.py +3 -0
  140. pynode/nodes/QueueLengthProbeNode/queue_length_probe_node.py +116 -0
  141. pynode/nodes/Qwen3VLMNode/__init__.py +3 -0
  142. pynode/nodes/Qwen3VLMNode/qwen3_vlm_node.py +519 -0
  143. pynode/nodes/Qwen3VLMNode/requirements.txt +5 -0
  144. pynode/nodes/RESTEndpointNode/__init__.py +3 -0
  145. pynode/nodes/RESTEndpointNode/rest_endpoint_node.py +491 -0
  146. pynode/nodes/RangeNode/__init__.py +3 -0
  147. pynode/nodes/RangeNode/range_node.py +115 -0
  148. pynode/nodes/RangeNode/requirements.txt +1 -0
  149. pynode/nodes/RateProbeNode/__init__.py +3 -0
  150. pynode/nodes/RateProbeNode/rate_probe_node.py +183 -0
  151. pynode/nodes/RateProbeNode/requirements.txt +1 -0
  152. pynode/nodes/RoboflowUploadNode/__init__.py +3 -0
  153. pynode/nodes/RoboflowUploadNode/requirements.txt +1 -0
  154. pynode/nodes/RoboflowUploadNode/roboflow_upload_node.py +229 -0
  155. pynode/nodes/SliceNode/README.md +149 -0
  156. pynode/nodes/SliceNode/__init__.py +5 -0
  157. pynode/nodes/SliceNode/merge_predictions_node.py +444 -0
  158. pynode/nodes/SliceNode/requirements.txt +3 -0
  159. pynode/nodes/SliceNode/slice_collector_node.py +370 -0
  160. pynode/nodes/SliceNode/slice_image_node.py +380 -0
  161. pynode/nodes/SplitNode/__init__.py +3 -0
  162. pynode/nodes/SplitNode/requirements.txt +1 -0
  163. pynode/nodes/SplitNode/split_node.py +155 -0
  164. pynode/nodes/SwitchNode/requirements.txt +1 -0
  165. pynode/nodes/SwitchNode/switch_node.py +259 -0
  166. pynode/nodes/SyncNode/__init__.py +3 -0
  167. pynode/nodes/SyncNode/sync_node.py +287 -0
  168. pynode/nodes/TemplateNode/requirements.txt +1 -0
  169. pynode/nodes/TemplateNode/template_node.py +86 -0
  170. pynode/nodes/TrackerNode/__init__.py +2 -0
  171. pynode/nodes/TrackerNode/supervision_tracker_node.py +248 -0
  172. pynode/nodes/TrackerNode/tracker_node.py +745 -0
  173. pynode/nodes/UltralyticsNode/__init__.py +4 -0
  174. pynode/nodes/UltralyticsNode/requirements.txt +2 -0
  175. pynode/nodes/UltralyticsNode/ultralytics_node.py +364 -0
  176. pynode/nodes/UnknownNode/__init__.py +93 -0
  177. pynode/nodes/VLLMNode/__init__.py +3 -0
  178. pynode/nodes/VLLMNode/vllm_node.py +345 -0
  179. pynode/nodes/VideoReaderNode/__init__.py +3 -0
  180. pynode/nodes/VideoReaderNode/video_reader_node.py +425 -0
  181. pynode/nodes/VideoWriterNode/__init__.py +3 -0
  182. pynode/nodes/VideoWriterNode/video_writer_node.py +441 -0
  183. pynode/nodes/WebhookNode/__init__.py +3 -0
  184. pynode/nodes/WebhookNode/webhook_node.py +591 -0
  185. pynode/nodes/__init__.py +109 -0
  186. pynode/nodes/base_node.py +562 -0
  187. pynode/nodes/image_utils.py +291 -0
  188. pynode/nodes/info.py +109 -0
  189. pynode/nodes/mDNSNode/__init__.py +13 -0
  190. pynode/nodes/mDNSNode/mdns_broadcast_node.py +214 -0
  191. pynode/nodes/mDNSNode/mdns_discovery_node.py +362 -0
  192. pynode/nodes/mDNSNode/mdns_manager.py +399 -0
  193. pynode/nodes/mDNSNode/requirements.txt +1 -0
  194. pynode/nodes/messages.py +81 -0
  195. pynode/server.py +187 -0
  196. pynode/static/images/favicon4.png +0 -0
  197. pynode/static/index.html +200 -0
  198. pynode/static/js/README.md +139 -0
  199. pynode/static/js/auth.js +89 -0
  200. pynode/static/js/clipboard.js +152 -0
  201. pynode/static/js/config.js +14 -0
  202. pynode/static/js/connections.js +844 -0
  203. pynode/static/js/debug.js +524 -0
  204. pynode/static/js/events.js +775 -0
  205. pynode/static/js/history.js +199 -0
  206. pynode/static/js/main.js +55 -0
  207. pynode/static/js/minimap.js +242 -0
  208. pynode/static/js/nodes.js +995 -0
  209. pynode/static/js/palette.js +255 -0
  210. pynode/static/js/properties.js +1270 -0
  211. pynode/static/js/selection.js +241 -0
  212. pynode/static/js/state.js +211 -0
  213. pynode/static/js/ui-utils.js +28 -0
  214. pynode/static/js/viewport.js +204 -0
  215. pynode/static/js/workflow.js +469 -0
  216. pynode/static/js/workflows.js +344 -0
  217. pynode/static/style.css +2751 -0
  218. pynode/workflow_engine.py +483 -0
  219. pynode/workflow_manager.py +440 -0
  220. pynode_flow-0.2.0.dist-info/METADATA +520 -0
  221. pynode_flow-0.2.0.dist-info/RECORD +227 -0
  222. pynode_flow-0.2.0.dist-info/WHEEL +5 -0
  223. pynode_flow-0.2.0.dist-info/entry_points.txt +3 -0
  224. pynode_flow-0.2.0.dist-info/licenses/LICENSE +21 -0
  225. pynode_flow-0.2.0.dist-info/scm_file_list.json +270 -0
  226. pynode_flow-0.2.0.dist-info/scm_version.json +8 -0
  227. pynode_flow-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,200 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>PyNode - Visual Workflow Editor</title>
7
+ <link rel="icon" type="image/png" href="images/favicon4.png">
8
+ <link rel="stylesheet" href="style.css">
9
+ </head>
10
+ <body>
11
+ <div class="container">
12
+ <!-- Toast notification -->
13
+ <div id="toast" class="toast"></div>
14
+
15
+ <!-- Header -->
16
+ <header class="header">
17
+ <h1><img src="images/favicon4.png" alt="PyNode" class="header-logo">PyNode Editor<span id="app-version" class="app-version"></span></h1>
18
+ <div class="header-actions">
19
+ <div class="deploy-container">
20
+ <button id="deploy-btn" class="btn btn-primary deploy-main" disabled>
21
+ <span id="deploy-mode-icon" class="deploy-btn-icon">◐</span>
22
+ Deploy
23
+ </button>
24
+ <button id="deploy-dropdown-btn" class="btn btn-primary deploy-arrow">▼</button>
25
+ <div id="deploy-dropdown" class="deploy-dropdown hidden">
26
+ <button id="deploy-modified-btn" class="menu-item deploy-option active">
27
+ <span class="deploy-icon">◐</span>
28
+ <span class="deploy-text">
29
+ <span class="deploy-label">Modified Nodes</span>
30
+ <span class="deploy-desc">Only deploy nodes that have changed</span>
31
+ </span>
32
+ </button>
33
+ <button id="deploy-full-btn" class="menu-item deploy-option">
34
+ <span class="deploy-icon">●</span>
35
+ <span class="deploy-text">
36
+ <span class="deploy-label">Full</span>
37
+ <span class="deploy-desc">Deploy all nodes, restarting the entire flow</span>
38
+ </span>
39
+ </button>
40
+ <div class="deploy-separator"></div>
41
+ <button id="deploy-restart-btn" class="menu-item deploy-option">
42
+ <span class="deploy-icon">↻</span>
43
+ <span class="deploy-text">
44
+ <span class="deploy-label">Restart</span>
45
+ <span class="deploy-desc">Restart all nodes without redeploying</span>
46
+ </span>
47
+ </button>
48
+ <button id="deploy-stop-btn" class="menu-item deploy-option">
49
+ <span class="deploy-icon">■</span>
50
+ <span class="deploy-text">
51
+ <span class="deploy-label">Stop</span>
52
+ <span class="deploy-desc">Stop processing; Deploy to start again</span>
53
+ </span>
54
+ </button>
55
+ </div>
56
+ </div>
57
+ <div class="menu-container">
58
+ <button id="menu-btn" class="btn btn-secondary" title="Menu">☰</button>
59
+ <div id="menu-dropdown" class="menu-dropdown hidden">
60
+ <button id="clear-btn" class="menu-item">Clear Workflow</button>
61
+
62
+ <div class="menu-submenu">
63
+ <button id="export-btn" class="menu-item submenu-toggle">◁ Export</button>
64
+ <div id="export-submenu" class="submenu-dropdown hidden">
65
+ <button id="export-flow-btn" class="menu-item">Flow</button>
66
+ <button id="export-selected-btn" class="menu-item">Selected Nodes</button>
67
+ </div>
68
+ </div>
69
+
70
+ <button id="import-btn" class="menu-item">Import</button>
71
+ </div>
72
+ </div>
73
+ </div>
74
+ </header>
75
+
76
+ <!-- Main content -->
77
+ <div class="main-content">
78
+ <!-- Node Palette -->
79
+ <aside class="palette" id="left-palette">
80
+ <div class="palette-scroll">
81
+ <h3>Node Palette</h3>
82
+ <div class="palette-search-container">
83
+ <input type="text" id="palette-search" class="palette-search" placeholder="Filter nodes..." />
84
+ <button id="palette-clear-btn" class="search-clear-btn" aria-label="Clear search">✕</button>
85
+ </div>
86
+ <div id="node-palette" class="node-list">
87
+ <!-- Nodes will be loaded here -->
88
+ </div>
89
+ </div>
90
+ <div class="panel-grab panel-grab-left" id="left-palette-grab" title="Drag to resize">
91
+ <button class="panel-toggle-btn" id="left-palette-toggle" type="button" aria-label="Toggle palette"></button>
92
+ </div>
93
+ </aside>
94
+
95
+ <!-- Canvas area with workflow tabs -->
96
+ <div class="canvas-area">
97
+ <!-- Workflow Tabs -->
98
+ <div class="workflow-tabs" id="workflow-tabs">
99
+ <!-- Tabs are rendered dynamically -->
100
+ <button class="workflow-tab-add" id="add-workflow-btn" title="New Workflow">+</button>
101
+ </div>
102
+
103
+ <!-- Canvas -->
104
+ <main class="canvas-container">
105
+ <!-- canvas-viewport is the zoom root: viewport.js applies
106
+ transform: scale(k) here so the connections SVG and the
107
+ HTML nodes layer zoom together -->
108
+ <div id="canvas-viewport">
109
+ <svg id="canvas" width="100%" height="100%">
110
+ <!-- <defs>
111
+ <marker id="arrowhead" markerWidth="10" markerHeight="7"
112
+ refX="0" refY="3.5" orient="auto" markerUnits="strokeWidth">
113
+ <polygon points="0 0, 10 3.5, 0 7" fill="#999" />
114
+ </marker>
115
+ </defs> -->
116
+ <g id="connections"></g>
117
+ <g id="temp-line"></g>
118
+ </svg>
119
+ <div id="nodes-container"></div>
120
+ </div>
121
+ </main>
122
+
123
+ <!-- Canvas overlay controls: minimap + zoom -->
124
+ <div class="canvas-controls" id="canvas-controls">
125
+ <div id="minimap-panel" class="minimap-panel hidden">
126
+ <canvas id="minimap-canvas" width="220" height="150"></canvas>
127
+ </div>
128
+ <div class="canvas-controls-bar">
129
+ <button id="zoom-out-btn" class="canvas-ctrl-btn" title="Zoom out (Ctrl+wheel)">−</button>
130
+ <button id="zoom-reset-btn" class="canvas-ctrl-btn zoom-readout" title="Reset zoom">100%</button>
131
+ <button id="zoom-in-btn" class="canvas-ctrl-btn" title="Zoom in (Ctrl+wheel)">+</button>
132
+ <button id="minimap-toggle-btn" class="canvas-ctrl-btn" title="Toggle minimap">▦</button>
133
+ </div>
134
+ </div>
135
+ </div>
136
+
137
+ <!-- Properties Panel -->
138
+ <aside class="properties hidden" id="properties-panel-container">
139
+ <div class="resize-handle" id="properties-resize-handle"></div>
140
+ <div class="properties-header">
141
+ <h3>Properties</h3>
142
+ <button class="properties-close-btn" id="close-properties-btn" title="Close Properties">✕</button>
143
+ </div>
144
+ <div id="properties-panel">
145
+ <p class="placeholder">Select a node to edit properties</p>
146
+ </div>
147
+ </aside>
148
+
149
+ <!-- Right Sidebar (permanent) -->
150
+ <aside class="right-sidebar" id="right-sidebar">
151
+ <div class="sidebar-tabs">
152
+ <button class="sidebar-tab active" data-panel="debug" title="Debug">🐛</button>
153
+ <button class="sidebar-tab" data-panel="info" title="Info">ℹ️</button>
154
+ </div>
155
+ <div class="sidebar-content">
156
+ <!-- Debug Panel -->
157
+ <div class="sidebar-panel active" id="debug-panel">
158
+ <div class="debug-header">
159
+ <h3>Debug Messages</h3>
160
+ <div class="debug-controls">
161
+ <button id="pause-debug-btn" class="btn btn-secondary btn-small icon-btn" title="Pause updates"></button>
162
+ <button id="clear-debug-btn" class="btn btn-secondary btn-small" title="Clear">🗑️</button>
163
+ </div>
164
+ </div>
165
+ <div class="debug-filters">
166
+ <button class="filter-toggle-btn active" id="filter-info" data-filter="info">
167
+ Info
168
+ </button>
169
+ <button class="filter-toggle-btn active" id="filter-errors" data-filter="errors">
170
+ Errors
171
+ </button>
172
+ <button class="filter-toggle-btn" id="collapse-similar" data-filter="collapse">
173
+ Collapse
174
+ </button>
175
+ </div>
176
+ <div id="debug-messages"></div>
177
+ </div>
178
+ <!-- Info Panel -->
179
+ <div class="sidebar-panel" id="info-panel">
180
+ <div class="info-header">
181
+ <h3>Information</h3>
182
+ </div>
183
+ <div class="info-content">
184
+ <p class="placeholder">Select a node to see information</p>
185
+ </div>
186
+ </div>
187
+ </div>
188
+ <div class="panel-grab panel-grab-right" id="right-sidebar-grab" title="Drag to resize">
189
+ <button class="panel-toggle-btn" id="right-sidebar-toggle" type="button" aria-label="Toggle sidebar"></button>
190
+ </div>
191
+ </aside>
192
+ </div>
193
+ </div>
194
+
195
+ <!-- auth.js must load first (classic script, runs during parse) so its
196
+ fetch wrapper covers every API call made by the module bundle -->
197
+ <script src="js/auth.js"></script>
198
+ <script type="module" src="js/main.js"></script>
199
+ </body>
200
+ </html>
@@ -0,0 +1,139 @@
1
+ # PyNode Frontend Architecture
2
+
3
+ The frontend has been restructured into modular ES6 modules for better maintainability and organization.
4
+
5
+ ## Directory Structure
6
+
7
+ ```
8
+ static/
9
+ ├── index.html # Main HTML file
10
+ ├── style.css # All styles
11
+ ├── app.js # Legacy monolithic file (can be removed)
12
+ └── js/ # Modular JavaScript
13
+ ├── main.js # Application entry point
14
+ ├── config.js # Configuration and constants
15
+ ├── state.js # State management
16
+ ├── ui-utils.js # UI utility functions
17
+ ├── palette.js # Node palette rendering
18
+ ├── nodes.js # Node creation and rendering
19
+ ├── connections.js # Connection management
20
+ ├── selection.js # Node selection logic
21
+ ├── properties.js # Properties panel
22
+ ├── workflow.js # Workflow import/export/deploy
23
+ ├── debug.js # Debug panel and SSE
24
+ └── events.js # Event handlers
25
+ ```
26
+
27
+ ## Module Overview
28
+
29
+ ### `main.js` - Entry Point
30
+ - Initializes the application
31
+ - Exposes functions to window for inline event handlers
32
+ - Coordinates module loading
33
+
34
+ ### `config.js` - Configuration
35
+ - API base URL
36
+ - Node categories definition
37
+ - Global constants
38
+
39
+ ### `state.js` - State Management
40
+ - Central state object
41
+ - State modification functions
42
+ - Node ID generation
43
+
44
+ ### `ui-utils.js` - UI Utilities
45
+ - Toast notifications
46
+ - Other UI helper functions
47
+
48
+ ### `palette.js` - Node Palette
49
+ - Loads available node types from API
50
+ - Renders node palette by category
51
+ - Handles drag-and-drop initiation
52
+
53
+ ### `nodes.js` - Node Management
54
+ - Node creation (client-side)
55
+ - Node rendering
56
+ - Node deletion
57
+ - Node drag-and-drop handling
58
+
59
+ ### `connections.js` - Connection Management
60
+ - Connection creation and rendering
61
+ - Connection drawing (temp lines)
62
+ - Connection deletion
63
+ - Connection updates on node movement
64
+
65
+ ### `selection.js` - Selection Logic
66
+ - Single and multi-node selection
67
+ - Selection box handling
68
+ - Deselection
69
+
70
+ ### `properties.js` - Properties Panel
71
+ - Property panel rendering
72
+ - Node property updates
73
+ - Node config updates
74
+ - Gate toggle (special case)
75
+ - Action triggers (inject, etc.)
76
+
77
+ ### `workflow.js` - Workflow Operations
78
+ - Load workflow from API
79
+ - Deploy workflow to backend
80
+ - Clear workflow
81
+ - Import/export JSON files
82
+
83
+ ### `debug.js` - Debug Panel
84
+ - SSE connection for debug messages
85
+ - Image viewer updates via SSE
86
+ - Debug message display
87
+
88
+ ### `events.js` - Event Handling
89
+ - Canvas event listeners
90
+ - Keyboard shortcuts
91
+ - Selection box logic
92
+ - Drag-and-drop handling
93
+
94
+ ## Key Design Decisions
95
+
96
+ ### ES6 Modules
97
+ - Clean imports/exports
98
+ - Better dependency management
99
+ - Tree-shaking potential
100
+ - Modern JavaScript practices
101
+
102
+ ### Circular Dependency Prevention
103
+ - Dynamic imports used where needed (`import().then()`)
104
+ - Careful module organization
105
+ - Clear separation of concerns
106
+
107
+ ### Window Globals
108
+ Some functions are exposed to `window` for inline event handlers in dynamically generated HTML:
109
+ - `updateNodeProperty()`
110
+ - `updateNodeConfig()`
111
+ - `triggerNodeAction()`
112
+ - `toggleGate()`
113
+
114
+ This is necessary because the properties panel HTML is generated dynamically with inline `onchange` handlers.
115
+
116
+ ## Benefits of New Structure
117
+
118
+ 1. **Maintainability**: Each module has a single responsibility
119
+ 2. **Testability**: Modules can be tested independently
120
+ 3. **Reusability**: Functions are organized logically
121
+ 4. **Scalability**: Easy to add new features in appropriate modules
122
+ 5. **Readability**: ~100-300 lines per file instead of 1000+ lines
123
+ 6. **Collaboration**: Multiple developers can work on different modules
124
+
125
+ ## Migration Notes
126
+
127
+ - The old `app.js` is no longer used
128
+ - All functionality is preserved
129
+ - No API changes required
130
+ - Works with existing backend
131
+ - Module loading is automatic via `type="module"` in HTML
132
+
133
+ ## Future Improvements
134
+
135
+ - Consider removing inline event handlers in favor of event delegation
136
+ - Add TypeScript for better type safety
137
+ - Implement unit tests for each module
138
+ - Add build step for production (minification, bundling)
139
+ - Consider state management library (Redux, MobX) if state grows more complex
@@ -0,0 +1,89 @@
1
+ // Optional API key support for PyNode.
2
+ //
3
+ // Loaded as a CLASSIC script before the js/main.js module bundle (classic
4
+ // scripts execute during parse; module scripts are deferred), so the fetch
5
+ // wrapper below is installed before any application code runs. Every
6
+ // frontend module calls the global fetch() with '/api/...' URLs, so
7
+ // wrapping window.fetch here covers ALL API requests (JSON, FormData
8
+ // uploads, DELETEs, ...) without touching individual call sites.
9
+ //
10
+ // When the server has no API key configured it never returns 401, so none
11
+ // of this activates and behavior is identical to before.
12
+ (function () {
13
+ 'use strict';
14
+
15
+ var STORAGE_KEY = 'pynode_api_key';
16
+ var nativeFetch = window.fetch.bind(window);
17
+ var prompting = false; // Only ever prompt once (page reloads on success)
18
+
19
+ function getStoredKey() {
20
+ try {
21
+ return window.localStorage.getItem(STORAGE_KEY) || '';
22
+ } catch (e) {
23
+ return '';
24
+ }
25
+ }
26
+
27
+ function storeKey(key) {
28
+ try {
29
+ window.localStorage.setItem(STORAGE_KEY, key);
30
+ } catch (e) {
31
+ // localStorage unavailable; the key is lost on reload but the
32
+ // user will simply be prompted again.
33
+ }
34
+ }
35
+
36
+ function isApiRequest(input) {
37
+ var url = (typeof input === 'string') ? input : ((input && input.url) || '');
38
+ try {
39
+ var u = new URL(url, window.location.href);
40
+ return u.origin === window.location.origin && u.pathname.indexOf('/api/') === 0;
41
+ } catch (e) {
42
+ return false;
43
+ }
44
+ }
45
+
46
+ function withApiKey(init, key) {
47
+ var next = Object.assign({}, init);
48
+ var headers = new Headers((init && init.headers) || undefined);
49
+ headers.set('X-API-Key', key);
50
+ next.headers = headers;
51
+ return next;
52
+ }
53
+
54
+ window.fetch = function (input, init) {
55
+ if (!isApiRequest(input)) {
56
+ return nativeFetch(input, init);
57
+ }
58
+ var key = getStoredKey();
59
+ return nativeFetch(input, key ? withApiKey(init, key) : init).then(function (response) {
60
+ if (response.status !== 401 || prompting) {
61
+ return response;
62
+ }
63
+ prompting = true;
64
+ var entered = window.prompt(
65
+ 'This PyNode server requires an API key.\nEnter the API key:');
66
+ if (!entered) {
67
+ prompting = false;
68
+ return response; // User cancelled; surface the 401 as-is
69
+ }
70
+ storeKey(entered);
71
+ // Verify the key against the failed request, then reload so
72
+ // every in-flight/initial request reruns with the key attached.
73
+ return nativeFetch(input, withApiKey(init, entered)).then(function (retry) {
74
+ if (retry.status === 401) {
75
+ prompting = false;
76
+ window.alert('API key rejected by the server.');
77
+ return retry;
78
+ }
79
+ window.location.reload();
80
+ // Hold callers until the reload tears the page down.
81
+ return new Promise(function () {});
82
+ });
83
+ });
84
+ };
85
+
86
+ // For consumers that cannot set headers (EventSource / SSE): returns the
87
+ // stored key so it can be passed as an api_key query parameter.
88
+ window.pynodeApiKey = getStoredKey;
89
+ })();
@@ -0,0 +1,152 @@
1
+ // Clipboard management for copy, cut, paste operations
2
+ import { state, generateNodeId, setModified, markNodeAdded } from './state.js';
3
+ import { renderNode, generateUniqueName } from './nodes.js';
4
+ import { createConnection } from './connections.js';
5
+ import { selectNode, deselectAllNodes } from './selection.js';
6
+
7
+ // Clipboard state
8
+ const clipboard = {
9
+ nodes: [],
10
+ connections: [],
11
+ isCut: false
12
+ };
13
+
14
+ /**
15
+ * Copy selected nodes to clipboard
16
+ */
17
+ export function copySelectedNodes() {
18
+ if (state.selectedNodes.size === 0) {
19
+ console.log('No nodes selected to copy');
20
+ return;
21
+ }
22
+
23
+ clipboard.nodes = [];
24
+ clipboard.connections = [];
25
+ clipboard.isCut = false;
26
+
27
+ // Copy node data. Deep-clone so the clipboard is an independent snapshot -
28
+ // a shallow copy shares the nested `config` object with the live node, so
29
+ // later edits to either would mutate both (and every paste of it).
30
+ state.selectedNodes.forEach(nodeId => {
31
+ const node = state.nodes.get(nodeId);
32
+ if (node) {
33
+ const snapshot = structuredClone(node);
34
+ snapshot.originalId = node.id;
35
+ clipboard.nodes.push(snapshot);
36
+ }
37
+ });
38
+
39
+ // Copy connections between selected nodes
40
+ state.connections.forEach(conn => {
41
+ if (state.selectedNodes.has(conn.source) && state.selectedNodes.has(conn.target)) {
42
+ clipboard.connections.push({ ...conn });
43
+ }
44
+ });
45
+
46
+ console.log(`Copied ${clipboard.nodes.length} nodes and ${clipboard.connections.length} connections`);
47
+ }
48
+
49
+ /**
50
+ * Cut selected nodes to clipboard
51
+ */
52
+ export function cutSelectedNodes() {
53
+ if (state.selectedNodes.size === 0) {
54
+ console.log('No nodes selected to cut');
55
+ return;
56
+ }
57
+
58
+ // First copy the nodes
59
+ copySelectedNodes();
60
+ clipboard.isCut = true;
61
+
62
+ // Then delete them
63
+ const nodesToDelete = Array.from(state.selectedNodes);
64
+
65
+ // Import deleteNode dynamically
66
+ import('./nodes.js').then(({ deleteNode }) => {
67
+ nodesToDelete.forEach(nodeId => {
68
+ deleteNode(nodeId);
69
+ });
70
+ });
71
+
72
+ console.log(`Cut ${clipboard.nodes.length} nodes`);
73
+ }
74
+
75
+ /**
76
+ * Paste nodes from clipboard
77
+ */
78
+ export function pasteNodes() {
79
+ if (clipboard.nodes.length === 0) {
80
+ console.log('Clipboard is empty');
81
+ return;
82
+ }
83
+
84
+ // Clear current selection synchronously. (A dynamic import().then() runs as
85
+ // a microtask AFTER the paste loop below has already selected the new
86
+ // nodes, so it would wipe out that fresh selection.)
87
+ deselectAllNodes();
88
+
89
+ // Map old IDs to new IDs
90
+ const idMap = new Map();
91
+
92
+ // Calculate paste offset (20px down and right from original position).
93
+ // Node x/y and this offset are all CANVAS coordinates, so pasting is
94
+ // zoom-independent by construction - no client->canvas conversion needed.
95
+ const pasteOffset = clipboard.isCut ? 0 : 20;
96
+
97
+ // Create new nodes
98
+ clipboard.nodes.forEach(nodeData => {
99
+ const newId = generateNodeId();
100
+ idMap.set(nodeData.originalId, newId);
101
+
102
+ // Generate unique name for pasted node
103
+ const uniqueName = generateUniqueName(nodeData.name, nodeData.type);
104
+
105
+ // Deep-clone so each pasted node gets its own `config` object rather
106
+ // than sharing the clipboard snapshot's (which repeated pastes would
107
+ // otherwise alias, leaking edits between the copies).
108
+ const newNode = {
109
+ ...structuredClone(nodeData),
110
+ id: newId,
111
+ name: uniqueName,
112
+ x: nodeData.x + pasteOffset,
113
+ y: nodeData.y + pasteOffset
114
+ };
115
+
116
+ delete newNode.originalId;
117
+
118
+ state.nodes.set(newId, newNode);
119
+ renderNode(newNode);
120
+ markNodeAdded(newId);
121
+
122
+ // Select the new node
123
+ selectNode(newId, true);
124
+ });
125
+
126
+ // Recreate connections with new IDs
127
+ clipboard.connections.forEach(conn => {
128
+ const newSourceId = idMap.get(conn.source);
129
+ const newTargetId = idMap.get(conn.target);
130
+
131
+ if (newSourceId && newTargetId) {
132
+ createConnection(newSourceId, newTargetId, conn.sourceOutput, conn.targetInput);
133
+ }
134
+ });
135
+
136
+ // If this was a cut operation, clear the clipboard
137
+ if (clipboard.isCut) {
138
+ clipboard.nodes = [];
139
+ clipboard.connections = [];
140
+ clipboard.isCut = false;
141
+ }
142
+
143
+ setModified(true);
144
+ console.log(`Pasted ${idMap.size} nodes`);
145
+ }
146
+
147
+ /**
148
+ * Check if clipboard has content
149
+ */
150
+ export function hasClipboardContent() {
151
+ return clipboard.nodes.length > 0;
152
+ }
@@ -0,0 +1,14 @@
1
+ // Configuration and constants
2
+ // Use relative path for API to work across different hosts and ports
3
+ export const API_BASE = '/api';
4
+
5
+ export const NODE_CATEGORIES = {
6
+ 'common': { title: 'Common', nodes: [] },
7
+ 'input': { title: 'Input', nodes: [] },
8
+ 'output': { title: 'Output', nodes: [] },
9
+ 'network': { title: 'Network', nodes: [] },
10
+ 'vision': { title: 'Vision', nodes: [] },
11
+ 'logic': { title: 'Logic', nodes: [] },
12
+ 'system': { title: 'System', nodes: [] },
13
+ 'custom': { title: 'Custom', nodes: [] }
14
+ };