darabos commited on
Commit
a8859a7
·
1 Parent(s): 7d3eca5

Some nodes are now visible.

Browse files
server/crdt.py CHANGED
@@ -1,6 +1,8 @@
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
@@ -10,33 +12,39 @@ 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
 
@@ -47,10 +55,16 @@ class WebsocketServer(pycrdt_websocket.WebsocketServer):
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
@@ -60,6 +74,7 @@ def clean_input(ws_pyd):
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():
@@ -73,6 +88,8 @@ def crdt_update(crdt_obj, python_obj, boxes=set()):
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):
@@ -91,41 +108,47 @@ def crdt_update(crdt_obj, python_obj, boxes=set()):
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)
 
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 fastapi
7
  import os.path
8
  import pycrdt
 
12
 
13
  router = fastapi.APIRouter()
14
 
15
+
16
  def ws_exception_handler(exception, log):
17
+ print("exception", exception)
18
  log.exception(exception)
19
  return True
20
 
21
+
22
  class WebsocketServer(pycrdt_websocket.WebsocketServer):
23
  async def init_room(self, name):
24
+ ystore = pycrdt_websocket.ystore.FileYStore(f"crdt_data/{name}.crdt")
25
  ydoc = pycrdt.Doc()
26
+ ydoc["workspace"] = ws = pycrdt.Map()
27
  # Replay updates from the store.
28
  try:
29
+ for update, timestamp in [
30
+ (item[0], item[-1]) async for item in ystore.read()
31
+ ]:
32
  ydoc.apply_update(update)
33
  except pycrdt_websocket.ystore.YDocNotFound:
34
  pass
35
+ if "nodes" not in ws:
36
+ ws["nodes"] = pycrdt.Array()
37
+ if "edges" not in ws:
38
+ ws["edges"] = pycrdt.Array()
39
+ if "env" not in ws:
40
+ ws["env"] = "unset"
41
  try_to_load_workspace(ws, name)
42
  room = pycrdt_websocket.YRoom(ystore=ystore, ydoc=ydoc)
43
  room.ws = ws
44
+
45
  def on_change(changes):
46
  asyncio.create_task(workspace_changed(changes, ws))
47
+
48
  ws.observe_deep(on_change)
49
  return room
50
 
 
55
  await self.start_room(room)
56
  return room
57
 
58
+
59
+ websocket_server = WebsocketServer(
60
+ # exception_handler=ws_exception_handler,
61
+ auto_clean_rooms=False,
62
+ )
63
  asgi_server = pycrdt_websocket.ASGIServer(websocket_server)
64
 
65
  last_ws_input = None
66
+
67
+
68
  def clean_input(ws_pyd):
69
  for node in ws_pyd.nodes:
70
  node.data.display = None
 
74
  for key in list(node.model_extra.keys()):
75
  delattr(node, key)
76
 
77
+
78
  def crdt_update(crdt_obj, python_obj, boxes=set()):
79
  if isinstance(python_obj, dict):
80
  for key, value in python_obj.items():
 
88
  if crdt_obj.get(key) is None:
89
  crdt_obj[key] = pycrdt.Array()
90
  crdt_update(crdt_obj[key], value, boxes)
91
+ elif isinstance(value, enum.Enum):
92
+ crdt_obj[key] = str(value)
93
  else:
94
  crdt_obj[key] = value
95
  elif isinstance(python_obj, list):
 
108
  else:
109
  crdt_obj[i] = value
110
  else:
111
+ raise ValueError("Invalid type:", python_obj)
112
 
113
 
114
  def try_to_load_workspace(ws, name):
115
  from . import workspace
116
+
117
+ json_path = f"data/{name}"
118
  if os.path.exists(json_path):
119
  ws_pyd = workspace.load(json_path)
120
+ crdt_update(ws, ws_pyd.model_dump(), boxes={"display"})
121
+
122
 
123
  async def workspace_changed(e, ws_crdt):
124
  global last_ws_input
125
  from . import workspace
