├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── assets ├── CanvasControl.mp4 ├── ElementToStreamlit.mp4 ├── FastBuild.mp4 ├── FastLayout.mp4 ├── MarkdownNode.png ├── reactflow-logo.svg ├── streamlit-flow-banner-bg.svg ├── streamlit-flow-banner.svg ├── streamlit-flow-logo-bg.svg ├── streamlit-flow-logo.svg └── streamlit-logo.svg ├── build_and_upload.sh ├── example.py ├── post_build_cleanup.sh ├── setup.py └── streamlit_flow ├── __init__.py ├── elements.py ├── frontend ├── .env ├── .gitignore ├── package-lock.json ├── package.json ├── public │ ├── bootstrap.min.css │ └── index.html └── src │ ├── StreamlitFlowApp.jsx │ ├── StreamlitFlowComponent.jsx │ ├── components │ ├── EdgeContextMenu.jsx │ ├── MarkdownNode.jsx │ ├── NodeContextMenu.jsx │ └── PaneContextMenu.jsx │ ├── index.jsx │ ├── layouts │ └── ElkLayout.js │ └── style.css ├── layouts.py └── state.py /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | dist 4 | *.egg-info 5 | __pycache__ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Dhruv Kapur 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include streamlit_flow/frontend/build * 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |  2 | 3 | # Streamlit Flow 4 | [](https://stflow.streamlit.app) 5 | 6 | 7 | **Build beautiful, interactive flow diagrams: Powered by React Flow, Simplified by Streamlit.** 8 | 9 |  10 | 11 | ### 🎉 Version 1.6.1 is out now! 🎉 12 | 13 | > [!WARNING] 14 | > 15 | > StreamlitFlow v1.6.1 has breaking changes. Please read on to understand how to migrate your code to the new version. 16 | 17 | 18 | This version of StreamlitFlow fixes 2 major issues: 19 | 20 | 1. Memory leak when interacting with the component (thanks @yyahav for bringing this up). 21 | 2. Component not reflecting state changes made in Python. 22 | 23 | A side-effect of synchronizing the frontend and backend changes is that if the state is defined in a way that its re-initialized with every re-run, it would cause the component to re-render infinitely and will hang your streamlit app. You must store the state as part of the `session_state` to prevent it from re-initalizing every run. 24 | 25 | **DON'T** 26 | ```python 27 | state = StreamlitFlowState(nodes, edges) 28 | streamlit_flow('key', state) 29 | ``` 30 | 31 | **DO** 32 | ```python 33 | if 'state' not in st.session_state: 34 | st.session_state.state = StreamlitFlowState(nodes, edge) 35 | streamlit_flow('key', st.session_state.state) 36 | ``` 37 | 38 | ## Features 39 | 40 | - Create, edit and visualize beautiful flow diagrams. 41 | - Add nodes and edges, move them around, pan and zoom. 42 | - Edit node and edge properties. 43 | - Easy to use Layouts - Layered, Tree, Force, Stress, Radial, Random, and Manual. 44 | - Markdown Support in Nodes. 45 | - Interactions with Streamlit - clicks on nodes and edges can be captured in Streamlit. 46 | - Synchronized state management - changes to the state of the flow can be made seamlessly from the UI through user interactions as well as programmatically in Python, and the changes reflect on the UI without any state modification wizardry. 47 | 48 | 49 | A demo for all these features can be found [here](https://stflow.streamlit.app). 50 | 51 | ## Installation 52 | 53 | ```bash 54 | pip install streamlit-flow-component 55 | ``` 56 | 57 | ## Running the example 58 | 59 | 60 | #### Install the dependencies 61 | ```bash 62 | git clone https://github.com/dkapur17/streamlit-flow.git 63 | cd streamlit-flow 64 | npm install --prefix streamlit_flow/frontend 65 | ``` 66 | 67 | #### Run the frontent 68 | On the first terminal, run from the root of the repository 69 | ```bash 70 | cd streamlit_flow/frontend 71 | npm start 72 | ``` 73 | 74 | #### Run this Example Streamlit App 75 | On the second terminal, run from the root of the repository 76 | ```bash 77 | streamlit run example.py 78 | ``` 79 | 80 | ## Change log 81 | 82 | ### Version 1.5.0 83 | 84 | > [!WARNING] 85 | > 86 | > StreamlitFlow v1.5.0 has breaking changes. Please read on to understand how to migrate your code to the new version. 87 | 88 | 89 | Say hello to the new and improved state management system that syncs the states between changes in the frontend and the backend. This means that you can now modify the state of the flow diagram from Python and see the changes reflected in the frontend without having to hack into the component's state. 90 | 91 | **Note**: The updated state management uses a new `StreamlitFlowState` class to manage the state within streamlit. Instead of taking `nodes` and `edges` as arguments, the `streamlit_flow` function now takes a single `StreamlitFlowState` object. This means that code using earlier versions of this library will need to be tweaked slightly to work with the new version. 92 | 93 | **Old** 94 | ```python 95 | from streamlit_flow import streamlit_flow 96 | from streamlit_flow.elements import StreamlitFlowNode, StreamlitFlowEdge 97 | 98 | nodes = [...] 99 | edges = [...] 100 | 101 | streamlit_flow('flow', nodes, edges) 102 | ``` 103 | 104 | **New** 105 | ```python 106 | from streamlit_flow import streamlit_flow 107 | from streamlit_flow.elements import StreamlitFlowNode, StreamlitFlowEdge 108 | from streamlit_flow.state import StreamlitFlowState 109 | 110 | nodes = [...] 111 | edges = [...] 112 | state = StreamlitFlowState(nodes, edges) 113 | 114 | streamlit_flow('flow', state) 115 | ``` 116 | 117 | The benefits we get from this are significant, as the `StreamlitFlowState` class makes sure that the states in the frontend and the backend are synced wth the latest changes from either side. 118 | 119 | **Example** 120 | ```python 121 | from streamlit_flow import streamlit_flow 122 | from streamlit_flow.elements import StreamlitFlowNode, StreamlitFlowEdge 123 | from streamlit_flow.state import StreamlitFlowState 124 | from uuid import uuid4 125 | 126 | # Initialize the state if it doesn't exist 127 | if 'flow_state' not in st.session_state: 128 | nodes = [...] 129 | edges = [...] 130 | st.session_state.flow_state = StreamlitFlowState(key="flow", nodes, edges) 131 | 132 | # Use any operation that alters the state, for example add node, and then rerun 133 | if st.button("Add node"): 134 | new_node = StreamlitFlowNode(id=str(f"st-flow-node_{uuid4()}"), 135 | pos=(0, 0), 136 | data={'content': f'Node {len(st.session_state.flow_state.nodes) + 1}'}, 137 | node_type='default', 138 | source_position='right', 139 | target_position='left') 140 | st.session_state.flow_state.nodes.append(new_node) 141 | st.rerun() 142 | 143 | # Use the state as the argument, as well as to store the return value 144 | st.session_state.flow_state = streamlit_flow('flow', st.session_state.flow_state) 145 | ``` 146 | #### Minor Updates 147 | - **More Robust Returns**: The `streamlit_flow` component now returns the updated state on several user interactions, such as creating/deleting/editing/moving a node or an edge, to make sure the states stay synced. 148 | - **Edge Markers**: Ends of the edges can now be set to `arrow` or `arrowclosed` to represent directed edges, as well as further styled. Check out the style options [here](https://reactflow.dev/api-reference/types/edge-marker). 149 | - **Unified node dimensions**: Streamlit flow now only sets the dimensions of the nodes in the `style` dictionary, and let Reactflow handle computing the dimensions. This means that the `width` and `height` attributes of the nodes are now deprecated. 150 | 151 | 152 | ### Version 1.2.9 153 | 154 | * **Markdown Nodes**: Use Markdown and HTML in the nodes to make them more expressive. 155 | 156 | ### Version 1.0.0 157 | 158 | * **Overhauled State Management**: The component now manages the state of the flow diagram automatically. As such, it keeps track of changes to node positions, connections, and more. 159 | 160 | * **Pane Context Menu**: Right-clicking on the canvas now opens a context menu, allowing you to add new nodes or reset the layout. 161 | 162 | * **Node Context Menu**: Right-clicking on a node now opens a context menu, allowing you to edit or delete the node. 163 | 164 | * **Edge Context Menu**: Right-clicking on an edge now opens a context menu, allowing you to edit or delete the edge. 165 | 166 | * **Way more Layouts**: The layouts are now more extensive, including Tree, Layered, Force, Stress, Radial, Random and Manual. One can also make a custom layout by inheriting from the `Layout` class. 167 | 168 | * **Hackable Python State**: The primary state management is done within the component, and the state can be read by Python. This is the intended usage. However, while not recommended, it is possible to modify the state from Python as well if the user wishes to. 169 | 170 | ### Version 0.6.0 171 | 172 | * The initial release of the library, with the ability to visualize flow diagrams. 173 | * Create nodes and edges, move them around, pan and zoom. 174 | * Automatic Layered Layout Supported 175 | * Interactions sent to Streamlit - The component returns the ID of the element that was clicked on. 176 | -------------------------------------------------------------------------------- /assets/CanvasControl.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkapur17/streamlit-flow/364b78dd0ba430353cfbeab862317a281ae5b740/assets/CanvasControl.mp4 -------------------------------------------------------------------------------- /assets/ElementToStreamlit.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkapur17/streamlit-flow/364b78dd0ba430353cfbeab862317a281ae5b740/assets/ElementToStreamlit.mp4 -------------------------------------------------------------------------------- /assets/FastBuild.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkapur17/streamlit-flow/364b78dd0ba430353cfbeab862317a281ae5b740/assets/FastBuild.mp4 -------------------------------------------------------------------------------- /assets/FastLayout.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkapur17/streamlit-flow/364b78dd0ba430353cfbeab862317a281ae5b740/assets/FastLayout.mp4 -------------------------------------------------------------------------------- /assets/MarkdownNode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkapur17/streamlit-flow/364b78dd0ba430353cfbeab862317a281ae5b740/assets/MarkdownNode.png -------------------------------------------------------------------------------- /assets/reactflow-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/streamlit-flow-banner-bg.svg: -------------------------------------------------------------------------------- 1 | 28 | -------------------------------------------------------------------------------- /assets/streamlit-flow-banner.svg: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /assets/streamlit-flow-logo-bg.svg: -------------------------------------------------------------------------------- 1 | 26 | -------------------------------------------------------------------------------- /assets/streamlit-flow-logo.svg: -------------------------------------------------------------------------------- 1 | 25 | -------------------------------------------------------------------------------- /assets/streamlit-logo.svg: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /build_and_upload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | npm run build --prefix streamlit_flow/frontend 4 | sed -i 's/_RELEASE = False/_RELEASE = True/g' streamlit_flow/__init__.py 5 | python setup.py sdist bdist_wheel 6 | python -m twine upload --repository pypi dist/* -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | from streamlit_flow import streamlit_flow 3 | from streamlit_flow.elements import StreamlitFlowNode, StreamlitFlowEdge 4 | from streamlit_flow.state import StreamlitFlowState 5 | from streamlit_flow.layouts import TreeLayout, RadialLayout 6 | import random 7 | from uuid import uuid4 8 | 9 | st.set_page_config("Streamlit Flow Example", layout="wide") 10 | 11 | st.title("Streamlit Flow Example") 12 | 13 | 14 | if 'curr_state' not in st.session_state: 15 | nodes = [StreamlitFlowNode("1", (0, 0), {'content': 'Node 1'}, 'input', 'right'), 16 | StreamlitFlowNode("2", (1, 0), {'content': 'Node 2'}, 'default', 'right', 'left'), 17 | StreamlitFlowNode("3", (2, 0), {'content': 'Node 3'}, 'default', 'right', 'left'), 18 | ] 19 | 20 | edges = [StreamlitFlowEdge("1-2", "1", "2", animated=True, marker_start={}, marker_end={'type': 'arrow'}), 21 | StreamlitFlowEdge("1-3", "1", "3", animated=True), 22 | ] 23 | 24 | st.session_state.curr_state = StreamlitFlowState(nodes, edges) 25 | 26 | col1, col2, col3, col4, col5 = st.columns(5) 27 | 28 | with col1: 29 | if st.button("Add node"): 30 | new_node = StreamlitFlowNode(str(f"st-flow-node_{uuid4()}"), (0, 0), {'content': f'Node {len(st.session_state.curr_state.nodes) + 1}'}, 'default', 'right', 'left') 31 | st.session_state.curr_state.nodes.append(new_node) 32 | st.rerun() 33 | 34 | with col2: 35 | if st.button("Delete Random Node"): 36 | if len(st.session_state.curr_state.nodes) > 0: 37 | node_to_delete = random.choice(st.session_state.curr_state.nodes) 38 | st.session_state.curr_state.nodes = [node for node in st.session_state.curr_state.nodes if node.id != node_to_delete.id] 39 | st.session_state.curr_state.edges = [edge for edge in st.session_state.curr_state.edges if edge.source != node_to_delete.id and edge.target != node_to_delete.id] 40 | st.rerun() 41 | 42 | with col3: 43 | if st.button("Add Random Edge"): 44 | if len(st.session_state.curr_state.nodes) > 1: 45 | 46 | source_candidates = [streamlit_node for streamlit_node in st.session_state.curr_state.nodes if streamlit_node.type in ['input', 'default']] 47 | target_candidates = [streamlit_node for streamlit_node in st.session_state.curr_state.nodes if streamlit_node.type in ['default', 'output']] 48 | source = random.choice(source_candidates) 49 | target = random.choice(target_candidates) 50 | new_edge = StreamlitFlowEdge(f"{source.id}-{target.id}", source.id, target.id, animated=True) 51 | if not any(edge.id == new_edge.id for edge in st.session_state.curr_state.edges): 52 | st.session_state.curr_state.edges.append(new_edge) 53 | st.rerun() 54 | 55 | with col4: 56 | if st.button("Delete Random Edge"): 57 | if len(st.session_state.curr_state.edges) > 0: 58 | edge_to_delete = random.choice(st.session_state.curr_state.edges) 59 | st.session_state.curr_state.edges = [edge for edge in st.session_state.curr_state.edges if edge.id != edge_to_delete.id] 60 | st.rerun() 61 | 62 | with col5: 63 | if st.button("Random Flow"): 64 | nodes = [StreamlitFlowNode(str(f"st-flow-node_{uuid4()}"), (0, 0), {'content': f'Node {i}'}, 'default', 'right', 'left') for i in range(5)] 65 | edges = [] 66 | for _ in range(5): 67 | source = random.choice(nodes) 68 | target = random.choice(nodes) 69 | if source.id != target.id: 70 | new_edge = StreamlitFlowEdge(f"{source.id}-{target.id}", source.id, target.id, animated=True) 71 | if not any(edge.id == new_edge.id for edge in edges): 72 | edges.append(new_edge) 73 | st.session_state.curr_state = StreamlitFlowState( 74 | nodes=nodes, 75 | edges=edges 76 | ) 77 | st.rerun() 78 | 79 | 80 | st.session_state.curr_state = streamlit_flow('example_flow', 81 | st.session_state.curr_state, 82 | layout=TreeLayout(direction='right'), 83 | fit_view=True, 84 | height=500, 85 | enable_node_menu=True, 86 | enable_edge_menu=True, 87 | enable_pane_menu=True, 88 | get_edge_on_click=True, 89 | get_node_on_click=True, 90 | show_minimap=True, 91 | hide_watermark=True, 92 | allow_new_edges=True, 93 | min_zoom=0.1) 94 | 95 | 96 | col1, col2, col3 = st.columns(3) 97 | 98 | with col1: 99 | for node in st.session_state.curr_state.nodes: 100 | st.write(node) 101 | 102 | with col2: 103 | for edge in st.session_state.curr_state.edges: 104 | st.write(edge) 105 | 106 | with col3: 107 | st.write(st.session_state.curr_state.selected_id) -------------------------------------------------------------------------------- /post_build_cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -rf build dist streamlit_flow_component.egg-info 4 | sed -i 's/_RELEASE = True/_RELEASE = False/g' streamlit_flow/__init__.py 5 | rm -rf streamlit_flow/frontend/build -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import setuptools 4 | 5 | this_directory = Path(__file__).parent 6 | long_description = (this_directory / "README.md").read_text() 7 | 8 | setuptools.setup( 9 | name="streamlit-flow-component", 10 | version="1.6.1", 11 | author="Dhruv Kapur", 12 | author_email="dhruvkapur4@gmail.com", 13 | description="Streamlit Component Wrapper for React Flow", 14 | long_description=long_description, 15 | long_description_content_type="text/markdown", 16 | url="https://github.com/dkapur17/streamlit-flow", 17 | packages=setuptools.find_packages(), 18 | include_package_data=True, 19 | classifiers=[], 20 | python_requires=">=3.8", 21 | install_requires=[ 22 | # By definition, a Custom Component depends on Streamlit. 23 | # If your component has other Python dependencies, list 24 | # them here. 25 | "streamlit >= 1.0.0", 26 | ], 27 | extras_require={ 28 | "devel": [ 29 | "wheel", 30 | "pytest==7.4.0", 31 | "playwright==1.39.0", 32 | "requests==2.31.0", 33 | "pytest-playwright-snapshot==1.0", 34 | "pytest-rerunfailures==12.0", 35 | ] 36 | } 37 | ) 38 | -------------------------------------------------------------------------------- /streamlit_flow/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import streamlit.components.v1 as components 3 | 4 | 5 | from .elements import StreamlitFlowNode, StreamlitFlowEdge 6 | from .layouts import Layout, ManualLayout 7 | from .state import StreamlitFlowState 8 | 9 | _RELEASE = False 10 | 11 | if not _RELEASE: 12 | _st_flow_func = components.declare_component( 13 | "streamlit_flow", 14 | url="http://localhost:3001/", 15 | ) 16 | 17 | else: 18 | parent_dir = os.path.dirname(os.path.abspath(__file__)) 19 | build_dir = os.path.join(parent_dir, "frontend/build") 20 | _st_flow_func = components.declare_component("streamlit_flow", path=build_dir) 21 | 22 | 23 | 24 | def streamlit_flow(key:str, 25 | state:StreamlitFlowState, 26 | height:int=500, 27 | fit_view:bool=False, 28 | show_controls:bool=True, 29 | show_minimap:bool=False, 30 | allow_new_edges:bool=False, 31 | animate_new_edges:bool=False, 32 | style:dict={}, 33 | layout:Layout=ManualLayout(), 34 | get_node_on_click:bool=False, 35 | get_edge_on_click:bool=False, 36 | pan_on_drag:bool=True, 37 | allow_zoom:bool=True, 38 | min_zoom:float=0.5, 39 | enable_pane_menu:bool=False, 40 | enable_node_menu:bool=False, 41 | enable_edge_menu:bool=False, 42 | hide_watermark:bool=False): 43 | 44 | """ 45 | The main function to render the flowchart component in Streamlit. 46 | 47 | Arguments 48 | - **key** : str : A unique identifier for the component. 49 | - **state** : StreamlitFlowState : The current state of the flowchart component. 50 | - **height** : int : The height of the component in pixels. 51 | - **fit_view** : bool : Whether to fit the view of the component. 52 | - **show_controls** : bool : Whether to show the controls of the component. 53 | - **show_minimap** : bool : Whether to show the minimap of the component. 54 | - **allow_new_edges** : bool : Whether to allow new edges to be created. 55 | - **animate_new_edges** : bool : Whether to animate new edges created on the canvas. 56 | - **style** : dict : CSS style of the component. 57 | - **layout** : Layout : The layout of the nodes in the component. 58 | - **get_node_on_click** : bool : Whether to get the node on click. 59 | - **get_edge_on_click** : bool : Whether to get the edge on click. 60 | - **pan_on_drag** : bool : Whether to pan on drag. 61 | - **allow_zoom** : bool : Whether to allow zoom. 62 | - **min_zoom** : float : The minimum zoom level. 63 | - **enable_pane_menu** : bool : Whether to enable the pane menu. 64 | - **enable_node_menu** : bool : Whether to enable the node menu. 65 | - **enable_edge_menu** : bool : Whether to enable the edge menu. 66 | - **hide_watermark** : bool : Whether to hide the watermark. 67 | """ 68 | 69 | nodes = [node.asdict() for node in state.nodes] 70 | edges = [edge.asdict() for edge in state.edges] 71 | 72 | component_value = _st_flow_func( nodes=nodes, 73 | edges=edges, 74 | height=height, 75 | showControls=show_controls, 76 | fitView=fit_view, 77 | showMiniMap=show_minimap, 78 | style=style, 79 | animateNewEdges=animate_new_edges, 80 | allowNewEdges=allow_new_edges, 81 | layoutOptions=layout.__to_dict__(), 82 | getNodeOnClick=get_node_on_click, 83 | getEdgeOnClick=get_edge_on_click, 84 | panOnDrag=pan_on_drag, 85 | allowZoom=allow_zoom, 86 | minZoom=min_zoom, 87 | enableNodeMenu=enable_node_menu, 88 | enablePaneMenu=enable_pane_menu, 89 | enableEdgeMenu=enable_edge_menu, 90 | hideWatermark=hide_watermark, 91 | key=key, 92 | timestamp=state.timestamp, 93 | component='streamlit_flow') 94 | 95 | 96 | if component_value is None: 97 | return state 98 | 99 | new_state = StreamlitFlowState( 100 | nodes=[StreamlitFlowNode.from_dict(node) for node in component_value['nodes']], 101 | edges=[StreamlitFlowEdge.from_dict(edge) for edge in component_value['edges']], 102 | selected_id=component_value['selectedId'], 103 | timestamp=component_value['timestamp'] 104 | ) 105 | 106 | return new_state 107 | -------------------------------------------------------------------------------- /streamlit_flow/elements.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Tuple, Union, Type, TypeVar, Literal 2 | 3 | T_StreamlitFlowNode = TypeVar('T_StreamlitFlowNode', bound='StreamlitFlowNode') 4 | T_StreamlitFlowEdge = TypeVar('T_StreamlitFlowEdge', bound='StreamlitFlowEdge') 5 | 6 | class StreamlitFlowNode: 7 | 8 | """ 9 | Represents a node in streamlit_flow 10 | 11 | Arguments 12 | - **id** : str : Unique identifier for the node 13 | - **pos** : Tuple[float, float] : Position of the node in the canvas 14 | - **data** : Dict[str, any] : Arbitrary data to save in the node. Use {'content': 'Node content'} to set the content of the node 15 | - **node_type** : str : Type of the node. One of ['default', 'input', 'output'] 16 | - **source_position** : str : Position of the source anchor. One of ['top', 'bottom', 'left', 'right'] 17 | - **target_position** : str : Position of the target anchor. One of ['top', 'bottom', 'left', 'right'] 18 | - **hidden** : bool : Whether the node is hidden 19 | - **selected** : bool : Whether the node is selected 20 | - **dragging** : bool : Whether the node is being dragged (?) 21 | - **draggable** : bool : Whether the node is draggable 22 | - **selectable** : bool : Whether the node is selectable 23 | - **connectable** : bool : Whether the node is connectable 24 | - **resizing** : bool : Whether the node is resizable 25 | - **deletable** : bool : Whether the node is deletable 26 | - **z_index** : float : Z-index of the node 27 | - **focusable** : bool : Whether the node is focusable 28 | - **style** : Dict[str, any] : CSS style of the node 29 | """ 30 | 31 | def __init__(self, 32 | id:str, 33 | pos: Tuple[float, float], 34 | data:Dict[str, any], 35 | node_type:Literal['default', 'input', 'output'] = 'default', 36 | source_position:Literal['bottom', 'top', 'left', 'right'] = 'bottom', 37 | target_position:Literal['bottom', 'top', 'left', 'right'] = 'top', 38 | hidden:bool=False, 39 | selected:bool=False, 40 | dragging:bool=False, 41 | draggable:bool=True, 42 | selectable:bool=False, 43 | connectable:bool=False, 44 | resizing:bool=False, 45 | deletable:bool=False, 46 | z_index:float=0, 47 | focusable:bool=True, 48 | style:Dict[str, any]={}, 49 | **kwargs) -> None: 50 | 51 | if 'width' not in style: 52 | style['width'] = 'auto' 53 | if 'height' not in style: 54 | style['height'] = 'auto' 55 | 56 | self.id = id 57 | self.position = {"x": pos[0], "y": pos[1]} 58 | self.data = data 59 | self.type = node_type 60 | self.source_position = source_position 61 | self.target_position = target_position 62 | self.hidden = hidden 63 | self.selected = selected 64 | self.dragging = dragging 65 | self.draggable = draggable 66 | self.selectable = selectable 67 | self.connectable = connectable 68 | self.resizing = resizing 69 | self.deletable = deletable 70 | self.z_index = z_index 71 | self.focusable = focusable 72 | self.style = style 73 | self.kwargs = kwargs 74 | 75 | # Remove post V1.3.0 76 | if 'label' in self.data: 77 | content = self.data.pop('label') 78 | self.data['content'] = content 79 | 80 | self.__validate__() 81 | 82 | @classmethod 83 | def from_dict(cls: Type[T_StreamlitFlowNode], node_dict:Dict[str, any]) -> T_StreamlitFlowNode: 84 | 85 | # other_attributes_dict = {key: value for key, value in node_dict.items() if key not in ['id', 'position', 'data', 'type', 'sourcePosition', 'targetPosition', 'hidden', 'selected', 'dragging', 'draggable', 'selectable', 'connectable', 'resizing', 'deletable', 'width', 'height', 'zIndex', 'focusable', 'style']} 86 | 87 | return cls( id=node_dict.get('id', ''), 88 | pos=(node_dict['position'].get('x', 0), node_dict['position'].get('y', 0)), 89 | data=node_dict.get('data', {}), 90 | node_type=node_dict.get('type', 'default'), 91 | source_position=node_dict.get('sourcePosition', 'bottom'), 92 | target_position=node_dict.get('targetPosition', 'top'), 93 | hidden=node_dict.get('hidden', False), 94 | selected=node_dict.get('selected', False), 95 | dragging=node_dict.get('dragging', False), 96 | draggable=node_dict.get('draggable', True), 97 | selectable=node_dict.get('selectable', False), 98 | connectable=node_dict.get('connectable', True), 99 | resizing=node_dict.get('resizing', False), 100 | deletable=node_dict.get('deletable', False), 101 | z_index=node_dict.get('zIndex', 0), 102 | focusable=node_dict.get('focusable', True), 103 | style=node_dict.get('style', {})) 104 | 105 | 106 | def __validate__(self): 107 | assert self.type in ['default', 'input', 'output'], f"Node type must be one of ['default', 'input', 'output']. Got {self.type}" 108 | assert self.source_position in ['top', 'bottom', 'left', 'right'], f"Source position must be one of ['top', 'bottom', 'left', 'right']. Got {self.source_position}" 109 | assert self.target_position in ['top', 'bottom', 'left', 'right'], f"Target position must be one of ['top', 'bottom', 'left', 'right']. Got {self.target_position}" 110 | 111 | 112 | def asdict(self) -> Dict[str, any]: 113 | node_dict = { 114 | "id": self.id, 115 | "position": self.position, 116 | "data": self.data, 117 | "type": self.type, 118 | "sourcePosition": self.source_position, 119 | "targetPosition": self.target_position, 120 | "hidden": self.hidden, 121 | "selected": self.selected, 122 | "dragging": self.dragging, 123 | "draggable": self.draggable, 124 | "selectable": self.selectable, 125 | "connectable": self.connectable, 126 | "resizing": self.resizing, 127 | "deletable": self.deletable, 128 | "zIndex": self.z_index, 129 | "focusable": self.focusable, 130 | "style": self.style, 131 | } 132 | node_dict.update(self.kwargs) 133 | return node_dict 134 | 135 | def __repr__(self): 136 | return f"StreamlitFlowNode({self.id}, ({round(self.position['x'], 2)}, {round(self.position['y'],2)}), '{self.data.get('content', '')}')" 137 | 138 | 139 | class StreamlitFlowEdge: 140 | 141 | """ 142 | Represents an edge in streamlit_flow 143 | 144 | Arguments 145 | - **id** : str : Unique identifier for the edge 146 | - **source** : str : ID of the source node 147 | - **target** : str : ID of the target node 148 | - **edge_type** : str : Type of the edge. One of ['default', 'straight', 'step', "smoothstep", "simplebezier"] 149 | - **marker_start** : dict : Marker at the start of the edge. Eg: {'type': 'arrow'/'arrowclosed'} 150 | - **marker_end** : dict : Marker at the end of the edge. Eg: {'type': 'arrow'/'arrowclosed'} 151 | - **hidden** : bool : Whether the edge is hidden 152 | - **animated** : bool : Whether the edge is animated 153 | - **selected** : bool : Whether the edge is selected 154 | - **deletable** : bool : Whether the edge is deletable 155 | - **focusable** : bool : Whether the edge is focusable 156 | - **z_index** : float : Z-index of the edge 157 | - **label** : str : Label of the edge 158 | - **label_style** : Dict[str, any] : CSS style of the label 159 | - **label_show_bg** : bool : Whether to show background for the label 160 | - **label_bg_style** : Dict[str, any] : CSS style of the label background 161 | - **style** : Dict[str, any] : CSS style of the edge 162 | """ 163 | 164 | def __init__(self, 165 | id:str, 166 | source:str, 167 | target:str, 168 | edge_type:Literal['default', 'straight', 'step', "smoothstep", "simplebezier"]="default", 169 | marker_start:dict={}, 170 | marker_end:dict={}, 171 | hidden:bool=False, 172 | animated:bool=False, 173 | selected:bool=False, 174 | deletable:bool=False, 175 | focusable:bool=False, 176 | z_index:float=0, 177 | label:str="", 178 | label_style:Dict[str, any]={}, 179 | label_show_bg:bool=False, 180 | label_bg_style:Dict[str, any]={}, 181 | style:Dict[str, any]={}, 182 | **kwargs) -> None: 183 | 184 | self.id = id 185 | self.source = source 186 | self.target = target 187 | self.type = edge_type 188 | self.marker_start = marker_start 189 | self.marker_end = marker_end 190 | self.hidden = hidden 191 | self.animated = animated 192 | self.selected = selected 193 | self.deletable = deletable 194 | self.focusable = focusable 195 | self.z_index = z_index 196 | self.label = label 197 | self.label_style = label_style 198 | self.label_show_bg = label_show_bg 199 | self.label_bg_style = label_bg_style 200 | self.style = style 201 | self.kwargs = kwargs 202 | 203 | self.__validate__() 204 | 205 | @classmethod 206 | def from_dict(cls: Type[T_StreamlitFlowEdge], edge_dict:Dict[str, any]) -> T_StreamlitFlowEdge: 207 | 208 | # other_attributes_dict = {key: value for key, value in edge_dict.items() if key not in ['id', 'source', 'target', 'type', 'hidden', 'animated', 'selected', 'deletable', 'focusable', 'zIndex', 'label', 'labelStyle', 'labelShowBg', 'labelBgStyle', 'style']} 209 | return cls( id=edge_dict.get('id', ''), 210 | source=edge_dict.get('source', ''), 211 | target=edge_dict.get('target', ''), 212 | edge_type=edge_dict.get('type', 'default'), 213 | marker_start=edge_dict.get('markerStart', {}), 214 | marker_end=edge_dict.get('markerEnd', {}), 215 | hidden=edge_dict.get('hidden', False), 216 | animated=edge_dict.get('animated', False), 217 | selected=edge_dict.get('selected', False), 218 | deletable=edge_dict.get('deletable', False), 219 | focusable=edge_dict.get('focusable', False), 220 | z_index=edge_dict.get('zIndex', 0), 221 | label=edge_dict.get('label', ''), 222 | label_style=edge_dict.get('labelStyle', {}), 223 | label_show_bg=edge_dict.get('labelShowBg', False), 224 | label_bg_style=edge_dict.get('labelBgStyle', {}), 225 | style=edge_dict.get('style', {})) 226 | 227 | 228 | def __validate__(self) -> None: 229 | assert self.type in ['default', 'straight', 'step', "smoothstep", "simplebezier"], f"Edge type must be one of ['default', 'straight', 'step', 'smoothstep', 'simplebezier']. Got {self.type}" 230 | 231 | 232 | def asdict(self) -> Dict[str, any]: 233 | edge_dict = { 234 | "id": self.id, 235 | "source": self.source, 236 | "target": self.target, 237 | "type": self.type, 238 | "markerStart": self.marker_start, 239 | "markerEnd": self.marker_end, 240 | "hidden": self.hidden, 241 | "animated": self.animated, 242 | "selected": self.selected, 243 | "deletable": self.deletable, 244 | "focusable": self.focusable, 245 | "zIndex": self.z_index, 246 | "label": self.label, 247 | "labelStyle": self.label_style, 248 | "labelShowBg": self.label_show_bg, 249 | "labelBgStyle": self.label_bg_style, 250 | "style": self.style 251 | } 252 | 253 | edge_dict.update(self.kwargs) 254 | return edge_dict 255 | 256 | def __repr__(self): 257 | return f"StreamlitFlowEdge({self.id}, {self.source}->{self.target}, '{self.label}')" -------------------------------------------------------------------------------- /streamlit_flow/frontend/.env: -------------------------------------------------------------------------------- 1 | # Run the component's dev server on :3001 2 | # (The Streamlit dev server already runs on :3000) 3 | PORT=3001 4 | 5 | # Don't automatically open the web browser on `npm run start`. 6 | BROWSER=none 7 | -------------------------------------------------------------------------------- /streamlit_flow/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /streamlit_flow/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "streamlit_flow", 3 | "version": "1.6.1", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "@types/jest": "^24.0.0", 10 | "@types/node": "^12.0.0", 11 | "@types/react": "^18.3.3", 12 | "@types/react-dom": "^16.9.0", 13 | "bootstrap": "^5.3.3", 14 | "bootstrap-icons": "^1.11.3", 15 | "elkjs": "^0.9.3", 16 | "nanoid": "^5.0.7", 17 | "react": "^18.3.1", 18 | "react-bootstrap": "^2.10.2", 19 | "react-dom": "^18.3.1", 20 | "react-markdown": "^9.0.1", 21 | "react-router-dom": "^6.26.2", 22 | "react-scripts": "5.0.1", 23 | "reactflow": "^11.11.1", 24 | "rehype-highlight": "^7.0.0", 25 | "rehype-katex": "^7.0.0", 26 | "rehype-raw": "^7.0.0", 27 | "remark-gfm": "^4.0.0", 28 | "remark-math": "^6.0.0", 29 | "streamlit-component-lib": "^2.0.0" 30 | }, 31 | "scripts": { 32 | "start": "GENERATE_SOURCEMAP=false react-scripts start", 33 | "build": "GENERATE_SOURCEMAP=false react-scripts build", 34 | "test": "react-scripts test", 35 | "eject": "react-scripts eject" 36 | }, 37 | "eslintConfig": { 38 | "extends": "react-app" 39 | }, 40 | "browserslist": { 41 | "production": [ 42 | ">0.2%", 43 | "not dead", 44 | "not op_mini all" 45 | ], 46 | "development": [ 47 | "last 1 chrome version", 48 | "last 1 firefox version", 49 | "last 1 safari version" 50 | ] 51 | }, 52 | "homepage": ".", 53 | "devDependencies": { 54 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /streamlit_flow/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |