darabos commited on
Commit
05c2c27
·
1 Parent(s): 7d69c0d

Delete open-source packages, just depend on them. Move bio stuff into lynxkite-bio.

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .pre-commit-config.yaml +4 -14
  2. lynxkite-app/.gitignore +0 -5
  3. lynxkite-app/MANIFEST.in +0 -2
  4. lynxkite-app/README.md +0 -31
  5. lynxkite-app/pyproject.toml +0 -60
  6. lynxkite-app/src/build_frontend.py +0 -26
  7. lynxkite-app/src/lynxkite_app/__init__.py +0 -0
  8. lynxkite-app/src/lynxkite_app/__main__.py +0 -20
  9. lynxkite-app/src/lynxkite_app/crdt.py +0 -342
  10. lynxkite-app/src/lynxkite_app/main.py +0 -165
  11. lynxkite-app/src/lynxkite_app/web_assets/__init__.py +0 -1
  12. lynxkite-app/src/lynxkite_app/web_assets/assets/__init__.py +0 -1
  13. lynxkite-app/tests/test_crdt.py +0 -72
  14. lynxkite-app/tests/test_main.py +0 -57
  15. lynxkite-app/uv.lock +0 -0
  16. lynxkite-app/web/.gitignore +0 -24
  17. lynxkite-app/web/README.md +0 -7
  18. lynxkite-app/web/index.html +0 -12
  19. lynxkite-app/web/package-lock.json +0 -0
  20. lynxkite-app/web/package.json +0 -61
  21. lynxkite-app/web/playwright.config.ts +0 -30
  22. lynxkite-app/web/postcss.config.js +0 -6
  23. lynxkite-app/web/src/Code.tsx +0 -119
  24. lynxkite-app/web/src/Directory.tsx +0 -243
  25. lynxkite-app/web/src/Tooltip.tsx +0 -21
  26. lynxkite-app/web/src/apiTypes.ts +0 -65
  27. lynxkite-app/web/src/assets/favicon.ico +0 -0
  28. lynxkite-app/web/src/assets/logo.png +0 -0
  29. lynxkite-app/web/src/code-theme.ts +0 -38
  30. lynxkite-app/web/src/common.ts +0 -16
  31. lynxkite-app/web/src/index.css +0 -727
  32. lynxkite-app/web/src/main.tsx +0 -53
  33. lynxkite-app/web/src/vite-env.d.ts +0 -1
  34. lynxkite-app/web/src/workspace/EnvironmentSelector.tsx +0 -22
  35. lynxkite-app/web/src/workspace/LynxKiteEdge.tsx +0 -32
  36. lynxkite-app/web/src/workspace/LynxKiteState.ts +0 -4
  37. lynxkite-app/web/src/workspace/NodeSearch.tsx +0 -93
  38. lynxkite-app/web/src/workspace/Workspace.tsx +0 -655
  39. lynxkite-app/web/src/workspace/nodes/GraphCreationNode.tsx +0 -296
  40. lynxkite-app/web/src/workspace/nodes/Group.tsx +0 -64
  41. lynxkite-app/web/src/workspace/nodes/LynxKiteNode.tsx +0 -209
  42. lynxkite-app/web/src/workspace/nodes/ModelMappingParameter.tsx +0 -169
  43. lynxkite-app/web/src/workspace/nodes/NodeGroupParameter.tsx +0 -48
  44. lynxkite-app/web/src/workspace/nodes/NodeParameter.tsx +0 -147
  45. lynxkite-app/web/src/workspace/nodes/NodeWithComment.tsx +0 -58
  46. lynxkite-app/web/src/workspace/nodes/NodeWithImage.tsx +0 -12
  47. lynxkite-app/web/src/workspace/nodes/NodeWithMolecule.tsx +0 -68
  48. lynxkite-app/web/src/workspace/nodes/NodeWithParams.tsx +0 -44
  49. lynxkite-app/web/src/workspace/nodes/NodeWithTableView.tsx +0 -82
  50. lynxkite-app/web/src/workspace/nodes/NodeWithVisualization.tsx +0 -43
.pre-commit-config.yaml CHANGED
@@ -11,10 +11,6 @@ repos:
11
  - id: ruff
12
  args: [ --fix ]
13
  - id: ruff-format
14
- - repo: https://github.com/biomejs/pre-commit
15
- rev: v1.9.4
16
- hooks:
17
- - id: biome-check
18
  # https://github.com/astral-sh/ty/issues/269
19
  - repo: local
20
  hooks:
@@ -29,14 +25,8 @@ repos:
29
  rev: "0.23.0"
30
  hooks:
31
  - id: deptry
32
- name: deptry for lynxkite-app
33
- entry: bash -c 'cd lynxkite-app && deptry .'
34
- - id: deptry
35
- name: deptry for lynxkite-core
36
- entry: bash -c 'cd lynxkite-core && deptry .'
37
- - id: deptry
38
- name: deptry for lynxkite-graph-analytics
39
- entry: bash -c 'cd lynxkite-graph-analytics && deptry .'
40
  - id: deptry
41
- name: deptry for lynxkite-pillow-example
42
- entry: bash -c 'cd lynxkite-pillow-example && deptry .'
 
11
  - id: ruff
12
  args: [ --fix ]
13
  - id: ruff-format
 
 
 
 
14
  # https://github.com/astral-sh/ty/issues/269
15
  - repo: local
16
  hooks:
 
25
  rev: "0.23.0"
26
  hooks:
27
  - id: deptry
28
+ name: deptry for lynxkite-bio
29
+ entry: bash -c 'cd lynxkite-bio && deptry .'
 
 
 
 
 
 
30
  - id: deptry
31
+ name: deptry for lynxkite-lynxscribe
32
+ entry: bash -c 'cd lynxkite-lynxscribe && deptry .'
lynxkite-app/.gitignore DELETED
@@ -1,5 +0,0 @@
1
- /src/lynxkite_app/web_assets
2
- !/src/lynxkite_app/web_assets/__init__.py
3
- !/src/lynxkite_app/web_assets/assets/__init__.py
4
- data/
5
- !/web/tests/data
 
 
 
 
 
 
lynxkite-app/MANIFEST.in DELETED
@@ -1,2 +0,0 @@
1
- graft web
2
- prune web/node_modules
 
 
 
