Spaces:
Running
Running
Nodes that can contain flows.
Browse files- server/main.py +5 -9
- server/ops.py +33 -12
- server/pytorch_model_ops.py +48 -0
- web/index.html +1 -1
- web/src/LynxKiteFlow.svelte +39 -9
- web/src/LynxKiteNode.svelte +13 -5
- web/src/NodeSearch.svelte +4 -5
- web/src/NodeWithParamsVertical.svelte +34 -0
- web/src/NodeWithSubFlow.svelte +38 -0
server/main.py
CHANGED
|
@@ -9,6 +9,7 @@ import traceback
|
|
| 9 |
from . import ops
|
| 10 |
from . import basic_ops
|
| 11 |
from . import networkx_ops
|
|
|
|
| 12 |
|
| 13 |
class BaseConfig(pydantic.BaseModel):
|
| 14 |
model_config = pydantic.ConfigDict(
|
|
@@ -30,6 +31,7 @@ class WorkspaceNode(BaseConfig):
|
|
| 30 |
type: str
|
| 31 |
data: WorkspaceNodeData
|
| 32 |
position: Position
|
|
|
|
| 33 |
|
| 34 |
class WorkspaceEdge(BaseConfig):
|
| 35 |
id: str
|
|
@@ -46,17 +48,11 @@ app = fastapi.FastAPI()
|
|
| 46 |
|
| 47 |
@app.get("/api/catalog")
|
| 48 |
def get_catalog():
|
| 49 |
-
return [
|
| 50 |
-
{
|
| 51 |
-
'type': op.type,
|
| 52 |
-
'data': { 'title': op.name, 'params': op.params },
|
| 53 |
-
'targetPosition': 'left' if op.inputs else None,
|
| 54 |
-
'sourcePosition': 'right' if op.outputs else None,
|
| 55 |
-
}
|
| 56 |
-
for op in ops.ALL_OPS.values()]
|
| 57 |
|
| 58 |
def execute(ws):
|
| 59 |
-
|
|
|
|
| 60 |
outputs = {}
|
| 61 |
failed = 0
|
| 62 |
while len(outputs) + failed < len(nodes):
|
|
|
|
| 9 |
from . import ops
|
| 10 |
from . import basic_ops
|
| 11 |
from . import networkx_ops
|
| 12 |
+
from . import pytorch_model_ops
|
| 13 |
|
| 14 |
class BaseConfig(pydantic.BaseModel):
|
| 15 |
model_config = pydantic.ConfigDict(
|
|
|
|
| 31 |
type: str
|
| 32 |
data: WorkspaceNodeData
|
| 33 |
position: Position
|
| 34 |
+
parentNode: Optional[str] = None
|
| 35 |
|
| 36 |
class WorkspaceEdge(BaseConfig):
|
| 37 |
id: str
|
|
|
|
| 48 |
|
| 49 |
@app.get("/api/catalog")
|
| 50 |
def get_catalog():
|
| 51 |
+
return [op.to_json() for op in ops.ALL_OPS.values()]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
def execute(ws):
|
| 54 |
+
# Nodes are responsible for interpreting/executing their child nodes.
|
| 55 |
+
nodes = [n for n in ws.nodes if not n.parentNode]
|
| 56 |
outputs = {}
|
| 57 |
failed = 0
|
| 58 |
while len(outputs) + failed < len(nodes):
|
server/ops.py
CHANGED
|
@@ -11,10 +11,11 @@ ALL_OPS = {}
|
|
| 11 |
class Op:
|
| 12 |
func: callable
|
| 13 |
name: str
|
| 14 |
-
params: dict
|
| 15 |
-
inputs: dict
|
| 16 |
-
outputs: dict
|
| 17 |
-
type: str
|
|
|
|
| 18 |
|
| 19 |
def __call__(self, *inputs, **params):
|
| 20 |
# Convert parameters.
|
|
@@ -39,20 +40,37 @@ class Op:
|
|
| 39 |
res = self.func(*inputs, **params)
|
| 40 |
return res
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
@dataclasses.dataclass
|
| 43 |
class RelationDefinition:
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
|
|
|
| 51 |
|
| 52 |
@dataclasses.dataclass
|
| 53 |
class Bundle:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
dfs: dict
|
| 55 |
relations: list[RelationDefinition]
|
|
|
|
| 56 |
|
| 57 |
@classmethod
|
| 58 |
def from_nx(cls, graph: nx.Graph):
|
|
@@ -94,7 +112,7 @@ def nx_node_attribute_func(name):
|
|
| 94 |
return decorator
|
| 95 |
|
| 96 |
|
| 97 |
-
def op(name, *, view='basic'):
|
| 98 |
'''Decorator for defining an operation.'''
|
| 99 |
def decorator(func):
|
| 100 |
sig = inspect.signature(func)
|
|
@@ -109,6 +127,9 @@ def op(name, *, view='basic'):
|
|
| 109 |
if param.kind == param.KEYWORD_ONLY}
|
| 110 |
outputs = {'output': 'yes'} if view == 'basic' else {} # Maybe more fancy later.
|
| 111 |
op = Op(func, name, params=params, inputs=inputs, outputs=outputs, type=view)
|
|
|
|
|
|
|
|
|
|
| 112 |
ALL_OPS[name] = op
|
| 113 |
return func
|
| 114 |
return decorator
|
|
|
|
| 11 |
class Op:
|
| 12 |
func: callable
|
| 13 |
name: str
|
| 14 |
+
params: dict # name -> default
|
| 15 |
+
inputs: dict # name -> type
|
| 16 |
+
outputs: dict # name -> type
|
| 17 |
+
type: str # The UI to use for this operation.
|
| 18 |
+
sub_nodes: list = None # If set, these nodes can be placed inside the operation's node.
|
| 19 |
|
| 20 |
def __call__(self, *inputs, **params):
|
| 21 |
# Convert parameters.
|
|
|
|
| 40 |
res = self.func(*inputs, **params)
|
| 41 |
return res
|
| 42 |
|
| 43 |
+
def to_json(self):
|
| 44 |
+
return {
|
| 45 |
+
'type': self.type,
|
| 46 |
+
'data': { 'title': self.name, 'params': self.params },
|
| 47 |
+
'targetPosition': 'left' if self.inputs else None,
|
| 48 |
+
'sourcePosition': 'right' if self.outputs else None,
|
| 49 |
+
'sub_nodes': [sub.to_json() for sub in self.sub_nodes.values()] if self.sub_nodes else None,
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
|
| 53 |
@dataclasses.dataclass
|
| 54 |
class RelationDefinition:
|
| 55 |
+
'''Defines a set of edges.'''
|
| 56 |
+
df: str # The DataFrame that contains the edges.
|
| 57 |
+
source_column: str # The column in the edge DataFrame that contains the source node ID.
|
| 58 |
+
target_column: str # The column in the edge DataFrame that contains the target node ID.
|
| 59 |
+
source_table: str # The DataFrame that contains the source nodes.
|
| 60 |
+
target_table: str # The DataFrame that contains the target nodes.
|
| 61 |
+
source_key: str # The column in the source table that contains the node ID.
|
| 62 |
+
target_key: str # The column in the target table that contains the node ID.
|
| 63 |
|
| 64 |
@dataclasses.dataclass
|
| 65 |
class Bundle:
|
| 66 |
+
'''A collection of DataFrames and other data.
|
| 67 |
+
|
| 68 |
+
Can efficiently represent a knowledge graph (homogeneous or heterogeneous) or tabular data.
|
| 69 |
+
It can also carry other data, such as a trained model.
|
| 70 |
+
'''
|
| 71 |
dfs: dict
|
| 72 |
relations: list[RelationDefinition]
|
| 73 |
+
other: dict = None
|
| 74 |
|
| 75 |
@classmethod
|
| 76 |
def from_nx(cls, graph: nx.Graph):
|
|
|
|
| 112 |
return decorator
|
| 113 |
|
| 114 |
|
| 115 |
+
def op(name, *, view='basic', sub_nodes=None):
|
| 116 |
'''Decorator for defining an operation.'''
|
| 117 |
def decorator(func):
|
| 118 |
sig = inspect.signature(func)
|
|
|
|
| 127 |
if param.kind == param.KEYWORD_ONLY}
|
| 128 |
outputs = {'output': 'yes'} if view == 'basic' else {} # Maybe more fancy later.
|
| 129 |
op = Op(func, name, params=params, inputs=inputs, outputs=outputs, type=view)
|
| 130 |
+
if sub_nodes is not None:
|
| 131 |
+
op.sub_nodes = sub_nodes
|
| 132 |
+
op.type = 'sub_flow'
|
| 133 |
ALL_OPS[name] = op
|
| 134 |
return func
|
| 135 |
return decorator
|
server/pytorch_model_ops.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'''Boxes for defining and using PyTorch models.'''
|
| 2 |
+
import inspect
|
| 3 |
+
from . import ops
|
| 4 |
+
|
| 5 |
+
LAYERS = {}
|
| 6 |
+
|
| 7 |
+
@ops.op("Define PyTorch model", sub_nodes=LAYERS)
|
| 8 |
+
def define_pytorch_model(*, sub_flow):
|
| 9 |
+
# import torch # Lazy import because it's slow.
|
| 10 |
+
print('sub_flow:', sub_flow)
|
| 11 |
+
return 'hello ' + str(sub_flow)
|
| 12 |
+
|
| 13 |
+
def register_layer(name):
|
| 14 |
+
def decorator(func):
|
| 15 |
+
sig = inspect.signature(func)
|
| 16 |
+
inputs = {
|
| 17 |
+
name: param.annotation
|
| 18 |
+
for name, param in sig.parameters.items()
|
| 19 |
+
if param.kind != param.KEYWORD_ONLY}
|
| 20 |
+
params = {
|
| 21 |
+
name: param.default if param.default is not inspect._empty else None
|
| 22 |
+
for name, param in sig.parameters.items()
|
| 23 |
+
if param.kind == param.KEYWORD_ONLY}
|
| 24 |
+
outputs = {'x': 'tensor'}
|
| 25 |
+
LAYERS[name] = ops.Op(func, name, params=params, inputs=inputs, outputs=outputs, type='vertical')
|
| 26 |
+
return func
|
| 27 |
+
return decorator
|
| 28 |
+
|
| 29 |
+
@register_layer('LayerNorm')
|
| 30 |
+
def normalization():
|
| 31 |
+
return 'LayerNorm'
|
| 32 |
+
|
| 33 |
+
@register_layer('Dropout')
|
| 34 |
+
def dropout(*, p=0.5):
|
| 35 |
+
return f'Dropout ({p})'
|
| 36 |
+
|
| 37 |
+
@register_layer('Linear')
|
| 38 |
+
def linear(*, output_dim: int):
|
| 39 |
+
return f'Linear {output_dim}'
|
| 40 |
+
|
| 41 |
+
@register_layer('Graph Convolution')
|
| 42 |
+
def graph_convolution():
|
| 43 |
+
return 'GraphConv'
|
| 44 |
+
|
| 45 |
+
@register_layer('Nonlinearity')
|
| 46 |
+
def nonlinearity():
|
| 47 |
+
return 'ReLU'
|
| 48 |
+
|
web/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
| 4 |
<meta charset="UTF-8" />
|
| 5 |
<link rel="icon" type="image/png" href="/public/favicon.ico" />
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
-
<title>
|
| 8 |
</head>
|
| 9 |
<body>
|
| 10 |
<div id="app"></div>
|
|
|
|
| 4 |
<meta charset="UTF-8" />
|
| 5 |
<link rel="icon" type="image/png" href="/public/favicon.ico" />
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
+
<title>LynxKite 2024</title>
|
| 8 |
</head>
|
| 9 |
<body>
|
| 10 |
<div id="app"></div>
|
web/src/LynxKiteFlow.svelte
CHANGED
|
@@ -14,8 +14,10 @@
|
|
| 14 |
type NodeTypes,
|
| 15 |
} from '@xyflow/svelte';
|
| 16 |
import NodeWithParams from './NodeWithParams.svelte';
|
|
|
|
| 17 |
import NodeWithGraphView from './NodeWithGraphView.svelte';
|
| 18 |
import NodeWithTableView from './NodeWithTableView.svelte';
|
|
|
|
| 19 |
import NodeSearch from './NodeSearch.svelte';
|
| 20 |
import '@xyflow/svelte/dist/style.css';
|
| 21 |
|
|
@@ -23,8 +25,10 @@
|
|
| 23 |
|
| 24 |
const nodeTypes: NodeTypes = {
|
| 25 |
basic: NodeWithParams,
|
|
|
|
| 26 |
graph_view: NodeWithGraphView,
|
| 27 |
table_view: NodeWithTableView,
|
|
|
|
| 28 |
};
|
| 29 |
|
| 30 |
export let path = '';
|
|
@@ -43,23 +47,23 @@
|
|
| 43 |
$: fetchWorkspace(path);
|
| 44 |
|
| 45 |
function closeNodeSearch() {
|
| 46 |
-
|
| 47 |
}
|
| 48 |
function toggleNodeSearch({ detail: { event } }) {
|
| 49 |
-
if (
|
| 50 |
closeNodeSearch();
|
| 51 |
return;
|
| 52 |
}
|
| 53 |
event.preventDefault();
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
};
|
| 58 |
}
|
| 59 |
function addNode(e) {
|
| 60 |
const node = {...e.detail};
|
| 61 |
nodes.update((n) => {
|
| 62 |
-
node.position = screenToFlowPosition({x:
|
| 63 |
const title = node.data.title;
|
| 64 |
let i = 1;
|
| 65 |
node.id = `${title} ${i}`;
|
|
@@ -67,6 +71,12 @@
|
|
| 67 |
i += 1;
|
| 68 |
node.id = `${title} ${i}`;
|
| 69 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
return [...n, node]
|
| 71 |
});
|
| 72 |
closeNodeSearch();
|
|
@@ -79,7 +89,11 @@
|
|
| 79 |
}
|
| 80 |
getBoxes();
|
| 81 |
|
| 82 |
-
let
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
const graph = derived([nodes, edges], ([nodes, edges]) => ({ nodes, edges }));
|
| 85 |
let backendWorkspace: string;
|
|
@@ -120,20 +134,36 @@
|
|
| 120 |
return edges.filter((e) => e.source === connection.source || e.target !== connection.target);
|
| 121 |
});
|
| 122 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
</script>
|
| 125 |
|
| 126 |
<div style:height="100%">
|
| 127 |
<SvelteFlow {nodes} {edges} {nodeTypes} fitView
|
| 128 |
on:paneclick={toggleNodeSearch}
|
|
|
|
| 129 |
proOptions={{ hideAttribution: true }}
|
| 130 |
maxZoom={1.5}
|
| 131 |
minZoom={0.3}
|
| 132 |
onconnect={onconnect}
|
| 133 |
>
|
| 134 |
-
<Background patternColor="#39bcf3" />
|
| 135 |
<Controls />
|
| 136 |
<MiniMap />
|
| 137 |
-
{#if
|
|
|
|
|
|
|
| 138 |
</SvelteFlow>
|
| 139 |
</div>
|
|
|
|
| 14 |
type NodeTypes,
|
| 15 |
} from '@xyflow/svelte';
|
| 16 |
import NodeWithParams from './NodeWithParams.svelte';
|
| 17 |
+
import NodeWithParamsVertical from './NodeWithParamsVertical.svelte';
|
| 18 |
import NodeWithGraphView from './NodeWithGraphView.svelte';
|
| 19 |
import NodeWithTableView from './NodeWithTableView.svelte';
|
| 20 |
+
import NodeWithSubFlow from './NodeWithSubFlow.svelte';
|
| 21 |
import NodeSearch from './NodeSearch.svelte';
|
| 22 |
import '@xyflow/svelte/dist/style.css';
|
| 23 |
|
|
|
|
| 25 |
|
| 26 |
const nodeTypes: NodeTypes = {
|
| 27 |
basic: NodeWithParams,
|
| 28 |
+
vertical: NodeWithParamsVertical,
|
| 29 |
graph_view: NodeWithGraphView,
|
| 30 |
table_view: NodeWithTableView,
|
| 31 |
+
sub_flow: NodeWithSubFlow,
|
| 32 |
};
|
| 33 |
|
| 34 |
export let path = '';
|
|
|
|
| 47 |
$: fetchWorkspace(path);
|
| 48 |
|
| 49 |
function closeNodeSearch() {
|
| 50 |
+
nodeSearchSettings = undefined;
|
| 51 |
}
|
| 52 |
function toggleNodeSearch({ detail: { event } }) {
|
| 53 |
+
if (nodeSearchSettings) {
|
| 54 |
closeNodeSearch();
|
| 55 |
return;
|
| 56 |
}
|
| 57 |
event.preventDefault();
|
| 58 |
+
nodeSearchSettings = {
|
| 59 |
+
pos: { x: event.clientX, y: event.clientY },
|
| 60 |
+
boxes: $boxes,
|
| 61 |
};
|
| 62 |
}
|
| 63 |
function addNode(e) {
|
| 64 |
const node = {...e.detail};
|
| 65 |
nodes.update((n) => {
|
| 66 |
+
node.position = screenToFlowPosition({x: nodeSearchSettings.pos.x, y: nodeSearchSettings.pos.y});
|
| 67 |
const title = node.data.title;
|
| 68 |
let i = 1;
|
| 69 |
node.id = `${title} ${i}`;
|
|
|
|
| 71 |
i += 1;
|
| 72 |
node.id = `${title} ${i}`;
|
| 73 |
}
|
| 74 |
+
node.parentNode = nodeSearchSettings.parentNode;
|
| 75 |
+
if (node.parentNode) {
|
| 76 |
+
node.extent = 'parent';
|
| 77 |
+
const parent = n.find((x) => x.id === node.parentNode);
|
| 78 |
+
node.position = { x: node.position.x - parent.position.x, y: node.position.y - parent.position.y };
|
| 79 |
+
}
|
| 80 |
return [...n, node]
|
| 81 |
});
|
| 82 |
closeNodeSearch();
|
|
|
|
| 89 |
}
|
| 90 |
getBoxes();
|
| 91 |
|
| 92 |
+
let nodeSearchSettings: {
|
| 93 |
+
pos: XYPosition,
|
| 94 |
+
boxes: any[],
|
| 95 |
+
parentNode: string,
|
| 96 |
+
};
|
| 97 |
|
| 98 |
const graph = derived([nodes, edges], ([nodes, edges]) => ({ nodes, edges }));
|
| 99 |
let backendWorkspace: string;
|
|
|
|
| 134 |
return edges.filter((e) => e.source === connection.source || e.target !== connection.target);
|
| 135 |
});
|
| 136 |
}
|
| 137 |
+
function nodeClick(e) {
|
| 138 |
+
const node = e.detail.node;
|
| 139 |
+
const meta = $boxes.find(m => m.data.title === node.data.title);
|
| 140 |
+
if (!meta) return;
|
| 141 |
+
const sub_nodes = meta.sub_nodes;
|
| 142 |
+
if (!sub_nodes) return;
|
| 143 |
+
const event = e.detail.event;
|
| 144 |
+
if (event.target.classList.contains('title')) return;
|
| 145 |
+
nodeSearchSettings = {
|
| 146 |
+
pos: { x: event.clientX, y: event.clientY },
|
| 147 |
+
boxes: sub_nodes,
|
| 148 |
+
parentNode: node.id,
|
| 149 |
+
};
|
| 150 |
+
}
|
| 151 |
|
| 152 |
</script>
|
| 153 |
|
| 154 |
<div style:height="100%">
|
| 155 |
<SvelteFlow {nodes} {edges} {nodeTypes} fitView
|
| 156 |
on:paneclick={toggleNodeSearch}
|
| 157 |
+
on:nodeclick={nodeClick}
|
| 158 |
proOptions={{ hideAttribution: true }}
|
| 159 |
maxZoom={1.5}
|
| 160 |
minZoom={0.3}
|
| 161 |
onconnect={onconnect}
|
| 162 |
>
|
|
|
|
| 163 |
<Controls />
|
| 164 |
<MiniMap />
|
| 165 |
+
{#if nodeSearchSettings}
|
| 166 |
+
<NodeSearch pos={nodeSearchSettings.pos} boxes={nodeSearchSettings.boxes} on:cancel={closeNodeSearch} on:add={addNode} />
|
| 167 |
+
{/if}
|
| 168 |
</SvelteFlow>
|
| 169 |
</div>
|
web/src/LynxKiteNode.svelte
CHANGED
|
@@ -3,6 +3,8 @@
|
|
| 3 |
|
| 4 |
type $$Props = NodeProps;
|
| 5 |
|
|
|
|
|
|
|
| 6 |
export let id: $$Props['id']; id;
|
| 7 |
export let data: $$Props['data'];
|
| 8 |
export let dragHandle: $$Props['dragHandle'] = undefined; dragHandle;
|
|
@@ -17,15 +19,20 @@
|
|
| 17 |
export let sourcePosition: $$Props['sourcePosition'] = undefined; sourcePosition;
|
| 18 |
export let positionAbsoluteX: $$Props['positionAbsoluteX'] = undefined; positionAbsoluteX;
|
| 19 |
export let positionAbsoluteY: $$Props['positionAbsoluteY'] = undefined; positionAbsoluteY;
|
|
|
|
| 20 |
|
| 21 |
let expanded = true;
|
| 22 |
function titleClicked() {
|
| 23 |
expanded = !expanded;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
}
|
| 25 |
</script>
|
| 26 |
|
| 27 |
-
<div class="node-container">
|
| 28 |
-
<div class="lynxkite-node">
|
| 29 |
<div class="title" on:click={titleClicked}>
|
| 30 |
{data.title}
|
| 31 |
{#if data.error}<span class="error-sign">⚠️</span>{/if}
|
|
@@ -56,15 +63,16 @@
|
|
| 56 |
}
|
| 57 |
.node-container {
|
| 58 |
padding: 8px;
|
|
|
|
|
|
|
|
|
|
| 59 |
}
|
| 60 |
.lynxkite-node {
|
| 61 |
box-shadow: 0px 5px 50px 0px rgba(0, 0, 0, 0.3);
|
| 62 |
background: white;
|
| 63 |
-
min-width: 200px;
|
| 64 |
-
max-width: 400px;
|
| 65 |
-
max-height: 400px;
|
| 66 |
overflow-y: auto;
|
| 67 |
border-radius: 1px;
|
|
|
|
| 68 |
}
|
| 69 |
.title {
|
| 70 |
background: #ff8800;
|
|
|
|
| 3 |
|
| 4 |
type $$Props = NodeProps;
|
| 5 |
|
| 6 |
+
export let nodeStyle = '';
|
| 7 |
+
export let containerStyle = '';
|
| 8 |
export let id: $$Props['id']; id;
|
| 9 |
export let data: $$Props['data'];
|
| 10 |
export let dragHandle: $$Props['dragHandle'] = undefined; dragHandle;
|
|
|
|
| 19 |
export let sourcePosition: $$Props['sourcePosition'] = undefined; sourcePosition;
|
| 20 |
export let positionAbsoluteX: $$Props['positionAbsoluteX'] = undefined; positionAbsoluteX;
|
| 21 |
export let positionAbsoluteY: $$Props['positionAbsoluteY'] = undefined; positionAbsoluteY;
|
| 22 |
+
export let onToggle = () => {};
|
| 23 |
|
| 24 |
let expanded = true;
|
| 25 |
function titleClicked() {
|
| 26 |
expanded = !expanded;
|
| 27 |
+
onToggle({ expanded });
|
| 28 |
+
}
|
| 29 |
+
function asPx(n: number) {
|
| 30 |
+
return n ? n + 'px' : undefined;
|
| 31 |
}
|
| 32 |
</script>
|
| 33 |
|
| 34 |
+
<div class="node-container" style:width={asPx(width)} style:height={asPx(height)} style={containerStyle}>
|
| 35 |
+
<div class="lynxkite-node" style={nodeStyle}>
|
| 36 |
<div class="title" on:click={titleClicked}>
|
| 37 |
{data.title}
|
| 38 |
{#if data.error}<span class="error-sign">⚠️</span>{/if}
|
|
|
|
| 63 |
}
|
| 64 |
.node-container {
|
| 65 |
padding: 8px;
|
| 66 |
+
min-width: 200px;
|
| 67 |
+
max-width: 400px;
|
| 68 |
+
max-height: 400px;
|
| 69 |
}
|
| 70 |
.lynxkite-node {
|
| 71 |
box-shadow: 0px 5px 50px 0px rgba(0, 0, 0, 0.3);
|
| 72 |
background: white;
|
|
|
|
|
|
|
|
|
|
| 73 |
overflow-y: auto;
|
| 74 |
border-radius: 1px;
|
| 75 |
+
height: 100%;
|
| 76 |
}
|
| 77 |
.title {
|
| 78 |
background: #ff8800;
|
web/src/NodeSearch.svelte
CHANGED
|
@@ -30,7 +30,8 @@
|
|
| 30 |
}
|
| 31 |
function addSelected() {
|
| 32 |
const node = {...hits[selectedIndex].item};
|
| 33 |
-
node.
|
|
|
|
| 34 |
dispatch('add', node);
|
| 35 |
}
|
| 36 |
async function lostFocus(e) {
|
|
@@ -41,9 +42,7 @@
|
|
| 41 |
|
| 42 |
</script>
|
| 43 |
|
| 44 |
-
<div class="node-search"
|
| 45 |
-
style="top: {pos.top}px; left: {pos.left}px; right: {pos.right}px; bottom: {pos.bottom}px;">
|
| 46 |
-
|
| 47 |
<input
|
| 48 |
bind:this={searchBox}
|
| 49 |
on:input={onInput}
|
|
@@ -83,7 +82,7 @@ style="top: {pos.top}px; left: {pos.left}px; right: {pos.right}px; bottom: {pos.
|
|
| 83 |
border-radius: 4px;
|
| 84 |
}
|
| 85 |
.node-search {
|
| 86 |
-
position:
|
| 87 |
width: 300px;
|
| 88 |
z-index: 5;
|
| 89 |
padding: 4px;
|
|
|
|
| 30 |
}
|
| 31 |
function addSelected() {
|
| 32 |
const node = {...hits[selectedIndex].item};
|
| 33 |
+
delete node.sub_nodes;
|
| 34 |
+
node.position = pos;
|
| 35 |
dispatch('add', node);
|
| 36 |
}
|
| 37 |
async function lostFocus(e) {
|
|
|
|
| 42 |
|
| 43 |
</script>
|
| 44 |
|
| 45 |
+
<div class="node-search" style="top: {pos.y}px; left: {pos.x}px;">
|
|
|
|
|
|
|
| 46 |
<input
|
| 47 |
bind:this={searchBox}
|
| 48 |
on:input={onInput}
|
|
|
|
| 82 |
border-radius: 4px;
|
| 83 |
}
|
| 84 |
.node-search {
|
| 85 |
+
position: fixed;
|
| 86 |
width: 300px;
|
| 87 |
z-index: 5;
|
| 88 |
padding: 4px;
|
web/src/NodeWithParamsVertical.svelte
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import { type NodeProps, useSvelteFlow } from '@xyflow/svelte';
|
| 3 |
+
import LynxKiteNode from './LynxKiteNode.svelte';
|
| 4 |
+
type $$Props = NodeProps;
|
| 5 |
+
export let id: $$Props['id'];
|
| 6 |
+
export let data: $$Props['data'];
|
| 7 |
+
const { updateNodeData } = useSvelteFlow();
|
| 8 |
+
</script>
|
| 9 |
+
|
| 10 |
+
<LynxKiteNode {...$$props} sourcePosition="bottom" targetPosition="top">
|
| 11 |
+
{#each Object.entries(data.params) as [name, value]}
|
| 12 |
+
<div class="param">
|
| 13 |
+
<label>
|
| 14 |
+
{name}<br>
|
| 15 |
+
<input
|
| 16 |
+
value={value}
|
| 17 |
+
on:input={(evt) => updateNodeData(id, { params: { ...data.params, [name]: evt.currentTarget.value } })}
|
| 18 |
+
/>
|
| 19 |
+
</label>
|
| 20 |
+
</div>
|
| 21 |
+
{/each}
|
| 22 |
+
</LynxKiteNode>
|
| 23 |
+
<style>
|
| 24 |
+
.param {
|
| 25 |
+
padding: 8px;
|
| 26 |
+
}
|
| 27 |
+
.param label {
|
| 28 |
+
font-size: 12px;
|
| 29 |
+
display: block;
|
| 30 |
+
}
|
| 31 |
+
.param input {
|
| 32 |
+
width: calc(100% - 8px);
|
| 33 |
+
}
|
| 34 |
+
</style>
|
web/src/NodeWithSubFlow.svelte
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import { type NodeProps, useNodes } from '@xyflow/svelte';
|
| 3 |
+
import LynxKiteNode from './LynxKiteNode.svelte';
|
| 4 |
+
type $$Props = NodeProps;
|
| 5 |
+
const nodes = useNodes();
|
| 6 |
+
export let id: $$Props['id'];
|
| 7 |
+
export let data: $$Props['data'];
|
| 8 |
+
let isExpanded = true;
|
| 9 |
+
function onToggle({ expanded }) {
|
| 10 |
+
isExpanded = expanded;
|
| 11 |
+
console.log('onToggle', expanded, height);
|
| 12 |
+
nodes.update((n) =>
|
| 13 |
+
n.map((node) =>
|
| 14 |
+
node.parentNode === id
|
| 15 |
+
? { ...node, hidden: !expanded }
|
| 16 |
+
: node));
|
| 17 |
+
}
|
| 18 |
+
function computeSize(nodes) {
|
| 19 |
+
let width = 200;
|
| 20 |
+
let height = 200;
|
| 21 |
+
for (const node of nodes) {
|
| 22 |
+
if (node.parentNode === id) {
|
| 23 |
+
width = Math.max(width, node.position.x + 300);
|
| 24 |
+
height = Math.max(height, node.position.y + 200);
|
| 25 |
+
}
|
| 26 |
+
}
|
| 27 |
+
return { width, height };
|
| 28 |
+
}
|
| 29 |
+
$: ({ width, height } = computeSize($nodes));
|
| 30 |
+
</script>
|
| 31 |
+
|
| 32 |
+
<LynxKiteNode
|
| 33 |
+
{...$$props}
|
| 34 |
+
width={isExpanded && width} height={isExpanded && height}
|
| 35 |
+
nodeStyle="background: transparent;" containerStyle="max-width: none; max-height: none;" {onToggle}>
|
| 36 |
+
</LynxKiteNode>
|
| 37 |
+
<style>
|
| 38 |
+
</style>
|