// The LynxKite workspace editor. import { useParams } from "react-router"; import useSWR from 'swr'; import { useEffect, useMemo, useCallback, useState } from "react"; import favicon from '../assets/favicon.ico'; import { ReactFlow, Controls, MarkerType, ReactFlowProvider, applyEdgeChanges, applyNodeChanges, useUpdateNodeInternals, type XYPosition, type Node, type Edge, type Connection, type NodeTypes, } from '@xyflow/react'; // @ts-ignore import ArrowBack from '~icons/tabler/arrow-back.jsx'; // @ts-ignore import Backspace from '~icons/tabler/backspace.jsx'; // @ts-ignore import Atom from '~icons/tabler/atom.jsx'; import { syncedStore, getYjsDoc } from "@syncedstore/core"; import { WebsocketProvider } from "y-websocket"; import NodeWithParams from './nodes/NodeWithParams'; // import NodeWithVisualization from './NodeWithVisualization'; // import NodeWithImage from './NodeWithImage'; // import NodeWithTableView from './NodeWithTableView'; // import NodeWithSubFlow from './NodeWithSubFlow'; // import NodeWithArea from './NodeWithArea'; // import NodeSearch from './NodeSearch'; import EnvironmentSelector from './EnvironmentSelector'; import { LynxKiteState } from './LynxKiteState'; import '@xyflow/react/dist/style.css'; import { Workspace } from "../apiTypes.ts"; export default function (props: any) { return ( ); } function LynxKiteFlow() { const updateNodeInternals = useUpdateNodeInternals() const [nodes, setNodes] = useState([] as Node[]); const [edges, setEdges] = useState([] as Edge[]); const { path } = useParams(); const [state, setState] = useState({ workspace: {} as Workspace }); useEffect(() => { const state = syncedStore({ workspace: {} as Workspace }); setState(state); const doc = getYjsDoc(state); const wsProvider = new WebsocketProvider("ws://localhost:8000/ws/crdt", path!, doc); const onChange = (update: any, origin: any, doc: any, tr: any) => { if (origin === wsProvider) { // An update from the CRDT. Apply it to the local state. // This is only necessary because ReactFlow keeps secret internal copies of our stuff. if (!state.workspace) return; if (!state.workspace.nodes) return; if (!state.workspace.edges) return; setNodes([...state.workspace.nodes] as Node[]); setEdges([...state.workspace.edges] as Edge[]); for (const node of state.workspace.nodes) { // Make sure the internal copies are updated. updateNodeInternals(node.id); } } }; doc.on('update', onChange); return () => { doc.destroy(); wsProvider.destroy(); } }, [path]); const onNodesChange = (changes: any[]) => { // An update from the UI. Apply it to the local state... setNodes((nds) => applyNodeChanges(changes, nds)); // ...and to the CRDT state. (Which could be the same, except for ReactFlow's internal copies.) const wnodes = state.workspace?.nodes; if (!wnodes) return; for (const ch of changes) { const nodeIndex = wnodes.findIndex((n) => n.id === ch.id); if (nodeIndex === -1) continue; const node = wnodes[nodeIndex]; if (!node) continue; // Position events sometimes come with NaN values. Ignore them. if (ch.type === 'position' && !isNaN(ch.position.x) && !isNaN(ch.position.y)) { getYjsDoc(state).transact(() => { Object.assign(node.position, ch.position); }); } else if (ch.type === 'select') { } else if (ch.type === 'dimensions') { getYjsDoc(state).transact(() => Object.assign(node, ch.dimensions)); } else if (ch.type === 'replace') { // Ideally we would only update the parameter that changed. But ReactFlow does not give us that detail. const u = { collapsed: ch.item.data.collapsed, // The "..." expansion on a Y.map returns an empty object. Copying with fromEntries/entries instead. params: { ...Object.fromEntries(Object.entries(ch.item.data.params)) }, __execution_delay: ch.item.data.__execution_delay, }; getYjsDoc(state).transact(() => Object.assign(node.data, u)); } else { console.log('Unknown node change', ch); } } }; const onEdgesChange = (changes: any[]) => { setEdges((eds) => applyEdgeChanges(changes, eds)); }; const fetcher = (resource: string, init?: RequestInit) => fetch(resource, init).then(res => res.json()); const catalog = useSWR('/api/catalog', fetcher); const nodeTypes = useMemo(() => ({ basic: NodeWithParams, table_view: NodeWithParams, }), []); const parentDir = path!.split('/').slice(0, -1).join('/'); return (
{path}
{ state.workspace.env = env; }} />
{/* {#if nodeSearchSettings} {/if} */}
); }