126
+
127
  ws_pyd = workspace.Workspace.model_validate(ws_crdt.to_py())
128
  clean_input(ws_pyd)
129
  if ws_pyd == last_ws_input:
130
  return
131
  last_ws_input = ws_pyd.model_copy(deep=True)
132
  await workspace.execute(ws_pyd)
133
+ for nc, np in zip(ws_crdt["nodes"], ws_pyd.nodes):
134
+ if "data" not in nc:
135
+ nc["data"] = pycrdt.Map()
136
  # Display is added as an opaque Box.
137
+ nc["data"]["display"] = np.data.display
138
+ nc["data"]["error"] = np.data.error
139
+
140
 
141
  @contextlib.asynccontextmanager
142
  async def lifespan(app):
143
  async with websocket_server:
144
  yield
145
 
146
+
147
  def sanitize_path(path):
148
  return os.path.relpath(os.path.normpath(os.path.join("/", path)), "/")
149
 
150
+
151
  @router.websocket("/ws/crdt/{room_name}")
152
  async def crdt_websocket(websocket: fastapi.WebSocket, room_name: str):
153
  room_name = sanitize_path(room_name)
154
+ await asgi_server({"path": room_name}, websocket._receive, websocket._send)
web/app/globals.css CHANGED
@@ -2,18 +2,6 @@
2
  @tailwind components;
3
  @tailwind utilities;
4
 
