Spaces:
Running
Running
Automatically find location for "/".
Browse files
lynxkite-app/uv.lock
CHANGED
The diff for this file is too large to render.
See raw diff
|
|
lynxkite-app/web/src/apiTypes.ts
CHANGED
@@ -34,6 +34,8 @@ export interface WorkspaceNode {
|
|
34 |
type: string;
|
35 |
data: WorkspaceNodeData;
|
36 |
position: Position;
|
|
|
|
|
37 |
[k: string]: unknown;
|
38 |
}
|
39 |
export interface WorkspaceNodeData {
|
|
|
34 |
type: string;
|
35 |
data: WorkspaceNodeData;
|
36 |
position: Position;
|
37 |
+
width: number;
|
38 |
+
height: number;
|
39 |
[k: string]: unknown;
|
40 |
}
|
41 |
export interface WorkspaceNodeData {
|
lynxkite-app/web/src/workspace/Workspace.tsx
CHANGED
@@ -16,7 +16,7 @@ import {
|
|
16 |
useUpdateNodeInternals,
|
17 |
} from "@xyflow/react";
|
18 |
import axios from "axios";
|
19 |
-
import { type MouseEvent, useCallback, useEffect, useMemo, useState } from "react";
|
20 |
import { Link } from "react-router";
|
21 |
import useSWR, { type Fetcher } from "swr";
|
22 |
import { WebsocketProvider } from "y-websocket";
|
@@ -61,6 +61,7 @@ export default function Workspace(props: any) {
|
|
61 |
function LynxKiteFlow() {
|
62 |
const updateNodeInternals = useUpdateNodeInternals();
|
63 |
const reactFlow = useReactFlow();
|
|
|
64 |
const [nodes, setNodes] = useState([] as Node[]);
|
65 |
const [edges, setEdges] = useState([] as Edge[]);
|
66 |
const path = usePath().replace(/^[/]edit[/]/, "");
|
@@ -210,7 +211,7 @@ function LynxKiteFlow() {
|
|
210 |
if (event.key === "/") {
|
211 |
event.preventDefault();
|
212 |
setNodeSearchSettings({
|
213 |
-
pos:
|
214 |
boxes: catalog.data![state.workspace.env!],
|
215 |
});
|
216 |
} else if (event.key === "r") {
|
@@ -225,6 +226,39 @@ function LynxKiteFlow() {
|
|
225 |
};
|
226 |
}, [catalog.data, nodeSearchSettings, state.workspace.env]);
|
227 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
228 |
function isTypingInFormElement() {
|
229 |
const activeElement = document.activeElement;
|
230 |
return (
|
@@ -504,7 +538,12 @@ function LynxKiteFlow() {
|
|
504 |
</Tooltip>
|
505 |
</div>
|
506 |
</div>
|
507 |
-
<div
|
|
|
|
|
|
|
|
|
|
|
508 |
<LynxKiteState.Provider value={state}>
|
509 |
<ReactFlow
|
510 |
nodes={nodes}
|
@@ -537,6 +576,7 @@ function LynxKiteFlow() {
|
|
537 |
stroke: "black",
|
538 |
},
|
539 |
}}
|
|
|
540 |
>
|
541 |
<Controls />
|
542 |
{nodeSearchSettings && (
|
|
|
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";
|
|
|
61 |
function LynxKiteFlow() {
|
62 |
const updateNodeInternals = useUpdateNodeInternals();
|
63 |
const reactFlow = useReactFlow();
|
64 |
+
const reactFlowContainer = useRef<HTMLDivElement>(null);
|
65 |
const [nodes, setNodes] = useState([] as Node[]);
|
66 |
const [edges, setEdges] = useState([] as Edge[]);
|
67 |
const path = usePath().replace(/^[/]edit[/]/, "");
|
|
|
211 |
if (event.key === "/") {
|
212 |
event.preventDefault();
|
213 |
setNodeSearchSettings({
|
214 |
+
pos: getBestPosition(),
|
215 |
boxes: catalog.data![state.workspace.env!],
|
216 |
});
|
217 |
} else if (event.key === "r") {
|
|
|
226 |
};
|
227 |
}, [catalog.data, nodeSearchSettings, state.workspace.env]);
|
228 |
|
229 |
+
function getBestPosition() {
|
230 |
+
const W = reactFlowContainer.current!.clientWidth;
|
231 |
+
const H = reactFlowContainer.current!.clientHeight;
|
232 |
+
const w = 200;
|
233 |
+
const h = 200;
|
234 |
+
const SPEED = 20;
|
235 |
+
const GAP = 50;
|
236 |
+
const pos = { x: 100, y: 100 };
|
237 |
+
while (pos.y < H) {
|
238 |
+
// Find a position that is not occupied by a node.
|
239 |
+
const fpos = reactFlow.screenToFlowPosition(pos);
|
240 |
+
const occupied = state.workspace.nodes!.some((n) => {
|
241 |
+
const np = n.position;
|
242 |
+
return (
|
243 |
+
np.x < fpos.x + w + GAP &&
|
244 |
+
np.x + n.width + GAP > fpos.x &&
|
245 |
+
np.y < fpos.y + h + GAP &&
|
246 |
+
np.y + n.height + GAP > fpos.y
|
247 |
+
);
|
248 |
+
});
|
249 |
+
if (!occupied) {
|
250 |
+
return pos;
|
251 |
+
}
|
252 |
+
// Move the position to the right and down until we find a free spot.
|
253 |
+
pos.x += SPEED;
|
254 |
+
if (pos.x + w > W) {
|
255 |
+
pos.x = 100;
|
256 |
+
pos.y += SPEED;
|
257 |
+
}
|
258 |
+
}
|
259 |
+
return { x: 100, y: 100 };
|
260 |
+
}
|
261 |
+
|
262 |
function isTypingInFormElement() {
|
263 |
const activeElement = document.activeElement;
|
264 |
return (
|
|
|
538 |
</Tooltip>
|
539 |
</div>
|
540 |
</div>
|
541 |
+
<div
|
542 |
+
style={{ height: "100%", width: "100vw" }}
|
543 |
+
onDragOver={onDragOver}
|
544 |
+
onDrop={onDrop}
|
545 |
+
ref={reactFlowContainer}
|
546 |
+
>
|
547 |
<LynxKiteState.Provider value={state}>
|
548 |
<ReactFlow
|
549 |
nodes={nodes}
|
|
|
576 |
stroke: "black",
|
577 |
},
|
578 |
}}
|
579 |
+
fitViewOptions={{ maxZoom: 1 }}
|
580 |
>
|
581 |
<Controls />
|
582 |
{nodeSearchSettings && (
|
lynxkite-app/web/src/workspace/nodes/NodeParameter.tsx
CHANGED
@@ -237,7 +237,7 @@ export default function NodeParameter({ name, value, meta, data, setParam }: Nod
|
|
237 |
<textarea
|
238 |
className="textarea textarea-bordered w-full"
|
239 |
rows={6}
|
240 |
-
value={value}
|
241 |
onChange={(evt) => onChange(evt.currentTarget.value, { delay: 2 })}
|
242 |
onBlur={(evt) => onChange(evt.currentTarget.value, { delay: 0 })}
|
243 |
/>
|
|
|
237 |
<textarea
|
238 |
className="textarea textarea-bordered w-full"
|
239 |
rows={6}
|
240 |
+
value={value || ""}
|
241 |
onChange={(evt) => onChange(evt.currentTarget.value, { delay: 2 })}
|
242 |
onBlur={(evt) => onChange(evt.currentTarget.value, { delay: 0 })}
|
243 |
/>
|
lynxkite-app/web/tests/lynxkite.ts
CHANGED
@@ -53,14 +53,8 @@ export class Workspace {
|
|
53 |
}
|
54 |
|
55 |
async addBox(boxName) {
|
56 |
-
//TODO: Support passing box parameters
|
57 |
const allBoxes = await this.getBoxes().all();
|
58 |
-
if (allBoxes) {
|
59 |
-
// Avoid overlapping with existing nodes
|
60 |
-
const numNodes = allBoxes.length || 1;
|
61 |
-
await this.page.mouse.wheel(0, numNodes * 400);
|
62 |
-
}
|
63 |
-
|
64 |
await this.page.locator(".ws-name").click();
|
65 |
await this.page.keyboard.press("/");
|
66 |
await this.page.locator(".node-search").getByText(boxName, { exact: true }).click();
|
|
|
53 |
}
|
54 |
|
55 |
async addBox(boxName) {
|
56 |
+
// TODO: Support passing box parameters.
|
57 |
const allBoxes = await this.getBoxes().all();
|
|
|
|
|
|
|
|
|
|
|
|
|
58 |
await this.page.locator(".ws-name").click();
|
59 |
await this.page.keyboard.press("/");
|
60 |
await this.page.locator(".node-search").getByText(boxName, { exact: true }).click();
|
lynxkite-core/src/lynxkite/core/workspace.py
CHANGED
@@ -40,12 +40,13 @@ class WorkspaceNodeData(BaseConfig):
|
|
40 |
|
41 |
|
42 |
class WorkspaceNode(BaseConfig):
|
43 |
-
#
|
44 |
-
# modyfing them will break the frontend.
|
45 |
id: str
|
46 |
type: str
|
47 |
data: WorkspaceNodeData
|
48 |
position: Position
|
|
|
|
|
49 |
_crdt: pycrdt.Map
|
50 |
|
51 |
def publish_started(self):
|
|
|
40 |
|
41 |
|
42 |
class WorkspaceNode(BaseConfig):
|
43 |
+
# Most of these fields are shared with ReactFlow.
|
|
|
44 |
id: str
|
45 |
type: str
|
46 |
data: WorkspaceNodeData
|
47 |
position: Position
|
48 |
+
width: float
|
49 |
+
height: float
|
50 |
_crdt: pycrdt.Map
|
51 |
|
52 |
def publish_started(self):
|