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.
- pynode/__init__.py +37 -0
- pynode/__main__.py +9 -0
- pynode/_perf.py +65 -0
- pynode/_version.py +24 -0
- pynode/api/__init__.py +15 -0
- pynode/api/helpers.py +79 -0
- pynode/api/nodes.py +423 -0
- pynode/api/services.py +124 -0
- pynode/api/sse.py +58 -0
- pynode/api/uploads.py +59 -0
- pynode/api/workflows.py +466 -0
- pynode/config.py +93 -0
- pynode/install_nodes.py +143 -0
- pynode/main.py +118 -0
- pynode/node_registry.py +156 -0
- pynode/nodes/AutoSyncNode/auto_sync_node.py +151 -0
- pynode/nodes/BBoxMetricsNode/__init__.py +3 -0
- pynode/nodes/BBoxMetricsNode/bbox_metrics_node.py +250 -0
- pynode/nodes/BBoxMetricsNode/requirements.txt +1 -0
- pynode/nodes/BatchNode/__init__.py +3 -0
- pynode/nodes/BatchNode/batch_node.py +86 -0
- pynode/nodes/BatchNode/requirements.txt +1 -0
- pynode/nodes/CameraNode/__init__.py +4 -0
- pynode/nodes/CameraNode/camera_node.py +204 -0
- pynode/nodes/CameraNode/requirements.txt +2 -0
- pynode/nodes/ChangeNode/__init__.py +1 -0
- pynode/nodes/ChangeNode/change_node.py +255 -0
- pynode/nodes/ChangeNode/requirements.txt +1 -0
- pynode/nodes/ConfidenceFilterNode/__init__.py +4 -0
- pynode/nodes/ConfidenceFilterNode/confidence_filter_node.py +208 -0
- pynode/nodes/ConfidenceFilterNode/requirements.txt +1 -0
- pynode/nodes/CounterNode/__init__.py +3 -0
- pynode/nodes/CounterNode/counter_node.py +191 -0
- pynode/nodes/CropNode/__init__.py +4 -0
- pynode/nodes/CropNode/crop_node.py +317 -0
- pynode/nodes/CropNode/requirements.txt +2 -0
- pynode/nodes/DebugNode/__init__.py +4 -0
- pynode/nodes/DebugNode/debug_node.py +169 -0
- pynode/nodes/DebugNode/requirements.txt +1 -0
- pynode/nodes/DelayNode/delay_node.py +218 -0
- pynode/nodes/DelayNode/requirements.txt +1 -0
- pynode/nodes/DrawPredictionsNode/__init__.py +3 -0
- pynode/nodes/DrawPredictionsNode/draw_predictions_node.py +365 -0
- pynode/nodes/DrawPredictionsNode/requirements.txt +2 -0
- pynode/nodes/ErrorNode/error_node.py +94 -0
- pynode/nodes/ErrorNode/requirements.txt +1 -0
- pynode/nodes/FilterNode/__init__.py +3 -0
- pynode/nodes/FilterNode/filter_node.py +102 -0
- pynode/nodes/FilterNode/requirements.txt +1 -0
- pynode/nodes/FrameSourceNode/__init__.py +4 -0
- pynode/nodes/FrameSourceNode/frame_source_node.py +319 -0
- pynode/nodes/FrameSourceNode/requirements.txt +1 -0
- pynode/nodes/FunctionNode/__init__.py +3 -0
- pynode/nodes/FunctionNode/function_node.py +168 -0
- pynode/nodes/FunctionNode/requirements.txt +1 -0
- pynode/nodes/GateNode/gate_node.py +60 -0
- pynode/nodes/GateNode/requirements.txt +1 -0
- pynode/nodes/GetiUploadNode/__init__.py +3 -0
- pynode/nodes/GetiUploadNode/geti_upload_node.py +278 -0
- pynode/nodes/GetiUploadNode/requirements.txt +1 -0
- pynode/nodes/ImageFormatNode/__init__.py +4 -0
- pynode/nodes/ImageFormatNode/image_format_node.py +310 -0
- pynode/nodes/ImageFormatNode/requirements.txt +2 -0
- pynode/nodes/ImageUploadNode/image_upload_node.py +96 -0
- pynode/nodes/ImageViewerNode/__init__.py +4 -0
- pynode/nodes/ImageViewerNode/image_viewer_node.py +162 -0
- pynode/nodes/ImageViewerNode/requirements.txt +2 -0
- pynode/nodes/ImageWriterNode/__init__.py +3 -0
- pynode/nodes/ImageWriterNode/imagewriter_node.py +352 -0
- pynode/nodes/ImageWriterNode/requirements.txt +1 -0
- pynode/nodes/InferenceNode/InferenceEngine/__init__.py +15 -0
- pynode/nodes/InferenceNode/InferenceEngine/device_detection.py +149 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/base_engine.py +155 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/example_engine_template.py +272 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/geti_engine.py +309 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/onnx_engine.py +346 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/pass_engine.py +86 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/ultralytics_engine.py +863 -0
- pynode/nodes/InferenceNode/InferenceEngine/inference_engine_factory.py +533 -0
- pynode/nodes/InferenceNode/InferenceEngine/readme.md +107 -0
- pynode/nodes/InferenceNode/InferenceEngine/result_converters.py +121 -0
- pynode/nodes/InferenceNode/inference_node.py +451 -0
- pynode/nodes/InferenceNode/requirements.txt +13 -0
- pynode/nodes/InjectNode/inject_node.py +245 -0
- pynode/nodes/InjectNode/requirements.txt +1 -0
- pynode/nodes/JoinNode/__init__.py +3 -0
- pynode/nodes/JoinNode/join_node.py +156 -0
- pynode/nodes/JoinNode/requirements.txt +1 -0
- pynode/nodes/LabelFilterNode/__init__.py +3 -0
- pynode/nodes/LabelFilterNode/label_filter_node.py +188 -0
- pynode/nodes/LabelFilterNode/requirements.txt +1 -0
- pynode/nodes/MQTTNode/__init__.py +8 -0
- pynode/nodes/MQTTNode/mqtt_in_node.py +148 -0
- pynode/nodes/MQTTNode/mqtt_out_node.py +157 -0
- pynode/nodes/MQTTNode/mqtt_service.py +367 -0
- pynode/nodes/MQTTNode/requirements.txt +1 -0
- pynode/nodes/MessageWriterNode/__init__.py +7 -0
- pynode/nodes/MessageWriterNode/messagereader_node.py +568 -0
- pynode/nodes/MessageWriterNode/messagewriter_node.py +505 -0
- pynode/nodes/OmronCameraNode/__init__.py +3 -0
- pynode/nodes/OmronCameraNode/omron_camera_node.py +652 -0
- pynode/nodes/OmronCameraNode/requirements.txt +4 -0
- pynode/nodes/OpenCV/__init__.py +1 -0
- pynode/nodes/OpenCV/arithmetic_node.py +376 -0
- pynode/nodes/OpenCV/bitwise_node.py +127 -0
- pynode/nodes/OpenCV/blend_node.py +127 -0
- pynode/nodes/OpenCV/blob_detector_node.py +233 -0
- pynode/nodes/OpenCV/blur_node.py +141 -0
- pynode/nodes/OpenCV/color_space_node.py +139 -0
- pynode/nodes/OpenCV/colormap_node.py +262 -0
- pynode/nodes/OpenCV/contours_node.py +258 -0
- pynode/nodes/OpenCV/denormalize_coords_node.py +365 -0
- pynode/nodes/OpenCV/draw_node.py +312 -0
- pynode/nodes/OpenCV/edge_detector_node.py +177 -0
- pynode/nodes/OpenCV/equirectangular_node.py +447 -0
- pynode/nodes/OpenCV/fft_node.py +102 -0
- pynode/nodes/OpenCV/focus_estimation_node.py +189 -0
- pynode/nodes/OpenCV/frequency_filter_node.py +241 -0
- pynode/nodes/OpenCV/histogram_node.py +160 -0
- pynode/nodes/OpenCV/hough_circles_node.py +224 -0
- pynode/nodes/OpenCV/hough_lines_node.py +243 -0
- pynode/nodes/OpenCV/in_range_node.py +186 -0
- pynode/nodes/OpenCV/morphology_node.py +136 -0
- pynode/nodes/OpenCV/normalize_coords_node.py +344 -0
- pynode/nodes/OpenCV/paste_node.py +244 -0
- pynode/nodes/OpenCV/perspective_node.py +174 -0
- pynode/nodes/OpenCV/realsense_depth_node.py +253 -0
- pynode/nodes/OpenCV/requirements.txt +2 -0
- pynode/nodes/OpenCV/resize_node.py +175 -0
- pynode/nodes/OpenCV/rotate_node.py +159 -0
- pynode/nodes/OpenCV/template_match_node.py +227 -0
- pynode/nodes/OpenCV/threshold_node.py +167 -0
- pynode/nodes/PointInShapeNode/__init__.py +3 -0
- pynode/nodes/PointInShapeNode/point_in_shape_node.py +313 -0
- pynode/nodes/PointInShapeNode/requirements.txt +2 -0
- pynode/nodes/PolygonMetricsNode/__init__.py +3 -0
- pynode/nodes/PolygonMetricsNode/polygon_metrics_node.py +331 -0
- pynode/nodes/PolygonMetricsNode/requirements.txt +2 -0
- pynode/nodes/QueueLengthProbeNode/__init__.py +3 -0
- pynode/nodes/QueueLengthProbeNode/queue_length_probe_node.py +116 -0
- pynode/nodes/Qwen3VLMNode/__init__.py +3 -0
- pynode/nodes/Qwen3VLMNode/qwen3_vlm_node.py +519 -0
- pynode/nodes/Qwen3VLMNode/requirements.txt +5 -0
- pynode/nodes/RESTEndpointNode/__init__.py +3 -0
- pynode/nodes/RESTEndpointNode/rest_endpoint_node.py +491 -0
- pynode/nodes/RangeNode/__init__.py +3 -0
- pynode/nodes/RangeNode/range_node.py +115 -0
- pynode/nodes/RangeNode/requirements.txt +1 -0
- pynode/nodes/RateProbeNode/__init__.py +3 -0
- pynode/nodes/RateProbeNode/rate_probe_node.py +183 -0
- pynode/nodes/RateProbeNode/requirements.txt +1 -0
- pynode/nodes/RoboflowUploadNode/__init__.py +3 -0
- pynode/nodes/RoboflowUploadNode/requirements.txt +1 -0
- pynode/nodes/RoboflowUploadNode/roboflow_upload_node.py +229 -0
- pynode/nodes/SliceNode/README.md +149 -0
- pynode/nodes/SliceNode/__init__.py +5 -0
- pynode/nodes/SliceNode/merge_predictions_node.py +444 -0
- pynode/nodes/SliceNode/requirements.txt +3 -0
- pynode/nodes/SliceNode/slice_collector_node.py +370 -0
- pynode/nodes/SliceNode/slice_image_node.py +380 -0
- pynode/nodes/SplitNode/__init__.py +3 -0
- pynode/nodes/SplitNode/requirements.txt +1 -0
- pynode/nodes/SplitNode/split_node.py +155 -0
- pynode/nodes/SwitchNode/requirements.txt +1 -0
- pynode/nodes/SwitchNode/switch_node.py +259 -0
- pynode/nodes/SyncNode/__init__.py +3 -0
- pynode/nodes/SyncNode/sync_node.py +287 -0
- pynode/nodes/TemplateNode/requirements.txt +1 -0
- pynode/nodes/TemplateNode/template_node.py +86 -0
- pynode/nodes/TrackerNode/__init__.py +2 -0
- pynode/nodes/TrackerNode/supervision_tracker_node.py +248 -0
- pynode/nodes/TrackerNode/tracker_node.py +745 -0
- pynode/nodes/UltralyticsNode/__init__.py +4 -0
- pynode/nodes/UltralyticsNode/requirements.txt +2 -0
- pynode/nodes/UltralyticsNode/ultralytics_node.py +364 -0
- pynode/nodes/UnknownNode/__init__.py +93 -0
- pynode/nodes/VLLMNode/__init__.py +3 -0
- pynode/nodes/VLLMNode/vllm_node.py +345 -0
- pynode/nodes/VideoReaderNode/__init__.py +3 -0
- pynode/nodes/VideoReaderNode/video_reader_node.py +425 -0
- pynode/nodes/VideoWriterNode/__init__.py +3 -0
- pynode/nodes/VideoWriterNode/video_writer_node.py +441 -0
- pynode/nodes/WebhookNode/__init__.py +3 -0
- pynode/nodes/WebhookNode/webhook_node.py +591 -0
- pynode/nodes/__init__.py +109 -0
- pynode/nodes/base_node.py +562 -0
- pynode/nodes/image_utils.py +291 -0
- pynode/nodes/info.py +109 -0
- pynode/nodes/mDNSNode/__init__.py +13 -0
- pynode/nodes/mDNSNode/mdns_broadcast_node.py +214 -0
- pynode/nodes/mDNSNode/mdns_discovery_node.py +362 -0
- pynode/nodes/mDNSNode/mdns_manager.py +399 -0
- pynode/nodes/mDNSNode/requirements.txt +1 -0
- pynode/nodes/messages.py +81 -0
- pynode/server.py +187 -0
- pynode/static/images/favicon4.png +0 -0
- pynode/static/index.html +200 -0
- pynode/static/js/README.md +139 -0
- pynode/static/js/auth.js +89 -0
- pynode/static/js/clipboard.js +152 -0
- pynode/static/js/config.js +14 -0
- pynode/static/js/connections.js +844 -0
- pynode/static/js/debug.js +524 -0
- pynode/static/js/events.js +775 -0
- pynode/static/js/history.js +199 -0
- pynode/static/js/main.js +55 -0
- pynode/static/js/minimap.js +242 -0
- pynode/static/js/nodes.js +995 -0
- pynode/static/js/palette.js +255 -0
- pynode/static/js/properties.js +1270 -0
- pynode/static/js/selection.js +241 -0
- pynode/static/js/state.js +211 -0
- pynode/static/js/ui-utils.js +28 -0
- pynode/static/js/viewport.js +204 -0
- pynode/static/js/workflow.js +469 -0
- pynode/static/js/workflows.js +344 -0
- pynode/static/style.css +2751 -0
- pynode/workflow_engine.py +483 -0
- pynode/workflow_manager.py +440 -0
- pynode_flow-0.2.0.dist-info/METADATA +520 -0
- pynode_flow-0.2.0.dist-info/RECORD +227 -0
- pynode_flow-0.2.0.dist-info/WHEEL +5 -0
- pynode_flow-0.2.0.dist-info/entry_points.txt +3 -0
- pynode_flow-0.2.0.dist-info/licenses/LICENSE +21 -0
- pynode_flow-0.2.0.dist-info/scm_file_list.json +270 -0
- pynode_flow-0.2.0.dist-info/scm_version.json +8 -0
- pynode_flow-0.2.0.dist-info/top_level.txt +1 -0
pynode/static/index.html
ADDED
|
@@ -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
|
pynode/static/js/auth.js
ADDED
|
@@ -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
|
+
};
|