lynxkite-app/README.md DELETED
@@ -1,31 +0,0 @@
1
- # LynxKite MM
2
-
3
- This is an experimental rewrite of [LynxKite](https://github.com/lynxkite/lynxkite). It is not compatible with the
4
- original LynxKite. The primary goals of this rewrite are:
5
-
6
- - Target GPU clusters instead of Hadoop clusters. We use Python instead of Scala, RAPIDS instead of Apache Spark.
7
- - More extensible backend. Make it easy to add new LynxKite boxes. Make it easy to use our frontend for other purposes,
8
- configuring and executing other pipelines.
9
-
10
- ## Development
11
-
12
- To run the backend:
13
-
14
- ```bash
15
- uv pip install -e .
16
- cd ../examples && LYNXKITE_RELOAD=1 lynxkite
17
- ```
18
-
19
- To run the frontend:
20
-
21
- ```bash
22
- cd web
23
- npm i
24
- npm run dev
25
- ```
26
-
27
- To update the frontend types with the backend types:
28
-
29
- ```bash
30
- $ uv run pydantic2ts --module lynxkite_app.main --output ./web/src/apiTypes.ts --json2ts-cmd "npx json-schema-to-typescript"
31
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/pyproject.toml DELETED
@@ -1,60 +0,0 @@
1
- [project]
2
- name = "lynxkite"
3
- version = "0.1.0"
4
- description = "The LynxKite application, with web server and UI"
5
- readme = "README.md"
6
- requires-python = ">=3.11"
7
- dependencies = [
8
- "fastapi[standard]>=0.115.6",
9
- "griffe>=1.7.3",
10
- "joblib>=1.5.1",
11
- "lynxkite-core",
12
- "pycrdt-websocket>=0.16",
13
- "pycrdt>=0.12.26",
14
- "pydantic>=2.11.7",
15
- "sse-starlette>=2.2.1",
16
- "uvicorn>=0.35.0",
17
- ]
18
- classifiers = ["Private :: Do Not Upload"]
19
-
20
- [project.urls]
21
- Homepage = "https://github.com/lynxkite/lynxkite-2000/"
22
-
23
- [dependency-groups]
24
- dev = [
25
- "pydantic-to-typescript>=2.0.0",
26
- "setuptools>=80.9.0",
27
- ]
28
-
29
- [tool.uv.sources]
30
- lynxkite-core = { workspace = true }
31
-
32
- [build-system]
33
- requires = ["setuptools", "wheel", "setuptools-scm"]
34
- build-backend = "setuptools.build_meta"
35
-
36
- [tool.setuptools.packages.find]
37
- namespaces = true
38
- where = ["src"]
39
-
40
- [tool.setuptools.package-data]
41
- "lynxkite_app.web_assets" = ["*"]
42
- "lynxkite_app.web_assets.assets" = ["*"]
43
-
44
- [tool.setuptools]
45
- py-modules = ["build_frontend"]
46
- include-package-data = true
47
-
48
- [tool.setuptools.cmdclass]
49
- build_py = "build_frontend.build_py"
50
-
51
- [project.scripts]
52
- lynxkite = "lynxkite_app.__main__:main"
53
-
54
- [tool.deptry.package_module_name_map]
55
- lynxkite-core = "lynxkite"
56
- sse-starlette = "starlette"
57
-
58
- [tool.deptry.per_rule_ignores]
59
- DEP002 = ["pycrdt-websocket", "griffe"]
60
- DEP004 = ["setuptools"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/src/build_frontend.py DELETED
@@ -1,26 +0,0 @@
1
- """Customized build process for setuptools."""
2
-
3
- import subprocess
4
- from setuptools.command.build_py import build_py as _build_py
5
- from pathlib import Path
6
- import shutil
7
-
8
-
9
- class build_py(_build_py):
10
- def run(self):
11
- print("\n\nBuilding frontend...", __file__)
12
- here = Path(__file__).parent.parent
13
- frontend_dir = here / "web"
14
- package_dir = here / "src" / "lynxkite_app" / "web_assets"
15
- subprocess.check_call(["npm", "install"], cwd=frontend_dir)
16
- subprocess.check_call(["npm", "run", "build"], cwd=frontend_dir)
17
- print("files in", frontend_dir / "dist")
18
- for file in (frontend_dir / "dist").iterdir():
19
- print(file)
20
- # shutil.rmtree(package_dir)
21
- shutil.copytree(frontend_dir / "dist", package_dir, dirs_exist_ok=True)
22
- # (frontend_dir / "dist").rename(package_dir)
23
- print("files in", package_dir)
24
- for file in package_dir.iterdir():
25
- print(file)
26
- super().run()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/src/lynxkite_app/__init__.py DELETED
File without changes
lynxkite-app/src/lynxkite_app/__main__.py DELETED
@@ -1,20 +0,0 @@
1
- import uvicorn
2
- from .main import app # noqa: F401
3
- import os
4
-
5
-
6
- def main():
7
- port = int(os.environ.get("PORT", "8000"))
8
- reload = bool(os.environ.get("LYNXKITE_RELOAD", ""))
9
- uvicorn.run(
10
- "lynxkite_app.main:app",
11
- host="0.0.0.0",
12
- port=port,
13
- reload=reload,
14
- loop="asyncio",
15
- proxy_headers=True,
16
- )
17
-
18
-
19
- if __name__ == "__main__":
20
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/src/lynxkite_app/crdt.py DELETED
@@ -1,342 +0,0 @@
1
- """CRDT is used to synchronize workspace state for backend and frontend(s)."""
2
-
3
- import asyncio
4
- import contextlib
5
- import enum
6
- import pathlib
7
- import fastapi
8
- import os.path
9
- import pycrdt.websocket
10
- import pycrdt.store.file
11
- import uvicorn.protocols.utils
12
- import builtins
13
- from lynxkite.core import workspace, ops
14
-
15
- router = fastapi.APIRouter()
16
-
17
-
18
- def ws_exception_handler(exception, log):
19
- if isinstance(exception, builtins.ExceptionGroup):
20
- for ex in exception.exceptions:
21
- if not isinstance(ex, uvicorn.protocols.utils.ClientDisconnected):
22
- log.exception(ex)
23
- else:
24
- log.exception(exception)
25
- return True
26
-
27
-
28
- class WorkspaceWebsocketServer(pycrdt.websocket.WebsocketServer):
29
- async def init_room(self, name: str) -> pycrdt.websocket.YRoom:
30
- """Initialize a room for the workspace with the given name.
31
-
32
- The workspace is loaded from ".crdt" if it exists there, or from a JSON file, or a new workspace is created.
33
- """
34
- crdt_path = pathlib.Path(".crdt")
35
- path = crdt_path / f"{name}.crdt"
36
- assert path.is_relative_to(crdt_path), f"Path '{path}' is invalid"
37
- ystore = pycrdt.store.file.FileYStore(path)
38
- ydoc = pycrdt.Doc()
39
- ydoc["workspace"] = ws = pycrdt.Map()
40
- # Replay updates from the store.
41
- try:
42
- for update, timestamp in [(item[0], item[-1]) async for item in ystore.read()]:
43
- ydoc.apply_update(update)
44
- except pycrdt.store.YDocNotFound:
45
- pass
46
- if "nodes" not in ws:
47
- ws["nodes"] = pycrdt.Array()
48
- if "edges" not in ws:
49
- ws["edges"] = pycrdt.Array()
50
- if "env" not in ws:
51
- ws["env"] = next(iter(ops.CATALOGS), "unset")
52
- # We have two possible sources of truth for the workspaces, the YStore and the JSON files.
53
- # In case we didn't find the workspace in the YStore, we try to load it from the JSON files.
54
- try_to_load_workspace(ws, name)
55
- ws_simple = workspace.Workspace.model_validate(ws.to_py())
56
- clean_input(ws_simple)
57
- # Set the last known version to the current state, so we don't trigger a change event.
58
- last_known_versions[name] = ws_simple
59
- room = pycrdt.websocket.YRoom(
60
- ystore=ystore, ydoc=ydoc, exception_handler=ws_exception_handler
61
- )
62
- # We hang the YDoc pointer on the room, so it only gets garbage collected when the room does.
63
- room.ws = ws # ty: ignore[unresolved-attribute]
64
-
65
- def on_change(changes):
66
- task = asyncio.create_task(workspace_changed(name, changes, ws))
67
- # We have no way to await workspace_changed(). The best we can do is to
68
- # dereference its result after it's done, so exceptions are logged normally.
69
- task.add_done_callback(lambda t: t.result())
70
-
71
- ws.observe_deep(on_change)
72
- return room
73
-
74
- async def get_room(self, name: str) -> pycrdt.websocket.YRoom:
75
- """Get a room by name.
76
-
77
- This method overrides the parent get_room method. The original creates an empty room,
78
- with no associated Ydoc. Instead, we want to initialize the the room with a Workspace
79
- object.
80
- """
81
- if name not in self.rooms:
82
- self.rooms[name] = await self.init_room(name)
83
- room = self.rooms[name]
84
- await self.start_room(room)
85
- return room
86
-
87
-
88
- class CodeWebsocketServer(WorkspaceWebsocketServer):
89
- async def init_room(self, name: str) -> pycrdt.websocket.YRoom:
90
- """Initialize a room for a text document with the given name."""
91
- crdt_path = pathlib.Path(".crdt")
92
- path = crdt_path / f"{name}.crdt"
93
- assert path.is_relative_to(crdt_path), f"Path '{path}' is invalid"
94
- ystore = pycrdt.store.file.FileYStore(path)
95
- ydoc = pycrdt.Doc()
96
- ydoc["text"] = text = pycrdt.Text()
97
- # Replay updates from the store.
98
- try:
99
- for update, timestamp in [(item[0], item[-1]) async for item in ystore.read()]:
100
- ydoc.apply_update(update)
101
- except pycrdt.store.YDocNotFound:
102
- pass
103
- if len(text) == 0:
104
- if os.path.exists(name):
105
- with open(name, encoding="utf-8") as f:
106
- text += f.read().replace("\r\n", "\n")
107
- room = pycrdt.websocket.YRoom(
108
- ystore=ystore, ydoc=ydoc, exception_handler=ws_exception_handler
109
- )
110
- # We hang the YDoc pointer on the room, so it only gets garbage collected when the room does.
111
- room.text = text # ty: ignore[unresolved-attribute]
112
-
113
- def on_change(changes):
114
- asyncio.create_task(code_changed(name, changes, text))
115
-
116
- text.observe(on_change)
117
- return room
118
-
119
-
120
- last_ws_input = None
121
-
122
-
123
- def clean_input(ws_pyd):
124
- """Delete everything that we want to ignore for the purposes of change detection."""
125
- for node in ws_pyd.nodes:
126
- node.data.display = None
127
- node.data.input_metadata = None
128
- node.data.error = None
129
- node.data.status = workspace.NodeStatus.done
130
- for p in list(node.data.params):
131
- if p.startswith("_"):
132
- del node.data.params[p]
133
- if node.data.op_id == "Comment":
134
- node.data.params = {}
135
- node.position.x = 0
136
- node.position.y = 0
137
- node.width = 0
138
- node.height = 0
139
- if node.model_extra:
140
- for key in list(node.model_extra.keys()):
141
- delattr(node, key)
142
-
143
-
144
- def crdt_update(
145
- crdt_obj: pycrdt.Map | pycrdt.Array,
146
- python_obj: dict | list,
147
- non_collaborative_fields: set[str] = set(),
148
- ):
149
- """Update a CRDT object to match a Python object.
150
-
151
- The types between the CRDT object and the Python object must match. If the Python object
152
- is a dict, the CRDT object must be a Map. If the Python object is a list, the CRDT object
153
- must be an Array.
154
-
155
- Args:
156
- crdt_obj: The CRDT object, that will be updated to match the Python object.
157
- python_obj: The Python object to update with.
158
- non_collaborative_fields: List of fields to treat as a black box. Black boxes are
159
- updated as a whole, instead of having a fine-grained data structure to edit
160
- collaboratively. Useful for complex fields that contain auto-generated data or
161
- metadata.
162
- The default is an empty set.
163
-
164
- Raises:
165
- ValueError: If the Python object provided is not a dict or list.
166
- """
167
- if isinstance(python_obj, dict):
168
- assert isinstance(crdt_obj, pycrdt.Map), "CRDT object must be a Map for a dict input"
169
- for key, value in python_obj.items():
170
- if key in non_collaborative_fields:
171
- crdt_obj[key] = value
172
- elif isinstance(value, dict):
173
- if crdt_obj.get(key) is None:
174
- crdt_obj[key] = pycrdt.Map()
175
- crdt_update(crdt_obj[key], value, non_collaborative_fields)
176
- elif isinstance(value, list):
177
- if crdt_obj.get(key) is None:
178
- crdt_obj[key] = pycrdt.Array()
179
- crdt_update(crdt_obj[key], value, non_collaborative_fields)
180
- elif isinstance(value, enum.Enum):
181
- crdt_obj[key] = str(value.value)
182
- else:
183
- crdt_obj[key] = value
184
- elif isinstance(python_obj, list):
185
- assert isinstance(crdt_obj, pycrdt.Array), "CRDT object must be an Array for a list input"
186
- for i, value in enumerate(python_obj):
187
- if isinstance(value, dict):
188
- if i >= len(crdt_obj):
189
- crdt_obj.append(pycrdt.Map())
190
- crdt_update(crdt_obj[i], value, non_collaborative_fields)
191
- elif isinstance(value, list):
192
- if i >= len(crdt_obj):
193
- crdt_obj.append(pycrdt.Array())
194
- crdt_update(crdt_obj[i], value, non_collaborative_fields)
195
- else:
196
- if isinstance(value, enum.Enum):
197
- value = str(value.value)
198
- if i >= len(crdt_obj):
199
- crdt_obj.append(value)
200
- else:
201
- crdt_obj[i] = value
202
- else:
203
- raise ValueError("Invalid type:", python_obj)
204
-
205
-
206
- def try_to_load_workspace(ws: pycrdt.Map, name: str):
207
- """Load the workspace `name`, if it exists, and update the `ws` CRDT object to match its contents.
208
-
209
- Args:
210
- ws: CRDT object to udpate with the workspace contents.
211
- name: Name of the workspace to load.
212
- """
213
- if os.path.exists(name):
214
- ws_pyd = workspace.Workspace.load(name)
215
- crdt_update(
216
- ws,
217
- ws_pyd.model_dump(),
218
- # We treat some fields as black boxes. They are not edited on the frontend.
219
- non_collaborative_fields={"display", "input_metadata", "meta"},
220
- )
221
-
222
-
223
- last_known_versions = {}
224
- delayed_executions = {}
225
-
226
-
227
- async def workspace_changed(name: str, changes: list[pycrdt.MapEvent], ws_crdt: pycrdt.Map):
228
- """Callback to react to changes in the workspace.
229
-
230
- Args:
231
- name: Name of the workspace.
232
- changes: Changes performed to the workspace.
233
- ws_crdt: CRDT object representing the workspace.
234
- """
235
- ws_pyd = workspace.Workspace.model_validate(ws_crdt.to_py())
236
- # Do not trigger execution for superficial changes.
237
- # This is a quick solution until we build proper caching.
238
- ws_simple = ws_pyd.model_copy(deep=True)
239
- clean_input(ws_simple)
240
- if ws_simple == last_known_versions.get(name):
241
- return
242
- last_known_versions[name] = ws_simple
243
- # Frontend changes that result from typing are delayed to avoid
244
- # rerunning the workspace for every keystroke.
245
- if name in delayed_executions:
246
- delayed_executions[name].cancel()
247
- delay = min(
248
- getattr(change, "keys", {}).get("__execution_delay", {}).get("newValue", 0)
249
- for change in changes
250
- )
251
- # Check if workspace is paused - if so, skip automatic execution
252
- if getattr(ws_pyd, "paused", False):
253
- print(f"Skipping automatic execution for {name} in {ws_pyd.env} - workspace is paused")
254
- return
255
- if delay:
256
- task = asyncio.create_task(execute(name, ws_crdt, ws_pyd, delay))
257
- delayed_executions[name] = task
258
- else:
259
- await execute(name, ws_crdt, ws_pyd)
260
-
261
-
262
- async def execute(name: str, ws_crdt: pycrdt.Map, ws_pyd: workspace.Workspace, delay: int = 0):
263
- """Execute the workspace and update the CRDT object with the results.
264
-
265
- Args:
266
- name: Name of the workspace.
267
- ws_crdt: CRDT object representing the workspace.
268
- ws_pyd: Workspace object to execute.
269
- delay: Wait time before executing the workspace. The default is 0.
270
- """
271
- if delay:
272
- try:
273
- await asyncio.sleep(delay)
274
- except asyncio.CancelledError:
275
- return
276
- print(f"Running {name} in {ws_pyd.env}...")
277
- cwd = pathlib.Path()
278
- path = cwd / name
279
- assert path.is_relative_to(cwd), f"Path '{path}' is invalid"
280
- # Save user changes before executing, in case the execution fails.
281
- ws_pyd.save(path)
282
- ops.load_user_scripts(name)
283
- ws_pyd.connect_crdt(ws_crdt)
284
- ws_pyd.update_metadata()
285
- if not ws_pyd.has_executor():
286
- return
287
- with ws_crdt.doc.transaction():
288
- for nc in ws_crdt["nodes"]:
289
- nc["data"]["status"] = "planned"
290
- ws_pyd.normalize()
291
- await ws_pyd.execute()
292
- ws_pyd.save(path)
293
- print(f"Finished running {name} in {ws_pyd.env}.")
294
-
295
-
296
- async def code_changed(name: str, changes: pycrdt.TextEvent, text: pycrdt.Text):
297
- contents = str(text).strip() + "\n"
298
- with open(name, "w", encoding="utf-8") as f:
299
- f.write(contents)
300
-
301
-
302
- ws_websocket_server: WorkspaceWebsocketServer
303
- code_websocket_server: CodeWebsocketServer
304
-
305
-
306
- def get_room(name):
307
- return ws_websocket_server.get_room(name)
308
-
309
-
310
- @contextlib.asynccontextmanager
311
- async def lifespan(app):
312
- global ws_websocket_server
313
- global code_websocket_server
314
- ws_websocket_server = WorkspaceWebsocketServer(auto_clean_rooms=False)
315
- code_websocket_server = CodeWebsocketServer(auto_clean_rooms=False)
316
- async with ws_websocket_server:
317
- async with code_websocket_server:
318
- yield
319
- print("closing websocket server")
320
-
321
-
322
- def delete_room(name: str):
323
- if name in ws_websocket_server.rooms:
324
- del ws_websocket_server.rooms[name]
325
-
326
-
327
- def sanitize_path(path):
328
- return os.path.relpath(os.path.normpath(os.path.join("/", path)), "/")
329
-
330
-
331
- @router.websocket("/ws/crdt/{room_name:path}")
332
- async def crdt_websocket(websocket: fastapi.WebSocket, room_name: str):
333
- room_name = sanitize_path(room_name)
334
- server = pycrdt.websocket.ASGIServer(ws_websocket_server)
335
- await server({"path": room_name, "type": "websocket"}, websocket._receive, websocket._send)
336
-
337
-
338
- @router.websocket("/ws/code/crdt/{room_name:path}")
339
- async def code_crdt_websocket(websocket: fastapi.WebSocket, room_name: str):
340
- room_name = sanitize_path(room_name)
341
- server = pycrdt.websocket.ASGIServer(code_websocket_server)
342
- await server({"path": room_name, "type": "websocket"}, websocket._receive, websocket._send)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/src/lynxkite_app/main.py DELETED
@@ -1,165 +0,0 @@
1
- """The FastAPI server for serving the LynxKite application."""
2
-
3
- import shutil
4
- import pydantic
5
- import fastapi
6
- import importlib
7
- import joblib
8
- import pathlib
9
- import pkgutil
10
- from fastapi.staticfiles import StaticFiles
11
- from fastapi.middleware.gzip import GZipMiddleware
12
- import starlette.exceptions
13
- from lynxkite.core import ops
14
- from lynxkite.core import workspace
15
- from . import crdt
16
-
17
- mem = joblib.Memory(".joblib-cache")
18
- ops.CACHE_WRAPPER = mem.cache
19
-
20
-
21
- def detect_plugins():
22
- plugins = {}
23
- for _, name, _ in pkgutil.iter_modules():
24
- if name.startswith("lynxkite_") and name != "lynxkite_app":
25
- print(f"Importing {name}")
26
- plugins[name] = importlib.import_module(name)
27
- if not plugins:
28
- print("No LynxKite plugins found. Be sure to install some!")
29
- return plugins
30
-
31
-
32
- lynxkite_plugins = detect_plugins()
33
- ops.save_catalogs("plugins loaded")
34
-
35
- app = fastapi.FastAPI(lifespan=crdt.lifespan)
36
- app.include_router(crdt.router)
37
- app.add_middleware(GZipMiddleware)
38
-
39
-
40
- def _get_ops(env: str):
41
- catalog = ops.CATALOGS[env]
42
- res = {op.name: op.model_dump() for op in catalog.values()}
43
- res.setdefault("Comment", ops.COMMENT_OP.model_dump())
44
- return res
45
-
46
-
47
- @app.get("/api/catalog")
48
- def get_catalog(workspace: str):
49
- ops.load_user_scripts(workspace)
50
- return {env: _get_ops(env) for env in ops.CATALOGS}
51
-
52
-
53
- data_path = pathlib.Path()
54
-
55
-
56
- @app.post("/api/delete")
57
- async def delete_workspace(req: dict):
58
- json_path: pathlib.Path = data_path / req["path"]
59
- crdt_path: pathlib.Path = data_path / ".crdt" / f"{req['path']}.crdt"
60
- assert json_path.is_relative_to(data_path), f"Path '{json_path}' is invalid"
61
- json_path.unlink()
62
- crdt_path.unlink()
63
- crdt.delete_room(req["path"])
64
-
65
-
66
- class DirectoryEntry(pydantic.BaseModel):
67
- name: str
68
- type: str
69
-
70
-
71
- def _get_path_type(path: pathlib.Path) -> str:
72
- if path.is_dir():
73
- return "directory"
74
- elif path.suffixes[-2:] == [".lynxkite", ".json"]:
75
- return "workspace"
76
- else:
77
- return "file"
78
-
79
-
80
- @app.get("/api/dir/list")
81
- def list_dir(path: str):
82
- path = data_path / path
83
- assert path.is_relative_to(data_path), f"Path '{path}' is invalid"
84
- return sorted(
85
- [
86
- DirectoryEntry(
87
- name=str(p.relative_to(data_path)),
88
- type=_get_path_type(p),
89
- )
90
- for p in path.iterdir()
91
- if not p.name.startswith(".")
92
- ],
93
- key=lambda x: (x.type != "directory", x.name.lower()),
94
- )
95
-
96
-
97
- @app.post("/api/dir/mkdir")
98
- def make_dir(req: dict):
99
- path = data_path / req["path"]
100
- assert path.is_relative_to(data_path), f"Path '{path}' is invalid"
101
- assert not path.exists(), f"{path} already exists"
102
- path.mkdir()
103
-
104
-
105
- @app.post("/api/dir/delete")
106
- def delete_dir(req: dict):
107
- path: pathlib.Path = data_path / req["path"]
108
- assert all([path.is_relative_to(data_path), path.exists(), path.is_dir()]), (
109
- f"Path '{path}' is invalid"
110
- )
111
- shutil.rmtree(path)
112
-
113
-
114
- @app.get("/api/service/{module_path:path}")
115
- async def service_get(req: fastapi.Request, module_path: str):
116
- """Executors can provide extra HTTP APIs through the /api/service endpoint."""
117
- module = lynxkite_plugins[module_path.split("/")[0]]
118
- return await module.api_service_get(req)
119
-
120
-
121
- @app.post("/api/service/{module_path:path}")
122
- async def service_post(req: fastapi.Request, module_path: str):
123
- """Executors can provide extra HTTP APIs through the /api/service endpoint."""
124
- module = lynxkite_plugins[module_path.split("/")[0]]
125
- return await module.api_service_post(req)
126
-
127
-
128
- @app.post("/api/upload")
129
- async def upload(req: fastapi.Request):
130
- """Receives file uploads and stores them in DATA_PATH."""
131
- form = await req.form()
132
- for file in form.values():
133
- file_path = data_path / "uploads" / file.filename
134
- assert file_path.is_relative_to(data_path), f"Path '{file_path}' is invalid"
135
- with file_path.open("wb") as buffer:
136
- shutil.copyfileobj(file.file, buffer)
137
- return {"status": "ok"}
138
-
139
-
140
- @app.post("/api/execute_workspace")
141
- async def execute_workspace(name: str):
142
- """Trigger and await the execution of a workspace."""
143
- room = await crdt.get_room(name)
144
- ws_pyd = workspace.Workspace.model_validate(room.ws.to_py())
145
- await crdt.execute(name, room.ws, ws_pyd)
146
-
147
-
148
- class SPAStaticFiles(StaticFiles):
149
- """Route everything to index.html. https://stackoverflow.com/a/73552966/3318517"""
150
-
151
- async def get_response(self, path: str, scope):
152
- try:
153
- return await super().get_response(path, scope)
154
- except (
155
- fastapi.HTTPException,
156
- starlette.exceptions.HTTPException,
157
- ) as ex:
158
- if ex.status_code == 404:
159
- return await super().get_response(".", scope)
160
- else:
161
- raise ex
162
-
163
-
164
- static_dir = SPAStaticFiles(packages=[("lynxkite_app", "web_assets")], html=True)
165
- app.mount("/", static_dir, name="web_assets")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/src/lynxkite_app/web_assets/__init__.py DELETED
@@ -1 +0,0 @@
1
- """The build process (uv build) puts the frontend build artifacts here."""
 
 
lynxkite-app/src/lynxkite_app/web_assets/assets/__init__.py DELETED
@@ -1 +0,0 @@
1
- """More frontend build artifacts."""
 
 
lynxkite-app/tests/test_crdt.py DELETED
@@ -1,72 +0,0 @@
1
- from enum import Enum
2
- import pycrdt
3
- import pytest
4
- from lynxkite_app.crdt import crdt_update
5
-
6
-
7
- @pytest.fixture
8
- def empty_dict_workspace():
9
- ydoc = pycrdt.Doc()
10
- ydoc["workspace"] = ws = pycrdt.Map()
11
- yield ws
12
-
13
-
14
- @pytest.fixture
15
- def empty_list_workspace():
16
- ydoc = pycrdt.Doc()
17
- ydoc["workspace"] = ws = pycrdt.Array()
18
- yield ws
19
-
20
-
21
- class MyEnum(int, Enum):
22
- VALUE = 1
23
-
24
-
25
- @pytest.mark.parametrize(
26
- "python_obj,expected",
27
- [
28
- (
29
- {
30
- "key1": "value1",
31
- "key2": {
32
- "nested_key1": "nested_value1",
33
- "nested_key2": ["nested_value2"],
34
- "nested_key3": MyEnum.VALUE,
35
- },
36
- },
37
- {
38
- "key1": "value1",
39
- "key2": {
40
- "nested_key1": "nested_value1",
41
- "nested_key2": ["nested_value2"],
42
- "nested_key3": "1",
43
- },
44
- },
45
- )
46
- ],
47
- )
48
- def test_crdt_update_with_dict(empty_dict_workspace, python_obj, expected):
49
- crdt_update(empty_dict_workspace, python_obj)
50
- assert empty_dict_workspace.to_py() == expected
51
-
52
-
53
- @pytest.mark.parametrize(
54
- "python_obj,expected",
55
- [
56
- (
57
- [
58
- "value1",
59
- {"nested_key1": "nested_value1", "nested_key2": ["nested_value2"]},
60
- MyEnum.VALUE,
61
- ],
62
- [
63
- "value1",
64
- {"nested_key1": "nested_value1", "nested_key2": ["nested_value2"]},
65
- "1",
66
- ],
67
- ),
68
- ],
69
- )
70
- def test_crdt_update_with_list(empty_list_workspace, python_obj, expected):
71
- crdt_update(empty_list_workspace, python_obj)
72
- assert empty_list_workspace.to_py() == expected
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/tests/test_main.py DELETED
@@ -1,57 +0,0 @@
1
- import pathlib
2
- import uuid
3
- from fastapi.testclient import TestClient
4
- from lynxkite_app.main import app, detect_plugins
5
- from lynxkite.core import ops
6
- import os
7
-
8
-
9
- ops.user_script_root = None
10
- client = TestClient(app)
11
-
12
-
13
- def test_detect_plugins_with_plugins():
14
- # This test assumes that these plugins are installed as part of the testing process.
15
- plugins = detect_plugins()
16
- assert all(
17
- plugin in plugins.keys()
18
- for plugin in [
19
- "lynxkite_graph_analytics",
20
- "lynxkite_lynxscribe",
21
- "lynxkite_pillow_example",
22
- ]
23
- )
24
-
25
-
26
- def test_get_catalog():
27
- response = client.get("/api/catalog?workspace=test")
28
- assert response.status_code == 200
29
-
30
-
31
- def test_list_dir():
32
- test_dir = pathlib.Path() / str(uuid.uuid4())
33
- test_dir.mkdir(parents=True, exist_ok=True)
34
- dir = test_dir / "test_dir"
35
- dir.mkdir(exist_ok=True)
36
- file = test_dir / "test_file.txt"
37
- file.touch()
38
- ws = test_dir / "test_workspace.lynxkite.json"
39
- ws.touch()
40
- response = client.get(f"/api/dir/list?path={str(test_dir)}")
41
- assert response.status_code == 200
42
- assert response.json() == [
43
- {"name": f"{test_dir}/test_dir", "type": "directory"},
44
- {"name": f"{test_dir}/test_file.txt", "type": "file"},
45
- {"name": f"{test_dir}/test_workspace.lynxkite.json", "type": "workspace"},
46
- ]
47
- file.unlink()
48
- ws.unlink()
49
- dir.rmdir()
50
-
51
-
52
- def test_make_dir():
53
- dir_name = str(uuid.uuid4())
54
- response = client.post("/api/dir/mkdir", json={"path": dir_name})
55
- assert response.status_code == 200
56
- assert os.path.exists(dir_name)
57
- os.rmdir(dir_name)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/uv.lock DELETED
The diff for this file is too large to render. See raw diff
 
lynxkite-app/web/.gitignore DELETED
@@ -1,24 +0,0 @@
1
- # Logs
2
- logs
3
- *.log
4
- npm-debug.log*
5
- yarn-debug.log*
6
- yarn-error.log*
7
- pnpm-debug.log*
8
- lerna-debug.log*
9
-
10
- node_modules
11
- dist
12
- dist-ssr
13
- *.local
14
-
15
- # Editor directories and files
16
- .vscode/*
17
- !.vscode/extensions.json
18
- .idea
19
- .DS_Store
20
- *.suo
21
- *.ntvs*
22
- *.njsproj
23
- *.sln
24
- *.sw?
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/web/README.md DELETED
@@ -1,7 +0,0 @@
1
- To set up:
2
-
3
- npm i
4
-
5
- To start dev server:
6
-
7
- npm run dev
 
 
 
 
 
 
 
 
lynxkite-app/web/index.html DELETED
@@ -1,12 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <link rel="icon" type="image/svg+xml" href="/src/assets/favicon.ico" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- </head>
8
- <body>
9
- <div id="root"></div>
10
- <script type="module" src="/src/main.tsx"></script>
11
- </body>
12
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/web/package-lock.json DELETED
The diff for this file is too large to render. See raw diff
 
lynxkite-app/web/package.json DELETED
@@ -1,61 +0,0 @@
1
- {
2
- "name": "lynxkite",
3
- "private": true,
4
- "version": "0.0.0",
5
- "type": "module",
6
- "scripts": {
7
- "test": "playwright test",
8
- "dev": "npx vite",
9
- "build": "npx tsc -b && npx vite build",
10
- "preview": "npx vite preview"
11
- },
12
- "dependencies": {
13
- "@esbuild/linux-x64": "^0.25.0",
14
- "@iconify-json/tabler": "^1.2.10",
15
- "@monaco-editor/react": "^4.7.0",
16
- "@svgr/core": "^8.1.0",
17
- "@svgr/plugin-jsx": "^8.1.0",
18
- "@swc/core": "^1.10.1",
19
- "@syncedstore/core": "^0.6.0",
20
- "@syncedstore/react": "^0.6.0",
21
- "@types/node": "^22.10.1",
22
- "@xyflow/react": "^12.6.0",
23
- "3dmol": "^2.4.2",
24
- "axios": "^1.8.2",
25
- "daisyui": "^4.12.20",
26
- "echarts": "^5.5.1",
27
- "fuse.js": "^7.0.0",
28
- "jmespath": "^0.16.0",
29
- "json-schema-to-typescript": "^15.0.3",
30
- "monaco-editor": "^0.52.2",
31
- "react": "^18.3.1",
32
- "react-dom": "^18.3.1",
33
- "react-error-boundary": "^5.0.0",
34
- "react-markdown": "^9.0.1",
35
- "react-router-dom": "^7.5.2",
36
- "react-tooltip": "^5.28.1",
37
- "swr": "^2.2.5",
38
- "unplugin-icons": "^0.21.0",
39
- "y-monaco": "^0.1.6",
40
- "y-websocket": "^2.0.4",
41
- "yjs": "^13.6.20"
42
- },
43
- "devDependencies": {
44
- "@playwright/test": "^1.50.1",
45
- "@tailwindcss/typography": "^0.5.16",
46
- "@types/jmespath": "^0.15.2",
47
- "@types/node": "^22.13.1",
48
- "@types/react": "^18.3.14",
49
- "@types/react-dom": "^18.3.2",
50
- "@vitejs/plugin-react-swc": "^3.5.0",
51
- "autoprefixer": "^10.4.20",
52
- "globals": "^15.12.0",
53
- "postcss": "^8.4.49",
54
- "tailwindcss": "^3.4.16",
55
- "typescript": "~5.6.2",
56
- "vite": "^6.3.4"
57
- },
58
- "optionalDependencies": {
59
- "@rollup/rollup-linux-x64-gnu": "^4.28.1"
60
- }
61
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/web/playwright.config.ts DELETED
@@ -1,30 +0,0 @@
1
- import { defineConfig, devices } from "@playwright/test";
2
-
3
- export default defineConfig({
4
- testDir: "./tests",
5
- timeout: process.env.CI ? 30000 : 10000,
6
- fullyParallel: false,
7
- /* Fail the build on CI if you accidentally left test.only in the source code. */
8
- forbidOnly: !!process.env.CI,
9
- retries: 0,
10
- maxFailures: 3,
11
- workers: 1,
12
- reporter: process.env.CI ? [["github"], ["html"]] : "html",
13
- use: {
14
- /* Base URL to use in actions like `await page.goto('/')`. */
15
- baseURL: "http://127.0.0.1:8000",
16
- trace: "on",
17
- testIdAttribute: "data-nodeid", // Useful for easily selecting nodes using getByTestId
18
- },
19
- projects: [
20
- {
21
- name: "chromium",
22
- use: { ...devices["Desktop Chrome"] },
23
- },
24
- ],
25
- webServer: {
26
- command: "cd ../../examples && LYNXKITE_SUPPRESS_OP_ERRORS=1 lynxkite",
27
- port: 8000,
28
- reuseExistingServer: false,
29
- },
30
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/web/postcss.config.js DELETED
@@ -1,6 +0,0 @@
1
- export default {
2
- plugins: {
3
- tailwindcss: {},
4
- autoprefixer: {},
5
- },
6
- };
 
 
 
 
 
 
 
lynxkite-app/web/src/Code.tsx DELETED
@@ -1,119 +0,0 @@
1
- // Full-page editor for code files.
2
-
3
- import Editor, { type Monaco } from "@monaco-editor/react";
4
- import type { editor } from "monaco-editor";
5
- import { useEffect, useRef } from "react";
6
- import { Link } from "react-router";
7
- import { WebsocketProvider } from "y-websocket";
8
- import * as Y from "yjs";
9
- // @ts-ignore
10
- import Atom from "~icons/tabler/atom.jsx";
11
- // @ts-ignore
12
- import Backspace from "~icons/tabler/backspace.jsx";
13
- // @ts-ignore
14
- import Close from "~icons/tabler/x.jsx";
15
- import favicon from "./assets/favicon.ico";
16
- import theme from "./code-theme.ts";
17
- import { usePath } from "./common.ts";
18
-
19
- export default function Code() {
20
- const path = usePath().replace(/^[/]code[/]/, "");
21
- const parentDir = path!.split("/").slice(0, -1).join("/");
22
- const yDocRef = useRef<any>();
23
- const wsProviderRef = useRef<any>();
24
- const monacoBindingRef = useRef<any>();
25
- const yMonacoRef = useRef<any>();
26
- const yMonacoLoadingRef = useRef(false);
27
- const editorRef = useRef<any>();
28
- useEffect(() => {
29
- const loadMonaco = async () => {
30
- if (yMonacoLoadingRef.current) return;
31
- yMonacoLoadingRef.current = true;
32
- // y-monaco is gigantic. The other Monaco packages are small.
33
- yMonacoRef.current = await import("y-monaco");
34
- initCRDT();
35
- };
36
- loadMonaco();
37
- }, []);
38
- function beforeMount(monaco: Monaco) {
39
- monaco.editor.defineTheme("lynxkite", theme);
40
- }
41
- function onMount(_editor: editor.IStandaloneCodeEditor, monaco: Monaco) {
42
- // Do nothing on Ctrl+S. We save after every keypress anyway.
43
- _editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {});
44
- editorRef.current = _editor;
45
- initCRDT();
46
- }
47
- function initCRDT() {
48
- if (!yMonacoRef.current || !editorRef.current) return;
49
- if (yDocRef.current) return;
50
- yDocRef.current = new Y.Doc();
51
- const text = yDocRef.current.getText("text");
52
- const proto = location.protocol === "https:" ? "wss:" : "ws:";
53
- const encodedPath = path!
54
- .split("/")
55
- .map((segment) => encodeURIComponent(segment))
56
- .join("/");
57
- wsProviderRef.current = new WebsocketProvider(
58
- `${proto}//${location.host}/ws/code/crdt`,
59
- encodedPath!,
60
- yDocRef.current,
61
- );
62
- editorRef.current.getModel()!.setEOL(0); // https://github.com/yjs/y-monaco/issues/6
63
- monacoBindingRef.current = new yMonacoRef.current.MonacoBinding(
64
- text,
65
- editorRef.current.getModel()!,
66
- new Set([editorRef.current]),
67
- wsProviderRef.current.awareness,
68
- );
69
- }
70
- useEffect(() => {
71
- return () => {
72
- yDocRef.current?.destroy();
73
- wsProviderRef.current?.destroy();
74
- monacoBindingRef.current?.destroy();
75
- };
76
- });
77
- return (
78
- <div className="workspace">
79
- <div className="top-bar bg-neutral">
80
- <Link className="logo" to="/">
81
- <img alt="" src={favicon} />
82
- </Link>
83
- <div className="ws-name">{path}</div>
84
- <title>{path}</title>
85
- <div className="tools text-secondary">
86
- <button className="btn btn-link">
87
- <Atom />
88
- </button>
89
- <button className="btn btn-link">
90
- <Backspace />
91
- </button>
92
- <Link
93
- to={`/dir/${parentDir
94
- .split("/")
95
- .map((segment) => encodeURIComponent(segment))
96
- .join("/")}`}
97
- className="btn btn-link"
98
- >
99
- <Close />
100
- </Link>
101
- </div>
102
- </div>
103
- <Editor
104
- defaultLanguage="python"
105
- theme="lynxkite"
106
- path={path}
107
- beforeMount={beforeMount}
108
- onMount={onMount}
109
- loading={null}
110
- options={{
111
- cursorStyle: "block",
112
- cursorBlinking: "solid",
113
- minimap: { enabled: false },
114
- renderLineHighlight: "none",
115
- }}
116
- />
117
- </div>
118
- );
119
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/web/src/Directory.tsx DELETED
@@ -1,243 +0,0 @@
1
- import { useState } from "react";
2
- // The directory browser.
3
- import { Link, useNavigate } from "react-router";
4
- import useSWR from "swr";
5
- import type { DirectoryEntry } from "./apiTypes.ts";
6
- import { usePath } from "./common.ts";
7
-
8
- // @ts-ignore
9
- import File from "~icons/tabler/file";
10
- // @ts-ignore
11
- import FilePlus from "~icons/tabler/file-plus";
12
- // @ts-ignore
13
- import Folder from "~icons/tabler/folder";
14
- // @ts-ignore
15
- import FolderPlus from "~icons/tabler/folder-plus";
16
- // @ts-ignore
17
- import Home from "~icons/tabler/home";
18
- // @ts-ignore
19
- import LayoutGrid from "~icons/tabler/layout-grid";
20
- // @ts-ignore
21
- import LayoutGridAdd from "~icons/tabler/layout-grid-add";
22
- // @ts-ignore
23
- import Trash from "~icons/tabler/trash";
24
- import logo from "./assets/logo.png";
25
-
26
- function EntryCreator(props: {
27
- label: string;
28
- icon: JSX.Element;
29
- onCreate: (name: string) => void;
30
- }) {
31
- const [isCreating, setIsCreating] = useState(false);
32
- const [nameValidationError, setNameValidationError] = useState("");
33
-
34
- function validateName(name: string): boolean {
35
- if (name.includes("/")) {
36
- setNameValidationError("Name cannot contain '/' characters");
37
- return false;
38
- }
39
- if (name.trim() === "") {
40
- setNameValidationError("Name cannot be empty");
41
- return false;
42
- }
43
- setNameValidationError("");
44
- return true;
45
- }
46
-
47
- return (
48
- <>
49
- {isCreating ? (
50
- <form
51
- onSubmit={(e) => {
52
- e.preventDefault();
53
- const name = (e.target as HTMLFormElement).entryName.value.trim();
54
- if (validateName(name)) {
55
- props.onCreate(name);
56
- setIsCreating(false);
57
- }
58
- }}
59
- >
60
- <input
61
- className={`input input-ghost w-full ${nameValidationError ? "input-error" : ""}`}
62
- autoFocus
63
- type="text"
64
- name="entryName"
65
- onBlur={() => setIsCreating(false)}
66
- onChange={(e) => validateName(e.target.value)}
67
- placeholder={`${props.label} name`}
68
- />
69
- {nameValidationError && (
70
- <div
71
- className="error-message"
72
- role="alert"
73
- style={{ position: "absolute", zIndex: 10 }}
74
- >
75
- <span className="error-icon" aria-hidden="true">
76
- ⚠️
77
- </span>
78
- <span className="error-text">{nameValidationError}</span>
79
- </div>
80
- )}
81
- </form>
82
- ) : (
83
- <button type="button" onClick={() => setIsCreating(true)}>
84
- {props.icon} {props.label}
85
- </button>
86
- )}
87
- </>
88
- );
89
- }
90
-
91
- const fetcher = (url: string) => fetch(url).then((res) => res.json());
92
-
93
- export default function Directory() {
94
- const path = usePath().replace(/^[/]$|^[/]dir$|^[/]dir[/]/, "");
95
- const encodedPath = encodeURIComponent(path || "");
96
- const list = useSWR(`/api/dir/list?path=${encodedPath}`, fetcher, {
97
- dedupingInterval: 0,
98
- });
99
- const navigate = useNavigate();
100
-
101
- function link(item: DirectoryEntry) {
102
- const encodedName = encodePathSegments(item.name);
103
- if (item.type === "directory") {
104
- return `/dir/${encodedName}`;
105
- }
106
- if (item.type === "workspace") {
107
- return `/edit/${encodedName}`;
108
- }
109
- return `/code/${encodedName}`;
110
- }
111
-
112
- function shortName(item: DirectoryEntry) {
113
- return item.name
114
- .split("/")
115
- .pop()
116
- ?.replace(/[.]lynxkite[.]json$/, "");
117
- }
118
-
119
- function encodePathSegments(path: string): string {
120
- const segments = path.split("/");
121
- return segments.map((segment) => encodeURIComponent(segment)).join("/");
122
- }
123
-
124
- function newWorkspaceIn(path: string, workspaceName: string) {
125
- const pathSlash = path ? `${encodePathSegments(path)}/` : "";
126
- navigate(`/edit/${pathSlash}${encodeURIComponent(workspaceName)}.lynxkite.json`, {
127
- replace: true,
128
- });
129
- }
130
- function newCodeFile(path: string, name: string) {
131
- const pathSlash = path ? `${encodePathSegments(path)}/` : "";
132
- navigate(`/code/${pathSlash}${encodeURIComponent(name)}`, { replace: true });
133
- }
134
- async function newFolderIn(path: string, folderName: string) {
135
- const pathSlash = path ? `${path}/` : "";
136
- const res = await fetch("/api/dir/mkdir", {
137
- method: "POST",
138
- headers: { "Content-Type": "application/json" },
139
- body: JSON.stringify({ path: pathSlash + folderName }),
140
- });
141
- if (res.ok) {
142
- const pathSlash = path ? `${encodePathSegments(path)}/` : "";
143
- navigate(`/dir/${pathSlash}${encodeURIComponent(folderName)}`);
144
- } else {
145
- alert("Failed to create folder.");
146
- }
147
- }
148
-
149
- async function deleteItem(item: DirectoryEntry) {
150
- if (!window.confirm(`Are you sure you want to delete "${item.name}"?`)) return;
151
- const apiPath = item.type === "directory" ? "/api/dir/delete" : "/api/delete";
152
- await fetch(apiPath, {
153
- method: "POST",
154
- headers: { "Content-Type": "application/json" },
155
- body: JSON.stringify({ path: item.name }),
156
- });
157
- }
158
-
159
- return (
160
- <div className="directory">
161
- <div className="logo">
162
- <a href="https://lynxkite.com/">
163
- <img src={logo} className="logo-image" alt="LynxKite logo" />
164
- </a>
165
- <div className="tagline">The Complete Graph Data Science Platform</div>
166
- </div>
167
- <div className="entry-list">
168
- {list.error && <p className="error">{list.error.message}</p>}
169
- {list.isLoading && (
170
- <output className="loading spinner-border">
171
- <span className="visually-hidden">Loading...</span>
172
- </output>
173
- )}
174
-
175
- {list.data && (
176
- <>
177
- <div className="actions">
178
- <EntryCreator
179
- onCreate={(name) => {
180
- newWorkspaceIn(path || "", name);
181
- }}
182
- icon={<LayoutGridAdd />}
183
- label="New workspace"
184
- />
185
- <EntryCreator
186
- onCreate={(name) => {
187
- newCodeFile(path || "", name);
188
- }}
189
- icon={<FilePlus />}
190
- label="New code file"
191
- />
192
- <EntryCreator
193
- onCreate={(name: string) => {
194
- newFolderIn(path || "", name);
195
- }}
196
- icon={<FolderPlus />}
197
- label="New folder"
198
- />
199
- </div>
200
-
201
- {path ? (
202
- <div className="breadcrumbs">
203
- <Link to="/dir/" aria-label="home">
204
- <Home />
205
- </Link>{" "}
206
- <span className="current-folder">{path}</span>
207
- <title>{path}</title>
208
- </div>
209
- ) : (
210
- <title>LynxKite 2000:MM</title>
211
- )}
212
-
213
- {list.data.map(
214
- (item: DirectoryEntry) =>
215
- !shortName(item)?.startsWith("__") && (
216
- <div key={item.name} className="entry">
217
- <Link key={link(item)} to={link(item)}>
218
- {item.type === "directory" ? (
219
- <Folder />
220
- ) : item.type === "workspace" ? (
221
- <LayoutGrid />
222
- ) : (
223
- <File />
224
- )}
225
- <span className="entry-name">{shortName(item)}</span>
226
- </Link>
227
- <button
228
- type="button"
229
- onClick={() => {
230
- deleteItem(item);
231
- }}
232
- >
233
- <Trash />
234
- </button>
235
- </div>
236
- ),
237
- )}
238
- </>
239
- )}
240
- </div>{" "}
241
- </div>
242
- );
243
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/web/src/Tooltip.tsx DELETED
@@ -1,21 +0,0 @@
1
- import { useId } from "react";
2
- import Markdown from "react-markdown";
3
- import { Tooltip as ReactTooltip } from "react-tooltip";
4
-
5
- export default function Tooltip(props: any) {
6
- const id = useId();
7
- if (!props.doc) return null;
8
- return (
9
- <>
10
- <span data-tooltip-id={id} tabIndex={0}>
11
- {props.children}
12
- </span>
13
- <ReactTooltip id={id} className="tooltip prose" place="top-end">
14
- {props.doc.map?.(
15
- (section: any, i: number) =>
16
- section.kind === "text" && <Markdown key={i}>{section.value}</Markdown>,
17
- ) ?? <Markdown>{props.doc}</Markdown>}
18
- </ReactTooltip>
19
- </>
20
- );
21
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/web/src/apiTypes.ts DELETED
@@ -1,65 +0,0 @@
1
- /* tslint:disable */
2
- /* eslint-disable */
3
- /**
4
- /* This file was automatically generated from pydantic models by running pydantic2ts.
5
- /* Do not modify it by hand - just update the pydantic models and then re-run the script
6
- */
7
-
8
- export type NodeStatus = "planned" | "active" | "done";
9
-
10
- export interface DirectoryEntry {
11
- name: string;
12
- type: string;
13
- }
14
- export interface SaveRequest {
15
- path: string;
16
- ws: Workspace;
17
- [k: string]: unknown;
18
- }
19
- /**
20
- * A workspace is a representation of a computational graph that consists of nodes and edges.
21
- *
22
- * Each node represents an operation or task, and the edges represent the flow of data between
23
- * the nodes. Each workspace is associated with an environment, which determines the operations
24
- * that can be performed in the workspace and the execution method for the operations.
25
- */
26
- export interface Workspace {
27
- env?: string;
28
- paused?: boolean;
29
- nodes?: WorkspaceNode[];
30
- edges?: WorkspaceEdge[];
31
- [k: string]: unknown;
32
- }
33
- export interface WorkspaceNode {
34
- id: string;
35
- type: string;
36
- data: WorkspaceNodeData;
37
- position: Position;
38
- width: number;
39
- height: number;
40
- [k: string]: unknown;
41
- }
42
- export interface WorkspaceNodeData {
43
- title: string;
44
- params: {
45
- [k: string]: unknown;
46
- };
47
- display?: unknown;
48
- input_metadata?: unknown;
49
- error?: string | null;
50
- status?: NodeStatus;
51
- [k: string]: unknown;
52
- }
53
- export interface Position {
54
- x: number;
55
- y: number;
56
- [k: string]: unknown;
57
- }
58
- export interface WorkspaceEdge {
59
- id: string;
60
- source: string;
61
- target: string;
62
- sourceHandle: string;
63
- targetHandle: string;
64
- [k: string]: unknown;
65
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/web/src/assets/favicon.ico DELETED
Binary file (4.28 kB)
 
lynxkite-app/web/src/assets/logo.png DELETED
Binary file (23.4 kB)
 
lynxkite-app/web/src/code-theme.ts DELETED
@@ -1,38 +0,0 @@
1
- // A simple theme using the LynxKite colors.
2
-
3
- import type { editor } from "monaco-editor/esm/vs/editor/editor.api";
4
-
5
- const theme: editor.IStandaloneThemeData = {
6
- base: "vs-dark",
7
- inherit: true,
8
- rules: [
9
- {
10
- foreground: "ff8800",
11
- token: "keyword",
12
- },
13
- {
14
- foreground: "0088ff",
15
- fontStyle: "italic",
16
- token: "comment",
17
- },
18
- {
19
- foreground: "39bcf3",
20
- token: "string",
21
- },
22
- {
23
- foreground: "ffc600",
24
- token: "",
25
- },
26
- ],
27
- colors: {
28
- "editor.foreground": "#FFFFFF",
29
- "editor.background": "#002a4c",
30
- "editor.selectionBackground": "#0050a4",
31
- "editor.lineHighlightBackground": "#1f4662",
32
- "editorCursor.foreground": "#ffc600",
33
- "editorWhitespace.foreground": "#7f7f7fb2",
34
- "editorIndentGuide.background": "#3b5364",
35
- "editorIndentGuide.activeBackground": "#ffc600",
36
- },
37
- };
38
- export default theme;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/web/src/common.ts DELETED
@@ -1,16 +0,0 @@
1
- import { useLocation } from "react-router";
2
-
3
- export function usePath() {
4
- // Decode special characters. Drop trailing slash. (Some clients add it, e.g. Playwright.)
5
- const path = decodeURIComponent(useLocation().pathname).replace(/[/]$/, "");
6
- return path;
7
- }
8
-
9
- export const COLORS: { [key: string]: string } = {
10
- gray: "oklch(95% 0 0)",
11
- pink: "oklch(75% 0.2 0)",
12
- orange: "oklch(75% 0.2 55)",
13
- green: "oklch(75% 0.2 150)",
14
- blue: "oklch(75% 0.2 230)",
15
- purple: "oklch(75% 0.2 290)",
16
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/web/src/index.css DELETED
@@ -1,727 +0,0 @@
1
- @tailwind base;
2
- @tailwind components;
3
- @tailwind utilities;
4
-
5
- :root {
6
- font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
7
- line-height: 1.5;
8
- font-weight: 400;
9
-
10
- font-synthesis: none;
11
- text-rendering: optimizeLegibility;
12
- -webkit-font-smoothing: antialiased;
13
- -moz-osx-font-smoothing: grayscale;
14
-
15
- background: #002a4c;
16
- }
17
-
18
- img,
19
- svg {
20
- display: inline-block;
21
- }
22
-
23
- body {
24
- color: var(--foreground);
25
- background: var(--background);
26
- font-family: Arial, Helvetica, sans-serif;
27
- }
28
-
29
- .workspace {
30
- background: white;
31
- display: flex;
32
- flex-direction: column;
33
- height: 100vh;
34
-
35
- .top-bar {
36
- display: flex;
37
- justify-content: space-between;
38
- align-items: center;
39
- background: #002a4c;
40
-
41
- .ws-name {
42
- font-size: 1.5em;
43
- flex: 1;
44
- color: white;
45
- }
46
-
47
- .logo img {
48
- height: 2em;
49
- vertical-align: middle;
50
- margin: 4px;
51
- }
52
-
53
- .tools {
54
- display: flex;
55
- align-items: center;
56
-
57
- .btn {
58
- color: oklch(75% 0.13 230);
59
- font-size: 1.5em;
60
- padding: 0 10px;
61
- }
62
- }
63
- }
64
-
65
- .error {
66
- background: #ffdddd;
67
- padding: 8px;
68
- font-size: 12px;
69
- }
70
-
71
- .node-container {
72
- padding: 8px;
73
- position: relative;
74
- }
75
-
76
- .lynxkite-node {
77
- box-shadow: 0px 5px 50px 0px rgba(0, 0, 0, 0.3);
78
- border-radius: 4px;
79
- overflow: hidden;
80
- background: white;
81
- display: flex;
82
- flex-direction: column;
83
-
84
- > :not(.title, .react-flow__handle) {
85
- user-select: text;
86
- cursor: default;
87
- }
88
-
89
- .node-content {
90
- flex: 1;
91
- overflow: auto;
92
- display: flex;
93
- flex-direction: column;
94
- }
95
- }
96
-
97
- .in-group .lynxkite-node {
98
- box-shadow: 0px 1px 5px 0px rgba(0, 0, 0, 0.3);
99
- opacity: 0.3;
100
- transition: opacity 0.3s;
101
- }
102
-
103
- .in-group .lynxkite-node:hover {
104
- opacity: 1;
105
- }
106
-
107
- .node-group {
108
- box-shadow: 0px 3px 30px 0px rgba(0, 0, 0, 0.3);
109
- border-radius: 20px;
110
- border: none;
111
- background-color: white;
112
- opacity: 0.9;
113
- display: flex;
114
- flex-direction: column;
115
- align-items: end;
116
- padding: 10px 20px;
117
- }
118
-
119
- .node-group.in-group {
120
- opacity: 0.5;
121
- }
122
-
123
- .node-group-color-picker-icon {
124
- font-size: 30px;
125
- opacity: 0.1;
126
- transition: opacity 0.3s;
127
- }
128
-
129
- .node-group:hover .node-group-color-picker-icon {
130
- opacity: 1;
131
- }
132
-
133
- .color-picker-button {
134
- font-size: 30px;
135
- }
136
-
137
- .tooltip {
138
- padding: 8px;
139
- border-radius: 4px;
140
- opacity: 1;
141
- text-align: left;
142
- background: #fffa;
143
- color: black;
144
- box-shadow: 0px 5px 50px 0px rgba(0, 0, 0, 0.1);
145
- backdrop-filter: blur(10px);
146
- font-size: 16px;
147
- font-weight: initial;
148
- max-width: 300px;
149
- }
150
-
151
- .prose p {
152
- margin-bottom: 0;
153
- }
154
-
155
- .expanded .lynxkite-node {
156
- height: 100%;
157
- }
158
-
159
- .lynxkite-node .title {
160
- font-weight: bold;
161
- padding: 8px;
162
- background-image: linear-gradient(
163
- to right,
164
- var(--status-color-1),
165
- var(--status-color-2) 40%,
166
- var(--status-color-2) 60%,
167
- var(--status-color-3)
168
- );
169
- background-blend-mode: luminosity;
170
- background-size: 180% 180%;
171
- --status-color-1: #0000;
172
- --status-color-2: #0000;
173
- --status-color-3: #0000;
174
- transition: --status-color-1 0.3s, --status-color-2 0.3s, --status-color-3 0.3s;
175
- display: flex;
176
- flex-direction: row;
177
- gap: 10px;
178
-
179
- .title-title {
180
- flex: 1;
181
- }
182
- }
183
-
184
- .lynxkite-node .title.active {
185
- --status-color-1: #0000;
186
- --status-color-2: #fff4;
187
- --status-color-3: #888f;
188
- animation: active-node-gradient-animation 1.2s alternate ease-in-out infinite;
189
- }
190
-
191
- .lynxkite-node .title.planned {
192
- --status-color-1: #888f;
193
- --status-color-2: #888f;
194
- --status-color-3: #888f;
195
- }
196
-
197
- .handle-name {
198
- font-size: 10px;
199
- color: black;
200
- letter-spacing: 0.05em;
201
- text-align: right;
202
- white-space: nowrap;
203
- position: absolute;
204
- top: -5px;
205
- backdrop-filter: blur(10px);
206
- padding: 2px 8px;
207
- border-radius: 4px;
208
- visibility: hidden;
209
- }
210
-
211
- .react-flow__handle-left {
212
- left: -5px;
213
-
214
- .handle-name {
215
- right: 30px;
216
- }
217
- }
218
-
219
- .react-flow__handle-right {
220
- right: -5px;
221
-
222
- .handle-name {
223
- left: 30px;
224
- }
225
- }
226
-
227
- .react-flow__handle-top {
228
- top: -5px;
229
-
230
- .handle-name {
231
- top: -3px;
232
- left: 13px;
233
- backdrop-filter: none;
234
- }
235
- }
236
-
237
- .react-flow__handle-bottom {
238
- bottom: -5px;
239
-
240
- .handle-name {
241
- top: 0px;
242
- left: 13px;
243
- backdrop-filter: none;
244
- }
245
- }
246
-
247
- .node-container:hover .handle-name {
248
- visibility: visible;
249
- }
250
-
251
- .node-resizer {
252
- position: absolute;
253
- bottom: 8px;
254
- right: 8px;
255
- cursor: nwse-resize;
256
- }
257
-
258
- .lynxkite-node {
259
- .param {
260
- padding: 4px 8px 4px 8px;
261
- display: block;
262
- }
263
-
264
- .param-name-row {
265
- display: flex;
266
- flex-direction: row;
267
- justify-content: space-between;
268
- align-items: end;
269
- }
270
-
271
- .param-name {
272
- display: block;
273
- font-size: 10px;
274
- letter-spacing: 0.05em;
275
- margin-left: 10px;
276
- width: fit-content;
277
- padding: 2px 8px;
278
- border-radius: 4px 4px 0 0;
279
- }
280
- }
281
-
282
- .node-search {
283
- position: fixed;
284
- width: 300px;
285
- z-index: 5;
286
- padding: 4px;
287
- border-radius: 4px;
288
- border: 1px solid #888;
289
- background-color: white;
290
- max-height: -webkit-fill-available;
291
- max-height: -moz-available;
292
- display: flex;
293
- flex-direction: column;
294
-
295
- input {
296
- width: calc(100% - 26px);
297
- font-size: 20px;
298
- padding: 8px;
299
- border-radius: 4px;
300
- border: 1px solid #eee;
301
- margin: 4px;
302
- }
303
-
304
- .search-result {
305
- padding: 4px;
306
- cursor: pointer;
307
- }
308
-
309
- .search-result.selected {
310
- background-color: oklch(75% 0.2 55);
311
- border-radius: 4px;
312
- }
313
-
314
- .matches {
315
- overflow-y: auto;
316
- }
317
- }
318
-
319
- .react-flow__node-table_view {
320
- .df-head {
321
- font-weight: bold;
322
- padding: 8px;
323
- background: #f0f0f0;
324
- cursor: pointer;
325
- }
326
-
327
- dl {
328
- margin: 10px;
329
- }
330
- }
331
-
332
- .react-flow__node-comment {
333
- width: auto !important;
334
- height: auto !important;
335
- max-width: 400px;
336
-
337
- .comment-view {
338
- border-radius: 4px;
339
- padding: 5px 10px;
340
- }
341
-
342
- .comment-editor {
343
- width: 400px;
344
- box-shadow: 0px 5px 20px 0px rgba(0, 0, 0, 0.3);
345
- border-radius: 4px;
346
- padding: 5px 10px;
347
- border: 1px solid #ccc;
348
- overflow-y: hidden;
349
- }
350
- }
351
-
352
- .env-select {
353
- background: transparent;
354
- color: #39bcf3;
355
- }
356
-
357
- .workspace-message {
358
- position: absolute;
359
- left: 50%;
360
- bottom: 20px;
361
- transform: translateX(-50%);
362
- box-shadow: 0 5px 50px 0px #8008;
363
- padding: 10px 40px 10px 20px;
364
- border-radius: 5px;
365
-
366
- .close {
367
- position: absolute;
368
- right: 10px;
369
- cursor: pointer;
370
- }
371
- }
372
-
373
- .model-mapping-param {
374
- border: 1px solid var(--fallback-bc, oklch(var(--bc) / 0.2));
375
- border-collapse: separate;
376
- border-radius: 5px;
377
- padding: 5px 10px;
378
- width: 100%;
379
- }
380
-
381
- .table-viewer {
382
- td {
383
- padding: 5px 10px;
384
- vertical-align: top;
385
- }
386
-
387
- .image-in-table {
388
- max-height: 100px;
389
- }
390
-
391
- .sort-indicator {
392
- display: inline-block;
393
- width: 0;
394
- font-size: 10px;
395
- color: #0006;
396
- transform: translate(5px, -2px);
397
- }
398
- }
399
- }
400
-
401
- .params-expander {
402
- font-size: 15px;
403
- padding: 4px;
404
- color: #000a;
405
- }
406
-
407
- .flippy {
408
- transition: transform 0.5s;
409
- }
410
-
411
- .flippy.flippy-90 {
412
- transform: rotate(-90deg);
413
- }
414
-
415
- .directory {
416
- .entry-list {
417
- width: 100%;
418
- margin: 10px auto;
419
- background-color: white;
420
- border-radius: 10px;
421
- box-shadow: 0px 2px 4px;
422
- padding: 0 0 10px 0;
423
- }
424
-
425
- @media (min-width: 768px) {
426
- .entry-list {
427
- width: 768px;
428
- }
429
- }
430
-
431
- @media (min-width: 960px) {
432
- .entry-list {
433
- width: 80%;
434
- }
435
- }
436
-
437
- .logo {
438
- margin: 0;
439
- padding-top: 50px;
440
- text-align: center;
441
- }
442
-
443
- .logo-image {
444
- max-width: 50%;
445
- }
446
-
447
- .tagline {
448
- color: #39bcf3;
449
- font-size: 14px;
450
- font-weight: 500;
451
- }
452
-
453
- @media (min-width: 1400px) {
454
- .tagline {
455
- font-size: 18px;
456
- }
457
- }
458
-
459
- .actions {
460
- display: flex;
461
- justify-content: space-evenly;
462
- align-items: center;
463
- height: 50px;
464
- padding: 5px;
465
-
466
- form,
467
- button {
468
- flex: 1;
469
- }
470
- }
471
-
472
- .actions a {
473
- padding: 2px 10px;
474
- border-radius: 5px;
475
- }
476
-
477
- .actions a:hover {
478
- background: #39bcf3;
479
- color: white;
480
- }
481
-
482
- .breadcrumbs {
483
- padding-left: 10px;
484
- font-size: 20px;
485
- background: #002a4c20;
486
- }
487
-
488
- .breadcrumbs a:hover {
489
- color: #39bcf3;
490
- }
491
-
492
- .entry-list .entry {
493
- display: flex;
494
- border-bottom: 1px solid whitesmoke;
495
- color: #004165;
496
- cursor: pointer;
497
- user-select: none;
498
-
499
- a {
500
- text-decoration: none;
501
- flex: 1;
502
- padding-left: 10px;
503
- }
504
-
505
- .entry-name {
506
- padding-left: 10px;
507
- }
508
-
509
- button {
510
- padding-right: 10px;
511
- }
512
- }
513
-
514
- .entry-list .open .entry,
515
- .entry-list .entry:hover,
516
- .entry-list .entry:focus {
517
- background: #39bcf3;
518
- color: white;
519
- }
520
-
521
- .entry-list .entry:last-child {
522
- border-bottom: none;
523
- }
524
-
525
- a {
526
- text-decoration: none;
527
- }
528
-
529
- .loading {
530
- color: #39bcf3;
531
- margin: 10px;
532
- }
533
- }
534
-
535
- @keyframes active-node-gradient-animation {
536
- to {
537
- background-position-x: 100%;
538
- }
539
- }
540
-
541
- @property --status-color-1 {
542
- syntax: "<color>";
543
- initial-value: red;
544
- inherits: false;
545
- }
546
-
547
- @property --status-color-2 {
548
- syntax: "<color>";
549
- initial-value: red;
550
- inherits: false;
551
- }
552
-
553
- @property --status-color-3 {
554
- syntax: "<color>";
555
- initial-value: red;
556
- inherits: false;
557
- }
558
-
559
- .react-flow__edge.selected path.react-flow__edge-path {
560
- outline: var(--xy-selection-border, var(--xy-selection-border-default));
561
- outline-offset: 10px;
562
- border-radius: 1px;
563
- }
564
-
565
- .react-flow__handle {
566
- border-color: black;
567
- background: white;
568
- width: 20px;
569
- height: 20px;
570
- border-width: 2px;
571
- }
572
-
573
- .react-flow__arrowhead * {
574
- stroke: none;
575
- fill: black;
576
- }
577
-
578
- .react-flow__node-area {
579
- z-index: -10 !important;
580
- }
581
-
582
- .selected .node-group,
583
- .selected .comment-view,
584
- .selected .lynxkite-node {
585
- outline: var(--xy-selection-border, var(--xy-selection-border-default));
586
- outline-offset: 7.5px;
587
- }
588
-
589
- .selected .node-group {
590
- outline-offset: 20px;
591
- }
592
-
593
- .graph-creation-view {
594
- display: flex;
595
- width: 100%;
596
- margin-top: 10px;
597
- }
598
-
599
- .graph-tables,
600
- .graph-relations {
601
- flex: 1;
602
- padding-left: 10px;
603
- padding-right: 10px;
604
- }
605
-
606
- .graph-table-header {
607
- display: flex;
608
- justify-content: space-between;
609
- font-weight: bold;
610
- text-align: left;
611
- background-color: #333;
612
- color: white;
613
- padding: 10px;
614
- border-bottom: 2px solid #222;
615
- font-size: 16px;
616
- }
617
-
618
- .graph-creation-view .df-head {
619
- font-weight: bold;
620
- display: flex;
621
- justify-content: space-between;
622
- padding: 8px 12px;
623
- /* Adds a separator between rows */
624
- border-bottom: 1px solid #ccc;
625
- }
626
-
627
- /* Alternating background colors for table-like effect */
628
- .graph-creation-view .df-head:nth-child(odd) {
629
- background-color: #f9f9f9;
630
- }
631
-
632
- .graph-creation-view .df-head:nth-child(even) {
633
- background-color: #e0e0e0;
634
- }
635
-
636
- .graph-relation-attributes {
637
- display: flex;
638
- flex-direction: column;
639
- /* Adds space between each label-input pair */
640
- gap: 10px;
641
- width: 100%;
642
- }
643
-
644
- .graph-relation-attributes label {
645
- font-size: 12px;
646
- font-weight: bold;
647
- display: block;
648
- margin-bottom: 2px;
649
- /* Lighter text for labels */
650
- color: #666;
651
- }
652
-
653
- .graph-relation-attributes input {
654
- width: 100%;
655
- padding: 8px;
656
- font-size: 14px;
657
- border: 1px solid #ccc;
658
- border-radius: 4px;
659
- outline: none;
660
- }
661
-
662
- .graph-relation-attributes input:focus {
663
- /* Highlight input on focus */
664
- border-color: #007bff;
665
- }
666
-
667
- .add-relationship-button {
668
- background-color: #28a745;
669
- color: white;
670
- border: none;
671
- font-size: 16px;
672
- cursor: pointer;
673
- padding: 4px 10px;
674
- border-radius: 4px;
675
- }
676
-
677
- .add-relationship-button:hover {
678
- background-color: #218838;
679
- }
680
-
681
- .yRemoteSelection {
682
- background-color: rgb(250, 129, 0, 0.5);
683
- }
684
-
685
- .yRemoteSelectionHead {
686
- position: absolute;
687
- border-left: #ff8800 solid 2px;
688
- border-top: #ff8800 solid 2px;
689
- border-bottom: #ff8800 solid 2px;
690
- height: 100%;
691
- box-sizing: border-box;
692
- }
693
-
694
- .yRemoteSelectionHead::after {
695
- position: absolute;
696
- content: " ";
697
- border: 3px solid #ff8800;
698
- border-radius: 4px;
699
- left: -4px;
700
- top: -5px;
701
- }
702
-
703
- .error-message {
704
- display: flex;
705
- align-items: center;
706
- gap: 0.5rem;
707
- margin-top: 0.25rem;
708
- padding: 0.5rem;
709
- background-color: #fee2e2;
710
- border: 1px solid #fecaca;
711
- border-radius: 0.375rem;
712
- color: #dc2626;
713
- font-size: 0.875rem;
714
- }
715
-
716
- .error-icon {
717
- flex-shrink: 0;
718
- }
719
-
720
- .error-text {
721
- line-height: 1.4;
722
- }
723
-
724
- .input-error {
725
- border-color: #dc2626;
726
- box-shadow: 0 0 0 1px #dc2626;
727
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/web/src/main.tsx DELETED
@@ -1,53 +0,0 @@
1
- import { StrictMode } from "react";
2
- import { createRoot } from "react-dom/client";
3
- import "@xyflow/react/dist/style.css";
4
- import "./index.css";
5
- import {
6
- Link,
7
- Route,
8
- RouterProvider,
9
- createBrowserRouter,
10
- createRoutesFromElements,
11
- useRouteError,
12
- } from "react-router";
13
- import Code from "./Code.tsx";
14
- import Directory from "./Directory.tsx";
15
- import Workspace from "./workspace/Workspace.tsx";
16
-
17
- function WorkspaceError() {
18
- const error = useRouteError();
19
- const stack = error instanceof Error ? error.stack : null;
20
- return (
21
- <div className="hero min-h-screen">
22
- <div className="card bg-base-100 shadow-sm">
23
- <div className="card-body">
24
- <h2 className="card-title">Something went wrong...</h2>
25
- <pre>{stack || "Unknown error."}</pre>
26
- <div className="card-actions justify-end">
27
- <Link to="/" className="btn btn-primary">
28
- Close workspace
29
- </Link>
30
- </div>
31
- </div>
32
- </div>
33
- </div>
34
- );
35
- }
36
-
37
- const router = createBrowserRouter(
38
- createRoutesFromElements(
39
- <>
40
- <Route path="/" element={<Directory />} />
41
- <Route path="/dir" element={<Directory />} />
42
- <Route path="/dir/*" element={<Directory />} />
43
- <Route path="/edit/*" element={<Workspace />} errorElement={<WorkspaceError />} />
44
- <Route path="/code/*" element={<Code />} />
45
- </>,
46
- ),
47
- );
48
-
49
- createRoot(document.getElementById("root")!).render(
50
- <StrictMode>
51
- <RouterProvider router={router} />
52
- </StrictMode>,
53
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/web/src/vite-env.d.ts DELETED
@@ -1 +0,0 @@
1
- /// <reference types="vite/client" />
 
 
lynxkite-app/web/src/workspace/EnvironmentSelector.tsx DELETED
@@ -1,22 +0,0 @@
1
- export default function EnvironmentSelector(props: {
2
- options: string[];
3
- value: string;
4
- onChange: (val: string) => void;
5
- }) {
6
- return (
7
- <>
8
- <select
9
- className="env-select select w-full max-w-xs"
10
- name="workspace-env"
11
- value={props.value}
12
- onChange={(evt) => props.onChange(evt.currentTarget.value)}
13
- >
14
- {props.options.map((option) => (
15
- <option key={option} value={option}>
16
- {option}
17
- </option>
18
- ))}
19
- </select>
20
- </>
21
- );
22
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/web/src/workspace/LynxKiteEdge.tsx DELETED
@@ -1,32 +0,0 @@
1
- import { BaseEdge, Position } from "@xyflow/react";
2
-
3
- function addOffset(x: number, y: number, p: Position, offset: number) {
4
- if (p === Position.Top) return `${x},${y - offset}`;
5
- if (p === Position.Bottom) return `${x},${y + offset}`;
6
- if (p === Position.Left) return `${x - offset},${y}`;
7
- return `${x + offset},${y}`;
8
- }
9
-
10
- export default function LynxKiteEdge(props: any) {
11
- const offset = 0.3 * Math.hypot(props.targetX - props.sourceX, props.targetY - props.sourceY);
12
- const s = addOffset(props.sourceX, props.sourceY, props.sourcePosition, 0);
13
- const sc = addOffset(props.sourceX, props.sourceY, props.sourcePosition, offset);
14
- const tc = addOffset(props.targetX, props.targetY, props.targetPosition, offset);
15
- const t = addOffset(props.targetX, props.targetY, props.targetPosition, 0);
16
- const path = `M${s} C${sc} ${tc} ${t}`;
17
- return (
18
- <>
19
- <BaseEdge
20
- path={path}
21
- labelX={props.labelX}
22
- labelY={props.labelY}
23
- markerStart={props.markerStart}
24
- markerEnd={props.markerEnd}
25
- style={{
26
- strokeWidth: 2,
27
- stroke: "black",
28
- }}
29
- />
30
- </>
31
- );
32
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/web/src/workspace/LynxKiteState.ts DELETED
@@ -1,4 +0,0 @@
1
- import { createContext } from "react";
2
- import type { Workspace } from "../apiTypes.ts";
3
-
4
- export const LynxKiteState = createContext({ workspace: {} as Workspace });
 
 
 
 
 
lynxkite-app/web/src/workspace/NodeSearch.tsx DELETED
@@ -1,93 +0,0 @@
1
- import Fuse from "fuse.js";
2
- import { useEffect, useMemo, useRef, useState } from "react";
3
-
4
- export type OpsOp = {
5
- name: string;
6
- id: string;
7
- categories: string[];
8
- type: string;
9
- position: { x: number; y: number };
10
- params: { name: string; default: any }[];
11
- };
12
- export type Catalog = { [op: string]: OpsOp };
13
- export type Catalogs = { [env: string]: Catalog };
14
-
15
- export default function NodeSearch(props: {
16
- boxes: Catalog;
17
- onCancel: any;
18
- onAdd: any;
19
- pos: { x: number; y: number };
20
- }) {
21
- const searchBox = useRef(null as unknown as HTMLInputElement);
22
- const [searchText, setSearchText] = useState("");
23
- const fuse = useMemo(
24
- () =>
25
- new Fuse(Object.values(props.boxes), {
26
- keys: ["name"],
27
- }),
28
- [props.boxes],
29
- );
30
- const allOps = useMemo(() => {
31
- const boxes = Object.values(props.boxes).map((box) => ({ item: box }));
32
- boxes.sort((a, b) => a.item.name.localeCompare(b.item.name));
33
- return boxes;
34
- }, [props.boxes]);
35
- const hits: { item: OpsOp }[] = searchText ? fuse.search<OpsOp>(searchText) : allOps;
36
- const [selectedIndex, setSelectedIndex] = useState(0);
37
- useEffect(() => searchBox.current.focus());
38
- function typed(text: string) {
39
- setSearchText(text);
40
- setSelectedIndex(Math.max(0, Math.min(selectedIndex, hits.length - 1)));
41
- }
42
- function onKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
43
- if (e.key === "ArrowDown") {
44
- e.preventDefault();
45
- setSelectedIndex(Math.min(selectedIndex + 1, hits.length - 1));
46
- } else if (e.key === "ArrowUp") {
47
- e.preventDefault();
48
- setSelectedIndex(Math.max(selectedIndex - 1, 0));
49
- } else if (e.key === "Enter") {
50
- addSelected();
51
- } else if (e.key === "Escape") {
52
- props.onCancel();
53
- }
54
- }
55
- function addSelected() {
56
- const node = { ...hits[selectedIndex].item };
57
- node.position = props.pos;
58
- props.onAdd(node);
59
- }
60
- async function lostFocus(e: any) {
61
- // If it's a click on a result, let the click handler handle it.
62
- if (e.relatedTarget?.closest(".node-search")) return;
63
- props.onCancel();
64
- }
65
-
66
- return (
67
- <div className="node-search" style={{ top: props.pos.y, left: props.pos.x }}>
68
- <input
69
- ref={searchBox}
70
- value={searchText}
71
- onChange={(event) => typed(event.target.value)}
72
- onKeyDown={onKeyDown}
73
- onBlur={lostFocus}
74
- placeholder="Search for box"
75
- />
76
- <div className="matches">
77
- {hits.map((box, index) => (
78
- <div
79
- key={box.item.name}
80
- tabIndex={0}
81
- onFocus={() => setSelectedIndex(index)}
82
- onMouseEnter={() => setSelectedIndex(index)}
83
- onClick={addSelected}
84
- className={`search-result ${index === selectedIndex ? "selected" : ""}`}
85
- >
86
- {box.item.categories.map((category) => `${category}\u00A0›\u00A0`)}
87
- {box.item.name}
88
- </div>
89
- ))}
90
- </div>
91
- </div>
92
- );
93
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/web/src/workspace/Workspace.tsx DELETED
@@ -1,655 +0,0 @@
1
- // The LynxKite workspace editor.
2
-
3
- import { getYjsDoc, syncedStore } from "@syncedstore/core";
4
- import {
5
- type Connection,
6
- Controls,
7
- type Edge,
8
- MarkerType,
9
- type Node,
10
- ReactFlow,
11
- ReactFlowProvider,
12
- type XYPosition,
13
- applyEdgeChanges,
14
- applyNodeChanges,
15
- useReactFlow,
16
- useUpdateNodeInternals,
17
- } from "@xyflow/react";
18
- import axios from "axios";
19
- import { type MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from "react";
20
- import { Link } from "react-router";
21
- import useSWR, { type Fetcher } from "swr";
22
- import { WebsocketProvider } from "y-websocket";
23
- // @ts-ignore
24
- import Atom from "~icons/tabler/atom.jsx";
25
- // @ts-ignore
26
- import Backspace from "~icons/tabler/backspace.jsx";
27
- // @ts-ignore
28
- import UngroupIcon from "~icons/tabler/library-minus.jsx";
29
- // @ts-ignore
30
- import GroupIcon from "~icons/tabler/library-plus.jsx";
31
- // @ts-ignore
32
- import Pause from "~icons/tabler/player-pause.jsx";
33
- // @ts-ignore
34
- import Play from "~icons/tabler/player-play.jsx";
35
- // @ts-ignore
36
- import Restart from "~icons/tabler/rotate-clockwise.jsx";
37
- // @ts-ignore
38
- import Close from "~icons/tabler/x.jsx";
39
- import Tooltip from "../Tooltip.tsx";
40
- import type { WorkspaceNode, Workspace as WorkspaceType } from "../apiTypes.ts";
41
- import favicon from "../assets/favicon.ico";
42
- import { usePath } from "../common.ts";
43
- // import NodeWithTableView from './NodeWithTableView';
44
- import EnvironmentSelector from "./EnvironmentSelector";
45
- import LynxKiteEdge from "./LynxKiteEdge.tsx";
46
- import { LynxKiteState } from "./LynxKiteState";
47
- import NodeSearch, { type OpsOp, type Catalog, type Catalogs } from "./NodeSearch.tsx";
48
- import NodeWithGraphCreationView from "./nodes/GraphCreationNode.tsx";
49
- import Group from "./nodes/Group.tsx";
50
- import NodeWithComment from "./nodes/NodeWithComment.tsx";
51
- import NodeWithImage from "./nodes/NodeWithImage.tsx";
52
- import NodeWithMolecule from "./nodes/NodeWithMolecule.tsx";
53
- import NodeWithParams from "./nodes/NodeWithParams";
54
- import NodeWithTableView from "./nodes/NodeWithTableView.tsx";
55
- import NodeWithVisualization from "./nodes/NodeWithVisualization.tsx";
56
-
57
- export default function Workspace(props: any) {
58
- return (
59
- <ReactFlowProvider>
60
- <LynxKiteFlow {...props} />
61
- </ReactFlowProvider>
62
- );
63
- }
64
-
65
- function LynxKiteFlow() {
66
- const updateNodeInternals = useUpdateNodeInternals();
67
- const reactFlow = useReactFlow();
68
- const reactFlowContainer = useRef<HTMLDivElement>(null);
69
- const [nodes, setNodes] = useState([] as Node[]);
70
- const [edges, setEdges] = useState([] as Edge[]);
71
- const path = usePath().replace(/^[/]edit[/]/, "");
72
- const shortPath = path!
73
- .split("/")
74
- .pop()!
75
- .replace(/[.]lynxkite[.]json$/, "");
76
- const [state, setState] = useState({ workspace: {} as WorkspaceType });
77
- const [message, setMessage] = useState(null as string | null);
78
- const [pausedUIState, setPausedUIState] = useState(false);
79
- useEffect(() => {
80
- const state = syncedStore({ workspace: {} as WorkspaceType });
81
- setState(state);
82
- const doc = getYjsDoc(state);
83
- const proto = location.protocol === "https:" ? "wss:" : "ws:";
84
- const encodedPath = path!
85
- .split("/")
86
- .map((segment) => encodeURIComponent(segment))
87
- .join("/");
88
- const wsProvider = new WebsocketProvider(
89
- `${proto}//${location.host}/ws/crdt`,
90
- encodedPath,
91
- doc,
92
- );
93
- if (state.workspace && typeof state.workspace.paused === "undefined") {
94
- state.workspace.paused = false;
95
- }
96
- const onChange = (_update: any, origin: any, _doc: any, _tr: any) => {
97
- if (origin === wsProvider) {
98
- // An update from the CRDT. Apply it to the local state.
99
- // This is only necessary because ReactFlow keeps secret internal copies of our stuff.
100
- if (!state.workspace) return;
101
- if (!state.workspace.nodes) return;
102
- if (!state.workspace.edges) return;
103
- for (const n of state.workspace.nodes) {
104
- if (n.type !== "node_group" && n.dragHandle !== ".drag-handle") {
105
- n.dragHandle = ".drag-handle";
106
- }
107
- }
108
- const nodes = reactFlow.getNodes();
109
- const selection = nodes.filter((n) => n.selected).map((n) => n.id);
110
- const newNodes = state.workspace.nodes.map((n) =>
111
- selection.includes(n.id) ? { ...n, selected: true } : n,
112
- );
113
- setNodes([...newNodes] as Node[]);
114
- setEdges([...state.workspace.edges] as Edge[]);
115
- for (const node of state.workspace.nodes) {
116
- // Make sure the internal copies are updated.
117
- updateNodeInternals(node.id);
118
- }
119
- setPausedUIState(state.workspace.paused || false);
120
- }
121
- };
122
- doc.on("update", onChange);
123
- return () => {
124
- doc.destroy();
125
- wsProvider.destroy();
126
- };
127
- }, [path, updateNodeInternals]);
128
-
129
- const onNodesChange = useCallback(
130
- (changes: any[]) => {
131
- // An update from the UI. Apply it to the local state...
132
- setNodes((nds) => applyNodeChanges(changes, nds));
133
- // ...and to the CRDT state. (Which could be the same, except for ReactFlow's internal copies.)
134
- const wnodes = state.workspace?.nodes;
135
- if (!wnodes) return;
136
- for (const ch of changes) {
137
- const nodeIndex = wnodes.findIndex((n) => n.id === ch.id);
138
- if (nodeIndex === -1) continue;
139
- const node = wnodes[nodeIndex];
140
- if (!node) continue;
141
- // Position events sometimes come with NaN values. Ignore them.
142
- if (
143
- ch.type === "position" &&
144
- !Number.isNaN(ch.position.x) &&
145
- !Number.isNaN(ch.position.y)
146
- ) {
147
- getYjsDoc(state).transact(() => {
148
- node.position.x = ch.position.x;
149
- node.position.y = ch.position.y;
150
- });
151
- // Update edge positions.
152
- updateNodeInternals(ch.id);
153
- } else if (ch.type === "select") {
154
- } else if (ch.type === "dimensions") {
155
- getYjsDoc(state).transact(() => {
156
- node.width = ch.dimensions.width;
157
- node.height = ch.dimensions.height;
158
- });
159
- // Update edge positions when node size changes.
160
- updateNodeInternals(ch.id);
161
- } else if (ch.type === "remove") {
162
- wnodes.splice(nodeIndex, 1);
163
- } else if (ch.type === "replace") {
164
- // Ideally we would only update the parameter that changed. But ReactFlow does not give us that detail.
165
- getYjsDoc(state).transact(() => {
166
- if (node.data.collapsed !== ch.item.data.collapsed) {
167
- node.data.collapsed = ch.item.data.collapsed;
168
- // Update edge positions when node collapses/expands.
169
- setTimeout(() => updateNodeInternals(ch.id), 0);
170
- }
171
- if (node.data.__execution_delay !== ch.item.data.__execution_delay) {
172
- node.data.__execution_delay = ch.item.data.__execution_delay;
173
- }
174
- for (const [key, value] of Object.entries(ch.item.data.params)) {
175
- if (node.data.params[key] !== value) {
176
- node.data.params[key] = value;
177
- }
178
- }
179
- });
180
- } else {
181
- console.log("Unknown node change", ch);
182
- }
183
- }
184
- },
185
- [state, updateNodeInternals],
186
- );
187
- const onEdgesChange = useCallback(
188
- (changes: any[]) => {
189
- setEdges((eds) => applyEdgeChanges(changes, eds));
190
- const wedges = state.workspace?.edges;
191
- if (!wedges) return;
192
- for (const ch of changes) {
193
- const edgeIndex = wedges.findIndex((e) => e.id === ch.id);
194
- if (ch.type === "remove") {
195
- wedges.splice(edgeIndex, 1);
196
- } else if (ch.type === "select") {
197
- } else {
198
- console.log("Unknown edge change", ch);
199
- }
200
- }
201
- },
202
- [state],
203
- );
204
-
205
- const fetcher: Fetcher<Catalogs> = (resource: string, init?: RequestInit) =>
206
- fetch(resource, init).then((res) => res.json());
207
- const encodedPathForAPI = path!
208
- .split("/")
209
- .map((segment) => encodeURIComponent(segment))
210
- .join("/");
211
- const catalog = useSWR(`/api/catalog?workspace=${encodedPathForAPI}`, fetcher);
212
- const [suppressSearchUntil, setSuppressSearchUntil] = useState(0);
213
- const [nodeSearchSettings, setNodeSearchSettings] = useState(
214
- undefined as
215
- | {
216
- pos: XYPosition;
217
- boxes: Catalog;
218
- }
219
- | undefined,
220
- );
221
- const nodeTypes = useMemo(
222
- () => ({
223
- basic: NodeWithParams,
224
- visualization: NodeWithVisualization,
225
- image: NodeWithImage,
226
- table_view: NodeWithTableView,
227
- graph_creation_view: NodeWithGraphCreationView,
228
- molecule: NodeWithMolecule,
229
- comment: NodeWithComment,
230
- node_group: Group,
231
- }),
232
- [],
233
- );
234
- const edgeTypes = useMemo(
235
- () => ({
236
- default: LynxKiteEdge,
237
- }),
238
- [],
239
- );
240
-
241
- // Global keyboard shortcuts.
242
- useEffect(() => {
243
- const handleKeyDown = (event: KeyboardEvent) => {
244
- // Show the node search dialog on "/".
245
- if (nodeSearchSettings || isTypingInFormElement()) return;
246
- if (event.key === "/") {
247
- event.preventDefault();
248
- setNodeSearchSettings({
249
- pos: getBestPosition(),
250
- boxes: catalog.data![state.workspace.env!],
251
- });
252
- } else if (event.key === "r") {
253
- event.preventDefault();
254
- executeWorkspace();
255
- }
256
- };
257
- // TODO: Switch to keydown once https://github.com/xyflow/xyflow/pull/5055 is merged.
258
- document.addEventListener("keyup", handleKeyDown);
259
- return () => {
260
- document.removeEventListener("keyup", handleKeyDown);
261
- };
262
- }, [catalog.data, nodeSearchSettings, state.workspace.env]);
263
-
264
- function getBestPosition() {
265
- const W = reactFlowContainer.current!.clientWidth;
266
- const H = reactFlowContainer.current!.clientHeight;
267
- const w = 200;
268
- const h = 200;
269
- const SPEED = 20;
270
- const GAP = 50;
271
- const pos = { x: 100, y: 100 };
272
- while (pos.y < H) {
273
- // Find a position that is not occupied by a node.
274
- const fpos = reactFlow.screenToFlowPosition(pos);
275
- const occupied = state.workspace.nodes!.some((n) => {
276
- const np = n.position;
277
- return (
278
- np.x < fpos.x + w + GAP &&
279
- np.x + n.width + GAP > fpos.x &&
280
- np.y < fpos.y + h + GAP &&
281
- np.y + n.height + GAP > fpos.y
282
- );
283
- });
284
- if (!occupied) {
285
- return pos;
286
- }
287
- // Move the position to the right and down until we find a free spot.
288
- pos.x += SPEED;
289
- if (pos.x + w > W) {
290
- pos.x = 100;
291
- pos.y += SPEED;
292
- }
293
- }
294
- return { x: 100, y: 100 };
295
- }
296
-
297
- function isTypingInFormElement() {
298
- const activeElement = document.activeElement;
299
- return (
300
- activeElement &&
301
- (activeElement.tagName === "INPUT" ||
302
- activeElement.tagName === "TEXTAREA" ||
303
- (activeElement as HTMLElement).isContentEditable)
304
- );
305
- }
306
-
307
- const closeNodeSearch = useCallback(() => {
308
- setNodeSearchSettings(undefined);
309
- setSuppressSearchUntil(Date.now() + 200);
310
- }, []);
311
- const toggleNodeSearch = useCallback(
312
- (event: MouseEvent) => {
313
- if (suppressSearchUntil > Date.now()) return;
314
- if (nodeSearchSettings) {
315
- closeNodeSearch();
316
- return;
317
- }
318
- event.preventDefault();
319
- setNodeSearchSettings({
320
- pos: { x: event.clientX, y: event.clientY },
321
- boxes: catalog.data![state.workspace.env!],
322
- });
323
- },
324
- [catalog, state, nodeSearchSettings, suppressSearchUntil, closeNodeSearch],
325
- );
326
- function findFreeId(prefix: string) {
327
- let i = 1;
328
- let id = `${prefix} ${i}`;
329
- const used = new Set(state.workspace.nodes!.map((n) => n.id));
330
- while (used.has(id)) {
331
- i += 1;
332
- id = `${prefix} ${i}`;
333
- }
334
- return id;
335
- }
336
- function addNode(node: Partial<WorkspaceNode>) {
337
- state.workspace.nodes!.push(node as WorkspaceNode);
338
- setNodes([...nodes, node as WorkspaceNode]);
339
- }
340
- function nodeFromMeta(meta: OpsOp): Partial<WorkspaceNode> {
341
- const node: Partial<WorkspaceNode> = {
342
- type: meta.type,
343
- data: {
344
- meta: { value: meta },
345
- title: meta.name,
346
- op_id: meta.id,
347
- params: Object.fromEntries(meta.params.map((p) => [p.name, p.default])),
348
- },
349
- };
350
- return node;
351
- }
352
- const addNodeFromSearch = useCallback(
353
- (meta: OpsOp) => {
354
- const node = nodeFromMeta(meta);
355
- const nss = nodeSearchSettings!;
356
- node.position = reactFlow.screenToFlowPosition({
357
- x: nss.pos.x,
358
- y: nss.pos.y,
359
- });
360
- node.id = findFreeId(node.data!.title);
361
- addNode(node);
362
- closeNodeSearch();
363
- },
364
- [nodeSearchSettings, state, reactFlow, nodes, closeNodeSearch],
365
- );
366
-
367
- const onConnect = useCallback(
368
- (connection: Connection) => {
369
- setSuppressSearchUntil(Date.now() + 200);
370
- const edge = {
371
- id: `${connection.source} ${connection.sourceHandle} ${connection.target} ${connection.targetHandle}`,
372
- source: connection.source,
373
- sourceHandle: connection.sourceHandle!,
374
- target: connection.target,
375
- targetHandle: connection.targetHandle!,
376
- };
377
- state.workspace.edges!.push(edge);
378
- setEdges((oldEdges) => [...oldEdges, edge]);
379
- },
380
- [state],
381
- );
382
- const parentDir = path!.split("/").slice(0, -1).join("/");
383
- function onDragOver(e: React.DragEvent<HTMLDivElement>) {
384
- e.stopPropagation();
385
- e.preventDefault();
386
- }
387
- async function onDrop(e: React.DragEvent<HTMLDivElement>) {
388
- e.stopPropagation();
389
- e.preventDefault();
390
- const file = e.dataTransfer.files[0];
391
- const formData = new FormData();
392
- formData.append("file", file);
393
- try {
394
- await axios.post("/api/upload", formData, {
395
- onUploadProgress: (progressEvent) => {
396
- const percentCompleted = Math.round((100 * progressEvent.loaded) / progressEvent.total!);
397
- if (percentCompleted === 100) setMessage("Processing file...");
398
- else setMessage(`Uploading ${percentCompleted}%`);
399
- },
400
- });
401
- setMessage(null);
402
- const cat = catalog.data![state.workspace.env!];
403
- const node = nodeFromMeta(cat["Import file"]);
404
- node.id = findFreeId(node.data!.title);
405
- node.position = reactFlow.screenToFlowPosition({
406
- x: e.clientX,
407
- y: e.clientY,
408
- });
409
- node.data!.params.file_path = `uploads/${file.name}`;
410
- if (file.name.includes(".csv")) {
411
- node.data!.params.file_format = "csv";
412
- } else if (file.name.includes(".parquet")) {
413
- node.data!.params.file_format = "parquet";
414
- } else if (file.name.includes(".json")) {
415
- node.data!.params.file_format = "json";
416
- } else if (file.name.includes(".xls")) {
417
- node.data!.params.file_format = "excel";
418
- }
419
- addNode(node);
420
- } catch (error) {
421
- setMessage("File upload failed.");
422
- }
423
- }
424
- async function executeWorkspace() {
425
- const response = await axios.post(`/api/execute_workspace?name=${encodeURIComponent(path)}`);
426
- if (response.status !== 200) {
427
- setMessage("Workspace execution failed.");
428
- }
429
- }
430
- function togglePause() {
431
- state.workspace.paused = !state.workspace.paused;
432
- setPausedUIState(state.workspace.paused);
433
- }
434
- function deleteSelection() {
435
- const selectedNodes = nodes.filter((n) => n.selected);
436
- const selectedEdges = edges.filter((e) => e.selected);
437
- reactFlow.deleteElements({ nodes: selectedNodes, edges: selectedEdges });
438
- }
439
- function groupSelection() {
440
- const selectedNodes = nodes.filter((n) => n.selected && !n.parentId);
441
- const groupNode = {
442
- id: findFreeId("Group"),
443
- type: "node_group",
444
- position: { x: 0, y: 0 },
445
- width: 0,
446
- height: 0,
447
- data: { title: "Group", params: {} },
448
- };
449
- let top = Number.POSITIVE_INFINITY;
450
- let left = Number.POSITIVE_INFINITY;
451
- let bottom = Number.NEGATIVE_INFINITY;
452
- let right = Number.NEGATIVE_INFINITY;
453
- const PAD = 10;
454
- for (const node of selectedNodes) {
455
- if (node.position.y - PAD < top) top = node.position.y - PAD;
456
- if (node.position.x - PAD < left) left = node.position.x - PAD;
457
- if (node.position.y + PAD + node.height! > bottom)
458
- bottom = node.position.y + PAD + node.height!;
459
- if (node.position.x + PAD + node.width! > right) right = node.position.x + PAD + node.width!;
460
- }
461
- groupNode.position = {
462
- x: left,
463
- y: top,
464
- };
465
- groupNode.width = right - left;
466
- groupNode.height = bottom - top;
467
- setNodes([
468
- { ...(groupNode as WorkspaceNode), selected: true },
469
- ...nodes.map((n) =>
470
- n.selected
471
- ? {
472
- ...n,
473
- position: { x: n.position.x - left, y: n.position.y - top },
474
- parentId: groupNode.id,
475
- extent: "parent" as const,
476
- selected: false,
477
- }
478
- : n,
479
- ),
480
- ]);
481
- getYjsDoc(state).transact(() => {
482
- state.workspace.nodes!.unshift(groupNode as WorkspaceNode);
483
- const selectedNodeIds = new Set(selectedNodes.map((n) => n.id));
484
- for (const node of state.workspace.nodes!) {
485
- if (selectedNodeIds.has(node.id)) {
486
- node.position.x -= left;
487
- node.position.y -= top;
488
- node.parentId = groupNode.id;
489
- node.extent = "parent";
490
- node.selected = false;
491
- }
492
- }
493
- });
494
- }
495
- function ungroupSelection() {
496
- const groups = Object.fromEntries(
497
- nodes
498
- .filter((n) => n.selected && n.type === "node_group" && !n.parentId)
499
- .map((n) => [n.id, n]),
500
- );
501
- setNodes(
502
- nodes
503
- .filter((n) => !groups[n.id])
504
- .map((n) => {
505
- const g = groups[n.parentId!];
506
- if (!g) return n;
507
- return {
508
- ...n,
509
- position: { x: n.position.x + g.position.x, y: n.position.y + g.position.y },
510
- parentId: undefined,
511
- extent: undefined,
512
- selected: true,
513
- };
514
- }),
515
- );
516
- getYjsDoc(state).transact(() => {
517
- const wnodes = state.workspace.nodes!;
518
- for (const node of state.workspace.nodes!) {
519
- const g = groups[node.parentId as string];
520
- if (!g) continue;
521
- node.position.x += g.position.x;
522
- node.position.y += g.position.y;
523
- node.parentId = undefined;
524
- node.extent = undefined;
525
- }
526
- for (const groupId in groups) {
527
- const groupIdx = wnodes.findIndex((n) => n.id === groupId);
528
- wnodes.splice(groupIdx, 1);
529
- }
530
- });
531
- }
532
- const areMultipleNodesSelected = nodes.filter((n) => n.selected).length > 1;
533
- const isAnyGroupSelected = nodes.some((n) => n.selected && n.type === "node_group");
534
- return (
535
- <div className="workspace">
536
- <div className="top-bar bg-neutral">
537
- <Link className="logo" to="/">
538
- <img alt="" src={favicon} />
539
- </Link>
540
- <div className="ws-name">{shortPath}</div>
541
- <title>{shortPath}</title>
542
- <EnvironmentSelector
543
- options={Object.keys(catalog.data || {})}
544
- value={state.workspace.env!}
545
- onChange={(env) => {
546
- state.workspace.env = env;
547
- }}
548
- />
549
- <div className="tools text-secondary">
550
- {areMultipleNodesSelected && (
551
- <Tooltip doc="Group selected nodes">
552
- <button className="btn btn-link" onClick={groupSelection}>
553
- <GroupIcon />
554
- </button>
555
- </Tooltip>
556
- )}
557
- {isAnyGroupSelected && (
558
- <Tooltip doc="Ungroup selected nodes">
559
- <button className="btn btn-link" onClick={ungroupSelection}>
560
- <UngroupIcon />
561
- </button>
562
- </Tooltip>
563
- )}
564
- <Tooltip doc="Delete selected nodes and edges">
565
- <button className="btn btn-link" onClick={deleteSelection}>
566
- <Backspace />
567
- </button>
568
- </Tooltip>
569
- <Tooltip doc={pausedUIState ? "Resume automatic execution" : "Pause automatic execution"}>
570
- <button className="btn btn-link" onClick={togglePause}>
571
- {pausedUIState ? <Play /> : <Pause />}
572
- </button>
573
- </Tooltip>
574
- <Tooltip doc="Re-run the workspace">
575
- <button className="btn btn-link" onClick={executeWorkspace}>
576
- <Restart />
577
- </button>
578
- </Tooltip>
579
- <Tooltip doc="Close workspace">
580
- <Link
581
- className="btn btn-link"
582
- to={`/dir/${parentDir
583
- .split("/")
584
- .map((segment) => encodeURIComponent(segment))
585
- .join("/")}`}
586
- aria-label="close"
587
- >
588
- <Close />
589
- </Link>
590
- </Tooltip>
591
- </div>
592
- </div>
593
- <div
594
- style={{ height: "100%", width: "100vw" }}
595
- onDragOver={onDragOver}
596
- onDrop={onDrop}
597
- ref={reactFlowContainer}
598
- >
599
- <LynxKiteState.Provider value={state}>
600
- <ReactFlow
601
- nodes={nodes}
602
- edges={edges}
603
- nodeTypes={nodeTypes}
604
- edgeTypes={edgeTypes}
605
- fitView
606
- onNodesChange={onNodesChange}
607
- onEdgesChange={onEdgesChange}
608
- onPaneClick={toggleNodeSearch}
609
- onConnect={onConnect}
610
- proOptions={{ hideAttribution: true }}
611
- maxZoom={10}
612
- minZoom={0.2}
613
- zoomOnScroll={false}
614
- panOnScroll={true}
615
- panOnDrag={false}
616
- selectionOnDrag={true}
617
- panOnScrollSpeed={1}
618
- preventScrolling={false}
619
- defaultEdgeOptions={{
620
- markerEnd: {
621
- type: MarkerType.ArrowClosed,
622
- color: "black",
623
- width: 15,
624
- height: 15,
625
- },
626
- style: {
627
- strokeWidth: 2,
628
- stroke: "black",
629
- },
630
- }}
631
- fitViewOptions={{ maxZoom: 1 }}
632
- >
633
- <Controls />
634
- {nodeSearchSettings && (
635
- <NodeSearch
636
- pos={nodeSearchSettings.pos}
637
- boxes={nodeSearchSettings.boxes}
638
- onCancel={closeNodeSearch}
639
- onAdd={addNodeFromSearch}
640
- />
641
- )}
642
- </ReactFlow>
643
- </LynxKiteState.Provider>
644
- {message && (
645
- <div className="workspace-message">
646
- <span className="close" onClick={() => setMessage(null)}>
647
- <Close />
648
- </span>
649
- {message}
650
- </div>
651
- )}
652
- </div>
653
- </div>
654
- );
655
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/web/src/workspace/nodes/GraphCreationNode.tsx DELETED
@@ -1,296 +0,0 @@
1
- import { useReactFlow } from "@xyflow/react";
2
- import { useState } from "react";
3
- import React from "react";
4
- import Markdown from "react-markdown";
5
- // @ts-ignore
6
- import Trash from "~icons/tabler/trash";
7
- import LynxKiteNode from "./LynxKiteNode";
8
- import Table from "./Table";
9
-
10
- function toMD(v: any): string {
11
- if (typeof v === "string") {
12
- return v;
13
- }
14
- if (Array.isArray(v)) {
15
- return v.map(toMD).join("\n\n");
16
- }
17
- return JSON.stringify(v);
18
- }
19
-
20
- function displayTable(name: string, df: any) {
21
- if (df.data.length > 1) {
22
- return (
23
- <Table key={`${name}-table`} name={`${name}-table`} columns={df.columns} data={df.data} />
24
- );
25
- }
26
- if (df.data.length) {
27
- return (
28
- <dl key={`${name}-dl`}>
29
- {df.columns.map((c: string, i: number) => (
30
- <React.Fragment key={`${name}-${c}`}>
31
- <dt>{c}</dt>
32
- <dd>
33
- <Markdown>{toMD(df.data[0][i])}</Markdown>
34
- </dd>
35
- </React.Fragment>
36
- ))}
37
- </dl>
38
- );
39
- }
40
- return JSON.stringify(df.data);
41
- }
42
-
43
- function relationsToDict(relations: any[]) {
44
- if (!relations) {
45
- return {};
46
- }
47
- return Object.assign({}, ...relations.map((r: any) => ({ [r.name]: r })));
48
- }
49
-
50
- export type UpdateOptions = { delay?: number };
51
-
52
- function NodeWithGraphCreationView(props: any) {
53
- const reactFlow = useReactFlow();
54
- const [open, setOpen] = useState({} as { [name: string]: boolean });
55
- const display = props.data.display?.value;
56
- const tables = display?.dataframes || {};
57
- const singleTable = tables && Object.keys(tables).length === 1;
58
- const [relations, setRelations] = useState(relationsToDict(display?.relations) || {});
59
- const singleRelation = relations && Object.keys(relations).length === 1;
60
- function setParam(name: string, newValue: any, opts: UpdateOptions) {
61
- reactFlow.updateNodeData(props.id, {
62
- params: { ...props.data.params, [name]: newValue },
63
- __execution_delay: opts.delay || 0,
64
- });
65
- }
66
-
67
- function updateRelation(event: any, relation: any) {
68
- event.preventDefault();
69
-
70
- const updatedRelation = {
71
- ...relation,
72
- ...Object.fromEntries(new FormData(event.target).entries()),
73
- };
74
-
75
- // Avoid mutating React state directly
76
- const newRelations = { ...relations };
77
- if (relation.name !== updatedRelation.name) {
78
- delete newRelations[relation.name];
79
- }
80
- newRelations[updatedRelation.name] = updatedRelation;
81
- setRelations(newRelations);
82
- // There is some issue with how Yjs handles complex objects (maps, arrays)
83
- // so we need to serialize the relations object to a string
84
- setParam("relations", JSON.stringify(newRelations), {});
85
- }
86
-
87
- const addRelation = () => {
88
- const new_relation = {
89
- name: "new_relation",
90
- df: "",
91
- source_column: "",
92
- target_column: "",
93
- source_table: "",
94
- target_table: "",
95
- source_key: "",
96
- target_key: "",
97
- };
98
- setRelations({
99
- ...relations,
100
- [new_relation.name]: new_relation,
101
- });
102
- setOpen({ ...open, [new_relation.name]: true });
103
- };
104
-
105
- const deleteRelation = (relation: any) => {
106
- const newOpen = { ...open };
107
- delete newOpen[relation.name];
108
- setOpen(newOpen);
109
- const newRelations = { ...relations };
110
- delete newRelations[relation.name];
111
- setRelations(newRelations);
112
- // There is some issue with how Yjs handles complex objects (maps, arrays)
113
- // so we need to serialize the relations object to a string
114
- setParam("relations", JSON.stringify(newRelations), {});
115
- };
116
-
117
- function displayRelation(relation: any) {
118
- // TODO: Dynamic autocomplete
119
- return (
120
- <form
121
- className="graph-relation-attributes"
122
- onSubmit={(e) => {
123
- updateRelation(e, relation);
124
- }}
125
- >
126
- <label htmlFor="name">Name:</label>
127
- <input type="text" id="name" name="name" defaultValue={relation.name} />
128
-
129
- <label htmlFor="df">DataFrame:</label>
130
- <input
131
- type="text"
132
- id="df"
133
- name="df"
134
- defaultValue={relation.df}
135
- list="df-options"
136
- required
137
- />
138
-
139
- <label htmlFor="source_column">Source Column:</label>
140
- <input
141
- type="text"
142
- id="source_column"
143
- name="source_column"
144
- defaultValue={relation.source_column}
145
- list="edges-column-options"
146
- required
147
- />
148
-
149
- <label htmlFor="target_column">Target Column:</label>
150
- <input
151
- type="text"
152
- id="target_column"
153
- name="target_column"
154
- defaultValue={relation.target_column}
155
- list="edges-column-options"
156
- required
157
- />
158
-
159
- <label htmlFor="source_table">Source Table:</label>
160
- <input
161
- type="text"
162
- id="source_table"
163
- name="source_table"
164
- defaultValue={relation.source_table}
165
- list="df-options"
166
- required
167
- />
168
-
169
- <label htmlFor="target_table">Target Table:</label>
170
- <input
171
- type="text"
172
- id="target_table"
173
- name="target_table"
174
- defaultValue={relation.target_table}
175
- list="df-options"
176
- required
177
- />
178
-
179
- <label htmlFor="source_key">Source Key:</label>
180
- <input
181
- type="text"
182
- id="source_key"
183
- name="source_key"
184
- defaultValue={relation.source_key}
185
- list="source-node-column-options"
186
- required
187
- />
188
-
189
- <label htmlFor="target_key">Target Key:</label>
190
- <input
191
- type="text"
192
- id="target_key"
193
- name="target_key"
194
- defaultValue={relation.target_key}
195
- list="target-node-column-options"
196
- required
197
- />
198
-
199
- <datalist id="df-options">
200
- {Object.keys(tables).map((name) => (
201
- <option key={name} value={name} />
202
- ))}
203
- </datalist>
204
-
205
- <datalist id="edges-column-options">
206
- {tables[relation.source_table] &&
207
- tables[relation.df].columns.map((name: string) => <option key={name} value={name} />)}
208
- </datalist>
209
-
210
- <datalist id="source-node-column-options">
211
- {tables[relation.source_table] &&
212
- tables[relation.source_table].columns.map((name: string) => (
213
- <option key={name} value={name} />
214
- ))}
215
- </datalist>
216
-
217
- <datalist id="target-node-column-options">
218
- {tables[relation.source_table] &&
219
- tables[relation.target_table].columns.map((name: string) => (
220
- <option key={name} value={name} />
221
- ))}
222
- </datalist>
223
-
224
- <button className="submit-relationship-button" type="submit">
225
- Create
226
- </button>
227
- </form>
228
- );
229
- }
230
-
231
- return (
232
- <div className="graph-creation-view">
233
- <div className="graph-tables">
234
- <div className="graph-table-header">Node Tables</div>
235
- {display && [
236
- Object.entries(tables).map(([name, df]: [string, any]) => (
237
- <React.Fragment key={name}>
238
- {!singleTable && (
239
- <div
240
- key={`${name}-header`}
241
- className="df-head"
242
- onClick={() => setOpen({ ...open, [name]: !open[name] })}
243
- >
244
- {name}
245
- </div>
246
- )}
247
- {(singleTable || open[name]) && displayTable(name, df)}
248
- </React.Fragment>
249
- )),
250
- Object.entries(display.others || {}).map(([name, o]) => (
251
- <>
252
- <div
253
- key={name}
254
- className="df-head"
255
- onClick={() => setOpen({ ...open, [name]: !open[name] })}
256
- >
257
- {name}
258
- </div>
259
- {open[name] && <pre>{(o as any).toString()}</pre>}
260
- </>
261
- )),
262
- ]}
263
- </div>
264
- <div className="graph-relations">
265
- <div className="graph-table-header">
266
- Relationships
267
- <button className="add-relationship-button" onClick={(_) => addRelation()}>
268
- +
269
- </button>
270
- </div>
271
- {relations &&
272
- Object.entries(relations).map(([name, relation]: [string, any]) => (
273
- <React.Fragment key={name}>
274
- <div
275
- key={`${name}-header`}
276
- className="df-head"
277
- onClick={() => setOpen({ ...open, [name]: !open[name] })}
278
- >
279
- {name}
280
- <button
281
- onClick={() => {
282
- deleteRelation(relation);
283
- }}
284
- >
285
- <Trash />
286
- </button>
287
- </div>
288
- {(singleRelation || open[name]) && displayRelation(relation)}
289
- </React.Fragment>
290
- ))}
291
- </div>
292
- </div>
293
- );
294
- }
295
-
296
- export default LynxKiteNode(NodeWithGraphCreationView);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/web/src/workspace/nodes/Group.tsx DELETED
@@ -1,64 +0,0 @@
1
- import { useReactFlow } from "@xyflow/react";
2
- import { useState } from "react";
3
- // @ts-ignore
4
- import Palette from "~icons/tabler/palette-filled.jsx";
5
- // @ts-ignore
6
- import Square from "~icons/tabler/square-filled.jsx";
7
- import Tooltip from "../../Tooltip.tsx";
8
- import { COLORS } from "../../common.ts";
9
-
10
- export default function Group(props: any) {
11
- const reactFlow = useReactFlow();
12
- const [displayingColorPicker, setDisplayingColorPicker] = useState(false);
13
- function setColor(newValue: string) {
14
- reactFlow.updateNodeData(props.id, (prevData: any) => ({
15
- ...prevData,
16
- params: { color: newValue },
17
- }));
18
- setDisplayingColorPicker(false);
19
- }
20
- function toggleColorPicker(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) {
21
- e.stopPropagation();
22
- setDisplayingColorPicker(!displayingColorPicker);
23
- }
24
- const currentColor = props.data?.params?.color || "gray";
25
- return (
26
- <div
27
- className={`node-group ${props.parentId ? "in-group" : ""}`}
28
- style={{
29
- width: props.width,
30
- height: props.height,
31
- backgroundColor: COLORS[currentColor],
32
- }}
33
- >
34
- <button
35
- className="node-group-color-picker-icon"
36
- onClick={toggleColorPicker}
37
- aria-label="Change group color"
38
- >
39
- <Tooltip doc="Change color">
40
- <Palette />
41
- </Tooltip>
42
- </button>
43
- {displayingColorPicker && <ColorPicker currentColor={currentColor} onPick={setColor} />}
44
- </div>
45
- );
46
- }
47
-
48
- function ColorPicker(props: { currentColor: string; onPick: (color: string) => void }) {
49
- const colors = Object.keys(COLORS).filter((color) => color !== props.currentColor);
50
- return (
51
- <div className="color-picker">
52
- {colors.map((color) => (
53
- <button
54
- key={color}
55
- className="color-picker-button"
56
- style={{ color: COLORS[color] }}
57
- onClick={() => props.onPick(color)}
58
- >
59
- <Square />
60
- </button>
61
- ))}
62
- </div>
63
- );
64
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/web/src/workspace/nodes/LynxKiteNode.tsx DELETED
@@ -1,209 +0,0 @@
1
- import { Handle, NodeResizeControl, type Position, useReactFlow } from "@xyflow/react";
2
- import React from "react";
3
- import { ErrorBoundary } from "react-error-boundary";
4
- // @ts-ignore
5
- import AlertTriangle from "~icons/tabler/alert-triangle-filled.jsx";
6
- // @ts-ignore
7
- import ChevronDownRight from "~icons/tabler/chevron-down-right.jsx";
8
- // @ts-ignore
9
- import Dots from "~icons/tabler/dots.jsx";
10
- // @ts-ignore
11
- import Help from "~icons/tabler/question-mark.jsx";
12
- // @ts-ignore
13
- import Skull from "~icons/tabler/skull.jsx";
14
- import Tooltip from "../../Tooltip";
15
- import { COLORS } from "../../common.ts";
16
-
17
- interface LynxKiteNodeProps {
18
- id: string;
19
- width: number;
20
- height: number;
21
- nodeStyle: any;
22
- data: any;
23
- children: any;
24
- parentId?: string;
25
- }
26
-
27
- function getHandles(inputs: any[], outputs: any[]) {
28
- const handles: {
29
- position: "top" | "bottom" | "left" | "right";
30
- name: string;
31
- index: number;
32
- offsetPercentage: number;
33
- showLabel: boolean;
34
- type: "source" | "target";
35
- }[] = [];
36
- for (const e of inputs) {
37
- handles.push({ ...e, type: "target" });
38
- }
39
- for (const e of outputs) {
40
- handles.push({ ...e, type: "source" });
41
- }
42
- const counts = { top: 0, bottom: 0, left: 0, right: 0 };
43
- for (const e of handles) {
44
- e.index = counts[e.position];
45
- counts[e.position]++;
46
- }
47
- const simpleHorizontal =
48
- counts.top === 0 && counts.bottom === 0 && counts.left <= 1 && counts.right <= 1;
49
- const simpleVertical =
50
- counts.left === 0 && counts.right === 0 && counts.top <= 1 && counts.bottom <= 1;
51
- for (const e of handles) {
52
- e.offsetPercentage = (100 * (e.index + 1)) / (counts[e.position] + 1);
53
- e.showLabel = !simpleHorizontal && !simpleVertical;
54
- }
55
- return handles;
56
- }
57
-
58
- function canScrollX(element: HTMLElement) {
59
- const style = getComputedStyle(element);
60
- return style.overflowX === "auto" || style.overflow === "auto";
61
- }
62
- function canScrollY(element: HTMLElement) {
63
- const style = getComputedStyle(element);
64
- return style.overflowY === "auto" || style.overflow === "auto";
65
- }
66
- function canScrollUp(e: HTMLElement) {
67
- return canScrollY(e) && e.scrollTop > 0;
68
- }
69
- function canScrollDown(e: HTMLElement) {
70
- return canScrollY(e) && e.scrollTop < e.scrollHeight - e.clientHeight - 1;
71
- }
72
- function canScrollLeft(e: HTMLElement) {
73
- return canScrollX(e) && e.scrollLeft > 0;
74
- }
75
- function canScrollRight(e: HTMLElement) {
76
- return canScrollX(e) && e.scrollLeft < e.scrollWidth - e.clientWidth - 1;
77
- }
78
-
79
- function onWheel(e: WheelEvent) {
80
- if (e.ctrlKey) return; // Zoom, not scroll.
81
- let t = e.target as HTMLElement;
82
- // If we find an element inside the node container that can apply this scroll event, we stop propagation.
83
- // Otherwise ReactFlow can have it and pan the workspace.
84
- while (t && !t.classList.contains("node-container")) {
85
- if (
86
- (e.deltaX < 0 && canScrollLeft(t)) ||
87
- (e.deltaX > 0 && canScrollRight(t)) ||
88
- (e.deltaY < 0 && canScrollUp(t)) ||
89
- (e.deltaY > 0 && canScrollDown(t))
90
- ) {
91
- e.stopPropagation();
92
- return;
93
- }
94
- t = t.parentElement as HTMLElement;
95
- }
96
- }
97
-
98
- function LynxKiteNodeComponent(props: LynxKiteNodeProps) {
99
- const reactFlow = useReactFlow();
100
- const containerRef = React.useRef<HTMLDivElement>(null);
101
- const data = props.data;
102
- const expanded = !data.collapsed;
103
- const handles = getHandles(data.meta?.value?.inputs || [], data.meta?.value?.outputs || []);
104
- React.useEffect(() => {
105
- // ReactFlow handles wheel events to zoom/pan and this would prevent scrolling inside the node.
106
- // To stop the event from reaching ReactFlow, we stop propagation on the wheel event.
107
- // This must be done with a "passive: false" listener, which we can only register like this.
108
- containerRef.current?.addEventListener("wheel", onWheel, {
109
- passive: false,
110
- });
111
- return () => {
112
- containerRef.current?.removeEventListener("wheel", onWheel);
113
- };
114
- }, [containerRef]);
115
- function titleClicked() {
116
- reactFlow.updateNodeData(props.id, { collapsed: expanded });
117
- }
118
- const handleOffsetDirection = {
119
- top: "left",
120
- bottom: "left",
121
- left: "top",
122
- right: "top",
123
- };
124
- const titleStyle: { backgroundColor?: string } = {};
125
- if (data.meta?.value?.color) {
126
- titleStyle.backgroundColor = COLORS[data.meta.value.color] || data.meta.value.color;
127
- }
128
- return (
129
- <div
130
- className={`node-container ${expanded ? "expanded" : "collapsed"} ${props.parentId ? "in-group" : ""}`}
131
- style={{
132
- width: props.width || 200,
133
- height: expanded ? props.height || 200 : undefined,
134
- }}
135
- ref={containerRef}
136
- >
137
- <div className="lynxkite-node" style={props.nodeStyle}>
138
- <div
139
- className={`title bg-primary drag-handle ${data.status}`}
140
- style={titleStyle}
141
- onClick={titleClicked}
142
- >
143
- <span className="title-title">{data.title}</span>
144
- {data.error && (
145
- <Tooltip doc={`Error: ${data.error}`}>
146
- <AlertTriangle />
147
- </Tooltip>
148
- )}
149
- {expanded || (
150
- <Tooltip doc="Click to expand node">
151
- <Dots />
152
- </Tooltip>
153
- )}
154
- <Tooltip doc={data.meta?.value?.doc}>
155
- <Help />
156
- </Tooltip>
157
- </div>
158
- {expanded && (
159
- <>
160
- {data.error && <div className="error">{data.error}</div>}
161
- <ErrorBoundary
162
- resetKeys={[props]}
163
- fallback={
164
- <p className="error" style={{ display: "flex", alignItems: "center", gap: 8 }}>
165
- <Skull style={{ fontSize: 20 }} />
166
- Failed to display this node.
167
- </p>
168
- }
169
- >
170
- <div className="node-content">{props.children}</div>
171
- </ErrorBoundary>
172
- <NodeResizeControl
173
- minWidth={100}
174
- minHeight={50}
175
- style={{ background: "transparent", border: "none" }}
176
- >
177
- <ChevronDownRight className="node-resizer" />
178
- </NodeResizeControl>
179
- </>
180
- )}
181
- {handles.map((handle) => (
182
- <Handle
183
- key={`${handle.name} on ${handle.position}`}
184
- id={handle.name}
185
- type={handle.type}
186
- position={handle.position as Position}
187
- style={{
188
- [handleOffsetDirection[handle.position]]: `${handle.offsetPercentage}% `,
189
- }}
190
- >
191
- {handle.showLabel && (
192
- <span className="handle-name">{handle.name.replace(/_/g, " ")}</span>
193
- )}
194
- </Handle>
195
- ))}
196
- </div>
197
- </div>
198
- );
199
- }
200
-
201
- export default function LynxKiteNode(Component: React.ComponentType<any>) {
202
- return (props: any) => {
203
- return (
204
- <LynxKiteNodeComponent {...props}>
205
- <Component {...props} />
206
- </LynxKiteNodeComponent>
207
- );
208
- };
209
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/web/src/workspace/nodes/ModelMappingParameter.tsx DELETED
@@ -1,169 +0,0 @@
1
- import { useRef } from "react";
2
- // @ts-ignore
3
- import ArrowsHorizontal from "~icons/tabler/arrows-horizontal.jsx";
4
- // @ts-ignore
5
- import Help from "~icons/tabler/question-mark.jsx";
6
- import ParameterInput from "./ParameterInput";
7
-
8
- type Bindings = {
9
- [key: string]: {
10
- df: string;
11
- column: string;
12
- };
13
- };
14
-
15
- type NamedId = {
16
- name: string;
17
- id: string;
18
- };
19
-
20
- function getModelBindings(
21
- data: any,
22
- variant: "training input" | "inference input" | "output",
23
- ): NamedId[] {
24
- function bindingsOfModel(m: any): string[] {
25
- switch (variant) {
26
- case "training input":
27
- return [
28
- ...m.model_inputs,
29
- ...m.loss_inputs.filter((i: string) => !m.model_outputs.includes(i)),
30
- ];
31
- case "inference input":
32
- return m.model_inputs;
33
- case "output":
34
- return m.model_outputs;
35
- }
36
- }
37
- const bindings = new Set<NamedId>();
38
- const inputs = data?.input_metadata?.value ?? data?.input_metadata ?? [];
39
- for (const input of inputs) {
40
- const other = input.other ?? {};
41
- for (const e of Object.values(other) as any[]) {
42
- if (e.type === "model") {
43
- for (const id of bindingsOfModel(e.model)) {
44
- bindings.add({ id, name: e.model.input_output_names[id] ?? id });
45
- }
46
- }
47
- }
48
- }
49
- const list = [...bindings];
50
- list.sort((a, b) => {
51
- if (a.name < b.name) return -1;
52
- if (a.name > b.name) return 1;
53
- return 0;
54
- });
55
- return list;
56
- }
57
-
58
- function parseJsonOrEmpty(json: string): object {
59
- try {
60
- const j = JSON.parse(json);
61
- if (j !== null && typeof j === "object") {
62
- return j;
63
- }
64
- } catch (e) {}
65
- return {};
66
- }
67
-
68
- export default function ModelMapping({ value, onChange, data, variant }: any) {
69
- const dfsRef = useRef({} as { [binding: string]: HTMLSelectElement | null });
70
- const columnsRef = useRef(
71
- {} as { [binding: string]: HTMLSelectElement | HTMLInputElement | null },
72
- );
73
- const v: any = parseJsonOrEmpty(value);
74
- v.map ??= {};
75
- const dfs: { [df: string]: string[] } = {};
76
- const inputs = data?.input_metadata?.value ?? data?.input_metadata ?? [];
77
- for (const input of inputs) {
78
- if (!input.dataframes) continue;
79
- const dataframes = input.dataframes as {
80
- [df: string]: { columns: string[] };
81
- };
82
- for (const [df, { columns }] of Object.entries(dataframes)) {
83
- dfs[df] = columns;
84
- }
85
- }
86
- const bindings = getModelBindings(data, variant);
87
- function getMap() {
88
- const map: Bindings = {};
89
- for (const binding of bindings) {
90
- const df = dfsRef.current[binding.id]?.value ?? "";
91
- const column = columnsRef.current[binding.id]?.value ?? "";
92
- if (df.length || column.length) {
93
- map[binding.id] = { df, column };
94
- }
95
- }
96
- return map;
97
- }
98
- return (
99
- <table className="model-mapping-param">
100
- <tbody>
101
- {bindings.length > 0 ? (
102
- bindings.map((binding: NamedId) => (
103
- <tr key={binding.id}>
104
- <td>{binding.name}</td>
105
- <td>
106
- <ArrowsHorizontal />
107
- </td>
108
- <td>
109
- <select
110
- className="select select-ghost"
111
- value={v.map?.[binding.id]?.df}
112
- ref={(el) => {
113
- dfsRef.current[binding.id] = el;
114
- }}
115
- onChange={() => onChange(JSON.stringify({ map: getMap() }))}
116
- >
117
- <option key="" value="" />
118
- {Object.keys(dfs).map((df: string) => (
119
- <option key={df} value={df}>
120
- {df}
121
- </option>
122
- ))}
123
- </select>
124
- </td>
125
- <td>
126
- {variant === "output" ? (
127
- <ParameterInput
128
- inputRef={(el) => {
129
- columnsRef.current[binding.id] = el;
130
- }}
131
- value={v.map?.[binding.id]?.column}
132
- onChange={(column, options) => {
133
- const map = getMap();
134
- // At this point the <input> has not been updated yet. We use the value from the event.
135
- const df = dfsRef.current[binding.id]?.value ?? "";
136
- map[binding.id] ??= { df, column };
137
- map[binding.id].column = column;
138
- onChange(JSON.stringify({ map }), options);
139
- }}
140
- />
141
- ) : (
142
- <select
143
- className="select select-ghost"
144
- value={v.map?.[binding.id]?.column}
145
- ref={(el) => {
146
- columnsRef.current[binding.id] = el;
147
- }}
148
- onChange={() => onChange(JSON.stringify({ map: getMap() }))}
149
- >
150
- <option key="" value="" />
151
- {dfs[v.map?.[binding.id]?.df]?.map((col: string) => (
152
- <option key={col} value={col}>
153
- {col}
154
- </option>
155
- ))}
156
- </select>
157
- )}
158
- </td>
159
- </tr>
160
- ))
161
- ) : (
162
- <tr>
163
- <td>no bindings</td>
164
- </tr>
165
- )}
166
- </tbody>
167
- </table>
168
- );
169
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/web/src/workspace/nodes/NodeGroupParameter.tsx DELETED
@@ -1,48 +0,0 @@
1
- import NodeParameter, { type UpdateOptions } from "./NodeParameter";
2
-
3
- interface SelectorType {
4
- name: string;
5
- default: string;
6
- type: {
7
- enum: string[];
8
- };
9
- }
10
-
11
- interface ParameterType {
12
- name: string;
13
- default: string;
14
- type: {
15
- type: string;
16
- };
17
- }
18
-
19
- interface GroupsType {
20
- [key: string]: ParameterType[];
21
- }
22
-
23
- interface NodeGroupParameterProps {
24
- meta: { selector: SelectorType; groups: GroupsType };
25
- data: any;
26
- setParam: (name: string, value: any, options: UpdateOptions) => void;
27
- }
28
-
29
- export default function NodeGroupParameter({ meta, data, setParam }: NodeGroupParameterProps) {
30
- const selector = meta.selector;
31
- const selectorValue = data.params[selector.name] || selector.default;
32
- const group = meta.groups[selectorValue] || [];
33
-
34
- return (
35
- <>
36
- {group.map((meta: any) => (
37
- <NodeParameter
38
- name={meta.name}
39
- key={meta.name}
40
- value={data.params[meta.name] ?? meta.default}
41
- data={data}
42
- meta={meta}
43
- setParam={setParam}
44
- />
45
- ))}
46
- </>
47
- );
48
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/web/src/workspace/nodes/NodeParameter.tsx DELETED
@@ -1,147 +0,0 @@
1
- import jmespath from "jmespath";
2
- // @ts-ignore
3
- import ArrowsHorizontal from "~icons/tabler/arrows-horizontal.jsx";
4
- // @ts-ignore
5
- import Help from "~icons/tabler/question-mark.jsx";
6
- import Tooltip from "../../Tooltip";
7
- import ModelMapping from "./ModelMappingParameter";
8
- import NodeGroupParameter from "./NodeGroupParameter";
9
- import ParameterInput from "./ParameterInput";
10
-
11
- const BOOLEAN = "<class 'bool'>";
12
- const MODEL_TRAINING_INPUT_MAPPING =
13
- "<class 'lynxkite_graph_analytics.ml_ops.ModelTrainingInputMapping'>";
14
- const MODEL_INFERENCE_INPUT_MAPPING =
15
- "<class 'lynxkite_graph_analytics.ml_ops.ModelInferenceInputMapping'>";
16
- const MODEL_OUTPUT_MAPPING = "<class 'lynxkite_graph_analytics.ml_ops.ModelOutputMapping'>";
17
-
18
- function ParamName({ name, doc }: { name: string; doc: string }) {
19
- const help = doc && (
20
- <Tooltip doc={doc} width={200}>
21
- <Help />
22
- </Tooltip>
23
- );
24
- return (
25
- <div className="param-name-row">
26
- <span className="param-name bg-base-200">{name.replace(/_/g, " ")}</span>
27
- {help}
28
- </div>
29
- );
30
- }
31
-
32
- interface NodeParameterProps {
33
- name: string;
34
- value: any;
35
- meta: any;
36
- data: any;
37
- setParam: (name: string, value: any, options: UpdateOptions) => void;
38
- }
39
-
40
- export type UpdateOptions = { delay?: number };
41
-
42
- function findDocs(docs: any, parameter: string) {
43
- for (const sec of docs) {
44
- if (sec.kind === "parameters") {
45
- for (const p of sec.value) {
46
- if (p.name === parameter) {
47
- return p.description;
48
- }
49
- }
50
- }
51
- }
52
- }
53
-
54
- export default function NodeParameter({ name, value, meta, data, setParam }: NodeParameterProps) {
55
- const doc = findDocs(data.meta?.value?.doc ?? [], name);
56
- function onChange(value: any, opts?: UpdateOptions) {
57
- setParam(meta.name, value, opts || {});
58
- }
59
- return meta?.type?.format === "textarea" ? (
60
- <label className="param">
61
- <ParamName name={name} doc={doc} />
62
- <textarea
63
- className="textarea textarea-bordered w-full"
64
- rows={(value ?? "").split("\n").length}
65
- value={value ?? ""}
66
- onChange={(evt) => onChange(evt.currentTarget.value, { delay: 2 })}
67
- onBlur={(evt) => onChange(evt.currentTarget.value, { delay: 0 })}
68
- />
69
- </label>
70
- ) : meta?.type?.format === "dropdown" ? (
71
- <label className="param">
72
- <ParamName name={name} doc={doc} />
73
- <select
74
- className="select select-bordered w-full"
75
- value={value ?? ""}
76
- onChange={(evt) => onChange(evt.currentTarget.value)}
77
- >
78
- {getDropDownValues(data, meta).map((option: string) => (
79
- <option key={option} value={option}>
80
- {option}
81
- </option>
82
- ))}
83
- </select>
84
- </label>
85
- ) : meta?.type === "group" ? (
86
- <NodeGroupParameter meta={meta} data={data} setParam={setParam} />
87
- ) : meta?.type?.enum ? (
88
- <label className="param">
89
- <ParamName name={name} doc={doc} />
90
- <select
91
- className="select select-bordered w-full"
92
- value={value || meta.type.enum[0]}
93
- onChange={(evt) => onChange(evt.currentTarget.value)}
94
- >
95
- {meta.type.enum.map((option: string) => (
96
- <option key={option} value={option}>
97
- {option}
98
- </option>
99
- ))}
100
- </select>
101
- </label>
102
- ) : meta?.type?.type === BOOLEAN ? (
103
- <div className="form-control">
104
- <label className="label cursor-pointer">
105
- {name.replace(/_/g, " ")}
106
- <input
107
- className="checkbox"
108
- type="checkbox"
109
- checked={value}
110
- onChange={(evt) => onChange(evt.currentTarget.checked)}
111
- />
112
- </label>
113
- </div>
114
- ) : meta?.type?.type === MODEL_TRAINING_INPUT_MAPPING ? (
115
- <label className="param">
116
- <ParamName name={name} doc={doc} />
117
- <ModelMapping value={value} data={data} variant="training input" onChange={onChange} />
118
- </label>
119
- ) : meta?.type?.type === MODEL_INFERENCE_INPUT_MAPPING ? (
120
- <label className="param">
121
- <ParamName name={name} doc={doc} />
122
- <ModelMapping value={value} data={data} variant="inference input" onChange={onChange} />
123
- </label>
124
- ) : meta?.type?.type === MODEL_OUTPUT_MAPPING ? (
125
- <label className="param">
126
- <ParamName name={name} doc={doc} />
127
- <ModelMapping value={value} data={data} variant="output" onChange={onChange} />
128
- </label>
129
- ) : (
130
- <label className="param">
131
- <ParamName name={name} doc={doc} />
132
- <ParameterInput value={value} onChange={onChange} />
133
- </label>
134
- );
135
- }
136
-
137
- function getDropDownValues(data: any, meta: any): string[] {
138
- const metadata = data.input_metadata.value;
139
- let query = meta.type.metadata_query;
140
- // Substitute parameters in the query.
141
- for (const p in data.params) {
142
- query = query.replace(`<${p}>`, data.params[p]);
143
- }
144
- const res = ["", ...jmespath.search(metadata, query)];
145
- res.sort();
146
- return res;
147
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/web/src/workspace/nodes/NodeWithComment.tsx DELETED
@@ -1,58 +0,0 @@
1
- import { useReactFlow } from "@xyflow/react";
2
- import { useState } from "react";
3
- import Markdown from "react-markdown";
4
- import type { UpdateOptions } from "./NodeParameter";
5
-
6
- export default function NodeWithComment(props: any) {
7
- const reactFlow = useReactFlow();
8
- const [editing, setEditing] = useState(false);
9
- function setComment(newValue: string, opts?: UpdateOptions) {
10
- reactFlow.updateNodeData(props.id, (prevData: any) => ({
11
- ...prevData,
12
- params: { text: newValue },
13
- __execution_delay: opts?.delay || 0,
14
- }));
15
- }
16
- function onClick(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
17
- // Start editing on double-click.
18
- if (e.detail === 2) {
19
- setEditing(true);
20
- }
21
- }
22
- function finishEditing(el: HTMLTextAreaElement) {
23
- setComment(el.value);
24
- setEditing(false);
25
- }
26
- function onKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
27
- if (e.key === "Escape") {
28
- finishEditing(e.currentTarget);
29
- }
30
- }
31
- function onInput(el: HTMLTextAreaElement | null) {
32
- if (!el) return;
33
- el.focus();
34
- // Resize the textarea to the content.
35
- el.style.height = "auto";
36
- el.style.height = `${el.scrollHeight}px`;
37
- }
38
- if (editing) {
39
- return (
40
- <textarea
41
- className="comment-editor"
42
- onBlur={(e) => finishEditing(e.currentTarget)}
43
- onKeyDown={onKeyDown}
44
- onInput={(e) => onInput(e.currentTarget)}
45
- ref={(el) => onInput(el)}
46
- defaultValue={props.data.params.text}
47
- onClick={(e) => e.stopPropagation()}
48
- placeholder="Enter workspace comment"
49
- />
50
- );
51
- }
52
- const text = props.data.params.text || "_double-click to edit_";
53
- return (
54
- <div className="comment-view drag-handle prose" onClick={onClick}>
55
- <Markdown>{text}</Markdown>
56
- </div>
57
- );
58
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/web/src/workspace/nodes/NodeWithImage.tsx DELETED
@@ -1,12 +0,0 @@
1
- import LynxKiteNode from "./LynxKiteNode";
2
- import { NodeWithParams } from "./NodeWithParams";
3
-
4
- const NodeWithImage = (props: any) => {
5
- return (
6
- <NodeWithParams collapsed {...props}>
7
- {props.data.display && <img src={props.data.display} alt="Node Display" />}
8
- </NodeWithParams>
9
- );
10
- };
11
-
12
- export default LynxKiteNode(NodeWithImage);
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/web/src/workspace/nodes/NodeWithMolecule.tsx DELETED
@@ -1,68 +0,0 @@
1
- import React, { useEffect, type CSSProperties } from "react";
2
- import LynxKiteNode from "./LynxKiteNode";
3
- import { NodeWithParams } from "./NodeWithParams";
4
-
5
- const NodeWithMolecule = (props: any) => {
6
- const containerRef = React.useRef<HTMLDivElement>(null);
7
- const viewerRef = React.useRef<any>(null);
8
-
9
- useEffect(() => {
10
- const config = props.data?.display?.value;
11
- if (!config || !containerRef.current) return;
12
- async function run() {
13
- const $3Dmol = await import("3dmol");
14
-
15
- try {
16
- // Initialize viewer only once
17
- if (!viewerRef.current) {
18
- viewerRef.current = $3Dmol.createViewer(containerRef.current, {
19
- backgroundColor: "white",
20
- });
21
- }
22
-
23
- const viewer = viewerRef.current;
24
-
25
- // Clear previous models
26
- viewer.clear();
27
-
28
- // Add new model and style it
29
- viewer.addModel(config.data, config.format);
30
- viewer.setStyle({}, { stick: {} });
31
- viewer.zoomTo();
32
- viewer.render();
33
- } catch (error) {
34
- console.error("Error rendering 3D molecule:", error);
35
- }
36
- }
37
- run();
38
- const resizeObserver = new ResizeObserver(() => {
39
- viewerRef.current?.resize();
40
- });
41
-
42
- const observed = containerRef.current;
43
- resizeObserver.observe(observed);
44
- return () => {
45
- resizeObserver.unobserve(observed);
46
- if (viewerRef.current) {
47
- viewerRef.current.clear();
48
- }
49
- };
50
- }, [props.data?.display?.value]);
51
-
52
- const vizStyle: CSSProperties = {
53
- flex: 1,
54
- minHeight: "300px",
55
- border: "1px solid #ddd",
56
- borderRadius: "4px",
57
- overflow: "hidden",
58
- position: "relative",
59
- };
60
-
61
- return (
62
- <NodeWithParams collapsed {...props}>
63
- <div style={vizStyle} ref={containerRef} />
64
- </NodeWithParams>
65
- );
66
- };
67
-
68
- export default LynxKiteNode(NodeWithMolecule);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/web/src/workspace/nodes/NodeWithParams.tsx DELETED
@@ -1,44 +0,0 @@
1
- import { useReactFlow } from "@xyflow/react";
2
- import React from "react";
3
- // @ts-ignore
4
- import Triangle from "~icons/tabler/triangle-inverted-filled.jsx";
5
- import LynxKiteNode from "./LynxKiteNode";
6
- import NodeParameter, { type UpdateOptions } from "./NodeParameter";
7
-
8
- export function NodeWithParams(props: any) {
9
- const reactFlow = useReactFlow();
10
- const metaParams = props.data.meta?.value?.params ?? [];
11
- const [collapsed, setCollapsed] = React.useState(props.collapsed);
12
-
13
- function setParam(name: string, newValue: any, opts: UpdateOptions) {
14
- reactFlow.updateNodeData(props.id, (prevData: any) => ({
15
- ...prevData,
16
- params: { ...prevData.data.params, [name]: newValue },
17
- __execution_delay: opts.delay || 0,
18
- }));
19
- }
20
-
21
- return (
22
- <>
23
- {props.collapsed && metaParams.length > 0 && (
24
- <div className="params-expander" onClick={() => setCollapsed(!collapsed)}>
25
- <Triangle className={`flippy ${collapsed ? "flippy-90" : ""}`} />
26
- </div>
27
- )}
28
- {!collapsed &&
29
- metaParams.map((meta: any) => (
30
- <NodeParameter
31
- name={meta.name}
32
- key={meta.name}
33
- value={props.data.params[meta.name]}
34
- data={props.data}
35
- meta={meta}
36
- setParam={setParam}
37
- />
38
- ))}
39
- {props.children}
40
- </>
41
- );
42
- }
43
-
44
- export default LynxKiteNode(NodeWithParams);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/web/src/workspace/nodes/NodeWithTableView.tsx DELETED
@@ -1,82 +0,0 @@
1
- import { useReactFlow } from "@xyflow/react";
2
- import { useState } from "react";
3
- import React from "react";
4
- import Markdown from "react-markdown";
5
- import LynxKiteNode from "./LynxKiteNode";
6
- import Table from "./Table";
7
-
8
- function toMD(v: any): string {
9
- if (typeof v === "string") {
10
- return v;
11
- }
12
- if (Array.isArray(v)) {
13
- return v.map(toMD).join("\n\n");
14
- }
15
- return JSON.stringify(v);
16
- }
17
-
18
- type OpenState = { [name: string]: boolean };
19
-
20
- function NodeWithTableView(props: any) {
21
- const reactFlow = useReactFlow();
22
- const [open, setOpen] = useState((props.data?.params?._tables_open ?? {}) as OpenState);
23
- const display = props.data.display?.value;
24
- const single = display?.dataframes && Object.keys(display?.dataframes).length === 1;
25
- const dfs = Object.entries(display?.dataframes || {});
26
- dfs.sort();
27
- function setParam(name: string, newValue: any) {
28
- reactFlow.updateNodeData(props.id, (prevData: any) => ({
29
- ...prevData,
30
- params: { ...prevData.data.params, [name]: newValue },
31
- }));
32
- }
33
- function toggleTable(name: string) {
34
- setOpen((prevOpen: OpenState) => {
35
- const newOpen = { ...prevOpen, [name]: !prevOpen[name] };
36
- setParam("_tables_open", newOpen);
37
- return newOpen;
38
- });
39
- }
40
- return (
41
- <>
42
- {display && [
43
- dfs.map(([name, df]: [string, any]) => (
44
- <React.Fragment key={name}>
45
- {!single && (
46
- <div key={`${name}-header`} className="df-head" onClick={() => toggleTable(name)}>
47
- {name}
48
- </div>
49
- )}
50
- {(single || open[name]) &&
51
- (df.data.length > 1 ? (
52
- <Table key={`${name}-table`} columns={df.columns} data={df.data} />
53
- ) : df.data.length ? (
54
- <dl key={`${name}-dl`}>
55
- {df.columns.map((c: string, i: number) => (
56
- <React.Fragment key={`${name}-${c}`}>
57
- {df.columns.length > 1 && <dt>{c}</dt>}
58
- <dd className="prose">
59
- <Markdown>{toMD(df.data[0][i])}</Markdown>
60
- </dd>
61
- </React.Fragment>
62
- ))}
63
- </dl>
64
- ) : (
65
- JSON.stringify(df.data)
66
- ))}
67
- </React.Fragment>
68
- )),
69
- Object.entries(display.others || {}).map(([name, o]) => (
70
- <>
71
- <div key={`${name}-header`} className="df-head" onClick={() => toggleTable(name)}>
72
- {name}
73
- </div>
74
- {open[name] && <pre>{(o as any).toString()}</pre>}
75
- </>
76
- )),
77
- ]}
78
- </>
79
- );
80
- }
81
-
82
- export default LynxKiteNode(NodeWithTableView);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lynxkite-app/web/src/workspace/nodes/NodeWithVisualization.tsx DELETED
@@ -1,43 +0,0 @@
1
- import React, { useEffect } from "react";
2
- import LynxKiteNode from "./LynxKiteNode";
3
- import { NodeWithParams } from "./NodeWithParams";
4
- const echarts = await import("echarts");
5
-
6
- function NodeWithVisualization(props: any) {
7
- const chartsRef = React.useRef<HTMLDivElement>(null);
8
- const chartsInstanceRef = React.useRef<echarts.ECharts>();
9
- useEffect(() => {
10
- const opts = props.data?.display?.value;
11
- if (!opts || !chartsRef.current) return;
12
- if (opts.tooltip?.formatter === "GET_THIRD_VALUE") {
13
- // We can't pass a function from the backend, and can't get good tooltips otherwise.
14
- opts.tooltip.formatter = (params: any) => params.value[2];
15
- }
16
- chartsInstanceRef.current = echarts.init(chartsRef.current, null, {
17
- renderer: "canvas",
18
- width: "auto",
19
- height: "auto",
20
- });
21
- chartsInstanceRef.current.setOption(opts);
22
- const resizeObserver = new ResizeObserver(() => {
23
- const e = chartsRef.current;
24
- if (!e) return;
25
- e.style.padding = "1px";
26
- chartsInstanceRef.current?.resize();
27
- e.style.padding = "0";
28
- });
29
- const observed = chartsRef.current;
30
- resizeObserver.observe(observed);
31
- return () => {
32
- resizeObserver.unobserve(observed);
33
- chartsInstanceRef.current?.dispose();
34
- };
35
- }, [props.data?.display?.value]);
36
- return (
37
- <NodeWithParams collapsed {...props}>
38
- <div style={{ flex: 1 }} ref={chartsRef} />
39
- </NodeWithParams>
40
- );
41
- }
42
-
43
- export default LynxKiteNode(NodeWithVisualization);