5
- :root {
6
- --background: #ffffff;
7
- --foreground: #171717;
8
- }
9
-
10
- /* @media (prefers-color-scheme: dark) {
11
- :root {
12
- --background: #0a0a0a;
13
- --foreground: #ededed;
14
- }
15
- } */
16
-
17
  body {
18
  color: var(--foreground);
19
  background: var(--background);
@@ -23,14 +11,15 @@ body {
23
  .top-bar {
24
  display: flex;
25
  justify-content: space-between;
26
- background: oklch(30% 0.13 230);
27
- color: white;
28
  }
29
  .ws-name {
30
  font-size: 1.5em;
 
 
31
  }
32
- .ws-name img {
33
- height: 1.5em;
34
  vertical-align: middle;
35
  margin: 4px;
36
  }
@@ -45,7 +34,87 @@ body {
45
  align-items: center;
46
  }
47
  .tools a {
48
- color: oklch(75% 0.13 230);
49
  font-size: 1.5em;
50
  padding: 0 10px;
51
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  @tailwind components;
3
  @tailwind utilities;
4
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  body {
6
  color: var(--foreground);
7
  background: var(--background);
 
11
  .top-bar {
12
  display: flex;
13
  justify-content: space-between;
14
+ align-items: center;
 
15
  }
16
  .ws-name {
17
  font-size: 1.5em;
18
+ flex: 1;
19
+ color: white;
20
  }
21
+ .top-bar .logo img {
22
+ height: 2em;
23
  vertical-align: middle;
24
  margin: 4px;
25
  }
 
34
  align-items: center;
35
  }
36
  .tools a {
 
37
  font-size: 1.5em;
38
  padding: 0 10px;
39
  }
40
+ .error {
41
+ background: #ffdddd;
42
+ padding: 8px;
43
+ font-size: 12px;
44
+ }
45
+ .title-icon {
46
+ margin-left: 5px;
47
+ float: right;
48
+ }
49
+ .node-container {
50
+ padding: 8px;
51
+ position: relative;
52
+ }
53
+ .lynxkite-node {
54
+ box-shadow: 0px 5px 50px 0px rgba(0, 0, 0, 0.3);
55
+ border-radius: 4px;
56
+ background: white;
57
+ }
58
+ .expanded .lynxkite-node {
59
+ overflow-y: auto;
60
+ height: 100%;
61
+ }
62
+ .lynxkite-node .title {
63
+ /* background: oklch(75% 0.2 55); */
64
+ font-weight: bold;
65
+ padding: 8px;
66
+ }
67
+ .handle-name {
68
+ font-size: 10px;
69
+ color: black;
70
+ letter-spacing: 0.05em;
71
+ text-align: right;
72
+ white-space: nowrap;
73
+ position: absolute;
74
+ top: -5px;
75
+ backdrop-filter: blur(10px);
76
+ padding: 2px 8px;
77
+ border-radius: 4px;
78
+ visibility: hidden;
79
+ }
80
+ .left .handle-name {
81
+ right: 20px;
82
+ }
83
+ .right .handle-name {
84
+ left: 20px;
85
+ }
86
+ .top .handle-name,
87
+ .bottom .handle-name {
88
+ top: -5px;
89
+ left: 5px;
90
+ backdrop-filter: none;
91
+ }
92
+ .node-container:hover .handle-name {
93
+ visibility: visible;
94
+ }
95
+ .node-resizer {
96
+ position: absolute;
97
+ bottom: 8px;
98
+ right: 8px;
99
+ cursor: nwse-resize;
100
+ }
101
+ .lynxkite-node {
102
+ .param {
103
+ padding: 4px 8px 4px 8px;
104
+ display: block;
105
+ }
106
+ .param-name {
107
+ display: block;
108
+ font-size: 10px;
109
+ letter-spacing: 0.05em;
110
+ margin-left: 10px;
111
+ width: fit-content;
112
+ padding: 2px 8px;
113
+ border-radius: 4px 4px 0 0;
114
+ ;
115
+ }
116
+ .collapsed-param {
117
+ min-height: 20px;
118
+ line-height: 10px;
119
+ }
120
+ }
web/app/layout.tsx CHANGED
@@ -1,21 +1,9 @@
1
  import type { Metadata } from "next";
2
- import localFont from "next/font/local";
3
  import "./globals.css";
4
 
5
- const geistSans = localFont({
6
- src: "./fonts/GeistVF.woff",
7
- variable: "--font-geist-sans",
8
- weight: "100 900",
9
- });
10
- const geistMono = localFont({
11
- src: "./fonts/GeistMonoVF.woff",
12
- variable: "--font-geist-mono",
13
- weight: "100 900",
14
- });
15
-
16
  export const metadata: Metadata = {
17
- title: "Create Next App",
18
- description: "Generated by create next app",
19
  };
20
 
21
  export default function RootLayout({
@@ -26,7 +14,7 @@ export default function RootLayout({
26
  return (
27
  <html lang="en">
28
  <body
29
- className={`${geistSans.variable} ${geistMono.variable} antialiased`}
30
  >
31
  {children}
32
  </body>
 
1
  import type { Metadata } from "next";
 
2
  import "./globals.css";
3
 
 
 
 
 
 
 
 
 
 
 
 
4
  export const metadata: Metadata = {
5
+ title: "LynxKite MM",
6
+ description: "From Lynx Analytics",
7
  };
8
 
9
  export default function RootLayout({
 
14
  return (
15
  <html lang="en">
16
  <body
17
+ className={`antialiased`}
18
  >
19
  {children}
20
  </body>
web/app/workspace/EnvironmentSelector.tsx CHANGED
@@ -1,12 +1,15 @@
1
  export default function EnvironmentSelector(props: { options: string[], value: string, onChange: (val: string) => void }) {
2
  return (
3
- <select className="form-select form-select-sm"
4
- value={props.value}
5
- onChange={(evt) => props.onChange(evt.currentTarget.value)}
6
- >
7
- {props.options.map(option =>
8
- <option key={option} value={option}>{option}</option>
9
- )}
10
- </select>
 
 
 
11
  );
12
  }
 
1
  export default function EnvironmentSelector(props: { options: string[], value: string, onChange: (val: string) => void }) {
2
  return (
3
+ <>
4
+ <select className="select w-full max-w-xs"
5
+ name="workspace-env"
6
+ value={props.value}
7
+ onChange={(evt) => props.onChange(evt.currentTarget.value)}
8
+ >
9
+ {props.options.map(option =>
10
+ <option key={option} value={option}>{option}</option>
11
+ )}
12
+ </select>
13
+ </>
14
  );
15
  }
web/app/workspace/page.tsx CHANGED
@@ -1,224 +1,7 @@
1
  'use client';
2
- import useSWR from 'swr';
3
  import { useMemo } from "react";
4
- import {
5
- ReactFlow,
6
- useNodesState,
7
- useEdgesState,
8
- Controls,
9
- MiniMap,
10
- MarkerType,
11
- useReactFlow,
12
- type XYPosition,
13
- type Node,
14
- type Edge,
15
- type Connection,
16
- type NodeTypes,
17
- } from '@xyflow/React';
18
- // @ts-ignore
19
- import ArrowBack from '~icons/tabler/arrow-back.jsx';
20
- // @ts-ignore
21
- import Backspace from '~icons/tabler/backspace.jsx';
22
- // @ts-ignore
23
- import Atom from '~icons/tabler/atom.jsx';
24
- // import NodeWithParams from './NodeWithParams';
25
- // import NodeWithVisualization from './NodeWithVisualization';
26
- // import NodeWithImage from './NodeWithImage';
27
- // import NodeWithTableView from './NodeWithTableView';
28
- // import NodeWithSubFlow from './NodeWithSubFlow';
29
- // import NodeWithArea from './NodeWithArea';
30
- // import NodeSearch from './NodeSearch';
31
- import EnvironmentSelector from './EnvironmentSelector';
32
- import '@xyflow/react/dist/style.css';
33
 
34
- export default function Home() {
35
- const [nodes, setNodes, onNodesChange] = useNodesState([]);
36
- const [edges, setEdges, onEdgesChange] = useEdgesState([]);
37
-
38
- let path = '';
39
-
40
- // const { screenToFlowPosition } = useReactFlow();
41
- // const queryClient = useQueryClient();
42
- // const backendWorkspace = useQuery(['workspace', path], async () => {
43
- // const res = await fetch(`/api/load?path=${path}`);
44
- // return res.json();
45
- // }, { staleTime: 10000, retry: false });
46
- // const mutation = useMutation(async (update) => {
47
- // const res = await fetch('/api/save', {
48
- // method: 'POST',
49
- // headers: {
50
- // 'Content-Type': 'application/json',
51
- // },
52
- // body: JSON.stringify(update),
53
- // });
54
- // return await res.json();
55
- // }, {
56
- // onSuccess: data => queryClient.setQueryData(['workspace', path], data),
57
- // });
58
-
59
- // const nodeTypes: NodeTypes = {
60
- // basic: NodeWithParams,
61
- // visualization: NodeWithVisualization,
62
- // image: NodeWithImage,
63
- // table_view: NodeWithTableView,
64
- // sub_flow: NodeWithSubFlow,
65
- // area: NodeWithArea,
66
- // };
67
-
68
- // const nodes = writable<Node[]>([]);
69
- // const edges = writable<Edge[]>([]);
70
- // let doNotSave = true;
71
- // $: if ($backendWorkspace.isSuccess) {
72
- // doNotSave = true; // Change is coming from the backend.
73
- // nodes.set(JSON.parse(JSON.stringify($backendWorkspace.data?.nodes || [])));
74
- // edges.set(JSON.parse(JSON.stringify($backendWorkspace.data?.edges || [])));
75
- // doNotSave = false;
76
- // }
77
-
78
- // function closeNodeSearch() {
79
- // nodeSearchSettings = undefined;
80
- // }
81
- // function toggleNodeSearch({ detail: { event } }) {
82
- // if (nodeSearchSettings) {
83
- // closeNodeSearch();
84
- // return;
85
- // }
86
- // event.preventDefault();
87
- // nodeSearchSettings = {
88
- // pos: { x: event.clientX, y: event.clientY },
89
- // boxes: $catalog.data[$backendWorkspace.data?.env],
90
- // };
91
- // }
92
- // function addNode(e) {
93
- // const meta = { ...e.detail };
94
- // nodes.update((n) => {
95
- // const node = {
96
- // type: meta.type,
97
- // data: {
98
- // meta: meta,
99
- // title: meta.name,
100
- // params: Object.fromEntries(
101
- // Object.values(meta.params).map((p) => [p.name, p.default])),
102
- // },
103
- // };
104
- // node.position = screenToFlowPosition({ x: nodeSearchSettings.pos.x, y: nodeSearchSettings.pos.y });
105
- // const title = node.data.title;
106
- // let i = 1;
107
- // node.id = `${title} ${i}`;
108
- // while (n.find((x) => x.id === node.id)) {
109
- // i += 1;
110
- // node.id = `${title} ${i}`;
111
- // }
112
- // node.parentId = nodeSearchSettings.parentId;
113
- // if (node.parentId) {
114
- // node.extent = 'parent';
115
- // const parent = n.find((x) => x.id === node.parentId);
116
- // node.position = { x: node.position.x - parent.position.x, y: node.position.y - parent.position.y };
117
- // }
118
- // return [...n, node]
119
- // });
120
- // closeNodeSearch();
121
- // }
122
- const fetcher = (resource: string, init?: RequestInit) => fetch(resource, init).then(res => res.json());
123
- const catalog = useSWR('/api/catalog', fetcher);
124
-
125
- // let nodeSearchSettings: {
126
- // pos: XYPosition,
127
- // boxes: any[],
128
- // parentId: string,
129
- // };
130
-
131
- // const graph = derived([nodes, edges], ([nodes, edges]) => ({ nodes, edges }));
132
- // // Like JSON.stringify, but with keys sorted.
133
- // function orderedJSON(obj: any) {
134
- // const allKeys = new Set();
135
- // JSON.stringify(obj, (key, value) => (allKeys.add(key), value));
136
- // return JSON.stringify(obj, Array.from(allKeys).sort());
137
- // }
138
- // graph.subscribe(async (g) => {
139
- // if (doNotSave) return;
140
- // const dragging = g.nodes.find((n) => n.dragging);
141
- // if (dragging) return;
142
- // const resizing = g.nodes.find((n) => n.data?.beingResized);
143
- // if (resizing) return;
144
- // scheduleSave(g);
145
- // });
146
- // let saveTimeout;
147
- // function scheduleSave(g) {
148
- // // A slight delay, so we don't send a million requests when a node is resized, for example.
149
- // clearTimeout(saveTimeout);
150
- // saveTimeout = setTimeout(() => save(g), 500);
151
- // }
152
- // function save(g) {
153
- // g = JSON.parse(JSON.stringify(g));
154
- // for (const node of g.nodes) {
155
- // delete node.measured;
156
- // delete node.selected;
157
- // delete node.dragging;
158
- // delete node.beingResized;
159
- // }
160
- // for (const node of g.edges) {
161
- // delete node.markerEnd;
162
- // delete node.selected;
163
- // }
164
- // g.env = $backendWorkspace.data?.env;
165
- // const ws = orderedJSON(g);
166
- // const bd = orderedJSON($backendWorkspace.data);
167
- // if (ws === bd) return;
168
- // console.log('changed', JSON.stringify(diff(g, $backendWorkspace.data), null, 2));
169
- // $mutation.mutate({ path, ws: g });
170
- // }
171
- // function nodeClick(e) {
172
- // const node = e.detail.node;
173
- // const meta = node.data.meta;
174
- // if (!meta) return;
175
- // const sub_nodes = meta.sub_nodes;
176
- // if (!sub_nodes) return;
177
- // const event = e.detail.event;
178
- // if (event.target.classList.contains('title')) return;
179
- // nodeSearchSettings = {
180
- // pos: { x: event.clientX, y: event.clientY },
181
- // boxes: sub_nodes,
182
- // parentId: node.id,
183
- // };
184
- // }
185
- // $: parentDir = path.split('/').slice(0, -1).join('/');
186
-
187
- const nodeTypes = useMemo(() => ({}), []);
188
- return (
189
-
190
- <div className="page">
191
- <div className="top-bar">
192
- <div className="ws-name">
193
- <a href=""><img src="/favicon.ico" /></a>
194
- {path}
195
- </div>
196
- <div className="tools">
197
- <EnvironmentSelector
198
- options={Object.keys(catalog.data || {})}
199
- value={'asd'}
200
- onChange={(env) => 1}
201
- />
202
- <a href=""><Atom /></a>
203
- <a href=""><Backspace /></a>
204
- <a href="#dir?path={parentDir}"><ArrowBack /></a>
205
- </div>
206
- </div>
207
- <div style={{ height: "100%", width: '100vw' }}>
208
- <ReactFlow nodes={nodes} edges={edges} nodeTypes={nodeTypes} fitView
209
- proOptions={{ hideAttribution: true }}
210
- maxZoom={3}
211
- minZoom={0.3}
212
- defaultEdgeOptions={{ markerEnd: { type: MarkerType.Arrow } }}
213
- >
214
- <Controls />
215
- <MiniMap />
216
- {/* {#if nodeSearchSettings}
217
- <NodeSearch pos={nodeSearchSettings.pos} boxes={nodeSearchSettings.boxes} on:cancel={closeNodeSearch} on:add={addNode} />
218
- {/if} */}
219
- </ReactFlow>
220
- </div>
221
- </div>
222
-
223
- );
224
- }
 
1
  'use client';
 
2
  import { useMemo } from "react";
3
+ import dynamic from 'next/dynamic';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
+ export default dynamic(() => import('./Workspace'), {
6
+ ssr: false,
7
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/package-lock.json CHANGED
@@ -6932,6 +6932,111 @@
6932
  "version": "2.3.0",
6933
  "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
6934
  "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6935
  }
6936
  }
6937
  }
 
6932
  "version": "2.3.0",
6933
  "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
6934
  "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
6935
+ },
6936
+ "node_modules/@next/swc-darwin-x64": {
6937
+ "version": "15.0.3",
6938
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.0.3.tgz",
6939
+ "integrity": "sha512-Zxl/TwyXVZPCFSf0u2BNj5sE0F2uR6iSKxWpq4Wlk/Sv9Ob6YCKByQTkV2y6BCic+fkabp9190hyrDdPA/dNrw==",
6940
+ "cpu": [
6941
+ "x64"
6942
+ ],
6943
+ "optional": true,
6944
+ "os": [
6945
+ "darwin"
6946
+ ],
6947
+ "engines": {
6948
+ "node": ">= 10"
6949
+ }
6950
+ },
6951
+ "node_modules/@next/swc-linux-arm64-gnu": {
6952
+ "version": "15.0.3",
6953
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.0.3.tgz",
6954
+ "integrity": "sha512-T5+gg2EwpsY3OoaLxUIofmMb7ohAUlcNZW0fPQ6YAutaWJaxt1Z1h+8zdl4FRIOr5ABAAhXtBcpkZNwUcKI2fw==",
6955
+ "cpu": [
6956
+ "arm64"
6957
+ ],
6958
+ "optional": true,
6959
+ "os": [
6960
+ "linux"
6961
+ ],
6962
+ "engines": {
6963
+ "node": ">= 10"
6964
+ }
6965
+ },
6966
+ "node_modules/@next/swc-linux-arm64-musl": {
6967
+ "version": "15.0.3",
6968
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.0.3.tgz",
6969
+ "integrity": "sha512-WkAk6R60mwDjH4lG/JBpb2xHl2/0Vj0ZRu1TIzWuOYfQ9tt9NFsIinI1Epma77JVgy81F32X/AeD+B2cBu/YQA==",
6970
+ "cpu": [
6971
+ "arm64"
6972
+ ],
6973
+ "optional": true,
6974
+ "os": [
6975
+ "linux"
6976
+ ],
6977
+ "engines": {
6978
+ "node": ">= 10"
6979
+ }
6980
+ },
6981
+ "node_modules/@next/swc-linux-x64-gnu": {
6982
+ "version": "15.0.3",
6983
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.0.3.tgz",
6984
+ "integrity": "sha512-gWL/Cta1aPVqIGgDb6nxkqy06DkwJ9gAnKORdHWX1QBbSZZB+biFYPFti8aKIQL7otCE1pjyPaXpFzGeG2OS2w==",
6985
+ "cpu": [
6986
+ "x64"
6987
+ ],
6988
+ "optional": true,
6989
+ "os": [
6990
+ "linux"
6991
+ ],
6992
+ "engines": {
6993
+ "node": ">= 10"
6994
+ }
6995
+ },
6996
+ "node_modules/@next/swc-linux-x64-musl": {
6997
+ "version": "15.0.3",
6998
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.0.3.tgz",
6999
+ "integrity": "sha512-QQEMwFd8r7C0GxQS62Zcdy6GKx999I/rTO2ubdXEe+MlZk9ZiinsrjwoiBL5/57tfyjikgh6GOU2WRQVUej3UA==",
7000
+ "cpu": [
7001
+ "x64"
7002
+ ],
7003
+ "optional": true,
7004
+ "os": [
7005
+ "linux"
7006
+ ],
7007
+ "engines": {
7008
+ "node": ">= 10"
7009
+ }
7010
+ },
7011
+ "node_modules/@next/swc-win32-arm64-msvc": {
7012
+ "version": "15.0.3",
7013
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.0.3.tgz",
7014
+ "integrity": "sha512-9TEp47AAd/ms9fPNgtgnT7F3M1Hf7koIYYWCMQ9neOwjbVWJsHZxrFbI3iEDJ8rf1TDGpmHbKxXf2IFpAvheIQ==",
7015
+ "cpu": [
7016
+ "arm64"
7017
+ ],
7018
+ "optional": true,
7019
+ "os": [
7020
+ "win32"
7021
+ ],
7022
+ "engines": {
7023
+ "node": ">= 10"
7024
+ }
7025
+ },
7026
+ "node_modules/@next/swc-win32-x64-msvc": {
7027
+ "version": "15.0.3",
7028
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.0.3.tgz",
7029
+ "integrity": "sha512-VNAz+HN4OGgvZs6MOoVfnn41kBzT+M+tB+OK4cww6DNyWS6wKaDpaAm/qLeOUbnMh0oVx1+mg0uoYARF69dJyA==",
7030
+ "cpu": [
7031
+ "x64"
7032
+ ],
7033
+ "optional": true,
7034
+ "os": [
7035
+ "win32"
7036
+ ],
7037
+ "engines": {
7038
+ "node": ">= 10"
7039
+ }
7040
  }
7041
  }
7042
  }
web/tailwind.config.js CHANGED
@@ -1,8 +1,22 @@
1
  /** @type {import('tailwindcss').Config} */
2
  module.exports = {
3
- content: [],
 
4
  theme: {
5
  extend: {},
6
  },
7
  plugins: [require('daisyui')],
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  };
 
1
  /** @type {import('tailwindcss').Config} */
2
  module.exports = {
3
+ darkMode: 'selector',
4
+ content: ['./app/**/*.{js,ts,jsx,tsx,mdx}'],
5
  theme: {
6
  extend: {},
7
  },
8
  plugins: [require('daisyui')],
9
+ daisyui: {
10
+ themes: [
11
+ {
12
+ lynxkite: {
13
+ primary: 'oklch(75% 0.2 55)',
14
+ secondary: 'oklch(75% 0.13 230)',
15
+ accent: 'oklch(55% 0.25 320)',
16
+ neutral: 'oklch(35% 0.1 240)',
17
+ 'base-100': '#ffffff',
18
+ },
19
+ },
20
+ ],
21
+ },
22
  };
web/tsconfig.json CHANGED
@@ -13,6 +13,7 @@
13
  "isolatedModules": true,
14
  "jsx": "preserve",
15
  "incremental": true,
 
16
  "plugins": [
17
  {
18
  "name": "next"
 
13
  "isolatedModules": true,
14
  "jsx": "preserve",
15
  "incremental": true,
16
+ "noImplicitAny": false,
17
  "plugins": [
18
  {
19
  "name": "next"