panel-reactflow 0.0.1a1__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.
@@ -0,0 +1,561 @@
1
+ import React from "react";
2
+ import {
3
+ Background,
4
+ Controls,
5
+ Handle,
6
+ MiniMap,
7
+ NodeToolbar,
8
+ Panel,
9
+ Position,
10
+ ReactFlow,
11
+ ReactFlowProvider,
12
+ addEdge,
13
+ useEdgesState,
14
+ useNodesState,
15
+ useReactFlow,
16
+ } from "@xyflow/react";
17
+ import "@xyflow/react/dist/style.css";
18
+
19
+ const { useCallback, useEffect, useMemo, useRef } = React;
20
+
21
+ const BUILTIN_NODE_TYPES = {
22
+ panel: { label: "Panel" },
23
+ default: { label: "Default" },
24
+ minimal: { label: "Minimal", minimal: true },
25
+ };
26
+
27
+ function getPropertySummary(data, spec) {
28
+ if (!spec?.properties?.length) {
29
+ return [];
30
+ }
31
+ return spec.properties
32
+ .filter((prop) => prop.visible_in_node)
33
+ .map((prop) => {
34
+ const value = data?.[prop.name] ?? prop.default;
35
+ const label = prop.label || prop.name;
36
+ return { label, value };
37
+ });
38
+ }
39
+
40
+ function renderHandles(direction, handles) {
41
+ if (!handles?.length) {
42
+ return (
43
+ <Handle
44
+ type={direction === "input" ? "target" : "source"}
45
+ position={direction === "input" ? Position.Left : Position.Right}
46
+ />
47
+ );
48
+ }
49
+ const spacing = 100 / (handles.length + 1);
50
+ return handles.map((handle, index) => (
51
+ <Handle
52
+ key={`${direction}-${handle}`}
53
+ id={handle}
54
+ type={direction === "input" ? "target" : "source"}
55
+ position={direction === "input" ? Position.Left : Position.Right}
56
+ style={{ top: `${(index + 1) * spacing}%` }}
57
+ />
58
+ ));
59
+ }
60
+
61
+ function makeNodeComponent(typeName, model, typeSpec, editorMode) {
62
+ return function NodeComponent({ id, data }) {
63
+ const [toolbarOpen, toggleToolbar] = React.useState(false);
64
+ const spec = typeSpec || {};
65
+ const showGear = editorMode === "toolbar";
66
+ const showToolbar = (editorMode === "toolbar" && toolbarOpen);
67
+ const showView = data?.view && !spec.minimal;
68
+
69
+ const summary = getPropertySummary(data, spec);
70
+ const label = data?.label || spec.label || typeName;
71
+
72
+ const handleGearClick = (e) => {
73
+ e.stopPropagation();
74
+ toggleToolbar((v) => !v);
75
+ };
76
+
77
+ const handleCloseToolbar = (e) => {
78
+ e.stopPropagation();
79
+ toggleToolbar(false);
80
+ };
81
+
82
+ return (
83
+ <div style={{ padding: "8px 10px", minWidth: "140px" }}>
84
+ {showToolbar ? (
85
+ <NodeToolbar
86
+ isVisible={true}
87
+ position={Position.Top}
88
+ style={{background: "white"}}
89
+ >
90
+ {data.editor}
91
+ </NodeToolbar>
92
+ ): null}
93
+ {showGear && (
94
+ <button
95
+ aria-label={showToolbar ? "Hide node toolbar" : "Show node toolbar"}
96
+ onClick={handleGearClick}
97
+ style={{
98
+ position: "absolute",
99
+ top: "7px",
100
+ right: "7px",
101
+ border: "none",
102
+ background: "transparent",
103
+ fontSize: "17px",
104
+ lineHeight: "18px",
105
+ cursor: "pointer",
106
+ zIndex: 2,
107
+ padding: 0,
108
+ color: showToolbar ? "#3477db" : "#888",
109
+ filter: showToolbar
110
+ ? "drop-shadow(0 0 1px #3477db) brightness(1.15)"
111
+ : "none",
112
+ transition: "color 0.1s",
113
+ }}
114
+ tabIndex={0}
115
+ type="button"
116
+ title={showToolbar ? "Hide node toolbar" : "Show node toolbar"}
117
+ >
118
+ <img
119
+ src={import.meta.url.replace(/(\/[^\/?#]+)?(\?.*)?$/,"/icons/gear.svg")}
120
+ alt=""
121
+ width={14}
122
+ height={14}
123
+ aria-hidden="true"
124
+ style={{
125
+ opacity: showToolbar ? 1 : 0.85,
126
+ transform: showToolbar ? "rotate(22deg)" : "none",
127
+ transition: "color 0.13s, filter 0.13s, opacity 0.13s, transform 0.18s",
128
+ background: showToolbar ? "#eaf2fd" : "none",
129
+ borderRadius: "50%",
130
+ boxShadow: showToolbar
131
+ ? "0 0 0 2px #cfe1fc"
132
+ : "none",
133
+ stroke: showToolbar ? "#3477db" : "#888",
134
+ filter: showToolbar
135
+ ? "drop-shadow(0 0 1px #3477db) brightness(1.15)"
136
+ : "none",
137
+ }}
138
+ />
139
+ </button>
140
+ )}
141
+ {renderHandles("input", spec.inputs)}
142
+ <div style={{ fontWeight: 600, marginBottom: summary.length ? 6 : 0 }}>
143
+ {label}
144
+ </div>
145
+ {summary.length > 0 && (
146
+ <div style={{ display: "grid", gap: "2px"}}>
147
+ {summary.map((item) => (
148
+ <div key={item.label}>
149
+ <span style={{ opacity: 0.7 }}>{item.label}:</span>{" "}
150
+ <span>{String(item.value ?? "")}</span>
151
+ </div>
152
+ ))}
153
+ </div>
154
+ )}
155
+ {(showView || editorMode === "node") && (
156
+ <div style={{ marginTop: 6 }}>
157
+ {data.view}
158
+ {editorMode === "node" ? data.editor : null}
159
+ </div>
160
+ )}
161
+ {renderHandles("output", spec.outputs)}
162
+ </div>
163
+ );
164
+ };
165
+ }
166
+
167
+ function useDebouncedSync(syncMode, debounceMs, syncFn) {
168
+ const timeoutRef = useRef(null);
169
+
170
+ return useCallback(
171
+ (payload) => {
172
+ if (syncMode === "debounce") {
173
+ if (timeoutRef.current) {
174
+ clearTimeout(timeoutRef.current);
175
+ }
176
+ timeoutRef.current = setTimeout(() => syncFn(payload), debounceMs);
177
+ } else {
178
+ syncFn(payload);
179
+ }
180
+ },
181
+ [syncMode, debounceMs, syncFn]
182
+ );
183
+ }
184
+
185
+ function areEqual(a, b) {
186
+ try {
187
+ return JSON.stringify(a) === JSON.stringify(b);
188
+ } catch (error) {
189
+ return false;
190
+ }
191
+ }
192
+
193
+ function signature(value) {
194
+ try {
195
+ return JSON.stringify(value);
196
+ } catch (error) {
197
+ return null;
198
+ }
199
+ }
200
+
201
+ function FlowInner({
202
+ model,
203
+ hydratedNodes,
204
+ pyNodes,
205
+ hydratedEdges,
206
+ selectionSetter,
207
+ currentSelection,
208
+ views,
209
+ viewportSetter,
210
+ onNodeDoubleClick,
211
+ onPaneClick,
212
+ defaultEdgeOptions,
213
+ nodeTypes,
214
+ editable,
215
+ enableConnect,
216
+ enableDelete,
217
+ enableMultiselect,
218
+ showMinimap,
219
+ syncMode,
220
+ debounceMs,
221
+ viewport,
222
+ }) {
223
+ const [nodes, setNodes, onNodesChange] = useNodesState(hydratedNodes);
224
+ const [edges, setEdges, onEdgesChange] = useEdgesState(hydratedEdges);
225
+ const nodesRef = useRef(nodes);
226
+ const edgesRef = useRef(edges);
227
+ const lastHydrated = useRef({ nodesSig: null, viewsRef: null, edgesSig: null });
228
+ const lastViewportSig = useRef(null);
229
+ const { setViewport: setRfViewport } = useReactFlow();
230
+
231
+ useEffect(() => {
232
+ const handler = (msg) => {
233
+ if (!msg || typeof msg !== "object") {
234
+ return;
235
+ }
236
+ if (msg.type === "patch_node_data") {
237
+ setNodes((current) =>
238
+ current.map((node) => {
239
+ if (node.id !== msg.node_id) {
240
+ return node;
241
+ }
242
+ const data = { ...(node.data || {}), ...(msg.patch || {}) };
243
+ return { ...node, data };
244
+ })
245
+ );
246
+ return;
247
+ }
248
+ if (msg.type === "patch_edge_data") {
249
+ setEdges((current) =>
250
+ current.map((edge) => {
251
+ if (edge.id !== msg.edge_id) {
252
+ return edge;
253
+ }
254
+ const data = { ...(edge.data || {}), ...(msg.patch || {}) };
255
+ const nextLabel = msg.patch?.label ?? edge.label;
256
+ return { ...edge, data, label: nextLabel };
257
+ })
258
+ );
259
+ }
260
+ };
261
+ model.on("msg:custom", handler);
262
+ return () => {
263
+ model.off("msg:custom", handler);
264
+ };
265
+ }, [model, setEdges, setNodes]);
266
+
267
+ useEffect(() => {
268
+ nodesRef.current = nodes;
269
+ }, [nodes]);
270
+
271
+ useEffect(() => {
272
+ edgesRef.current = edges;
273
+ }, [edges]);
274
+
275
+ useEffect(() => {
276
+ const nodesSig = signature(pyNodes);
277
+ if (nodesSig === lastHydrated.current.nodesSig) return;
278
+ lastHydrated.current.nodesSig = nodesSig;
279
+
280
+ setNodes((curr) => {
281
+ const nextById = new Map(hydratedNodes.map((n) => [n.id, n]));
282
+ const currById = new Map(curr.map((n) => [n.id, n]));
283
+ const merged = hydratedNodes.map((n) => {
284
+ const prev = currById.get(n.id);
285
+ if (!prev) return n;
286
+ return {
287
+ ...n,
288
+ selected: prev.selected,
289
+ dragging: prev.dragging,
290
+ };
291
+ });
292
+ return merged;
293
+ });
294
+ }, [hydratedNodes, pyNodes, setNodes]);
295
+
296
+ useEffect(() => {
297
+ const edgesSig = signature(hydratedEdges);
298
+ if (edgesSig !== lastHydrated.current.edgesSig) {
299
+ lastHydrated.current.edgesSig = edgesSig;
300
+ setEdges(hydratedEdges);
301
+ }
302
+ }, [hydratedEdges, setEdges]);
303
+
304
+ useEffect(() => {
305
+ if (viewport) {
306
+ const nextSig = signature(viewport);
307
+ if (nextSig !== lastViewportSig.current) {
308
+ lastViewportSig.current = nextSig;
309
+ setRfViewport(viewport);
310
+ }
311
+ }
312
+ }, [setRfViewport, viewport]);
313
+
314
+ const sendPatch = useCallback(
315
+ (payload) => {
316
+ if (!payload) {
317
+ return;
318
+ }
319
+ model.send_msg(payload);
320
+ },
321
+ [model]
322
+ );
323
+
324
+ const schedulePatch = useDebouncedSync(syncMode, debounceMs, sendPatch);
325
+
326
+ const onConnect = useCallback(
327
+ (connection) => {
328
+ if (!enableConnect) {
329
+ return;
330
+ }
331
+ const edgeId = connection.id || `${connection.source}->${connection.target}`;
332
+ const newEdge = { ...connection, id: edgeId };
333
+ const updated = addEdge(newEdge, edgesRef.current);
334
+ setEdges(updated);
335
+ sendPatch({ type: "edge_added", edge: newEdge });
336
+ },
337
+ [enableConnect, sendPatch, setEdges]
338
+ );
339
+
340
+ const handleNodesChange = useCallback(
341
+ (changes) => {
342
+ onNodesChange(changes);
343
+ const moved = changes.filter(
344
+ (change) => change.type === "position" && change.dragging !== true
345
+ );
346
+ if (!moved.length) {
347
+ return;
348
+ }
349
+ moved.forEach((change) => {
350
+ schedulePatch({
351
+ type: "node_moved",
352
+ node_id: change.id,
353
+ position: change.position,
354
+ });
355
+ });
356
+ },
357
+ [onNodesChange, schedulePatch]
358
+ );
359
+
360
+ const onSelectionChange = useCallback(
361
+ ({ nodes: selectedNodes, edges: selectedEdges }) => {
362
+ const selection = {
363
+ nodes: selectedNodes.map((node) => node.id),
364
+ edges: selectedEdges.map((edge) => edge.id),
365
+ };
366
+ if (areEqual(selection, currentSelection)) {
367
+ return;
368
+ }
369
+ selectionSetter(selection);
370
+ schedulePatch({
371
+ type: "selection_changed",
372
+ nodes: selection.nodes,
373
+ edges: selection.edges,
374
+ });
375
+ },
376
+ [currentSelection, schedulePatch, selectionSetter]
377
+ );
378
+
379
+ const onNodesDelete = useCallback(
380
+ (deletedNodes) => {
381
+ const deletedIds = deletedNodes.map((node) => node.id);
382
+ const deletedEdges = edgesRef.current.filter(
383
+ (edge) =>
384
+ deletedIds.includes(edge.source) || deletedIds.includes(edge.target)
385
+ );
386
+ schedulePatch({
387
+ type: "node_deleted",
388
+ node_id: deletedIds.length === 1 ? deletedIds[0] : null,
389
+ node_ids: deletedIds,
390
+ deleted_edges: deletedEdges.map((edge) => edge.id),
391
+ });
392
+ },
393
+ [schedulePatch]
394
+ );
395
+
396
+ const onEdgesDelete = useCallback(
397
+ (deletedEdges) => {
398
+ schedulePatch({
399
+ type: "edge_deleted",
400
+ edge_id: deletedEdges.length === 1 ? deletedEdges[0].id : null,
401
+ edge_ids: deletedEdges.map((edge) => edge.id),
402
+ });
403
+ },
404
+ [schedulePatch]
405
+ );
406
+
407
+ const onMoveEnd = useCallback(
408
+ (_event, nextViewport) => {
409
+ if (!areEqual(nextViewport, viewport)) {
410
+ viewportSetter(nextViewport);
411
+ }
412
+ },
413
+ [viewport, viewportSetter]
414
+ );
415
+
416
+ return (
417
+ <ReactFlow
418
+ nodes={nodes}
419
+ edges={edges}
420
+ nodeTypes={nodeTypes}
421
+ defaultEdgeOptions={defaultEdgeOptions}
422
+ onNodesChange={handleNodesChange}
423
+ onEdgesChange={onEdgesChange}
424
+ onSelectionChange={onSelectionChange}
425
+ onNodesDelete={onNodesDelete}
426
+ onEdgesDelete={onEdgesDelete}
427
+ onConnect={onConnect}
428
+ onMoveEnd={onMoveEnd}
429
+ onNodeDoubleClick={onNodeDoubleClick}
430
+ onPaneClick={onPaneClick}
431
+ nodesDraggable={editable}
432
+ nodesConnectable={editable && enableConnect}
433
+ elementsSelectable={editable}
434
+ deleteKeyCode={enableDelete ? "Backspace" : null}
435
+ multiSelectionKeyCode={enableMultiselect ? "Shift" : null}
436
+ fitView
437
+ >
438
+ <Controls />
439
+ {showMinimap ? <MiniMap /> : null}
440
+ <Background />
441
+ </ReactFlow>
442
+ );
443
+ }
444
+
445
+ export function render({ model, view }) {
446
+ const [pyNodes] = model.useState("nodes");
447
+ const [pyEdges] = model.useState("edges");
448
+ const [defaultEdgeOptions] = model.useState("default_edge_options");
449
+ const [selection, setSelection] = model.useState("selection");
450
+ const [syncMode] = model.useState("sync_mode");
451
+ const [debounceMs] = model.useState("debounce_ms");
452
+ const [editable] = model.useState("editable");
453
+ const [editorMode] = model.useState("editor_mode");
454
+ const [enableConnect] = model.useState("enable_connect");
455
+ const [enableDelete] = model.useState("enable_delete");
456
+ const [enableMultiselect] = model.useState("enable_multiselect");
457
+ const [showMinimap] = model.useState("show_minimap");
458
+ const [viewport, setViewport] = model.useState("viewport");
459
+ const views = model.get_child("_views");
460
+ const nodeEditors = model.get_child("_node_editor_views");
461
+ const topPanels = model.get_child("top_panel");
462
+ const bottomPanels = model.get_child("bottom_panel");
463
+ const leftPanels = model.get_child("left_panel");
464
+ const rightPanels = model.get_child("right_panel");
465
+
466
+ const nodeEditorMap = {};
467
+ pyNodes.forEach((node, idx) => {
468
+ if (node && node.id !== undefined) {
469
+ nodeEditorMap[node.id] = nodeEditors[idx];
470
+ }
471
+ });
472
+
473
+ const hydratedNodes = useMemo(() => {
474
+ return (pyNodes || []).map((node, idx) => {
475
+ const data = node.data || {};
476
+ const viewIndex = data.view_idx;
477
+ const baseView = views[viewIndex];
478
+ const editorView = nodeEditors[idx];
479
+ return {
480
+ ...node,
481
+ data: {
482
+ ...data,
483
+ view: baseView,
484
+ editor: editorView,
485
+ },
486
+ };
487
+ });
488
+ }, [pyNodes, nodeEditors, views, editorMode]);
489
+
490
+ const hydratedEdges = useMemo(() => {
491
+ return (pyEdges || []).map((edge) => {
492
+ const data = edge.data || {};
493
+ const label = edge.label ?? data.label;
494
+ if (label === undefined) {
495
+ return edge;
496
+ }
497
+ return { ...edge, data, label };
498
+ });
499
+ }, [pyEdges]);
500
+
501
+ const nodeTypes = useMemo(() => {
502
+ const mapping = {};
503
+ Object.entries(BUILTIN_NODE_TYPES).forEach(([typeName, spec]) => {
504
+ mapping[typeName] = makeNodeComponent(
505
+ typeName,
506
+ model,
507
+ spec,
508
+ editorMode,
509
+ );
510
+ });
511
+ return mapping;
512
+ }, [editorMode]);
513
+
514
+ return (
515
+ <div style={{ width: "100%", height: "100%" }}>
516
+ <ReactFlowProvider>
517
+ <FlowInner
518
+ model={model}
519
+ hydratedNodes={hydratedNodes}
520
+ pyNodes={pyNodes || []}
521
+ hydratedEdges={hydratedEdges}
522
+ selectionSetter={setSelection}
523
+ currentSelection={selection}
524
+ views={views}
525
+ viewportSetter={setViewport}
526
+ defaultEdgeOptions={defaultEdgeOptions}
527
+ nodeTypes={nodeTypes}
528
+ editable={editable}
529
+ enableConnect={enableConnect}
530
+ enableDelete={enableDelete}
531
+ enableMultiselect={enableMultiselect}
532
+ showMinimap={showMinimap}
533
+ syncMode={syncMode}
534
+ debounceMs={debounceMs}
535
+ viewport={viewport}
536
+ />
537
+ {(topPanels || []).map((panel, idx) => (
538
+ <Panel key={`top-${idx}`} position="top-center">
539
+ {panel}
540
+ </Panel>
541
+ ))}
542
+ {(bottomPanels || []).map((panel, idx) => (
543
+ <Panel key={`bottom-${idx}`} position="bottom-center">
544
+ {panel}
545
+ </Panel>
546
+ ))}
547
+ {(leftPanels || []).map((panel, idx) => (
548
+ <Panel key={`left-${idx}`} position="center-left">
549
+ {panel}
550
+ </Panel>
551
+ ))}
552
+ {(rightPanels || []).map((panel, idx) => (
553
+ <Panel key={`right-${idx}`} position="center-right">
554
+ {panel}
555
+ {(selection.nodes.length && editorMode === "side") ? nodeEditorMap[selection.nodes[0]] : null}
556
+ </Panel>
557
+ ))}
558
+ </ReactFlowProvider>
559
+ </div>
560
+ );
561
+ }
File without changes