Spaces:
Running
Running
Merge pull request #32 from biggraph/darabos-crdt-updated
Browse files- requirements.txt +2 -0
- server/crdt.py +131 -0
- server/executors/one_by_one.py +8 -3
- server/lynxkite_ops.py +1 -6
- server/main.py +5 -4
- server/workspace.py +2 -2
- web/package-lock.json +0 -0
- web/package.json +20 -19
- web/src/LynxKiteFlow.svelte +78 -96
- web/src/LynxKiteNode.svelte +10 -2
- web/src/NodeWithParams.svelte +13 -5
- web/src/NodeWithTableView.svelte +7 -5
- web/src/NodeWithVisualization.svelte +5 -4
- web/src/Table.svelte +18 -34
- web/src/app.scss +0 -1
- web/vite.config.ts +8 -1
requirements.txt
CHANGED
@@ -6,6 +6,8 @@ orjson
|
|
6 |
pandas
|
7 |
scipy
|
8 |
uvicorn[standard]
|
|
|
|
|
9 |
# For llm_ops
|
10 |
chromadb
|
11 |
Jinja2
|
|
|
6 |
pandas
|
7 |
scipy
|
8 |
uvicorn[standard]
|
9 |
+
pycrdt
|
10 |
+
pycrdt-websocket
|
11 |
# For llm_ops
|
12 |
chromadb
|
13 |
Jinja2
|
server/crdt.py
ADDED
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'''CRDT is used to synchronize workspace state for backend and frontend(s).'''
|
2 |
+
import asyncio
|
3 |
+
import contextlib
|
4 |
+
import fastapi
|
5 |
+
import os.path
|
6 |
+
import pycrdt
|
7 |
+
import pycrdt_websocket
|
8 |
+
|
9 |
+
import pycrdt_websocket.ystore
|
10 |
+
|
11 |
+
router = fastapi.APIRouter()
|
12 |
+
|
13 |
+
def ws_exception_handler(exception, log):
|
14 |
+
print('exception', exception)
|
15 |
+
log.exception(exception)
|
16 |
+
return True
|
17 |
+
|
18 |
+
class WebsocketServer(pycrdt_websocket.WebsocketServer):
|
19 |
+
async def init_room(self, name):
|
20 |
+
ystore = pycrdt_websocket.ystore.FileYStore(f'crdt_data/{name}.crdt')
|
21 |
+
ydoc = pycrdt.Doc()
|
22 |
+
ydoc['workspace'] = ws = pycrdt.Map()
|
23 |
+
# Replay updates from the store.
|
24 |
+
try:
|
25 |
+
for update, timestamp in [(item[0], item[-1]) async for item in ystore.read()]:
|
26 |
+
ydoc.apply_update(update)
|
27 |
+
except pycrdt_websocket.ystore.YDocNotFound:
|
28 |
+
pass
|
29 |
+
if 'nodes' not in ws:
|
30 |
+
ws['nodes'] = pycrdt.Array()
|
31 |
+
if 'edges' not in ws:
|
32 |
+
ws['edges'] = pycrdt.Array()
|
33 |
+
if 'env' not in ws:
|
34 |
+
ws['env'] = 'unset'
|
35 |
+
try_to_load_workspace(ws, name)
|
36 |
+
room = pycrdt_websocket.YRoom(ystore=ystore, ydoc=ydoc)
|
37 |
+
room.ws = ws
|
38 |
+
def on_change(changes):
|
39 |
+
asyncio.create_task(workspace_changed(changes, ws))
|
40 |
+
ws.observe_deep(on_change)
|
41 |
+
return room
|
42 |
+
|
43 |
+
async def get_room(self, name: str) -> pycrdt_websocket.YRoom:
|
44 |
+
if name not in self.rooms:
|
45 |
+
self.rooms[name] = await self.init_room(name)
|
46 |
+
room = self.rooms[name]
|
47 |
+
await self.start_room(room)
|
48 |
+
return room
|
49 |
+
|
50 |
+
websocket_server = WebsocketServer(exception_handler=ws_exception_handler, auto_clean_rooms=False)
|
51 |
+
asgi_server = pycrdt_websocket.ASGIServer(websocket_server)
|
52 |
+
|
53 |
+
last_ws_input = None
|
54 |
+
def clean_input(ws_pyd):
|
55 |
+
for node in ws_pyd.nodes:
|
56 |
+
node.data.display = None
|
57 |
+
node.position.x = 0
|
58 |
+
node.position.y = 0
|
59 |
+
if node.model_extra:
|
60 |
+
for key in list(node.model_extra.keys()):
|
61 |
+
delattr(node, key)
|
62 |
+
|
63 |
+
def crdt_update(crdt_obj, python_obj, boxes=set()):
|
64 |
+
if isinstance(python_obj, dict):
|
65 |
+
for key, value in python_obj.items():
|
66 |
+
if key in boxes:
|
67 |
+
crdt_obj[key] = value
|
68 |
+
elif isinstance(value, dict):
|
69 |
+
if crdt_obj.get(key) is None:
|
70 |
+
crdt_obj[key] = pycrdt.Map()
|
71 |
+
crdt_update(crdt_obj[key], value, boxes)
|
72 |
+
elif isinstance(value, list):
|
73 |
+
if crdt_obj.get(key) is None:
|
74 |
+
crdt_obj[key] = pycrdt.Array()
|
75 |
+
crdt_update(crdt_obj[key], value, boxes)
|
76 |
+
else:
|
77 |
+
crdt_obj[key] = value
|
78 |
+
elif isinstance(python_obj, list):
|
79 |
+
for i, value in enumerate(python_obj):
|
80 |
+
if isinstance(value, dict):
|
81 |
+
if i >= len(crdt_obj):
|
82 |
+
crdt_obj.append(pycrdt.Map())
|
83 |
+
crdt_update(crdt_obj[i], value, boxes)
|
84 |
+
elif isinstance(value, list):
|
85 |
+
if i >= len(crdt_obj):
|
86 |
+
crdt_obj.append(pycrdt.Array())
|
87 |
+
crdt_update(crdt_obj[i], value, boxes)
|
88 |
+
else:
|
89 |
+
if i >= len(crdt_obj):
|
90 |
+
crdt_obj.append(value)
|
91 |
+
else:
|
92 |
+
crdt_obj[i] = value
|
93 |
+
else:
|
94 |
+
raise ValueError('Invalid type:', python_obj)
|
95 |
+
|
96 |
+
|
97 |
+
def try_to_load_workspace(ws, name):
|
98 |
+
from . import workspace
|
99 |
+
json_path = f'data/{name}'
|
100 |
+
if os.path.exists(json_path):
|
101 |
+
ws_pyd = workspace.load(json_path)
|
102 |
+
crdt_update(ws, ws_pyd.model_dump(), boxes={'display'})
|
103 |
+
|
104 |
+
async def workspace_changed(e, ws_crdt):
|
105 |
+
global last_ws_input
|
106 |
+
from . import workspace
|
107 |
+
ws_pyd = workspace.Workspace.model_validate(ws_crdt.to_py())
|
108 |
+
clean_input(ws_pyd)
|
109 |
+
if ws_pyd == last_ws_input:
|
110 |
+
return
|
111 |
+
last_ws_input = ws_pyd.model_copy(deep=True)
|
112 |
+
await workspace.execute(ws_pyd)
|
113 |
+
for nc, np in zip(ws_crdt['nodes'], ws_pyd.nodes):
|
114 |
+
if 'data' not in nc:
|
115 |
+
nc['data'] = pycrdt.Map()
|
116 |
+
# Display is added as an opaque Box.
|
117 |
+
nc['data']['display'] = np.data.display
|
118 |
+
nc['data']['error'] = np.data.error
|
119 |
+
|
120 |
+
@contextlib.asynccontextmanager
|
121 |
+
async def lifespan(app):
|
122 |
+
async with websocket_server:
|
123 |
+
yield
|
124 |
+
|
125 |
+
def sanitize_path(path):
|
126 |
+
return os.path.relpath(os.path.normpath(os.path.join("/", path)), "/")
|
127 |
+
|
128 |
+
@router.websocket("/ws/crdt/{room_name}")
|
129 |
+
async def crdt_websocket(websocket: fastapi.WebSocket, room_name: str):
|
130 |
+
room_name = sanitize_path(room_name)
|
131 |
+
await asgi_server({'path': room_name}, websocket._receive, websocket._send)
|
server/executors/one_by_one.py
CHANGED
@@ -74,7 +74,12 @@ def make_cache_key(obj):
|
|
74 |
|
75 |
EXECUTOR_OUTPUT_CACHE = {}
|
76 |
|
77 |
-
def
|
|
|
|
|
|
|
|
|
|
|
78 |
nodes = {n.id: n for n in ws.nodes}
|
79 |
contexts = {n.id: Context(node=n) for n in ws.nodes}
|
80 |
edges = {n.id: [] for n in ws.nodes}
|
@@ -113,10 +118,10 @@ def execute(ws, catalog, cache=None):
|
|
113 |
if cache is not None:
|
114 |
key = make_cache_key((inputs, params))
|
115 |
if key not in cache:
|
116 |
-
cache[key] = op(*inputs, **params)
|
117 |
result = cache[key]
|
118 |
else:
|
119 |
-
result = op(*inputs, **params)
|
120 |
except Exception as e:
|
121 |
traceback.print_exc()
|
122 |
data.error = str(e)
|
|
|
74 |
|
75 |
EXECUTOR_OUTPUT_CACHE = {}
|
76 |
|
77 |
+
async def await_if_needed(obj):
|
78 |
+
if inspect.isawaitable(obj):
|
79 |
+
return await obj
|
80 |
+
return obj
|
81 |
+
|
82 |
+
async def execute(ws, catalog, cache=None):
|
83 |
nodes = {n.id: n for n in ws.nodes}
|
84 |
contexts = {n.id: Context(node=n) for n in ws.nodes}
|
85 |
edges = {n.id: [] for n in ws.nodes}
|
|
|
118 |
if cache is not None:
|
119 |
key = make_cache_key((inputs, params))
|
120 |
if key not in cache:
|
121 |
+
cache[key] = await await_if_needed(op(*inputs, **params))
|
122 |
result = cache[key]
|
123 |
else:
|
124 |
+
result = await await_if_needed(op(*inputs, **params))
|
125 |
except Exception as e:
|
126 |
traceback.print_exc()
|
127 |
data.error = str(e)
|
server/lynxkite_ops.py
CHANGED
@@ -83,7 +83,7 @@ def disambiguate_edges(ws):
|
|
83 |
|
84 |
|
85 |
@ops.register_executor('LynxKite')
|
86 |
-
def execute(ws):
|
87 |
catalog = ops.CATALOGS['LynxKite']
|
88 |
# Nodes are responsible for interpreting/executing their child nodes.
|
89 |
nodes = [n for n in ws.nodes if not n.parentId]
|
@@ -105,11 +105,6 @@ def execute(ws):
|
|
105 |
data = node.data
|
106 |
op = catalog[data.title]
|
107 |
params = {**data.params}
|
108 |
-
if op.sub_nodes:
|
109 |
-
sub_nodes = children.get(node.id, [])
|
110 |
-
sub_node_ids = [node.id for node in sub_nodes]
|
111 |
-
sub_edges = [edge for edge in ws.edges if edge.source in sub_node_ids]
|
112 |
-
params['sub_flow'] = {'nodes': sub_nodes, 'edges': sub_edges}
|
113 |
# Convert inputs.
|
114 |
for i, (x, p) in enumerate(zip(inputs, op.inputs.values())):
|
115 |
if p.type == nx.Graph and isinstance(x, Bundle):
|
|
|
83 |
|
84 |
|
85 |
@ops.register_executor('LynxKite')
|
86 |
+
async def execute(ws):
|
87 |
catalog = ops.CATALOGS['LynxKite']
|
88 |
# Nodes are responsible for interpreting/executing their child nodes.
|
89 |
nodes = [n for n in ws.nodes if not n.parentId]
|
|
|
105 |
data = node.data
|
106 |
op = catalog[data.title]
|
107 |
params = {**data.params}
|
|
|
|
|
|
|
|
|
|
|
108 |
# Convert inputs.
|
109 |
for i, (x, p) in enumerate(zip(inputs, op.inputs.values())):
|
110 |
if p.type == nx.Graph and isinstance(x, Bundle):
|
server/main.py
CHANGED
@@ -3,6 +3,7 @@ import fastapi
|
|
3 |
import importlib
|
4 |
import pathlib
|
5 |
import pkgutil
|
|
|
6 |
from . import ops
|
7 |
from . import workspace
|
8 |
|
@@ -14,8 +15,8 @@ for _, name, _ in pkgutil.iter_modules([str(here)]):
|
|
14 |
name = f"server.{name}"
|
15 |
lynxkite_modules[name] = importlib.import_module(name)
|
16 |
|
17 |
-
app = fastapi.FastAPI()
|
18 |
-
|
19 |
|
20 |
|
21 |
@app.get("/api/catalog")
|
@@ -38,9 +39,9 @@ def save(req: SaveRequest):
|
|
38 |
|
39 |
|
40 |
@app.post("/api/save")
|
41 |
-
def save_and_execute(req: SaveRequest):
|
42 |
save(req)
|
43 |
-
workspace.execute(req.ws)
|
44 |
save(req)
|
45 |
return req.ws
|
46 |
|
|
|
3 |
import importlib
|
4 |
import pathlib
|
5 |
import pkgutil
|
6 |
+
from . import crdt
|
7 |
from . import ops
|
8 |
from . import workspace
|
9 |
|
|
|
15 |
name = f"server.{name}"
|
16 |
lynxkite_modules[name] = importlib.import_module(name)
|
17 |
|
18 |
+
app = fastapi.FastAPI(lifespan=crdt.lifespan)
|
19 |
+
app.include_router(crdt.router)
|
20 |
|
21 |
|
22 |
@app.get("/api/catalog")
|
|
|
39 |
|
40 |
|
41 |
@app.post("/api/save")
|
42 |
+
async def save_and_execute(req: SaveRequest):
|
43 |
save(req)
|
44 |
+
await workspace.execute(req.ws)
|
45 |
save(req)
|
46 |
return req.ws
|
47 |
|
server/workspace.py
CHANGED
@@ -43,9 +43,9 @@ class Workspace(BaseConfig):
|
|
43 |
edges: list[WorkspaceEdge] = dataclasses.field(default_factory=list)
|
44 |
|
45 |
|
46 |
-
def execute(ws: Workspace):
|
47 |
if ws.env in ops.EXECUTORS:
|
48 |
-
ops.EXECUTORS[ws.env](ws)
|
49 |
|
50 |
|
51 |
def save(ws: Workspace, path: str):
|
|
|
43 |
edges: list[WorkspaceEdge] = dataclasses.field(default_factory=list)
|
44 |
|
45 |
|
46 |
+
async def execute(ws: Workspace):
|
47 |
if ws.env in ops.EXECUTORS:
|
48 |
+
await ops.EXECUTORS[ws.env](ws)
|
49 |
|
50 |
|
51 |
def save(ws: Workspace, path: str):
|
web/package-lock.json
CHANGED
The diff for this file is too large to render.
See raw diff
|
|
web/package.json
CHANGED
@@ -10,27 +10,28 @@
|
|
10 |
"check": "svelte-check --tsconfig ./tsconfig.json"
|
11 |
},
|
12 |
"devDependencies": {
|
13 |
-
"@sveltejs/vite-plugin-svelte": "
|
14 |
-
"@
|
15 |
-
"
|
16 |
-
"svelte": "
|
17 |
-
"
|
|
|
|
|
18 |
"svelte-markdown": "^0.4.1",
|
19 |
-
"tslib": "
|
20 |
-
"typescript": "
|
21 |
-
"unplugin-icons": "
|
22 |
-
"vite": "
|
|
|
23 |
},
|
24 |
"dependencies": {
|
25 |
-
"@iconify-json/tabler": "
|
26 |
-
"@popperjs/core": "
|
27 |
-
"@sveltestack/svelte-query": "
|
28 |
-
"@xyflow/svelte": "
|
29 |
-
"bootstrap": "
|
30 |
-
"
|
31 |
-
"
|
32 |
-
"
|
33 |
-
"svelte-echarts": "^1.0.0-rc1",
|
34 |
-
"tabulator-tables": "^6.2.1"
|
35 |
}
|
36 |
}
|
|
|
10 |
"check": "svelte-check --tsconfig ./tsconfig.json"
|
11 |
},
|
12 |
"devDependencies": {
|
13 |
+
"@sveltejs/vite-plugin-svelte": "3.1.2",
|
14 |
+
"@syncedstore/core": "0.6.0",
|
15 |
+
"@syncedstore/svelte": "0.6.0",
|
16 |
+
"@tsconfig/svelte": "5.0.4",
|
17 |
+
"sass": "1.79.5",
|
18 |
+
"svelte": "4.2.19",
|
19 |
+
"svelte-check": "4.0.5",
|
20 |
"svelte-markdown": "^0.4.1",
|
21 |
+
"tslib": "2.7.0",
|
22 |
+
"typescript": "5.6.3",
|
23 |
+
"unplugin-icons": "0.19.3",
|
24 |
+
"vite": "5.4.9",
|
25 |
+
"y-websocket": "2.0.4"
|
26 |
},
|
27 |
"dependencies": {
|
28 |
+
"@iconify-json/tabler": "1.2.5",
|
29 |
+
"@popperjs/core": "2.11.8",
|
30 |
+
"@sveltestack/svelte-query": "1.6.0",
|
31 |
+
"@xyflow/svelte": "0.1.21",
|
32 |
+
"bootstrap": "5.3.3",
|
33 |
+
"echarts": "5.5.1",
|
34 |
+
"fuse.js": "7.0.0",
|
35 |
+
"svelte-echarts": "^1.0.0-rc3"
|
|
|
|
|
36 |
}
|
37 |
}
|
web/src/LynxKiteFlow.svelte
CHANGED
@@ -1,12 +1,13 @@
|
|
1 |
<script lang="ts">
|
2 |
-
import {
|
3 |
-
import { writable
|
4 |
import {
|
5 |
SvelteFlow,
|
6 |
Controls,
|
7 |
MiniMap,
|
8 |
MarkerType,
|
9 |
useSvelteFlow,
|
|
|
10 |
type XYPosition,
|
11 |
type Node,
|
12 |
type Edge,
|
@@ -16,7 +17,7 @@
|
|
16 |
import ArrowBack from 'virtual:icons/tabler/arrow-back'
|
17 |
import Backspace from 'virtual:icons/tabler/backspace'
|
18 |
import Atom from 'virtual:icons/tabler/Atom'
|
19 |
-
import { useQuery
|
20 |
import NodeWithParams from './NodeWithParams.svelte';
|
21 |
import NodeWithVisualization from './NodeWithVisualization.svelte';
|
22 |
import NodeWithImage from './NodeWithImage.svelte';
|
@@ -26,27 +27,30 @@
|
|
26 |
import NodeSearch from './NodeSearch.svelte';
|
27 |
import EnvironmentSelector from './EnvironmentSelector.svelte';
|
28 |
import '@xyflow/svelte/dist/style.css';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
29 |
|
30 |
export let path = '';
|
31 |
|
32 |
const { screenToFlowPosition } = useSvelteFlow();
|
33 |
-
const queryClient = useQueryClient();
|
34 |
-
const backendWorkspace = useQuery(['workspace', path], async () => {
|
35 |
-
const res = await fetch(`/api/load?path=${path}`);
|
36 |
-
return res.json();
|
37 |
-
}, {staleTime: 10000, retry: false});
|
38 |
-
const mutation = useMutation(async(update) => {
|
39 |
-
const res = await fetch('/api/save', {
|
40 |
-
method: 'POST',
|
41 |
-
headers: {
|
42 |
-
'Content-Type': 'application/json',
|
43 |
-
},
|
44 |
-
body: JSON.stringify(update),
|
45 |
-
});
|
46 |
-
return await res.json();
|
47 |
-
}, {
|
48 |
-
onSuccess: data => queryClient.setQueryData(['workspace', path], data),
|
49 |
-
});
|
50 |
|
51 |
const nodeTypes: NodeTypes = {
|
52 |
basic: NodeWithParams,
|
@@ -59,13 +63,6 @@
|
|
59 |
|
60 |
const nodes = writable<Node[]>([]);
|
61 |
const edges = writable<Edge[]>([]);
|
62 |
-
let doNotSave = true;
|
63 |
-
$: if ($backendWorkspace.isSuccess) {
|
64 |
-
doNotSave = true; // Change is coming from the backend.
|
65 |
-
nodes.set(JSON.parse(JSON.stringify($backendWorkspace.data?.nodes || [])));
|
66 |
-
edges.set(JSON.parse(JSON.stringify($backendWorkspace.data?.edges || [])));
|
67 |
-
doNotSave = false;
|
68 |
-
}
|
69 |
|
70 |
function closeNodeSearch() {
|
71 |
nodeSearchSettings = undefined;
|
@@ -78,37 +75,36 @@
|
|
78 |
event.preventDefault();
|
79 |
nodeSearchSettings = {
|
80 |
pos: { x: event.clientX, y: event.clientY },
|
81 |
-
boxes: $catalog.data[$
|
82 |
};
|
83 |
}
|
84 |
function addNode(e) {
|
85 |
const meta = {...e.detail};
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
params
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
|
|
|
|
|
|
99 |
node.id = `${title} ${i}`;
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
node.position = { x: node.position.x - parent.position.x, y: node.position.y - parent.position.y };
|
109 |
-
}
|
110 |
-
return [...n, node]
|
111 |
-
});
|
112 |
closeNodeSearch();
|
113 |
}
|
114 |
const catalog = useQuery(['catalog'], async () => {
|
@@ -122,46 +118,6 @@
|
|
122 |
parentId: string,
|
123 |
};
|
124 |
|
125 |
-
const graph = derived([nodes, edges], ([nodes, edges]) => ({ nodes, edges }));
|
126 |
-
// Like JSON.stringify, but with keys sorted.
|
127 |
-
function orderedJSON(obj: any) {
|
128 |
-
const allKeys = new Set();
|
129 |
-
JSON.stringify(obj, (key, value) => (allKeys.add(key), value));
|
130 |
-
return JSON.stringify(obj, Array.from(allKeys).sort());
|
131 |
-
}
|
132 |
-
graph.subscribe(async (g) => {
|
133 |
-
if (doNotSave) return;
|
134 |
-
const dragging = g.nodes.find((n) => n.dragging);
|
135 |
-
if (dragging) return;
|
136 |
-
const resizing = g.nodes.find((n) => n.data?.beingResized);
|
137 |
-
if (resizing) return;
|
138 |
-
scheduleSave(g);
|
139 |
-
});
|
140 |
-
let saveTimeout;
|
141 |
-
function scheduleSave(g) {
|
142 |
-
// A slight delay, so we don't send a million requests when a node is resized, for example.
|
143 |
-
clearTimeout(saveTimeout);
|
144 |
-
saveTimeout = setTimeout(() => save(g), 500);
|
145 |
-
}
|
146 |
-
function save(g) {
|
147 |
-
g = JSON.parse(JSON.stringify(g));
|
148 |
-
for (const node of g.nodes) {
|
149 |
-
delete node.measured;
|
150 |
-
delete node.selected;
|
151 |
-
delete node.dragging;
|
152 |
-
delete node.beingResized;
|
153 |
-
}
|
154 |
-
for (const node of g.edges) {
|
155 |
-
delete node.markerEnd;
|
156 |
-
delete node.selected;
|
157 |
-
}
|
158 |
-
g.env = $backendWorkspace.data?.env;
|
159 |
-
const ws = orderedJSON(g);
|
160 |
-
const bd = orderedJSON($backendWorkspace.data);
|
161 |
-
if (ws === bd) return;
|
162 |
-
console.log('changed', JSON.stringify(diff(g, $backendWorkspace.data), null, 2));
|
163 |
-
$mutation.mutate({ path, ws: g });
|
164 |
-
}
|
165 |
function nodeClick(e) {
|
166 |
const node = e.detail.node;
|
167 |
const meta = node.data.meta;
|
@@ -176,11 +132,32 @@
|
|
176 |
parentId: node.id,
|
177 |
};
|
178 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
179 |
$: parentDir = path.split('/').slice(0, -1).join('/');
|
180 |
-
|
181 |
</script>
|
182 |
|
183 |
<div class="page">
|
|
|
184 |
<div class="top-bar">
|
185 |
<div class="ws-name">
|
186 |
<a href><img src="/favicon.ico"></a>
|
@@ -189,8 +166,10 @@
|
|
189 |
<div class="tools">
|
190 |
<EnvironmentSelector
|
191 |
options={Object.keys($catalog.data || {})}
|
192 |
-
value={$
|
193 |
-
onChange={(env) =>
|
|
|
|
|
194 |
/>
|
195 |
<a href><Atom /></a>
|
196 |
<a href><Backspace /></a>
|
@@ -201,6 +180,8 @@
|
|
201 |
<SvelteFlow {nodes} {edges} {nodeTypes} fitView
|
202 |
on:paneclick={toggleNodeSearch}
|
203 |
on:nodeclick={nodeClick}
|
|
|
|
|
204 |
proOptions={{ hideAttribution: true }}
|
205 |
maxZoom={3}
|
206 |
minZoom={0.3}
|
@@ -213,6 +194,7 @@
|
|
213 |
{/if}
|
214 |
</SvelteFlow>
|
215 |
</div>
|
|
|
216 |
</div>
|
217 |
|
218 |
<style>
|
|
|
1 |
<script lang="ts">
|
2 |
+
import { setContext } from 'svelte';
|
3 |
+
import { writable } from 'svelte/store';
|
4 |
import {
|
5 |
SvelteFlow,
|
6 |
Controls,
|
7 |
MiniMap,
|
8 |
MarkerType,
|
9 |
useSvelteFlow,
|
10 |
+
useUpdateNodeInternals,
|
11 |
type XYPosition,
|
12 |
type Node,
|
13 |
type Edge,
|
|
|
17 |
import ArrowBack from 'virtual:icons/tabler/arrow-back'
|
18 |
import Backspace from 'virtual:icons/tabler/backspace'
|
19 |
import Atom from 'virtual:icons/tabler/Atom'
|
20 |
+
import { useQuery } from '@sveltestack/svelte-query';
|
21 |
import NodeWithParams from './NodeWithParams.svelte';
|
22 |
import NodeWithVisualization from './NodeWithVisualization.svelte';
|
23 |
import NodeWithImage from './NodeWithImage.svelte';
|
|
|
27 |
import NodeSearch from './NodeSearch.svelte';
|
28 |
import EnvironmentSelector from './EnvironmentSelector.svelte';
|
29 |
import '@xyflow/svelte/dist/style.css';
|
30 |
+
import { syncedStore, getYjsDoc } from "@syncedstore/core";
|
31 |
+
import { svelteSyncedStore } from "@syncedstore/svelte";
|
32 |
+
import { WebsocketProvider } from "y-websocket";
|
33 |
+
const updateNodeInternals = useUpdateNodeInternals();
|
34 |
+
|
35 |
+
function getCRDTStore(path) {
|
36 |
+
const sstore = syncedStore({ workspace: {} });
|
37 |
+
const doc = getYjsDoc(sstore);
|
38 |
+
const wsProvider = new WebsocketProvider("ws://localhost:8000/ws/crdt", path, doc);
|
39 |
+
return {store: svelteSyncedStore(sstore), sstore, doc};
|
40 |
+
}
|
41 |
+
$: connection = getCRDTStore(path);
|
42 |
+
$: store = connection.store;
|
43 |
+
$: store.subscribe((value) => {
|
44 |
+
if (!value?.workspace?.edges) return;
|
45 |
+
$nodes = [...value.workspace.nodes];
|
46 |
+
$edges = [...value.workspace.edges];
|
47 |
+
updateNodeInternals();
|
48 |
+
});
|
49 |
+
$: setContext('LynxKite store', store);
|
50 |
|
51 |
export let path = '';
|
52 |
|
53 |
const { screenToFlowPosition } = useSvelteFlow();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
54 |
|
55 |
const nodeTypes: NodeTypes = {
|
56 |
basic: NodeWithParams,
|
|
|
63 |
|
64 |
const nodes = writable<Node[]>([]);
|
65 |
const edges = writable<Edge[]>([]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
66 |
|
67 |
function closeNodeSearch() {
|
68 |
nodeSearchSettings = undefined;
|
|
|
75 |
event.preventDefault();
|
76 |
nodeSearchSettings = {
|
77 |
pos: { x: event.clientX, y: event.clientY },
|
78 |
+
boxes: $catalog.data[$store.workspace.env],
|
79 |
};
|
80 |
}
|
81 |
function addNode(e) {
|
82 |
const meta = {...e.detail};
|
83 |
+
const node = {
|
84 |
+
type: meta.type,
|
85 |
+
data: {
|
86 |
+
meta: meta,
|
87 |
+
title: meta.name,
|
88 |
+
params: Object.fromEntries(
|
89 |
+
Object.values(meta.params).map((p) => [p.name, p.default])),
|
90 |
+
},
|
91 |
+
};
|
92 |
+
node.position = screenToFlowPosition({x: nodeSearchSettings.pos.x, y: nodeSearchSettings.pos.y});
|
93 |
+
const title = node.data.title;
|
94 |
+
let i = 1;
|
95 |
+
node.id = `${title} ${i}`;
|
96 |
+
const nodes = $store.workspace.nodes;
|
97 |
+
while (nodes.find((x) => x.id === node.id)) {
|
98 |
+
i += 1;
|
99 |
node.id = `${title} ${i}`;
|
100 |
+
}
|
101 |
+
node.parentId = nodeSearchSettings.parentId;
|
102 |
+
if (node.parentId) {
|
103 |
+
node.extent = 'parent';
|
104 |
+
const parent = nodes.find((x) => x.id === node.parentId);
|
105 |
+
node.position = { x: node.position.x - parent.position.x, y: node.position.y - parent.position.y };
|
106 |
+
}
|
107 |
+
nodes.push(node);
|
|
|
|
|
|
|
|
|
108 |
closeNodeSearch();
|
109 |
}
|
110 |
const catalog = useQuery(['catalog'], async () => {
|
|
|
118 |
parentId: string,
|
119 |
};
|
120 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
121 |
function nodeClick(e) {
|
122 |
const node = e.detail.node;
|
123 |
const meta = node.data.meta;
|
|
|
132 |
parentId: node.id,
|
133 |
};
|
134 |
}
|
135 |
+
function onConnect(params: Connection) {
|
136 |
+
const edge = {
|
137 |
+
id: `${params.source} ${params.target}`,
|
138 |
+
source: params.source,
|
139 |
+
sourceHandle: params.sourceHandle,
|
140 |
+
target: params.target,
|
141 |
+
targetHandle: params.targetHandle,
|
142 |
+
};
|
143 |
+
$store.workspace.edges.push(edge);
|
144 |
+
}
|
145 |
+
function onDelete(params) {
|
146 |
+
const { nodes, edges } = params;
|
147 |
+
for (const node of nodes) {
|
148 |
+
const index = $store.workspace.nodes.findIndex((x) => x.id === node.id);
|
149 |
+
if (index !== -1) $store.workspace.nodes.splice(index, 1);
|
150 |
+
}
|
151 |
+
for (const edge of edges) {
|
152 |
+
const index = $store.workspace.edges.findIndex((x) => x.id === edge.id);
|
153 |
+
if (index !== -1) $store.workspace.edges.splice(index, 1);
|
154 |
+
}
|
155 |
+
}
|
156 |
$: parentDir = path.split('/').slice(0, -1).join('/');
|
|
|
157 |
</script>
|
158 |
|
159 |
<div class="page">
|
160 |
+
{#if $store.workspace !== undefined}
|
161 |
<div class="top-bar">
|
162 |
<div class="ws-name">
|
163 |
<a href><img src="/favicon.ico"></a>
|
|
|
166 |
<div class="tools">
|
167 |
<EnvironmentSelector
|
168 |
options={Object.keys($catalog.data || {})}
|
169 |
+
value={$store.workspace.env}
|
170 |
+
onChange={(env) => {
|
171 |
+
$store.workspace.env = env;
|
172 |
+
}}
|
173 |
/>
|
174 |
<a href><Atom /></a>
|
175 |
<a href><Backspace /></a>
|
|
|
180 |
<SvelteFlow {nodes} {edges} {nodeTypes} fitView
|
181 |
on:paneclick={toggleNodeSearch}
|
182 |
on:nodeclick={nodeClick}
|
183 |
+
onconnect={onConnect}
|
184 |
+
ondelete={onDelete}
|
185 |
proOptions={{ hideAttribution: true }}
|
186 |
maxZoom={3}
|
187 |
minZoom={0.3}
|
|
|
194 |
{/if}
|
195 |
</SvelteFlow>
|
196 |
</div>
|
197 |
+
{/if}
|
198 |
</div>
|
199 |
|
200 |
<style>
|
web/src/LynxKiteNode.svelte
CHANGED
@@ -1,4 +1,5 @@
|
|
1 |
<script lang="ts">
|
|
|
2 |
import { Handle, useSvelteFlow, useUpdateNodeInternals, type NodeProps, NodeResizeControl } from '@xyflow/svelte';
|
3 |
import ChevronDownRight from 'virtual:icons/tabler/chevron-down-right';
|
4 |
|
@@ -10,6 +11,10 @@
|
|
10 |
export let containerStyle = '';
|
11 |
export let id: $$Props['id']; id;
|
12 |
export let data: $$Props['data'];
|
|
|
|
|
|
|
|
|
13 |
export let dragHandle: $$Props['dragHandle'] = undefined; dragHandle;
|
14 |
export let type: $$Props['type'] = undefined; type;
|
15 |
export let selected: $$Props['selected'] = undefined; selected;
|
@@ -24,11 +29,14 @@
|
|
24 |
export let positionAbsoluteY: $$Props['positionAbsoluteY'] = undefined; positionAbsoluteY;
|
25 |
export let onToggle = () => {};
|
26 |
|
|
|
27 |
$: expanded = !data.collapsed;
|
28 |
function titleClicked() {
|
29 |
-
|
30 |
-
data =
|
31 |
onToggle({ expanded });
|
|
|
|
|
32 |
updateNodeInternals();
|
33 |
}
|
34 |
function asPx(n: number | undefined) {
|
|
|
1 |
<script lang="ts">
|
2 |
+
import { getContext } from 'svelte';
|
3 |
import { Handle, useSvelteFlow, useUpdateNodeInternals, type NodeProps, NodeResizeControl } from '@xyflow/svelte';
|
4 |
import ChevronDownRight from 'virtual:icons/tabler/chevron-down-right';
|
5 |
|
|
|
11 |
export let containerStyle = '';
|
12 |
export let id: $$Props['id']; id;
|
13 |
export let data: $$Props['data'];
|
14 |
+
export let deletable: $$Props['deletable'] = undefined; deletable;
|
15 |
+
export let draggable: $$Props['draggable'] = undefined; draggable;
|
16 |
+
export let parentId: $$Props['parentId'] = undefined; parentId;
|
17 |
+
export let selectable: $$Props['selectable'] = undefined; selectable;
|
18 |
export let dragHandle: $$Props['dragHandle'] = undefined; dragHandle;
|
19 |
export let type: $$Props['type'] = undefined; type;
|
20 |
export let selected: $$Props['selected'] = undefined; selected;
|
|
|
29 |
export let positionAbsoluteY: $$Props['positionAbsoluteY'] = undefined; positionAbsoluteY;
|
30 |
export let onToggle = () => {};
|
31 |
|
32 |
+
$: store = getContext('LynxKite store');
|
33 |
$: expanded = !data.collapsed;
|
34 |
function titleClicked() {
|
35 |
+
const i = $store.workspace.nodes.findIndex((n) => n.id === id);
|
36 |
+
$store.workspace.nodes[i].data.collapsed = expanded;
|
37 |
onToggle({ expanded });
|
38 |
+
// Trigger update.
|
39 |
+
data = data;
|
40 |
updateNodeInternals();
|
41 |
}
|
42 |
function asPx(n: number | undefined) {
|
web/src/NodeWithParams.svelte
CHANGED
@@ -1,21 +1,29 @@
|
|
1 |
<script lang="ts">
|
2 |
-
import {
|
|
|
3 |
import LynxKiteNode from './LynxKiteNode.svelte';
|
4 |
import NodeParameter from './NodeParameter.svelte';
|
5 |
type $$Props = NodeProps;
|
6 |
export let id: $$Props['id'];
|
7 |
export let data: $$Props['data'];
|
8 |
-
const { updateNodeData } = useSvelteFlow();
|
9 |
$: metaParams = data.meta?.params;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
</script>
|
11 |
|
12 |
-
<LynxKiteNode {
|
13 |
-
{#each
|
14 |
<NodeParameter
|
15 |
{name}
|
16 |
{value}
|
17 |
meta={metaParams?.[name]}
|
18 |
-
onChange={(
|
19 |
/>
|
20 |
{/each}
|
21 |
<slot />
|
|
|
1 |
<script lang="ts">
|
2 |
+
import { getContext } from 'svelte';
|
3 |
+
import { type NodeProps, useNodes } from '@xyflow/svelte';
|
4 |
import LynxKiteNode from './LynxKiteNode.svelte';
|
5 |
import NodeParameter from './NodeParameter.svelte';
|
6 |
type $$Props = NodeProps;
|
7 |
export let id: $$Props['id'];
|
8 |
export let data: $$Props['data'];
|
|
|
9 |
$: metaParams = data.meta?.params;
|
10 |
+
$: store = getContext('LynxKite store');
|
11 |
+
function setParam(name, newValue) {
|
12 |
+
const i = $store.workspace.nodes.findIndex((n) => n.id === id);
|
13 |
+
$store.workspace.nodes[i].data.params[name] = newValue;
|
14 |
+
}
|
15 |
+
$: params = $nodes && data?.params ? Object.entries(data.params) : [];
|
16 |
+
const nodes = useNodes(); // We don't properly get updates to "data". This is a hack.
|
17 |
+
$: props = $nodes && $$props;
|
18 |
</script>
|
19 |
|
20 |
+
<LynxKiteNode {...props}>
|
21 |
+
{#each params as [name, value]}
|
22 |
<NodeParameter
|
23 |
{name}
|
24 |
{value}
|
25 |
meta={metaParams?.[name]}
|
26 |
+
onChange={(value) => setParam(name, value)}
|
27 |
/>
|
28 |
{/each}
|
29 |
<slot />
|
web/src/NodeWithTableView.svelte
CHANGED
@@ -1,12 +1,14 @@
|
|
1 |
<script lang="ts">
|
2 |
-
import { type NodeProps } from '@xyflow/svelte';
|
3 |
import LynxKiteNode from './LynxKiteNode.svelte';
|
4 |
import Table from './Table.svelte';
|
5 |
import SvelteMarkdown from 'svelte-markdown'
|
6 |
type $$Props = NodeProps;
|
7 |
export let data: $$Props['data'];
|
|
|
|
|
8 |
const open = {};
|
9 |
-
$: single =
|
10 |
function toMD(v) {
|
11 |
if (typeof v === 'string') {
|
12 |
return v;
|
@@ -19,8 +21,8 @@
|
|
19 |
</script>
|
20 |
|
21 |
<LynxKiteNode {...$$props}>
|
22 |
-
{#if
|
23 |
-
{#each Object.entries(
|
24 |
{#if !single}<div class="df-head" on:click={() => open[name] = !open[name]}>{name}</div>{/if}
|
25 |
{#if single || open[name]}
|
26 |
{#if df.data.length > 1}
|
@@ -35,7 +37,7 @@
|
|
35 |
{/if}
|
36 |
{/if}
|
37 |
{/each}
|
38 |
-
{#each Object.entries(
|
39 |
<div class="df-head" on:click={() => open[name] = !open[name]}>{name}</div>
|
40 |
{#if open[name]}
|
41 |
<pre>{o}</pre>
|
|
|
1 |
<script lang="ts">
|
2 |
+
import { useNodes, type NodeProps } from '@xyflow/svelte';
|
3 |
import LynxKiteNode from './LynxKiteNode.svelte';
|
4 |
import Table from './Table.svelte';
|
5 |
import SvelteMarkdown from 'svelte-markdown'
|
6 |
type $$Props = NodeProps;
|
7 |
export let data: $$Props['data'];
|
8 |
+
const nodes = useNodes(); // We don't properly get updates to "data". This is a hack.
|
9 |
+
$: D = $nodes && data;
|
10 |
const open = {};
|
11 |
+
$: single = D.display?.value?.dataframes && Object.keys(D.display.value.dataframes).length === 1;
|
12 |
function toMD(v) {
|
13 |
if (typeof v === 'string') {
|
14 |
return v;
|
|
|
21 |
</script>
|
22 |
|
23 |
<LynxKiteNode {...$$props}>
|
24 |
+
{#if D?.display?.value}
|
25 |
+
{#each Object.entries(D.display.value.dataframes || {}) as [name, df]}
|
26 |
{#if !single}<div class="df-head" on:click={() => open[name] = !open[name]}>{name}</div>{/if}
|
27 |
{#if single || open[name]}
|
28 |
{#if df.data.length > 1}
|
|
|
37 |
{/if}
|
38 |
{/if}
|
39 |
{/each}
|
40 |
+
{#each Object.entries(D.display.value.others || {}) as [name, o]}
|
41 |
<div class="df-head" on:click={() => open[name] = !open[name]}>{name}</div>
|
42 |
{#if open[name]}
|
43 |
<pre>{o}</pre>
|
web/src/NodeWithVisualization.svelte
CHANGED
@@ -1,16 +1,17 @@
|
|
1 |
<script lang="ts">
|
2 |
-
import { type NodeProps } from '@xyflow/svelte';
|
3 |
import NodeWithParams from './NodeWithParams.svelte';
|
4 |
import { Chart } from 'svelte-echarts';
|
5 |
import { init } from 'echarts';
|
6 |
type $$Props = NodeProps;
|
7 |
export let data: $$Props['data'];
|
|
|
|
|
|
|
8 |
</script>
|
9 |
|
10 |
<NodeWithParams {...$$props}>
|
11 |
-
{
|
12 |
-
<Chart {init} options={data.display} initOptions={{renderer: 'canvas', width: 250, height: 250}}/>
|
13 |
-
{/if}
|
14 |
</NodeWithParams>
|
15 |
<style>
|
16 |
</style>
|
|
|
1 |
<script lang="ts">
|
2 |
+
import { useNodes, type NodeProps } from '@xyflow/svelte';
|
3 |
import NodeWithParams from './NodeWithParams.svelte';
|
4 |
import { Chart } from 'svelte-echarts';
|
5 |
import { init } from 'echarts';
|
6 |
type $$Props = NodeProps;
|
7 |
export let data: $$Props['data'];
|
8 |
+
|
9 |
+
const nodes = useNodes(); // We don't properly get updates to "data". This is a hack.
|
10 |
+
$: D = $nodes && data;
|
11 |
</script>
|
12 |
|
13 |
<NodeWithParams {...$$props}>
|
14 |
+
<Chart {init} options={D?.display?.value || {}} initOptions={{renderer: 'canvas', width: 250, height: 250}}/>
|
|
|
|
|
15 |
</NodeWithParams>
|
16 |
<style>
|
17 |
</style>
|
web/src/Table.svelte
CHANGED
@@ -1,38 +1,22 @@
|
|
1 |
<script lang="ts">
|
2 |
-
import {TabulatorFull as Tabulator} from 'tabulator-tables';
|
3 |
-
import {onMount} from 'svelte';
|
4 |
-
|
5 |
export let data, columns;
|
6 |
-
|
7 |
-
let tableComponent;
|
8 |
-
let tab;
|
9 |
-
|
10 |
-
// The rows in the data are arrays, but Tabulator expects objects.
|
11 |
-
const objs = [];
|
12 |
-
$: {
|
13 |
-
objs.splice();
|
14 |
-
for (const row of data) {
|
15 |
-
const obj = {};
|
16 |
-
for (let i = 0; i < columns.length; i++) {
|
17 |
-
let d = row[i];
|
18 |
-
if (typeof d !== 'string' && typeof d !== 'number') {
|
19 |
-
d = JSON.stringify(d);
|
20 |
-
}
|
21 |
-
obj[columns[i]] = d;
|
22 |
-
}
|
23 |
-
objs.push(obj);
|
24 |
-
}
|
25 |
-
}
|
26 |
-
|
27 |
-
onMount(() => {
|
28 |
-
tab = new Tabulator(tableComponent, {
|
29 |
-
data: objs,
|
30 |
-
columns: columns.map(c => ({title: c, field: c, widthGrow: 1})),
|
31 |
-
height: '311px',
|
32 |
-
reactiveData: true,
|
33 |
-
layout: "fitColumns",
|
34 |
-
});
|
35 |
-
});
|
36 |
</script>
|
37 |
|
38 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
<script lang="ts">
|
|
|
|
|
|
|
2 |
export let data, columns;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3 |
</script>
|
4 |
|
5 |
+
<table>
|
6 |
+
<thead>
|
7 |
+
<tr>
|
8 |
+
{#each columns as column}
|
9 |
+
<th>{column}</th>
|
10 |
+
{/each}
|
11 |
+
</tr>
|
12 |
+
</thead>
|
13 |
+
<tbody>
|
14 |
+
{#each data as row}
|
15 |
+
<tr>
|
16 |
+
{#each columns as column}
|
17 |
+
<td>{row[column]}</td>
|
18 |
+
{/each}
|
19 |
+
</tr>
|
20 |
+
{/each}
|
21 |
+
</tbody>
|
22 |
+
</table>
|
web/src/app.scss
CHANGED
@@ -1,7 +1,6 @@
|
|
1 |
// Import all of Bootstrap's CSS
|
2 |
$form-select-indicator-color: oklch(90% 0.01 55);
|
3 |
@import "bootstrap/scss/bootstrap";
|
4 |
-
@import "tabulator-tables";
|
5 |
:root {
|
6 |
--bs-border-color: oklch(90% 0.01 55);
|
7 |
}
|
|
|
1 |
// Import all of Bootstrap's CSS
|
2 |
$form-select-indicator-color: oklch(90% 0.01 55);
|
3 |
@import "bootstrap/scss/bootstrap";
|
|
|
4 |
:root {
|
5 |
--bs-border-color: oklch(90% 0.01 55);
|
6 |
}
|
web/vite.config.ts
CHANGED
@@ -9,6 +9,13 @@ export default defineConfig({
|
|
9 |
Icons({ compiler: 'svelte', defaultStyle: 'vertical-align: sub;' }),
|
10 |
],
|
11 |
server: {
|
12 |
-
proxy: {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
},
|
14 |
})
|
|
|
9 |
Icons({ compiler: 'svelte', defaultStyle: 'vertical-align: sub;' }),
|
10 |
],
|
11 |
server: {
|
12 |
+
proxy: {
|
13 |
+
'/api': 'http://127.0.0.1:8000',
|
14 |
+
'/ws': {
|
15 |
+
target: 'ws://127.0.0.1:8000',
|
16 |
+
ws: true,
|
17 |
+
changeOrigin: true,
|
18 |
+
},
|
19 |
+
},
|
20 |
},
|
21 |
})
|